|
| 1 | +# Copyright (c) 2025 Bart van de Lint |
| 2 | +# SPDX-License-Identifier: MPL-2.0 |
| 3 | + |
| 4 | +""" |
| 5 | +Real-Time 3D Visualization Example |
| 6 | +
|
| 7 | +This example demonstrates how to create a custom simulation loop with real-time |
| 8 | +3D visualization using Makie's Observable system and the existing plot functions. |
| 9 | +
|
| 10 | +Key features: |
| 11 | +- Real-time 3D updates using Observables |
| 12 | +- Proper sleep timing to maintain real-time speed |
| 13 | +- Interactive camera control during simulation (hover, click-to-zoom) |
| 14 | +- Configurable visualization frame rate |
| 15 | +- Uses existing plot() functions for consistency |
| 16 | +""" |
| 17 | + |
| 18 | +using GLMakie |
| 19 | +using SymbolicAWEModels |
| 20 | +using KiteUtils |
| 21 | +using LinearAlgebra |
| 22 | +using Printf |
| 23 | + |
| 24 | +# ============================================================================ |
| 25 | +# SIMULATION PARAMETERS |
| 26 | +# ============================================================================ |
| 27 | + |
| 28 | +dt = 0.05 # Time step [s] |
| 29 | +total_time = 20.0 # Total simulation time [s] |
| 30 | +vsm_interval = 3 # VSM update interval |
| 31 | +realtime_factor = 1.0 # 1.0 = realtime, 2.0 = 2x speed, 0.5 = half speed |
| 32 | +plot_interval = 1 # Update plot every N steps (1 = every step) |
| 33 | +vector_scale = 1.0 # Scale for wing orientation arrows |
| 34 | + |
| 35 | +# Steering parameters |
| 36 | +steering_freq = 0.5 # Hz - full left-right cycle frequency |
| 37 | +steering_magnitude = 10.0 # Magnitude of steering input [Nm] |
| 38 | +bias = 0.2 # Steering bias |
| 39 | + |
| 40 | +# ============================================================================ |
| 41 | +# INITIALIZE MODEL |
| 42 | +# ============================================================================ |
| 43 | + |
| 44 | +println("Initializing model...") |
| 45 | +set_data_path("data") |
| 46 | +set = Settings("system.yaml") |
| 47 | +set.profile_law = 3 |
| 48 | +sam = SymbolicAWEModel(set) |
| 49 | +init!(sam) |
| 50 | +find_steady_state!(sam) |
| 51 | + |
| 52 | +sys_struct = sam.sys_struct |
| 53 | +steps = Int(round(total_time / dt)) |
| 54 | +num_winches = length(sys_struct.winches) |
| 55 | + |
| 56 | +# ============================================================================ |
| 57 | +# CREATE OBSERVABLES AND PLOT SCENE |
| 58 | +# ============================================================================ |
| 59 | + |
| 60 | +println("Creating 3D visualization window with observables...") |
| 61 | + |
| 62 | +# Create observables for dynamic visualization |
| 63 | +# Initialize with current system state |
| 64 | +segment_points_obs = Observable(Point3f[]) |
| 65 | +point_positions_obs = Observable(Point3f[]) |
| 66 | +wing_origins_obs = Observable(Point3f[]) |
| 67 | +wing_directions_obs = Observable(Vec3f[]) |
| 68 | + |
| 69 | +# Initialize observables with current state |
| 70 | +update_plot_observables!( |
| 71 | + segment_points_obs, point_positions_obs, |
| 72 | + wing_origins_obs, wing_directions_obs, |
| 73 | + sys_struct; vector_scale |
| 74 | +) |
| 75 | + |
| 76 | +# Create 3D scene using existing plot function with observables |
| 77 | +scene = plot(sys_struct; |
| 78 | + segment_points_obs, |
| 79 | + point_positions_obs, |
| 80 | + wing_origins_obs, |
| 81 | + wing_directions_obs, |
| 82 | + vector_scale, |
| 83 | + size=(1400, 900)) |
| 84 | + |
| 85 | +# Add text overlays for time and progress directly on the scene |
| 86 | +sim_time_text = Observable("Time: 0.00 s") |
| 87 | +progress_text = Observable("Progress: 0%") |
| 88 | + |
| 89 | +# Add text labels to scene (top-left and top-right) |
| 90 | +text!(scene, sim_time_text, position = Point2f(20, 20), space = :pixel, |
| 91 | + fontsize = 24, color = :black, align = (:left, :top)) |
| 92 | +text!(scene, progress_text, position = Point2f(1380, 20), space = :pixel, |
| 93 | + fontsize = 20, color = :black, align = (:right, :top)) |
| 94 | + |
| 95 | +# Display the scene (non-blocking) |
| 96 | +display(scene) |
| 97 | + |
| 98 | +# ============================================================================ |
| 99 | +# PREPARE CONTROL INPUTS |
| 100 | +# ============================================================================ |
| 101 | + |
| 102 | +println("Preparing control inputs...") |
| 103 | +set_values = zeros(Float64, steps, num_winches) |
| 104 | + |
| 105 | +for step in 1:steps |
| 106 | + t = (step-1) * dt |
| 107 | + steering = steering_magnitude * cos(2π * steering_freq * t + bias) |
| 108 | + set_values[step, :] = [0.0, steering, -steering] |
| 109 | +end |
| 110 | + |
| 111 | +# ============================================================================ |
| 112 | +# RUN REAL-TIME SIMULATION |
| 113 | +# ============================================================================ |
| 114 | + |
| 115 | +println("\nStarting real-time simulation...") |
| 116 | +println(" Total time: $(total_time)s") |
| 117 | +println(" Time step: $(dt)s") |
| 118 | +println(" Realtime factor: $(realtime_factor)x") |
| 119 | +println(" Plot update interval: every $(plot_interval) step(s)") |
| 120 | +println("\nSimulation running... (you can interact with the 3D view)\n") |
| 121 | + |
| 122 | +# Initialize state |
| 123 | +logger = SymbolicAWEModels.Logger(length(sys_struct.points), steps+1) |
| 124 | +sys_state = SysState(sam) |
| 125 | +sys_state.time = 0.0 |
| 126 | +SymbolicAWEModels.log!(logger, sys_state) |
| 127 | + |
| 128 | +steady_torque = SymbolicAWEModels.calc_steady_torque(sam) |
| 129 | +torque_damp = 0.9 |
| 130 | +set_torques = similar(set_values) |
| 131 | + |
| 132 | +# Simulation loop with real-time visualization |
| 133 | +start_time = time() |
| 134 | +for step in 1:steps |
| 135 | + global steady_torque # Declare that we're modifying the global variable |
| 136 | + t = step * dt |
| 137 | + target_elapsed = t / realtime_factor |
| 138 | + |
| 139 | + # Calculate control torques |
| 140 | + steady_torque = torque_damp * steady_torque + (1-torque_damp) * SymbolicAWEModels.calc_steady_torque(sam) |
| 141 | + set_torques[step, :] = steady_torque .+ set_values[step, :] |
| 142 | + |
| 143 | + # Simulation step |
| 144 | + try |
| 145 | + next_step!(sam; set_values=set_torques[step, :], dt, vsm_interval) |
| 146 | + catch e |
| 147 | + if e isa AssertionError |
| 148 | + @warn "Simulation crashed at t=$t" |
| 149 | + break |
| 150 | + else |
| 151 | + rethrow(e) |
| 152 | + end |
| 153 | + end |
| 154 | + |
| 155 | + # Update system state and log |
| 156 | + SymbolicAWEModels.update_sys_state!(sys_state, sam) |
| 157 | + sys_state.time = t |
| 158 | + SymbolicAWEModels.log!(logger, sys_state) |
| 159 | + |
| 160 | + # Update visualization |
| 161 | + if step % plot_interval == 0 |
| 162 | + # Update observables from current system state |
| 163 | + update_plot_observables!( |
| 164 | + segment_points_obs, point_positions_obs, |
| 165 | + wing_origins_obs, wing_directions_obs, |
| 166 | + sys_struct; vector_scale |
| 167 | + ) |
| 168 | + |
| 169 | + # Update text overlays |
| 170 | + sim_time_text[] = @sprintf("Time: %.2f s", t) |
| 171 | + progress_text[] = @sprintf("Progress: %d%%", round(Int, 100 * step / steps)) |
| 172 | + |
| 173 | + # Force Makie to process events and update display |
| 174 | + sleep(0.001) |
| 175 | + end |
| 176 | + |
| 177 | + # Sleep to maintain real-time pacing |
| 178 | + actual_elapsed = time() - start_time |
| 179 | + sleep_time = max(0.0, target_elapsed - actual_elapsed) |
| 180 | + sleep(sleep_time) |
| 181 | + |
| 182 | + # Print progress every 10% |
| 183 | + if step % (steps ÷ 10) == 0 |
| 184 | + @printf(" %.0f%% complete (t=%.1fs)\n", 100 * step / steps, t) |
| 185 | + end |
| 186 | +end |
| 187 | + |
| 188 | +total_elapsed = time() - start_time |
| 189 | +println("\nSimulation complete!") |
| 190 | +println(" Total runtime: $(round(total_elapsed, digits=2))s") |
| 191 | +println(" Speedup: $(round(total_time / total_elapsed, digits=2))x realtime") |
| 192 | + |
| 193 | +# ============================================================================ |
| 194 | +# SAVE AND PLOT RESULTS |
| 195 | +# ============================================================================ |
| 196 | + |
| 197 | +println("\nSaving results...") |
| 198 | +mkpath(get_data_path()) |
| 199 | +SymbolicAWEModels.save_log(logger, "tmp_realtime_run") |
| 200 | +lg = load_log("tmp_realtime_run") |
| 201 | + |
| 202 | +println("Creating post-simulation plots...") |
| 203 | +fig_results = plot(sam.sys_struct, lg; plot_default=false, plot_heading=true, plot_aoa=true) |
| 204 | +display(fig_results) |
| 205 | + |
| 206 | +println("\nDone! Close the windows to exit.") |
0 commit comments