Skip to content

Commit 016f5ca

Browse files
authored
Merge pull request #109 from OpenSourceAWE/transform
Add the function reposition
2 parents 116bbc5 + 9903120 commit 016f5ca

File tree

11 files changed

+252
-22
lines changed

11 files changed

+252
-22
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/LocalPreferences*
2+
.helix
23
*.arrow
34
*.bin
45
*.bin.xz

bin/run_julia

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ if [[ "$copy_manifest" == "true" ]]; then
110110
else
111111
println("Copying default manifest: \n\t", src_manifest, " -> \n\t", dest_manifest)
112112
cp(src_manifest, dest_manifest, force=true)
113+
# Ensure the copied manifest is not write-protected
114+
chmod(dest_manifest, 0o644)
113115
end
114116
'
115117

docs/src/exported_functions.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ These functions provide convenient wrappers for running common simulation scenar
1616
sim!
1717
sim_oscillate!
1818
sim_turn!
19+
sim_reposition!
1920
```
2021

2122
## Low-Level Simulation and Analysis

docs/src/private_functions.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ SymbolicAWEModels.setstate!
2828
SymbolicAWEModels.set_measured!
2929
SymbolicAWEModels.copy!
3030
SymbolicAWEModels.reinit!
31+
SymbolicAWEModels.reposition!
3132
SymbolicAWEModels.update_sys_struct!
3233
SymbolicAWEModels.get_set_hash
3334
SymbolicAWEModels.get_sys_struct_hash

examples/reposition.jl

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Copyright (c) 2025 Bart van de Lint
2+
# SPDX-License-Identifier: MPL-2.0
3+
4+
using Pkg
5+
if ! ("ControlPlots" keys(Pkg.project().dependencies))
6+
using TestEnv; TestEnv.activate()
7+
end
8+
using SymbolicAWEModels, KiteUtils, LinearAlgebra, ControlPlots, OrdinaryDiffEqBDF
9+
10+
# Initialize the model
11+
set = Settings("system.yaml")
12+
set.sample_freq = 100
13+
sam = SymbolicAWEModel(set)
14+
init!(sam)
15+
find_steady_state!(sam)
16+
17+
# Run the simulation with repositioning
18+
lg = SymbolicAWEModels.sim_reposition!(
19+
sam,
20+
total_time=10.0,
21+
reposition_interval_s=0.5,
22+
target_elevation_deg=50.0,
23+
target_azimuth_deg=10.0
24+
)
25+
26+
# Plot the results
27+
plot(sam.sys_struct, lg)
28+
29+

src/SymbolicAWEModels.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export DynamicsType, DYNAMIC, QUASI_STATIC, WING, STATIC
7070
export SegmentType, POWER_LINE, STEERING_LINE, BRIDLE
7171

7272
# --- High-Level Simulation Functions (Workers) ---
73-
export sim!, sim_oscillate!, sim_turn!
73+
export sim!, sim_oscillate!, sim_turn!, sim_reposition!
7474

7575
# --- Low-Level Simulation Functions ---
7676
export find_steady_state!

src/model_management.jl

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -237,10 +237,8 @@ Create and cache a simplified linear model if it does not exist or if the output
237237
# Returns
238238
- `true` if a new model was created, `false` otherwise.
239239
"""
240-
function maybe_create_simple_lin_model!(sam, outputs; create_simple_lin_model=true,
241-
outputs_changed=false, prn=true)
242-
if create_simple_lin_model &&
243-
(isnothing(sam.simple_lin_model) || outputs_changed)
240+
function maybe_create_simple_lin_model!(sam, outputs; create_simple_lin_model=true, prn=true)
241+
if create_simple_lin_model && isnothing(sam.simple_lin_model)
244242
sys = sam.prob.sys
245243
time = @elapsed slm_attrs = generate_simple_lin_model(sam.sys_struct, sys, outputs)
246244
if !isnothing(slm_attrs.model)
@@ -267,10 +265,8 @@ Create and cache the `LinearizationProblem` if it does not exist or if the outpu
267265
# Returns
268266
- `true` if a new problem was created, `false` otherwise.
269267
"""
270-
function maybe_create_lin_prob!(sam, outputs; create_lin_prob=true,
271-
outputs_changed=false, prn=true)
272-
if create_lin_prob &&
273-
(isnothing(sam.lin_prob) || outputs_changed)
268+
function maybe_create_lin_prob!(sam, outputs; create_lin_prob=true, prn=true)
269+
if create_lin_prob && isnothing(sam.lin_prob)
274270
time = @elapsed @suppress_err begin
275271
lin_fun, lin_sys = linearization_function(sam.full_sys, [sam.inputs...], outputs;
276272
op=sam.defaults, guesses=sam.guesses)
@@ -300,10 +296,8 @@ Create and cache the control functions if they do not exist or if the outputs ha
300296
# Returns
301297
- `true` if new functions were created, `false` otherwise.
302298
"""
303-
function maybe_create_control_functions!(sam, outputs; create_control_func=false,
304-
outputs_changed=false, prn=true)
305-
if create_control_func &&
306-
(isnothing(sam.control_funcs) || outputs_changed)
299+
function maybe_create_control_functions!(sam, outputs; create_control_func=false, prn=true)
300+
if create_control_func && isnothing(sam.control_funcs)
307301
inputs = [sam.inputs...]
308302
time = @elapsed result = generate_control_funcs(sam.full_sys, inputs, outputs)
309303
sam.control_funcs = ControlFuncWithAttributes(; result...)
@@ -388,16 +382,19 @@ function init!(sam::SymbolicAWEModel;
388382
outputs_changed = isnothing(sam.outputs) ||
389383
length(outputs) != length(sam.outputs) ||
390384
!all(string.(outputs) .== string.(sam.outputs))
385+
if outputs_changed
386+
sam.simple_lin_model = nothing
387+
sam.lin_prob = nothing
388+
sam.control_funcs = nothing
389+
end
391390
sam.outputs = outputs
392391

392+
changed |= outputs_changed
393393
changed |= maybe_create_prob!(sam; create_prob, prn)
394394
changed |= maybe_create_simple_lin_model!(sam, outputs;
395-
create_simple_lin_model=create_prob,
396-
outputs_changed, prn)
397-
changed |= maybe_create_lin_prob!(sam, outputs; create_lin_prob,
398-
outputs_changed, prn)
399-
changed |= maybe_create_control_functions!(sam, outputs; create_control_func,
400-
outputs_changed, prn)
395+
create_simple_lin_model=create_prob, prn)
396+
changed |= maybe_create_lin_prob!(sam, outputs; create_lin_prob, prn)
397+
changed |= maybe_create_control_functions!(sam, outputs; create_control_func, prn)
401398

402399
if changed
403400
prn && @info "Serializing model to: \n\t$model_path"

src/precompile.jl

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,13 +127,16 @@ end
127127
data_path = joinpath(path, "data")
128128
set_data_path(data_path)
129129

130-
# Copy .default files to expected files
131-
cp(joinpath(path, "Artifacts.toml.default"), joinpath(path, "Artifacts.toml"); force=true)
130+
# Copy .default files to expected files and make them writable
131+
artifacts_toml_path = joinpath(path, "Artifacts.toml")
132+
cp(joinpath(path, "Artifacts.toml.default"), artifacts_toml_path; force=true)
133+
chmod(artifacts_toml_path, 0o644)
132134

133135
version_minor = VERSION.minor
134136
manifest_default = joinpath(path, "Manifest-v1.$version_minor.toml.default")
135137
manifest_actual = joinpath(path, "Manifest-v1.$version_minor.toml")
136138
cp(manifest_default, manifest_actual; force=true)
139+
chmod(manifest_actual, 0o644)
137140

138141
# Use explicit Artifacts API to get the artifact and extract it
139142
artifact_toml = joinpath(path, "Artifacts.toml")
@@ -144,7 +147,10 @@ end
144147
model_dir = artifact_path(model_hash)
145148
mkpath(data_path)
146149
for f in readdir(model_dir)
147-
cp(joinpath(model_dir, f), joinpath(data_path, f); force=true)
150+
src_path = joinpath(model_dir, f)
151+
dest_path = joinpath(data_path, f)
152+
cp(src_path, dest_path; force=true)
153+
chmod(dest_path, 0o644) # Ensure copied artifact file is writable
148154
end
149155
@info "Downloaded and extracted $artifact_name to $data_path"
150156
else

src/simulate.jl

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,113 @@ function sim_turn!(
225225
end
226226

227227

228+
"""
229+
sim_reposition!(sam; dt, total_time, reposition_interval_s, target_elevation_deg,
230+
target_azimuth_deg, prn)
231+
232+
Run a simulation that periodically resets the kite's elevation and azimuth.
233+
234+
This function simulates the AWE model and, at a specified time interval, calls
235+
`reposition!` to reposition the kite to a target elevation and azimuth. It logs
236+
the entire simulation and returns a `SysLog`.
237+
238+
# Arguments
239+
- `sam::SymbolicAWEModel`: Initialized AWE model.
240+
241+
# Keywords
242+
- `dt::Float64`: Time step [s]. Defaults to `1/sam.set.sample_freq`.
243+
- `total_time::Float64`: Total simulation duration [s]. Defaults to 20.0.
244+
- `reposition_interval_s::Float64`: The interval in seconds at which to reset the pose. Defaults to 5.0.
245+
- `target_elevation::Float64`: The target elevation in rad for repositioning. Defaults to deg2rad(45.0).
246+
- `target_azimuth::Float64`: The target azimuth in rad for repositioning. Defaults to 0.0.
247+
- `target_heading::Float64`: The target heading in rad for repositioning. Defaults to 0.0.
248+
- `prn::Bool`: If true, prints status messages during the simulation. Defaults to true.
249+
250+
# Returns
251+
- `SysLog`: A log of the complete simulation.
252+
"""
253+
function sim_reposition!(
254+
sam::SymbolicAWEModel;
255+
dt=1/sam.set.sample_freq,
256+
total_time=20.0,
257+
reposition_interval_s=5.0,
258+
target_elevation=deg2rad(45.0),
259+
target_azimuth=0.0,
260+
target_heading=0.0,
261+
prn=true
262+
)
263+
# 1. --- Initialization ---
264+
sys_struct = sam.sys_struct
265+
steps = Int(round(total_time / dt))
266+
reposition_interval_steps = Int(round(reposition_interval_s / dt))
267+
set_values = zeros(Float64, steps, length(sys_struct.winches))
268+
vsm_interval = 1 ÷ dt
269+
270+
logger = Logger(length(sys_struct.points), steps)
271+
sys_state = SysState(sam)
272+
273+
if prn
274+
println("--- Starting simulation with periodic repositioning ---")
275+
println("Total time: $(total_time)s, Reposition interval: $(reposition_interval_s)s")
276+
end
277+
278+
# 2. --- Simulation Loop ---
279+
time = @elapsed for step in 1:steps
280+
t = (step-1) * dt
281+
282+
# Hold the kite in place by countering the tether forces with winch torques
283+
set_values[step, :] = -sam.set.drum_radius .* [norm(winch.force) for winch in sys_struct.winches]
284+
285+
# --- Repositioning Logic ---
286+
if step > 1 && (step - 1) % reposition_interval_steps == 0
287+
if prn
288+
println("\n>>> Time: $(round(t, digits=2))s. Repositioning kite...")
289+
println(">>> Target Elevation: $(rad2deg(target_elevation))°,"*
290+
" Target Azimuth: $(rad2deg(target_azimuth))°")
291+
end
292+
293+
# Update the transform with the new target pose
294+
sys_struct.transforms[1].elevation = target_elevation
295+
sys_struct.transforms[1].azimuth = target_azimuth
296+
sys_struct.transforms[1].heading = target_heading - sys_struct.wings[1].heading
297+
298+
# Apply the transformation without changing velocities
299+
SymbolicAWEModels.reposition!(sys_struct.transforms, sys_struct)
300+
301+
# Reinitialize the solver to handle the state discontinuity
302+
SymbolicAWEModels.reinit!(sam, sam.prob, FBDF())
303+
304+
if prn
305+
# Verify the new pose after one step
306+
next_step!(sam; dt=dt, set_values=set_values[step, :], vsm_interval)
307+
updated_elevation_deg = rad2deg(sys_struct.wings[1].elevation)
308+
println(">>> Pose updated. New Elevation is now: " *
309+
"$(round(updated_elevation_deg, digits=2)) degrees.\n")
310+
end
311+
else
312+
# --- Normal simulation step ---
313+
next_step!(sam; dt=dt, set_values=set_values[step, :], vsm_interval)
314+
end
315+
316+
# Log the state at the current time step
317+
update_sys_state!(sys_state, sam)
318+
sys_state.time = t
319+
log!(logger, sys_state)
320+
end
321+
322+
if prn
323+
println("--- Simulation Finished ---")
324+
println("Runtime: $time")
325+
println("Times realtime: $(dt*steps/time)")
326+
end
327+
328+
# Save and return the log
329+
mkpath(get_data_path())
330+
save_log(logger, "tmp_reposition_run")
331+
return load_log("tmp_reposition_run")
332+
end
333+
334+
228335
"""
229336
SysState(y::AbstractVector, sam::SymbolicAWEModel, t::Real; zoom=1.0)
230337

src/system_structure.jl

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -866,6 +866,66 @@ function reinit!(transforms::Vector{Transform}, sys_struct::SystemStructure)
866866
end
867867
end
868868

869+
"""
870+
reposition!(transforms::Vector{Transform}, sys_struct::SystemStructure)
871+
872+
Update the system's spatial orientation based on its current position, preserving velocities.
873+
874+
This function adjusts the orientation of all components in the `SystemStructure` without
875+
altering their dynamic state. Unlike `reinit!`, it uses the current world positions (`pos_w`)
876+
as the starting point for rotations, rather than resetting from the CAD coordinates.
877+
878+
This function is useful for making real-time adjustments to the system's pose during a simulation.
879+
Crucially, it **preserves the existing velocities (`vel_w`) of all points and wings**.
880+
881+
NOTE: the transform.heading is applied relative to the current heading of the system.
882+
883+
# Arguments
884+
- `sys_struct::SystemStructure`: The system model to update.
885+
"""
886+
function reposition!(transforms::Vector{Transform}, sys_struct::SystemStructure)
887+
@unpack points, wings = sys_struct
888+
for transform in transforms
889+
# Get the current positions of the base and the rotating object
890+
base_pos = points[transform.base_point_idx].pos_w
891+
rot_pos = get_rot_pos(transform, wings, points)
892+
893+
# Calculate the current orientation in spherical coordinates
894+
current_rel_pos = rot_pos - base_pos
895+
curr_elevation = KiteUtils.calc_elevation(current_rel_pos)
896+
curr_azimuth = -KiteUtils.azimuth_east(current_rel_pos)
897+
898+
# Get the rotation matrices for the current and target orientations
899+
curr_R_t_w = calc_R_t_w(curr_elevation, curr_azimuth)
900+
R_t_w = calc_R_t_w(transform.elevation, transform.azimuth)
901+
902+
# Apply the rotation to all relevant points
903+
for point in points
904+
if point.transform_idx == transform.idx
905+
vec = point.pos_w - base_pos
906+
point.pos_w .= base_pos + apply_heading(vec, R_t_w, curr_R_t_w, transform.heading)
907+
end
908+
end
909+
910+
# Apply the rotation to all relevant wings
911+
for wing in wings
912+
if wing.transform_idx == transform.idx
913+
# Rotate the wing's position
914+
vec = wing.pos_w - base_pos
915+
wing.pos_w .= base_pos + apply_heading(vec, R_t_w, curr_R_t_w, transform.heading)
916+
917+
# Rotate the wing's orientation matrix
918+
R_b_w = zeros(3,3)
919+
current_R_b_w = wing.R_b_w
920+
for i in 1:3
921+
R_b_w[:, i] .= apply_heading(current_R_b_w[:, i], R_t_w, curr_R_t_w, transform.heading)
922+
end
923+
wing.R_b_w = R_b_w
924+
end
925+
end
926+
end
927+
end
928+
869929
"""
870930
calc_pos(wing::RamAirWing, gamma, frac)
871931

0 commit comments

Comments
 (0)