Skip to content
Merged
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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@ SPDX-FileCopyrightText: 2025 Uwe Fechner, Bart van de Lint
SPDX-License-Identifier: MPL-2.0
-->

# v0.7.1 27-02-2026

## Added
- `update_sys_struct_from_yaml!()` — update a `SystemStructure` in-place
from a modified YAML file (point `pos_cad` and segment `l0`).
- `segment_cad_length()` and `autocalc_tether_len()` shared helpers,
replacing duplicated code in the constructor, `reinit!`, and YAML loader.

## Fixed
- `SystemStructure` constructor auto-calculates `winch.tether_len` from
all connected tethers (was only using the first).

# v0.7.0 DD-02-2026

## Changed
Expand Down
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name = "SymbolicAWEModels"
uuid = "9c9a347c-5289-41db-a9b9-25ccc76c3360"
version = "0.7.0"
version = "0.7.1"
authors = ["Bart van de Lint <bart@vandelint.net> and contributors"]

[deps]
Expand Down
1 change: 1 addition & 0 deletions docs/src/exported_functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ simple_linearize!

```@docs
load_sys_struct_from_yaml
update_sys_struct_from_yaml!
```

## System configuration
Expand Down
2 changes: 2 additions & 0 deletions docs/src/private_functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ SymbolicAWEModels.update_aero_yaml_from_struc_yaml!
## SystemStructure internals

```@docs
SymbolicAWEModels.segment_cad_length
SymbolicAWEModels.autocalc_tether_len
SymbolicAWEModels.assign_indices_and_resolve!
SymbolicAWEModels.resolve_ref
SymbolicAWEModels.resolve_ref_spec
Expand Down
1 change: 1 addition & 0 deletions src/SymbolicAWEModels.jl
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ export update_plot_observables!
export animate
export load_sys_struct_from_yaml
export update_yaml_from_sys_struct!
export update_sys_struct_from_yaml!
export update_aero_yaml_from_struc_yaml!
export replay
export record
Expand Down
7 changes: 3 additions & 4 deletions src/system_structure/system_structure_core.jl
Original file line number Diff line number Diff line change
Expand Up @@ -573,7 +573,7 @@ function SystemStructure(name, set;
end
for (i, segment) in enumerate(segments)
@assert segment.idx == i
(segment.l0 ≈ 0) && (segment.l0 = norm(points[segment.point_idxs[1]].pos_cad - points[segment.point_idxs[2]].pos_cad))
(segment.l0 ≈ 0) && (segment.l0 = segment_cad_length(segment, points))
end
for (i, pulley) in enumerate(pulleys)
@assert pulley.idx == i
Expand All @@ -584,9 +584,8 @@ function SystemStructure(name, set;
for (i, winch) in enumerate(winches)
@assert winch.idx == i
if iszero(winch.tether_len)
for segment_idx in tethers[winch.tether_idxs[1]].segment_idxs
winch.tether_len += segments[segment_idx].l0
end
winch.tether_len = autocalc_tether_len(
winch, tethers, segments)
end
end
# Compute body frame (COM + principal axes) and
Expand Down
37 changes: 30 additions & 7 deletions src/system_structure/utilities.jl
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,32 @@ This file contains:
- Segment statistics
"""

# ==================== SHARED HELPERS ==================== #

"""
segment_cad_length(segment::Segment, points)

Compute segment length from endpoint `pos_cad` positions.
"""
function segment_cad_length(segment::Segment, points)
p1 = points[segment.point_idxs[1]]
p2 = points[segment.point_idxs[2]]
return norm(p1.pos_cad - p2.pos_cad)
end

"""
autocalc_tether_len(winch::Winch, tethers, segments)

Average unstretched tether length across all tethers connected
to this winch (sum of segment `l0` per tether, then average).
"""
function autocalc_tether_len(winch::Winch, tethers, segments)
n = length(winch.tether_idxs)
return sum(segments[seg_idx].l0
for tether_idx in winch.tether_idxs
for seg_idx in tethers[tether_idx].segment_idxs) / n
end

# ==================== TETHER CREATION ==================== #

"""
Expand Down Expand Up @@ -310,20 +336,17 @@ function reinit!(sys_struct::SystemStructure, set::Settings;
# Transforms are not updated from Settings - YAML structure geometry has priority

for segment in segments
len = norm(points[segment.point_idxs[1]].pos_cad -
points[segment.point_idxs[2]].pos_cad)
len = segment_cad_length(segment, points)
(segment.l0 ≈ 0) && (segment.l0 = len)
segment.len = len
end

# Calculate winch tether_len from settings or sum of segment l0 values
# Calculate winch tether_len from settings or segment l0s
for winch in winches
l_tether = set.l_tethers[winch.idx]
if l_tether == 0
# Calculate from total segment l0 of all segments in this winch's tethers
l_tether = sum(segments[seg_idx].l0
for tether_idx in winch.tether_idxs
for seg_idx in tethers[tether_idx].segment_idxs)
l_tether = autocalc_tether_len(
winch, tethers, segments)
end
winch.tether_len = l_tether
end
Expand Down
81 changes: 81 additions & 0 deletions src/yaml_loader.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1012,6 +1012,87 @@ function update_yaml_from_sys_struct!(sys_struct::SystemStructure,
return nothing
end

"""
update_sys_struct_from_yaml!(sys_struct::SystemStructure,
struc_yaml::AbstractString)

Update an existing `SystemStructure` in-place from a (possibly modified)
structural geometry YAML file. Inverse of `update_yaml_from_sys_struct!`.

Updates `pos_cad` for points and `l0` for segments, matched by symbolic
name. When `l0` is `nothing` in the YAML, it is auto-calculated from the
endpoint `pos_cad` positions.

Only raw geometry is updated. Call `reinit!(sys_struct, set)` afterward
to recompute derived quantities (`pos_b`, `pos_w`, wing frames, etc.).

Unmatched names are silently skipped (the YAML may contain a subset of
components).

# Arguments
- `sys_struct`: The SystemStructure to update in-place.
- `struc_yaml`: Path to the structural geometry YAML file.

# Example
```julia
sys = load_sys_struct_from_yaml("struc_geometry.yaml"; ...)
# ... edit YAML externally ...
update_sys_struct_from_yaml!(sys, "struc_geometry.yaml")
```
"""
function update_sys_struct_from_yaml!(
sys_struct::SystemStructure,
struc_yaml::AbstractString)
yaml_path = isabspath(struc_yaml) ? struc_yaml :
joinpath(pwd(), struc_yaml)
isfile(yaml_path) ||
error("YAML file not found: $yaml_path")

data = YAML.load_file(yaml_path)

# --- Update points ---
n_points = 0
if haskey(data, "points")
point_rows = parse_table(data["points"])
for row in point_rows
haskey(row, :name) || continue
name = Symbol(row.name)
haskey(sys_struct.points, name) || continue

point = sys_struct.points[name]
point.pos_cad .= KVec3(row.pos_cad...)
n_points += 1
end
end

# --- Update segment l0 ---
n_segments = 0
if haskey(data, "segments")
segment_rows = parse_table(data["segments"])
for row in segment_rows
haskey(row, :name) || continue
name = Symbol(row.name)
haskey(sys_struct.segments, name) || continue

seg = sys_struct.segments[name]

# l0: use YAML value, or auto-calc from pos_cad
l0_val = haskey(row, :l0) ? row.l0 : nothing
if !isnothing(l0_val) && l0_val != "nothing"
seg.l0 = Float64(l0_val)
else
seg.l0 = segment_cad_length(
seg, sys_struct.points)
end

n_segments += 1
end
end

@info "update_sys_struct_from_yaml!" n_points n_segments
return nothing
end

"""
update_aero_yaml_from_struc_yaml!(source_struc_yaml, source_aero_yaml,
dest_aero_yaml=source_aero_yaml)
Expand Down
172 changes: 172 additions & 0 deletions test/test_yaml_update.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# SPDX-FileCopyrightText: 2025 Bart van de Lint
# SPDX-License-Identifier: MPL-2.0

# test_yaml_update.jl - Tests for update_sys_struct_from_yaml!
#
# Verifies:
# 1. Unchanged YAML round-trip: pos_cad and l0 unchanged
# 2. Modified pos_cad: only the changed point is updated
# 3. Modified segment l0: segment updated correctly
# 4. l0=nothing in YAML: auto-calc from pos_cad

using Test
using SymbolicAWEModels
using SymbolicAWEModels: KVec3
using LinearAlgebra: norm
using KiteUtils

const YAML_UPDATE_BASE = """
points:
headers: [name, pos_cad, type, wing_idx, transform_idx,
extra_mass, body_frame_damping,
world_frame_damping, area, drag_coeff]
data:
- [pt_a, [1.0, 2.0, 3.0], DYNAMIC, nothing, nothing,
1.0, 0.0, 0.0, 0.0, 0.0]
- [pt_b, [4.0, 5.0, 6.0], DYNAMIC, nothing, nothing,
1.0, 0.0, 0.0, 0.0, 0.0]

segments:
headers: [name, point_i, point_j, type, l0,
diameter_mm, unit_stiffness, unit_damping,
compression_frac]
data:
- [seg_ab, pt_a, pt_b, POWER_LINE, 10.0,
1.0, 5000.0, 10.0, 1.0]
"""

const SETTINGS_YAML_UPDATE = """
system:
log_file: "data/2plate"
g_earth: 9.81
initial:
l_tethers: [0.0]
v_reel_outs: [0.0]
solver:
solver: "FBDF"
abs_tol: 0.01
rel_tol: 0.01
relaxation: 0.6
kite:
model: ""
foil_file: "ram_air_kite/ram_air_kite_foil.dat"
physical_model: "2plate"
struc_geometry_path: "struc_geometry.yaml"
aero_geometry_path: "aero_geometry.yaml"
mass: 0.0
quasi_static: false
tether:
cd_tether: 0.958
unit_damping: 0.0
unit_stiffness: 0.0
rho_tether: 724.0
e_tether: 5.5e10
winch:
winch_model: "TorqueControlledMachine"
drum_radius: 0.110
gear_ratio: 1.0
inertia_total: 0.024
f_coulomb: 122.0
c_vf: 30.6
environment:
rho_0: 1.225
v_wind: 0.0
upwind_dir: -90.0
profile_law: 0
"""

@testset "update_sys_struct_from_yaml!" begin
tmpdir = mktempdir()
yaml_path = joinpath(tmpdir, "geometry.yaml")
write(yaml_path, YAML_UPDATE_BASE)

settings_path = joinpath(tmpdir, "settings.yaml")
write(settings_path, SETTINGS_YAML_UPDATE)
system_path = joinpath(tmpdir, "system.yaml")
write(system_path, """
system:
sim_settings: settings.yaml
""")

set_data_path(tmpdir)
set = Settings("system.yaml")

sys = load_sys_struct_from_yaml(yaml_path;
system_name="yaml_update_test", set=set)

# --------------------------------------------------------
# Test 1: Unchanged YAML round-trip
# --------------------------------------------------------
@testset "Unchanged round-trip" begin
orig_a = copy(sys.points[:pt_a].pos_cad)
orig_b = copy(sys.points[:pt_b].pos_cad)
orig_l0 = sys.segments[:seg_ab].l0

update_sys_struct_from_yaml!(sys, yaml_path)

@test sys.points[:pt_a].pos_cad == orig_a
@test sys.points[:pt_b].pos_cad == orig_b
@test sys.segments[:seg_ab].l0 == orig_l0
end

# --------------------------------------------------------
# Test 2: Modified point position
# --------------------------------------------------------
@testset "Modified point position" begin
modified_yaml = replace(
YAML_UPDATE_BASE,
"[1.0, 2.0, 3.0]" => "[10.0, 20.0, 30.0]")
mod_path = joinpath(tmpdir, "modified_pos.yaml")
write(mod_path, modified_yaml)

orig_b = copy(sys.points[:pt_b].pos_cad)

update_sys_struct_from_yaml!(sys, mod_path)

@test sys.points[:pt_a].pos_cad ==
KVec3(10.0, 20.0, 30.0)
# pt_b should be unchanged
@test sys.points[:pt_b].pos_cad == orig_b
end

# --------------------------------------------------------
# Test 3: Modified segment l0
# --------------------------------------------------------
@testset "Modified segment l0" begin
modified_yaml = replace(
YAML_UPDATE_BASE,
"10.0,\n 1.0, 5000.0" =>
"42.5,\n 1.0, 5000.0")
mod_path = joinpath(tmpdir, "modified_l0.yaml")
write(mod_path, modified_yaml)

update_sys_struct_from_yaml!(sys, mod_path)

@test sys.segments[:seg_ab].l0 == 42.5
end

# --------------------------------------------------------
# Test 4: l0=nothing auto-calculates from pos_cad
# --------------------------------------------------------
@testset "l0=nothing auto-calc from pos_cad" begin
# First reset to base YAML so pos_cad is known
write(yaml_path, YAML_UPDATE_BASE)
update_sys_struct_from_yaml!(sys, yaml_path)

expected_l0 = norm(
KVec3(1.0, 2.0, 3.0) - KVec3(4.0, 5.0, 6.0))

nothing_yaml = replace(
YAML_UPDATE_BASE,
"10.0,\n 1.0, 5000.0" =>
"nothing,\n 1.0, 5000.0")
nothing_path = joinpath(tmpdir, "nothing_l0.yaml")
write(nothing_path, nothing_yaml)

update_sys_struct_from_yaml!(sys, nothing_path)

@test sys.segments[:seg_ab].l0 ≈ expected_l0
end

rm(tmpdir; recursive=true)
end
Loading