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.TetherSettingsType
TetherSettings

Simulation 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: if true, save PNG files to the video folder
source
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.

source
Tethers.TetherComponents.Point3DFunction
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).

source
Tethers.TetherComponents.TetherFunction
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].

source
Tethers.TetherComponents.FixedEndFunction
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.

source
Tethers.TetherComponents.MovingEndFunction
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.

source
Tethers.TetherComponents.FreeEndFunction
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.

source
Tethers.TetherComponents.assemble_tetherFunction
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.

source

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.StaticSettingsType
StaticSettings

Physical, 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 - 1 nodes are stored, see n_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 to DEFAULT_SOLVER
  • abs_tol::Float64: absolute tolerance of the nonlinear solver [m]
  • rel_tol::Float64: relative tolerance of the nonlinear solver
source
Tethers.QuasiSteady.TetherType
Tether

Mutable 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]
source
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
source
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
source
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
source
Tethers.QuasiSteady.get_initial_conditionsFunction
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
source
Tethers.QuasiSteady.get_analytic_catenaryFunction
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
source

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_nodesFunction
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.

source
Tethers.QuasiSteady.simulate_tetherFunction
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
source
Tethers.QuasiSteady.init_quasisteadyFunction
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 - 1 nodes 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
source
Tethers.QuasiSteady.tether_shapeFunction
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, settings and segments, see simulate_tether
  • pj: (3, n_nodes(segments)) matrix that receives the node positions, or nothing to skip them. Node n_nodes(segments) is the one closest to the ground station, node 1 the last one before the kite attachment point p0.

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
source
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 - 1 nodes 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)
source
Tethers.QuasiSteady.scaled_resFunction
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.

source
Tethers.QuasiSteady.convergedFunction
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.

source
Tethers.QuasiSteady.node_kinematicsFunction
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.

source
Tethers.QuasiSteady.segment_dragFunction
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.

source
Tethers.QuasiSteady.matlab_to_windFunction
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]
source
Tethers.QuasiSteady.wind_to_matlabFunction
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]
source

Utilities

Tethers.display_if_interactiveFunction
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.

source
Tethers.run_pythonFunction
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.

source
Tethers.copy_binFunction
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_julia starts Julia with the settings used for the examples
  • bin/create_sys_image builds a system image for a faster start of the examples
  • test/create_sys_image.jl and test/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.

source
Tethers.copy_fileFunction
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.

source
Tethers.copy_examplesFunction
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.

source
Tethers.example_packagesFunction
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.

source
Tethers.install_examplesFunction
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")
source