Private API
This page documents the internal functions and types of SymbolicAWEModels.jl. These are not part of the public API and may change without notice. They are listed here for developers and for those interested in the model's internal workings.
Core types and constructors
SymbolicAWEModels.SerializedModel — Type
@with_kw mutable struct SerializedModel{...}A type-stable container for the compiled and serialized components of a SymbolicAWEModel: the products of the ModelingToolkit.jl compilation, grouped into nested attribute structs (ProbWithAttributes, etc.).
set_hash::Vector{UInt8}sys_struct_hash::Vector{UInt8}full_sys::Union{Nothing, ModelingToolkitBase.System}: Unsimplified system of the mtk model Default: nothingdefaults::AbstractVector: Default: Pair{Num, Any}[]inputs::Union{Symbolics.Arr, Vector{Symbolics.Num}}: Symbolic representation of the control inputs. Default: Num[]outputs::Union{Symbolics.Arr, Vector{Symbolics.Num}}: Outputs of the linearization and control function. Default: Num[]prob::Union{Nothing, SymbolicAWEModels.ProbWithAttributes}: Container for the ODE problem and its getters/setters. Default: nothinglin_prob::Union{Nothing, SymbolicAWEModels.LinProbWithAttributes}: Container for the linearization problem and its components. Default: nothingcontrol_functions::Union{Nothing, SymbolicAWEModels.ControlFuncWithAttributes}: Container for the control functions. Default: nothing
SymbolicAWEModels.SimFloat — Type
const SimFloat = Float64This type is used for all real variables, used in the Simulation. Possible alternatives: Float32, Double64, Dual Other types than Float64 or Float32 do require support of Julia types by the solver.
SymbolicAWEModels.KVec3 — Type
const KVec3 = MVector{3, SimFloat} const KVec2 = MVector{2, SimFloat}
Basic 3-dimensional vector, stack allocated, mutable.
SymbolicAWEModels.SVec3 — Type
const SVec3 = SVector{3, SimFloat}
Basic 3-dimensional vector, stack allocated, immutable.
SymbolicAWEModels.InplaceGetter — Type
InplaceGetter{F, B, G}A single zero-allocation getter that both reads and scatters all per-step component state. fn is an in-place MTK observed function fn(buf, u, p, t) over the concatenation of every component's output arrays; buf is a preallocated flat buffer reused each call; groups is a tuple of ScatterGroups that write the freshly-computed buffer straight into the SystemStructure fields. One spec drives both the buffer layout and the scatter.
SymbolicAWEModels.ScatterGroup — Type
ScatterGroup{Sel, Fns, Views}One component group of an InplaceGetter. selector(sys_struct) returns the group's component vector (e.g. sys_struct.points); copyfns is a tuple of (component, view) -> _ closures, one per output array, each copying that array's slice into the component's struct field; views is the matching tuple of zero-copy reshaped views into the getter's buffer.
SymbolicAWEModels.create_vsm_wing — Function
create_vsm_wing(set::Settings, vsm_set::VortexStepMethod.VSMSettings;
prn=true, sort_sections=true)Create a VortexStepMethod.Wing geometry object from the settings provided.
This function checks for a .obj file in the model directory. If present it uses VortexStepMethod.ObjWing(obj_path; …) to generate the aero geometry (airfoils are extracted from the mesh; no .dat is needed). The mesh is sliced at panel resolution — ObjWing defaults to one unrefined section per panel boundary (n_panels + 1) — independent of the wing's station count; the twist surfaces map onto the refined panels by distance later. The generated geometry.yaml is cached under <model_dir>/obj_geometry and reused on later runs. When no .obj is present it falls back to vsm_set's geometry_file. Aero only — mass properties are handled separately (see VSMWing and ObjAdapter).
SymbolicAWEModels.build_vsm_engine — Function
build_vsm_engine(set, vsm_set, dynamics_type; point_to_vsm_point=nothing,
wing_segments=nothing, aero_scale_chord=0.0, aero_z_offset=0.0)Build a VSMEngine: create the VortexStepMethod vsm_wing/vsm_aero/ vsm_solver and size the linearization state vectors. Aero-state sizes are placeholders for RIGID_DYNAMICS (using the mesh's n_unrefined_sections as the station-count proxy) and resized by SystemStructure once stations are resolved.
Keywords
point_to_vsm_point,wing_segments: VSM structural↔panel maps.aero_scale_chord,aero_z_offset: VSM force/panel adjustments.unsteady: the wing'sUnsteadyAerocorrections;nothingtakes the defaults, which are all off.
State management and model simplification
SymbolicAWEModels.copy! — Function
copy!(sys1::SystemStructure, sys2::SystemStructure)Copy the dynamic state (positions, velocities, …) from one SystemStructure to another, which may be of a different fidelity — e.g. from a multi-segment tether model to a single-segment one.
- Same structure: direct copy of all point states.
sys2a 1-segment-per-tether version ofsys1: the tether endpoints' positions and velocities are copied.- Wing, station, winch and pulley states are copied where applicable.
SymbolicAWEModels.reinit! — Function
reinit!(transforms::AbstractVector{Transform}, sys_struct::SystemStructure;
update_vel=true)Apply transforms to all components in a SystemStructure.
Expects pos_w to already be set (via copy_cad_to_world! and optionally apply_tether_init_stretched_lens! from reinit!(sys_struct, set; ...)). Applies: translate (from pos_w) → azimuth/elevation → heading.
reinit!(sys_struct::SystemStructure, set::Settings; kwargs...)Reset the component states (winch lengths, station twists, pulley positions, …) to the initial values defined in set, before a new simulation run.
Pulley lengths are initialized proportionally based on current segment lengths: pulley.len = segment1.len / (segment1.len+segment2.len) * pulley.sum_len
Keyword Arguments
ignore_l0::Bool=false: If true, recalculate segment rest lengths from current positionsremake_vsm::Bool=false: If true, recreate VSM wing, aerodynamics, and solver from settings. This is useful after modifyingaero_geometry.yamlor other VSM-related configuration files. For PARTICLEDYNAMICS wings, also rebuilds the `pointtovsmpoint` mapping.apply_transforms::Bool=true: If false, skip applying spatial transforms (translate, rotate, heading) during reinitialization.apply_tether_lengths::Bool=true: If false, skip scaling point positions to matchtether.init_stretched_len.prn::Bool=true: If true, print info messages (e.g. when several root tethers are placed to their mean stretched length).
reinit!(sam, integrator; solver=integrator.alg, kwargs...) -> (ODEIntegrator, Bool)Reset integrator, which is sam's own, from the current SystemStructure. solver defaults to the one integrator was built with, so the reset stays on the compiled right-hand side instead of forcing a second compilation at another Jacobian's element type. kwargs are those of reinit!(sam, prob, solver; …).
reinit!(sam, prob, solver; kwargs...) -> (ODEIntegrator, Bool)Reset the ODE integrator from new initial conditions without rebuilding the symbolic model. See init! for adaptive, reset_integrator, lin_vsm, and vsm_min_wind. prn toggles the per-phase timing logs (integrator build / first-call JIT, initial aero solve).
SymbolicAWEModels.reposition! — Function
reposition!(transforms::AbstractVector{Transform},
sys_struct::SystemStructure; update_vel=false)Update the system's spatial orientation based on its current position, preserving velocities. update_vel instead overwrites them with the velocity of the rigid rotation each transform's elevation_vel and azimuth_vel describe.
Unlike reinit!, uses current world positions (pos_w) as the starting point (no reset from CAD coordinates, no tether length scaling). Heading uses the tangential sphere frame, consistent with reinit!.
SymbolicAWEModels.update_sys_struct! — Function
update_sys_struct!(s::SymbolicAWEModel, sys_struct::SystemStructure, integ=s.integrator)Update the high-level SystemStructure from the integrator state vector, through the generated getter functions.
SymbolicAWEModels.get_set_hash — Function
get_set_hash(set::Settings; fields)Calculates a SHA1 hash for structural fields in the Settings object. This is used to check if a cached compiled model is still valid.
Structural Fields (affect symbolic equations):
:segments: Number of tether segments (affects state vector size):model: Kite model name (affects geometry):foil_file: Airfoil data file (affects VSM setup):physical_model: Model type (ram, simpleram, 4attach_ram):winch_model: Winch dynamics model (affects winch equations)
Anything read back from a struct at sync time stays out, so one build serves a sweep over it. That is every numeric setting the equations use — :g_earth, :wind_vec, :cd_tether, :v_wind, the profile law, initial conditions — since each enters as a flat parameter sync_params! refreshes from sys_struct.set, not as a literal.
Runtime Fields (don't affect compilation, excluded from hash):
:profile_law: Wind profile law (evaluated at runtime via symbolic function):elevation: Initial conditions- Other runtime parameters
SymbolicAWEModels.get_sys_struct_hash — Function
get_sys_struct_hash(sys_struct::SystemStructure)Calculates a SHA1 hash for the topology and structure of a SystemStructure. This is used to check if a cached compiled model is still valid.
Includes all structural properties that affect the symbolic equations:
- Point connectivity and types (STATIC, DYNAMIC, BODYSTATIC), including the beam joint a BODYSTATIC point anchors to (selects which bodies enter its equations)
- Segment connectivity
- Station structure and types (STATIC, DYNAMIC)
- Pulley constraints and types
- Tether topology
- Winch configuration
- Wing topology, connectivity, aerodynamic model type (RIGIDDYNAMICS vs PARTICLEDYNAMICS), and aero mode
- Transform hierarchy
- The wind mode, which decides whether the wind is a height profile or a per-point parameter. Only
PerPointWindenters the hash, so structures on the defaultProfileWindkeep the cached models they already have.
Excludes runtime-configurable properties like masses, lengths, stiffnesses.
Physics and geometry helpers
SymbolicAWEModels.WindFactor — Type
WindFactor(am, profile_law)Callable wind-shear factor, used as a callable flat parameter w(pos_z): the ratio of wind speed at height pos_z to the ground value, from atmospheric model am under profile_law (1.0 when profile_law == 0). ForwardDiff.Dual-safe in pos_z. Read live from sys_struct via WindFactorReader.
SymbolicAWEModels.WindFactorReader — Type
WindFactorReader()Serializable flat-param reader producing a WindFactor from the live sys_struct's atmospheric model and wind profile law.
SymbolicAWEModels.AbstractWindSource — Type
AbstractWindSourceBuild-time source of the wind vector at one component. source(height) returns the world-frame wind as a length-3 expression, either the profile law's wind at that height (ProfileWindSource) or a wind the caller prescribes (PrescribedWindSource). Which one a component gets is decided once, by the model's WindMode, so the equations carry no runtime branch.
SymbolicAWEModels.ProfileWindSource — Type
ProfileWindSource(factor, ground)The wind of a height profile law: the ground wind (ground_wind_vec) scaled by the callable wind_factor parameter at the caller's height.
SymbolicAWEModels.PrescribedWindSource — Type
PrescribedWindSource(wind)A wind expression that does not depend on height — under PerPointWind the component's own wind parameter, or an expression in the winds of the points it spans. The height it is asked for is ignored.
SymbolicAWEModels.wind_mode — Function
The WindMode of the structure a build-time params view reads.
SymbolicAWEModels.segment_wind_params — Function
segment_wind_params(params, idx, with_drag) -> (wind_source, minted)The wind source a segment kernel uses and the parameters it has to declare for it. Under PerPointWind a drag-carrying segment mints src_wind/dst_wind, which bind_segment_winds! points at its two endpoints' point.wind_vec — the kernel is instanced over :segments, so the endpoints cannot be reached through the registry's per-instance index remapping. The monolith reads its endpoints' wind_at_point directly and calls segment_wind_source itself.
SymbolicAWEModels.bind_segment_winds! — Function
bind_segment_winds!(slots, readers, system, sys_struct, segment_instances)Point each segment's src_wind/dst_wind parameters at its two endpoints' point.wind_vec, which is how a PerPointWind segment reads the wind of the points it spans: a segment kernel is instanced over :segments, so its endpoints cannot both be reached through the registry's per-instance index remapping. segment_wind_params mints these parameters, so nothing else binds them; a segment without tether drag has none.
SymbolicAWEModels.profile_wind_source — Function
profile_wind_source(params, ground)The height-profile wind source, registering the ground wind and the live wind_factor on params. The monolith passes its own wind_vec_gnd variable as ground so the ground wind keeps being written once for the whole system; nothing registers it here.
SymbolicAWEModels.point_wind_source — Function
point_wind_source(params, idx, ground=nothing)The wind source of point idx: its own wind_vec parameter under PerPointWind, else the height profile.
SymbolicAWEModels.segment_wind_source — Function
segment_wind_source(params, idx, src_wind, dst_wind, ground=nothing)The wind source of segment idx: the mean of the winds at its two endpoints under PerPointWind — the same averaging its drag already applies to their velocities — else the height profile at its midpoint. src_wind/dst_wind are the endpoint winds as each backend addresses them and are unused under ProfileWind.
SymbolicAWEModels.wing_wind_source — Function
wing_wind_source(params, idx, ground=nothing)The wind source of wing idx: its own wind_vec parameter under PerPointWind, else the height profile. This is the wind a rigid wing's aerodynamics fly in; the per-point winds reach the VSM panels through AeroInflowPoint instead.
SymbolicAWEModels.seed_per_point_wind! — Function
seed_per_point_wind!(sys_struct::SystemStructure)Give every point and every wing the ground wind set.wind_vec as its own wind, so a PerPointWind model that is never written to flies in the uniform wind the settings describe. Called from reinit!; from then on these winds belong to the caller, who writes them between steps.
SymbolicAWEModels.calc_angle_of_attack — Function
calc_angle_of_attack(va_wing_b)Calculate the angle of attack [rad] from the apparent wind vector va_wing_b in the body frame.
SymbolicAWEModels.calc_heading — Function
calc_heading(sys::SystemStructure)Calculate heading angles for all wings using the tangential sphere frame method. Returns a vector of heading angles, one per wing.
calc_heading(R_b_to_w, wing_pos)Calculate heading angle using the tangential sphere frame.
Projects the body x-axis onto the tangent plane of the tether sphere at wing_pos. Heading is measured from the elevation direction (xt, away from zenith) toward the azimuthal direction (yt). Heading = 0 when the kite nose points toward the ground station.
SymbolicAWEModels.calc_R_t_to_w — Function
calc_R_t_to_w(wing_pos)Calculate the rotation matrix from the local tether frame (_t) to the world frame (_w).
The tether frame is a local spherical coordinate system:
- z-axis: Aligned with the tether (radial direction).
- y-axis: Azimuthal direction, parallel to the XY plane.
- x-axis: Elevation direction, tangent to the sphere (
y × z).
SymbolicAWEModels.calc_R_v_to_w — Function
calc_R_v_to_w(wing_pos, e_x)Calculate the rotation matrix from the view frame (_v) to the world frame (_w).
The view frame is defined with its z-axis pointing from the origin to the wing, and its x-axis aligned with the wing's x-axis projected onto the view plane.
SymbolicAWEModels.cad_to_body_frame — Function
cad_to_body_frame(wing::Wing, pos)Transform a position from the CAD frame to the wing's body frame.
SymbolicAWEModels.calc_pos — Function
calc_pos(wing::Wing, gamma, frac)Calculate a position on the kite based on spanwise (gamma) and chordwise (frac) parameters.
SymbolicAWEModels.calc_winch_force — Function
calc_winch_force(sys, winch_vel, winch_acc, set_values)Calculate the tensile force on each winch tether from its motion and motor torque, using the default-component motor dynamics inverted for the force connector.
Reads friction from the live winch struct (populated each step from the component output), so the formula matches whatever the component reported even if the component overrides friction.
SymbolicAWEModels.quaternion_to_rotation_matrix — Function
quaternion_to_rotation_matrix(q)Convert a quaternion q (scalar-first format [w, x, y, z]) to a 3x3 rotation matrix. q need not be normalized: dividing each product by sum(abs2, q) folds the normalization into the leading factor, giving an orthonormal result for any nonzero q without a sqrt.
SymbolicAWEModels.rotation_matrix_to_quaternion — Function
rotation_matrix_to_quaternion(R)Convert a 3x3 rotation matrix R to a quaternion (scalar-first format [w, x, y, z]), selecting the numerically stable branch (largest of the trace / diagonal). Written with ifelse and & (not if/&&) so it evaluates symbolically on Num as well as on concrete Float64 — a single symbolic expression, computed once and common-subexpression-shared across components, with no @register_symbolic. The unselected branches' sqrt arguments are clamped to ≥0 so they never error; the selected branch always has a positive radicand.
SymbolicAWEModels.rotate_v_around_k — Function
rotate_v_around_k(v, k, θ)Rotate vector v around axis k by angle θ using Rodrigues' rotation formula.
SymbolicAWEModels.smooth_norm — Function
smooth_norm(v, eps=VortexStepMethod.SMOOTH_FLOOR)Differentiable norm: sqrt(sum(abs2, v) + eps^2). VortexStepMethod.smooth_norm under another name.
SymbolicAWEModels.smooth_sign — Function
smooth_sign(x, eps)Differentiable sign: x / sqrt(x^2 + eps^2). eps is the half-width of the transition through zero, in the units of x; below it the result is proportional to x rather than ±1, which is what keeps a Coulomb friction law solvable.
SymbolicAWEModels.coulomb_viscous_friction — Function
coulomb_viscous_friction(rate, coulomb_friction, viscous_coefficient, epsilon,
ratio = 1.0)Friction opposing rate: a constant coulomb_friction term carrying the sign of the motion (smooth_sign over epsilon) plus a viscous viscous_coefficient term growing with it. ratio transmits a tether-level coulomb_friction [N] and viscous_coefficient [N·s/m] onto another shaft — a winch passes drum_radius / gear_ratio, so the result is a torque [N·m]; at the default 1.0 the result is a force [N] in rate's own frame.
SymbolicAWEModels.smooth_normalize — Function
smooth_normalize(vec)Differentiable normalization: vec ./ smooth_norm(vec). Broadcast, not /: on a Vector{Num} the latter returns an unscalarised symbolic array, and a later smooth_norm/⋅ on it stays an opaque mapreduce/dot term that rebuilds the vector at runtime.
SymbolicAWEModels.apply_heading — Function
apply_heading(vec, R_t_to_w, curr_R_t_to_w, heading)Apply a heading rotation to a vector.
SymbolicAWEModels.get_rot_pos — Function
get_rot_pos(transform::Transform, bodies, points)Get the world position of the rotating object (body or point).
SymbolicAWEModels.get_base_pos — Function
get_base_pos(transform, transforms, bodies, points)Get (base_pos, curr_base_pos) for a transform.
For chained transforms (base_transform): returns the parent's current world position and CAD position, so T = base_pos - curr_base_pos shifts child points by the same displacement the parent transform applied.
For direct transforms (base_pos + base_point): returns the user-specified position and the base point's current position.
SymbolicAWEModels.calc_aoa — Function
calc_aoa(s::SymbolicAWEModel)Angle of attack [rad] of the first wing, dispatched on its aero mode (calc_aoa(::AbstractAeroModel, wing)). NaN if the mode defines no AoA.
calc_aoa(mode::AbstractAeroModel, wing) -> SimFloatAngle of attack [rad] for wing under aero mode. Defaults to NaN (undefined); VSM modes read the mid-span geometric AoA (wrapped to [-π, π]) and AeroPlate derives it from the body-frame apparent wind.
Shared component force-law kernels
SymbolicAWEModels.segment_geometry — Function
segment_geometry(pos_src, pos_dst, vel_src, vel_dst)Kinematics of a segment spanning pos_src → pos_dst. Returns (segment_vec, len, unit_vec, spring_vel), where len is the smoothed length, unit_vec the axis, and spring_vel = (vel_src - vel_dst) · unit_vec the along-axis closing speed (positive when the endpoints approach).
SymbolicAWEModels.segment_spring_force — Function
segment_spring_force(len, l0, spring_vel, unit_stiffness, unit_damping,
compression_frac, compression_damping_frac=1.0)Scalar spring-damper force along a segment (the Real-unit_stiffness branch of segment_eqs!). Stiffness is unit_stiffness / len in tension and softens to compression_frac of that under compression; the damping term (unit_damping / len) · spring_vel opposes the closing speed and softens to compression_damping_frac of itself over the same branch.
The default compression_damping_frac = 1.0 leaves the damping unaffected and the force continuous at len == l0, but the damping ratio jumps by 1/sqrt(compression_frac) as a segment goes slack. compression_damping_frac = compression_frac keeps one damping ratio on both branches, and 0.0 with compression_frac = 0.0 makes a slack segment carry no force at all — at the cost of a force step of (1 - compression_damping_frac) · unit_damping / l0 · spring_vel at the crossing, which a taut tether sits on.
Multiply by the segment unit_vec for the force vector on the source endpoint.
SymbolicAWEModels.segment_perp_drag — Function
segment_perp_drag(va, unit_vec, rho, cd_tether, area)Aerodynamic drag vector on a tether segment. Only the component of the apparent wind va perpendicular to the segment axis unit_vec contributes: 0.5 ρ c_d |va| A scaling the perpendicular apparent wind. area = len · diameter is the segment's projected area.
SymbolicAWEModels.air_density — Function
air_density(am, height)Air density [kg/m³] at height above the ground, clamped at zero so a component that dips below z = 0 keeps sea-level density instead of extrapolating the atmospheric model backwards. Every consumer — points, segments and wings, in both backends — reads density through here, so the clamp cannot differ between them.
SymbolicAWEModels.point_drag_force — Function
point_drag_force(va, rho, drag_coeff, area)Aerodynamic drag on a point mass from apparent wind va: 0.5 ρ c_d |va| A · va. Unlike a segment, the full apparent wind acts (no axis projection).
SymbolicAWEModels.segment_half_mass — Function
segment_half_mass(l0, diameter, density)Mass [kg] of half a tether segment, density · π (diameter/2)² · l0 / 2. Each endpoint of a segment carries this share, so the two halves sum to the full segment mass; a point's translational mass is extra_mass plus the halves of all incident segments.
Point/Segment component Systems
SymbolicAWEModels.point_acceleration — Function
point_acceleration(s, pos, vel, structural_force, mass, drag_coeff, area,
world_damping, wind_source, g_earth;
apparent_mass=0.0)(; net_force, accel) for a point mass: point_net_force and that per unit inertia, minus world-frame damping. apparent_mass is the entrained air the point accelerates (apply_apparent_mass!); it resists acceleration but has no weight, so it divides the net force without entering the gravity that built it.
SymbolicAWEModels.point_net_force — Function
point_net_force(s, pos, vel, structural_force, mass, drag_coeff, area,
wind_source, g_earth)The physical force on a point: the structural force gathered from its segments plus its own aerodynamic drag against the wind its wind_source gives at its height and gravity — the monolith's point_force. structural_force is the net force on the point (positive sign); each backend supplies it in its own aggregation convention. A clamped point reads it without moving, which is how an anchor's or a winch's load is read off. Both backends pass the registered params.set.g_earth as g_earth so gravity stays settable after construction; reading the setting here instead would bake it in at build time.
SymbolicAWEModels.confined_derivatives — Function
confined_derivatives(pos, vel, accel, pars)(D(pos), D(vel)) for a point that may be pinned: fix_static freezes it where it is, and fix_sphere confines it to a sphere about the world origin by keeping only the radial part of its velocity and acceleration. Matches the pair of ifelses in point_eqs!.
SymbolicAWEModels.point_particle_params — Function
point_particle_params(params, idx)The shared DYNAMIC-particle parameters read from params.points[idx] — mass, drag, area and world-frame damping as the point's own struct fields — plus the registered gravity g_earth and the point's point_wind_source. Returns a named tuple consumed by dynamic_point_dynamics; each read registers the parameter on params, so gravity stays settable after construction.
SymbolicAWEModels.dynamic_point_dynamics — Function
dynamic_point_dynamics(s, pos, vel, force, mass, pars, net_force)Shared body of the DYNAMIC point/pulley vertices: D(pos)=vel, D(vel)=point_acceleration(...) from the shared kernel, and the point's observed net_force, reading its drag/damping/wind parameters pars (a point_particle_params named tuple).
SymbolicAWEModels.wing_structural_segment — Function
wing_structural_segment(sys_struct, idx)Whether segment idx is an internal wing-structural link — both endpoints are wing nodes. Such a segment carries no tether drag (its aerodynamic load is owned by the wing's VSM), so the assembly gives it the drag-free segment type rather than passing a per-segment drag coefficient.
SymbolicAWEModels.segment_spring_params — Function
segment_spring_params(params, idx; with_drag=true)The spring-damper parameters read from params.segments[idx] (stiffness, damping, compression fraction, diameter, density as the segment's own struct fields), plus the global tether drag cd_tether (params.set.cd_tether). With with_drag=false (the wing_structural_segment edge) cd_tether is a literal 0 and unused. nonlinear marks a callable unit_stiffness force law. Each read registers the parameter on params.
SymbolicAWEModels.rigid_body_pose_expressions — Function
rigid_body_pose_expressions(force_w, moment_w, inertia_p, mass, R_b_to_p,
apparent_mass,
com_offset_b, com_w, com_vel, Q_p_to_w, ω_p;
ω_kinematic, d_ω_p, d_com_w, d_com_vel)Pure 6-DOF rigid-body derivative and body-frame output expressions (principal frame). Given the world-frame load at / about the COM (force_w, moment_w) and the principal state (com_w, com_vel, Q_p_to_w, ω_p as length-3/4 Num vectors), returns a named tuple of expressions: the state derivatives d_com_w/d_com_vel/d_Q/d_ω, the Euler angular accel α_p, the quaternion rate Q_p_vel, the COM accel com_acc, the principal moment moment_p, and the body-frame outputs R_p_to_w, R_b_to_w, pos_w, vel_w, acc_w, ω_b, α_b, Q_b_to_w. The optional integration overrides (ω_kinematic, d_ω_p, d_com_w, d_com_vel) reproduce fix_sphere/STATIC; left nothing the body integrates freely. apparent_mass is the entrained air the body accelerates (apply_apparent_mass!): it resists acceleration but has no weight, so it divides the net force without entering the gravity the caller put into force_w.
SymbolicAWEModels.joint_rayleigh_term — Function
joint_rayleigh_term(joint, params, kind, Δ, rate, beta)Rayleigh stiffness-proportional damping for one joint DOF: the restoring map evaluated at Δ + beta*rate minus at Δ. That is beta·K_tangent·rate to first order and exact beta·k·rate for a Real stiffness, and it vanishes identically when rate is zero, so rigid motion stays undamped whatever the stiffness law.
SymbolicAWEModels.timoshenko_local_wrench — Function
timoshenko_local_wrench(rigidities, L0, kshear, δ, θ_a, θ_b)Consistent Timoshenko end wrench (F_a, M_a, F_b, M_b) in element-frame coordinates for one set of generalized deformations. rigidities is (EA, GAy, GAz, GJ, EIy, EIz), already evaluated at the current strain. Linear in (δ, θ_a, θ_b), so passing β times the deformation rates yields the Rayleigh stiffness-proportional damping wrench from the same assembly.
SymbolicAWEModels.timoshenko_element_wrench — Function
timoshenko_element_wrench(joint, params; frame, theta_a, theta_b, force_a, force_b,
moment_a, moment_b, pos_a, R_a, com_a, com_vel_a, omega_a_w,
pos_b, R_b, com_b, com_vel_b, omega_b_w)Corotational Timoshenko element wrench. Given the two nodes' world poses (pos, R_b_to_w, com, com_vel, world spin omega_w) and the joint's rest geometry/rigidities, it builds the element frame and per-node deformations, evaluates the consistent Timoshenko stiffness (axial, torsion, two bending planes with shear reduction Φ) and damping, and returns (tear_eqs, force_on_a, moment_on_a, force_on_b, moment_on_b) — the restoring wrench on each node (world frame, transported to each COM). frame/theta_a/theta_b/force_a/ force_b/moment_a/moment_b are caller-supplied torn variables, array slices or standalone ones as the caller binds them, so the reused frame/force subtrees are not re-embedded; tear_eqs binds them.
SymbolicAWEModels.elastic_joint_wrench — Function
elastic_joint_wrench(joint, params; force_w, torque_w, pos_a, R_a, com_a, com_vel_a,
omega_a_w, pos_b, R_b, com_b, com_vel_b, omega_b_w)Lumped 6-DOF ElasticJoint restoring wrench. From the relative pose of the two anchors (in body A's frame) it builds the per-DOF restoring force/torque (axial, shear, torsion, bending stiffness + damping) and returns (tear_eqs, force_on_a, moment_on_a, force_on_b, moment_on_b) — the equal-and-opposite wrench transported to each COM. force_w/torque_w are the caller's torn world-frame wrench variables, array slices or standalone ones as the caller binds them.
SymbolicAWEModels.beam_hermite_ride_expressions — Function
beam_hermite_ride_expressions(joint, params, point_idx; pos_a, R_a, com_a, com_vel_a,
omega_a_w, pos_b, R_b, com_b, com_vel_b, omega_b_w)Kinematics of a point riding joint's corotational cubic-Hermite centerline at the point's beam_frac. From the two end bodies' world poses it builds the element frame, the two nodes' chord-relative rotations, the transverse Hermite deflection (+ a frame-carried beam_offset_b) and returns (pos_point, vel_point, sfrac, ride_velocity) — the ride position, its rigid-blend velocity, the axial fraction that splits any force at the point onto the two end bodies ((1−sfrac) to A, sfrac to B), and ride_velocity(p): the rigid-blend velocity as a function of a (possibly torn) ride position p, so a backend can tear pos_point first and avoid re-embedding the heavy element-frame subtree in the velocity.
SymbolicAWEModels.ground_wind_vec — Function
ground_wind_vec(params)Ground-level wind vector as symbolic parameters — params.set.wind_vec, with a tiny x-axis fallback when it is exactly zero (avoids normalize-by-zero, matching scalar_eqs!). Returns a length-3 Vector{Num}.
SymbolicAWEModels.wing_scalar_kinematics — Function
wing_scalar_kinematics(; rel_pos, e_x, R_t_to_w, R_v_to_w, R_b_to_w, vel, acc,
omega_b, alpha_b, va_b, twist_offset)The derived scalar kinematics of one wing.
rel_pos is the wing origin relative to its transform base point, so the spherical angles are centred there rather than on the world origin. The frames come in already built (calc_R_v_to_w, sym_calc_R_t_to_w) so a caller may pass bound variables instead of expressions. twist_offset is added to the angle of attack, carrying the mid-span twist of a wing that has stations.
Returns (; heading, turn_rate, turn_acc, distance, distance_vel, distance_acc, elevation, elevation_vel, elevation_acc, azimuth, azimuth_vel, azimuth_acc, course, angle_of_attack). turn_rate/turn_acc vanish for a wing whose omega_b and alpha_b are zero, which is every PARTICLE_DYNAMICS wing.
SymbolicAWEModels.station_dynamics — Function
station_dynamics(; free_angle, twist_vel, aero_moment, node_moment, mass,
chord, damping, stiffness)The hinged thin-plate twist degree of freedom.
Inertia about the hinged leading edge is ⅓·m·L² with L the surface chord, the angle is clamped to ±MAX_TWIST_ANGLE, and the driving moment is the wing aero's plus the one its points deliver, restrained by the surface's own stiffness and damping.
Returns (; inertia, angle, twist_acc, twist_vel_rate), where twist_acc is the unrestrained angular acceleration and twist_vel_rate the full right-hand side.
SymbolicAWEModels.MAX_TWIST_ANGLE — Constant
Largest twist a surface may reach either way [rad]; beyond it the plate is folded.
SymbolicAWEModels.pulley_split_eqs — Function
pulley_split_eqs(pulley_len, pulley_vel, tension_in, line_tension, pulley_mass,
pulley, pulley_len_out=nothing)The rope-split dynamics of a pulley: D(pulley_len)=pulley_vel, D(pulley_vel)=(tension_in − [pulleyfrictionforce](@ref))/pulley_mass (the aggregated tension_in being spring[seg1] − spring[seg2] and line_tension their mean). A braked pulley holds both derivatives at zero, freezing the split where it is. Given a pulley_len_out variable, pulley_len is also exposed through it so the incident segments read it as their l0; the monolith reads the split state directly and passes none. Used by PulleyParticle and pulley_eqs!.
SymbolicAWEModels.pulley_len_rate — Function
pulley_len_rate(pulley, pulley_vel)The rate the rope split travels at: pulley_vel, or zero while the pulley is braked. Both the split's own kinematic equation and the rest_len_rate its two segments feed to segment_load_terms read it here, so the rest length a damper sees moving is exactly the one the split integrates.
SymbolicAWEModels.pulley_friction_force — Function
pulley_friction_force(pulley, vel, line_tension)The friction force opposing rope travel vel over pulley (a params.pulleys[idx] view): (1 − efficiency) · line_tension, carrying the sign of the motion through smooth_sign over friction_epsilon. Both backends read the fields through here, so the pulley's friction lives in one place.
line_tension is the mean of the two leg tensions, so with both legs equally loaded this is the efficiency definition T_out = efficiency · T_in exactly. A slack pulley is frictionless, and has no tension driving its split either.
damping · vel is added on top: not a sheave property, defaults to zero, there to settle a ringing rope split while debugging.
SymbolicAWEModels.wing_frame_columns — Function
wing_frame_columns(zp1, zp2, yp1, yp2; torn_frame=nothing)The particle-wing body→world rotation columns fitted from the four structural ref points: z = normalize(zp2−zp1), x = normalize(normalize(yp2−yp1) × z), y = z × x. Returns (xaxis, yaxis, zaxis), each a 3-vector (the columns of R_b_to_w).
A caller that binds each column to its own equation passes the declared 3×3 rotation variable as torn_frame; the x and y expressions then read its z and x columns instead of re-embedding those subtrees.
SymbolicAWEModels.wing_frame_rates — Function
wing_frame_rates(zp1, zp2, yp1, yp2, rates, axes)Time derivatives of the wing_frame_columns axes from the reference points' velocities rates = (vz1, vz2, vy1, vy2). Differentiating the construction here keeps the frame's angular velocity an observable of pos/vel, rather than forcing MTK to differentiate the frame's algebraic equations.
SymbolicAWEModels.unit_vector_rate — Function
unit_vector_rate(vec, rate, unit)d/dt (vec/|vec|) given vec, its rate and its unit vector: the component of rate perpendicular to unit, over |vec|.
SymbolicAWEModels.body_frame_omega — Function
body_frame_omega(axes, rates)Body-frame angular velocity vee(Rᵀ·Ṙ) = [z·ẏ, x·ż, y·ẋ] of a frame from its axes and their rates, antisymmetrised so a non-orthonormal drift cannot bias it.
Equations and system management
SymbolicAWEModels.create_sys! — Function
create_sys!(s::SymbolicAWEModel, system::SystemStructure; prn=true)Create the full ModelingToolkit.ODESystem for the AWE model: calls the per-part equation generators (forces, wing dynamics, scalar kinematics, aerodynamics) and assembles their equations into a single System.
Arguments
s::SymbolicAWEModel: The main model object to be populated with the system.system::SystemStructure: The physical structure definition.prn::Bool=true: If true, print progress information during system creation.
Returns
set_values: The symbolic variable representing the control inputs (winch torques).
SymbolicAWEModels.scalar_eqs! — Function
scalar_eqs!(s, eqs, params; kwargs...)Generate equations for the derived scalar kinematics used in control and analysis: elevation, azimuth, heading, course, angle of attack, their time derivatives, and the apparent wind.
Arguments
s::SymbolicAWEModel: The main model object.eqs: Accumulating equation vector.kwargs...: Symbolic variables for the system's state.
Returns
eqs: The updated list of system equations.
SymbolicAWEModels.wing_eqs! — Function
wing_eqs!(s, eqs, defaults, params, initial; kwargs...)Generate the differential equations for the wing's rigid body dynamics.
For RIGID_DYNAMICS wings:
- ODE state:
com_w,com_vel,Q_p_to_w,ω_p(principal frame) - Wing-specific loads (aero transport, tether, damping) and pinning constraints are assembled here, then the generic 6-DOF integration is delegated to
rigid_body_eqs!.
For PARTICLE_DYNAMICS wings:
- No rigid body dynamics (handled by DYNAMIC points)
R_b_to_wfrom structural ref points- Principal frame variables set to zero/aliases
SymbolicAWEModels.rigid_body_eqs! — Function
rigid_body_eqs!(eqs, defaults, idx; kwargs...)Append the 6-DOF rigid body equations for body idx to eqs and its initial-condition defaults. Given a total force_w at the center of mass and moment_w about it (both world frame), integrate quaternion attitude and COM translation, and emit the body-frame output.
Aerodynamics, pinning constraints and damping are the caller's concern, imposed through the ω_kinematic/d_ω_p/d_com_w/d_com_vel integration overrides; at their defaults the body integrates freely.
State (principal frame, integrated)
com_w, com_vel, Q_p_to_w, ω_p.
Required keyword arguments
force_w,moment_w: length-3Numvectors, total force at / moment about the COM in world frame.inertia_p: length-3 principal inertia;mass: scalar.apparent_mass: entrained air resisting acceleration without weight (default 0).R_b_to_p: constant body→principal rotation;com_offset_b: COM offset in the body frame (origin→COM).- State / output array variables (indexed
[.., idx]internally):com_w,com_vel,Q_p_to_w,ω_p,com_acc,α_p,R_p_to_w,moment_p,Q_p_vel,R_b_to_w,wing_pos,wing_vel,wing_acc,ω_b,α_b,Q_b_to_w. - Initial conditions:
initial_com_w,initial_com_vel,initial_Q_p_to_w,initial_ω_p—initial.*view paths bound to the integrated state.
Optional integration overrides (default to the unconstrained body)
ω_kinematic: angular velocity used in quaternion kinematics (defaultω_p).d_ω_p: RHS ofD(ω_p)(defaultα_p).d_com_w: RHS ofD(com_w)(defaultcom_vel).d_com_vel: RHS ofD(com_vel)(defaultcom_acc).
SymbolicAWEModels.body_eqs! — Function
body_eqs!(eqs, defaults, bodies, params, initial; kwargs...)Generate the differential equations for each plain Body (no aero). Loads are the accumulated joint wrench (body_force/body_moment, filled by joint_eqs!) plus gravity (-g·mass at the COM) and the external wrench (ext_force_w world, ext_force_b/ext_moment_b body). STATIC bodies are frozen; fix_sphere confines the COM to a sphere about the world origin; damping is per-axis angular damping.
SymbolicAWEModels.joint_eqs! — Function
joint_eqs!(eqs, elastic_joints, params; kwargs...)For each ElasticJoint, compute the restoring wrench from the relative pose of the two anchors (in body A's frame) and accumulate it — equal and opposite — into body_force/body_moment (the same accumulators body_eqs! reads). The relative rotation uses the small-angle vector extraction, exact for the small per-joint rotations of a stiff chain.
SymbolicAWEModels.timoshenko_joint_eqs! — Function
timoshenko_joint_eqs!(eqs, timoshenko_joints, params; kwargs...)For each TimoshenkoJoint, build a corotational element frame, extract the small per-node deformations (axial stretch, chord-relative rotations) relative to the rest geometry, evaluate the consistent Timoshenko stiffness (axial, torsion, and two bending planes with the shear reduction Φ = 12·EI/(k·GA·L²)) — each rigidity either constant or a callable of its strain/curvature (timoshenko_rigidity) — and accumulate the restoring wrench — equal and opposite, transported to each COM — into body_force/body_moment (the same accumulators body_eqs! reads). Damping resists the axial stretch rate and the relative node spin.
SymbolicAWEModels.station_delta_eqs! — Function
station_delta_eqs!(eqs, stations; station_delta, body_R_b_to_w, pos)Emit the live flap deflection δ for each station into station_delta; a surface with no flap gets 0. The flap axis, reference chords and rest angle are frozen rest geometry baked in as constants.
A point flap (has_point_flap) gets the angle between its two chord segments, straight out of the point positions pos — the same three points the aerodynamics is built on, so a chord that bends over several beam elements still reads a deflection. A body flap gets the signed angle between its two flap bodies' reference chords. Both are the shared hinge_angle, referenced to rest.
SymbolicAWEModels.has_flap — Function
has_flap(station::Station) -> Booltrue if the station carries a flap hinge either way, so it drives a live deflection δ (flap_delta / point_flap_delta / station_delta_eqs!). A station given both reads the points.
SymbolicAWEModels.has_body_flap — Function
has_body_flap(station::Station) -> Booltrue if the station reads its deflection off two ordered flap bodies.
SymbolicAWEModels.has_point_flap — Function
has_point_flap(station::Station) -> Booltrue if the station reads its deflection off three structural points [fore, hinge, aft] (point_flap_delta).
SymbolicAWEModels.flap_delta — Function
flap_delta(station, R_main, R_flap) -> SimFloatSigned flap deflection δ [rad] of station from the two flap bodies' world orientations R_main, R_flap: project each body's reference chord direction onto the plane normal to the world hinge axis and take the signed angle about that axis, minus the rest deflection flap_rest_delta. Robust across the full polar δ range (an atan, not a small-angle approximation). The same formula the symbolic RHS uses (station_delta_eqs!), so it is the ground truth for tests.
SymbolicAWEModels.point_flap_delta — Function
point_flap_delta(station, points, R_wing) -> SimFloatDeflection δ [rad] of a point flap from the current positions of its three points, with R_wing the owning wing's orientation. The same point_flap_delta_expression the symbolic RHS evaluates, so it is the ground truth for tests.
SymbolicAWEModels.init_station_flap! — Function
init_station_flap!(station, sys_struct)Capture a flapped KINEMATIC station's rest geometry, so the undeformed configuration reads δ = 0. A body flap defaults its reference chords to each body's x-axis and measures the placed poses; a point flap measures its three points in CAD, which is the geometry the polars were tabulated from and is unaffected by whatever the placement or a settling run has since done to the structure. No-op for surfaces without a flap.
SymbolicAWEModels.derive_point_beam_anchor! — Function
derive_point_beam_anchor!(point, joint, bodies)Derive a beam-anchored point's beam_frac and beam_offset_b from its pos_cad: project onto the rest beam line between the joint's two node anchors (CAD frame), storing the axial fraction s ∈ [0,1] and the perpendicular remainder expressed in the rest element frame (so the point tracks the same off-centerline offset as the beam bends). Usually near-centerline (offset ≈ 0).
SymbolicAWEModels.init_rigid_body! — Function
init_rigid_body!(body::Body)Derive the body's principal-frame ODE state from its body-frame initial conditions via init_principal_state!.
SymbolicAWEModels.n_orient_frames — Function
Number of orientation frames (wings + rigid bodies, at least 1).
SymbolicAWEModels.aero_eqs! — Function
aero_eqs!(s, eqs; kwargs...)
-> (eqs, aero_subsystems)Instantiate and wire each wing's aero component. Returns the list of component subsystems to attach to the parent System.
SymbolicAWEModels.point_eqs! — Function
point_eqs!(s, eqs, defaults, points, segments, stations, wings, params, initial;
R_b_to_w, wing_vel, wind_vec_gnd, twist_angle,
pos, vel, acc, point_force, point_mass, spring_force_vec, drag_force, l0,
spring_sum_force, point_aero_drag, total_drag,
disturb_force, tether_r, chord_b, fixed_pos, normal, pos_b,
fix_point_sphere, fix_static,
va_point_b, va_point_w, wind_at_point, height,
aero_force_point_b,
station_y_airf)Generate equations for all point types (STATIC, DYNAMIC, BODY_STATIC).
Each point's net force is the shared point_net_force: the structural load gathered from its incident segments (their spring force with the endpoint sign and half their drag), plus per-node aero, its own drag and gravity. A point that rides a rigid body has zero gravitational mass here, since that mass is carried at the body's COM. Free particles integrate through confined_derivatives, which applies fix_static and fix_sphere; a rigid wing node is placed instead by twist_deformed_offset from its body's COM.
Arguments
s::SymbolicAWEModel: The main model object (for atmospheric model).eqs,defaults: Accumulating vectors for the MTK system.points,segments,stations,wings: System components.R_b_to_w: Symbolic rotation matrix (body to world).wing_vel: Symbolic wing center of mass velocity.wind_vec_gnd: Symbolic ground-level wind vector.twist_angle: Symbolic station twist angle.pos,vel,acc: Pre-declared point state variables.point_force,point_mass: Pre-declared point force and mass variables.spring_force_vec,drag_force,l0: Pre-declared segment force variables.spring_sum_force: Pre-declared accumulated spring/drag forces variable.- Other variables: Various point-specific symbolic variables.
body_force,body_moment: Mutable arrays to accumulate wing-node loads onto bodies.
Returns
- Tuple
(eqs, defaults)with updated equation vectors. Note:body_forceandbody_momentare modified in-place.
SymbolicAWEModels.segment_eqs! — Function
segment_eqs!(s, eqs, points, segments, pulleys, tethers, bodies, params;
pos, vel, wind_vec_gnd, wind_at_point, spring_force_vec,
drag_force, l0, pulley_len, pulley_vel, tether_len, tether_vel)Generate equations for segment spring-damper forces and aerodynamic drag.
Every load term comes from the shared segment_load_terms with the parameters read by segment_spring_params, so the monolith and the KernelBackend evaluate the same force law. A wing_structural_segment gets with_drag = false (its aerodynamic load is owned by the wing's VSM), and one whose wing has RIGID_DYNAMICS skips the spring as well, keeping only the geometry.
Arguments
s::SymbolicAWEModel: The main model object (for atmospheric model).eqs: Accumulating equation vector for the MTK system.points,segments,pulleys,tethers,bodies: System components.pos,vel: Symbolic point state variables.wind_vec_gnd: Symbolic ground-level wind vector.wind_at_point: Per-point wind, which aPerPointWindsegment averages over its two endpoints instead of evaluating a height profile at its midpoint.spring_force_vec,drag_force,l0: Pre-declared segment force variables.pulley_len,tether_len: Symbolic state variables for pulley and tether lengths.pulley_vel,tether_vel: Their rates, which a moving rest length's damper reads throughsegment_rest_length_eqs.
Returns
- Tuple
(eqs, len, spring_force)with updated equation vector and the segment length and spring force variables for use by other components.
SymbolicAWEModels.refresh_aero! — Function
refresh_aero!(sam::SymbolicAWEModel; vsm_min_wind=0.5, cold_start=false,
vsm_warn_on_fail=false)Refresh each wing's aerodynamic state, dispatching on the wing's aero mode (refresh_rigid_aero! / refresh_particle_aero!), and re-spread the entrained air (apply_apparent_mass!) at the density it just read. Runs on the low-frequency VSM-update schedule (vsm_interval), not the compiled RHS. Reads per-point apparent wind from the points (populated by update_sys_struct!, which always runs first), so it does not re-extract integrator state.
RIGID_DYNAMICS VSM modes: compute wind-axis coefficients (CL, CD, CS, CM, cm) at the operating point, plus the ForwardDiff Jacobian over [α, β, ω₁, ω₂, ω₃, θ_twist…] (AeroLinearized) or the frozen forces (AeroDirect).
PARTICLE_DYNAMICS VSM modes: full nonlinear VSM solve with per-point force distribution. Non-VSM modes (AeroNone/AeroPlate) are no-ops, so callers should gate this on has_vsm_wing.
cold_start discards the previous solve's circulation as the warm start (safe_vsm_solve!), making the result a function of the current state alone. The per-step refresh wants the warm start; the first solve after a reinit! must not have it, or it inherits whatever ran on this model before.
vsm_warn_on_fail downgrades a VortexStepMethod.SolveFailure to a warning (warn_or_rethrow).
SymbolicAWEModels.wing_kinematics_from_points! — Function
wing_kinematics_from_points!(wing, points, set, am, wind_mode;
zp1, zp2, yp1, yp2, origin, aero_points)Recompute a KINEMATIC/PARTICLE wing's kinematic state directly from the current point positions/velocities, for a backend that does not carry these quantities as state. zp1, zp2, yp1, yp2 and origin are WeightedRefPoints, so each reference is the weighted blend of its points. Writes the body frame R_b_to_w (wing_frame_columns), the origin pose pos_w/vel_w, the frame's own ω_b (body_frame_omega), the origin's acceleration acc_w (point_acceleration_w), the reported scalars (write_wing_scalars!), the wing apparent wind va_b, and each aero point's va_b = R'·(wind − vel), the wind coming from the height profile or, under PerPointWind, from point.wind_vec and wing.wind_vec, which the caller owns — the same quantities the monolith's get_all_state copies out of the integrator.
SymbolicAWEModels.point_acceleration_w — Function
point_acceleration_w(point, wing_frame, wing_vel) -> KVec3A free particle's world-frame acceleration, rebuilt from what the struct already carries: its net force per unit mass less the world-frame and body-frame damping. The same expression point_eqs! binds to acc, which is where the monolith's fitted wing reads its own acc_w from.
SymbolicAWEModels.write_wing_scalars! — Function
write_wing_scalars!(wing, points; base_point, alpha_b, stations) -> nothingFill a fitted wing's reported scalars — heading, course, elevation, azimuth, distance and angle of attack with their rates — from its freshly rebuilt pose, via the one definition in wing_scalar_kinematics.
turn_rate follows the ω_b the caller fitted from the frame's own ref points, the _acc scalars the wing.acc_w it wrote, and turn_acc the alpha_b it passed. alpha_b defaults to zero, which is what the monolith binds a fitted wing's to.
SymbolicAWEModels.sync_aero_density! — Function
sync_aero_density!(wing, am)Set the wing's VSM solver air density to air_density(am, wing.pos_w[3]), the same altitude-dependent density the symbolic RHS uses to dimensionalize aero forces (see aero_eqs.jl). Keeps the VSM solve and the model consistent on dynamic pressure. No-op for non-VSM aero modes.
SymbolicAWEModels.jacobian — Function
jacobian(f::Function, x::AbstractVector, ϵ::AbstractVector) -> MatrixNumerically compute the Jacobian of a vector-valued function f at point x, by forward finite differences.
Arguments
f::Function: The function to differentiate (y = f(x)).x::AbstractVector: The point at which to evaluate the Jacobian.ϵ::AbstractVector: A vector of perturbation sizes for each component ofx.
Returns
Matrix: The Jacobian matrixJ, whereJ[i, j] = ∂f[i] / ∂x[j].
SymbolicAWEModels.load_serialized_model! — Function
load_serialized_model!(sam, model_path; remake=false, reload=false)Load a serialized model from disk if it is valid.
A model is considered valid if its settings and system structure hashes match the current ones in the SymbolicAWEModel object (sam).
Arguments
sam::SymbolicAWEModel: The main model object.model_path::String: The path to the serialized model file.remake::Bool: If true, forces the model to be considered invalid, triggering a rebuild.reload::Bool: If true, forces reloading from disk even if the model is already in memory.prn::Bool: If true, say what happened — kept in memory, read back from disk, or rejected — since from the outside a cache hit and a rebuild look the same.
Returns
trueif a valid model was successfully loaded intosam.serialized_model,falseotherwise.
SymbolicAWEModels.in_memory — Function
in_memory(serialized_model) -> BoolWhether the model already carries a build, so a valid bin on disk need not be read back. The MonolithBackend leaves its full_sys behind and the KernelBackend only its assembled problem, so either one counts.
SymbolicAWEModels.backend_tag — Function
backend_tag(backend) -> StringThe backend's mark in a serialized model's filename. Two backends assemble the same SystemStructure into different artefacts, so they need separate cache entries; the MonolithBackend tag is empty so its existing bins keep loading.
SymbolicAWEModels.default_autodiff — Function
default_autodiff(backend)How init! differentiates the right-hand side for the solver's Jacobian.
The MonolithBackend takes AutoFiniteDiff(): forward mode would compile its single enormous right-hand side a second time, at ForwardDiff.Dual, which costs more at the first init! than it saves.
The KernelBackend takes AutoForwardDiff(). Its right-hand side is small per-kernel functions and buffers already keeps a scratch set per element type, so the Dual specialization is one more compilation of each kernel rather than of one enormous function, and the chunked forward Jacobian needs an order of magnitude fewer right-hand side evaluations than the dense finite-difference one.
SymbolicAWEModels.default_analytic_jacobian — Function
default_analytic_jacobian(backend) -> BoolWhether init! gives the solver an analytical Jacobian rather than letting it differentiate the right-hand side numerically.
The KernelBackend does: the model is a layered composition of small components, so build_jacobian differentiates each component at its own width and composes the result through the constant wiring — one pass per kernel instead of one chunked pass per twelve states of the whole model.
The MonolithBackend does not. Its only route is MTK's jac=true, which differentiates the flattened right-hand side symbolically: several times the build cost, and it does not run, because a registered numerical leaf — the wind profile, an aerodynamic polar — has no symbolic derivative and leaves a Differential in the generated matrix.
SymbolicAWEModels.default_sparse — Function
default_sparse(backend) -> BoolWhether init! hands the solver the Jacobian's sparsity pattern rather than letting it factorize a dense matrix.
The KernelBackend does. It knows the pattern already — state_sparsity walks it out of the wiring — and a structure is mostly zeros: on a 392-point beam the Jacobian is 1301 states square and 5.15% dense, where factorizing it dense is most of a step. That measured 35.5 ms a step against 50.2 ms. It also threads where the dense one does not: eight models stepping at once manage 48.4 steps/s against 4.8, because a dense factorization opens a BLAS thread pool per calling worker over the same cores.
The MonolithBackend does not, so the bins it has already written keep loading.
SymbolicAWEModels.default_linsolve — Function
default_linsolve(backend)Which factorization init!'s solver uses, or nothing to leave the choice to LinearSolve.
The KernelBackend takes KLUFactorization. default_sparse gives it a sparse Jacobian, for which LinearSolve would otherwise pick UMFPACK; KLU refactorizes one pattern over and over, which is what a BDF integrator does with it, and measured 1.3x faster at every worker count on a large kite.
The MonolithBackend takes nothing: its Jacobian is dense, so neither sparse factorization applies.
SymbolicAWEModels.maybe_create_lin_prob! — Function
maybe_create_lin_prob!(sam, outputs; ...)Create and cache the LinearizationProblem if it does not exist or if the outputs have changed.
Arguments
sam::SymbolicAWEModel: The main model object.outputs: A vector of output variables for the linearization.create_lin_prob::Bool: Flag to enable/disable creation.outputs_changed::Bool: Flag indicating if the output vector has changed.prn::Bool: Flag to enable/disable printing of progress messages.
Returns
trueif a new problem was created,falseotherwise.
SymbolicAWEModels.maybe_create_control_functions! — Function
maybe_create_control_functions!(sam, outputs; ...)Create and cache the control functions if they do not exist or if the outputs have changed.
Arguments
sam::SymbolicAWEModel: The main model object.outputs: A vector of output variables for the control functions.create_control_func::Bool: Flag to enable/disable creation.outputs_changed::Bool: Flag indicating if the output vector has changed.prn::Bool: Flag to enable/disable printing of progress messages.
Returns
trueif new functions were created,falseotherwise.
SymbolicAWEModels.maybe_create_prob! — Function
maybe_create_prob!(sam; create_prob=true, sparse=false,
analytic_jacobian=false, prn=true)Compile the full system, create the ODEProblem and its getter/setter functions, if they do not already exist.
Arguments
sam::SymbolicAWEModel: The main model object.create_prob::Bool: A flag to enable or disable the creation of the problem.sparse::Bool: Give the solver a Jacobian sparsity pattern (seeinit!).analytic_jacobian::Bool: Give the solver an analytical Jacobian (seeinit!).prn::Bool: A flag to enable or disable printing of progress messages.
Returns
trueif a new problem was created,falseotherwise.
SymbolicAWEModels.build_prob! — Function
build_prob!(backend, sam; sparse=false, analytic_jacobian=false, prn=true)Assemble sam.prob for the given ModelBackend. The MonolithBackend method mtkcompiles the flattened full_sys into one ODEProblem; the KernelBackend assembles one kernel per component type and schedules them. sparse gives the solver a Jacobian sparsity pattern and analytic_jacobian a Jacobian, both of which each backend derives its own way — the monolith's is MTK's symbolic one, which is why it is off by default (default_analytic_jacobian). Returns true when a problem was built.
build_prob!(::KernelBackend, sam; sparse=false, analytic_jacobian=true, prn=true)Assemble sam.sys_struct into a KernelModel and wrap its right-hand side as an ODEProblem. The problem's parameter object is our own flat buffer plus the callable store, so sync_params! writes struct fields straight into it. sparse hands the solver state_sparsity as the Jacobian prototype; without it the Jacobian is dense, as the monolith's is. analytic_jacobian hands it a KernelJacobian rather than leaving it to differentiate the right-hand side numerically. FullSpecialize because the right-hand side is one concrete type, so SciMLBase's function wrappers would only add indirection and allocate; it goes on the ODEFunction as well as the problem, a bare ODEFunction being AutoSpecialize.
SymbolicAWEModels.print_kernel_times — Function
print_kernel_times(system)List what each kernel cost to compile, dearest first, with how many instances share it. Compilation is per kernel, so this is where a slow build has to be read: a large model with few kernel types is cheap, a small one with many types is not.
SymbolicAWEModels.init_backend! — Function
init_backend!(backend, sam, solver; kwargs...)Full init! path for a non-MonolithBackend. The monolith uses the init! body directly; other backends (currently KernelBackend) implement their own assembly + integrator build here and return the fresh ODEIntegrator: refresh the SystemStructure (positions, rest lengths), assemble the problem from it, and store sam.prob/sam.integrator.
init_backend!(::KernelBackend, sam, solver; kwargs...)Full init! path for the KernelBackend: refresh the SystemStructure, assemble the problem from it, and build the integrator. The assembled problem is serialized to get_model_name's bin and read back on the next init! of the same structure, exactly as the monolith's is — the kernels' mtkcompile is the slow part and it is what the bin saves. remake forces a rebuild, reload re-reads the bin over an in-memory build. The struct's positions and rest lengths are pushed onto a reused problem by KernelInitialSync, so a bin stays valid across them; a parameter no registry reader syncs keeps the value the struct had when the bin was built, as on the monolith.
SymbolicAWEModels.has_custom_component — Function
has_custom_component(sys_struct)Return true when the system has a non-default winch model or a wing using a custom aero model, in which case the compiled model cannot be reused from cache and must be rebuilt.
SymbolicAWEModels.generate_control_funcs — Function
generate_control_funcs(model, inputs, outputs)Generate in-place and out-of-place control functions from a ModelingToolkit system, wrapping ModelingToolkit.generate_control_function and ModelingToolkit.build_explicit_observed_function.
Arguments
model: The fullODESystem.inputs: A vector of input variables.outputs: A vector of output variables.
Returns
- A
NamedTuplecontaining the generated functions (f_oop,f_ip,h_oop,h_ip), system dimensions (nu,nx,ny), and symbolic variables (dvs,psym,io_sys).
SymbolicAWEModels.generate_lin_getters — Function
generate_lin_getters(sys)Generate setter functions for the parameters of a linearized system.
Arguments
sys: The linearized ModelingToolkit system.
Returns
- A
NamedTuplecontaining the setter function for the winch set-points (set_set_values).
SymbolicAWEModels.generate_prob_getters — Function
generate_prob_getters(sys_struct, sys)Generate getter and setter functions for the state variables of the full system model.
These functions provide a convenient way to access and modify the state and parameters of the compiled ODESystem (sys).
Arguments
sys_struct::SystemStructure: The structure defining the system topology.sys::ODESystem: The compiled ModelingToolkit system.
Returns
- A
NamedTuplecontaining various getter and setter functions for different parts of the system state.
SymbolicAWEModels.scatter_spec — Function
scatter_spec(selector, pairs...)Describe one component group: selector(sys_struct) yields its component vector and each sys_array => copyfn pair maps a symbolic output array to a (component, view) -> _ closure that writes that array's slice into the component's struct field. This is the single source of truth — both the buffer layout and the scatter derive from the same ordered list.
SymbolicAWEModels.build_inplace_getter — Function
build_inplace_getter(sys, specs)Build one InplaceGetter from the per-group specs (see scatter_spec). All component output arrays are concatenated into a single MTK in-place observed function so shared subexpressions (e.g. the spring/force network) are computed once.
SymbolicAWEModels.build_grouped_views — Function
build_grouped_views(buf, group_shapes)Build a tuple (per group) of tuples (per output array) of zero-copy reshaped views into buf. Flat order is groups-in-order, arrays-in-order, column-major within each array — shared by build_inplace_getter and deserialization so layouts always match.
SymbolicAWEModels.copy_vec! — Function
Copy column idx of the matrix view v into the mutable vector field in place.
SymbolicAWEModels.scatter_component — Function
Apply each (component, view) copy closure for one component (tuple recursion).
SymbolicAWEModels.scatter_groups — Function
Scatter every group into sys_struct (tuple recursion over heterogeneous groups).
SymbolicAWEModels.LinProbWithAttributes — Type
@with_kw struct LinProbWithAttributes{SetLinSetValues, SetLinSys, SetLinSet, LinOut}A container for the general-purpose linearization problem and the resulting full linearized model (A,B,C,D matrices).
prob::Any: Linearization problem of the mtk model.set_set_values::Any
SymbolicAWEModels.ProbWithAttributes — Type
@with_kw struct ProbWithAttributes{...}A container for the main Ordinary Differential Equation (ODE) problem and its associated getter and setter functions for the full, nonlinear physical state.
SymbolicAWEModels.ControlFuncWithAttributes — Type
@with_kw struct ControlFuncWithAttributes{FIP, FOOP, HIP, HOOP, DVS, PSYM}A container for callable control functions and their symbolic representations, generated from the full system model.
f_ip::Any: In-place dynamics function f(dx, x, u, p, t).f_oop::Any: Out-of-place dynamics function dx = f(x, u, p, t).h_ip::Any: In-place observation function h(y, x, u, p, t).h_oop::Any: Out-of-place observation function y = h(x, u, p, t).nu::Int64: Number of inputs (u).nx::Int64: Number of states (x).ny::Int64: Number of outputs (y).dvs::Any: The symbolic state vector.psym::Any: The symbolic parameter vector.io_sys::ModelingToolkitBase.System: The generated input-output system.
Utility and internal functions
SymbolicAWEModels.get_model_name — Function
get_model_name(set::Settings, sys_struct::SystemStructure; precompile=false,
sparse=false, analytic_jacobian=false, backend=MonolithBackend())Constructs a unique filename for the serialized model based on its configuration. The filename includes the SymbolicAWEModels version, Julia version, physical model, wing type, dynamics type, and component counts to ensure that the correct cached model is loaded. sparse, analytic_jacobian and backend are part of the name because the cached ODEProblem carries its Jacobian prototype, its Jacobian and its backend's assembly, so those builds are different artefacts; naming them apart keeps all of them on disk rather than invalidating one with the other.
SymbolicAWEModels.calc_height — Function
calc_height(s::SymbolicAWEModel)Returns the height (z-position) [m] of the wing.
SymbolicAWEModels.pos — Function
pos(s::SymbolicAWEModel)Returns a vector of the position vectors [m] for each point in the system.
SymbolicAWEModels.spring_forces — Function
spring_forces(s::SymbolicAWEModel)Returns the spring force [N] for each tether segment.
SymbolicAWEModels.create_model_archive — Function
create_model_archive(source_dir, archive_path)Finds all model*.bin files in the source_dir, copies them to a temporary directory, and compresses that directory into a .tar.gz archive at the specified archive_path.
SymbolicAWEModels.filecmp — Function
filecmp(path1::AbstractString, path2::AbstractString) -> BoolCompare two files byte-by-byte to check if they are identical.
SymbolicAWEModels.extract_model_archive — Function
extract_model_archive(archive_path, dest_dir)Safely decompress a .tar.gz file by first extracting to a temporary directory and then copying the contents to the final destination.
Arguments
archive_path::String: The path to the.tar.gzfile to be extracted.dest_dir::String: The path to the target directory.
SymbolicAWEModels.copy_bin — Function
copy_bin()Copy all example scripts to the folder "bin" (it will be created if it doesn't exist).
SymbolicAWEModels.copy_examples — Function
copy_examples()Copy all example scripts to the folder "examples" (it will be created if it doesn't exist).
SymbolicAWEModels.copy_data — Function
copy_data()Copy all data scripts to the folder "data" (it will be created if it doesn't exist).
SymbolicAWEModels.copy_dir — Function
copy_dir(src_dir, dst_dir)Copies all files from src_dir to dst_dir. Overwrites existing files if force=true. Creates dst_dir if it does not exist.
SymbolicAWEModels.get_example_packages — Function
get_example_packages()Get the list of packages from examples/Project.toml, excluding SymbolicAWEModels itself. This ensures init_module installs the correct dependencies for running examples.
SymbolicAWEModels.make_lin_sys_state — Function
make_lin_sys_state(y::AbstractVector, sam::SymbolicAWEModel, t::Real)Construct a SysState for logging linear state-space simulation output y (ordered as sam.outputs).
Base overloads (internal use)
SymbolicAWEModels.SAM_FIELDS — Constant
SAM_FIELDSTuple of field names that are direct fields of SymbolicAWEModel (as opposed to fields delegated to the nested serialized_model). Used by getproperty and setproperty! to dispatch field access correctly.
Base.getindex — Function
Access item by numeric index.
Access item by symbolic name.
Base.getindex(x::ModelingToolkit.Symbolics.Arr, idxs::Vector{Int64})Extend Base.getindex to allow indexing a symbolic array with a vector of integer indices, which is not natively supported by ModelingToolkit.
Base.getproperty — Function
Base.getproperty(pa::ProbWithAttributes, sym::Symbol)Overloads getproperty to provide convenient access to the simplified system (sys) contained within the ODE problem's function definition.
Base.setproperty! — Function
Base.setproperty!(sam::SymbolicAWEModel, sym::Symbol, val)Overloads setproperty! to allow direct setting of fields within the nested serialized_model. This allows you to change properties of the compiled model as if they were fields of the SymbolicAWEModel itself.
Serialization.serialize — Method
serialize(s, g::InplaceGetter)Julia's serializer does not preserve SubArray.parent === buf sharing, so serialize the generated function plus a cheap layout and rebuild the aliased buffer and views on load.
YAML loader internals
SymbolicAWEModels.get_field_or_nothing — Function
get_field_or_nothing(::Type{T}, row::NamedTuple,
field::Symbol) where TConvert field to type T if present, otherwise return nothing.
Examples
get_field_or_nothing(Int64, row, :idx) # -> Int64 or nothing
get_field_or_nothing(Tuple{Int64,Int64}, row, :pair)
# -> (Int64, Int64) or nothingSymbolicAWEModels.convert_to_type — Function
convert_to_type(::Type{T}, value) where TConvert value to type T. Handles special cases like Tuples.
SymbolicAWEModels.substitute_variables — Function
substitute_variables(value, lookup)Replace every string inside value (scalars, list entries and mapping values, recursively) by lookup(string). The headers entry of a table is left alone, so a column may share its name with a variable.
SymbolicAWEModels.resolve_variable! — Function
resolve_variable!(resolved, raw, name, pending) -> valueValue of variable name, substituting any variables it refers to first. Strings that are not variable names are returned unchanged; pending tracks the chain being resolved to report cycles.
SymbolicAWEModels.check_variable_names — Function
check_variable_names(data, variables)Error when a variable name is also used as a component name, since references to that component would resolve to the variable instead.
SymbolicAWEModels.expand_multi_variable_row — Function
expand_multi_variable_row(row, headers, multi_variables) -> rowReplace every cell of row naming a multi-variable by that variable's fields. In a headers/data row the fields fill the columns starting at the cell, written in header order, so the row carries one entry for the whole group. In a dict row they are merged in without overwriting fields the row states itself.
SymbolicAWEModels.expand_multi_variables — Function
expand_multi_variables(table, multi_variables) -> tableExpand the multi-variables used in the rows of one YAML block. Blocks that hold no data rows are returned unchanged.
SymbolicAWEModels.resolve_yaml_variables — Function
resolve_yaml_variables(data) -> dataApply an optional top-level variables block to the rest of the YAML tree and drop the block. A variable holding a number, string or list replaces any cell written as its name; a variable holding a mapping is a multi-variable and fills the columns it names at once. Variables may refer to other variables.
variables:
bridle_comp: 0.01
dyneema: {youngs_modulus: 55.0e9, damping_per_stiffness: 0.00077,
density: 724.0}SymbolicAWEModels.extract_args — Function
extract_args(row, args_spec, mappings)Extract positional constructor arguments from a YAML row.
For each name in args_spec, this helper first checks for a mapping in mappings, then falls back to row[arg_name]. Throws an error if a required argument is missing.
SymbolicAWEModels.call_yaml_constructor — Function
call_yaml_constructor(Constructor, row::NamedTuple,
args_spec, kwargs_spec; mappings=Dict())Generic YAML-to-constructor caller. Extracts positional args and kwargs from YAML row and calls constructor.
Arguments
Constructor: Constructor function to callrow::NamedTuple: Parsed YAML rowargs_spec::Vector{Symbol}: Names for positional argskwargs_spec::Vector{Symbol}: Names for kwargs
Keyword Arguments
mappings::Dict{Symbol, Function}: Mapping functions that take the row and return the arg value
Example
row = (idx=1, x=0.0, y=0.0, z=0.0, type="STATIC")
point = call_yaml_constructor(Point, row,
[:idx, :pos_cad, :type], # positional args
[:extra_mass, :wing_idx]; # kwargs
mappings=Dict(
:pos_cad => r -> [Float64(r.x),
Float64(r.y), Float64(r.z)],
:type => r -> parse_dynamics_type(
String(r.type))
))SymbolicAWEModels.parse_tether_init — Function
parse_tether_init(row, tether_name)
-> (stretched_length, tether_force, stretch_frac)Read init_stretched_length, init_tether_force and init_stretch_frac from a tether YAML row (each nothing if absent). Errors if the deprecated init_unstretched_length field is present — the unstretched rest length is now derived from the placed stretched length with init_tether_force or init_stretch_frac.
SymbolicAWEModels.yaml_row_name — Function
yaml_row_name(row, i)Name for a YAML row: its name field (as a Symbol) when present, else the 1-based row index i. Shared by the body/joint loaders so components can be referenced either by name or by position.
SymbolicAWEModels.yaml_block_empty — Function
yaml_block_empty(data, key) -> Booltrue when the top-level YAML key is absent or holds no data rows.
SymbolicAWEModels.yaml_vec3 — Function
yaml_vec3(row, field) -> KVec3 or nothingRead a 3-element vector field, or nothing when absent.
SymbolicAWEModels.yaml_matrix3 — Function
yaml_matrix3(value) -> Matrix{SimFloat}Convert a nested [[…],[…],[…]] YAML value into a 3×3 matrix (rows preserved).
SymbolicAWEModels.yaml_ref_field — Function
yaml_ref_field(row, field, to_ref) -> reference or nothingResolve an optional reference column field (e.g. :transform_idx) through to_ref, or nothing when the field is absent or empty.
SymbolicAWEModels.yaml_unset — Function
yaml_unset(value) -> BoolWhether a cell is unset: nothing, or the nothing placeholder a written table uses to leave one row's cell empty in a column the other rows fill.
SymbolicAWEModels.yaml_field — Function
yaml_field(row, field)Optional row field, nothing when the column is absent or yaml_unset.
SymbolicAWEModels.yaml_float_or_nan — Function
yaml_float_or_nan(row, field) -> Float64Optional numeric field as a Float64, NaN when it is absent or nothing.
SymbolicAWEModels.load_yaml_bodies — Function
load_yaml_bodies(data, yaml_to_ref) -> Vector{Body}Build the plain rigid Bodys from a bodies YAML block (empty when the block is absent). Field names mirror the Body constructor: required name, mass, pos, and one of inertia_principal (3-vector) or inertia (3×3); optional type (DYNAMIC/STATIC), transform_idx, vel, Q_b_to_w (4-vector), omega_b, com_offset_b, wing (the parent wing a body-frame damping resolves against), angular_damping, world_frame_damping, body_frame_damping (each scalar or 3-vector), fix_sphere, fix_static, ext_force_w, ext_force_b, ext_moment_b, principal_frame_method.
SymbolicAWEModels.load_yaml_joints — Function
load_yaml_joints(::Type{Joint}, data, key, required, optional; yaml_to_ref)Build two-body joints of type Joint from the key YAML block (empty when the block is absent). Every row needs body_a, body_b and each required scalar; anchor_a/anchor_b (3-vectors) and each optional scalar are read when present. Shared by the TimoshenkoJoint and ElasticJoint loaders — scalar fields from YAML are linear; callable/nonlinear laws are supplied programmatically.
SymbolicAWEModels.load_body_state! — Function
load_body_state!(body, row)Overwrite a freshly constructed body's rigid state from the vel, Q_b_to_w and omega_b columns of its YAML row, leaving each untouched when its column is absent. The wing constructors take no state keywords, so a saved orientation or velocity is restored here instead.
SystemStructure internals
SymbolicAWEModels.resolve_material — Function
resolve_material(label, set; diameter_m, unit_stiffness, unit_damping,
density, youngs_modulus, damping_per_stiffness)Complete the elastic properties of a spring element, returning (diameter_m, unit_stiffness, unit_damping, density).
unit_stiffness [N] and unit_damping [N·s] scale with the cross section, so they describe one element rather than a material. A material shared by elements of different diameter is given as youngs_modulus [Pa] and damping_per_stiffness [s] instead: unit_stiffness = youngs_modulus·π(d/2)² and unit_damping = damping_per_stiffness·unit_stiffness. Giving both forms of the same quantity is an error. What is left NaN comes from set (d_tether, rho_tether, e_tether, rel_damping). A callable unit_stiffness (nonlinear force law) needs an explicit unit_damping.
SymbolicAWEModels.segment_cad_length — Function
segment_cad_length(segment::Segment, points)Compute segment length from endpoint pos_cad positions.
SymbolicAWEModels.segment_world_length — Function
segment_world_length(segment::Segment, points)Compute segment length from endpoint pos_w positions.
SymbolicAWEModels.tether_ordered_point_idxs — Function
tether_ordered_point_idxs(tether, segments)Point indices along the tether, ordered from start_point_idx through each segment's far endpoint to the end point.
SymbolicAWEModels.tether_anchor_free — Function
tether_anchor_free(tether, boundary)Return (anchor_idx, free_idx) for a root tether: the endpoint in boundary (STATIC/winch points) is the anchor, the other is free. Returns (nothing, nothing) if neither endpoint is on a boundary, or if both are (a both-fixed tether cannot be placed; the caller warns and skips it).
SymbolicAWEModels.anchor_body_idxs — Function
anchor_body_idxs(point, timoshenko_joints) -> TupleBody indices whose placement carries point: the node Body it rides (body_idx), both ends of the beam element it rides (joint_idx, a BODY_STATIC point on a Timoshenko centerline), or the body of the RIGID_DYNAMICS wing it is a node of (wing_idx, since wings are bodies). Empty for a point that stands free.
SymbolicAWEModels.beam_body_neighbors — Function
beam_body_neighbors(joint_collections...) -> Dict{Int64, Vector{Int64}}Adjacency of the beam graph: each body index mapped to the bodies it shares a joint with, over every joint in each collection.
SymbolicAWEModels.translated_body_idxs — Function
translated_body_idxs(seeds, bodies, body_neighbors) -> Set{Int64}Bodies that translate rigidly with seeds (the bodies the repositioned points ride): the seeds plus everything reachable from them through body_neighbors. This is what carries beam bodies no point rides at all. Expansion stops at STATIC bodies, which are clamped to the world — a beam with a clamped end deforms rather than translating, so only its free part moves.
SymbolicAWEModels.rigid_point_siblings — Function
rigid_point_siblings(points, wings, timoshenko_joints, root)Map each point index that rides a rigid structure to the set of all points sharing it, so downstream traversal moves them as one unit. Which bodies carry a point comes from anchor_body_idxs; root (from connected_body_groups) collapses bodies tied by beam joints into one component, so all points riding any body in a joint-connected chain are siblings. This is how the two halves of a beam wing — bridged only through the beam, not by inter-point segments — are recognised as one structure.
SymbolicAWEModels.tether_downstream_idxs — Function
tether_downstream_idxs(tether, segments, boundary, from_idx,
anchor_idx, rigid_siblings)Breadth-first set of point indices reachable from from_idx (the tether's free end) through segments outside this tether and through rigid_siblings, stopping at boundary points. These are the points that must translate with the free end when the tether is repositioned. Errors if traversal reaches anchor_idx (a loop back to the anchor).
SymbolicAWEModels.station_tethers_by_overlap — Function
station_tethers_by_overlap(specified, reach)Cluster the specified tethers with a union-find over reach (point indices each tether touches): tethers whose reaches intersect share structure and land in the same cluster. Returns a vector of tether vectors, one per cluster.
SymbolicAWEModels.tether_unit_stiffness — Function
tether_unit_stiffness(tether, segments)Return the common per-unit-length stiffness [N] of the tether's segments. Errors if the segments are not uniform, since the spring inversion in apply_tether_init_forces! assumes a single stiffness.
SymbolicAWEModels.apply_cluster_init_stretched_len! — Function
apply_cluster_init_stretched_len!(cluster, points, segments, bodies,
timoshenko_joints, body_neighbors,
downstream, boundary; prn=true)Reposition one cluster of root tethers so each sits at its init_stretched_len standoff. Each tether contributes the displacement that would move its free end onto the target length along the anchor→free direction; the free end and everything downstream of it are translated by the mean of those displacements, then interior points are redistributed proportionally along each tether. For a multi-tether cluster, logs an @info when prn.
The bodies the moved points ride are translated too, expanded over the beam graph by translated_body_idxs so bodies that carry no point of their own do not stay behind.
SymbolicAWEModels.apply_tether_init_stretched_lens! — Function
apply_tether_init_stretched_lens!(sys_struct::SystemStructure; prn=true)Scale pos_w so each tether with an explicit init_stretched_len sits at that standoff. Call after copy_cad_to_world!. Rest length is derived separately by apply_tether_init_forces!.
Only tethers with one endpoint on a boundary (STATIC or winch point) are placed; that endpoint is the fixed anchor (start or end). Scaling runs from the anchor toward the free end, translating everything downstream of it. A tether with neither endpoint anchored is an error. Roots feeding one structure form a cluster, placed by their mean displacement (length and direction).
Errors if a downstream segment connects back to the anchor.
SymbolicAWEModels.init_unstretched_len — Function
init_unstretched_len(tether, segments) -> SimFloatDerived initial unstretched (rest) length from the tether's current (placed) stretched length stretched = Σ segment lengths:
init_stretch_fracset:len = stretch_frac · stretched(< 1pre-stretch,1neutral,> 1slack).- otherwise from
init_tether_force(default 0):len = stretched · (1 − force / unit_stiffness)(zero-velocity, tension branch). Force 0 giveslen = stretched(no tension).
Errors if both init_stretch_frac and init_tether_force are set, if stretch_frac ≤ 0, if force < 0, if force ≥ unit_stiffness, or if the segments have non-uniform unit_stiffness.
SymbolicAWEModels.apply_tether_init_forces! — Function
apply_tether_init_forces!(sys_struct::SystemStructure)Set every tether's len to its init_unstretched_len. Must be called after segment world lengths are current.
SymbolicAWEModels.joint_endpoint_frames — Function
joint_endpoint_frames(joint, bodies) -> (R_a, R_b, anchor_a_w, anchor_b_w)World rotations of the two connected bodies and the world positions of the joint's two anchors, from the current (placed) poses. Shared by every init_joint_rest!.
SymbolicAWEModels.init_joint_rest! — Function
init_joint_rest!(joint, bodies)Capture joint's rest reference from the current (placed) body poses, so the as-placed (CAD) geometry is unstrained and the joint wrench is exactly zero at initialization. One method per joint type; a new joint type adds a method.
ElasticJoint: rest anchor offset (body-A frame) and rest relative rotationR_a' R_b.TimoshenkoJoint: rest length (if unset) and per-node orientations relative to the corotational element frame.
SymbolicAWEModels.timoshenko_element_frame — Function
timoshenko_element_frame(x_a, x_b, R_a) -> (e1, e2, e3, len)Orthonormal corotational element frame: e1 along the chord from node A to node B, e2/e3 from node A's y-axis projected transverse to the chord. len is the current chord length. Generic over numeric and symbolic inputs.
SymbolicAWEModels.assign_indices_and_resolve! — Function
assign_indices_and_resolve!(components, name_dicts)Assign indices to all components based on their position in the vectors, and resolve all references to indices.
SymbolicAWEModels.resolve_ref — Function
resolve_ref(ref::NameRef, name_dict::Dict{Symbol, Int64}, component_type::String) -> Int64Resolve a reference (name or index) to an index using the name dictionary. If ref is an integer, returns it directly. If ref is a symbol, looks up in dictionary.
SymbolicAWEModels.resolve_ref_spec — Function
resolve_ref_spec(spec, name_dict, component_type) -> Union{Int64, Vector{Int64}, Nothing}Resolve a reference point specification (single ref or vector of refs) to indices.
SymbolicAWEModels.validate_sys_struct — Function
validate_sys_struct(sys_struct::SystemStructure)Check a SystemStructure for configurations that cause initialization failures or numerical problems: warnings for suspicious values, assertions for definite errors.
Validations Performed
- Points: NaN or negative
extra_mass, non-positivetotal_masson DYNAMIC points, NaN position (usually a consequence of zero mass). - Wings: non-positive mass, zero/near-zero or NaN principal inertia on RIGID_DYNAMICS wings, empty station list, NaN position.
- Winches: non-positive, tiny or NaN
inertia_total, non-positivedrum_radiusorgear_ratio. - Segments: diameter outside (0, 1) m, non-positive rest length
l0, non-positive stiffness, negative damping. - Pulleys: zero total length constraint.
- Stations: inconsistent
moment_frac.
SymbolicAWEModels.build_name_dict — Function
build_name_dict(items::Vector) -> Dict{Symbol, Int64}Build a name→index dictionary from a vector of items with optional name fields. Items with name=nothing are skipped. Integer names are converted to Symbols.
SymbolicAWEModels.setup_wing_frame! — Function
setup_wing_frame!(wing, points; prn=true)Compute a wing's body frame (R_b_to_c, pos_cad) and, for RIGID_DYNAMICS, its COM offset and principal inertia, from the wing's structural points and ref points. This is dynamics/geometry only — independent of the aero mode, which does its own mode-specific setup afterwards in setup_aero!.
Without ref points the body frame keeps the CAD orientation (origin at the COM).
SymbolicAWEModels.connected_body_groups — Function
connected_body_groups(n_bodies, joint_collections...) -> Vector{Int64}Union-find over body indices 1:n_bodies, uniting the two bodies of every joint in each collection (each joint exposes body_a_idx/body_b_idx). Returns a root vector mapping each body to its component representative, so all bodies tied into one continuous beam share a root.
SymbolicAWEModels.particle_wing_masses — Function
particle_wing_masses(wing, stations, points, bodies, root)
-> (point_mass, body_mass)Mass associated with a PARTICLEDYNAMICS wing, split by source. `pointmasssums theextramassof the wing's member points;bodymasssums the mass of every non-wing body sharing a beam component (root, from [connectedbodygroups](@ref)) with the wing's structure. A particle wing carries its mass on its section bodies — most of which are beam-internal and ride no point — sobody_mass` is the usual source; both being nonzero means gravity is counted twice (points and bodies).
The wing's components are seeded from its stations' member/flap bodies and the bodies its member points ride, then expanded over the joint graph. Members are the union of the wing's station points, falling back to wing_frame_member points when the wing has no stations.
SymbolicAWEModels.finalize_particle_wing_mass! — Function
finalize_particle_wing_mass!(wing, stations, points, bodies, set, root)Set a PARTICLEDYNAMICS wing's bookkeeping mass from its member points and the section bodies of its beam component (see [`particlewing_masses](@ref)), warning when the wing is massless or when mass is counted twice (points and bodies). Falls back to distributingset.mass` over the member points when neither source carries mass. Deferred until point/station→body and joint→body references resolve.
SymbolicAWEModels.compute_station_geometry! — Function
compute_station_geometry!(wing, stations, points)For each of wing's stations with an unset chord, derive its leading-edge position, chord vector, and spanwise airfoil axis from the nearest VSM refined section (body frame). Fills the chord for any station left unset.
SymbolicAWEModels.setup_particle_point_mapping! — Function
setup_particle_point_mapping!(wing, points, stations)For a VSM PARTICLE_DYNAMICS wing, build the structural↔panel point mapping and LE/TE wing_segments if not already set. Errors if the required body-frame z_ref_points/y_ref_points are missing.
SymbolicAWEModels.identify_wing_segments — Function
identify_wing_segments(wing_points; stations=nothing, wing_station_idxs=nothing)Identify wing segments (LE/TE pairs) from wing nodes.
When stations and wing_station_idxs are provided, uses station point_idxs to determine LE (point_idxs[1]) and TE (point_idxs[end]) for each section. Falls back to a consecutive-pair heuristic (sorted by point index) when stations are unavailable.
In both paths an x-coordinate check swaps LE/TE if needed (LE has smaller pos_cad[1]).
Arguments
wing_points::AbstractVector{Point}: wing nodes for a wing.
Keyword Arguments
stations::Union{Nothing, AbstractVector{Station}}: All stations in the system (indexed bywing_station_idxs).wing_station_idxs::Union{Nothing, AbstractVector{<:Integer}}: Indices intostationsbelonging to this wing.
Returns
Vector{Tuple{Int64, Int64}}: (lepointidx, tepointidx) pairs.
SymbolicAWEModels.match_aero_sections_to_structure! — Function
match_aero_sections_to_structure!(wing, points; stations)Reconcile a wing's aerodynamic sections with its structural geometry.
RIGIDDYNAMICS wings own their aero panel geometry (mesh- or YAML-defined) and keep it; only the station→section mapping (`wing.wingsegments) is recorded. PARTICLE_DYNAMICS wings deform with their structural points, so each unrefined section is rebuilt onto its structural LE/TE pair: a 1:1 copy when counts match, otherwiseusepriorpolarand existingrefined_sections` are required to preserve polars.
Keyword Arguments
stations::AbstractVector{Station}: Stations used for LE/TE identification viaidentify_wing_segments.
SymbolicAWEModels.compute_spatial_station_mapping! — Function
compute_spatial_station_mapping!(the_wing, stations, points)Partition the wing's unrefined VSM sections among its stations by spatial proximity: each unrefined section is assigned to the single closest station (by distance between section centre and station centre, both in body frame).
n_stations == n_unrefined gives a 1:1 mapping; with fewer stations one may own several adjacent sections and drive them all as a rigid unit with its single twist DOF. More stations than sections is rejected.
SymbolicAWEModels.copy_cad_to_world! — Function
copy_cad_to_world!(points, bodies; update_vel=true)Copy CAD geometry to world frame for ALL points and bodies. Sets pos_w = pos_cad (and Q_b_to_w to initial CAD orientation for bodies). Must be called before reinit!(transforms, ...).
SymbolicAWEModels.adjust_vsm_panels_to_origin! — Function
adjust_vsm_panels_to_origin!(vsm_wing, origin_offset)Adjust VSM panel positions when body frame origin changes.
When RIGIDDYNAMICS wings are loaded from YAML, the panel positions in aerogeometry.yaml are specified in an absolute body frame. However, the body frame origin is adjusted to the mean position of all wing-nodes. This function updates all panel positions to be relative to the new origin by subtracting the offset.
Arguments
vsm_wing: VortexStepMethod.Wing with sections to adjustorigin_offset: Vector [x, y, z] to subtract from panel positions
SymbolicAWEModels.apply_aero_z_offset! — Function
apply_aero_z_offset!(vsm_wing, aero_z_offset)Apply z-axis offset to VSM panel positions in body frame.
For RIGID_DYNAMICS wings, this shifts the aerodynamic center of pressure in the positive z-direction (body frame) to adjust the moment arm. This is applied AFTER the COM adjustment.
Arguments
vsm_wing: VortexStepMethod.Wing with sections to adjustaero_z_offset: Distance to shift panels in +z direction [m]
SymbolicAWEModels.calc_particle_dynamics_wing_frame — Function
calc_particle_dynamics_wing_frame(points, z_ref_points, y_ref_points, origin)Calculate Rbto_w rotation matrix and origin position from structural point positions.
Algorithm
- Weighted ref point positions
- Z-axis (normal): zp1 → zp2
- X-axis (chord): Y_temp × Z
- Y-axis (span): Z × X (orthogonal, right-handed)
- Origin from weighted
originref points
SymbolicAWEModels.principal_frame — Function
principal_frame(inertia) -> (inertia_principal, R_to_principal)Diagonalise a 3×3 symmetric inertia tensor. Returns the principal moments inertia_principal and the rotation R_to_principal mapping the input frame to the principal frame, so that R · inertia · R' = Diagonal(inertia_principal).
The principal axes are permuted and signed to align as closely as possible with the input-frame axes: a near-diagonal tensor gives R ≈ I, and a body symmetric about a coordinate plane gives a pure rotation about the normal of that plane. R is always a proper rotation (det = +1).
SymbolicAWEModels.calc_inertia_y_rotation — Function
calc_inertia_y_rotation(I_tensor) -> (inertia_principal, R_to_principal)Diagonalize a 3×3 inertia tensor via a closed-form rotation about the Y axis, returning (moments, R) in the same format as principal_frame.
The rotation angle is θ = atan(2·I₁₃, I₁₁ − I₃₃) / 2, zeroing out the I[1,3] / I[3,1] cross terms while leaving the Y axis unchanged.
Unlike principal_frame (full 3-axis eigendecomposition + permutation search), this is a unique, closed-form solution — the right choice for a wing symmetric about the XZ-plane (no Y products of inertia), where the generic permutation search is ambiguous when two principal moments are close.
SymbolicAWEModels.init_principal_state! — Function
init_principal_state!(obj)Derive the principal-frame ODE state (com_w, com_vel, Q_p_to_w, ω_p) from the body-frame initial conditions (pos_w, vel_w, Q_b_to_w, ω_b) of a rigid body or RIGID_DYNAMICS wing, with R_p_to_w = R_b_to_w * R_b_to_p'. Shared by init_rigid_body! and the rigid branch of init_principal_frame! (for a RIGID_DYNAMICS wing R_b_to_p = R_p_to_c' * R_b_to_c, so the two agree).
SymbolicAWEModels.is_wing — Function
A wing is a Body carrying aerodynamics; Body{AeroNone} is a plain body.
SymbolicAWEModels.wing_dynamics — Function
wing_dynamics(dynamics_type::WingType) -> Type{<:WingDynamics}Map a WingType to its WingDynamics tag.
SymbolicAWEModels.WingDynamics — Type
abstract type WingDynamicsPhantom tag for a wing's dynamics, carried as the second type parameter of Wing. It mirrors the dynamics_type field but lifts the rigid/particle distinction into the type domain so aero (and other) methods can dispatch on RigidWing/ParticleWing instead of branching on dynamics_type. Subtypes: RigidDynamics, ParticleDynamics.
SymbolicAWEModels.RigidDynamics — Type
Dynamics tag for a RIGID_DYNAMICS wing (integrated 6-DOF body).
SymbolicAWEModels.ParticleDynamics — Type
Dynamics tag for a PARTICLE_DYNAMICS wing (frame fitted from points).
SymbolicAWEModels.rotate_vsm_sections! — Function
rotate_vsm_sections!(vsm_wing, R)Rotate all VSM section LE/TE points by rotation matrix R.
Used during initialization to transform sections from CAD frame to body frame. After the first step, refresh_aero!() updates positions from pos_b (already in body frame).
SymbolicAWEModels.AERO_SCALE_CHORD — Constant
const AERO_SCALE_CHORD = 0.0Baseline chord-based aero scaling for PARTICLEDYNAMICS wings; effective multiplier is `1 + (wing.aeroscale_chord or this default)`.
SymbolicAWEModels.COLLOCATION_CHORD_FRAC — Constant
const COLLOCATION_CHORD_FRAC = 0.75Chordwise station, as a fraction from LE to TE, that a section's inflow is sampled at. Thin-airfoil theory enforces flow tangency at the 3/4-chord point, so sampling there lets a chord twist rate reach alpha as downwash and be damped. Sampling at mid-chord instead leaves the twist rate invisible to the aero, or, for a chord pivoting between mid- and 3/4-chord, damped with the wrong sign.
SymbolicAWEModels.body_vsm_engine — Function
body_vsm_engine(body::Body, sym::Symbol)Return the body's VSM engine; errors if the aero mode carries none (e.g. AeroNone/AeroPlate).
SymbolicAWEModels.expand_auto_tethers! — Function
expand_auto_tethers!(points, segments, tethers, set)For Route 2 tethers (auto-generation), create intermediate DYNAMIC points and segments, each carrying the tether's material and its compression_frac/compression_damping_frac. Must be called before assign_indices_and_resolve!.
Detects Route 2 tethers by checking start_point_ref !== nothing and segment_refs names not yet present in segments.
SymbolicAWEModels.resolve! — Function
resolve!(ref_pt::WeightedRefPoints, name_dict, type)Resolve symbolic refs to integer indices, filling ref_pt.ids. No-op if refs is empty (already resolved).
SymbolicAWEModels.validate_weights! — Function
Warn and normalize if weights don't sum to 1.
SymbolicAWEModels.mark_wing_nodes! — Function
mark_wing_nodes!(points, stations)Set point.is_wing_node for every point that is a member of some station. A wing's aerodynamic-surface structural points carry their wing membership through station membership (there is no WING dynamics type); this flag, derived once here, drives the per-point aero and wing-frame equations. Requires station.point_idxs to be resolved first.
SymbolicAWEModels.wing_frame_member — Function
wing_frame_member(point, wing_idx) -> BoolWhether point contributes to wing wing_idx's mass, COM and body frame. Its aerodynamic-surface nodes (is_wing_node) plus any BODY_STATIC structural point sharing its wing_idx — e.g. a rigid wing's attachment points (like the KCU) that ride the wing body without being aero-surface members. Bridle points riding a beam carry wing_idx == 0, so they are excluded.
SymbolicAWEModels.distribute_mass_over_points! — Function
distribute_mass_over_points!(points, point_idxs, wing, total_mass)Split total_mass equally across the wing's point_idxs (writing each point's extra_mass) and record it as wing.mass. Used for a PARTICLE wing given a lumped set.mass rather than per-point masses.
NamedCollection internals
SymbolicAWEModels.names — Function
names(nc::NamedCollection)Return a vector of all symbolic names in order of their indices. Names are nothing for unnamed items.
SymbolicAWEModels.get_idx — Function
get_idx(nc::NamedCollection, name::Symbol)Get the numeric index for a given symbolic name.
SymbolicAWEModels.get_name — Function
get_name(nc::NamedCollection, idx::Integer)Get the symbolic name for a given numeric index, or nothing if unnamed.
Base.values — Method
Get all values (same as iterating).
Base.haskey — Method
Check if a symbolic name exists in the collection.
Base.setindex! — Method
Set item by numeric index.
Base.setindex! — Method
Set item by symbolic name.
Equation builders
SymbolicAWEModels.tether_eqs! — Function
tether_eqs!(eqs, tethers; len, spring_force)Generate equations for tether stretched length and average spring force.
Arguments
eqs: Accumulating equation vector.tethers: Collection of Tether objects.len: Symbolic segment length variable.spring_force: Symbolic segment spring force variable.
Returns
- Updated
eqsvector with tether equations.
SymbolicAWEModels.pulley_eqs! — Function
pulley_eqs!(eqs, defaults, pulleys, segments, params;
spring_force, pulley_len, pulley_vel)Generate equations for pulley dynamics (rope distribution over pulleys).
The rope mass and the split dynamics come from the shared pulley_rope_mass and pulley_split_eqs; pulley_force (the tension imbalance driving the split) and pulley_acc (that imbalance over the rope mass) are emitted as diagnostics on top.
Arguments
eqs,defaults: Accumulating vectors for the MTK system.pulleys: Collection of Pulley objects.segments: Collection of Segment objects.spring_force: Symbolic segment spring force variable.pulley_len,pulley_vel: Symbolic pulley state variables.l0: Symbolic segment rest lengths (segment 1'sl0is bound to the pulley length).
Returns
- Tuple
(eqs, defaults)with updated equation vectors.
SymbolicAWEModels.winch_eqs! — Function
winch_eqs!(eqs, defaults, winches, tethers, segments, points,
sys_struct, params;
spring_force_vec, set_values, tether_len, tether_vel,
winch_vel, winch_acc, winch_force_vec, winch_friction)Generate equations for winch motor dynamics and per-tether length state, and return the list of ODESystem subsystems to attach to the parent system.
For each winch:
- Sum spring force vectors of the segments meeting the winch point (sign-aware via segment
point_idxs) →winch_force_vec. - Instantiate the winch component via
winch_component(winch.model, …). - Validate the connector contract with
validate_winch_component. - Bind connectors to the parent variables (including
subsys.len ~ mean(tether_len[tether_idx] for tether_idx in winch.tether_idxs)) and integrateD(winch_vel) = ifelse(brake > 0.5, 0, winch_acc). Whenwinch.speed_controlledis true,winch_accis forced to 0 (ignoringsubsys.acc) so velocity is prescribed viawinch.vel.
For each tether, tether_vel is the reeling speed its length follows and its segments' dampers read as a rest-length rate:
- With winch:
tether_vel = ifelse(brake > 0.5, 0, winch_vel)andD(tether_len) = tether_vel, so the length is integrator state. - Without winch:
tether_vel = 0andtether_len ~ params.tethers[i].len, a parameter, so writingtether.lenretrims a running simulation without areinit!.
SymbolicAWEModels.station_eqs! — Function
station_eqs!(eqs, defaults, stations, bodies, params;
R_b_to_w, fix_wing, twist_angle, twist_ω, station_aero_moment,
point_force, station_y_airf, station_chord, station_le_pos)Generate equations for deformable wing station twist dynamics. The couple each of a surface's points delivers to its hinge is built from the shared twist_bridle_couple.
Arguments
eqs,defaults: Accumulating vectors for the MTK system.stations: Collection of Station objects (deformable wing sections).bodies: Collection ofBodyobjects (the station owners).R_b_to_w: Symbolic rotation matrix (body to world).fix_wing: Symbolic boolean for fixing wing dynamics.twist_angle,twist_ω: Symbolic twist state variables.station_aero_moment: Symbolic aerodynamic moment on stations.point_force: Symbolic point force variable.station_y_airf,station_chord,station_le_pos: Symbolic station geometry variables.
Returns
- Tuple
(eqs, defaults)with updated equation vectors.
SymbolicAWEModels.validate_station_modes — Function
validate_station_modes(stations, bodies)Check that each station's twist mode is coherent with its owning body's dynamics and its point count. Errors loudly on an inconsistent combination:
DYNAMICtwist is an added rigid-body deformation DOF and needs a bridle couple, so it requires aRIGID_DYNAMICSbody and ≥2 points.- A 1-point station has no bridle couple to oppose a twist moment, so its only coherent twist is prescribed → must be
STATIC. STATICtwist is prescribed (default 0). On a rigid wing it imposes the section twist; on aPARTICLE_DYNAMICSwing the free points carry the deformation, so a multi-pointSTATICsurface is an inert aero-section membership marker.
SymbolicAWEModels.body_ride_eqs — Function
body_ride_eqs(point, body, force_on_body, params;
pos, vel, acc, body_pos_w, body_R_b_to_w, body_com_w,
body_com_vel, body_ω_b, body_force, body_moment)Kinematic pose equations for a point rigidly anchored to body at anchor_b, and the load it feeds back to that body. The point tracks the body's rigid motion (vel = com_vel + ω×arm), and its force_on_body is applied at the anchor with the moment about the body COM. Shared by the BODY_STATIC single-body ride and PARTICLE-wing beam-node coupling; mutates body_force/body_moment in place and returns the pos/vel/acc equations.
SymbolicAWEModels.beam_hermite_ride_eqs — Function
beam_hermite_ride_eqs(point, force_on_point, s, params; kwargs...)Kinematics of a point that rides its TimoshenkoJoint's corotational cubic-Hermite centerline at beam_frac (transverse deflection from the two end slopes, plus a frame-carried beam_offset_b), and the load it feeds to the two end bodies split by axial fraction. Mutates body_force/body_moment in place; returns the point's pos/vel/acc equations. Shared by beam-anchored bridle points and beam-anchored wing-node aero receivers, so both track the deformed beam identically.
SymbolicAWEModels.point_damping_accel — Function
point_damping_accel(point, params, R_b_to_w, wing_idx, vel_w, vel_diff_w)Per-mass damping acceleration for a DYNAMIC point. Each frame's term is built only when its coefficient is set; a nothing coefficient keeps that term out of the equation entirely (no zero-valued parameter to prune). The body-frame term also needs a wing frame — pass vel_diff_w = nothing (point velocity relative to its wing) to skip it when no wing is available.
SymbolicAWEModels.broadcast_damping — Function
broadcast_damping(damping) -> KVec3Widen a scalar damping coefficient to an isotropic per-axis vector, or pass a length-3 one through.
SymbolicAWEModels.assign_damping! — Function
assign_damping!(components, field, damping, idxs)Write damping (scalar or 3-vector) into field of components[idx] for every idx in idxs. A fresh vector reaches each component, so later edits to one never leak into the others.
SymbolicAWEModels.segment_rest_length_eqs — Function
segment_rest_length_eqs(segment, pulleys, tethers, params;
l0, pulley_len, pulley_vel, tether_len, tether_vel)Rest-length equations for one segment, and the rate that rest length moves at. A pulley member takes either the pulley state pulley_len or the remainder sum_len - pulley_len, a tether member takes tether_len / n_segments, and any other segment keeps its fixed l0 parameter. Returns (eqs, rest_len_rate), the rate carrying the sign of the state it follows and being zero for a fixed rest length; segment_load_terms needs it to damp the extension rather than the endpoint separation. Errors when a segment belongs to more than one pulley or more than one tether.
Plate aerodynamics internals
SymbolicAWEModels.load_plate_wing — Function
load_plate_wing(row, idx, data, set, wing_type, aero_mode,
yaml_to_ref, yaml_parse_ref_points,
yaml_parse_origin, stations)Load a flat-plate wing from a YAML wing row + surfaces block. Each surface becomes a 1-point STATIC Station appended to stations; the wing references them by name. CL/CD interpolations come from Settings polar data.
SymbolicAWEModels.plate_corners — Function
plate_corners(station, point_pos_w, R_b_to_w) -> NTuple{4, Vector}World-frame corners of a flat-plate section's display quad. The section's structural point sits at quarter chord; the quad is a square of side sqrt(area) spanned by the twisted chord direction and y_airf, so the quad area matches the section's area.
Aero-mode interface
SymbolicAWEModels.vsm_engine — Function
vsm_engine(mode::AbstractAeroModel) -> Union{Nothing, VSMEngine}The mode's VSMEngine (VSM geometry + linearization state), or nothing for modes without one. After construction every AbstractVSMAero carries an engine (require_vsm_engine enforces this in setup_aero!); nothing only occurs for non-VSM modes and bare pre-construction markers. Used for the wing's VSM property forwarding and the VSM-settings loading; per-mode behaviour goes through the dispatch hooks instead.
SymbolicAWEModels.has_vsm_engine — Function
has_vsm_engine(mode::AbstractAeroModel) -> Booltrue if vsm_engine(mode) returns an engine. Derived, not a dispatch point — implement vsm_engine instead.
SymbolicAWEModels.require_vsm_engine — Function
require_vsm_engine(mode, wing) -> VSMEngineReturn the mode's VSMEngine, erroring with construction advice when it is missing (a bare AeroDirect()/AeroLinearized() marker attached to a wing that was not built via VSMWing). Called once, in setup_aero!; after construction every AbstractVSMAero is guaranteed to carry an engine.
SymbolicAWEModels.couples_to_sections — Function
couples_to_sections(mode::AbstractAeroModel) -> Booltrue if the mode needs per-section stations (auto-creation and aero-section matching). VSM modes (AbstractVSMAero) do.
SymbolicAWEModels.has_vsm_wing — Function
has_vsm_wing(sys_struct::SystemStructure) -> Booltrue if any wing carries a VSM aero mode (AbstractVSMAero) whose state must be refreshed each vsm_interval. When false, refresh_aero! is a no-op for every wing, so callers skip it and the trailing parameter sync.
SymbolicAWEModels.provides_aero_override — Function
provides_aero_override(mode::AbstractAeroModel) -> Booltrue if the mode supplies frozen body-frame force/moment overrides read by the compiled RHS (the stored-force path of AeroDirect).
SymbolicAWEModels.aero_mode_tag — Function
aero_mode_tag(mode::AbstractAeroModel) -> StringShort identifier for the mode in the compiled-model cache filename. Required: the AbstractAeroModel fallback errors, so every aero mode must declare its own tag (no silent default that could collide two distinct modes on one cache file). Built-ins: "lin", "dir", "cont", "press", "none", "plate".
SymbolicAWEModels.calc_side_slip — Function
calc_side_slip(wing) -> SimFloatSide-slip angle [rad] from the body-frame apparent wind. Pure geometry — the same formula for every aero mode, so it does not dispatch.
SymbolicAWEModels.validate_aero_component — Function
validate_aero_component(subsys, wing)Check the built aero subsys exposes the connectors the wiring layer needs for the wing's dynamics_type; error naming the missing connector otherwise.
SymbolicAWEModels.validate_aero_structure — Function
validate_aero_structure(mode, wing, points; prn=false)Check structural invariants the mode's compiled equations rely on (run at build). Default no-op; VSM PARTICLE_DYNAMICS wings verify each unrefined section has its LE and TE structural point mapped (interior chord control points are allowed and need no mapping).
validate_aero_structure(mode::AeroPressure, wing, points; prn=false)Check the generic section↔point mapping plus the surface→point traction map.
SymbolicAWEModels.remake_aero! — Function
remake_aero!(mode, wing, set, vsm_set, points, stations)Rebuild the mode's aero engine from set/vsm_set (the remake_vsm path in reinit!, used after editing settings). Default no-op; VSM modes recreate the VSM wing/aero/solver, re-transform sections to the body frame, re-match aero sections to structure, and rebuild the station / point mappings.
remake_aero!(mode::ContinuousAero, wing, set, vsm_set, points,
stations)The generic VSM remake plus a rebuild of the mesh maps (the VSM wing geometry objects are replaced, invalidating the panel indexing).
remake_aero!(mode::AeroPressure, wing, set, vsm_set, points, stations)Rebuild the VSM objects from edited settings via the generic particle remake (coarsen onto the structural stations, refine, rebuild point_to_vsm_point), then rebuild the strut-interpolation caches, surface→point map and frozen buffers/polars.
SymbolicAWEModels.setup_aero! — Function
setup_aero!(mode, wing, points, stations; prn=false, vsm_set=nothing)Construction-time aero setup for wing, dispatched on its aero mode (default no-op). VSM modes transform the VSM panels into the body frame and, for section-coupled wings, auto-create stations, match aero sections to structure, and build the station / structural↔panel mappings. A custom mode adds a method to participate in construction without editing the SystemStructure constructor. Runs after setup_wing_frame! (which sets the body frame). vsm_set is the model's VSMSettings, for a mode that reads how its sections were generated rather than only the geometry they produced.
setup_aero!(mode::ContinuousAero, wing, points, stations; prn=false,
vsm_set=nothing)The generic VSM particle setup plus the ContinuousAero mesh maps (build_mesh_maps!).
setup_aero!(mode::AeroPressure, wing, points, stations; prn=false,
vsm_set=nothing)Run the generic particle VSM setup (rebuild the unrefined sections onto the structural LE/TE stations, refine, build point_to_vsm_point), freeze the strut-interpolation caches (build_section_interp), then build the surface→point traction map (build_station_point_map!) and size the frozen buffers/polars (init_pressure_buffers!). PARTICLE_DYNAMICS only.
Live polars are sampled at the airfoil: settings vsm_set carries, so a deformed section is re-solved on the network its tables were generated with.
SymbolicAWEModels.attach_engine! — Function
attach_engine!(mode, engine) -> modeAttach a freshly built VSMEngine to a VSM aero mode during wing construction. Built-in modes reconstruct (so the concrete engine type lands in the wing's type parameter, removing the abstract-engine dispatch from the RHS); the default mutates a custom mode in place.
SymbolicAWEModels.resize_aero_state! — Function
resize_aero_state!(mode, wing)Resize the mode's per-wing aero state after wing.station_idxs is resolved (name resolution can change the station count the initial sizing estimated from n_unrefined). Default no-op; VSM modes resize aero_y/aero_x/aero_jac for RIGID_DYNAMICS wings.
SymbolicAWEModels.init_aero_state! — Function
init_aero_state!(mode, wing, va_b_init)Initialize the mode's aero state from the initial body-frame apparent wind va_b_init (runs in update_sys_struct!, before the first refresh). Default no-op; VSM modes write the operating-point angles α, β into aero_y.
SymbolicAWEModels.normalized_inertia — Function
normalized_inertia(mode::AbstractAeroModel, wing, points)
-> (com_cad, inertia)Normalized (per-unit-mass) inertia of the wing body about its COM in the CAD frame, with inertia in [m²] — multiply by the wing's mass for the physical tensor [kg·m²]. inertia is nothing when there is no mass to normalize by. The default normalizes the wing nodes' point-mass inertia (normalized_point_inertia); VSM modes with an ObjWing mesh return the per-unit-mass mesh tensor as-is (its COM is -T_cad_body) and fall back to the point masses otherwise.
SymbolicAWEModels.normalized_point_inertia — Function
normalized_point_inertia(wing, points) -> (com_cad, inertia)Per-unit-mass inertia of the wing's wing nodes treated as point masses (extra_mass), normalized by their total mass. Exact under the construction invariant wing.mass == sum of wing-node masses (the constructor distributes set.mass onto the points). With zero total mass, com_cad is the unweighted centroid and inertia is nothing.
SymbolicAWEModels.n_aero_log_points — Function
n_aero_log_points(mode, wing) -> IntNumber of extra SysState log slots the mode contributes for wing (visualization geometry such as panel corners). Default 0; VSM modes log 4 corners per panel. Must match what write_aero_log_points! writes.
n_aero_log_points(::AeroPlate, wing) -> Int4 quad corners per flat-plate section (plate_corners).
SymbolicAWEModels.write_aero_log_points! — Function
write_aero_log_points!(mode, wing, sys_struct, sys_state, point_idx,
zoom) -> IntWrite the mode's log points (world frame, scaled by zoom) into sys_state.X/Y/Z starting after point_idx; return the last index written. Default writes nothing; VSM modes write the panel corners, AeroPlate writes each section's display quad. Flap deflections are logged separately, per aero segment, into sys_state.flap_angle (see write_flap_deflections!).
write_aero_log_points!(::AeroPlate, wing, sys_struct, sys_state,
point_idx, zoom) -> IntLog each flat-plate section's display quad (plate_corners).
SymbolicAWEModels.write_body_state! — Function
write_body_state!(ss, body, slot, frame, zoom)Store a body's pose and rates: position and velocity in X/Y/Z and VX/VY/VZ at slot, orientation and turn rate in orients and turn_rate_x/y/z at frame. Wings occupy both a wing slot and a body slot, so both are written.
SymbolicAWEModels.read_aero_log_points! — Function
read_aero_log_points!(mode, wing, sys_struct, sys_state, point_idx) -> IntInverse of write_aero_log_points!: restore the mode's state from the logged points starting after point_idx; return the last index consumed (the slots must be skipped even when unused). Default consumes nothing; VSM PARTICLE_DYNAMICS modes read the panel corners back (rigid wings recompute panels from twist instead and only skip their slots). Flap deflections are restored separately from sys_state.flap_angle (see restore_flap_delta!).
read_aero_log_points!(::AeroPlate, wing, sys_struct, sys_state,
point_idx) -> IntSkip the quad-corner slots: plate corners are derived from the restored structural point positions, so nothing is read back.
SymbolicAWEModels.restore_aero_twist! — Function
restore_aero_twist!(mode, wing, stations)Re-apply the (already restored) station angles to the mode's geometry when loading a SysState log frame. Default no-op; VSM RIGID_DYNAMICS modes deform the unrefined sections and reinit the panels.
SymbolicAWEModels.plot_wing_aero! — Function
plot_wing_aero!(ax, sys, wing, mode::AbstractAeroModel;
use_observables=false, geometry_obs=nothing)Render wing's aero geometry into ax, dispatched on its aero mode: VSM modes plot their panels via VortexStepMethod's recipe, flat-plate modes draw their section quads in the same style (red mesh, black borders). The default draws nothing — add a method for a custom mode to render its own geometry. With use_observables, the plot re-reads the live structure on every geometry_obs trigger (live plots and replay). Returns the plot object, or nothing when nothing was drawn. Defined in the Makie extension.
SymbolicAWEModels.update_wing_aero_plot! — Function
update_wing_aero_plot!(wing, mode::AbstractAeroModel)Per-frame update of wing's aero plot, dispatched on its aero mode. Default no-op; VSM modes push the current pose into the panel-mesh observables. Modes drawn through the geometry observable (flat-plate quads) need no update here. Defined in the Makie extension.
SymbolicAWEModels.load_wing — Function
load_wing(mode::AbstractAeroModel, row, idx, data, set, wing_type, vsm_set,
yaml_to_ref, yaml_parse_ref_points, yaml_parse_origin, stations)Build a wing from a parsed YAML row, dispatched on its aero mode. The default (VSM-backed modes) builds a VSMWing; AeroPlate builds a flat-plate wing via load_plate_wing. Add a method to load a wing for a custom aero mode.
SymbolicAWEModels.restore_flap_delta! — Function
restore_flap_delta!(mode, wing, sys_state)Restore each VSM panel's flap deflection δ from sys_state.flap_angle (mapped through the panel→station index) when loading a SysState frame, so a replayed frame shows the logged flap state. Default no-op; AeroPressure restores its PARTICLE_DYNAMICS panels.
SymbolicAWEModels.restore_live_shape! — Function
restore_live_shape!(mode, wing, points)Re-derive the mode's live airfoil shapes from a replayed log frame's structure. Default no-op; AeroPressure rebuilds them when it is on live polars.
restore_live_shape!(mode::AeroPressure, wing, points)Re-derive every panel's deformed airfoil from a replayed log frame, so the shape a plot draws is the one that frame's structure gives rather than the one the last live solve left on the panels. Only the shape is rebuilt: a replayed frame is never solved, so its polars would say nothing and the network pass they cost would buy nothing. No-op unless the wing is actually on live polars.
SymbolicAWEModels.write_flap_deflections! — Function
write_flap_deflections!(sys_state, sys_struct)Write each station's flap deflection δ [rad] into sys_state.flap_angle (indexed by station idx, via station_deltas). No-op when the state carries no flap slots (D == 0).
SymbolicAWEModels.n_flap_deflections — Function
n_flap_deflections(sys_struct) -> IntNumber of aero segments logged in SysState.flap_angle: one flap deflection δ per station. Sets the D type parameter of the model's SysState.
VSM and aerodynamics internals
SymbolicAWEModels.refresh_rigid_aero! — Function
refresh_rigid_aero!(mode, wing, am, stations; vsm_min_wind=0.5)Refresh a RIGID_DYNAMICS wing's aero state, dispatched on its aero mode:
AeroNone/ any non-VSM mode → no-op (fallback).AeroLinearized→ compute the baseline coefficients (rigid_aero_baseline!) and theForwardDiffJacobiand(coeffs)/d(inputs)intowing.aero_jac.AeroDirect→ compute the baseline coefficients and apply the frozen body-frame force/moment; belowvsm_min_windeverything is zeroed.
refresh_rigid_aero!(::AeroDirect, wing, am, stations; vsm_min_wind=0.5)Direct rigid-wing refresh. Computes the baseline coefficients and applies the resulting frozen body-frame force/moment (apply_direct_forces!), which the RHS holds constant until the next refresh. Below vsm_min_wind the coefficients, Jacobian, force, moment, and per-station moments are zeroed.
refresh_rigid_aero!(::AeroLinearized, wing, am, stations; vsm_min_wind=0.5)Linearized rigid-wing refresh. Computes the baseline wind-axis coefficients at the operating point (rigid_aero_baseline!), then the ForwardDiff Jacobian d(coeffs)/d(inputs) and stores it in wing.aero_jac. The compiled RHS uses that Jacobian to reconstruct forces via a first-order Taylor expansion about the operating point. wing.aero_force_b/aero_moment_b are set to the operating-point force so the reported value tracks the VSM solve; between refreshes update_sys_struct! overwrites it with the RHS-applied linearized force.
SymbolicAWEModels.refresh_particle_aero! — Function
refresh_particle_aero!(mode, wing, points, va_point_b_vals;
vsm_min_wind=0.5, cold_start=false)Refresh a PARTICLE_DYNAMICS wing's aero state, dispatched on its aero mode:
AeroNone/ any non-VSM mode → no-op (fallback).AeroDirect→ full nonlinear VSM solve with per-section apparent wind, then distribute panel forces onto the wing's structural points (distribute_panel_forces_to_points!); belowvsm_min_windthe point forces are zeroed.AeroLinearized→ unsupported (errors).
cold_start forwards to safe_vsm_solve!.
refresh_particle_aero!(::AeroDirect, wing, points, va_point_b_vals;
vsm_min_wind=0.5, cold_start=false)Direct particle-wing refresh. Runs the full nonlinear VSM solve using each section's apparent wind (averaged from its LE/TE point velocities in va_point_b_vals), then distributes the resulting panel forces onto the wing's structural points (distribute_panel_forces_to_points!). Below vsm_min_wind the point forces are zeroed. A failed solve throws (safe_vsm_solve!) before any point force is written.
refresh_particle_aero!(::AeroLinearized, wing, points, va_point_b_vals;
vsm_min_wind=0.5, cold_start=false)Unsupported: AeroLinearized is not implemented for PARTICLE_DYNAMICS wings and errors. Use AeroDirect (per-point nonlinear VSM) for particle wings.
refresh_particle_aero!(::ContinuousAero, wing, points, va_point_b_vals;
vsm_min_wind=0.5, cold_start=false)Refresh: update the VSM geometry from the structure, set the per-panel apparent wind (set_refined_panel_va!), solve and freeze the induced velocity. The forces are re-derived symbolically each RHS step; calc_forces! runs only so sol (alpha_dist, f_body_3D) is not left stale. Below vsm_min_wind the induced velocity is zeroed.
refresh_particle_aero!(mode::AeroPressure, wing, points, va_point_b_vals;
vsm_min_wind=0.5, cold_start=false)Solve at the per-panel apparent wind the symbolic RHS uses (set_refined_panel_va!), freezing the induced velocity and the per-node surface traction pattern (freeze_traction_pattern!). Point forces are re-derived symbolically each RHS step and read back into each node's aero_force_b. Below vsm_min_wind all frozen buffers are zeroed, the per-point offsets included, so no stale constant force survives.
SymbolicAWEModels.count_aero_log_points — Function
count_aero_log_points(wings) -> IntTotal extra SysState log slots contributed by the wings' aero modes (n_aero_log_points): 4 per VSM panel for VSM modes, 4 per section quad for flat-plate wings, 0 by default.
SymbolicAWEModels.build_point_to_vsm_point_mapping — Function
build_point_to_vsm_point_mapping(wing_segments)Invert the station-derived wing_segments — (le_point_idx, te_point_idx) per unrefined section — into the structural point → (section_idx, :LE/:TE) map.
Each unrefined section contributes exactly its LE and TE structural points. Any interior points on the same station (chord control points on the beam) are not aero station points and are simply absent from the map; there is no constraint on the total wing-node count.
SymbolicAWEModels.update_vsm_wing_from_structure! — Function
update_vsm_wing_from_structure!(wing::Body, points::AbstractVector{Point})Update VSM section points (LE/TE) directly from structural point positions, closing the two-way coupling structural deformation → VSM sections → aero forces.
Each structural wing node maps 1:1 onto a VSM section point through wing.point_to_vsm_point, and writes it its body-frame position pos_b = R_b_to_w' * (pos_w - origin).
Notes
- Section points are stored in body frame coordinates
wing.R_b_to_wandwing.pos_ware updated each timestep from structural geometry (symbolic equations)- To get world coordinates:
world_pos = wing.R_b_to_w * section.LE_point + wing.pos_w
Arguments
wing::Body: Wing with PARTICLE_DYNAMICS typepoints::AbstractVector{Point}: All structural points (filtered to this wing's nodes)
SymbolicAWEModels.distribute_panel_forces_to_points! — Function
distribute_panel_forces_to_points!(wing::Body, points::AbstractVector{Point})Distribute VSM forces to structural points using refined panel forces.
After the VSM solve, each refined panel's body-frame force/moment is split into corner-node forces (compute_aerostruc_loads, moment-preserving about the chosen reference) and accumulated on the structural LE/TE points of its parent section, which refined_panel_mapping names (1:1).
Arguments
wing::Body: Wing with PARTICLE_DYNAMICS type and solved VSM statepoints::AbstractVector{Point}: All structural points (filtered to this wing's nodes)
SymbolicAWEModels.rigid_aero_baseline! — Function
rigid_aero_baseline!(wing, stations; vsm_min_wind=0.5)Compute the operating point and baseline wind-axis coefficients for one wing: writes wing.aero_y / wing.aero_x and updates stations[gidx].aero_moment. Returns the context (va_mag, section counts, moment_frac, shadow_ref, y0) the mode-specific reduction (refresh_rigid_aero!) needs for the Jacobian.
SymbolicAWEModels.apply_direct_forces! — Function
Apply direct forces from wind-axis coefficients.
SymbolicAWEModels.vsm_aero_coeffs — Function
vsm_aero_coeffs(wing, y, va_mag, n_unrefined, n_stations,
station_idxs, stations, moment_frac, shadow_ref;
gamma_init=nothing) -> VectorRun one VSM solve at operating-point input y = [α, β, ω₁, ω₂, ω₃, θ_twist…] and return the wind-axis coefficient vector [CL, CD, CS, CM₁, CM₂, CM₃, cm_twist…]. ForwardDiff.Dual-aware via vsm_solve_objects: for a Dual eltype it solves on a cached dual shadow of the VSM solver, so the same routine yields the Jacobian under AD.
SymbolicAWEModels.vsm_solve_objects — Function
vsm_solve_objects(wing, ::Type{T}, shadow_ref) -> (body_aero, solver, wing)The VSM solve objects vsm_aero_coeffs runs on, selected by the input eltype T. A value pass (Float64) uses the wing's real objects. A ForwardDiff pass feeds Dual numbers through the solve to get the Jacobian, but the real objects' buffers are Float64 and can't hold Duals — so for a Dual eltype we solve on a Dual-typed "shadow" of the solver/aero. The shadow is expensive, so it is built lazily and cached in shadow_ref, keyed by the Dual eltype (rebuilt if it changes); use_gamma_prev warm-starts each perturbed solve from the previous circulation.
SymbolicAWEModels.safe_vsm_solve! — Function
safe_vsm_solve!(solver, body_aero, gamma_init=nothing;
moment_frac=0.1, cold_start=false)Solve under throw_on_fail, so a solve that missed the solver's tolerances or came back non-finite throws VortexStepMethod.SolveFailure. The circulation and the two angle-of-attack distributions of the last converged solve, which solve! has already overwritten with the diverged ones, are restored before it leaves.
Without gamma_init, solve! warm-starts from the circulation it left in solver.sol last time. cold_start starts from the solver's configured initial distribution instead: past stall the iteration has more than one fixed point, so a warm start makes the answer a function of what ran before rather than of the current state.
SymbolicAWEModels.warn_or_rethrow — Function
warn_or_rethrow(failure, vsm_warn_on_fail, wing)Rethrow failure, unless it is a VortexStepMethod.SolveFailure and vsm_warn_on_fail is set: then warn instead, and wing flies on with the circulation, the angles of attack and the frozen forces of its last converged solve.
SymbolicAWEModels.solve_and_freeze_circulation! — Function
solve_and_freeze_circulation!(mode, wing; cold_start=false)Solve and freeze the per-refined-panel induced velocity into mode.v_ind and the chord blend weights into mode.chord_weight, shared by the continuous VSM modes. Warm starting is VortexStepMethod.solve!'s own, gated by use_gamma_prev and by cold_start (safe_vsm_solve!), which throws on a failed solve before anything frozen is written.
SymbolicAWEModels.set_particle_panel_va! — Function
set_particle_panel_va!(wing, va_point_b_vals)Set the per-panel apparent wind on the wing's VSM BodyAerodynamics from the per-point body-frame apparent wind: each section's va is the mean of its LE/TE point values, refined panels take their parent section's value. Falls back to the wing-level va_b when the structural↔panel mapping is missing.
SymbolicAWEModels.set_refined_panel_va! — Function
set_refined_panel_va!(mode, wing, points, va_point_b_vals)Per-panel apparent wind for the continuous VSM modes: refined sections from strut_inflow_weights (numeric twin of reconstruct_inflow_sym), each panel the mean of its two bounding sections. Matches the inflow the symbolic RHS uses, so the frozen circulation belongs to it. Falls back to va_b without a structural↔panel mapping.
SymbolicAWEModels.surface_node_forces — Function
surface_node_forces(traction, first_column, n_nodes, panel_force, panel_net,
couple, shape)Force on each contour node of one panel: its frozen traction, its node_residual_shares share of panel_force - panel_net, and shape[node] · couple. Sums to panel_force over the panel — the frozen part cancels, share sums to one and couple_shape sums to zero — while the couple adds the pitching moment the shape is normalised to. traction is column-indexed panel-major as freeze_traction_pattern! fills it, first_column being this panel's first. The component sums this over each point's nodes rather than building it, so this stays the per-node statement of the pattern.
SymbolicAWEModels.couple_shape — Function
couple_shape(chord_fracs) -> Vector{SimFloat}Chordwise share of a pure pitching couple over a panel's surface contour nodes, from their chord fractions. Weights sum to zero and their first moment is -1 in chords, so Σ shape[k] · panel_couple places no net force and exactly the moment panel_force_eqs put in panel_couple — the same normalisation ContinuousAero gets from its ±couple pair at the LE and TE.
The shape is thin-airfoil theory's sin 2θ loading (θ from x/c = (1-cos θ)/2), the one mode of the chordwise distribution that carries a moment and no lift. That is what the increment needs: flow curvature is a parabolic-camber A₁ term whose lift is already in the panel force through the three-quarter-chord inflow (COLLOCATION_CHORD_FRAC), leaving only its moment to place. Fitting the constraints within α·sin 2θ + β keeps the placement in that family; β is nonzero only because the contour's nodes are not evenly spaced.
SymbolicAWEModels.build_mesh_maps! — Function
build_mesh_maps!(mode::ContinuousAero)Size the frozen per-panel buffers, set the polar callables, and freeze the refined-section → strut interpolation caches via build_section_interp.
SymbolicAWEModels.store_induced_velocity! — Function
store_induced_velocity!(v_ind, body_aero, gamma)Freeze the converged circulation into the buffer v_ind (3 × n_panels): each refined panel's induced velocity is AIC · gamma, the same product the VSM gamma loop converged on. Shared by the live particle modes.
SymbolicAWEModels.store_chord_weights! — Function
store_chord_weights!(chord_weight, body_aero)Freeze each refined panel's chord blend weight into chord_weight (npanels) via `VortexStepMethod.panelchordweight, so the weight follows the mesh as the wing deforms. Written at the same refresh as [storeinduced_velocity!`](@ref).
SymbolicAWEModels.size_frozen_panel_buffers! — Function
size_frozen_panel_buffers!(mode, n_panels)Size the per-panel buffers every live VSM mode freezes at a refresh — the induced velocity and the chord blend weight — reallocating only when the refined mesh changed. The chord weights start at the midpoint, which is what they are on a uniformly panelled wing.
SymbolicAWEModels.build_section_interp — Function
build_section_interp(vsm_wing) -> (left, weight, le_offset, te_offset)Freeze the refined-section → unrefined-strut interpolation of vsm_wing: refined section s sits at weight[s]·strut[left[s]] + (1−weight[s])·strut[left[s]+1]. le_offset/te_offset (3 × nsections) are each refined section's body-frame displacement off that straight strut line (nonzero only for BILLOWING). All are constants of the mesh, baked into the generated equations, so they enter the model-cache hash via each mode's `aerohash_id`.
SymbolicAWEModels.section_interp_caches — Function
section_interp_caches(mode) -> (left, weight, le_offset, te_offset)The frozen strut-interpolation caches a continuous mode stored via build_section_interp.
SymbolicAWEModels.aero_section_columns — Function
aero_section_columns(wing, points) -> Dict{Tuple{Int64,Symbol},Int}Map (unrefined_section, :LE/:TE) to the connector column (position in points) of the structural station point there, via wing.point_to_vsm_point. Interior chord control points (absent from the map) are skipped.
SymbolicAWEModels.interp_strut — Function
interp_strut(values, left, weight, s)Refined section s from its two bounding struts: weight[s]·values[left[s]] + (1−weight[s])·values[left[s]+1].
SymbolicAWEModels.interp_sections — Function
interp_sections(strut_le, strut_te, left, weight, le_offset, te_offset)
-> (sec_le, sec_te)Interpolate per-strut LE/TE positions to every refined section and add the frozen billow offset. Works on symbolic (from live connectors) or numeric (from pos_w) per-strut vectors alike.
SymbolicAWEModels.reconstruct_sections_sym — Function
reconstruct_sections_sym(mode, wing, points, connectors, column)
-> (sec_le, sec_te)Live symbolic body-frame LE/TE of every refined section: the strut LE/TE connector positions (connectors.point_pos, derived from world pos) interpolated by the frozen mesh weights plus the frozen billow offset. Shared by all continuous VSM modes so the force model and the plot use identical geometry.
SymbolicAWEModels.reconstruct_inflow_sym — Function
reconstruct_inflow_sym(mode, wing, connectors, column) -> (sec_va, sec_rho, sec_dva)Live symbolic body-frame apparent wind and density of every refined section: each strut's LE/TE connector values blended at COLLOCATION_CHORD_FRAC into one strut value, then interpolated by the frozen mesh weights — the same struts and weights reconstruct_sections_sym builds that section's corners from, so a section's inflow and its geometry read the same points. sec_dva is the same interpolation of each strut's trailing minus leading edge apparent wind, the pitch-rate input of panel_force_eqs; it is nothing unless the wing's solver asks for the flow curvature term, since building it for every section and discarding it is what the default path would otherwise pay. Symbolic twin of strut_inflow_weights and strut_pitch_weights. Shared by all continuous VSM modes.
SymbolicAWEModels.reconstruct_sections_b — Function
reconstruct_sections_b(mode, wing, points) -> (sec_le, sec_te)Numeric body-frame refined-section LE/TE from the live structural pos_w, exactly as reconstruct_sections_sym builds them symbolically. For plotting and logging so the drawn panels are the ones the dynamics use.
SymbolicAWEModels.write_live_aero_log_points! — Function
write_live_aero_log_points!(mode, wing, sys_struct, sys_state, point_idx, zoom)Log the panel corners the force model reconstructs (strut interpolation + frozen billow offset), not the raw VSM mesh, so the plot shows the deforming geometry the dynamics use. Shared by the continuous VSM modes.
SymbolicAWEModels.AeroHandle — Type
AeroHandle(body_aero)Mutable holder for the live BodyAerodynamics a polar reads, and the reason a serialized model does not carry one. A polar is a callable parameter, so its concrete type is baked into the parameter store and into what the solver specialized on and has to survive a round trip — but its contents are dead weight, because sync_params! rebinds every polar from the SystemStructure before the problem is evaluated. Serializing the holder empty keeps the type and drops the wing's whole Cp/cf surface tables, which were 2.4 GB of bin on a large kite. It is undefined between deserialize and that sync, and reading it there is a bug.
SymbolicAWEModels.ContinuousPolar — Type
ContinuousPolar(body_aero, coef)Callable polar for ContinuousAero, used as a callable flat parameter p(panel_idx, α): looks up refined panel panel_idx and evaluates the VSM coefficient function coef (calculate_cl/calculate_cd/calculate_cm) at angle of attack α. The panel is typeasserted concrete so the polar dispatches statically with no boxing in the compiled RHS; ForwardDiff.Dual-safe in α. The aerodynamics is held through an AeroHandle so that serializing the polar does not serialize the wing.
SymbolicAWEModels.transform_vsm_sections_to_body! — Function
transform_vsm_sections_to_body!(wing; aero_z_offset=nothing)Move the wing's VSM sections/panels from the CAD frame into the body frame (translate to wing.pos_cad, rotate by wing.R_b_to_c') and reinit the panels. With aero_z_offset set, also apply the chordwise aero z-offset (RIGID wings); PARTICLE wings pass nothing. Shared by setup_aero! and remake_aero!.
SymbolicAWEModels.frozen_point_force_component — Function
frozen_point_force_component(wing::AbstractWing, sys_struct; name, params) -> SystemParticle aero component binding each wing node's connector force to its frozen point.aero_force_b flat parameter (synced every refresh). Used by AeroDirect, whose refresh precomputes that force. Touching the parameter here is what registers it.
SymbolicAWEModels.build_panel_force_eqs — Function
build_panel_force_eqs(sec_le, sec_te, sec_va, sec_rho, vind_p, chord_w,
cl, cd, cm, spanwise, scale, orient)
-> (eqs, vars, panel_force, panel_couple, curvature_couple, slots)Shared per-refined-panel VSM force assembly for the live particle aero modes (ContinuousAero, AeroPressure). Traces the VortexStepMethod panel aerodynamics symbolically from the frozen circulation: per panel i (between section boundaries i and i+1) it builds the airfoil axes, chord, width, effective angle of attack (live apparent wind sec_va + frozen induced velocity vind_p), polar coefficients (cl/cd/cm callable params) and the lift/drag directions, and emits the panel force and pitching-moment couple. Returns the panel equations, the intermediate variables to register, the panel_force/panel_couple arrays for the caller's scatter, and the whole panel_force_slots named tuple for a scatter that needs the panel axes too. curvature_couple is the VortexStepMethod.flow_curvature_cm part of panel_couple alone.
sec_le/sec_te/sec_va are length-n_panels+1 vectors of body-frame 3-vectors (positions and apparent wind at the section boundaries), sec_rho the matching air densities; they may be live (interpolated from structure) or constant (a fixed mesh). spanwise is the wing spanwise direction, scale the chord-scale factor. orient is the per-panel ±1 span/normal sign from panel_span_signs and chord_w the per-panel chord blend weight from store_chord_weights!.
delta is an optional length-n_panels vector of symbolic per-panel flap deflections δ. When given the polars are evaluated as cl(i, alpha[i], delta[i]) (the (α, δ) tables); when nothing the 2-arg cl(i, alpha[i]) is used, so a mode without a flap (ContinuousAero) is untouched.
sec_dva is the matching per-section trailing minus leading edge apparent wind that carries the VortexStepMethod.flow_curvature_cm increment, or nothing to leave it out.
deficiency is the wing's Wagner lag (wagner_lag_eqs), subtracted from every panel's angle of attack before the polars are read; 0.0 leaves them steady.
SymbolicAWEModels.panel_force_eqs — Function
panel_force_eqs(slots, i, sections, flow, polars, spanwise, scale, orient,
chord_weight, delta)One panel's aerodynamic equations, writing into column i of the symbolic arrays in slots. sections is (le_1, te_1, le_2, te_2) in body frame, flow is (va_1, va_2, rho_1, rho_2, v_ind, dva_1, dva_2) and polars the (cl, cd, cm) callables, indexed by the panel number the polar tables were built for.
The physics is not written here: every expression comes from tracing the VortexStepMethod panel aerodynamics (panel_axes, panel_inflow, panel_force_directions, panel_loads and friends) with symbolic arguments, so the equations are the ones the numeric solver evaluates. All this function decides is where to tear the expression graph, binding each stage to a slot before feeding it to the next.
chord_weight is the section-1 share of the chord-direction blend (store_chord_weights!). delta is the flap deflection or nothing for the 2-argument polars. deficiency is the wing-wide Wagner lag subtracted from alpha to give the alpha_eff the polars read; the geometric alpha still sets the force directions.
dva_1/dva_2 are the sections' trailing minus leading edge apparent wind, giving the panel the pitch rate its VortexStepMethod.flow_curvature_cm moment increment is read at. They are nothing when the wing's solver has the term disabled (flow_curvature_enabled), and the rate is then bound to zero rather than dropped, so the slot means the same thing either way.
Every quantity it reads belongs to this panel alone, so the same equations serve a whole-wing system (looped by build_panel_force_eqs) and a per-panel component compiled once and instantiated for each panel.
SymbolicAWEModels.evaluate_polar — Function
evaluate_polar(polar, alpha, delta)The polar's coefficient at alpha, with the flap deflection when the tables carry one. Keeps panel_force_eqs blind to whether its caller bound the panel index into the polar (PanelPolar) or handed it a per-panel callable.
SymbolicAWEModels.flow_curvature_enabled — Function
flow_curvature_enabled(wing) -> BoolWhether this wing's VSM solver adds the VortexStepMethod.flow_curvature_cm increment. The aero modes all read the one flag, so a model cannot carry the term through AeroDirect and lose it in ContinuousAero. A wing without a VSM engine (PlateWing) has no flag and no increment.
SymbolicAWEModels.wagner_enabled — Function
wagner_enabled(wing) -> BoolWhether this wing carries the two-state Wagner lift lag (wagner_lag_eqs).
SymbolicAWEModels.wagner_wing_eqs — Function
wagner_wing_eqs(wing, sec_va, params) -> (eqs, vars, deficiency, initial)The wing's Wagner lag from its live section inflow, or an empty set when the wing carries no lag. Wraps wagner_lag_eqs with the mean of sec_va as the wing's apparent wind and the frozen wagner_reference_frame as the angle it is measured against, so both particle aero modes build the lag the same way.
SymbolicAWEModels.wagner_lag_eqs — Function
wagner_lag_eqs(gains, rates, va_ref, x_ref, z_ref, chord_ref)
-> (eqs, vars, deficiency, initial)The wing's two-state Wagner lift lag. A step in angle of attack does not build its full circulatory lift at once; Wagner's indicial function φ(s) = 1 - A₁·exp(-b₁·s) - A₂·exp(-b₂·s) gives the fraction reached after s semi-chords of travel, and starts at φ(0) = 1 - A₁ - A₂ = 0.5.
Driving two states with dxᵢ/ds = α - bᵢ·xᵢ and reading the deficiency d = Σ Aᵢ·(α - bᵢ·xᵢ) reproduces α_E = α - d = α·φ(s) after a step while leaving d = 0 in steady flow, and needs no dα/dt on the right-hand side. Converting to time with s = ∫2·v/c·dt gives the emitted D(xᵢ).
Steady flow puts xᵢ at α/bᵢ, which is what the returned defaults set, so a model starts trimmed instead of building its lift up from φ(0) = 0.5.
gains and rates are the wing's registered (A₁, A₂) and (b₁, b₂) parameters, so retuning the lag syncs rather than rebuilds. va_ref is the wing's mean body-frame apparent wind, measured against x_ref/z_ref/chord_ref from wagner_reference_frame. It carries no induced velocity, unlike a panel's v_eff, so this angle sits a downwash off the panels'; only changes in it drive the lag, which leaves a steady offset at zero deficiency. The one deficiency shifts every panel's angle of attack in panel_force_eqs, so the whole wing lags together and the spanwise shape of the loading is untouched.
SymbolicAWEModels.wagner_reference_frame — Function
wagner_reference_frame(wing) -> (x_ref, z_ref, chord_ref)The wing's mean chordwise and normal directions in body frame and its mean chord [m], averaged over the frozen VSM mesh. wagner_lag_eqs reads its one angle of attack against these, so the lag follows the whole wing rather than any one panel. Built the way panel_force_eqs builds a panel's axes, down to the panel_span_signs orientation, so the wing angle and the panel angles it shifts have the same sign.
SymbolicAWEModels.wagner_gain_params — Function
wagner_gain_params(params, wing_idx)The wing's registered Wagner gains (A₁, A₂). An ordinary parameter like every other tunable scalar, which is what keeps a change of lag out of the model cache key.
SymbolicAWEModels.wagner_rate_params — Function
wagner_rate_params(params, wing_idx)The wing's registered Wagner rates (b₁, b₂), tunable in the same way as wagner_gain_params.
SymbolicAWEModels.wagner_params — Function
wagner_params(wing, params) -> Vector{Any}The lag's parameters for a component's declared parameter list, empty for a wing without one. A whole-wing aero component names its parameters explicitly, so reading these in wagner_wing_eqs is not enough to declare them.
SymbolicAWEModels.panel_chord_width — Function
panel_chord_width(wing) -> (chord, width)Each refined panel's mean chord and quarter-chord width [m] read off the frozen VSM mesh: the build-time twins of the chord and width equations in panel_force_eqs.
SymbolicAWEModels.panel_apparent_mass — Function
panel_apparent_mass(wing, rho) -> Vector{SimFloat}The entrained air [kg] of each refined panel: the thin-plate added mass per unit span ρ·π·(c/2)² taken over the panel's width, which is the fluid a flat plate carries with it when it accelerates normal to itself.
SymbolicAWEModels.apparent_mass_carriers — Function
apparent_mass_carriers(sys_struct, point) -> Vector{Tuple{Any, SimFloat}}Whatever integrates point's translation, with each one's share — the only things the air it entrains can slow down. Three cases, and a beam wing has the last two:
- a free
DYNAMICnode integrates itself and takes all of it; - a node anchored to a rigid body is placed by that body, which takes all of it;
- a node riding a
TimoshenkoJoint's deformed centerline is placed by the two bodies the joint spans, which split it by where along the element it sits (beam_frac).
Mass left on a node that integrates nothing would never be felt, so a carrier that is not the node itself is the whole reason this lookup exists.
SymbolicAWEModels.PanelPolar — Type
PanelPolar(polar, panel)polar with its panel index bound, so it is called as p(alpha) or p(alpha, delta). A whole-wing system holds one polar addressed by panel number; a per-panel component holds a callable that is already only about its own panel. Applied while the equations are built, so both produce the same expression.
SymbolicAWEModels.loft_contour_node — Function
loft_contour_node(panel, xc, yc, k) -> Vector{SimFloat}Body-frame position of contour node k of a panel's airfoil: the mid-leading-edge plus xc[k] chords along the chord axis (x_airf) and yc[k] chords along the surface normal (z_airf). The (xc, yc) contour is chord-normalised and depends only on the panel's delta, so the loft geometry is static.
SymbolicAWEModels.build_station_point_map! — Function
build_station_point_map!(mode::AeroPressure, wing, points; prn=false)Build the static map from each refined panel's surface contour nodes to their nearest wing-node structural point (body frame), and apply the frame-alignment guard. Stored in mode.station_point; errors on a missing section_aero or a mesh that sits farther than frame_tol_frac chords from the points.
SymbolicAWEModels.build_panel_station_map! — Function
build_panel_station_map!(mode, wing, sys_struct)Map each refined VSM panel to the flap KINEMATIC station of wing whose spanwise station (midpoint of its two flap bodies) is nearest the panel's — the nearest-station philosophy of build_station_point_map!. Stored in mode.panel_station (global station idx, 0 = no flap); structural, so it enters aero_hash_id. All-zeros (no coupling) when the wing has no flap surface. Generic modes are a no-op.
SymbolicAWEModels.station_deltas — Function
station_deltas(sys_struct) -> Vector{SimFloat}Live flap deflection δ [rad] per station (indexed by station.idx; 0 for non-flap surfaces), from the current flap-body orientations via flap_delta. The Julia ground truth mirroring the symbolic station_delta_eqs!, used to drive panel.delta at refresh and by tests.
SymbolicAWEModels.apply_flap_delta! — Function
apply_flap_delta!(mode, wing, sys_struct)Set the VSM mesh's flap deflection from the live deflection of each mapped station (station_deltas), before the VSM solve, so the converged forces and the frozen traction contour track the flap at refresh cadence. No-op for modes/wings without flap coupling.
SymbolicAWEModels.set_panel_deltas! — Function
set_panel_deltas!(mode, wing, deltas) -> BoolPut one flap deflection [rad] per panel onto wing's VSM mesh, deltas indexed by station. false when the wing carries no panel-to-surface map to write through.
Both the panels and the wing's delta_dist are written, and the wing's is the one that matters: a panel's delta is derived, and VortexStepMethod.reinit! re-seeds every panel from delta_dist whenever the mesh is rebuilt — which refresh_particle_aero! does from the deformed structure before each solve. Writing the panels alone therefore lasts until the next refresh and no longer, leaving the solve and the traction contour at δ = 0 on a wing whose flaps are deflected.
SymbolicAWEModels.init_pressure_buffers! — Function
init_pressure_buffers!(mode::AeroPressure, wing)Size the frozen state buffers (v_ind, chord_weight, traction, traction_net) to the current mesh and set the cl/cd/cm polar callables. Run after build_station_point_map! (needs the per-panel node counts).
SymbolicAWEModels.freeze_traction_pattern! — Function
freeze_traction_pattern!(mode::AeroPressure, wing)Fill the frozen traction params from the converged VSM solve: for each refined panel loft the airfoil surface contour and take its traction pattern from surface_pattern — the section's own tables at the effective sectional α (sol.alpha_dist), or, on live polars, the deformed shape's — build the per-segment traction (−Cp·n̂ + cf·ŝ)·q·dA (n̂ outward, ŝ along the chord) into mode.traction (panel-major node order, matching aero_component), and store the per-panel net in mode.traction_net. The net anchors the symbolic scatter so each panel's point forces sum to the live VSM total.
mode.traction_moment and mode.residual_arm are the moment the frozen pattern already carries about the panel's leading-edge midpoint and the share-weighted lever arm the residual will be spread on; pressure_couple subtracts both from the polar's moment so the couple it places is only what is missing.
SymbolicAWEModels.LivePolarState — Type
LivePolarState(source, control_point, control_fraction, control_offset)Per-wing state of the live polar path: the LivePolars source that owns the base airfoils and the fit, plus the control points each panel reads its chordwise deformation from and where those points sat in the reference geometry.
The control points are the distinct structural points a panel's surface nodes map to (AeroPressure.station_point), so they are exactly the wing nodes that already carry that panel's load — a chord-line beam's nodes today, a membrane's nodes later. Their current offset off the deformed chord line, minus the reference offset stored here, is the camber increment handed to deform_kulfan.
SymbolicAWEModels.chord_frame_coordinates — Function
chord_frame_coordinates(panel, pos_b) -> (fraction, offset)Where a body-frame position sits in a panel's chord frame: along-chord fraction off the mid leading edge and offset off the chord line, both over the panel chord. The frame is built from the panel's four corner_points, which is the one part of a panel's geometry a replayed log frame restores, so a replay measures the deformation of the frame it draws rather than of whatever the last solve left behind. Its normal is sign-aligned to z_airf, so a control point and the traction pattern cannot disagree about which way is up.
SymbolicAWEModels.live_polar_settings — Function
live_polar_settings(wing, vsm_set) -> LivePolarSettingsThe live-polar sampling wing's VSM settings ask for: the airfoil: block of the vsm_set wing of the same name, or of the first wing when no name matches, which is the single-wing case every VSM-backed kite is built as. Without a vsm_set the package defaults stand.
SymbolicAWEModels.build_live_polars! — Function
build_live_polars!(mode::AeroPressure, wing, points, stations; vsm_set=nothing,
settings=live_polar_settings(wing, vsm_set))Build the wing's LivePolarState from the reference geometry: fit each panel's undeformed Kulfan parameters off its surface contour, take the spanwise stations the wing declares, and record where each of their control points sits in that station's chord frame. Run after build_station_point_map!, which binds each panel to one of those same stations, and while the mesh still stands on the reference (CAD) structure — the offsets stored here are the zero the live deformation is measured against.
Deformation and load read the same declared stations, so the points a panel is deformed by are the ones it is loaded through. Measuring per station and interpolating onto the panels is what keeps the deformation smooth across the span: a panel between two stations has no structural points of its own, and binding it to a single station would make the deformation a staircase.
SymbolicAWEModels.station_control_points — Function
station_control_points(stations, points, wing) -> Vector{Vector{Int64}}The wing's control points grouped into the spanwise stations the structure declares. A station already names the points of one station — its leading edge, its trailing edge and the control points between them — so the grouping is read rather than inferred from geometry or from the beam graph, and it holds for a wing with no beam joints at all.
Ownership is taken from the points rather than from the surface's wing_idx, which a geometry that never names a wing leaves at zero. A surface names its leading and trailing edge as well as the control points running between them, and the first and last of those often sit on the very same nodes, so coincident points are kept once: they are one place on the chord, and a fit handed the same station twice has no answer.
SymbolicAWEModels.panel_station_candidates — Function
panel_station_candidates(mode, panels, stations, points, wing,
point_idx, point_pos_b)
-> Vector{Vector{Tuple{Int64, KVec3}}}The two spanwise stations each panel's load is shared between, and the far one's share: the station it sits on, the next one out, and how far between them it lies.
Which strut a panel sits on comes from the refined-section interpolation the loft carries, not from measuring the panel's position, so it holds for a swept or dihedral wing as well as a flat one. A panel's load is split between its two neighbouring stations rather than rounded onto the nearer, because rounding is a discontinuity: two panels that mirror each other land on centres that agree to the last bit but not exactly, and the nearer station is then a different one for each, moving a whole panel's load a station across the span. Sharing it moves that disagreement back into the weights, where it stays the size it actually is. A panel is a slice of one airfoil, and it has to stay on one strut. Left to search the whole wing, the nodes near a station boundary defect to the neighbouring strut, so half an airfoil hangs off one station and half off the next — the panel then spans two spanwise planes, and a chordwise profile read off it has a step in it that no airfoil basis can represent. Choosing the station first and mapping within it keeps each panel whole. Returns nothing when the wing declares no stations, which leaves the caller on the plain nearest-point search: matching by chord fraction alone is only safe once the candidates are confined to one station, since the same fraction occurs at every station across the span.
SymbolicAWEModels.panel_strut_blend — Function
panel_strut_blend(mode::AeroPressure, n_stations, n_panels)
-> Vector{Tuple{Int64, Int64, SimFloat}}The two stations each panel lies between and how far it lies towards the second, taken from the refined-section interpolation the loft carries rather than measured off the panel's position. Load and deformation both read this one answer: a panel is deformed by the points it is loaded through, so the two cannot be allowed to disagree. A panel spans two refined sections, so it sits at the mean of their places.
A section's weight is its share of strut[left], which puts it at left + (1 - w) in strut units. Averaging the weights themselves only holds while both sections name the same left strut: across a strut the two are shares of different pairs, and their mean says nothing. A panel straddling a strut then lands on the far one.
SymbolicAWEModels.update_live_deflection! — Function
update_live_deflection!(mode::AeroPressure, wing, points)Refresh every spanwise station's camber increment from the deformed structure, then interpolate those increments onto the panels. Each control point contributes its current offset off its station's deformed chord line minus its reference offset, placed at its current chord fraction; the chord's own rotation and stretch are already absorbed by the frame, which the mesh rebuild took from the deformed leading and trailing edges. What travels spanwise is the increment, not the airfoil, so a panel between two stations keeps its own base fit. Fills mode.live.deflection, ready for refit_live_polars!.
SymbolicAWEModels.refit_live_polars! — Function
refit_live_polars!(mode::AeroPressure, wing, alpha) -> Float64Regenerate every panel's polar at the stored deformation and write it into the panel's own table about alpha [rad] per panel. Reynolds is per panel from the solver's air properties and the panel's own apparent wind and chord. Returns the lowest NeuralFoil analysis confidence over the wing; warns once below 0.5, which means a deformed section has left the region the network was trained on.
SymbolicAWEModels.refresh_live_pressure! — Function
refresh_live_pressure!(mode::AeroPressure, wing)Regenerate the surface traction pattern from the deformed shape at the converged angle of attack: the contour offset by the deformation's own camber increment, Cp from one batched NeuralFoil pass over every panel, and skin friction from the flat-plate closure at each panel's own Reynolds.
This is what keeps force and placement telling the same story. The polars already make a panel's total force follow its deformed shape; without this the pattern spreading that force over the structure would still be the undeformed section's. Both halves matter: Cp sets how hard each node is pulled, and the contour sets which way — every traction is -Cp·n̂ with n̂ a finite difference of neighbouring node positions, so an undeformed contour points the whole load the wrong way wherever the section has moved.
Run it after the solve has converged, on the shapes refit_live_polars! evaluated. The nodes move but their assignment to structural points does not: that map is baked into the generated equations, which is what keeps the scatter continuous when the section deforms.
SymbolicAWEModels.surface_pattern — Function
surface_pattern(mode::AeroPressure, panel, panel_idx, alpha) -> (x, y, cp, cf)The contour a panel's force is spread over and the traction pattern over it. Tabulated polars read the section's own (α, δ) tables. Live polars read the same contour at δ = 0 with the contour, Cp and skin friction all regenerated from the deformed shape by refresh_live_pressure!.
SymbolicAWEModels.panel_reynolds — Function
panel_reynolds(wing) -> Vector{SimFloat}Reynolds number of every panel, from the solver's air properties and the panel's own apparent wind and chord.
SymbolicAWEModels.live_polar_alpha — Function
live_polar_alpha(wing) -> Vector{SimFloat}The angles of attack [rad] to sample each panel's polar about: the previous solve's converged lr.alpha_dist, or, before there is one, each panel's geometric angle from its own apparent wind. Sampling the first solve of a run about zero would hand it polars that say nothing at the real angle.
SymbolicAWEModels.solve_with_live_polars! — Function
solve_with_live_polars!(mode::AeroPressure, wing, points; cold_start=false)Solve the wing with polars regenerated from its current shape, sampled about live_polar_alpha. One sampling and one solve: a sampled polar holds its last value past either end rather than extrapolating, so a solve landing outside the sampled range reads a bounded answer instead of a runaway one and needs no refit to be safe. How far it landed is reported by AirfoilAero.polar_drift, which warns once past the range — there the panel's polar is flat and no longer tracks the shape.
The traction pattern is regenerated from the same deformed shapes once the solve has converged, so the forces and their placement both follow the deformation.
SymbolicAWEModels.contour_winding — Function
contour_winding(section) -> Float64+1 when the closed contour section of (chordwise, normal) pairs runs counter-clockwise, -1 when it runs clockwise, by the shoelace area. Orienting a normal off the loop itself works on a cambered canopy, where both surfaces can lie on one side of the chord line and "away from the chord" points inward.
SymbolicAWEModels.write_aero_forces! — Function
write_aero_forces!(ss, sys_struct) -> ssFill ss.aero_force_x/y/z with the world-frame aerodynamic force each wing node carries, from its point.aero_force_b. A mode that stores none leaves its nodes at zero.
SymbolicAWEModels.accumulate_point_offset! — Function
accumulate_point_offset!(mode)Reduce the frozen pattern to one constant force per wing point, keyed by global point index: the tractions of the nodes it owns, less the share of each panel's frozen net that aero_scatter_entries re-adds through the live panel force. The scatter is then a pure weighted sum of panel forces, which is what the wiring layer carries. Derived wholly from traction, traction_net and station_point, and refreshed with them.
SymbolicAWEModels.restore_point_aero_forces! — Function
restore_point_aero_forces!(sys, wing, sys_state)Put each of wing's nodes back to the body-frame aero force the log holds for it, rotating the logged world-frame aero_force_x/y/z by the frame just restored. A log without those channels restores zeros.
SymbolicAWEModels.carries_point_aero — Function
carries_point_aero(sys_struct, point) -> BoolWhether point's per-node aerodynamic force has to be read back out of the model: it is a node of a PARTICLE_DYNAMICS wing whose mode scatters panel loads, so the load exists only in the equations. This selects the same nodes the KernelBackend builds an AeroPointForce for.
SymbolicAWEModels.point_aero_force_array — Function
point_aero_force_array(sys_struct, sys) -> Matrix{Num} or nothingThe body-frame aerodynamic force of every point as one 3 x n_points array to fetch, holding aero_force_point_b where a node has one and a literal zero elsewhere, so it scatters on the point index like the other per-point arrays. nothing when no wing writes per-node forces. The zeros are constants in the generated function, so a point carrying no aero costs nothing to fetch.
SymbolicAWEModels.zero_aero_force — Function
A fresh zero body-frame force, the accumulator accumulate_point_offset! starts each point from.
SymbolicAWEModels.PanelAeroList — Type
PanelAeroList(mode)mode's panels addressed one at a time, reached as wings[w].aero.panels[i]. It holds the mode rather than its matrices, so a VSM refresh that reallocates them is still seen, and nothing is copied.
SymbolicAWEModels.PanelAero — Type
PanelAero(mode, idx)One refined panel's frozen aerodynamic data. AeroPanel reads its parameters through this, so the remap_path index swap lands on the panel rather than on the wing, and every read goes to the parent mode's live matrices.
SymbolicAWEModels.PointAeroList — Type
PointAeroList(mode)mode's wing points addressed one at a time, reached as wings[w].aero.points[i], where i is the global point index. The panel counterpart of PanelAeroList.
SymbolicAWEModels.PointAero — Type
PointAero(mode, idx)One wing point's frozen aerodynamic data, addressed by its global point index so remap_path can swap it for each AeroPointForce instance.
SymbolicAWEModels.NO_AERO_FORCE — Constant
The force a point receives from a mode that stores none — read as a zero of the right shape while the frozen pattern is still empty.
SymbolicAWEModels.panel_force_slots — Function
panel_force_slots(n_panels)The symbolic intermediates panel_force_eqs writes into, as n_panels columns. A whole wing's system builds every column and an AeroPanel component builds one, so the two emit identical expressions per panel.
SymbolicAWEModels.panel_force_vars — Function
The slots of panel_force_slots as the variable list a System declares.
SymbolicAWEModels.supports_panel_decomposition — Function
supports_panel_decomposition(mode) -> BoolWhether the mode's PARTICLE_DYNAMICS aerodynamics can be built as one component per panel and per point instead of one per wing. A mode opts in by defining aero_inflow_groups and aero_scatter_entries; everything else about it — the panel equations, the strut geometry, the wrench — is already shared.
SymbolicAWEModels.scatter_couple — Function
scatter_couple(mode, slots, i, panel) -> VectorThe couple panel i's scatter places, from the panel_force_slots the panel equations wrote. The default is the whole panel_couple, which is what a scatter that carries no moment of its own needs. AeroPressure overrides it: its frozen traction already carries most of the moment, so it places only the deficit, see pressure_couple. panel addresses that mode's frozen per-panel params and is nothing for a mode that has none.
SymbolicAWEModels.pressure_couple — Function
pressure_couple(panel_couple, panel_force, traction_net, traction_moment,
residual_arm, x_airf, y_airf, z_airf, chord) -> VectorThe couple couple_shape has to place for the panel's pitching moment to be the polar's. The target about the panel's leading-edge midpoint is the one ContinuousAero places from its ±couple pair: panel_force acting at the quarter chord plus panel_couple as a pure couple. Against it stand the moment the frozen traction already carries (traction_moment) and the one the residual panel_force - traction_net will carry on its share-weighted arm residual_arm. What is left is the deficit, and dividing by the chord returns it to the force-along-z_airf currency panel_couple is written in.
The airfoil axes are not exactly orthogonal on a billowed panel, so the divisor carries the frame's own triple product rather than assuming one; on the SK100 that alone is most of a percent.
Without this the panel's pitching moment is whatever its Cp pattern integrates to and the polar's cm never reaches the structure — the two agree closely on a live polar, where both come from one solve of one shape, but nothing was holding them together.
SymbolicAWEModels.node_residual_shares — Function
node_residual_shares(xc, yc) -> Vector{SimFloat}Share of its panel's residual every contour node carries, summing to one over the panel: the node's area times the chordwise shape an added section force takes.
The residual is the panel force the VSM converged on less what the frozen Cp pattern integrates to, and it has to go somewhere. An equal split per node is the one choice that cannot be right, because it is a property of the mesh rather than of the flow: the contour is clustered where the airfoil turns, so the trailing edge owns two fifths of a section's nodes while the Kutta condition leaves it almost none of the load, and an equal split hands it two fifths of the correction — a download on the trailing edge of a lifting wing, which folds the flap the way the flap is already going.
Area alone is mesh-independent but still spreads the correction evenly along the chord. What an added section force actually looks like is thin-airfoil theory's additional load, sqrt((1-ξ)/ξ): largest at the leading edge, zero at the trailing edge. That puts the correction where a two-dimensional pattern goes wrong — the nose — and leaves the trailing edge at the zero load its sharp edge demands.
SymbolicAWEModels.aero_inflow_groups — Function
aero_inflow_groups(mode, wing, points) -> (groups, section_group)How each refined section's apparent wind and density are averaged over the wing's structural points. groups[g] is a list of (point column, weight) whose weights sum to one, and section_group[s] names the group refined section s reads. Sections sharing a group share the one AeroInflow that averages it.
The default is one group per refined section, gathered from the same bounding struts aero_geometry_entries reconstructs that section's corners from, so a section's inflow and its geometry read the same points. Overriding this with a wing-wide mean drops the rotational part of the velocity field — a rigid rotation about the point centroid averages to zero — leaving the panels without rate damping.
SymbolicAWEModels.aero_pitch_groups — Function
aero_pitch_groups(mode, wing, points) -> (groups, section_group)aero_inflow_groups with the trailing minus leading edge weights instead of the chordwise blend, so the same gather yields each section's pitch-rate input (strut_pitch_weights). Only built for a wing with flow_curvature_enabled.
SymbolicAWEModels.aero_station_groups — Function
aero_station_groups(mode, wing, points, station_weights) -> (groups, section_group)One group per refined section, each gathered by station_weights. Shared by aero_inflow_groups and aero_pitch_groups, which differ only in the chordwise weights they ask for.
SymbolicAWEModels.aero_scatter_entries — Function
aero_scatter_entries(mode, wing, points) -> VectorThe linear map from panel loads to point forces as (panel, point column, force weight, couple weight): point k's body-frame force gains force weight · panel_force + couple weight · panel_couple. The weights are constants of the mesh, so the wiring layer carries the whole scatter and no component holds it.
SymbolicAWEModels.aero_point_offset — Function
aero_point_offset(mode, params, wing_idx, point_idx)The constant body-frame force a wing point receives on top of the scattered panel loads, or nothing. AeroPressure puts its frozen surface traction here, net of the share of each panel's frozen total that the scatter already re-adds.
SymbolicAWEModels.aero_geometry_entries — Function
aero_geometry_entries(mode, wing, points) -> VectorWhere each panel's four section corners come from, as (panel, corner, point column, weight) with corner one of :le_a, :te_a, :le_b, :te_b. This is interp_sections split in two: the strut weights the wiring gathers, and the frozen billow offset the panel adds as a parameter. Mode-independent — every continuous mode reconstructs its sections the same way.
SymbolicAWEModels.strut_inflow_weights — Function
strut_inflow_weights(mode, section, column) -> Vector{Tuple{Int, SimFloat}}The (point column, weight) pairs averaging refined section section's inflow over its two bounding struts, each strut blending its LE and TE station at COLLOCATION_CHORD_FRAC. With no chord twist rate the two stations share a velocity, so the blend is inert in steady flight whatever the fraction.
SymbolicAWEModels.strut_pitch_weights — Function
strut_pitch_weights(mode, section, column) -> Vector{Tuple{Int, SimFloat}}The (point column, weight) pairs taking refined section section's trailing minus leading edge apparent wind, which VortexStepMethod.section_pitch_rate turns into the section's rotation rate about its own spanwise axis. Weights sum to zero, so a wing in uniform translation contributes nothing.
SymbolicAWEModels.strut_station_weights — Function
strut_station_weights(mode, section, column, le_weight, te_weight)
-> Vector{Tuple{Int, SimFloat}}The (point column, weight) pairs that combine refined section section's two bounding struts, each strut weighting its LE station by le_weight and its TE station by te_weight. The strut shares are the section's own interpolation weights, so whatever chordwise combination the caller asks for is taken at the same struts aero_geometry_entries builds that section's corners from.
SymbolicAWEModels.scatter_node_weights — Function
scatter_node_weights(mode, point_num)Every (panel, node, point, weight, column) one contour node is spread over: its near station takes 1 - far and its far one far, column being the node's place in the panel-major traction pattern. The symbolic component and the kernel entries both walk this, so the surface pattern has a single definition.
SymbolicAWEModels.scatter_totals! — Function
scatter_totals!(totals, panel, point, force_weight, couple_weight)Accumulate one contribution into the (panel, point) → [force weight, couple weight] map aero_scatter_entries builds, so a panel that reaches the same point twice becomes one wiring edge instead of two.
SymbolicAWEModels.zero_weight_pair — Function
A fresh [force weight, couple weight] accumulator.
SymbolicAWEModels.scatter_entry_list — Function
The (panel, point, force weight, couple weight) list of a scatter_totals! map, ordered so the wiring is reproducible.
SymbolicAWEModels.panel_span_signs — Function
panel_span_signs(wing, spanwise)Per-panel sign (±1) that orients the local span/normal so each panel's y_airf points along +spanwise and z_airf to the upper surface, independent of section ordering. Baked at build time because the section order is fixed for a wing.
SymbolicAWEModels.frame_sections — Function
frame_sections(vsm_wing)Every VSM section whose LE/TE are stored in the wing frame — refined_sections, non_deformed_sections and unrefined_sections — as one iterator so a frame transform reaches all three. The lists hold independent Section objects (VSM copies on refine), so each is moved exactly once.
SymbolicAWEModels.seed_wing_inertia! — Function
seed_wing_inertia!(vsm_wing, set, com, unit_inertia)Seed a VSM mesh's per-unit-mass inertia vsm_wing.inertia_tensor (3×3, [m²]) and COM vsm_wing.T_cad_body = -com so the SystemStructure inertia pipeline (normalized_inertia) uses them instead of the point-mass fallback. unit_inertia accepts either the symmetric 6-vector [Ixx,Iyy,Izz,Ixy,Ixz,Iyz] or a full 3×3. When com/unit_inertia are omitted they auto-compute from the .obj at joinpath(get_data_path(), set.model) if present; with neither given and no .obj, this is a no-op (point-mass fallback stays in effect).
OBJ mesh mass properties
SymbolicAWEModels.ObjAdapter — Module
ObjAdapterMesh mass-property helpers for wings authored from a triangulated .obj: center of mass, surface inertia tensor, and the per-unit-mass (com, inertia) used to seed a wing's mass properties. Mesh IO is delegated to VortexStepMethod.ObjAdapter.read_faces; this module owns only the mass-property maths, kept out of VortexStepMethod (which is aero-only).
SymbolicAWEModels.ObjAdapter.center_of_mass — Function
center_of_mass(vertices, faces) -> Vector{Float64}Area-weighted center of mass of a triangulated surface mesh, in the mesh's own CAD frame. Non-mutating; requires triangular faces. Unlike VortexStepMethod's old center_to_com!, this neither shifts the vertices nor forces the COM onto the xz-plane.
SymbolicAWEModels.ObjAdapter.calculate_inertia_tensor — Function
calculate_inertia_tensor(vertices, faces, mass, com) -> Matrix{Float64}Surface-area-weighted 3×3 inertia tensor about com, for total mass spread uniformly over the mesh surface. Pass mass = 1 for the per-unit-mass tensor [m²]; multiply by the physical mass for [kg·m²].
SymbolicAWEModels.ObjAdapter.unit_inertia_from_obj — Function
unit_inertia_from_obj(obj_path) -> (com, unit_inertia)Read a triangulated .obj and return its center of mass com [m] and its per-unit-mass inertia tensor unit_inertia (3×3, [m²], about com), both in the mesh CAD frame. Multiply unit_inertia by the wing mass for the physical tensor.
SymbolicAWEModels.ObjAdapter.unit_inertia_matrix — Function
unit_inertia_matrix(unit_inertia) -> Matrix{Float64}Rebuild the symmetric 3×3 inertia tensor from the 6-vector [Ixx, Iyy, Izz, Ixy, Ixz, Iyz].
SymbolicAWEModels.ObjAdapter.unit_inertia_vector — Function
unit_inertia_vector(tensor) -> Vector{Float64}Pack a symmetric 3×3 inertia tensor into [Ixx, Iyy, Izz, Ixy, Ixz, Iyz].
Heading and geometry
SymbolicAWEModels.solve_heading_rotation — Function
solve_heading_rotation(R_b_to_w, target_heading, wing_pos)Calculate the rotation angle around the radial axis needed to achieve target_heading.
With the tangential sphere heading, rotating around the radial axis by θ simply shifts the heading by θ, so the solution is target_heading - current_heading.
SymbolicAWEModels.get_ref_position_from_points — Function
get_ref_position_from_points(points, ref_pt; field=:pos_w)Weighted position from structural points. field selects which point coordinate to read (:pos_w or :pos_cad).
SymbolicAWEModels.sym_calc_R_t_to_w — Function
sym_calc_R_t_to_w(wing_pos)Symbolic version of calc_R_t_to_w that uses explicit element access to avoid slice scalarization issues.
SymbolicAWEModels.wrap_to_pi — Function
wrap_to_pi(angle)Wrap angle to [-π, π] range.
SymbolicAWEModels.heading_reference_body — Function
heading_reference_body(transform, bodies) -> Body or nothingThe single body whose frame defines the transform's heading: the wing/rot body (transform.wing_idx) when set, otherwise the first body in the transform. The heading rotation is applied once about the radial through it — not once per body, which would compound into a spurious rotation for multi-body (beam) transforms.
Transform internals
SymbolicAWEModels.apply_azimuth_elevation! — Function
apply_azimuth_elevation!(transform, points, bodies, base_pos)Apply the azimuth/elevation rotation of a single transform to all components in it (points and bodies). Rotates the current radial onto the target radial by the minimal (roll-free) rotation, so placement never depends on the source frame — which is undefined when the components start at the zenith. Roll about the radial is set afterwards by the heading step, which is well-defined at the target elevation/azimuth. Returns (curr_R_t_to_w, R_t_to_w) for use in that step.
SymbolicAWEModels.apply_heading! — Function
apply_heading!(transform, points, bodies,
curr_R_t_to_w, R_t_to_w, base_pos)Apply heading rotation to all components in a single transform. Rotates around the radial axis through base_pos (not the origin). Uses the reference body's R_b_to_w for the no-ref-points orientation source. After copy_cad_to_world!, this equals R_b_to_c (for reinit!), or the current world orientation (for reposition!). Bodies in the transform rotate with the same heading delta; a transform without a body target applies no heading (matching point behavior).
SymbolicAWEModels.spherical_spin — Function
spherical_spin(transform) -> VectorAngular velocity [rad/s], in world axes, of the rigid rotation about the transform's base that carries elevation_vel and azimuth_vel. Its axes are -y of the tangential frame for elevation and world -z for azimuth.
SymbolicAWEModels.apply_spherical_velocity! — Function
apply_spherical_velocity!(transform, points, bodies, base_pos)Set every component of transform to the velocity of the rigid rotation spherical_spin describes, spin × (pos_w - base_pos). Only a rigid body takes the matching ω_b.
SymbolicAWEModels.finalize_transforms! — Function
finalize_transforms!(points, bodies)Finalize transforms: update PARTICLEDYNAMICS body frames from structural point positions, then compute principal frame ODE state for every body (RIGIDDYNAMICS bodies re-derived from the transformed pos_w/Q_b_to_w).
SymbolicAWEModels.refresh_deformed_positions! — Function
refresh_deformed_positions!(points, bodies) -> nothingUpdate every PARTICLEDYNAMICS wing node's `posbfrom its live world position, measured in its wing's current frame about the wing origin — the same referencefinalizetransforms!buildsposundeformed_b` against, so the two differ only by the deformation the points have picked up.
Called once per state sync, so both backends report it from one definition. A RIGID_DYNAMICS wing's nodes keep the offset they were constructed with, that being what places them.
SymbolicAWEModels.min_rotation — Function
min_rotation(curr_dir, target_dir) -> (axis, angle)Axis and angle of the minimal rotation taking unit vector curr_dir onto target_dir. The rotation lies in the plane the two directions span, so it adds no roll about either — the tangential (heading) orientation is left untouched. Falls back to an arbitrary perpendicular axis when the directions are anti-parallel.
Flat parameters
SymbolicAWEModels.read_path — Function
read_path(obj, path)Walk path (a tuple of Symbol fields and Int indices) from obj, e.g. read_path(sys_struct, (:wings, 1, :aero, :engine, :aero_jac)).
SymbolicAWEModels.PathReader — Type
PathReader(path)Serialisable closure reading a fixed path from a sys_struct at sync time (holds only the path, never the struct).
The field names live in the type and the container indices stay values. A name has to be a compile-time constant for the field read to resolve to an offset — otherwise every step of the walk is a dynamic call that boxes both its argument and its result — while keeping the indices out of the type lets all components of one kind share a single compiled reader instead of one per index.
SymbolicAWEModels.path_of — Function
The (name, index, …) tuple reader was built from.
SymbolicAWEModels.ParamEntry — Type
ParamEntryOne flattened parameter: the symbolic param, a read(sys_struct) callable that returns its live value, a kind (:scalar, :array, or :callable), and the path it was read from (a (name,) tuple for computed leaves). The path lets a backend resolve a runtime address per instance (see ParamView).
SymbolicAWEModels.ParamRegistry — Type
ParamRegistrySingle source of truth for the flattened parameters. Built during equation generation; the params view records one ParamEntry per distinct field it reads (memoised by cache key). After compilation it drives sync_params!.
SymbolicAWEModels.make_param — Function
Numeric scalar parameter named name (default value).
SymbolicAWEModels.make_array_param — Function
Numeric vector parameter name[1:n] (default value).
Numeric matrix parameter name[1:m,1:n] (default value).
SymbolicAWEModels.make_callable_param — Function
Callable parameter name (invoked symbolically as name(x); default value). For a leaf that is a function/interpolation/polar — MTK codegens the call and ForwardDiff differentiates through it, so no @register_symbolic is needed.
SymbolicAWEModels.register_leaf! — Function
register_leaf!(reg, key, name, reader, value, path)Create (once, memoised on key) and record the flat parameter for a leaf value under symbol name. Numeric scalars/arrays become data params; any other (callable) leaf — an interpolation or polar — becomes a callable param applied as name(x). reader reads the live value from a sys_struct at sync time; path is stored on the entry for per-instance address resolution.
SymbolicAWEModels.leaf_param! — Function
leaf_param!(reg, path, reader, value)Record the flat parameter for a leaf read at path, named and memoised by the full path. Two reads of the same field on different components are therefore different symbols, which is what lets one kernel read both endpoints of a joint without them collapsing onto one parameter.
SymbolicAWEModels.param_computed! — Function
param_computed!(reg, name, reader)Escape hatch for a value that is not a plain field read — reader(sys_struct) computes it (e.g. a WindFactorReader building a callable wind-factor from the atmospheric model). reader must be a named struct (serialisable), not a closure over sys_struct.
SymbolicAWEModels.param_descend — Function
Types the view descends through (everything else is a leaf).
SymbolicAWEModels.ParamView — Type
Top-level params view wrapping a ParamRegistry: params.segments[i].l0 mirrors sys_struct.segments[i].l0 and records the parameter that stands for it (build-time only).
SymbolicAWEModels.PathView — Type
A partial path into sys_struct on its way to a parameter.
SymbolicAWEModels.param_unknowns — Function
param_unknowns(params)The symbolic parameters a params view recorded so far, in insertion order — passed as the parameter list of a component System so every params.… read it made is declared.
SymbolicAWEModels.ReaderGroup — Type
ReaderGroup(targets, readers)The readers sharing one concrete type, with the index each fills in the vector it writes to.
Readers are closures of many types, so a Vector{Any} of them costs a dynamic dispatch and a boxed return per element. Grouping by type lets sync_readers! take one dispatch per group and then run a fully inferred loop: at SK100 scale that is a dozen dispatches instead of 22756, twice a step.
SymbolicAWEModels.group_readers — Function
group_readers(targets, readers) -> Vector{ReaderGroup}Split the parallel targets/readers vectors into one ReaderGroup per concrete reader type, keeping the original order within each group.
SymbolicAWEModels.sync_readers! — Function
sync_readers!(group::ReaderGroup, destination, sys_struct)Write every reader of group into destination at its target index. Called with a concretely typed group, which is the function barrier the grouping exists for.
SymbolicAWEModels.ParamGroup — Type
ParamGroupA setp setter plus the type-grouped readers and preallocated value buffer for one parameter kind. eltype is SimFloat for numeric scalars, Any for arrays/callables.
SymbolicAWEModels.ParamSync — Type
Bundle of the per-kind sync groups (each may be nothing).
SymbolicAWEModels.survivor_index — Function
survivor_index(sys) -> Dict{String, Vector{param}}Map each name to every parameter surviving mtkcompile under it, keyed by both the full name and the leaf name (after the last ₊ namespace separator) so a registry's bare param matches its namespaced counterparts from subsystems.
A field read by both the parent equations and a subsystem is memoised to one registry entry but compiles to one parameter per reader, all sharing a leaf name. Keeping only the last would leave the others frozen at their build-time value, and whichever observed equation reads a frozen one then writes it back over the struct in update_sys_struct!.
SymbolicAWEModels.build_param_sync — Function
build_param_sync(sys, registry) -> ParamSync | NothingBuild the per-kind sync groups from the compiled system and the registry. Pruned parameters (no surviving equation references them) are dropped, so a setter never touches a parameter absent from the buffer.
SymbolicAWEModels.sync_params! — Function
sync_params!(sync, target, sys_struct)Copy every flattened field from the live sys_struct into target's parameter buffers (target is an ODEProblem or an ODEIntegrator). A no-op when there are no flattened parameters.
SymbolicAWEModels.joint_stiffness_term — Function
joint_stiffness_term(joint, params, kind, Δ)Restoring force/moment for one joint DOF, read as a flat parameter: a Real stiffness is a numeric scalar param (k·Δ); an interpolation is a callable param applied as k(Δ). kind: 1=axial, 2=shear, 3=torsion, 4=bending.
SymbolicAWEModels.timoshenko_rigidity — Function
timoshenko_rigidity(joint, params, field, arg)Effective rigidity for one TimoshenkoJoint mode, read as a flat parameter: a Real rigidity is a numeric scalar param used directly; a callable is a callable param evaluated at the mode's strain/curvature arg. field is one of :EA, :GA, :GJ, :EIy, :EIz.
Initial conditions
SymbolicAWEModels.InitialEntry — Type
InitialEntryOne initial-condition binding: the scalar state-variable terms vars (e.g. [pos[1,i], pos[2,i], pos[3,i]]) and a read(sys_struct) returning their live value (scalar or vector, element-aligned with vars).
SymbolicAWEModels.InitialRegistry — Type
InitialRegistryBuild-time record of every bind_initial! call. Transient: it lives only during equation generation and drives build_initial_sync after compilation.
SymbolicAWEModels.InitialView — Type
Top-level initial view wrapping an InitialRegistry. initial.points[i].pos_w mirrors sys_struct.points[i].pos_w.
SymbolicAWEModels.InitialPath — Type
A partial path into sys_struct identifying an initial-condition field.
SymbolicAWEModels.bind_initial! — Function
bind_initial!(initial_path, state_var) -> Vector{Pair}Record that struct field initial_path (e.g. initial.points[i].pos_w) provides the initial condition for state_var (a scalar state term, a collected vector, e.g. pos[:, i], or a matrix, e.g. R_b_to_w[:, :, i] — arrays are flattened column-major and must align element-wise with the read value). Returns the constant default pair(s) (build-time numeric value) to splice into the system defaults, which makes MTK expose a settable Initial(state_var).
SymbolicAWEModels.ElementReader — Type
ElementReader(base, index)Serialisable reader returning the index-th element of base(sys_struct). Used to map an array-valued struct field onto per-element Initial parameters (scalars are indexed at 1, which is a no-op).
SymbolicAWEModels.InitialSync — Type
InitialSyncA setp setter for a list of Initial(state) parameters, their per-element readers, and a preallocated value buffer.
SymbolicAWEModels.build_initial_sync — Function
build_initial_sync(sys, registry) -> InitialSync | NothingBuild the initial-condition sync from the compiled system and the registry. Each bound variable is routed by how it survived mtkcompile: a surviving unknown's u0 is written directly (setu) — under build_initializeprob=false there is no init solve to apply its Initial(x) parameter, so setting that parameter alone would leave u0 stale — while an observed variable that mtkcompile solves as an init constraint is set through its Initial parameter. Variables removed entirely are dropped.
SymbolicAWEModels.normalize_param_name — Function
normalize_param_name(x) -> StringCanonical name of a symbolic variable/parameter for matching a compiled parameter back to a defaults entry: strips the (t) time argument, the var"…" wrapper, an aliasing #N suffix, and whitespace, so var"tether_len[1]#0"(t) and (tether_len(t))[1] both become tether_len[1].
SymbolicAWEModels.missing_param_defaults — Function
missing_param_defaults(sys, defaults) -> Vector{Pair}Operating-point pairs for every compiled parameter of sys that has no default, matched by normalize_param_name to a value in defaults. Restores values for the aliasing artefacts (e.g. tether_len[1]#0, from a single-segment tether's l0↔tether_len alias) that ModelingToolkitBase ≥ 1.58 leaves missing at eager MTKParameters construction — the old raw-variable default no longer reaches the renamed parameter. sync_params!/sync_initial! overwrite these each reinit!, so the value only needs to be a consistent placeholder.
SymbolicAWEModels.sync_initial! — Function
sync_initial!(sync, prob, sys_struct)Copy every bound initial condition from the live sys_struct onto prob — both the Initial parameters and the directly-set unknown u0 values. Must run before a fresh init/solve (a reinit! with reinit_dae does not re-read them). A no-op when there are none.
Inflated-tube rigidity internals
SymbolicAWEModels.BREUKELS_BENDING — Constant
Breukels bending correlation constants C1-C8.
SymbolicAWEModels.BREUKELS_COLLAPSE — Constant
Breukels collapse-deflection correlation constants C9-C12.
SymbolicAWEModels.BREUKELS_TORSION — Constant
Breukels torsion correlation constants C13-C19.
SymbolicAWEModels.check_tube_geometry — Function
check_tube_geometry(radius, pressure)Error when radius [m] or pressure [bar] is not positive. Both Breukels correlations describe a real tube, and the torsion factor c2 takes log(pressure).
SymbolicAWEModels.breukels_tip_force_coefficients — Function
breukels_tip_force_coefficients(radius, pressure) -> (asymptote, slope)Saturation force [N] and initial slope [N] of the Breukels 1 m cantilever curve P(δ) = asymptote·(1 - exp(-(slope/asymptote)·δ)) (radius [m], pressure [bar]). Both are errors when non-positive: that is how the empirical fit announces it is outside the range it was made in, and letting it through gives a negative bending rigidity. The slope changes sign below a radius of roughly -(C8 + C6·pressure)/C7, about 38 mm at 0.25 bar and 34 mm at 0.5 bar.
SymbolicAWEModels.comer_levy_h — Function
Slack-section geometry factor h(θ) = sin θ + (π − θ) cos θ, monotone π → 0.
SymbolicAWEModels.comer_levy_n — Function
Slack-section geometry factor n(θ) = 2π − 2θ + sin 2θ.
SymbolicAWEModels.comer_levy_point — Function
comer_levy_point(θ, radius, pressure, membrane_stiffness) -> (κ, M)Curvature [1/m] and moment [N·m] of the Comer-Levy wrinkled section at slack angle θ ∈ (0, π): κ = π·p / (2·E·t·h(θ)) (radius-independent) and M = (p·r³/4)·π·n(θ)/h(θ).
SymbolicAWEModels.comer_levy_sample_curve — Function
comer_levy_sample_curve(radius, pressure, membrane_stiffness; n=60, frac_max=0.97)
-> (κ, M)Synthetic moment-curvature table [1/m], [N·m] for a tube of radius: linear samples below wrinkling plus exact Comer-Levy wrinkled-section samples up to frac_max of the collapse moment.
SymbolicAWEModels.fit_bending_law — Function
fit_bending_law(κ, M, moment_knee, moment_collapse)
-> (EI0, curvature_knee, exponent)Least-squares fit of the smooth TubeRigidityLaw bending law to sampled (κ, M) given the wrinkling/collapse anchors: EI0 from the linear region (M < moment_knee), curvature_knee = moment_knee/EI0, and the post-wrinkling exponent from fit_softening_exponent on the tail. No optimisation dependency.
SymbolicAWEModels.SOFTENING_EXPONENT_RANGE — Constant
Bounds fit_softening_exponent searches the post-knee decay exponent in.
SymbolicAWEModels.bending_softening — Function
bending_softening(excess_curvature, exponent, knee_slope) -> Float64Post-knee moment deficit (M_c − M)/(M_c − M_w) at excess_curvature = κ/κ_w − 1. Leaves the knee at slope −knee_slope and decays as κ^-exponent.
SymbolicAWEModels.fit_softening_exponent — Function
fit_softening_exponent(curvature, moment, curvature_knee, moment_knee,
moment_collapse) -> Float64Decay exponent of bending_softening minimising the relative moment error over sampled post-knee (curvature, moment). Golden-section search over SOFTENING_EXPONENT_RANGE, since the exponent enters the curvature scale too.
Other internals
SymbolicAWEModels.init_principal_frame! — Function
init_principal_frame!(bodies, points)Compute principal frame ODE state from body frame. Must be called after body frame (pos_w, R_b_to_w, vel_w, ω_b) is fully initialized.
Sets: com_w, Q_p_to_w, com_vel, ω_p (derived from body frame), and pos_b for RIGID_DYNAMICS wing points (body frame, relative to COM).
SymbolicAWEModels.init_body_frame_from_ref_points! — Function
init_body_frame_from_ref_points!(wing, points; prn=true)Initialize wing body frame (Rbtoc, poscad) from z/y reference points. Shared by VSMWing PARTICLE_DYNAMICS and PlateWing.
SymbolicAWEModels.get_rot_pos_cad — Function
get_rot_pos_cad(transform::Transform, bodies, points)Get the CAD-frame position of the rotating object (body or point). Used by get_base_pos to compute the translation offset for chained transforms.
KiteUtils.Logger — Method
KiteUtils.Logger(sam::SymbolicAWEModel, steps::Int)Constructs a Logger from a SymbolicAWEModel with the correct number of points.
This convenience constructor automatically calculates the total number of points including VSM panel corners (4 corners per panel) and creates a Logger with the appropriate size.
Arguments
sam::SymbolicAWEModel: The AWE model to create a logger for.steps::Int: The number of time steps to allocate for logging.
Returns
Logger: A new logger with size for all points including panel corners.
Example
logger = Logger(sam, 1000) # Instead of Logger(length(sam.sys_struct.points), 1000)