diff --git a/.JETLSConfig.toml.default b/.JETLSConfig.toml.default index 31945eb..ae4d9c2 100644 --- a/.JETLSConfig.toml.default +++ b/.JETLSConfig.toml.default @@ -5,3 +5,7 @@ match_by = "code" match_type = "literal" severity = "off" path = "examples/**/*.jl" + +# No inferred-type inlay hints in the editor; the `end`-tag hints stay. +[inlay_hint.types] +enabled = false diff --git a/bin/install_awetrim b/bin/install_awetrim index 2675730..c6ce605 100755 --- a/bin/install_awetrim +++ b/bin/install_awetrim @@ -2,19 +2,25 @@ # Copyright (c) 2026 Uwe Fechner # SPDX-License-Identifier: MIT # -# bash script to install the develop branch of https://github.com/awegroup/AWETrim -# in the folder ../AWETrim +# bash script to install the feat/server-aero-forces branch of +# https://github.com/awegroup/AWETrim in the folder ../AWETrim. That branch is +# develop plus the POST /aero_forces endpoint examples/validate_lift_drag.jl +# needs; switch BRANCH back to develop once it is merged. # # AWETrim is a Python project, so it gets its own venv with the dev and server # extras installed in editable mode; bin/run_server (here and in AWETrim) expects -# exactly that layout: ../AWETrim/venv/bin/python. +# exactly that layout: ../AWETrim/venv/bin/python. With uv in PATH, uv creates +# the venv and installs into it; without it, python3's venv and pip are used, +# and pip is bootstrapped first in a venv that lacks it (one uv created). # # The checkout is a sibling of this repository, not a subdirectory, so it is # shared with the other workspaces that talk to the same server. # # Re-running the script is safe: an existing checkout is fetched and fast-forwarded # rather than re-cloned, and the venv is only created if it is missing. A checkout -# with local modifications is left alone apart from the dependency install. +# with local modifications is left alone apart from the dependency install. A +# branch that exists only in the local checkout (not pushed yet) is checked out +# as it is. # # At the end the optimizer's two caches in output/ are cleared (the failed-request # cache opt_failure_cache.yaml and the solution cache opt_chain_cache/): their keys @@ -24,7 +30,7 @@ set -euo pipefail REPO_URL="https://github.com/awegroup/AWETrim" -BRANCH="develop" +BRANCH="feat/server-aero-forces" TARGET="../AWETrim" if [[ $(basename "$(pwd)") == "bin" ]]; then @@ -40,7 +46,7 @@ for cmd in git python3; do fi done -if ! python3 -c "import venv" >/dev/null 2>&1; then +if ! command -v uv >/dev/null 2>&1 && ! python3 -c "import venv" >/dev/null 2>&1; then echo "ERROR: the python3 venv module is missing." >&2 echo "On Debian/Ubuntu install it with: sudo apt install python3-venv" >&2 exit 1 @@ -49,12 +55,23 @@ fi # --- clone or update ---------------------------------------------------------- if [[ -d $TARGET/.git ]]; then echo "Found an existing checkout in $(cd "$TARGET" && pwd), updating it ..." - git -C "$TARGET" fetch origin "$BRANCH" + on_origin=false + if git -C "$TARGET" ls-remote --exit-code --heads origin "$BRANCH" >/dev/null 2>&1; then + on_origin=true + git -C "$TARGET" fetch origin "$BRANCH" + elif ! git -C "$TARGET" rev-parse --verify --quiet "refs/heads/$BRANCH" >/dev/null; then + echo "ERROR: branch $BRANCH is neither on origin nor in the local checkout." >&2 + exit 1 + fi if [[ -n $(git -C "$TARGET" status --porcelain) ]]; then echo "WARNING: the checkout has local modifications, skipping the branch update." >&2 else git -C "$TARGET" checkout "$BRANCH" - git -C "$TARGET" merge --ff-only "origin/$BRANCH" + if $on_origin; then + git -C "$TARGET" merge --ff-only "origin/$BRANCH" + else + echo "Branch $BRANCH is not on origin; using the local branch as it is." + fi fi elif [[ -e $TARGET ]]; then echo "ERROR: $TARGET exists but is not a git repository." >&2 @@ -70,13 +87,25 @@ cd "$TARGET" # --- virtual environment ------------------------------------------------------ if [[ ! -x venv/bin/python ]]; then echo "Creating the virtual environment in $(pwd)/venv ..." - python3 -m venv venv + if command -v uv >/dev/null 2>&1; then + uv venv venv + else + python3 -m venv venv + fi fi echo "Using $(venv/bin/python --version)" echo "Installing AWETrim with the dev and server extras ..." -venv/bin/pip install --upgrade pip -venv/bin/pip install -e '.[dev,server]' +if command -v uv >/dev/null 2>&1; then + uv pip install --python venv/bin/python -e '.[dev,server]' +else + # A venv created by uv has no pip. + if ! venv/bin/python -m pip --version >/dev/null 2>&1; then + venv/bin/python -m ensurepip --upgrade + fi + venv/bin/python -m pip install --upgrade pip + venv/bin/python -m pip install -e '.[dev,server]' +fi # --- check --------------------------------------------------------------------- if ! venv/bin/python -c "import fastapi, uvicorn" 2>/dev/null; then @@ -89,4 +118,5 @@ echo "Clearing the optimizer caches in $RKC_ROOT/output ..." rm -f "$RKC_ROOT/output/opt_failure_cache.yaml" rm -rf "$RKC_ROOT/output/opt_chain_cache" -echo "Done. Start the AWETrim server with bin/run_server" +echo "Done. Start the AWETrim server with bin/run_server start;" +echo "a server that is already running keeps the old code until bin/run_server restart." diff --git a/data/fc_settings_reelout.yaml b/data/fc_settings_reelout.yaml index 44bde6e..4d83142 100644 --- a/data/fc_settings_reelout.yaml +++ b/data/fc_settings_reelout.yaml @@ -88,7 +88,8 @@ fc_settings: winch: # compliance of the force-mode winch (its gains are in wc_settings.yaml) and force guards compliance: 0.0 # Fixed at POSITION mode; REEL_OUT needs the drum free [-] entry_f_min: 350.0 # Entry-guard (guard_lfc) force floor, phases 0-2; not wc's f_low [N] - first_lap_force_frac: 0.93 # f_high x this in lap 1 AND for the startup solve; 1.0 = no lap-1 de-rating [-] + first_lap_force_frac: 0.96 # f_high x this in lap 1 AND for the startup solve; 1.0 = no lap-1 de-rating; + # 0.93 until 2026-10-31, see docs/fig8_tuning_log.md [-] reelout: # REEL_OUT winch and phase 5; kv/f_low/f_high/v_sat are in wc_settings.yaml reelout_l_max: 380.0 # Length at which reel-out stops; AWETrim's reopt ceiling is ~325 m [m] @@ -123,6 +124,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] + # With the 12.62 kg wing the Maasvlakte 11 m/s (14.2 at 100 m) startup has no solution at 150 m + # and converges from 160 m up (2026-10-31); 10 m/s (12.9) flies at 150 m. + high_wind_speed: 14.0 # Wind speed at low_wind_height from which reel-out starts at the length below; 0 = off [m/s] + high_wind_l_tether: 170.0 # Tether length reel-out starts at from high_wind_speed on; 160 failed the curvature + # gate at Maasvlakte 11 m/s, 180 laps >= 2.5 at Cabauw 8 m/s (2026-10-31) [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) [-] @@ -133,3 +139,7 @@ fc_settings: entry_time: 62.0 # Settle time after park_time before the metrics [s] min_elevation: 6.5 # Floor criterion over the WHOLE run; also the reopt install gate [deg] min_span_frac: 0.7 # Pattern-SIZE criterion, fraction of f8_a / f8_b [-] + min_laps: 2.5 # Laps criterion: figure-eight laps in the scored window [-] + min_laps_high: 2.0 # The same from min_laps_high_wind on: Cabauw 9 and 10 m/s fly 30-32 s laps from + # their shortest feasible starts, 170 and 180 m, i.e. 2.0 laps (2026-11-01) [-] + min_laps_high_wind: 16.5 # Wind speed at low_wind_height from which min_laps_high applies; 0 = off [m/s] diff --git a/data/traj_opt.yaml b/data/traj_opt.yaml index af0c757..f7f4fc1 100644 --- a/data/traj_opt.yaml +++ b/data/traj_opt.yaml @@ -12,17 +12,27 @@ traj_opt: free_speed_reference_points: 6 # free_speed solves for the reference power AFTER the run; 0 = off [-] guess: # initial guess; seeds a multi-modal solve: changes the answer, not the run time - 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_a: 32.0 # width of the guess; azimuth spans ±guess_a [deg] + guess_b: 14.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_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] + # Guess sizes [guess_a, guess_b] tried at the base centre on a startup 422, before the offsets. + # rom_config_aerostructural2 (2026-10-28): 32x14 converges at 5, 6, 8, 8.25 m/s, 30x13 at 7, 8, + # 8.25 and 34x15 at 5, 6, 7, 8.25 m/s; no single size covers all; 26x11 at 5 m/s with the + # 12.62 kg wing (bridle 1.62 kg, 2026-10-30) where all the others fail [deg] + startup_retry_guess_sizes: [[30.0, 13.0], [34.0, 15.0], [26.0, 11.0]] + # Added to the starting tether when every seed above fails, and the model set up again there. + # Cabauw 4 m/s (2026-10-31) has no startup solution at 150, 155 or 160 m and converges at 165-175 m; + # Cabauw 10 m/s none at 160, 170 or 175 m and one at 180 m [m] + startup_retry_length_steps: [10.0, 20.0] 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.5 # depower SEED in metres of tape (server bounds 1.1-2.3), 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_per_wind: 0.0 # tape added per m/s above that; one-sided; 0.25 seeded 8 m/s at 1.75 m, which + # fails on rom_config_aerostructural2 where 1.5 m converges [m/(m/s)] input_depower_seed_max: 1.85 # ramp ceiling, below the server's 2.3 m [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 @@ -39,17 +49,21 @@ traj_opt: depower_offset: 0.0 # [-] 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] - pattern_azimuth_max_high: 34.0 # cap at/above pattern_elevation_amplitude_max_wind_ref: under the 45° climb + pattern_azimuth_max: 36.0 # 28 until 2026-10-28; rom_config_aerostructural2 turns ~43 % less and needs a + # wider and taller box: no startup solve at 5-8.25 m/s under 28 x 8.5 [deg] + pattern_azimuth_max_high: 45.0 # cap at/above pattern_elevation_amplitude_max_wind_ref; 36 until 2026-10-31: + # Cabauw 9 m/s (17.4 there) converges at 170 m and 10 m/s (19.3) at 180 m only + # with 45 and a 20° half-span cap. Older: under the 45° climb # limit Cabauw 10 needs ±28.4-28.8° at 150 m and IPOPT 2-3° of room: 422 under # 28 and 29 at r_min 22.8 m, under 30 at 23.5 m (quadratic depower # conversion); converges under 31 and 32 (2026-10-03). 36 -> 34 (2026-10-05): # at 36 the re-optimizations at Cabauw 10 grew to ±32° x 21°, RMS d 1.61°; # 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]; + pattern_elevation_amplitude_max: 15.0 # largest elevation HALF-SPAN of the figure; 0.0 = off; 8.5 until 2026-10-28 [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: 20.0 # cap at/above the ref wind (a step, not a ramp); 15 until 2026-10-31 [deg] + pattern_elevation_amplitude_max_wind_ref: 16.5 # 13.2 until 2026-10-31, when the high caps equalled the base ones. + # Older: wind AT the height below at/above which the high cap # 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); @@ -59,6 +73,13 @@ traj_opt: pattern_symmetric: true # mirror-symmetric figure about azimuth 0; the solve is multi-modal and # otherwise lands lopsided (Cabauw 2026-09-25: centre -2.3°, lobes 214/147 points) pattern_climb_angle_max: 45.0 # steepest climb of the path in the az/el plane; descending is free; 0 is off [°] + pattern_shape_wind_ref: 12.5 # wind at pattern_elevation_amplitude_max_wind_height at/above which the two + # _shape_high values below apply; 0 = off. With the 12.62 kg wing Maasvlakte + # 10 m/s (12.9 there) converges only without both (2026-10-31); 9 m/s is 11.6 [m/s] + pattern_symmetric_shape_high: false # pattern_symmetric at/above pattern_shape_wind_ref + pattern_climb_angle_max_shape_high: 0.0 # pattern_climb_angle_max at/above pattern_shape_wind_ref; 0 is off [°] + angle_of_attack_min: -7.0 # lowest trim angle of attack, the ROM's validity; .nan = the server's 0°, under which + # Cabauw 9 m/s (trims at -0.5 to +0.4°) has no solution (2026-10-31) [°] gates: # every returned path, at startup and after a re-optimization min_feasibility_margin: 0.82 # curvature gate AND request (as min_turn_radius); 0.0 = off; an 8 m/s number [-] diff --git a/data/wc_settings.yaml b/data/wc_settings.yaml index cd6e8b9..890d8df 100644 --- a/data/wc_settings.yaml +++ b/data/wc_settings.yaml @@ -73,9 +73,9 @@ wc_settings: max_acc: 8.0 # rate limit of v_set; the reel-out's acceleration_limit [m/s^2] damage_factor: 0.2 # winch damage weighting (unused here) [-] jerk_factor: 0.9 # jerk weighting in the damage calculation (unused here) [-] - # kv: `v_set = kv * sqrt(force)`, one value at every wind speed. It used to be - # a column of winch_kv_table.yaml (now winch_table.yaml), flat at 0.0408 from 3 to 12 m/s, and was - # removed from there 2026-10-02. The 10 m/s row was 0.039, identified + # kv: `v_set = kv * sqrt(force)`. Since 2026-10-30 the kv column of winch_table.yaml + # overrides this value (0.030 at 3-4 m/s, see there); before, it was flat at 0.0408 from + # 3 to 12 m/s and was no column from 2026-10-02. The 10 m/s row was 0.039, identified # 2026-08-27 (sweep below), and went back to the sweep's other passing value # 2026-09-22 — see the note under the sweep. 11 and 12 m/s fly 0.0408 # UNTESTED at the current tether and winch controller: the 2026-08-26 notes @@ -122,7 +122,7 @@ wc_settings: # 3.42 m/s mean, on the limit, so kv cannot lower its force: 0.043 made the # startup solve infeasible (2026-09-23). There the lever is f_high, see # docs/fig8_tuning_log.md. - kv: 0.0408 # v_set = kv * sqrt(force); same at every wind speed, see above [-] + kv: 0.0408 # v_set = kv * sqrt(force); the kv column of winch_table.yaml overrides it [-] # --- SPEED: how the force limits are enforced (PlanWinchSpeed.md, stage 4) -- force_limit: "soft" # "soft" (optimizer's tension curve) | "hard" (two limiters); winch_table.yaml overrides @@ -131,6 +131,6 @@ wc_settings: v_sat_beta: 10.0 # corner sharpness of the v_sat clamp; .inf = hard clamp [s/m] soft_lfc: true # also replace the LowerForceController with a reel-in line v_reel_in: -2.0 # reel-in speed at zero force [m/s] - reel_in_beta: 20.0 # sharpness of the line/curve handover; needs * kv * sqrt(f_low) >= 8 everywhere [s/m] + reel_in_beta: 22.0 # sharpness of the line/curve handover; needs * kv * sqrt(f_low) >= 8 everywhere [s/m] use_awe_trim: 0.0 # blend of the LOCAL law towards AWETrim's curve, in [0, 1]; the server gets opt_awe_trim [-] force_limit_tau: 1.0 # low-pass on the force before inverting; 0 = off [s] diff --git a/data/winch_table.yaml b/data/winch_table.yaml index d322d10..1f82313 100644 --- a/data/winch_table.yaml +++ b/data/winch_table.yaml @@ -1,11 +1,19 @@ # Wind-dependent winch-law parameters, interpolated linearly between rows and # CLAMPED outside the identified range rather than extrapolated. Read by # `winch_f_low(v_wind)` and `winch_force_limit(v_wind)`; each overrides the flat -# value of the same name in data/wc_settings.yaml. `kv` is no longer a column: -# it was 0.0408 at every wind speed, so the flat `kv` of data/wc_settings.yaml -# is used as is (its tuning notes moved there). Add a row and re-identify the curve +# value of the same name in data/wc_settings.yaml. Add a row and re-identify the curve # here, not a new key elsewhere, when another wind speed is tuned. # +# kv — gain of the reel-out law v_set = kv*sqrt(force), read by `winch_kv(v_wind)` +# (its tuning notes are at `kv` in data/wc_settings.yaml). It was flat at +# 0.0408 and no column from 2026-10-02 until 2026-10-30, when the kite got +# its 1.62 kg bridle (12.62 kg wing): the 4 m/s startup request then has no +# solution under the pattern box at 0.0408 or 0.050, and converges at 0.030 +# (2967 W) and 0.025 (3074 W), so 0.030 at 4 m/s, the measured value closest +# to the old one. 3.5 m/s fails at 0.030, 0.025, 0.018 and 0.023 and converges +# from 0.019 to 0.022 (1.73-1.67 kW), so 0.020 at 3.5 and, untested, at 3 m/s. +# The 5 m/s row holds 0.0408 so that 5 m/s and up fly as before. +# # f_low — force floor [N] below which the LowerForceController reels in. It has # to move with the wind because the winch's rating does not and the # kite's force does (with the square of the wind speed), so one value @@ -51,23 +59,38 @@ entries: - v_wind: 3.0 f_low: 350.0 force_limit: "soft" + kv: 0.020 + - v_wind: 3.5 + f_low: 525.0 + force_limit: "soft" + kv: 0.020 - v_wind: 4.0 f_low: 700.0 force_limit: "soft" + kv: 0.030 + - v_wind: 5.0 + f_low: 700.0 + force_limit: "soft" + kv: 0.0408 # Neutral for f_low (already flat from 4 to 9); force_limit is # "soft" throughout the table now, so this row no longer marks a step. - v_wind: 6.0 f_low: 700.0 force_limit: "soft" + kv: 0.0408 - v_wind: 9.0 f_low: 700.0 force_limit: "soft" + kv: 0.0408 - v_wind: 10.0 f_low: 700.0 force_limit: "soft" + kv: 0.0408 - v_wind: 11.0 f_low: 700.0 force_limit: "soft" + kv: 0.0408 - v_wind: 12.0 f_low: 700.0 force_limit: "soft" + kv: 0.0408 diff --git a/docs/fig8_tuning_log.md b/docs/fig8_tuning_log.md index 7b2e9ce..5b191d3 100644 --- a/docs/fig8_tuning_log.md +++ b/docs/fig8_tuning_log.md @@ -6455,3 +6455,230 @@ With V3Kite's linear tape map and the new AWETrim the Maasvlakte scenarios flew The -0.013 step gives d ln P / d rel_depower ~ -16. At -0.018: depower 0.268/0.268/0.287, measured 1962/11692/23126 W. The peak phase-4 force at 10 m/s rose from 7228 to 7735 N, above the 7200 N `f_high`; the other wind speeds were not re-flown. + +## 2026-10-28 — Lift/drag validation against the AWETrim ROM: not a valid comparison yet + +`examples/validate_lift_drag.jl` compares V3Kite's lift and drag with `rom_config_aerostructural2.yaml` +at the same power tape and apparent wind speed. The two sides are NOT trimmed under the same +condition, so the numbers are not a validation of either model: + +- V3Kite: parked at 150 m and 71-77° elevation, carrying its weight, the KCU's and the tether's; + angle of attack 3.5-4.8° (`span_mean_aoa`, the span mean of the VSM panels' geometric angle). +- ROM: its gravity-free trim at the centre of the wind window, `alpha = atan2(C_D, C_L) - theta_b(u_p)` + (AWETrim `system/rom_forces.py`); angle of attack 1.1-2.2° (the wing angle of its identification). + +| tape | v_a | V3Kite L / D | ROM trim L / D | ΔL | ΔD | +|--|--|--|--|--|--| +| 1.60 m | 15.3 | 1637 / 240 | 1676 / 265 | +2 % | +11 % | +| 1.60 m | 8.8 | 584 / 82 | 554 / 88 | -5 % | +7 % | +| 1.76 m | 15.3 | 1358 / 226 | 1408 / 253 | +4 % | +12 % | +| 1.76 m | 8.6 | 478 / 74 | 450 / 81 | -6 % | +9 % | + +V3Kite drag is the VSM wing plus all bridle lines, AWETrim's basis. Agreement within ±6 % in lift +and 7-12 % in drag comes from two different flight states, so it says nothing yet. A valid +comparison needs both models in the same trim condition: the same flight state (elevation, +gravity, KCU and tether loads) or the same angle of attack under one definition. + +## 2026-10-28 — Depower seed 1.6 -> 1.5 m for `rom_config_aerostructural2.yaml` + +With the new ROM (Oriol's, fitted to V3Kite, valid from 1.35 m of tape) every startup solve at +Maasvlakte 7 m/s failed, all four guess elevations. Replaying the exact startup request in AWETrim's +`ReeloutSession` (no simulation) isolates the seed `input_depower`: + +| ROM | seed [m] | result | +|--|--|--| +| flight-corrected | 1.6 | converged, 1.628 m, 11.7 kW | +| `rom_config_aerostructural.yaml` | 1.6 | converged, 1.329 m, 21.1 kW | +| `rom_config_aerostructural2.yaml` | 1.6 | IPOPT stuck near 2.0 m, alpha below its 0° bound | +| `rom_config_aerostructural2.yaml` | 1.5 | converged, 1.350 m, 17.8 kW | +| `rom_config_aerostructural2.yaml` | 1.4 | converged, 1.350 m, 17.8 kW | + +From 1.6 m IPOPT walks to the depowered side, where this ROM's trim angle of attack (about 1.5° +lower than the flight-corrected ROM's at the same tape) drops below the optimizer's 0° bound; +relaxing that bound to -5° does not help. The optimum is at the bottom of the band, 1.35 m. Only +7 m/s was checked; the seed ramp above 7 m/s (0.25 m per m/s) is unchanged and untested on this ROM. + +## 2026-10-28 — Startup on `rom_config_aerostructural2`: wider box, larger guess, size retries + +The scenario build with seed 1.5 m flew 4 and 7 m/s (all 10 criteria, power ratio 0.92 and 0.94, +18.4 kW at 7 m/s) and failed the startup solve at Maasvlakte 3.5, 5, 6, 8 and 8.25 m/s. The exact +startup requests, replayed in AWETrim's `ReeloutSession`: + +- seed 1.35-1.45 m, an alpha floor of -6° or a taller box alone: no change; +- `gain_roll_steering` -0.376 -> -0.666 (the flight-corrected ROM's): all five converge. The new + ROM turns ~43 % less. That is physical for the geometry it was identified on (Bart's), not a + fitting error, so the requests have to fit a kite that turns less. Failures show steering + and its rate on their bounds, `s_dot` near 0 and alpha below 0°. +- box 36° x 15° (half-spans) with a larger guess, seed 1.5 m: + +| guess | 5 | 6 | 7 | 8 | 8.25 m/s | +|--|--|--|--|--|--| +| 30 x 13 | fail | fail | OK | OK | OK | +| 32 x 14 | OK | OK | fail | OK | OK | +| 34 x 15 | OK | OK | OK | fail | OK | + +No single size covers all, so the guess is 32 x 14 and `startup_retry_guess_sizes` tries +30 x 13 and 34 x 15 at the same centre before the elevation offsets. The seed ramp +(`input_depower_per_wind` 0.25 -> 0) is gone: 8 m/s seeded at 1.75 m fails where 1.5 m converges. +3.5 m/s (195 m tether) fails with every guess, seed and box tried. Only the startup requests +were checked; the effect of the larger box on the flown runs is not. + +## 2026-10-30 — AWETrim flies V3Kite's bridle mass, 1.62 kg + +After AWETrim was merged up to its `main` (`feat/server-aero-forces` at `0947c52`) the scenario +build failed the startup solve at Maasvlakte 3.5, 4, 5, 8.5, 9, 10 and 11 m/s; 4 and 5 m/s had +flown on 2026-10-28 on the same commit (240de86) against the pre-merge AWETrim. AWETrim `aa25dfc` +(2026-10-29) adds the bridle (2.58 kg, "lines + pulleys") to the point-mass wing: 10.99 -> +13.58 kg, 22.0 kg airborne with the KCU. Replaying one failed 5 m/s request (default ROM) +in-process: with the bridle mass the march stalls from node 20 of the guess (alpha 27-29°) and +fails at node 25, IPOPT ends locally infeasible; with only the wing mass back at 11.0 kg it +converges at 7.5 kW. At 10 m/s the mass costs a third of the predicted power (12.5 vs 17.9 kW). + +`examples/validate_masses.jl` puts V3Kite's structure next to AWETrim's file: canopy 10.99 kg +(both), KCU 8.4 kg (both), bridle 1.62 kg in V3Kite (1.12 kg of lines at 724 kg/m³ plus five +0.1 kg point masses) against 2.58 kg, so 21.0 kg airborne against 22.0 kg (19.4 kg before +`aa25dfc`). Every `/init` now sends `system_config_path = awetrim_system_config()`, AWETrim's +LEI-V3 file with the bridle mass set to `AWETRIM_BRIDLE_MASS` = 1.62 kg, written to +`output/awetrim_system/` with the ROMs copied next to it. The file's content is part of the +request key, so the cached solutions of the 2.58 kg kite are not replayed. + +With `rom_config_aerostructural2` the same replay did not separate the mass from the rest of +the merge: 4 m/s converged before the merge and fails after it with either mass, 8.5 m/s the +other way round, and 5 m/s at seed 1.5 m fails at node 0 in every version. + +Flown at 1.62 kg, Maasvlakte 5 m/s still failed every startup retry: the march broke at node +28-30 of the guess. The replayed request (wing 12.62 kg) fails the same way with +`turn_radius_headroom` 1.0 (8.98 m) and with no turn-radius limit, with seed 1.35 m, 3 or 5° +lower, and 36 x 15; it converges with the canopy alone (10.99 kg, 6.3 kW), centred 3° higher +(6.2 kW) or at 26 x 11 (5.9 kW). `startup_retry_guess_sizes` gains `[26, 11]`, and with it +5 m/s flies: all 10 criteria, 6.1 kW measured, power ratio 0.99, 3 paths installed. + +## 2026-10-30 — `kv` 0.030 at 3-4 m/s: Maasvlakte 4 m/s flies with the 12.62 kg wing + +With the 1.62 kg bridle Maasvlakte 4 m/s (195 m) failed every startup retry, sizes and +elevation offsets from -6 to +5° included. Its exact requests were recorded +(`OPT_REQUEST_RECORD`) and replayed in AWETrim, which reproduces the failure and the failed ++3° retry. Converged: the wing at 10.99 kg (3.1 kW); the pattern box without `symmetric` +AND without `climb_angle_max` (2.5 kW, 61 s laps; either one alone, any other single +limit relaxed, or the full box restored after a relaxed solve all fail); and a softer winch, +`k_v` 0.030 (2967 W) or 0.025 (3074 W). No help: guess +2/+3°, 26 x 11 at +3°, seed +1.35 m, no turn-radius limit, `k_v` 0.050, `f_min` 400 or 1000 N, tether 150, 170 or 220 m. + +`kv` is a column of `winch_table.yaml` again (`winch_kv`): 0.030 at 3 and 4 m/s, 0.0408 +from 5 m/s up. Flown at 4 m/s: all 10 criteria, 3.1 kW measured, power ratio 0.99, +3 paths installed, 1.41 m/s mean reel-out speed. 3 m/s (Cabauw) flies 0.030 untested. + +Maasvlakte 3.5 m/s (195 m) at `k_v` 0.030 failed every startup retry too. Its recorded +request converges at `k_v` 0.019-0.022 (1.73-1.67 kW, also with the guess moved ±2°) and +fails at 0.015, 0.017, 0.018, 0.023, 0.025 and 0.030, with the light wing as the only other +converging variant (seed 1.35 m, +2° and the relaxed box fail). New 3.5 m/s row at `k_v` +0.020, and 0.020 at 3 m/s so that `k_v` does not rise as the wind drops (3 m/s untested). +Flown at 3.5 m/s: all 10 criteria, 1.9 kW measured, power ratio 1.02, 6 paths installed, +0.9 m/s mean reel-out speed. + +## 2026-10-31 — `first_lap_force_frac` 0.93 -> 0.96: Maasvlakte 9 m/s with the 12.62 kg wing + +Maasvlakte 8.5 m/s flies with the 1.62 kg bridle unchanged (all 10 criteria, 19.2 kW, power +ratio 0.91); 9 m/s failed every startup retry. Its recorded request, replayed in AWETrim, +converges with the light wing (18.3 kW), without the pattern box, with `symmetric` off alone +(17.4 kW, the climb limit alone does not help), or with the startup `f_max` (`f_high` x +`first_lap_force_frac`, 6696 N) raised to 6900 N (17.1 kW), 7200 N (18.2 kW) or 7500 N +(19.2 kW). No help: guess ±2°, 30 x 13, seed 1.75 m, `k_v` 0.036 or 0.045, no turn-radius +limit. `first_lap_force_frac` 0.96 (6912 N), the smallest step that converges, keeps the box; +it was lowered to 0.93 on 2026-09-24 because 9 m/s then crossed the 8400 N rating in lap 1. +Flown at 9 m/s: all 10 criteria (max force included), 17.8 kW measured, power ratio 0.89, +phase-4 peak 6893 N. The factor applies at every wind speed; 10 and 11 m/s are not yet flown +with it. + +## 2026-10-31 — High-wind shape: no symmetry, no climb limit from 12.5 m/s aloft + +Maasvlakte 10 m/s (12.9 m/s at 100 m) failed every startup retry with the 1.62 kg bridle and +`first_lap_force_frac` 0.96. Its recorded request converges with the light wing (17.1 kW), +without the box (16.9 kW), or with `symmetric` off AND no `climb_angle_max` (17.7 kW); either +alone, any other single box limit, `f_max` 7200 or 7500 N (also with `symmetric` off), `k_v` +0.039 or 0.045, seed 1.75 or 2.0 m and the guess +2° all fail. New `pattern_shape_wind_ref` +(12.5 m/s at `pattern_elevation_amplitude_max_wind_height`) with `pattern_symmetric_shape_high` +false and `pattern_climb_angle_max_shape_high` 0 (off). Flown at 10 m/s: all 10 criteria, +18.1 kW measured, power ratio 0.85, phase-4 peak 7008 N, cross-track RMS 0.82°; the four +installed paths are centred -0.7° to +2.4° in azimuth. It also reaches Cabauw from 7 m/s +(13.5 m/s aloft), not yet flown with it. + +## 2026-10-31 — High-wind start length: Maasvlakte 11 m/s starts at 180 m + +Maasvlakte 11 m/s (14.2 m/s at 100 m) failed every startup retry with the 1.62 kg bridle, the +high-wind shape included. Its recorded request at 150 m fails without the box too, and with seed +1.75 or 1.84 m (the light wing's optimum flies 1.84 m, far from the ROM's 2.25 m ceiling), ++3°, no turn-radius limit, `f_max` 7500 N, `k_v` 0.036 or 0.045; it converges with the light +wing (14.8 kW) and at a longer start: 160 m (17.0 kW), 165, 170 and 180 m (18.4 kW). New +`low_wind.high_wind_speed` (14.0 m/s at `low_wind_height`) and `high_wind_l_tether`. Flown at +160 m the startup converged but failed the curvature gate (margin 0.79 against 0.82, all four +radius retries 422); at 180 m: all 10 criteria, 17.8 kW measured, power ratio 0.82, phase-4 +peak 6720 N, cross-track RMS 0.78°. Cabauw from about 8 m/s is above 14 m/s aloft and starts at +180 m too, not yet flown. + +## 2026-10-31 — Cabauw with the 1.62 kg bridle; `reel_in_beta` 20 -> 22 + +Rebuilt with every 2026-10-30/31 change: Cabauw 5, 5.5, 5.75, 6, 6.25 and 7 m/s pass all 10 +criteria (19.7-21.8 kW). Cabauw 3 m/s threw before flying: `k_v` 0.020 at `f_low` 350 N needs +`reel_in_beta * k_v * sqrt(f_low) >= 8`, i.e. `reel_in_beta` >= 21.4 (Maasvlakte 3.5 m/s has +`f_low` 525 N and passed at 20). At 22: all 10 criteria, 4.5 kW measured, power ratio 0.99 (the +2026-10-26 scenario flew 2.0 kW at 0.71). + +Still failing, from recorded requests replayed in-process: +- 4 m/s (7.7 m/s at 100 m): the 150 m startup fails at every seed, guess size, box, radius limit + and `k_v` 0.030-0.0408; 165, 170 and 175 m converge, 155, 160 and 180 m do not. +- 8 m/s (15.5 aloft) starts at 180 m from `high_wind_speed`, converges, and fails laps >= 2.5 + (17.9 kW). A 150 m start fails, 160 and 170 m converge. +- 9 and 10 m/s (17.4 and 19.3 aloft) fail at 120-220 m, without the box or radius limit, with + the light wing, the old ROM, `f_max` 6000-15000 N, `k_v` 0.048, seed 1.8-2.2 m and +5 to +15°. + The 9 m/s request converges at 7 and 8 m/s and fails at 8.5, and converges at 9 m/s with + `alpha` 0.10 or 0.15 instead of 0.234: the wind aloft is past what the optimizer solves. The + 2026-10-26 scenarios at 9 and 10 m/s predate the bridle mass and `rom_config_aerostructural2`. + +## 2026-10-31 — Startup length retry; high-wind start 180 -> 170 m + +New `startup_retry_length_steps` (`traj_opt.yaml`, [20.0]): when no seed converges, the run is set +up again with the tether that much longer (`setup_with_startup`, through the new `l_tether` run +input). Cabauw 4 m/s fails every seed at 150 m and retries at 170 m: all 10 criteria, 11.6 kW. +`high_wind_l_tether` 180 -> 170 m: Maasvlakte 11 m/s flies at 170 m with a starting margin of +0.83 (gate 0.82): all 10 criteria, 17.5 kW. Cabauw 8 m/s converges at 170 m and still fails +laps >= 2.5 (2.0 laps, 18.0 kW): its laps take 23.7-30.1 s against 12.2-19.8 s in the 2026-10-26 +scenario, so its patterns are larger, not its start longer. + +## 2026-10-31 — Angle-of-attack floor -7° (the ROM's validity) + +AWETrim `eeeefa8` (2026-10-28) bounds the trim angle of attack to 0-14° unless a request says +otherwise. At Cabauw 9 m/s (17.4 m/s at 100 m) the failed iterate sits on that 0° floor with the +depower on its 2.25 m ceiling and the trim residual infeasible; the recorded 180 m request +converges with the floor at -3, -6 or -10° (18.7 kW predicted, depower 2.0 m, alpha -0.5 to ++0.4°). `rom_config_aerostructural2` is valid from -7°, so new `box.angle_of_attack_min` -7.0, sent +with every `/init` as `angle_of_attack_limits`. Cabauw 10 m/s converges with it only without the +pattern box (alpha -0.9 to +0.1°, depower 2.08 m); dropping any single box row is not enough. +Flown: Cabauw 8 m/s at 170 m, all 10 criteria, 17.9 kW. Cabauw 9 m/s found no path at 170 m, +retried at 190 m and failed laps >= 2.5 (16.3 kW). Cabauw 10 m/s found none at 170 or 190 m. + +## 2026-11-01 — Cabauw 9 and 10 m/s: high-wind box 45° x 20° from 16.5 m/s aloft, 2.0 laps + +With the -7° floor, recorded requests replayed in-process: Cabauw 9 m/s (17.4 m/s at 100 m) at +180 m converges at a 21.5 m turn radius only without the box, and 10 m/s (19.3) only with the +elevation half-span cap 15 -> 20° together with the azimuth cap 36 -> 45° (or no elevation floor). +Under the 45°/20° box with the floor kept, 9 m/s converges at 170 and 180 m and 10 m/s at 180 m +only (none at 150-175 m). Dropping the floor gives 25 s instead of 28 s laps but puts the lowest +point at 33-35 m, under the 40 m `min_height` gate, so it stays. The high caps had equalled the +base ones, so their step moves 13.2 -> 16.5 m/s and only these two winds change; +`startup_retry_length_steps` [20] -> [10, 20] gives 10 m/s its 180 m. +Flown: both converge and track, but their 30-32 s laps from 170 and 180 m hold 2.0 laps, under +2.5; 150 and 160 m have no startup solution. New `run.min_laps_high` 2.0 from +`min_laps_high_wind` 16.5 m/s at `low_wind_height`. Cabauw 9 m/s: all 10 criteria, 15.0 kW, +power ratio 0.76, peak 6543 N. Cabauw 10 m/s: all 10 criteria, 15.2 kW, ratio 0.77, peak 6566 N. + +## 2026-11-01 — A cached startup seed uses up its retry + +Rerunning Cabauw 10 m/s sent seven new startup requests at 170 m, all 422 (~35 s each, ~4 min +per run): the seven from the first run were skipped as cached, but since 2026-09-19 a skip cost +no retry, so the budget was spent on seeds walked outward (31, 27, 33, 26, 34, 25, 35°) and the +next rerun would have walked further, out to ±10°. 170 m has no path at that wind (see the +2026-11-01 high-wind box entry), so the walk could only lose time. A cached seed now uses up its +place in `startup_retry_guess_sizes`/`startup_retry_el_offsets`: a rerun skips what failed +before and moves on to the next length at once. The outward walk (`startup_seed_offsets`) is +gone; widen the listed seeds to try more. diff --git a/docs/src/api/internals.md b/docs/src/api/internals.md index cca9e7f..86b6588 100644 --- a/docs/src/api/internals.md +++ b/docs/src/api/internals.md @@ -22,16 +22,21 @@ RobustKiteControllers.gate_candidate RobustKiteControllers.wants_challenge RobustKiteControllers.set_yaml_fields! RobustKiteControllers.depower_command! +RobustKiteControllers.AngleOfAttackLimits +RobustKiteControllers.aoa_limits RobustKiteControllers.depower_seed RobustKiteControllers.depower_spec RobustKiteControllers.set_depower_conversion! RobustKiteControllers.awetrim_depower_to_v3kite RobustKiteControllers.elevation_min_request RobustKiteControllers.merge_into! +RobustKiteControllers.min_laps_at RobustKiteControllers.min_turn_radius_request RobustKiteControllers.request_constraints RobustKiteControllers.request_depower_estimate RobustKiteControllers.setup_run +RobustKiteControllers.setup_with_startup +RobustKiteControllers.StartupNoPath RobustKiteControllers.solve_startup_path! RobustKiteControllers.startup_feasibility RobustKiteControllers.steering_command! diff --git a/docs/src/api/winch.md b/docs/src/api/winch.md index dbc38ac..a08b0b8 100644 --- a/docs/src/api/winch.md +++ b/docs/src/api/winch.md @@ -11,6 +11,7 @@ The parameters of the reel-out law that change with wind speed, from the project ```@docs winch_f_low +winch_kv winch_force_limit winch_table_lookup winch_table_select diff --git a/docs/src/examples_identification.md b/docs/src/examples_identification.md index 3cbee72..dfd6c56 100644 --- a/docs/src/examples_identification.md +++ b/docs/src/examples_identification.md @@ -165,3 +165,24 @@ cross-track error to a step of the attractor offset, at constant length and duri fitted with a second-order model and compared with the closed-loop model of `stability_opt_reelout.jl`. The measured part works on the saved tests in `data/steptest/` without flying. This analysis is work in progress. + +## Validation of the aerodynamics against AWETrim + +### [`validate_lift_drag.jl`](https://github.com/OpenSourceAWE/RobustKiteControllers.jl/blob/main/examples/validate_lift_drag.jl) +Compares V3Kite's lift and drag with AWETrim's reduced-order model at the same power-tape length +and apparent wind speed. The kite is parked at 150 m, and the ground wind is trimmed in each block +until the apparent wind speed reaches its target. AWETrim's `POST /aero_forces` is then queried at +the measured apparent wind speed, with the ROM every optimizer request uses (`AWETRIM_ROM`), at +the ROM's own trim. Drag is compared as the wing's plus that of all bridle lines. The results go to `output/lift_drag_validation.json`; `fly = false` re-queries AWETrim +without flying. About 3 minutes of simulation. + +### [`show_validation.jl`](https://github.com/OpenSourceAWE/RobustKiteControllers.jl/blob/main/examples/show_validation.jl) +Shows the saved comparison as an HTML page in the default browser: the ROM against V3Kite, with the +deviation in percent and the points outside the ROM's validity band marked, and V3Kite's drag split. + +### [`validate_masses.jl`](https://github.com/OpenSourceAWE/RobustKiteControllers.jl/blob/main/examples/validate_masses.jl) +Prints the masses of V3Kite's structure (for the project selected in `data/gui.yaml`) next to +those of AWETrim's system file: canopy, bridle (lines and point masses), KCU, the airborne +total, the wing mass AWETrim's trim actually uses, and the tether mass per metre. Builds the +structure without compiling a model and reads AWETrim's file with AWETrim's own Python, so it +needs neither a running server nor a flight. diff --git a/examples/build_all_scenarios.jl b/examples/build_all_scenarios.jl index cc15ff9..3c27c3d 100644 --- a/examples/build_all_scenarios.jl +++ b/examples/build_all_scenarios.jl @@ -9,7 +9,8 @@ replace each scenario folder with the new run. For each site the wind speeds are read from the run summaries of its scenario folders; a `_2`, `_3`, ... repeat shares its wind speed with the unsuffixed folder, so each wind speed is flown once and replaces the unsuffixed folder -only (the repeats are kept as they are). Each run is +only (the repeats are kept as they are). A wind speed in `SKIP_WINDS` is not +flown at all and its old scenario is kept. Each run is `simple_opt_reelout.jl` without plots, at the project and wind speed set in `data/gui.yaml`, and is moved in by `move_scenario.jl`, but only if it finished cleanly and passed all success criteria: otherwise the old scenario is kept and @@ -43,6 +44,11 @@ const SITE_PROJECTS = [ "maasvlakte" => "system_reelout_maasvlakte.yaml", "cabauw" => "system_reelout_cabauw.yaml", ] +"Wind speeds [m/s] not flown at a site, e.g. because their scenario is already up to date" +const SKIP_WINDS = Dict( + "maasvlakte" => [], + "cabauw" => Float64[], +) output_dir = normpath(joinpath(@__DIR__, "..", "output")) run_done_file = joinpath(output_dir, "last_run_done.txt") progress_file = joinpath(output_dir, "build_all_scenarios.txt") @@ -74,6 +80,10 @@ try for (site, project) in SITE_PROJECTS set_selected_project(project) for wind in scenario_winds(site) + if wind in get(SKIP_WINDS, site, Float64[]) + log_line("$site $wind m/s: skipped (SKIP_WINDS), the old scenario is kept") + continue + end write_gui_field("wind_speed", wind) rm(run_done_file; force = true) t0 = time() diff --git a/examples/menu2.jl b/examples/menu2.jl index 626d200..6956f4a 100644 --- a/examples/menu2.jl +++ b/examples/menu2.jl @@ -41,6 +41,9 @@ const IDENTIFICATION_SCRIPTS = [ "identify_depower_factor.jl - growth of the response time with depower (6 min!)" => "identify_depower_factor.jl", "identify_kite_correction.jl - measured kite correction by multisine injection (5 min!)" => "identify_kite_correction.jl", "stability_opt_reelout.jl - disk margins of the reel-out course loop with the identified model" => "stability_opt_reelout.jl", + "validate_lift_drag.jl - lift and drag of V3Kite vs. the AWETrim ROM (3 min!)" => "validate_lift_drag.jl", + "show_validation.jl - show the lift/drag validation in the browser" => "show_validation.jl", + "validate_masses.jl - masses of V3Kite vs. AWETrim's model" => "validate_masses.jl", ] """ diff --git a/examples/reelout_results.jl b/examples/reelout_results.jl index 15d3bb6..3e59ebb 100644 --- a/examples/reelout_results.jl +++ b/examples/reelout_results.jl @@ -57,6 +57,7 @@ function score_log(setup, st::RunState) min_elevation = fcs.run.min_elevation, az_center = az_c_log, az_amplitude = az_amp_log, el_height = el_h_log, min_span_frac = fcs.run.min_span_frac, require_final = true, + min_laps = RobustKiteControllers.min_laps_at(fcs, project_set), max_force = project_set.max_force, cross_track = d_raw_log, # A parking kite leaves the path on purpose: the pattern ends where it parks. t_end = something(RobustKiteControllers.park_start_time(fcs, sl), Inf)) @@ -304,10 +305,8 @@ function traj_opt_block(setup, st::RunState, power_block, feasibility, scored) guess_el_center_high at and above guess_el_center_wind_ref, plus \ the retry offset below [deg]"), "el_center_retry_offset_deg" => (startup_seed_offset, - "offset of the seed the startup solve converged from after a 422 — a \ - startup_retry_el_offsets entry, or a whole degree walked outward past \ - them when the failure cache rejected the listed ones; 0 means the \ - shipped guess converged [deg]"), + "offset of the seed the startup solve converged from after a 422, a \ + startup_retry_el_offsets entry; 0 means the shipped guess converged [deg]"), "points" => (GUESS_POINTS, "points the guess was sent with"), "depower_seed_m" => (round(depower_seed(tos, inflow.wind_speed); digits = 3), "power-tape length the solve started from, input_depower ramped with \ diff --git a/examples/show_validation.jl b/examples/show_validation.jl new file mode 100644 index 0000000..9b7c856 --- /dev/null +++ b/examples/show_validation.jl @@ -0,0 +1,175 @@ +# Copyright (c) 2026 Uwe Fechner +# SPDX-License-Identifier: MPL-2.0 + +""" +Show the lift/drag comparison saved by `validate_lift_drag.jl` +(`output/lift_drag_validation.json`) as an HTML page in the default browser: one table +per AWETrim ROM, each row a V3Kite block (power tape, measured apparent wind speed), +with V3Kite's lift and drag next to the ROM's at its own trim, and the deviation of the +ROM from V3Kite in percent. Rows outside the ROM's validity band are marked. + + include("examples/show_validation.jl") +""" + +using Pkg +if Base.active_project() != joinpath(@__DIR__, "Project.toml") + Pkg.activate(joinpath(@__DIR__)) +end + +using JSON3 +using Printf + +"The results `validate_lift_drag.jl` writes" +const RESULT_FILE = normpath(joinpath(@__DIR__, "..", "output", "lift_drag_validation.json")) +# A snap-confined browser cannot read dotdirs under $HOME, so the page goes to $HOME itself. +"The page written and opened" +const HTML_FILE = joinpath(homedir(), "lift_drag_validation.html") + +const CSS = """ +:root { --bg:#fff; --fg:#1d1d1f; --muted:#6e6e73; --line:#d9d9de; --head:#f3f3f6; + --good:#1a7f37; --warn:#b26a00; --bad:#c62828; } +@media (prefers-color-scheme: dark) { + :root { --bg:#16161a; --fg:#ececf1; --muted:#9a9aa3; --line:#34343c; --head:#202027; + --good:#4cc26a; --warn:#e0a03a; --bad:#ef6b6b; } } +body { background:var(--bg); color:var(--fg); margin:0 auto; max-width:1100px; padding:24px 16px; + font:15px/1.45 system-ui, -apple-system, "Segoe UI", sans-serif; } +h1 { font-size:22px; margin:0 0 4px; } h2 { font-size:17px; margin:28px 0 8px; } +.meta { color:var(--muted); font-size:13px; } +.wrap { overflow-x:auto; } +table { border-collapse:collapse; width:100%; font-variant-numeric:tabular-nums; } +th, td { padding:6px 10px; border-bottom:1px solid var(--line); text-align:right; white-space:nowrap; } +thead th { background:var(--head); font-weight:600; } +thead tr:first-child th { text-align:center; } +th.grp { border-left:2px solid var(--line); } td.grp { border-left:2px solid var(--line); } +.good { color:var(--good); } .warn { color:var(--warn); } .bad { color:var(--bad); font-weight:600; } +.out { color:var(--bad); } +ul { color:var(--muted); font-size:13px; padding-left:18px; } +""" + +esc(x) = replace(string(x), "&" => "&", "<" => "<", ">" => ">") +num(x, d = 0) = x isa Number ? @sprintf("%.*f", d, x) : "–" +ld(r) = haskey(r, "error") ? "error" : + "$(num(r["lift"])) / $(num(r["drag"]))" + +"Deviation of `x` from the V3Kite value `ref` in percent, coloured by size" +function deviation(x, ref) + (x isa Number && ref isa Number && ref != 0) || return "–" + d = 100 * (x - ref) / ref + cls = abs(d) < 5 ? "good" : abs(d) < 15 ? "warn" : "bad" + return "$(@sprintf("%+.0f", d)) %" +end + +""" + rom_table(io, label, v3kite, awetrim) + +One table: the V3Kite blocks against the ROM `label`. +""" +function rom_table(io, label, v3kite, awetrim) + first_ok = something(findfirst(aw -> !haskey(aw[label]["trim"], "error"), awetrim), 0) + rom_file = first_ok == 0 ? "" : basename(string(awetrim[first_ok][label]["trim"]["rom_config"])) + println(io, "
| V3Kite | +AWETrim, ROM trim | |||||||
|---|---|---|---|---|---|---|---|---|
| tape [m] | va [m/s] | +AoA [°] | L / D [N] | +AoA [°] | L / D [N] | ΔL | ΔD | +in ROM band |
| $(num(res["tape"], 2)) | $(num(res["v_app"], 1)) | ", + "$(num(res["aoa"], 2)) | $(num(res["lift"])) / $(num(res["drag"])) | ", + "$(num(get(trim, "angle_of_attack", nothing), 2)) | $(ld(trim)) | ", + "$(deviation(get(trim, "lift", nothing), res["lift"])) | ", + "$(deviation(get(trim, "drag", nothing), res["drag"])) | ", + "$valid_cell |
| tape [m] | va [m/s] | VSM wing [N] | +bridle lines [N] | compared D [N] | bridle in lift_drag [N] |
+ lift_drag D [N] | tether [N] | KCU [N] | +
|---|---|---|---|---|---|---|---|---|
| $(num(res["tape"], 2)) | $(num(res["v_app"], 1)) | ", + "$(num(res["drag_vsm"])) | $(num(res["drag_bridle"])) | ", + "$(num(res["drag"])) | ", + "$(num(res["drag_bridle_in_wing"])) | $(num(res["drag_lift_drag"])) | ", + "$(num(res["drag_tether"])) | $(num(res["drag_kcu"])) |
Not a valid comparison: the two models are not in the same trim + condition. V3Kite is parked and carries its weight, the KCU's and the tether's; the + ROM is at its gravity-free trim at the centre of the wind window.
""") + for label in data["roms"] + rom_table(io, label, data["v3kite"], data["awetrim"]) + end + drag_table(io, data["v3kite"]) + println(io, """lift_drag, the wing's force perpendicular to its apparent
+ wind. V3Kite D: the VSM wing drag plus the drag of all bridle lines, AWETrim's
+ basis; lift_drag's own D holds only the bridle share lumped onto the
+ wing nodes.
+
@@ -249,7 +256,7 @@ state machine over the whole run. The first subplot, `d ', shows the distance be
#%% web id=time_series_plot controls=wind_speed
@web(html"""
+
@@ -298,7 +305,7 @@ mechanical power over the whole run, and the cumulative mechanical energy.
#%% web id=power_plot controls=wind_speed
@web(html"""
+
@@ -348,7 +355,7 @@ kite speed over the whole run.
#%% web id=aerodynamics_plot controls=wind_speed
@web(html"""
+
diff --git a/notebooks/results_cabauw.jl b/notebooks/results_cabauw.jl
index 153cf52..cf77c6a 100644
--- a/notebooks/results_cabauw.jl
+++ b/notebooks/results_cabauw.jl
@@ -5,10 +5,17 @@ try; import KaimonSlate; catch; error("This is a Kaimon Slate notebook — runni