Internals

Non-exported names that the docstrings of the other API pages refer to.

RobustKiteControllers.IDENTIFICATION_STEPS — Constant

The identification steps of "Re-identifying after a change of the kite" (documentation page "Examples - identification"), in order: the key each step's kite_id is recorded under and the script that writes it. Step 1 records it in every row of the turn-rate table, steps 2 – 5 in the provenance: section of the course-loop model file.

source
RobustKiteControllers.KITE_SETTINGS_KEYS — Constant

The values of a project's kite settings that make up the kite in kite_fingerprint: the physics of the wing, its bridle and its rigging. Left out are the solver's options (backend, analytic_jacobian, vsm_interval), the start-up (init_mode, init_state, remake_*, brake) and the keys no run of this package reads (body_sim_damping, wing_mass, wing_mass_le_frac).

source
RobustKiteControllers.RETIRED_YAML_KEYS — Constant

Keys that were removed from the settings structs, each with the one value the code now always behaves as: the shape parameters C and D of figure_eight_path, the TrajOptSettings fields that no run switched on, and the switches every run had on; traj_opt_settings.jl adds the fixed parameters of the optimizer client. Archived settings files still carry them.

source
RobustKiteControllers.attractor_floor — Function
attractor_floor(fcs::FC_Settings, l_tether) -> Float64

The floor [deg] of the attractor lead at tether length l_tether [m]: fcs.pattern.attractor_dist, scaled by attractor_dist_ref_length / l_tether when fcs.pattern.attractor_dist_ref_length > 0. Scaled, the floor is a fixed arc LENGTH, attractor_dist in rad times the reference length, so the guidance rate it sets no longer falls as the tether grows.

source
RobustKiteControllers.gate_candidate — Function
gate_candidate(tos, c) -> (; verdict, reason, detail, raise, low)

Judge the candidate c, a NamedTuple of what was measured on the reply as it will be flown:

  • margin: curvature margin at the current length l_now; opt_r_reply, r_span and opt_r_min describe the optimizer's own measure of the same curve, for the message.
  • clearance [m] and chk_el_min [deg]: lowest height and elevation of the path, against tos.gates.min_height and el_floor.
  • folds: whether the blend from the path in the air folds over itself.
  • new_pred, opt_power_pred, prev_install_pred [W]: predicted power of the candidate, of the startup path and of the previous install, and power_gate_off, whether the power gates are bypassed for this candidate.
  • size: the candidate's growth against the previous install (growth, az_ratio, el_ratio).
  • blend_attempt: how many cold restarts this cycle has spent already.

verdict is :accept; :retry, to ask for a fresh reply (reason says why, low whether it is about height and raise, for a height shortfall, how many degrees to add to the elevation floor of the next request, else nothing); or :reject, to give up on this cycle with detail as the event text. The checks run in the order of the gates of data/traj_opt.yaml: turn margin (never retried), clearance, elevation floor, blend fold or power, size growth.

source
RobustKiteControllers.wants_challenge — Function
wants_challenge(tos, c) -> Bool

Whether an ACCEPTED reply looks like a step into a worse basin and is cross-checked by a cold solve (challenge_growth): it GREW the pattern by more than tos.reopt.challenge_growth against the previous install and predicts LESS power than that install. c holds size, new_pred and prev_install_pred as in gate_candidate; a NaN prev_install_pred (before the first re-optimization has installed a path) never asks.

source
RobustKiteControllers.depower_command! — Function
depower_command!(st, setup, plant, t, phase_before, phase, rel_depower) -> (rel_depower, phase)

The depower actually flown: the optimizer's, ramped over path_blend_time in phases 3 and 4; phase 5 once the reel-out is done; the soft-stop ramp toward depower_final; and the phase-5 force limiter.

source
RobustKiteControllers.depower_seed — Function
depower_seed(tos, wind_speed) -> Float64

The power-tape length l_dp [m] a request STARTS from, tos.seed.input_depower plus tos.seed.input_depower_per_wind per m/s of wind above tos.seed.input_depower_wind_ref, clamped to DEPOWER_SEED_BOUNDS and, below that, to tos.seed.input_depower_seed_max when it is set.

A seed landing exactly on DEPOWER_SEED_BOUNDS' hard ceiling is itself a failure mode, not just a value: measured 2026-08-20 at 150 m / 10 m/s, the ramp wants 2.35 m, clamps to 2.3 m, and /step then runs to IPOPT's iteration cap with no room left to move — input_depower_seed_max caps the ramp short of that ceiling instead.

Only a seed — depower_mode stays "optimize" and the server moves it — but the seed is what decides whether the solve gets anywhere, because the AoA cap of 14° is already binding at 6 m/s and the solve has to start on the feasible side of it. Measured 2026-08-20 at 150 m: from 1.6 m, 6 m/s solved stage 1 of /step in 0.85 s and 21 iterations; 8 m/s hit the iteration cap with AoA at -25…88° and took the constrained stage 2 down with it (422). Stage 1 carries no turn-radius constraint, so this is not the min_turn_radius request going too far — the same 12.20 m ask solved at 6 m/s eighteen minutes earlier.

ONE-SIDED on purpose. Less wind would want less tape, but every run of the 5.0-7.0 m/s scan in docs/wind_scan_results.yaml converged from 1.6 m, and seeding those lower would move answers that are already measured. The ramp only adds where nothing has been flown.

The slope is anchored on two points, so treat it as provisional: 1.6 m is the largest seed known to converge (7.0 m/s, that scan) and 1.85 m is what 8 m/s was first solved with. A third measured wind should replace it rather than extend it.

source
RobustKiteControllers.elevation_min_request — Function
elevation_min_request(fcs, tos, l_tether; extra = 0.0) -> Union{Float64, Nothing}

The elevation floor [deg] to send with a request made for tether length l_tether: the highest of what the gates will demand there — asind(tos.gates.min_height/l_tether) for the clearance one and fcs.run.min_elevation + tos.gates.candidate_elevation_margin for the elevation one — plus extra. nothing asks for nothing and leaves the optimizer's own 0.6°.

Inverting path_min_height at the length being asked for is what makes the request and the gate the same question: AWETrim constrains HEIGHT and reaches its floor at the END of the lap's reel-out, while the reply is installed as angles at the anchor and judged there. The floor FALLS as the tether grows, so it belongs with every request and not in the session — see the analogous argument for min_turn_radius at the request site.

extra is what a retry raises the floor by after a reply was gated out, measured off that reply and carried forward: the shortfall is structural and the next length has it too.

source
RobustKiteControllers.min_turn_radius_request — Function
min_turn_radius_request(fcs, tos; scale = 1.0, c1 = nothing,
                        margin = tos.gates.min_feasibility_margin) -> Union{Float64, Nothing}

margin in METRES — margin/(c1*max_steering), the kite's own physical turning limit scaled by the margin — sent WITH the request so the optimizer cannot answer with a pattern that is about to be rejected. Defaults to tos.gates.min_feasibility_margin; pass a smaller value to ask for less than the full gate, e.g. a graduated startup retry. nothing — send no constraint — when margin is 0, which is also where the gate is off.

scale asks for MORE than that, and a reel-out run has to: the two numbers do not measure the same curve at the same radius, and the difference is NOT in the run's favour. Pass reelout_anchor_ratio(table) * tos.gates.turn_radius_headroom.

The optimizer enforces R = r/|kappa| at each node's OWN radius, and r grows through the lap — it starts at the anchor and reels out. The run installs the reply as a fixed (azimuth, elevation) curve and flies it at the ANCHOR, where the same angular curvature is physically tighter by L/r. Measured 2026-08-19 on a reply anchored at 380 m: distance_radial spanned 380.0 -> 415.0 m, the tightest node sat at r = 411 m with R = 12.32 m, and that curve at the 380 m anchor is 11.38 m — 0.92x. The factor is L/(L+dL) with dL the lap's reel-out, so it BITES HARDEST AT THE SHORT END: ~0.87 at 220 m against ~0.92 at 380 m. reelout_anchor_ratio measures 1 + dL/L off a reply and is the geometric half of scale.

On top of that the gate re-estimates the curvature from the reply's ~99 points (path_radius_profile, a finite difference on the resampled polyline) and reads ~5 % tighter than the exact anchor value — 10.81 m against 11.38 m on the same reply — and the run then adds el_offset_wing before checking, which compresses the azimuth axis by cos(elevation) a little more. tos.gates.turn_radius_headroom covers that half.

Both together are why an unscaled request came back at margin 0.63 against a min_feasibility_margin of 0.74, converged and with its constraint satisfied (2026-08-19, L = 220 m). The scaled request costs a slightly wider pattern, i.e. a little power; it does not make the gate any weaker, which still scores the curve that will actually be flown.

The conversion itself is exact: the gate's margin is a RATIO of two angular radii at one tether length, and scaling both by that length turns it into a ratio of physical ones, with the kite's physical minimum 1/(c1*u_s) = 11.35 m at c1 = 0.2752, u_s = 0.32. That much IS length-free — what is not is where the optimizer measures the path's own radius.

c1 defaults to the identified turn-rate table at fcs.course.depower_setpoint, which is the turn authority the pattern is flown with. PASS THE ONE THE GATE WILL USE: from phase 5 the run flies depower_final, where c1 is ~23 % lower (0.2133 against 0.2752), and a request made at the pattern's c1 is then ~23 % short of what the reply will be judged against — measured 2026-08-20, a reply of 12.15 m answering a 10.37 m request and rejected at margin 0.61, which is exactly 0.74 * c1_final/c1 of the 0.79 it would have scored in phase 4. c1_at(feas, phase, c1) is that number.

The table refuses to extrapolate off its grid. A body_damping/depower_setpoint it cannot serve therefore sends no constraint and warns, exactly as the feasibility GATE degrades — an off-grid run loses the advice, not the run.

source
RobustKiteControllers.request_constraints — Function
request_constraints(tos, fcs, inflow, cap_wind, l_opt)
    -> (; turn_radius_reel, opt_r_scale, depower_request, c1_request, opt_r_min, opt_r_on,
         opt_r_sent, opt_box)

The constraints the startup solve must respect, sized at the tether length l_opt that is sent to the optimizer and at the wind inflow.wind_speed (the only field of inflow read): the minimum turn radius opt_r_min (with the anchor ratio L/r and the gate's headroom in opt_r_scale; nothing when off, for margin 0 or an off-grid turn-rate cell) and the pattern box opt_box. opt_r_sent starts as opt_r_min, the radius the startup solve actually CONVERGED at, which the retry ladder bisects toward.

depower_request is the depower the reply will be FLOWN at, which is the c1 the request must be sized at, see min_turn_radius_request. That is the optimizer's own and so unknown before the solve; request_depower_estimate estimates it from the wind aloft (or takes the seed). The setpoint is NOT an estimate: it is the depower the loop is tuned at, typically far more powered, and a request sized there comes back a third too tight and is then gated out for a curvature the kite never had.

source
RobustKiteControllers.request_depower_estimate — Function
request_depower_estimate(tos, wind_speed, cap_wind) -> Float64

The tape length l_dp [m] the startup reply is expected to be flown at, which sizes the turn-radius request (request_constraints). With tos.seed.request_depower at 0 it is the seed, depower_seed(tos, wind_speed), as before 2026-10-05. Otherwise it is request_depower plus request_depower_per_wind per m/s of the wind ALOFT (cap_wind, at pattern_elevation_amplitude_max_wind_height) above request_depower_wind_ref, clamped to DEPOWER_SEED_BOUNDS.

The seed is a poor estimate: it is ramped on the 6 m wind for the solve to CONVERGE, and the sites differ in shear, so Maasvlakte 8 m/s was seeded at 1.85 m and flew 1.42-1.46 m. With the drag of the wing in the plant (wing_drag_coeff) the depower conversion maps a tape length onto more depower, where the kite turns worse, and the request sized at the seed asked for a turn radius no pattern in the box could meet (2026-10-05). Only the estimate is moved; the seed the solve starts from is not.

source
RobustKiteControllers.setup_run — Function
setup_run(inputs; init_model) -> RunSetup

Everything the run READS and never rebinds, built in order: the settings and their overrides, the plant s (built by the caller's init_model(project, project_set, fcs, wpc, sim_time; turbulence, set_overrides), since the package does not depend on the model), the winch and its controllers, the optimizer's conditions and session, the turn-rate laws (c1_at_depower, c1_ctrl_at) and the lobe lift. The script keeps the result in the one global setup and hands it to every function of the run; the loop destructures it (see run_loop!). The fields are documented at RunSetup.

source
RobustKiteControllers.solve_startup_path! — Function
solve_startup_path!(setup, st) -> NamedTuple

The startup solve (solve_startup, a 422 retried from startup_retry_el_offsets in order); its reply goes to st.opt_result. Returns what setup gains from it: the seed it converged from (el_center_seed, startup_seed_offset, guess_az, guess_el) and its wall time, opt_startup_solve_s. The startup solve holds the script; blocking re-optimizations hold the loop (reopt_blocked_s).

source
RobustKiteControllers.startup_feasibility — Function
startup_feasibility(setup, st) -> (; feas, margin5, c1_at_phase, phase5_margin_at)

The gates that refuse the run (check_startup_path) on the installed startup path, at the depower the pattern is FLOWN at (pattern_depower): the kite flies the optimizer's u_d from phase 3 on. Returns the verdicts feas, margin5, the Phase5MarginState of the in-air phase-5 check, and the two laws the loop reads off feas: c1_at_phase(phase, depower | st), the c1 to check a path against at time t, and phase5_margin_at(az, el), what phase 5 will fly a candidate path with.

source
RobustKiteControllers.steering_command! — Function
steering_command!(st, setup, plant, t, chi_set, dmin)
    -> (; rel_steering, rel_depower, phase, u_ff, chi_cmd, w_lim, w_course, err)

Entry state machine, descent limiter, feedback fusion, PID and rel_depower (see CourseController), with the gain scale and the curvature feed-forward, then the depower of depower_command!. The phase is the one after the switch to phase 5 when the reel-out is done.

source
RobustKiteControllers._dist — Function
_dist(az1, el1, az2, el2)

Small-area spherical distance [deg] in the (azimuth, elevation) plane; azimuth differences are compressed by cos(elevation).

source
RobustKiteControllers._path_geometry — Function
_path_geometry(az, el, up_loops)

Turn a closed curve in (azimuth, elevation) [deg] into what the controller holds: (az, el, seg_len, tangent), the cyclic path points [deg], the arc length of each segment [deg] and the path direction at each point [rad]. A duplicated closing point is dropped and the traversal direction is reversed if it does not match up_loops.

source