Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,34 @@

### Changed

- Flies against AWETrim's 2026-11-05 LEI-V3 ROM (`rom_config_aerostructural.yaml`), which
needs AWETrim with the reel-out angle-of-attack box from the ROM's validity (branch
`feat/server-aero-forces`). At the same power tape the two models do not pull the same:
at matched lift + drag resultant V3Kite's tape is the ROM's minus 0.372 m (std 0.010 m,
from `validate_lift_drag.jl` and AWETrim's `examples/server/identify_depower_offset.py`),
so `depower_offset` is -0.074. Power ratio before / after: Cabauw 4 m/s 0.52 / 1.11,
Maasvlakte 4 m/s 0.58 / 1.21; with it Cabauw 7 0.99, Cabauw 10 0.89, Maasvlakte 7 1.07,
Maasvlakte 10 0.96 (2026-11-08). Redo the offset whenever the ROM or V3Kite changes.
- The depower seed bounds are the new ROM's band, 1.5-2.6 m (was 1.1-2.3), and
`input_depower_seed_max` 2.45 m (was 1.85): at 10 m/s the optimum is near 2.4 m and a
seed at 1.85 m cannot be marched.
- The high-wind figure is larger: `pattern_elevation_amplitude_max_high` 17° (was 13°)
from 12.5 m/s at 100 m (was 13.2, so Maasvlakte 10 m/s gets it), with
`guess_el_center_high` 33° (was 30°). With 13° the 10 m/s laps were faster than the
0.29 1/s steering slew.
- A startup wind ramp (`guess.startup_wind_ramp_start`, `startup_wind_ramp_step`; 7 and
1 m/s, `0.0` = off): above the start wind the startup solve is solved there and the wind
stepped up warm through `/step` to the run's. Cabauw 10 m/s failed cold from every seed.
- A high-wind start length next to the low-wind schedule (`low_wind:` section of
`fc_settings_reelout.yaml`, `high_wind_speed` / `high_wind_l_tether`,
`high_wind_l_tether()`): at and above 16 m/s at 100 m reel-out starts at 180 m.
Cabauw 10 m/s (19.3 m/s there) has no feasible startup path at 150 m with the
steering slew and the 50 m height floor; Cabauw 7 and Maasvlakte 10 still start at 150 m.
- Known open: Maasvlakte 4 m/s reels out at a
power ratio of 1.21 but fails the phase-5 transition at the end of reel-out, flying
more powered (rel_depower ~0.19-0.21) than the turn-rate table covers (0.20-0.375),
which should be extended to about 0.18-0.40.

- `bin/install` records the Julia version in `install_version_<version>.txt` and moves
the system image to `bin/kps-image-<version>.so.bak` if it was installed with another
Julia version (or the file is missing); `bin/run_julia` stops if the Julia version
Expand Down
5 changes: 5 additions & 0 deletions data/fc_settings_reelout.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,11 @@ fc_settings:
low_wind_speeds: [4.51, 5.15, 5.80, 6.40] # Wind speed at low_wind_height, ascending [m/s]
low_wind_l_tether: [195.0, 195.0, 195.0, 150.0] # Tether length reel-out starts at; less guidance phase lag [m]
low_wind_guess_el_center: [25.0, 27.0, 27.0, 29.0] # Startup guess; 29° at 195 m lands in a 470 W optimum [deg]
# At and above high_wind_speed (same height) reel-out starts at high_wind_l_tether; 0.0 = off.
# On AWETrim's 2026-11-05 ROM Cabauw 10 m/s (19.3 at 100 m) has no feasible startup path at
# 150 m and flies from 180 m (2026-11-08); Cabauw 7 (13.5) and Maasvlakte 10 (12.9) fly at 150.
high_wind_speed: 16.0 # [m/s] at low_wind_height
high_wind_l_tether: 180.0 # [m]

run: # simulation conditions and pass criteria
vsm_interval: 5 # Steps between VSM aero updates, 1 = tightest; at 10 the identification flights sink at depower 0.375 (2026-10-02) [-]
Expand Down
25 changes: 19 additions & 6 deletions data/traj_opt.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,22 @@ traj_opt:
guess_a: 30.0 # width of the guess; azimuth spans ±guess_a [deg]
guess_b: 12.0 # height of the guess, peak to peak [deg]
guess_el_center: 29.0 # centre elevation of the guess; first knob to move on a 422 [deg]
guess_el_center_high: 30.0 # centre elevation at/above the ref wind (a step, not a ramp) [deg]
guess_el_center_high: 33.0 # centre elevation at/above the ref wind (a step, not a ramp); 33 keeps
# the bottom of the 17° high-wind figure above min_elevation [deg]
guess_el_center_wind_ref: 8.0 # wind at/above which guess_el_center_high applies [m/s]
startup_retry_el_offsets: [-1.0, 2.0, -2.0] # seeds tried in order on a startup 422; [] never retries [deg]
# Above this wind the startup solve starts here and steps the wind up warm to the run's
# (src/startup_path.jl, startup_solve); 0.0 = off. The 2026-11-05 ROM depowers far in
# strong wind: Cabauw 10 m/s failed cold from every seed, solved through 7-8-9-10 m/s.
startup_wind_ramp_start: 7.0 # [m/s at the reference height]
startup_wind_ramp_step: 1.0 # [m/s]

seed: # depower seed of the solve, ramped with the wind
input_depower: 1.6 # depower SEED in metres of tape (server bounds 1.1-2.3), not depower_setpoint [m]
input_depower: 1.6 # depower SEED in metres of tape (the ROM's band 1.5-2.6), not depower_setpoint [m]
input_depower_wind_ref: 7.0 # wind at which input_depower IS the seed [m/s]
input_depower_per_wind: 0.25 # tape added per m/s above that; one-sided [m/(m/s)]
input_depower_seed_max: 1.85 # ramp ceiling, below the server's 2.3 m [m]
input_depower_seed_max: 2.45 # ramp ceiling, inside the 2026-11-05 ROM's 2.6 m (1.85 for the old ROM): at
# 10 m/s the optimum is near 2.4 m and a seed at 1.85 cannot be marched [m]
# Estimate of the tape length the startup reply is FLOWN at, which sizes the turn-radius
# request; 0 takes the seed above. Fitted 2026-10-05 to the startup replies of the base
# runs (Cabauw 4-10, Maasvlakte 8, 11 m/s) against the wind at the caps' height (100 m):
Expand All @@ -36,7 +43,11 @@ traj_opt:
# Added to every reply's converted rel_depower, to bring the mean power_ratio to ~1.0.
# Maasvlakte 2026-10-26: mean power_ratio 0.75 at 0; at -0.013 4/7/10 m/s flew
# 0.97/0.88/0.91, i.e. d ln P / d rel_depower ~ -16, so -0.018 (docs/fig8_tuning_log.md).
depower_offset: 0.0 # [-]
# 2026-11-07, 2026-11-05 ROM: V3Kite tape = ROM tape - 0.372 m (std 0.010 m) at matched
# lift + drag resultant, from validate_lift_drag.jl and AWETrim's
# examples/server/identify_depower_offset.py; -0.372 m / 5 m = -0.074. Power ratios
# 0.89-1.11 at 4/7/10 m/s on both sites; redo whenever the ROM or V3Kite changes.
depower_offset: -0.074 # [-]

box: # pattern box the optimizer solves UNDER; 0.0 keeps the server default (|az| <= 45.8, 0.6 <= el <= 51.6)
pattern_azimuth_max: 28.0 # closes the wide basin seen at 5.5 m/s; good runs fly ±18-25 [deg]
Expand All @@ -48,8 +59,10 @@ traj_opt:
# at 34 ±22° x 16°, RMS d 1.44°, 22.7 instead of 20.4 kW [deg]
pattern_elevation_amplitude_max: 8.5 # largest elevation HALF-SPAN of the figure; 0.0 = off [deg];
# 8.0 left Maasvlakte 3.5 m/s infeasible at 8.12° (2026-09-24)
pattern_elevation_amplitude_max_high: 13.0 # cap at/above the ref wind (a step, not a ramp) [deg]
pattern_elevation_amplitude_max_wind_ref: 13.2 # wind AT the height below at/above which the high cap
pattern_elevation_amplitude_max_high: 17.0 # cap at/above the ref wind (a step, not a ramp); 13 left the
# 2026-11-05 ROM's 10 m/s laps faster than the 0.29 1/s steering slew [deg]
pattern_elevation_amplitude_max_wind_ref: 12.5 # wind AT the height below at/above which the high cap
# (12.5 since 2026-11-07, so Maasvlakte 10 m/s at 12.9 gets it)
# applies: Maasvlakte 10 m/s is 12.9 there and flies under
# 8°, Maasvlakte 11 (14.2) and Cabauw 7-10 (13.5-19.3) need
# 10° or the startup solve loses its basin (2026-09-21);
Expand Down
3 changes: 2 additions & 1 deletion examples/reelout_results.jl
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,8 @@ function power_comparison(setup, st::RunState, sl, rp, p4)
lengths_ro = Float64.(sl.var_10[rp.idx])
fs_ref = free_speed_reference(tos, setup.rcs, setup.inflow, setup.guess_az, setup.guess_el,
lengths_ro; min_turn_radius = st.opt_r_min,
pattern_limits = setup.opt_box)
pattern_limits = setup.opt_box,
steering_limits = setup.opt_steering)
if isnothing(fs_ref)
@warn "free_speed reference: no solve succeeded, omitting it from the summary."
else
Expand Down
2 changes: 1 addition & 1 deletion src/RobustKiteControllers.jl
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export FC_Settings, FC_Course, FC_FeedForward, FC_Pattern, FC_WindRamp, FC_Winch
export set_fc_field!, get_fc_field, winch_force_gains, project_file, fc_settings, load_yaml_fields!
export apply_overrides!
export attractor_distance, guidance_rate, wind_schedule, apply_wind_schedule!
export low_wind_schedule, low_wind_reference, apply_low_wind_schedule!
export low_wind_schedule, high_wind_l_tether, low_wind_reference, apply_low_wind_schedule!
export turn_rate_coeffs_file, course_loop_model_file, kite_correction_file, winch_table_file, traj_opt_settings_file

# Externally optimized flight path (examples/simple_opt_fig8.jl)
Expand Down
36 changes: 31 additions & 5 deletions src/awetrim_client.jl
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,21 @@ Base.@kwdef struct DepowerSpec
value::Union{Float64, Nothing} = nothing # fixed value, or the starting one
end

"""
SteeringLimits(; max = nothing, rate = nothing)

The kite's steering actuator as the optimizer must respect it: `|u_s| <= max` and
`|d u_s / dt| <= rate` [1/s] at every node, in the ROM's steering unit, the
steering-tape half-difference over 1.4 m, which is V3Kite's `rel_steering` too
(`V3_STEERING_GAIN`). The server only NARROWS its own box with them (LEI-V3 ROM:
0.357 and 0.29 1/s), so a path it returns is one the KCU can fly. Built from the
run's settings by [`steering_limits_from`](@ref).
"""
Base.@kwdef struct SteeringLimits
max::Union{Float64, Nothing} = nothing
rate::Union{Float64, Nothing} = nothing
end

"""
DepowerReply

Expand Down Expand Up @@ -120,6 +135,8 @@ Base.@kwdef struct InitParams
depower::Union{DepowerSpec, Nothing} = nothing
min_turn_radius::Union{Float64, Nothing} = nothing # [m], 0/nothing = off
pattern_limits::Union{PatternLimits, Nothing} = nothing
# The KCU's steering range and rate; the session keeps them for every step.
steering_limits::Union{SteeringLimits, Nothing} = nothing
end

Base.@kwdef struct StepParams
Expand Down Expand Up @@ -201,6 +218,9 @@ StructTypes.StructType(::Type{DepowerSpec}) = StructTypes.Struct()
StructTypes.StructType(::Type{PatternLimits}) = StructTypes.Struct()
# Left out of the JSON while unset: a server older than the field rejects the key even as null.
StructTypes.omitempties(::Type{PatternLimits}) = (:climb_angle_max,)
StructTypes.StructType(::Type{SteeringLimits}) = StructTypes.Struct()
StructTypes.omitempties(::Type{SteeringLimits}) = (:max, :rate)
StructTypes.omitempties(::Type{InitParams}) = (:steering_limits,)

# ---------------------------------------------------------------------------
# Failed-request cache
Expand Down Expand Up @@ -255,8 +275,12 @@ The key is a [`stable_hash`](@ref), not Base's `hash`, so it survives a Julia
upgrade. `"v4-"` retires the `hash`-based `"v3-"` keys.
"""
function opt_request_key(p::InitParams)
# `steering_limits` came last and is left out while unset, so the keys cached before it
# stay valid (as `PatternLimits`' `symmetric`).
return "v4-" * stable_hash(Tuple(_key_fields(getfield(p, f))
for f in fieldnames(InitParams) if f !== :name))
for f in fieldnames(InitParams)
if f !== :name &&
!(f === :steering_limits && isnothing(p.steering_limits))))
end

"""
Expand All @@ -276,7 +300,7 @@ stable_hash(x) = bytes2hex(sha256(repr(x)))[1:16]
# holding a `Vector` goes by the vector's identity, not its contents, so two
# equal requests would never share a key.
_key_fields(x::Union{WinchParams, InflowConditions, Trajectory, DepowerSpec, PatternLimits,
StepParams}) =
SteeringLimits, StepParams}) =
Tuple(_key_fields(getfield(x, f)) for f in fieldnames(typeof(x)))
# `symmetric` and `climb_angle_max` came last; each is left out while off, so the keys cached
# before it stay valid.
Expand Down Expand Up @@ -1413,7 +1437,8 @@ end

"""
free_speed_reference(tos, wc, inflow, guess_az, guess_el, lengths;
min_turn_radius = nothing, pattern_limits = nothing)
min_turn_radius = nothing, pattern_limits = nothing,
steering_limits = nothing)
-> (; points, weighted) | nothing

Mean reel-out power an OPTIMAL path could harvest with any winch inside
Expand All @@ -1435,7 +1460,8 @@ counts stay its own. A converged probe is stored as if applied: each is a cold
chain of one step, and it IS the result the caller uses.
"""
function free_speed_reference(tos, wc, inflow, guess_az, guess_el, lengths;
min_turn_radius = nothing, pattern_limits = nothing)
min_turn_radius = nothing, pattern_limits = nothing,
steering_limits = nothing)
n = tos.server.free_speed_reference_points
(n < 2 || isempty(lengths)) && return nothing
lo, hi = extrema(lengths)
Expand All @@ -1454,7 +1480,7 @@ function free_speed_reference(tos, wc, inflow, guess_az, guess_el, lengths;
trajectory = Trajectory(collect(guess_az), collect(guess_el)),
input_depower = depower_seed(tos, inflow.wind_speed),
reg_weight = REG_WEIGHT,
min_turn_radius, pattern_limits)
min_turn_radius, pattern_limits, steering_limits)
reply = chain_init(ref_chain, params)
result = chain_step(ref_chain, StepParams(l, ref_winch, reply.trajectory))
if !isnothing(result.metrics)
Expand Down
8 changes: 8 additions & 0 deletions src/fc_settings.jl
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,9 @@ wind speed at
wind speed the schedule is off and the settings files' own values are flown. Empty
vectors (the default) switch it off. See [`low_wind_schedule`](@ref).

At and above `high_wind_speed` (same height; `0.0`, the default, = off) reel-out starts
at `high_wind_l_tether` instead, see [`high_wind_l_tether`](@ref).

YAML (reel-out): [`fc_settings_reelout.yaml`](@ref example_fc_settings_reelout).

# Fields
Expand All @@ -280,6 +283,11 @@ $(TYPEDFIELDS)
low_wind_l_tether::Vector{Float64} = Float64[]
"Startup guess's centre elevation [deg] (`guess_el_center`)"
low_wind_guess_el_center::Vector{Float64} = Float64[]
"Wind speed [m/s] at `low_wind_height` at and above which reel-out starts at
`high_wind_l_tether`; `0.0` = off"
high_wind_speed::Float64 = 0.0
"Tether length [m] reel-out starts at from `high_wind_speed` on"
high_wind_l_tether::Float64 = 0.0
end

"""
Expand Down
31 changes: 29 additions & 2 deletions src/low_wind_schedule.jl
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,24 @@ function low_wind_schedule(fcs::FC_Settings, v_ref)
return map(c -> c[i] + frac * (c[i + 1] - c[i]), columns)
end

"""
high_wind_l_tether(fcs::FC_Settings, v_ref) -> Union{Float64, Nothing}

The tether length reel-out starts at in strong wind: `fcs.low_wind.high_wind_l_tether`
at and above `high_wind_speed` [m/s] at `low_wind_height`, `nothing` below it and when
`high_wind_speed` is `0.0`. A short tether in strong wind laps a figure big enough for the
steering slew too fast, or puts its bottom under the height floor: on AWETrim's 2026-11-05
ROM Cabauw 10 m/s (19.3 m/s at 100 m) has no feasible startup path at 150 m and flies
from 180 m (2026-11-08).
"""
function high_wind_l_tether(fcs::FC_Settings, v_ref)
lw = fcs.low_wind
(lw.high_wind_speed > 0 && v_ref >= lw.high_wind_speed) || return nothing
lw.high_wind_l_tether > 0 ||
error("low_wind: high_wind_speed is set but high_wind_l_tether is not.")
return lw.high_wind_l_tether
end

"""
low_wind_reference(fcs::FC_Settings, project_set) -> Float64

Expand All @@ -43,7 +61,8 @@ low_wind_reference(fcs::FC_Settings, project_set) =
apply_low_wind_schedule!(fcs, tos, project_set; keep = ()) -> NamedTuple

Overwrite the starting tether length `project_set.l_tether` and `tos.guess.guess_el_center` with
[`low_wind_schedule`](@ref) at [`low_wind_reference`](@ref). A name in `keep` (e.g. the
[`low_wind_schedule`](@ref) at [`low_wind_reference`](@ref), or only the length with
[`high_wind_l_tether`](@ref) in strong wind. A name in `keep` (e.g. the
keys of a run's overrides) is left alone; `tos = nothing` skips the guess, for a caller
without the optimizer's settings. Call it once, after the wind-speed override and
before anything is built from the settings. Returns `(; v_ref, values)`, `values`
Expand All @@ -63,8 +82,16 @@ function apply_low_wind_schedule!(fcs::FC_Settings, tos, project_set; keep = ())
end
v_ref = low_wind_reference(fcs, project_set)
values = low_wind_schedule(fcs, v_ref)
isnothing(values) && return (; v_ref, values)
keep = Symbol.(collect(keep))
if isnothing(values)
l_high = high_wind_l_tether(fcs, v_ref)
isnothing(l_high) && return (; v_ref, values)
kept = :l_tether in keep || :l_tethers in keep
kept || (project_set.l_tether = l_high)
@info @sprintf("High-wind start at %.2f m/s (%.0f m): l_tether = %.1f m%s.",
v_ref, lw.low_wind_height, l_high, kept ? " (override kept)" : "")
return (; v_ref, values = (; l_tether = l_high))
end
:l_tether in keep || :l_tethers in keep || (project_set.l_tether = values.l_tether)
isnothing(tos) || :guess_el_center in keep || (tos.guess.guess_el_center = values.guess_el_center)
@info @sprintf("Low-wind schedule at %.2f m/s (%.0f m): l_tether = %.1f m, guess_el_center = %.2f°%s.",
Expand Down
17 changes: 15 additions & 2 deletions src/opt_request.jl
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@ function awetrim_depower_to_v3kite(l_dp, tos)
return Float64(f(l_dp)) + tos.seed.depower_offset
end

"AWETrim's own bounds on `input_depower`, from `src/awetrim/utils/defaults.py` [m]."
const DEPOWER_SEED_BOUNDS = (1.1, 2.3)
"The ROM's depower band, `validity.input_depower` of AWETrim's
`rom_config_aerostructural.yaml`, to which the server clips its `input_depower` box [m].
Its powered end is 1.7 m since 2026-11-10: the 2025 flights stall already there."
const DEPOWER_SEED_BOUNDS = (1.7, 2.6)

"""
depower_seed(tos, wind_speed) -> Float64
Expand Down Expand Up @@ -195,6 +197,17 @@ function min_turn_radius_request(fcs, tos; scale = 1.0, c1 = nothing,
return scale * margin / (coeffs.c1 * fcs.course.max_steering)
end

"""
steering_limits_from(fcs, set) -> SteeringLimits

The KCU as the optimizer must respect it: the course controller's command limit
`fcs.course.max_steering` and the steering tape's rate limit `set.v_steering` [1/s] of
the run's settings. Sent with every `/init`, so the server plans no turn the KCU cannot
fly; its own box is the ROM's (0.357, 0.29 1/s on the LEI-V3).
"""
steering_limits_from(fcs, set) =
SteeringLimits(; max = Float64(fcs.course.max_steering), rate = Float64(set.v_steering))

"""
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,
Expand Down
Loading
Loading