Skip to content

Commit ec41fb3

Browse files
authored
Update sys struct from yaml (#154)
* Add update sys struct from yaml * Remove material resolution and update * Fix tether len bug * Fix docs
1 parent 55bc4ec commit ec41fb3

File tree

9 files changed

+303
-12
lines changed

9 files changed

+303
-12
lines changed

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@ SPDX-FileCopyrightText: 2025 Uwe Fechner, Bart van de Lint
33
SPDX-License-Identifier: MPL-2.0
44
-->
55

6+
# v0.7.1 27-02-2026
7+
8+
## Added
9+
- `update_sys_struct_from_yaml!()` — update a `SystemStructure` in-place
10+
from a modified YAML file (point `pos_cad` and segment `l0`).
11+
- `segment_cad_length()` and `autocalc_tether_len()` shared helpers,
12+
replacing duplicated code in the constructor, `reinit!`, and YAML loader.
13+
14+
## Fixed
15+
- `SystemStructure` constructor auto-calculates `winch.tether_len` from
16+
all connected tethers (was only using the first).
17+
618
# v0.7.0 DD-02-2026
719

820
## Changed

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name = "SymbolicAWEModels"
22
uuid = "9c9a347c-5289-41db-a9b9-25ccc76c3360"
3-
version = "0.7.0"
3+
version = "0.7.1"
44
authors = ["Bart van de Lint <bart@vandelint.net> and contributors"]
55

66
[deps]

docs/src/exported_functions.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ simple_linearize!
3434

3535
```@docs
3636
load_sys_struct_from_yaml
37+
update_sys_struct_from_yaml!
3738
```
3839

3940
## System configuration

docs/src/private_functions.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ SymbolicAWEModels.update_aero_yaml_from_struc_yaml!
127127
## SystemStructure internals
128128

129129
```@docs
130+
SymbolicAWEModels.segment_cad_length
131+
SymbolicAWEModels.autocalc_tether_len
130132
SymbolicAWEModels.assign_indices_and_resolve!
131133
SymbolicAWEModels.resolve_ref
132134
SymbolicAWEModels.resolve_ref_spec

src/SymbolicAWEModels.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ export update_plot_observables!
9797
export animate
9898
export load_sys_struct_from_yaml
9999
export update_yaml_from_sys_struct!
100+
export update_sys_struct_from_yaml!
100101
export update_aero_yaml_from_struc_yaml!
101102
export replay
102103
export record

src/system_structure/system_structure_core.jl

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -573,7 +573,7 @@ function SystemStructure(name, set;
573573
end
574574
for (i, segment) in enumerate(segments)
575575
@assert segment.idx == i
576-
(segment.l0 0) && (segment.l0 = norm(points[segment.point_idxs[1]].pos_cad - points[segment.point_idxs[2]].pos_cad))
576+
(segment.l0 0) && (segment.l0 = segment_cad_length(segment, points))
577577
end
578578
for (i, pulley) in enumerate(pulleys)
579579
@assert pulley.idx == i
@@ -584,9 +584,8 @@ function SystemStructure(name, set;
584584
for (i, winch) in enumerate(winches)
585585
@assert winch.idx == i
586586
if iszero(winch.tether_len)
587-
for segment_idx in tethers[winch.tether_idxs[1]].segment_idxs
588-
winch.tether_len += segments[segment_idx].l0
589-
end
587+
winch.tether_len = autocalc_tether_len(
588+
winch, tethers, segments)
590589
end
591590
end
592591
# Compute body frame (COM + principal axes) and

src/system_structure/utilities.jl

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,32 @@ This file contains:
1313
- Segment statistics
1414
"""
1515

16+
# ==================== SHARED HELPERS ==================== #
17+
18+
"""
19+
segment_cad_length(segment::Segment, points)
20+
21+
Compute segment length from endpoint `pos_cad` positions.
22+
"""
23+
function segment_cad_length(segment::Segment, points)
24+
p1 = points[segment.point_idxs[1]]
25+
p2 = points[segment.point_idxs[2]]
26+
return norm(p1.pos_cad - p2.pos_cad)
27+
end
28+
29+
"""
30+
autocalc_tether_len(winch::Winch, tethers, segments)
31+
32+
Average unstretched tether length across all tethers connected
33+
to this winch (sum of segment `l0` per tether, then average).
34+
"""
35+
function autocalc_tether_len(winch::Winch, tethers, segments)
36+
n = length(winch.tether_idxs)
37+
return sum(segments[seg_idx].l0
38+
for tether_idx in winch.tether_idxs
39+
for seg_idx in tethers[tether_idx].segment_idxs) / n
40+
end
41+
1642
# ==================== TETHER CREATION ==================== #
1743

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

312338
for segment in segments
313-
len = norm(points[segment.point_idxs[1]].pos_cad -
314-
points[segment.point_idxs[2]].pos_cad)
339+
len = segment_cad_length(segment, points)
315340
(segment.l0 0) && (segment.l0 = len)
316341
segment.len = len
317342
end
318343

319-
# Calculate winch tether_len from settings or sum of segment l0 values
344+
# Calculate winch tether_len from settings or segment l0s
320345
for winch in winches
321346
l_tether = set.l_tethers[winch.idx]
322347
if l_tether == 0
323-
# Calculate from total segment l0 of all segments in this winch's tethers
324-
l_tether = sum(segments[seg_idx].l0
325-
for tether_idx in winch.tether_idxs
326-
for seg_idx in tethers[tether_idx].segment_idxs)
348+
l_tether = autocalc_tether_len(
349+
winch, tethers, segments)
327350
end
328351
winch.tether_len = l_tether
329352
end

src/yaml_loader.jl

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1012,6 +1012,87 @@ function update_yaml_from_sys_struct!(sys_struct::SystemStructure,
10121012
return nothing
10131013
end
10141014

1015+
"""
1016+
update_sys_struct_from_yaml!(sys_struct::SystemStructure,
1017+
struc_yaml::AbstractString)
1018+
1019+
Update an existing `SystemStructure` in-place from a (possibly modified)
1020+
structural geometry YAML file. Inverse of `update_yaml_from_sys_struct!`.
1021+
1022+
Updates `pos_cad` for points and `l0` for segments, matched by symbolic
1023+
name. When `l0` is `nothing` in the YAML, it is auto-calculated from the
1024+
endpoint `pos_cad` positions.
1025+
1026+
Only raw geometry is updated. Call `reinit!(sys_struct, set)` afterward
1027+
to recompute derived quantities (`pos_b`, `pos_w`, wing frames, etc.).
1028+
1029+
Unmatched names are silently skipped (the YAML may contain a subset of
1030+
components).
1031+
1032+
# Arguments
1033+
- `sys_struct`: The SystemStructure to update in-place.
1034+
- `struc_yaml`: Path to the structural geometry YAML file.
1035+
1036+
# Example
1037+
```julia
1038+
sys = load_sys_struct_from_yaml("struc_geometry.yaml"; ...)
1039+
# ... edit YAML externally ...
1040+
update_sys_struct_from_yaml!(sys, "struc_geometry.yaml")
1041+
```
1042+
"""
1043+
function update_sys_struct_from_yaml!(
1044+
sys_struct::SystemStructure,
1045+
struc_yaml::AbstractString)
1046+
yaml_path = isabspath(struc_yaml) ? struc_yaml :
1047+
joinpath(pwd(), struc_yaml)
1048+
isfile(yaml_path) ||
1049+
error("YAML file not found: $yaml_path")
1050+
1051+
data = YAML.load_file(yaml_path)
1052+
1053+
# --- Update points ---
1054+
n_points = 0
1055+
if haskey(data, "points")
1056+
point_rows = parse_table(data["points"])
1057+
for row in point_rows
1058+
haskey(row, :name) || continue
1059+
name = Symbol(row.name)
1060+
haskey(sys_struct.points, name) || continue
1061+
1062+
point = sys_struct.points[name]
1063+
point.pos_cad .= KVec3(row.pos_cad...)
1064+
n_points += 1
1065+
end
1066+
end
1067+
1068+
# --- Update segment l0 ---
1069+
n_segments = 0
1070+
if haskey(data, "segments")
1071+
segment_rows = parse_table(data["segments"])
1072+
for row in segment_rows
1073+
haskey(row, :name) || continue
1074+
name = Symbol(row.name)
1075+
haskey(sys_struct.segments, name) || continue
1076+
1077+
seg = sys_struct.segments[name]
1078+
1079+
# l0: use YAML value, or auto-calc from pos_cad
1080+
l0_val = haskey(row, :l0) ? row.l0 : nothing
1081+
if !isnothing(l0_val) && l0_val != "nothing"
1082+
seg.l0 = Float64(l0_val)
1083+
else
1084+
seg.l0 = segment_cad_length(
1085+
seg, sys_struct.points)
1086+
end
1087+
1088+
n_segments += 1
1089+
end
1090+
end
1091+
1092+
@info "update_sys_struct_from_yaml!" n_points n_segments
1093+
return nothing
1094+
end
1095+
10151096
"""
10161097
update_aero_yaml_from_struc_yaml!(source_struc_yaml, source_aero_yaml,
10171098
dest_aero_yaml=source_aero_yaml)

test/test_yaml_update.jl

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
# SPDX-FileCopyrightText: 2025 Bart van de Lint
2+
# SPDX-License-Identifier: MPL-2.0
3+
4+
# test_yaml_update.jl - Tests for update_sys_struct_from_yaml!
5+
#
6+
# Verifies:
7+
# 1. Unchanged YAML round-trip: pos_cad and l0 unchanged
8+
# 2. Modified pos_cad: only the changed point is updated
9+
# 3. Modified segment l0: segment updated correctly
10+
# 4. l0=nothing in YAML: auto-calc from pos_cad
11+
12+
using Test
13+
using SymbolicAWEModels
14+
using SymbolicAWEModels: KVec3
15+
using LinearAlgebra: norm
16+
using KiteUtils
17+
18+
const YAML_UPDATE_BASE = """
19+
points:
20+
headers: [name, pos_cad, type, wing_idx, transform_idx,
21+
extra_mass, body_frame_damping,
22+
world_frame_damping, area, drag_coeff]
23+
data:
24+
- [pt_a, [1.0, 2.0, 3.0], DYNAMIC, nothing, nothing,
25+
1.0, 0.0, 0.0, 0.0, 0.0]
26+
- [pt_b, [4.0, 5.0, 6.0], DYNAMIC, nothing, nothing,
27+
1.0, 0.0, 0.0, 0.0, 0.0]
28+
29+
segments:
30+
headers: [name, point_i, point_j, type, l0,
31+
diameter_mm, unit_stiffness, unit_damping,
32+
compression_frac]
33+
data:
34+
- [seg_ab, pt_a, pt_b, POWER_LINE, 10.0,
35+
1.0, 5000.0, 10.0, 1.0]
36+
"""
37+
38+
const SETTINGS_YAML_UPDATE = """
39+
system:
40+
log_file: "data/2plate"
41+
g_earth: 9.81
42+
initial:
43+
l_tethers: [0.0]
44+
v_reel_outs: [0.0]
45+
solver:
46+
solver: "FBDF"
47+
abs_tol: 0.01
48+
rel_tol: 0.01
49+
relaxation: 0.6
50+
kite:
51+
model: ""
52+
foil_file: "ram_air_kite/ram_air_kite_foil.dat"
53+
physical_model: "2plate"
54+
struc_geometry_path: "struc_geometry.yaml"
55+
aero_geometry_path: "aero_geometry.yaml"
56+
mass: 0.0
57+
quasi_static: false
58+
tether:
59+
cd_tether: 0.958
60+
unit_damping: 0.0
61+
unit_stiffness: 0.0
62+
rho_tether: 724.0
63+
e_tether: 5.5e10
64+
winch:
65+
winch_model: "TorqueControlledMachine"
66+
drum_radius: 0.110
67+
gear_ratio: 1.0
68+
inertia_total: 0.024
69+
f_coulomb: 122.0
70+
c_vf: 30.6
71+
environment:
72+
rho_0: 1.225
73+
v_wind: 0.0
74+
upwind_dir: -90.0
75+
profile_law: 0
76+
"""
77+
78+
@testset "update_sys_struct_from_yaml!" begin
79+
tmpdir = mktempdir()
80+
yaml_path = joinpath(tmpdir, "geometry.yaml")
81+
write(yaml_path, YAML_UPDATE_BASE)
82+
83+
settings_path = joinpath(tmpdir, "settings.yaml")
84+
write(settings_path, SETTINGS_YAML_UPDATE)
85+
system_path = joinpath(tmpdir, "system.yaml")
86+
write(system_path, """
87+
system:
88+
sim_settings: settings.yaml
89+
""")
90+
91+
set_data_path(tmpdir)
92+
set = Settings("system.yaml")
93+
94+
sys = load_sys_struct_from_yaml(yaml_path;
95+
system_name="yaml_update_test", set=set)
96+
97+
# --------------------------------------------------------
98+
# Test 1: Unchanged YAML round-trip
99+
# --------------------------------------------------------
100+
@testset "Unchanged round-trip" begin
101+
orig_a = copy(sys.points[:pt_a].pos_cad)
102+
orig_b = copy(sys.points[:pt_b].pos_cad)
103+
orig_l0 = sys.segments[:seg_ab].l0
104+
105+
update_sys_struct_from_yaml!(sys, yaml_path)
106+
107+
@test sys.points[:pt_a].pos_cad == orig_a
108+
@test sys.points[:pt_b].pos_cad == orig_b
109+
@test sys.segments[:seg_ab].l0 == orig_l0
110+
end
111+
112+
# --------------------------------------------------------
113+
# Test 2: Modified point position
114+
# --------------------------------------------------------
115+
@testset "Modified point position" begin
116+
modified_yaml = replace(
117+
YAML_UPDATE_BASE,
118+
"[1.0, 2.0, 3.0]" => "[10.0, 20.0, 30.0]")
119+
mod_path = joinpath(tmpdir, "modified_pos.yaml")
120+
write(mod_path, modified_yaml)
121+
122+
orig_b = copy(sys.points[:pt_b].pos_cad)
123+
124+
update_sys_struct_from_yaml!(sys, mod_path)
125+
126+
@test sys.points[:pt_a].pos_cad ==
127+
KVec3(10.0, 20.0, 30.0)
128+
# pt_b should be unchanged
129+
@test sys.points[:pt_b].pos_cad == orig_b
130+
end
131+
132+
# --------------------------------------------------------
133+
# Test 3: Modified segment l0
134+
# --------------------------------------------------------
135+
@testset "Modified segment l0" begin
136+
modified_yaml = replace(
137+
YAML_UPDATE_BASE,
138+
"10.0,\n 1.0, 5000.0" =>
139+
"42.5,\n 1.0, 5000.0")
140+
mod_path = joinpath(tmpdir, "modified_l0.yaml")
141+
write(mod_path, modified_yaml)
142+
143+
update_sys_struct_from_yaml!(sys, mod_path)
144+
145+
@test sys.segments[:seg_ab].l0 == 42.5
146+
end
147+
148+
# --------------------------------------------------------
149+
# Test 4: l0=nothing auto-calculates from pos_cad
150+
# --------------------------------------------------------
151+
@testset "l0=nothing auto-calc from pos_cad" begin
152+
# First reset to base YAML so pos_cad is known
153+
write(yaml_path, YAML_UPDATE_BASE)
154+
update_sys_struct_from_yaml!(sys, yaml_path)
155+
156+
expected_l0 = norm(
157+
KVec3(1.0, 2.0, 3.0) - KVec3(4.0, 5.0, 6.0))
158+
159+
nothing_yaml = replace(
160+
YAML_UPDATE_BASE,
161+
"10.0,\n 1.0, 5000.0" =>
162+
"nothing,\n 1.0, 5000.0")
163+
nothing_path = joinpath(tmpdir, "nothing_l0.yaml")
164+
write(nothing_path, nothing_yaml)
165+
166+
update_sys_struct_from_yaml!(sys, nothing_path)
167+
168+
@test sys.segments[:seg_ab].l0 expected_l0
169+
end
170+
171+
rm(tmpdir; recursive=true)
172+
end

0 commit comments

Comments
 (0)