From 34f43c3779fd06a68b988bb7f07f639303811d3a Mon Sep 17 00:00:00 2001 From: Bart Date: Wed, 19 Aug 2026 11:50:27 +0200 Subject: [PATCH] Rotate a twisted section's chord fully about the spanwise axis _apply_refined_section_thetas! built the twisted TE point from cos(theta)*chord - sin(theta)*(chord x local_y), which is the Rodrigues rotation with its axial term k(k.c)(1-cos(theta)) dropped. That term vanishes only when the chord is perpendicular to the twist axis, so a swept or dihedral section had its along-span chord component scaled by cos(theta): the chord shortened and tilted out of the twist plane, both growing as theta^2. On the 2-plate kite of SymbolicAWEModels the chord runs 7.4 deg off the plane normal to the local span, and the deformed VSM chord left the structural chord it is meant to track by 0.065 deg at 7.7 deg of twist and 2.4 deg at 48 deg. Restoring the axial term takes both to 1e-13 deg, and the chord length is now preserved to machine precision. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 8 ++++++++ Project.toml | 2 +- src/wing_geometry.jl | 8 +++++++- test/yaml_geometry/test_yaml_wing_deformation.jl | 16 ++++++++++++++++ 4 files changed, 32 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2c939fe..c06c68a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## VortexStepMethod v4.1.1 2026-08-19 + +### Fixed +- Section twist now rotates the chord fully about the spanwise axis. The axial + Rodrigues term was missing, so a swept or dihedral section's along-span chord + component was scaled by `cos(theta)`: the chord shortened and tilted out of the + twist plane, growing as `theta^2`. + ## VortexStepMethod v4.1.0 2026-08-17 ### Added diff --git a/Project.toml b/Project.toml index 84d898de..b914de71 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "VortexStepMethod" uuid = "ed3cd733-9f0f-46a9-93e0-89b8d4998dd9" authors = ["1-Bart-1 ", "Oriol Cayon and contributors"] -version = "4.1.0" +version = "4.1.1" [workspace] projects = ["examples", "docs", "test"] diff --git a/src/wing_geometry.jl b/src/wing_geometry.jl index be6d5ebd..6abc004c 100644 --- a/src/wing_geometry.jl +++ b/src/wing_geometry.jl @@ -634,6 +634,10 @@ end Rotate each refined section's TE point around its LE point by the given per-section twist angle, using `wing.non_deformed_sections` as the reference geometry. + +The rotation is a full Rodrigues rotation about the spanwise axis. A swept or +dihedral section has a chord component along that axis, and dropping the axial +term would scale it by `cos(theta)`, shortening the chord and tilting it. """ function _apply_refined_section_thetas!(wing::Wing{P, T}, section_thetas) where {P, T} local_y = zeros(MVector{3, T}) @@ -677,8 +681,10 @@ function _apply_refined_section_thetas!(wing::Wing{P, T}, section_thetas) where chord .= section.TE_point .- le normal .= chord × local_y + axial = local_y ⋅ chord @. wing.refined_sections[i].TE_point = le + - cos(theta) * chord - sin(theta) * normal + cos(theta) * chord - sin(theta) * normal + + (1 - cos(theta)) * axial * local_y end return nothing end diff --git a/test/yaml_geometry/test_yaml_wing_deformation.jl b/test/yaml_geometry/test_yaml_wing_deformation.jl index c0d71ff7..124b988b 100644 --- a/test/yaml_geometry/test_yaml_wing_deformation.jl +++ b/test/yaml_geometry/test_yaml_wing_deformation.jl @@ -271,4 +271,20 @@ using Test @test wing.refined_section_weight[1] ≈ 1.0 @test wing.refined_section_weight[end] ≈ 0.0 end + + @testset "Twist rotates a swept chord rigidly" begin + # A swept or dihedral section's chord has a component along the twist + # axis, which only a full rotation carries through unchanged. + wing = Wing(8; n_unrefined_sections=3) + add_section!(wing, [0.0, 5.0, 1.0], [1.0, 4.6, 1.0], INVISCID) + add_section!(wing, [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], INVISCID) + add_section!(wing, [0.0, -5.0, 1.0], [1.0, -4.6, 1.0], INVISCID) + refine!(wing) + chords = [norm(s.TE_point - s.LE_point) for s in wing.refined_sections] + for theta in (0.05, 0.2, 0.5) + VortexStepMethod.unrefined_deform!(wing, fill(theta, 3), nothing) + twisted = [norm(s.TE_point - s.LE_point) for s in wing.refined_sections] + @test maximum(abs.(twisted .- chords) ./ chords) < 1e-12 + end + end end