diff --git a/CHANGELOG.md b/CHANGELOG.md index ec23fb6b..69a2bb66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Project.toml b/Project.toml index 145a222d..604ccc83 100644 --- a/Project.toml +++ b/Project.toml @@ -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 and contributors"] [deps] diff --git a/docs/src/exported_functions.md b/docs/src/exported_functions.md index aeae3efd..086c5350 100644 --- a/docs/src/exported_functions.md +++ b/docs/src/exported_functions.md @@ -34,6 +34,7 @@ simple_linearize! ```@docs load_sys_struct_from_yaml +update_sys_struct_from_yaml! ``` ## System configuration diff --git a/docs/src/private_functions.md b/docs/src/private_functions.md index d0b65537..6b1f6bef 100644 --- a/docs/src/private_functions.md +++ b/docs/src/private_functions.md @@ -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 diff --git a/src/SymbolicAWEModels.jl b/src/SymbolicAWEModels.jl index 04d91a21..b9c7dde5 100644 --- a/src/SymbolicAWEModels.jl +++ b/src/SymbolicAWEModels.jl @@ -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 diff --git a/src/system_structure/system_structure_core.jl b/src/system_structure/system_structure_core.jl index 25285e69..994d6b07 100644 --- a/src/system_structure/system_structure_core.jl +++ b/src/system_structure/system_structure_core.jl @@ -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 @@ -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 diff --git a/src/system_structure/utilities.jl b/src/system_structure/utilities.jl index 8592db95..797c21ea 100644 --- a/src/system_structure/utilities.jl +++ b/src/system_structure/utilities.jl @@ -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 ==================== # """ @@ -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 diff --git a/src/yaml_loader.jl b/src/yaml_loader.jl index cd76e0e1..542cb78b 100644 --- a/src/yaml_loader.jl +++ b/src/yaml_loader.jl @@ -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) diff --git a/test/test_yaml_update.jl b/test/test_yaml_update.jl new file mode 100644 index 00000000..e77e3bf7 --- /dev/null +++ b/test/test_yaml_update.jl @@ -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