API
Tether Component
The composable components used by Tether_10.jl, explained in Re-usable tether component. They live in the submodule Tethers.TetherComponents.
Tethers.TetherComponents.TetherSettings — Type
TetherSettingsSimulation parameters of the composable tether model; same physics and same units as Settings3 of Tether_08.jl.
Fields
g_earth::Vector{Float64}: gravitational acceleration vector [m/s²]v_wind_tether::Vector{Float64}: wind velocity acting on the tether [m/s]rho: air density [kg/m³]cd_tether: drag coefficient of the tether [-]l0: initial tether length [m]v_ro: reel-out speed [m/s]d_tether: tether diameter [mm]rho_tether: density of the tether material (Dyneema) [kg/m³]c_spring: unit spring constant [N]rel_compression_stiffness: relative compression stiffness [-]damping: unit damping constant [Ns]segments::Int64: number of tether segments [-]α0: initial tether angle [rad]duration: duration of the simulation [s]save::Bool: iftrue, save PNG files to thevideofolder
Tethers.TetherComponents.set_diameter! — Function
set_diameter!(se, d; c_spring_4mm = 614600, damping_4mm = 473)Set the tether diameter d [mm] in the settings se and scale the unit spring constant se.c_spring and the unit damping constant se.damping with the cross section, relative to the reference values of a 4 mm tether.
Tethers.TetherComponents.mass_per_meter — Function
mass_per_meter(se)Mass per meter of the tether [kg/m], derived from se.d_tether and se.rho_tether.
Tethers.TetherComponents.l_spring — Function
l_spring(se)Symbolic (time dependent) length of one tether segment [m], taking reel-out into account.
Tethers.TetherComponents.m_end — Function
m_end(se)Symbolic (time dependent) mass of a half tether particle [kg], i.e. the tether mass that a FreeEnd attached to a Tether built with the same settings has to carry.
Tethers.TetherComponents.Point3D — Function
Point3D(; name, pos0=zeros(3))Connector of a point in 3D space, the 3D equivalent of the Flange of the Mechanical.Translational components of the ModelingToolkitStandardLibrary.
Across variable (equal for everything connected to the same node):
pos(t)[1:3]: position [m]
Flow variable (sums up to zero over all components connected to the same node):
force(t)[1:3]: the force that the rest of the system exerts on the component owning this connector [N]
The velocity of a node is not part of the connector; a component that needs it uses D(pos), which keeps the connector balanced (3 across, 3 flow variables).
Tethers.TetherComponents.Tether — Function
Tether(; name, se, POS0=nothing, VEL0=nothing)A tether of se.segments non-linear spring-damper segments with aerodynamic drag and reel-out, with the two end points exposed as the connectors p1 and p2.
The tether owns the se.segments-1 inner particles (full particle mass each) and their equations of motion. It owns no inertia at p1 and p2; there it only reports the force it exerts, so both connectors must be connected to a FixedEnd or a FreeEnd that defines the kinematics of that node and carries the two half particle masses (m_end).
POS0 and VEL0 are 3 × (se.segments+1) matrices of initial positions and velocities of all particles; they default to the straight line between the connector defaults and zero.
The particle positions are available as the array variable pos(t)[1:3, 1:se.segments+1], the velocities as vel, and the segment lengths as len(t)[1:se.segments].
Tethers.TetherComponents.FixedEnd — Function
FixedEnd(; name, pos0)Holds the node it is connected to at the fixed position pos0, e.g. a winch or a ground anchor. The force needed to do so is the force flowing through its connector flange.
Tethers.TetherComponents.MovingEnd — Function
MovingEnd(; name, pos0, pos_expr)Imposes a prescribed motion on the node it is connected to, e.g. a kite whose flight path is known in advance. pos_expr is a length-3 vector of expressions in the symbolic time t (see ModelingToolkit.t_nounits); wherever the rest of the system needs the velocity of this node, it takes D(pos), so differentiating pos_expr symbolically is enough - no separate velocity or acceleration needs to be supplied. pos0 is only the numeric guess for the connector, typically pos_expr evaluated at t = 0.
Tethers.TetherComponents.FreeEnd — Function
FreeEnd(; name, se, m_extra=0.0, pos0, vel0=zeros(3))A point mass at the end of (or between) tethers, free to move under gravity and the forces flowing in through its connector flange.
Its mass is m_extra plus one half tether particle mass per tether attached, i.e. n_tethers * m_end(se); pass the number of tethers connected to it as n_tethers and use the same settings se as for those tethers. m_extra is the payload mass, e.g. of a kite.
Tethers.TetherComponents.assemble_tether — Function
assemble_tether(se; end1, end2, POS0, VEL0)Compose one Tether built from se, POS0 and VEL0, connected between the two already constructed end components end1 (attached to p1) and end2 (attached to p2) - each typically a FixedEnd, MovingEnd or FreeEnd. Which kind of boundary condition each end has is the caller's decision; this function only wires the three components together and simplifies the result.
Returns (simple_sys, sys), the mtkcompiled system and the system before simplification.
Quasi-steady model
A quasi-steady tether model: solves for tether shape and forces given the ground-station orientation/tension and the kite's position and velocity. See docs/quasisteady.md for implementation notes. It lives in the submodule Tethers.QuasiSteady.
Public API
StaticSettings, Tether, init! and step! are the entry point: build a StaticSettings, wrap it in a Tether, call init! once, then step! in a loop.
Tethers.QuasiSteady.StaticSettings — Type
StaticSettingsPhysical, structural and solver parameters of the quasi-steady tether model, plus the initial condition used by init!. Nothing in it changes while a simulation runs - see Tether for the state that does.
Fields
- segments::Int64: number of tether segments;
segments - 1nodes are stored, seen_nodes - elevation::Float64: initial elevation angle β [deg]
- azimuth::Float64: initial wind-frame azimuth angle φ [deg]
- l_tether::Float64: initial unstretched tether length [m]
- slack::Float64: initial tether slack,
l_tether = (1 + slack) * kite distance - rho::Float64: density of air [kg/m³]
- g_earth::MVector{Float64}: gravitational acceleration [m/s²]
- cd_tether::Float64: drag coefficient of the tether
- d_tether::Float64: diameter of the tether [mm]
- rho_tether::Float64: density of the tether (Dyneema) [kg/m³]
- c_spring::Float64: unit spring constant [N] (=
E*A) - alg: the nonlinear solver used by
init!/step!, defaults toDEFAULT_SOLVER - abs_tol::Float64: absolute tolerance of the nonlinear solver [m]
- rel_tol::Float64: relative tolerance of the nonlinear solver
Tethers.QuasiSteady.Tether — Type
TetherMutable state of one quasi-steady tether simulation: the settings, the persistent solver state, the boundary conditions of the last step! and its results. All vectors are in the W (wind) reference frame. Buffers are sized from set.segments at construction and reused by every step!, so a stepping loop stays allocation free.
Construct with Tether(se::StaticSettings), then call init! before the first step!.
Fields
- set::StaticSettings: settings, see
StaticSettings - state_vec::MVector{3, Float64}: (β [rad], φ [rad], Tn [N]) at the ground station
- kite_pos::MVector{3, Float64}: kite position of the last step [m]
- kite_vel::MVector{3, Float64}: kite velocity of the last step [m/s]
- wind_vel::Matrix{Float64}: wind velocity per segment of the last step [m/s]
- tether_length::Float64: unstretched tether length of the last step [m]
- tether_pos::Matrix{Float64}: node coordinates of the last step [m]
- force_gnd::Float64: tension at the ground station [N]
- force_kite::MVector{3, Float64}: force on the tether at the kite attachment point [N]
- p0::MVector{3, Float64}: kite-tether attachment point [m]
Tethers.QuasiSteady.init! — Function
init!(te::Tether; prn = false)Initialize te: derive the initial kite position from te.set.elevation, te.set.azimuth and te.set.l_tether, solve the catenary equation for an initial guess of te.state_vec, then run one step! so that te is left in a consistent, solved state. Takes no state arguments - everything comes from te.set.
Keyword arguments
- prn: print the solver statistics of the final
step!
Returns
- te::Tether, initialized and solved
Tethers.QuasiSteady.step! — Function
step!(te::Tether, kite_pos, kite_vel; tether_length = nothing, wind_vel = nothing, prn = false)Move the loose end of the tether to kite_pos/kite_vel and re-solve for the tether shape and forces, using te.state_vec as the initial guess. Writes state_vec, kite_pos, kite_vel, wind_vel, tether_length, tether_pos, force_gnd, force_kite and p0 into te.
Arguments
- te::Tether: the tether, see
Tether - kite_pos::MVector{3, Float64}: kite position in the W frame [m]
- kite_vel::MVector{3, Float64}: kite velocity in the W frame [m/s]
Keyword arguments
- tetherlength: unstretched tether length [m]; defaults to `(1 + te.set.slack) * norm(kitepos)`
- windvel: `(3, nnodes(te.set.segments))
matrix, wind velocity per node [m/s]; defaults tote.wind_vel` - prn: print the solver statistics
Returns
- te::Tether, with all fields above updated
Tethers.QuasiSteady.clear! — Function
clear!(te::Tether)Reset te's persistent state and result buffers to zero, resizing them if te.set.segments has changed since construction. Does not touch te.set. Called by init!; also useful on its own to restart a simulation with the same Tether.
Returns
- te::Tether, cleared
Tethers.QuasiSteady.elevation — Function
elevation(te::Tether)Elevation angle β [rad] at the ground station, from te.state_vec.
Tethers.QuasiSteady.azimuth — Function
azimuth(te::Tether)Wind-frame azimuth angle φ [rad] at the ground station, from te.state_vec.
Tethers.QuasiSteady.tension — Function
tension(te::Tether)Tension Tn [N] at the ground station, from te.state_vec.
Tethers.QuasiSteady.get_initial_conditions — Function
get_initial_conditions(filename)Loads the initialization data for the basic examples and tests. The two angles in stateVec are stored in the MATLAB reference convention and are converted to this package's elevation/wind-frame-azimuth convention via matlab_to_wind before being returned.
Arguments
- filename: the filename of the mat file to read
Returns
- state_vec::MVector{3, Float64} state vector (beta [rad], phi [rad], Tn [N]) tether orientation and tension at ground station
- kite_pos::MVector{3, Float64} kite position vector in wind reference frame
- kite_vel::MVector{3, Float64} kite velocity vector in wind reference frame
- windvel::MMatrix{3, nnodes(segments), Float64} wind velocity vector in wind reference frame, one column per node
- tether_length: Float64 tether length
- settings::StaticSettings struct containing environmental and tether parameters: see
StaticSettings
Tethers.QuasiSteady.get_analytic_catenary — Function
get_analytic_catenary(filename)Loads the analytic catenary curve for the 2D catenary example
Arguments
- filename: the filename of the mat file to read
Returns
- x_cat: x coordinates of the catenary curve
- y_cat: x coordinates of the catenary curve
Private API
Unexported internals that the public API is built on, documented here only because this project's checkdocs = :all requires every docstring in the module to appear in the manual - not part of the recommended interface.
Tethers.QuasiSteady.n_nodes — Function
n_nodes(segments)Number of tether nodes the model stores for a tether of segments segments.
segments counts segments, as in the dynamic mass-spring model, so a tether of segments segments has segments + 1 points. Neither of the two outer ones is stored: the ground station sits at the origin and the kite attachment point is returned separately as p0, which leaves segments - 1 nodes in between.
Tethers.QuasiSteady.check_wind_vel — Function
check_wind_vel(wind_vel, segments)Validate that wind_vel is a (3, n_nodes(segments)) matrix - one column per stored node, see n_nodes - as required by init! and step!. segments always comes from te.set.segments - it is never inferred from size(wind_vel, 2), unlike the internal simulate_tether.
Tethers.QuasiSteady.simulate_tether — Function
simulate_tether(state_vec, kite_pos, kite_vel, wind_vel, tether_length, settings)Function to determine the tether shape and forces, based on a quasi-steady model.
Arguments
- state_vec::MVector{3, Float64}: state vector (beta [rad], phi [rad], Tn [N]); tether orientation and tension at ground station
- kite_pos::MVector{3, Float64}: kite position vector in wind reference frame
- kite_vel::MVector{3, Float64}: kite velocity vector in wind reference frame
- windvel:: (3, nnodes(segments)) MMatrix{Float64} wind velocity vector in wind reference frame, one column per node
- tether_length: tether length
- settings:: StaticSettings struct containing environmental and tether parameters: see
StaticSettings
Keyword arguments
- prn: print the solver statistics
- alg: the nonlinear solver, defaults to
DEFAULT_SOLVER - tetherpos: `(3, nnodes(segments))
matrix that receives the node positions, ornothingto allocate a fresh one (the default). Passing a pre-allocated buffer, as [step!`](@ref) does, avoids that allocation in a stepping loop.
Returns
- state_vec::MVector{3, Float64}: state vector (beta [rad], phi [rad], Tn [N]); tether orientation and tension at ground station
- tether_pos::Matrix{Float64}: x,y,z - coordinates of the tether nodes
- force_gnd::Float64: Line tension at the ground station
- force_kite::MVector{3, Float64}: force from the kite to the end of tether
- p0::MVector{3, Float64}: x,y,z - coordinates of the kite-tether attachment
Tethers.QuasiSteady.init_quasisteady — Function
init_quasisteady(kite_pos, tether_length; kite_vel = nothing, segments = nothing, wind_vel = nothing, settings = nothing)Initialize the quasi-steady tether model providing an initial guess for the state vector based on the numerical solution of the catenary equation
Arguments
- kite_pos::MVector{3, Float64} kite position vector in wind reference frame
- tether_length: Float64 tether length
- kite_vel::MVector{3, Float64} kite velocity vector in wind reference frame
- segments::Int number of tether segments;
segments - 1nodes are stored - windvel::MMatrix{3, nnodes(segments), Float64} wind velocity vector in wind reference frame, one column per node
- settings::StaticSettings struct containing environmental and tether parameters: see
StaticSettings
Returns
- state_vec::MVector{3, Float64} state vector (beta [rad], phi [rad], Tn [N]) tether orientation and tension at ground station
Tethers.QuasiSteady.tether_shape — Function
tether_shape(β, φ, Tn, param, pj)Integrate the quasi-steady tether from the ground station up to the kite and return the gap between the kite and the end of the tether.
The tether is walked one segment at a time, so only the force, drag, velocity and acceleration of the current node are needed; keeping them in SVectors instead of in (3, n_nodes(segments)) buffers makes the whole integration allocation free and lets ForwardDiff run straight through it.
Arguments
- β, φ, Tn: elevation [rad], wind-frame azimuth [rad] and tension [N] at the ground station
- param: named tuple with
kite_pos,kite_vel,wind_vel,tether_length,settingsandsegments, seesimulate_tether - pj:
(3, n_nodes(segments))matrix that receives the node positions, ornothingto skip them. Noden_nodes(segments)is the one closest to the ground station, node 1 the last one before the kite attachment pointp0.
Returns
- res: difference between the kite position and the end of the tether
p0 - T0: force from the kite on the end of the tether
- p0: x,y,z - coordinates of the kite-tether attachment
Tethers.QuasiSteady.res! — Function
res!(res, state_vec, param)Calculates difference between tether end and kite given tether ground segment orientation and magnitude. Thin, mutating wrapper around tether_shape, kept for callers that work with the in-place (res, state_vec, param) signature.
Arguments
- res::Vector{Float64} difference between tether end and kite segment
- state_vec::MVector{3, Float64} state vector (beta [rad], phi [rad], Tn [N]); tether orientation and tension at ground station
- par:: 8-elements tuple:
- kite_pos::MVector{3, Float64} kite position vector in wind reference frame
- kite_vel::MVector{3, Float64} kite velocity vector in wind reference frame
- wind_vel::MMatrix{Float64} wind velocity vector in wind reference frame for each segment of the tether
- tether_length: tether length
- settings:: StaticSettings struct containing environmental and tether parameters: see
StaticSettings - buffers:: (5, ) Vector{Matrix{Float64}} Vector of (3, n_nodes(segments)) Matrix{Float64} empty matrices; only
buffers[3]is used, it receives the node positions - segments:: number of tether segments;
segments - 1nodes are stored - return_result:: Boolean to determine use for in-place optimization or for calculating returns
Returns (if return_result==true)
- res::Vector{Float64} difference between tether end and kite segment
- T0::MVector{3, Float64} force from the kite to the end of tether
- pj:: (3, n_nodes(segments)) Matrix{Float64} x,y,z - coordinates of the tether nodes
- p0::MVector{3, Float64} x,y,z - coordinates of the kite-tether attachment
Example usage
state_vec = rand(3,)
kite_pos = [100, 100, 300]
kite_vel = [0, 0, 0]
wind_vel = rand(3,15)
tether_length = 500
settings = StaticSettings(; rho=1.225, g_earth=[0, 0, -9.806], cd_tether=0.9, d_tether=4,
rho_tether=0.85, c_spring=500000)
res!(res, state_vec, kite_pos, kite_vel, wind_vel, tether_length, settings)Tethers.QuasiSteady.scaled_res — Function
scaled_res(u, param)Residual of tether_shape as seen by the nonlinear solver: u is (beta, phi, log(Tn/tension_scale)), see simulate_tether.
The tension is solved for on a logarithmic scale because it spans decades. A taut tether pulls with 1e5 N, a tether long enough to sag between kite and ground station with a few N, so an initial guess can easily be a factor 1e5 off, and a solver that walks that distance linearly needs an iteration per decade. On top of that the residual is far less sensitive to the tension than to the two angles - a stiff tether hardly stretches - which leaves the Jacobian nearly singular in the tension direction; d(res)/d(log Tn) is Tn * d(res)/dTn, which is of the same order as the two angle columns. Positivity of the tension comes free.
Tethers.QuasiSteady.lin_res — Function
lin_res(u, param)As scaled_res, but with u[3] the tension itself in units of tension_scale. Used only by the fallback solve in simulate_tether.
Tethers.QuasiSteady.converged — Function
converged(sol, tol)Whether a nonlinear solution actually solved the problem. Written so that a NaN residual counts as "not converged" rather than slipping through a > comparison.
Tethers.QuasiSteady.node_kinematics — Function
node_kinematics(ω, p_unit, v_parallel, pos)Velocity and acceleration of a tether node at pos, assuming the tether rotates rigidly with the kite: ω is the angular velocity of the kite position vector, and v_parallel the radial component of the kite velocity along the unit vector p_unit.
Tethers.QuasiSteady.segment_drag — Function
segment_drag(v_app, dir, drag_coeff)Drag force on one tether segment with unit direction dir and apparent wind velocity v_app: drag_coeff * |v_n| * v_n, with v_n the component of v_app normal to the segment. Below 1 mm/s of apparent wind the drag is zero by definition, which also keeps the normal direction well defined.
Tethers.QuasiSteady.matlab_to_wind — Function
matlab_to_wind(θ_m, φ_m)Convert the tether angles at the ground station from the MATLAB reference convention, dir ∝ [sin(θ)cos(φ), sin(φ), cos(θ)cos(φ)], to the convention used throughout this package: elevation measured up from the horizontal plane, azimuth in the wind reference frame, positive anti-clockwise seen from above.
Arguments
- θ_m: MATLAB-convention angle from the vertical [rad]
- φ_m: MATLAB-convention azimuth [rad]
Returns
- (β, φ): elevation and wind-frame azimuth [rad]
Tethers.QuasiSteady.wind_to_matlab — Function
wind_to_matlab(β, φ)Inverse of matlab_to_wind: convert elevation/wind-frame-azimuth angles back to the MATLAB reference convention, for regenerating .mat reference fixtures.
Arguments
- β: elevation, measured up from the horizontal plane [rad]
- φ: azimuth in the wind reference frame, positive anti-clockwise seen from above [rad]
Returns
- (θm, φm): MATLAB-convention angle from the vertical and azimuth [rad]
Utilities
Tethers.display_if_interactive — Function
display_if_interactive(x)
display_if_interactive(f, args...; kwargs...)In the first form, display x (e.g. a PlotX figure) only when running in an interactive session and not on CI.
In the second form, call f(args...; kwargs...) only when running in an interactive session and not on CI. Use this for functions like plot2d that open a window themselves and return nothing.
Tethers.run_python — Function
run_python(name)Run the Python version of the example name, e.g. run_python("Tether_01") for the script examples/python/Tether_01.py, in the Python environment managed by CondaPkg.
The script is located relative to this package, but writes its results to output/ relative to the current working directory, so run it from the package directory.
Tethers.copy_bin — Function
copy_bin(; overwrite=true)Copy the scripts needed to run the examples of an installed Tethers package to the folders bin and test in the current working directory (they will be created if they don't exist):
bin/run_juliastarts Julia with the settings used for the examplesbin/create_sys_imagebuilds a system image for a faster start of the examplestest/create_sys_image.jlandtest/test_for_precompile.jl, which it uses
The two scripts that build the system image exist in a variant for a clone of this repository and in a variant for an installed package; the latter have a 2 in their name and are copied without it. The remaining scripts in bin are specific to a clone of this repository and are not copied. Pre-built system images (*.so) are not copied either.
Tethers.copy_file — Function
copy_file(relpath, src_file, dst_file; overwrite=true)Copy src_file from the folder relpath of this package to dst_file in the folder relpath of the current working directory (it will be created if it doesn't exist). Used for the scripts that exist in a variant for a clone of this repository and in a variant for an installed package.
Tethers.copy_examples — Function
copy_examples(; overwrite=true)Copy all example scripts (Julia and Python) to the folder examples (it will be created if it doesn't exist). The examples/Project.toml of this package is not copied, since it points back at this package via a relative [sources] path that would not resolve in the destination. Any local Manifest.toml / Manifest-v*.toml left over from instantiating that Project.toml is skipped as well.
Tethers.example_packages — Function
example_packages()Return the names of the registered packages required to run the example scripts, read from examples/Project.toml. Tethers itself and standard library packages (currently only LinearAlgebra) are excluded, since they do not need to be added.
Tethers.install_examples — Function
install_examples(add_packages=true)Install the example scripts into the current working directory.
This copies the examples and bin folders. If add_packages is true, it also installs the packages used by the example scripts. Run the examples with:
include("examples/menu.jl")