From f078c77e32118f36476cd565b5513ac031f13f15 Mon Sep 17 00:00:00 2001 From: 1-Bart-1 Date: Sat, 25 Oct 2025 22:36:47 +0200 Subject: [PATCH 01/53] Make sure 0 groups works --- src/ram_geometry.jl | 2 +- src/solver.jl | 53 +++++-- src/wing_geometry.jl | 2 +- src/yaml_geometry.jl | 2 +- test/data/solver/wings/solver_test_wing.yaml | 12 ++ test/solver/test_solver.jl | 148 +++++++++++++++++++ test/yaml_geometry/test_wing_constructor.jl | 7 +- 7 files changed, 206 insertions(+), 20 deletions(-) diff --git a/src/ram_geometry.jl b/src/ram_geometry.jl index e352c4e2..c267d727 100644 --- a/src/ram_geometry.jl +++ b/src/ram_geometry.jl @@ -415,7 +415,7 @@ function RamAirWing( interp_steps=n_sections # TODO: check if interpolations are still needed ) - !(n_panels % n_groups == 0) && throw(ArgumentError("Number of panels should be divisible by number of groups")) + !(n_groups == 0 || n_panels % n_groups == 0) && throw(ArgumentError("Number of panels should be divisible by number of groups")) !isapprox(spanwise_direction, [0.0, 1.0, 0.0]) && throw(ArgumentError("Spanwise direction has to be [0.0, 1.0, 0.0], not $spanwise_direction")) # Load or create polars diff --git a/src/solver.jl b/src/solver.jl index ecf34e3b..8b2d75fd 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -292,21 +292,29 @@ function solve!(solver::Solver, body_aero::BodyAerodynamics, gamma_distribution= moment_coeff_dist[i] = moment_dist[i] / (q_inf * projected_area) end - group_moment_dist = solver.sol.group_moment_dist - group_moment_coeff_dist = solver.sol.group_moment_coeff_dist - group_moment_dist .= 0.0 - group_moment_coeff_dist .= 0.0 - panel_idx = 1 - group_idx = 1 - for wing in body_aero.wings - panels_per_group = wing.n_panels ÷ wing.n_groups - for _ in 1:wing.n_groups - for _ in 1:panels_per_group - group_moment_dist[group_idx] += moment_dist[panel_idx] - group_moment_coeff_dist[group_idx] += moment_coeff_dist[panel_idx] - panel_idx += 1 + # Only compute group moments if there are groups + if length(solver.sol.group_moment_dist) > 0 + group_moment_dist = solver.sol.group_moment_dist + group_moment_coeff_dist = solver.sol.group_moment_coeff_dist + group_moment_dist .= 0.0 + group_moment_coeff_dist .= 0.0 + panel_idx = 1 + group_idx = 1 + for wing in body_aero.wings + if wing.n_groups > 0 + panels_per_group = wing.n_panels ÷ wing.n_groups + for _ in 1:wing.n_groups + for _ in 1:panels_per_group + group_moment_dist[group_idx] += moment_dist[panel_idx] + group_moment_coeff_dist[group_idx] += moment_coeff_dist[panel_idx] + panel_idx += 1 + end + group_idx += 1 + end + else + # Skip panels for wings with n_groups=0 + panel_idx += wing.n_panels end - group_idx += 1 end end @@ -689,8 +697,8 @@ jac, results = linearize( ) ``` """ -function linearize(solver::Solver, body_aero::BodyAerodynamics, y::Vector{T}; - theta_idxs=1:4, +function linearize(solver::Solver, body_aero::BodyAerodynamics, y::Vector{T}; + theta_idxs=1:4, delta_idxs=nothing, va_idxs=nothing, omega_idxs=nothing, @@ -700,6 +708,19 @@ function linearize(solver::Solver, body_aero::BodyAerodynamics, y::Vector{T}; !(length(body_aero.wings) == 1) && throw(ArgumentError("Linearization only works for a body_aero with one wing")) wing = body_aero.wings[1] + # Validate that theta_idxs and delta_idxs match the number of groups + if !isnothing(theta_idxs) && wing.n_groups > 0 + length(theta_idxs) != wing.n_groups && throw(ArgumentError( + "Length of theta_idxs ($(length(theta_idxs))) must match number of groups ($(wing.n_groups))")) + end + if !isnothing(delta_idxs) && wing.n_groups > 0 + length(delta_idxs) != wing.n_groups && throw(ArgumentError( + "Length of delta_idxs ($(length(delta_idxs))) must match number of groups ($(wing.n_groups))")) + end + if wing.n_groups == 0 && (!isnothing(theta_idxs) || !isnothing(delta_idxs)) + throw(ArgumentError("Cannot use theta_idxs or delta_idxs when wing has n_groups=0 (no group functionality)")) + end + init_va = body_aero.cache[1][body_aero.va] init_va .= body_aero.va if !isnothing(theta_idxs) diff --git a/src/wing_geometry.jl b/src/wing_geometry.jl index 506b91b4..bb1f5e06 100644 --- a/src/wing_geometry.jl +++ b/src/wing_geometry.jl @@ -235,7 +235,7 @@ function Wing(n_panels::Int; spanwise_distribution::PanelDistribution=LINEAR, spanwise_direction::PosVector=MVec3([0.0, 1.0, 0.0]), remove_nan=true) - !(n_panels % n_groups == 0) && throw(ArgumentError("Number of panels should be divisible by number of groups")) + !(n_groups == 0 || n_panels % n_groups == 0) && throw(ArgumentError("Number of panels should be divisible by number of groups")) panel_props = PanelProperties{n_panels}() Wing(n_panels, n_groups, spanwise_distribution, panel_props, spanwise_direction, Section[], Section[], remove_nan) end diff --git a/src/yaml_geometry.jl b/src/yaml_geometry.jl index 1abf3118..3ba618f5 100644 --- a/src/yaml_geometry.jl +++ b/src/yaml_geometry.jl @@ -190,7 +190,7 @@ function Wing( remove_nan=true, prn=false ) - !(n_panels % n_groups == 0) && throw(ArgumentError("Number of panels should be divisible by number of groups")) + !(n_groups == 0 || n_panels % n_groups == 0) && throw(ArgumentError("Number of panels should be divisible by number of groups")) !isapprox(spanwise_direction, [0.0, 1.0, 0.0]) && throw(ArgumentError("Spanwise direction has to be [0.0, 1.0, 0.0], not $spanwise_direction")) prn && @info "Reading YAML wing configuration from $geometry_file" diff --git a/test/data/solver/wings/solver_test_wing.yaml b/test/data/solver/wings/solver_test_wing.yaml index e69de29b..14969bce 100644 --- a/test/data/solver/wings/solver_test_wing.yaml +++ b/test/data/solver/wings/solver_test_wing.yaml @@ -0,0 +1,12 @@ +wing_sections: + headers: [airfoil_id, LE_x, LE_y, LE_z, TE_x, TE_y, TE_z] + data: + - [1, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0] + - [1, 0.0, -1.0, 0.0, 1.0, -1.0, 0.0] + +wing_airfoils: + alpha_range: [-10, 10, 1] + reynolds: 1000000 + headers: [airfoil_id, type, info_dict] + data: + - [1, polars, {csv_file_path: ""}] diff --git a/test/solver/test_solver.jl b/test/solver/test_solver.jl index 1d60061a..0dbb9971 100644 --- a/test/solver/test_solver.jl +++ b/test/solver/test_solver.jl @@ -29,4 +29,152 @@ using Test rm(settings_file; force=true) end end + + @testset "Solver with n_groups=0" begin + # Test that solver works correctly when n_groups=0 (no group functionality) + settings_file = create_temp_wing_settings("solver", "solver_test_wing.yaml"; + alpha=5.0, beta=0.0, wind_speed=10.0, n_groups=0) + + try + settings = VSMSettings(settings_file) + wing = Wing(settings) + @test wing.n_groups == 0 + + body_aero = BodyAerodynamics([wing]) + solver = Solver(body_aero, settings) + + # Verify solver has zero groups + @test length(solver.sol.group_moment_dist) == 0 + @test length(solver.sol.group_moment_coeff_dist) == 0 + + # Test that solve! works without errors + va = [10.0, 0.0, 0.0] + set_va!(body_aero, va) + sol = solve!(solver, body_aero) + + @test sol isa VSMSolution + @test sol.solver_status == SUCCESS + + # Verify that group moments are empty + @test length(sol.group_moment_dist) == 0 + @test length(sol.group_moment_coeff_dist) == 0 + + # But force and moment should still be computed + @test !all(sol.force .== 0.0) + @test norm(sol.force) > 0 + + finally + rm(settings_file; force=true) + end + end + + @testset "Linearize with n_groups=0" begin + # Test that linearize works correctly when n_groups=0 + settings_file = create_temp_wing_settings("solver", "solver_test_wing.yaml"; + alpha=5.0, beta=0.0, wind_speed=10.0, n_groups=0, n_panels=4) + + try + settings = VSMSettings(settings_file) + wing = Wing(settings) + @test wing.n_groups == 0 + + body_aero = BodyAerodynamics([wing]) + solver = Solver(body_aero, settings) + + # Set velocity + va = [10.0, 0.0, 0.0] + set_va!(body_aero, va) + + # Test linearize with only velocity (no theta or delta since n_groups=0) + y = va # Only velocity in input vector + jac, results = VortexStepMethod.linearize( + solver, + body_aero, + y; + theta_idxs=nothing, + delta_idxs=nothing, + va_idxs=1:3, + omega_idxs=nothing + ) + + # Results should only have 6 elements (force + moment, no group moments) + @test length(results) == 6 + @test size(jac) == (6, 3) # 6 outputs, 3 inputs (vx, vy, vz) + + # Verify forces are non-zero + @test norm(results[1:3]) > 0 + + # Test that using theta_idxs with n_groups=0 throws an error + @test_throws ArgumentError VortexStepMethod.linearize( + solver, + body_aero, + [0.0, 10.0, 0.0, 0.0]; # Invalid: trying to use theta + theta_idxs=1:1, + va_idxs=2:4 + ) + + # Test that using delta_idxs with n_groups=0 throws an error + @test_throws ArgumentError VortexStepMethod.linearize( + solver, + body_aero, + [0.0, 10.0, 0.0, 0.0]; # Invalid: trying to use delta + delta_idxs=1:1, + va_idxs=2:4 + ) + + finally + rm(settings_file; force=true) + end + end + + @testset "Linearize theta_idxs validation" begin + # Test that theta_idxs length must match n_groups + settings_file = create_temp_wing_settings("solver", "solver_test_wing.yaml"; + alpha=5.0, beta=0.0, wind_speed=10.0, n_groups=2, n_panels=4) + + try + settings = VSMSettings(settings_file) + wing = Wing(settings) + @test wing.n_groups == 2 + + body_aero = BodyAerodynamics([wing]) + solver = Solver(body_aero, settings) + + va = [10.0, 0.0, 0.0] + set_va!(body_aero, va) + + # Test with correct number of theta angles (2) + y_correct = [0.0, 0.0, 10.0, 0.0, 0.0] # 2 theta + 3 va + jac, results = VortexStepMethod.linearize( + solver, + body_aero, + y_correct; + theta_idxs=1:2, + va_idxs=3:5 + ) + @test size(jac, 1) == 8 # 6 + 2 group moments + + # Test with wrong number of theta angles (should throw error) + y_wrong = [0.0, 0.0, 0.0, 0.0, 10.0, 0.0, 0.0] # 4 theta + 3 va + @test_throws ArgumentError VortexStepMethod.linearize( + solver, + body_aero, + y_wrong; + theta_idxs=1:4, # Wrong: 4 angles but only 2 groups + va_idxs=5:7 + ) + + # Test with wrong number of delta angles + @test_throws ArgumentError VortexStepMethod.linearize( + solver, + body_aero, + y_wrong; + delta_idxs=1:4, # Wrong: 4 angles but only 2 groups + va_idxs=5:7 + ) + + finally + rm(settings_file; force=true) + end + end end diff --git a/test/yaml_geometry/test_wing_constructor.jl b/test/yaml_geometry/test_wing_constructor.jl index b7b30a45..19ec9869 100644 --- a/test/yaml_geometry/test_wing_constructor.jl +++ b/test/yaml_geometry/test_wing_constructor.jl @@ -151,7 +151,12 @@ wing_airfoils: # Test invalid n_panels/n_groups combination @test_throws ArgumentError Wing(test_yaml_path; n_panels=5, n_groups=2) - + + # Test n_groups=0 (no grouping functionality) + wing_no_groups = Wing(test_yaml_path; n_panels=4, n_groups=0) + @test wing_no_groups.n_groups == 0 + @test wing_no_groups.n_panels == 4 + # Test invalid spanwise direction @test_throws ArgumentError Wing(test_yaml_path; spanwise_direction=[1.0, 0.0, 0.0]) end From dba02d946901d56eb4e3141ab75554099d15e9e8 Mon Sep 17 00:00:00 2001 From: 1-Bart-1 Date: Sun, 26 Oct 2025 10:45:22 +0100 Subject: [PATCH 02/53] Unify Wing and RamAirWing --- docs/src/types.md | 10 +- examples/bench.jl | 6 +- examples/ram_air_kite.jl | 8 +- src/VortexStepMethod.jl | 4 +- src/body_aerodynamics.jl | 6 +- src/{ram_geometry.jl => obj_geometry.jl} | 239 ++++------------------- src/solver.jl | 8 +- src/wing_geometry.jl | 197 ++++++++++++++++++- test/body_aerodynamics/test_results.jl | 2 +- test/plotting/test_plotting.jl | 2 +- test/ram_geometry/test_kite_geometry.jl | 12 +- test/solver/test_solver.jl | 45 ++++- test/wake/test_wake.jl | 2 +- 13 files changed, 309 insertions(+), 232 deletions(-) rename src/{ram_geometry.jl => obj_geometry.jl} (66%) diff --git a/docs/src/types.md b/docs/src/types.md index 14e5cbdd..de083b35 100644 --- a/docs/src/types.md +++ b/docs/src/types.md @@ -24,17 +24,17 @@ AeroData ``` ## Wing Geometry, Panel and Aerodynamics -A body is constructed of one or more abstract wings. An abstract wing can be a Wing or a RamAirWing. -A Wing/ RamAirWing has one or more sections. +A body is constructed of one or more abstract wings. All wings are of type Wing. +A Wing has one or more sections and can be created from YAML files or OBJ geometry. ```@docs Section Section(LE_point::PosVector, TE_point::PosVector, aero_model) Wing Wing(n_panels::Int; spanwise_distribution::PanelDistribution=LINEAR, spanwise_direction::PosVector=MVec3([0.0, 1.0, 0.0])) -RamAirWing -RamAirWing(obj_path, dat_path; alpha=0.0, crease_frac=0.75, wind_vel=10., mass=1.0, - n_panels=54, n_sections=n_panels+1, spanwise_distribution=UNCHANGED, +ObjWing +ObjWing(obj_path, dat_path; alpha=0.0, crease_frac=0.75, wind_vel=10., mass=1.0, + n_panels=54, n_sections=n_panels+1, spanwise_distribution=UNCHANGED, spanwise_direction=[0.0, 1.0, 0.0]) BodyAerodynamics ``` diff --git a/examples/bench.jl b/examples/bench.jl index 89544278..90c41ff1 100644 --- a/examples/bench.jl +++ b/examples/bench.jl @@ -56,9 +56,9 @@ println("Rectangular wing, solve:") @time solve(vsm_solver, body_aero, nothing) # Create wing geometry -wing = RamAirWing( - joinpath("data", "ram_air_kite", "ram_air_kite_body.obj"), - joinpath("data", "ram_air_kite", "ram_air_kite_foil.dat"); +wing = ObjWing( + joinpath("data", "ram_air_kite", "ram_air_kite_body.obj"), + joinpath("data", "ram_air_kite", "ram_air_kite_foil.dat"); prn=false ) body_aero = BodyAerodynamics([wing]) diff --git a/examples/ram_air_kite.jl b/examples/ram_air_kite.jl index f10b8d5b..13e60c62 100644 --- a/examples/ram_air_kite.jl +++ b/examples/ram_air_kite.jl @@ -9,9 +9,9 @@ DEFORM = false LINEARIZE = false # Create wing geometry -wing = RamAirWing( - joinpath("data", "ram_air_kite", "ram_air_kite_body.obj"), - joinpath("data", "ram_air_kite", "ram_air_kite_foil.dat"); +wing = ObjWing( + joinpath("data", "ram_air_kite", "ram_air_kite_body.obj"), + joinpath("data", "ram_air_kite", "ram_air_kite_foil.dat"); prn=PRN ) body_aero = BodyAerodynamics([wing];) @@ -21,7 +21,7 @@ println("First init") if DEFORM # Linear interpolation of alpha from 10° at one tip to 0° at the other println("Deform") - @time VortexStepMethod.smooth_deform!(wing, deg2rad.([10,20,10,0]), deg2rad.([-10,0,-10,0])) + @time group_deform!(wing, deg2rad.([10,20,10,0]), deg2rad.([-10,0,-10,0]); smooth=true) println("Deform init") @time VortexStepMethod.reinit!(body_aero; init_aero=false) end diff --git a/src/VortexStepMethod.jl b/src/VortexStepMethod.jl index c852c48e..64a4a657 100644 --- a/src/VortexStepMethod.jl +++ b/src/VortexStepMethod.jl @@ -27,7 +27,7 @@ using Xfoil # Export public interface export VSMSettings, WingSettings, SolverSettings -export Wing, Section, RamAirWing, reinit! +export Wing, Section, ObjWing, reinit! export BodyAerodynamics export Solver, solve, solve_base!, solve!, VSMSolution, linearize export calculate_results @@ -272,7 +272,7 @@ end include("settings.jl") include("wing_geometry.jl") include("polars.jl") -include("ram_geometry.jl") +include("obj_geometry.jl") include("yaml_geometry.jl") include("filament.jl") include("panel.jl") diff --git a/src/body_aerodynamics.jl b/src/body_aerodynamics.jl index 87e75fec..6444a598 100644 --- a/src/body_aerodynamics.jl +++ b/src/body_aerodynamics.jl @@ -5,7 +5,7 @@ Main structure for calculating aerodynamic properties of bodies. Use the constru # Fields - panels::Vector{Panel}: Vector of [Panel](@ref) structs -- wings::Union{Vector{Wing}, Vector{RamAirWing}}: A vector of wings; a body can have multiple wings +- wings::Vector{Wing}: A vector of wings; a body can have multiple wings - `va::MVec3` = zeros(MVec3): A vector of the apparent wind speed, see: [MVec3](@ref) - `omega`::MVec3 = zeros(MVec3): A vector of the turn rates around the kite body axes - `gamma_distribution`=zeros(Float64, P): A vector of the circulation @@ -23,7 +23,7 @@ Main structure for calculating aerodynamic properties of bodies. Use the constru """ @with_kw mutable struct BodyAerodynamics{P} panels::Vector{Panel} - wings::Union{Vector{Wing}, Vector{RamAirWing}} + wings::Vector{Wing} _va::MVec3 = zeros(MVec3) omega::MVec3 = zeros(MVec3) gamma_distribution::MVector{P, Float64} = zeros(P) @@ -142,7 +142,7 @@ function reinit!(body_aero::BodyAerodynamics; # Create panels for i in 1:wing.n_panels - if wing isa RamAirWing + if !isnothing(wing.delta_dist) delta = wing.delta_dist[i] else delta = 0.0 diff --git a/src/ram_geometry.jl b/src/obj_geometry.jl similarity index 66% rename from src/ram_geometry.jl rename to src/obj_geometry.jl index c267d727..dec38cb7 100644 --- a/src/ram_geometry.jl +++ b/src/obj_geometry.jl @@ -14,7 +14,7 @@ Read vertices and faces from an OBJ file. function read_faces(filename) vertices = [] faces = [] - + open(filename) do file for line in eachline(file) if startswith(line, "v ") && !startswith(line, "vt") && !startswith(line, "vn") @@ -115,13 +115,13 @@ function create_interpolations(vertices, circle_center_z, radius, gamma_tip, R=I gamma_range = range(-gamma_tip+1e-6, gamma_tip-1e-6, interp_steps) stepsize = gamma_range.step.hi vz_centered = [v[3] - circle_center_z for v in vertices] - + te_gammas = zeros(length(gamma_range)) le_gammas = zeros(length(gamma_range)) trailing_edges = zeros(3, length(gamma_range)) leading_edges = zeros(3, length(gamma_range)) areas = zeros(length(gamma_range)) - + for (j, gamma) in enumerate(gamma_range) trailing_edges[1, j] = -Inf leading_edges[1, j] = Inf @@ -164,7 +164,7 @@ function create_interpolations(vertices, circle_center_z, radius, gamma_tip, R=I te_interp = ntuple(i -> linear_interpolation(le_gammas, trailing_edges[i, :], extrapolation_bc=Line()), 3) area_interp = linear_interpolation(gamma_range, areas, extrapolation_bc=Line()) - + return (le_interp, te_interp, area_interp) end @@ -187,26 +187,26 @@ Calculate center of mass of a mesh and translate vertices so that COM is at orig function center_to_com!(vertices, faces; prn=true) area_total = 0.0 com = zeros(3) - + for face in faces if length(face) == 3 # Triangle case v1 = vertices[face[1]] v2 = vertices[face[2]] v3 = vertices[face[3]] - + # Calculate triangle area and centroid normal = cross(v2 - v1, v3 - v1) area = norm(normal) / 2 centroid = (v1 + v2 + v3) / 3 - + area_total += area com -= area * centroid else throw(ArgumentError("Triangulate faces in a CAD program first")) end end - + com = com / area_total !(abs(com[2]) < 0.01) && throw(ArgumentError("Center of mass $com of .obj file has to lie on the xz-plane.")) prn && @info "Centering vertices of .obj file to the center of mass: $com" @@ -220,7 +220,7 @@ end """ calculate_inertia_tensor(vertices, faces, mass, com) -Calculate the inertia tensor for a triangulated surface mesh, assuming a thin shell with uniform +Calculate the inertia tensor for a triangulated surface mesh, assuming a thin shell with uniform surface density. # Arguments @@ -244,23 +244,23 @@ function calculate_inertia_tensor(vertices, faces, mass, com) # Initialize inertia tensor I = zeros(3, 3) total_area = 0.0 - + for face in faces v1 = vertices[face[1]] .- com v2 = vertices[face[2]] .- com v3 = vertices[face[3]] .- com - + # Calculate triangle area normal = cross(v2 - v1, v3 - v1) area = norm(normal) / 2 total_area += area - + # Calculate contribution to inertia tensor for i in 1:3 for j in 1:3 # Vertices relative to center of mass points = [v1, v2, v3] - + # Calculate contribution to inertia tensor for p in points if i == j @@ -274,7 +274,7 @@ function calculate_inertia_tensor(vertices, faces, mass, com) end end end - + # Scale by mass/total_area to get actual inertia tensor return (mass / total_area) * I / 3 end @@ -291,14 +291,14 @@ function calc_inertia_y_rotation(I_b_tensor) # Transform inertia tensor I_rotated = R_y * I_b_tensor * R_y' # We want the off-diagonal xz elements to be zero - F[1] = I_rotated[1,3] + F[1] = I_rotated[1,3] end - + theta0 = [0.0] prob = NonlinearProblem(eq!, theta0, nothing) sol = NonlinearSolve.solve(prob, NewtonRaphson()) theta_opt = sol.u[1] - + R_b_p = [ cos(theta_opt) 0 sin(theta_opt); 0 1 0; @@ -312,67 +312,18 @@ end """ - RamAirWing <: AbstractWing - -A ram-air wing model that represents a curved parafoil with deformable aerodynamic surfaces. - -## Core Features -- Curved wing geometry derived from 3D mesh (.obj file) -- Aerodynamic properties based on 2D airfoil data (.dat file) -- Support for control inputs (twist angles and trailing edge deflections) -- Inertial and geometric properties calculation - -## Notable Fields -- `n_panels::Int16`: Number of panels in aerodynamic mesh -- `n_groups::Int16`: Number of control groups for distributed deformation -- `mass::Float64`: Total wing mass in kg -- `gamma_tip::Float64`: Angular extent from center to wing tip -- `inertia_tensor::Matrix{Float64}`: Full 3x3 inertia tensor in the kite body frame -- `T_cad_body::MVec3`: Translation vector from CAD frame to body frame -- `R_cad_body::MMat3`: Rotation matrix from CAD frame to body frame -- `radius::Float64`: Wing curvature radius -- `theta_dist::Vector{Float64}`: Panel twist angle distribution -- `delta_dist::Vector{Float64}`: Trailing edge deflection distribution - -See constructor `RamAirWing(obj_path, dat_path; kwargs...)` for usage details. -""" -mutable struct RamAirWing <: AbstractWing - n_panels::Int16 - n_groups::Int16 - spanwise_distribution::PanelDistribution - panel_props::PanelProperties - spanwise_direction::MVec3 - sections::Vector{Section} - refined_sections::Vector{Section} - remove_nan::Bool - - # Additional fields for RamAirWing - non_deformed_sections::Vector{Section} - mass::Float64 - gamma_tip::Float64 - inertia_tensor::Matrix{Float64} - T_cad_body::MVec3 - R_cad_body::MMat3 - radius::Float64 - le_interp::NTuple{3, Extrapolation} - te_interp::NTuple{3, Extrapolation} - area_interp::Extrapolation - theta_dist::Vector{Float64} - delta_dist::Vector{Float64} - cache::Vector{PreallocationTools.LazyBufferCache{typeof(identity), typeof(identity)}} -end + ObjWing(obj_path, dat_path; kwargs...) -""" - RamAirWing(obj_path, dat_path; kwargs...) - -Create a ram-air wing model from 3D geometry and airfoil data files. +Create a deformable wing model from 3D geometry (.obj) and airfoil data (.dat) files. This constructor builds a complete aerodynamic model by: -1. Loading or generating wing geometry from the .obj file -2. Creating aerodynamic polars from the airfoil .dat file +1. Loading wing geometry from the .obj file +2. Creating aerodynamic polars from the airfoil .dat file (or loading existing) 3. Computing inertial properties and coordinate transformations 4. Setting up control surfaces and panel distribution +The resulting Wing supports deformation through group_deform! and deform! functions. + # Arguments - `obj_path`: Path to .obj file containing 3D wing geometry - `dat_path`: Path to .dat file containing 2D airfoil profile @@ -389,32 +340,34 @@ This constructor builds a complete aerodynamic model by: - `remove_nan=true`: Interpolate NaN values in aerodynamic data - `alpha_range=deg2rad.(-5:1:20)`: Angle of attack range for polars (rad) - `delta_range=deg2rad.(-5:1:20)`: Trailing edge deflection range for polars (rad) -- prn=true: if info messages shall be printed +- `prn=true`: Print informational messages # Returns -A fully initialized `RamAirWing` instance ready for aerodynamic simulation. +A fully initialized `Wing` instance ready for aerodynamic simulation with deformation support. # Example ```julia -# Create a ram-air wing from geometry files -wing = RamAirWing( +# Create a deformable wing from geometry files +wing = ObjWing( "path/to/wing.obj", "path/to/airfoil.dat"; mass=1.5, n_panels=40, n_groups=4 ) + +# Apply deformation +group_deform!(wing, deg2rad.([5, 10, 5, 0]), deg2rad.([-5, 0, -5, 0])) ``` """ -function RamAirWing( - obj_path, dat_path; - crease_frac=0.9, wind_vel=10., mass=1.0, - n_panels=56, n_sections=n_panels+1, n_groups=4, spanwise_distribution=UNCHANGED, +function ObjWing( + obj_path, dat_path; + crease_frac=0.9, wind_vel=10., mass=1.0, + n_panels=56, n_sections=n_panels+1, n_groups=4, spanwise_distribution=UNCHANGED, spanwise_direction=[0.0, 1.0, 0.0], remove_nan=true, align_to_principal=false, alpha_range=deg2rad.(-5:1:20), delta_range=deg2rad.(-5:1:20), prn=true, - interp_steps=n_sections # TODO: check if interpolations are still needed + interp_steps=n_sections ) - !(n_groups == 0 || n_panels % n_groups == 0) && throw(ArgumentError("Number of panels should be divisible by number of groups")) !isapprox(spanwise_direction, [0.0, 1.0, 0.0]) && throw(ArgumentError("Spanwise direction has to be [0.0, 1.0, 0.0], not $spanwise_direction")) @@ -436,7 +389,7 @@ function RamAirWing( if align_to_principal inertia_tensor, R_cad_body = calc_inertia_y_rotation(inertia_tensor) else - R_cad_body = I(3) + R_cad_body = Matrix{Float64}(I, 3, 3) end circle_center_z, radius, gamma_tip = find_circle_center_and_radius(vertices) le_interp, te_interp, area_interp = create_interpolations(vertices, circle_center_z, radius, gamma_tip, R_cad_body; interp_steps) @@ -446,7 +399,7 @@ function RamAirWing( if !ispath(cl_polar_path) || !ispath(cd_polar_path) || !ispath(cm_polar_path) width = 2gamma_tip * radius area = area_interp(gamma_tip) - create_polars(; dat_path, cl_polar_path, cd_polar_path, cm_polar_path, wind_vel, + create_polars(; dat_path, cl_polar_path, cd_polar_path, cm_polar_path, wind_vel, area, width, crease_frac, alpha_range, delta_range, remove_nan) end @@ -459,7 +412,7 @@ function RamAirWing( any(isnan.(cd_matrix)) && interpolate_matrix_nans!(cd_matrix; prn) any(isnan.(cm_matrix)) && interpolate_matrix_nans!(cm_matrix; prn) end - + # Create sections sections = Section[] refined_sections = Section[] @@ -474,12 +427,13 @@ function RamAirWing( end panel_props = PanelProperties{n_panels}() - cache = [LazyBufferCache()] + cache = [PreallocationTools.LazyBufferCache()] - RamAirWing(n_panels, n_groups, spanwise_distribution, panel_props, spanwise_direction, sections, - refined_sections, remove_nan, non_deformed_sections, + Wing(n_panels, n_groups, spanwise_distribution, panel_props, MVec3(spanwise_direction), + sections, refined_sections, remove_nan, + non_deformed_sections, zeros(n_panels), zeros(n_panels), mass, gamma_tip, inertia_tensor, T_cad_body, R_cad_body, radius, - le_interp, te_interp, area_interp, zeros(n_panels), zeros(n_panels), cache) + le_interp, te_interp, area_interp, cache) catch e if e isa BoundsError @@ -488,110 +442,3 @@ function RamAirWing( rethrow(e) end end - -""" - group_deform!(wing::RamAirWing, theta_angles::AbstractVector, delta_angles::AbstractVector) - -Distribute control angles across wing panels and apply smoothing using a moving average filter. - -# Arguments -- `wing::RamAirWing`: The wing to deform -- `theta_angles::AbstractVector`: Twist angles in radians for each control section -- `delta_angles::AbstractVector`: Trailing edge deflection angles in radians for each control section -- `smooth::Bool`: Wether to apply smoothing or not - -# Algorithm -1. Distributes each control input to its corresponding group of panels -2. Applies moving average smoothing with window size based on control group size - -# Errors -- Throws `ArgumentError` if panel count is not divisible by the number of control inputs - -# Returns -- `nothing` (modifies wing in-place) -""" -function group_deform!(wing::RamAirWing, theta_angles=nothing, delta_angles=nothing; smooth=false) - !isnothing(theta_angles) && !(wing.n_panels % length(theta_angles) == 0) && - throw(ArgumentError("Number of angles has to be a multiple of number of panels")) - !isnothing(delta_angles) && !(wing.n_panels % length(delta_angles) == 0) && - throw(ArgumentError("Number of angles has to be a multiple of number of panels")) - isnothing(theta_angles) && isnothing(delta_angles) && return nothing - - n_panels = wing.n_panels - theta_dist = wing.theta_dist - delta_dist = wing.delta_dist - n_angles = isnothing(theta_angles) ? length(delta_angles) : length(theta_angles) - - dist_idx = 0 - for angle_idx in 1:n_angles - for _ in 1:(wing.n_panels ÷ n_angles) - dist_idx += 1 - !isnothing(theta_angles) && (theta_dist[dist_idx] = theta_angles[angle_idx]) - !isnothing(delta_angles) && (delta_dist[dist_idx] = delta_angles[angle_idx]) - end - end - @assert (dist_idx == wing.n_panels) - - if smooth - window_size = wing.n_panels ÷ n_angles - if n_panels > window_size - smoothed = wing.cache[1][theta_dist] - - if !isnothing(theta_angles) - smoothed .= theta_dist - for i in (window_size÷2 + 1):(n_panels - window_size÷2) - @views smoothed[i] = mean(theta_dist[(i - window_size÷2):(i + window_size÷2)]) - end - theta_dist .= smoothed - end - - if !isnothing(delta_angles) - smoothed .= delta_dist - for i in (window_size÷2 + 1):(n_panels - window_size÷2) - @views smoothed[i] = mean(delta_dist[(i - window_size÷2):(i + window_size÷2)]) - end - delta_dist .= smoothed - end - end - end - deform!(wing) - return nothing -end - -""" - deform!(wing::RamAirWing, theta_dist::AbstractVector, delta_dist::AbstractVector; width) - -Deform wing by applying theta and delta distributions. - -# Arguments -- `wing`: RamAirWing to deform -- `theta_dist`: the angle distribution between of the kite and the body x-axis in radians of each panel -- `delta_dist`: the deformation of the trailing edges of each panel - -# Effects -Updates wing.sections with deformed geometry -""" -function deform!(wing::RamAirWing, theta_dist::AbstractVector, delta_dist::AbstractVector) - !(length(theta_dist) == wing.n_panels) && throw(ArgumentError("theta_dist and panels are of different lengths")) - !(length(delta_dist) == wing.n_panels) && throw(ArgumentError("delta_dist and panels are of different lengths")) - wing.theta_dist .= theta_dist - wing.delta_dist .= delta_dist - - deform!(wing) -end - -function deform!(wing::RamAirWing) - local_y = zeros(MVec3) - chord = zeros(MVec3) - normal = zeros(MVec3) - - for i in 1:wing.n_panels - section1 = wing.non_deformed_sections[i] - section2 = wing.non_deformed_sections[i+1] - local_y .= normalize(section1.LE_point - section2.LE_point) - chord .= section1.TE_point .- section1.LE_point - normal .= chord × local_y - @. wing.sections[i].TE_point = section1.LE_point + cos(wing.theta_dist[i]) * chord - sin(wing.theta_dist[i]) * normal - end - return nothing -end diff --git a/src/solver.jl b/src/solver.jl index 8b2d75fd..9bce66ae 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -654,15 +654,15 @@ function smooth_circulation!( end """ - linearize(solver::Solver, body_aero::BodyAerodynamics, wing::RamAirWing, y::Vector{T}; + linearize(solver::Solver, body_aero::BodyAerodynamics, wing::Wing, y::Vector{T}; theta_idxs=1:4, delta_idxs=nothing, va_idxs=nothing, omega_idxs=nothing, kwargs...) where T -Compute the Jacobian matrix for a ram air wing around an operating point using finite differences. +Compute the Jacobian matrix for a deformable wing around an operating point using finite differences. # Arguments - `solver`: VSM solver instance (must be initialized) - `body_aero`: Aerodynamic body representation -- `wing`: RamAirWing model to linearize +- `wing`: Wing model to linearize (must support deformation, i.e., created with ObjWing()) - `y`: Input vector at operating point, containing a combination of control angles and velocities # Keyword Arguments @@ -679,7 +679,7 @@ Compute the Jacobian matrix for a ram air wing around an operating point using f # Example ```julia # Initialize wing and solver -wing = RamAirWing("path/to/body.obj", "path/to/foil.dat") +wing = ObjWing("path/to/body.obj", "path/to/foil.dat") body_aero = BodyAerodynamics([wing]) solver = Solver(body_aero) diff --git a/src/wing_geometry.jl b/src/wing_geometry.jl index bb1f5e06..02f7a308 100644 --- a/src/wing_geometry.jl +++ b/src/wing_geometry.jl @@ -192,7 +192,7 @@ end Represents a wing composed of multiple sections with aerodynamic properties. -# Fields +# Core Fields (all wings) - `n_panels::Int16`: Number of panels in aerodynamic mesh - `n_groups::Int16`: Number of panel groups - `spanwise_distribution`::PanelDistribution: [PanelDistribution](@ref) @@ -201,6 +201,23 @@ Represents a wing composed of multiple sections with aerodynamic properties. - `refined_sections::Vector{Section}`: Vector of refined wing sections, see: [Section](@ref) - `remove_nan::Bool`: Wether to remove the NaNs from interpolations or not +# Deformation Fields (optional, for deformable wings) +- `non_deformed_sections::Vector{Section}`: Original undeformed sections +- `theta_dist::Vector{Float64}`: Panel twist angle distribution +- `delta_dist::Vector{Float64}`: Trailing edge deflection distribution + +# Physical Properties (optional, for OBJ-based wings) +- `mass::Float64`: Total wing mass in kg (0.0 if not applicable) +- `gamma_tip::Float64`: Angular extent from center to wing tip (0.0 if not applicable) +- `inertia_tensor::Matrix{Float64}`: 3x3 inertia tensor (empty if not applicable) +- `T_cad_body::MVec3`: Translation from CAD to body frame (zeros if not applicable) +- `R_cad_body::MMat3`: Rotation from CAD to body frame (identity if not applicable) +- `radius::Float64`: Wing curvature radius (0.0 if not applicable) +- `le_interp::Union{Nothing, NTuple{3, Extrapolation}}`: Leading edge interpolation +- `te_interp::Union{Nothing, NTuple{3, Extrapolation}}`: Trailing edge interpolation +- `area_interp::Union{Nothing, Extrapolation}`: Area interpolation +- `cache::Vector{PreallocationTools.LazyBufferCache{typeof(identity), typeof(identity)}}`: Preallocated buffers + """ mutable struct Wing <: AbstractWing n_panels::Int16 @@ -211,6 +228,23 @@ mutable struct Wing <: AbstractWing sections::Vector{Section} refined_sections::Vector{Section} remove_nan::Bool + + # Deformation fields + non_deformed_sections::Vector{Section} + theta_dist::Vector{Float64} + delta_dist::Vector{Float64} + + # Physical properties (OBJ-based wings) + mass::Float64 + gamma_tip::Float64 + inertia_tensor::Matrix{Float64} + T_cad_body::MVec3 + R_cad_body::MMat3 + radius::Float64 + le_interp::Union{Nothing, NTuple{3, Extrapolation}} + te_interp::Union{Nothing, NTuple{3, Extrapolation}} + area_interp::Union{Nothing, Extrapolation} + cache::Vector{PreallocationTools.LazyBufferCache{typeof(identity), typeof(identity)}} end """ @@ -220,8 +254,8 @@ end spanwise_direction::PosVector=MVec3([0.0, 1.0, 0.0]), remove_nan::Bool=true) -Constructor for a [Wing](@ref) struct with default values that initializes the sections -and refined sections as empty arrays. +Constructor for a [Wing](@ref) struct with default values that initializes the sections +and refined sections as empty arrays. Creates a basic wing suitable for YAML-based construction. # Parameters - `n_panels::Int`: Number of panels in aerodynamic mesh @@ -237,12 +271,23 @@ function Wing(n_panels::Int; remove_nan=true) !(n_groups == 0 || n_panels % n_groups == 0) && throw(ArgumentError("Number of panels should be divisible by number of groups")) panel_props = PanelProperties{n_panels}() - Wing(n_panels, n_groups, spanwise_distribution, panel_props, spanwise_direction, Section[], Section[], remove_nan) + + # Initialize with default/empty values for optional fields + Wing( + n_panels, n_groups, spanwise_distribution, panel_props, spanwise_direction, + Section[], Section[], remove_nan, + # Deformation fields + Section[], zeros(n_panels), zeros(n_panels), + # Physical properties (defaults for non-OBJ wings) + 0.0, 0.0, zeros(0, 0), zeros(MVec3), Matrix{Float64}(I, 3, 3), + 0.0, nothing, nothing, nothing, + PreallocationTools.LazyBufferCache{typeof(identity), typeof(identity)}[] + ) end function reinit!(wing::AbstractWing) refine_aerodynamic_mesh!(wing) - + # Calculate panel properties update_panel_properties!( wing.panel_props, @@ -252,6 +297,148 @@ function reinit!(wing::AbstractWing) return nothing end +""" + group_deform!(wing::Wing, theta_angles=nothing, delta_angles=nothing; smooth=false) + +Distribute control angles across wing panels and optionally apply smoothing. + +For wings that support deformation (OBJ-based wings with non_deformed_sections), this +distributes theta_angles and delta_angles to panel groups and applies deformation. +For wings without deformation support (YAML-based), this is a no-op that only succeeds +if both angle inputs are nothing. + +# Arguments +- `wing::Wing`: The wing to deform +- `theta_angles::AbstractVector`: Twist angles in radians for each control group (or nothing) +- `delta_angles::AbstractVector`: Trailing edge deflection angles in radians (or nothing) +- `smooth::Bool`: Whether to apply moving average smoothing + +# Algorithm +1. Distributes each control input to its corresponding group of panels +2. Optionally applies moving average smoothing with window based on group size +3. Calls deform! to update wing geometry + +# Errors +- Throws `ArgumentError` if wing doesn't support deformation but angles are provided +- Throws `ArgumentError` if panel count is not divisible by number of control inputs + +# Returns +- `nothing` (modifies wing in-place) +""" +function group_deform!(wing::Wing, theta_angles=nothing, delta_angles=nothing; smooth=false) + # Check if deformation is supported + can_deform = !isempty(wing.non_deformed_sections) + + # If no deformation requested, just return + isnothing(theta_angles) && isnothing(delta_angles) && return nothing + + # If deformation requested but not supported, throw error + if !can_deform + throw(ArgumentError("This Wing does not support deformation. Only OBJ-based wings created with ObjWing() can be deformed.")) + end + + # Validate inputs + !isnothing(theta_angles) && !(wing.n_panels % length(theta_angles) == 0) && + throw(ArgumentError("Number of theta_angles has to be a divisor of number of panels")) + !isnothing(delta_angles) && !(wing.n_panels % length(delta_angles) == 0) && + throw(ArgumentError("Number of delta_angles has to be a divisor of number of panels")) + + n_panels = wing.n_panels + theta_dist = wing.theta_dist + delta_dist = wing.delta_dist + n_angles = isnothing(theta_angles) ? length(delta_angles) : length(theta_angles) + + # Distribute angles to panels + dist_idx = 0 + for angle_idx in 1:n_angles + for _ in 1:(wing.n_panels ÷ n_angles) + dist_idx += 1 + !isnothing(theta_angles) && (theta_dist[dist_idx] = theta_angles[angle_idx]) + !isnothing(delta_angles) && (delta_dist[dist_idx] = delta_angles[angle_idx]) + end + end + @assert (dist_idx == wing.n_panels) + + # Apply smoothing if requested + if smooth + window_size = wing.n_panels ÷ n_angles + if n_panels > window_size + smoothed = wing.cache[1][theta_dist] + + if !isnothing(theta_angles) + smoothed .= theta_dist + for i in (window_size÷2 + 1):(n_panels - window_size÷2) + @views smoothed[i] = mean(theta_dist[(i - window_size÷2):(i + window_size÷2)]) + end + theta_dist .= smoothed + end + + if !isnothing(delta_angles) + smoothed .= delta_dist + for i in (window_size÷2 + 1):(n_panels - window_size÷2) + @views smoothed[i] = mean(delta_dist[(i - window_size÷2):(i + window_size÷2)]) + end + delta_dist .= smoothed + end + end + end + + deform!(wing) + return nothing +end + +""" + deform!(wing::Wing, theta_dist::AbstractVector, delta_dist::AbstractVector) + +Deform wing by applying theta and delta distributions directly. + +# Arguments +- `wing::Wing`: Wing to deform (must support deformation) +- `theta_dist::AbstractVector`: Twist angle in radians for each panel +- `delta_dist::AbstractVector`: Trailing edge deflection for each panel + +# Effects +Updates wing.sections with deformed geometry based on wing.non_deformed_sections +""" +function deform!(wing::Wing, theta_dist::AbstractVector, delta_dist::AbstractVector) + !isempty(wing.non_deformed_sections) || throw(ArgumentError("Wing does not support deformation")) + !(length(theta_dist) == wing.n_panels) && throw(ArgumentError("theta_dist and panels are of different lengths")) + !(length(delta_dist) == wing.n_panels) && throw(ArgumentError("delta_dist and panels are of different lengths")) + wing.theta_dist .= theta_dist + wing.delta_dist .= delta_dist + + deform!(wing) +end + +""" + deform!(wing::Wing) + +Apply stored theta_dist and delta_dist to deform the wing geometry. + +# Arguments +- `wing::Wing`: Wing to deform (must have non_deformed_sections) + +# Effects +Updates wing.sections based on wing.non_deformed_sections and stored distributions +""" +function deform!(wing::Wing) + !isempty(wing.non_deformed_sections) || return nothing + + local_y = zeros(MVec3) + chord = zeros(MVec3) + normal = zeros(MVec3) + + for i in 1:wing.n_panels + section1 = wing.non_deformed_sections[i] + section2 = wing.non_deformed_sections[i+1] + local_y .= normalize(section1.LE_point - section2.LE_point) + chord .= section1.TE_point .- section1.LE_point + normal .= chord × local_y + @. wing.sections[i].TE_point = section1.LE_point + cos(wing.theta_dist[i]) * chord - sin(wing.theta_dist[i]) * normal + end + return nothing +end + """ remove_vector_nans(aero_data) diff --git a/test/body_aerodynamics/test_results.jl b/test/body_aerodynamics/test_results.jl index 2b47dc5b..60d2d4cf 100644 --- a/test/body_aerodynamics/test_results.jl +++ b/test/body_aerodynamics/test_results.jl @@ -31,7 +31,7 @@ if !@isdefined ram_wing_results error("Required data files not found: $body_src or $foil_src") end - ram_wing = RamAirWing(body_path, foil_path; alpha_range=deg2rad.(-1:1), delta_range=deg2rad.(-1:1)) + ram_wing = ObjWing(body_path, foil_path; alpha_range=deg2rad.(-1:1), delta_range=deg2rad.(-1:1)) end @testset "Nonlinear vs Linear - Comprehensive Input Testing" begin diff --git a/test/plotting/test_plotting.jl b/test/plotting/test_plotting.jl index 385abcef..e1f82959 100644 --- a/test/plotting/test_plotting.jl +++ b/test/plotting/test_plotting.jl @@ -29,7 +29,7 @@ if !@isdefined ram_wing foil_src = joinpath(_ram_data_dir, "ram_air_kite_foil.dat") cp(body_src, body_path; force=true) cp(foil_src, foil_path; force=true) - ram_wing = RamAirWing(body_path, foil_path; alpha_range=deg2rad.(-1:1), delta_range=deg2rad.(-1:1)) + ram_wing = ObjWing(body_path, foil_path; alpha_range=deg2rad.(-1:1), delta_range=deg2rad.(-1:1)) end function create_body_aero() diff --git a/test/ram_geometry/test_kite_geometry.jl b/test/ram_geometry/test_kite_geometry.jl index 74b6fd9c..47dee1d4 100644 --- a/test/ram_geometry/test_kite_geometry.jl +++ b/test/ram_geometry/test_kite_geometry.jl @@ -165,9 +165,9 @@ using Serialization @test R_b_p2 ≈ I(3) end - @testset "RamAirWing Construction" begin - wing = RamAirWing(test_obj_path, test_dat_path; remove_nan=true) - + @testset "ObjWing Construction" begin + wing = ObjWing(test_obj_path, test_dat_path; remove_nan=true) + @test wing.n_panels == 56 # Default value @test wing.spanwise_distribution == UNCHANGED @test wing.spanwise_direction ≈ [0.0, 1.0, 0.0] @@ -179,15 +179,15 @@ using Serialization @test !isnan(wing.sections[1].aero_data[4][end]) @test !isnan(wing.sections[1].aero_data[5][end]) - wing = RamAirWing(test_obj_path, test_dat_path; remove_nan=false) + wing = ObjWing(test_obj_path, test_dat_path; remove_nan=false) @test isnan(wing.sections[1].aero_data[3][end]) @test isnan(wing.sections[1].aero_data[4][end]) @test isnan(wing.sections[1].aero_data[5][end]) end @testset "Wing Deformation" begin - # Create a RamAirWing for testing - wing = RamAirWing(test_obj_path, test_dat_path; remove_nan=true) + # Create an ObjWing for testing + wing = ObjWing(test_obj_path, test_dat_path; remove_nan=true) body_aero = BodyAerodynamics([wing]) # Store original TE point for comparison diff --git a/test/solver/test_solver.jl b/test/solver/test_solver.jl index 0dbb9971..084be994 100644 --- a/test/solver/test_solver.jl +++ b/test/solver/test_solver.jl @@ -53,7 +53,7 @@ using Test sol = solve!(solver, body_aero) @test sol isa VSMSolution - @test sol.solver_status == SUCCESS + @test sol.solver_status == FEASIBLE # Verify that group moments are empty @test length(sol.group_moment_dist) == 0 @@ -177,4 +177,47 @@ using Test rm(settings_file; force=true) end end + + @testset "Wing type cannot deform" begin + # Test that regular Wing type (YAML-based) cannot use group_deform! + settings_file = create_temp_wing_settings("solver", "solver_test_wing.yaml"; + alpha=5.0, beta=0.0, wind_speed=10.0, n_groups=2, n_panels=4) + + try + settings = VSMSettings(settings_file) + wing = Wing(settings) + @test wing isa Wing + @test wing.n_groups == 2 + + body_aero = BodyAerodynamics([wing]) + solver = Solver(body_aero, settings) + + va = [10.0, 0.0, 0.0] + set_va!(body_aero, va) + + # Test that trying to use theta angles with Wing throws an error + y = [0.0, 0.0, 10.0, 0.0, 0.0] # 2 theta + 3 va + @test_throws ArgumentError VortexStepMethod.linearize( + solver, + body_aero, + y; + theta_idxs=1:2, + va_idxs=3:5 + ) + + # But linearize should work fine with only velocity (no deformation) + y_velocity_only = [10.0, 0.0, 0.0] + jac, results = VortexStepMethod.linearize( + solver, + body_aero, + y_velocity_only; + theta_idxs=nothing, + va_idxs=1:3 + ) + @test size(jac, 1) == 8 # 6 + 2 group moments + + finally + rm(settings_file; force=true) + end + end end diff --git a/test/wake/test_wake.jl b/test/wake/test_wake.jl index 156e4ec9..b1c45f87 100644 --- a/test/wake/test_wake.jl +++ b/test/wake/test_wake.jl @@ -20,7 +20,7 @@ using VortexStepMethod try # Create wing and body aerodynamics with known good geometry - wing = RamAirWing(body_path, foil_path; n_panels=56) # Use default panels + wing = ObjWing(body_path, foil_path; n_panels=56) # Use default panels body_aero = BodyAerodynamics([wing]) # Test that frozen_wake! doesn't throw errors From 15811702ff937442d915c86bbe5b24e27a34b9c4 Mon Sep 17 00:00:00 2001 From: 1-Bart-1 Date: Sun, 26 Oct 2025 13:52:56 +0100 Subject: [PATCH 03/53] Restore test --- test/solver/test_solver.jl | 191 ------------------------------------- 1 file changed, 191 deletions(-) diff --git a/test/solver/test_solver.jl b/test/solver/test_solver.jl index 084be994..1d60061a 100644 --- a/test/solver/test_solver.jl +++ b/test/solver/test_solver.jl @@ -29,195 +29,4 @@ using Test rm(settings_file; force=true) end end - - @testset "Solver with n_groups=0" begin - # Test that solver works correctly when n_groups=0 (no group functionality) - settings_file = create_temp_wing_settings("solver", "solver_test_wing.yaml"; - alpha=5.0, beta=0.0, wind_speed=10.0, n_groups=0) - - try - settings = VSMSettings(settings_file) - wing = Wing(settings) - @test wing.n_groups == 0 - - body_aero = BodyAerodynamics([wing]) - solver = Solver(body_aero, settings) - - # Verify solver has zero groups - @test length(solver.sol.group_moment_dist) == 0 - @test length(solver.sol.group_moment_coeff_dist) == 0 - - # Test that solve! works without errors - va = [10.0, 0.0, 0.0] - set_va!(body_aero, va) - sol = solve!(solver, body_aero) - - @test sol isa VSMSolution - @test sol.solver_status == FEASIBLE - - # Verify that group moments are empty - @test length(sol.group_moment_dist) == 0 - @test length(sol.group_moment_coeff_dist) == 0 - - # But force and moment should still be computed - @test !all(sol.force .== 0.0) - @test norm(sol.force) > 0 - - finally - rm(settings_file; force=true) - end - end - - @testset "Linearize with n_groups=0" begin - # Test that linearize works correctly when n_groups=0 - settings_file = create_temp_wing_settings("solver", "solver_test_wing.yaml"; - alpha=5.0, beta=0.0, wind_speed=10.0, n_groups=0, n_panels=4) - - try - settings = VSMSettings(settings_file) - wing = Wing(settings) - @test wing.n_groups == 0 - - body_aero = BodyAerodynamics([wing]) - solver = Solver(body_aero, settings) - - # Set velocity - va = [10.0, 0.0, 0.0] - set_va!(body_aero, va) - - # Test linearize with only velocity (no theta or delta since n_groups=0) - y = va # Only velocity in input vector - jac, results = VortexStepMethod.linearize( - solver, - body_aero, - y; - theta_idxs=nothing, - delta_idxs=nothing, - va_idxs=1:3, - omega_idxs=nothing - ) - - # Results should only have 6 elements (force + moment, no group moments) - @test length(results) == 6 - @test size(jac) == (6, 3) # 6 outputs, 3 inputs (vx, vy, vz) - - # Verify forces are non-zero - @test norm(results[1:3]) > 0 - - # Test that using theta_idxs with n_groups=0 throws an error - @test_throws ArgumentError VortexStepMethod.linearize( - solver, - body_aero, - [0.0, 10.0, 0.0, 0.0]; # Invalid: trying to use theta - theta_idxs=1:1, - va_idxs=2:4 - ) - - # Test that using delta_idxs with n_groups=0 throws an error - @test_throws ArgumentError VortexStepMethod.linearize( - solver, - body_aero, - [0.0, 10.0, 0.0, 0.0]; # Invalid: trying to use delta - delta_idxs=1:1, - va_idxs=2:4 - ) - - finally - rm(settings_file; force=true) - end - end - - @testset "Linearize theta_idxs validation" begin - # Test that theta_idxs length must match n_groups - settings_file = create_temp_wing_settings("solver", "solver_test_wing.yaml"; - alpha=5.0, beta=0.0, wind_speed=10.0, n_groups=2, n_panels=4) - - try - settings = VSMSettings(settings_file) - wing = Wing(settings) - @test wing.n_groups == 2 - - body_aero = BodyAerodynamics([wing]) - solver = Solver(body_aero, settings) - - va = [10.0, 0.0, 0.0] - set_va!(body_aero, va) - - # Test with correct number of theta angles (2) - y_correct = [0.0, 0.0, 10.0, 0.0, 0.0] # 2 theta + 3 va - jac, results = VortexStepMethod.linearize( - solver, - body_aero, - y_correct; - theta_idxs=1:2, - va_idxs=3:5 - ) - @test size(jac, 1) == 8 # 6 + 2 group moments - - # Test with wrong number of theta angles (should throw error) - y_wrong = [0.0, 0.0, 0.0, 0.0, 10.0, 0.0, 0.0] # 4 theta + 3 va - @test_throws ArgumentError VortexStepMethod.linearize( - solver, - body_aero, - y_wrong; - theta_idxs=1:4, # Wrong: 4 angles but only 2 groups - va_idxs=5:7 - ) - - # Test with wrong number of delta angles - @test_throws ArgumentError VortexStepMethod.linearize( - solver, - body_aero, - y_wrong; - delta_idxs=1:4, # Wrong: 4 angles but only 2 groups - va_idxs=5:7 - ) - - finally - rm(settings_file; force=true) - end - end - - @testset "Wing type cannot deform" begin - # Test that regular Wing type (YAML-based) cannot use group_deform! - settings_file = create_temp_wing_settings("solver", "solver_test_wing.yaml"; - alpha=5.0, beta=0.0, wind_speed=10.0, n_groups=2, n_panels=4) - - try - settings = VSMSettings(settings_file) - wing = Wing(settings) - @test wing isa Wing - @test wing.n_groups == 2 - - body_aero = BodyAerodynamics([wing]) - solver = Solver(body_aero, settings) - - va = [10.0, 0.0, 0.0] - set_va!(body_aero, va) - - # Test that trying to use theta angles with Wing throws an error - y = [0.0, 0.0, 10.0, 0.0, 0.0] # 2 theta + 3 va - @test_throws ArgumentError VortexStepMethod.linearize( - solver, - body_aero, - y; - theta_idxs=1:2, - va_idxs=3:5 - ) - - # But linearize should work fine with only velocity (no deformation) - y_velocity_only = [10.0, 0.0, 0.0] - jac, results = VortexStepMethod.linearize( - solver, - body_aero, - y_velocity_only; - theta_idxs=nothing, - va_idxs=1:3 - ) - @test size(jac, 1) == 8 # 6 + 2 group moments - - finally - rm(settings_file; force=true) - end - end end From df97a2187674f727b2cbb82c1b851baa97a98f23 Mon Sep 17 00:00:00 2001 From: 1-Bart-1 Date: Wed, 29 Oct 2025 14:52:52 +0100 Subject: [PATCH 04/53] Not passing bench --- src/body_aerodynamics.jl | 72 ++++++++++++++++++++++++++-------------- 1 file changed, 47 insertions(+), 25 deletions(-) diff --git a/src/body_aerodynamics.jl b/src/body_aerodynamics.jl index 6444a598..9e09f54c 100644 --- a/src/body_aerodynamics.jl +++ b/src/body_aerodynamics.jl @@ -129,13 +129,13 @@ Initialize a BodyAerodynamics struct in-place by setting up panels and coefficie # Returns nothing """ -function reinit!(body_aero::BodyAerodynamics; +function reinit!(body_aero::BodyAerodynamics; init_aero=true, va=[15.0, 0.0, 0.0], omega=zeros(MVec3) ) idx = 1 - vec = zeros(MVec3) + vec = @MVector zeros(3) for wing in body_aero.wings reinit!(wing) panel_props = wing.panel_props @@ -168,8 +168,8 @@ function reinit!(body_aero::BodyAerodynamics; end # Initialize rest of the struct - body_aero.projected_area = sum(wing -> calculate_projected_area(wing), body_aero.wings) - body_aero.stall_angle_list .= calculate_stall_angle_list(body_aero.panels) + body_aero.projected_area = sum(calculate_projected_area, body_aero.wings) + calculate_stall_angle_list!(body_aero.stall_angle_list, body_aero.panels) body_aero.alpha_array .= 0.0 body_aero.v_a_array .= 0.0 body_aero.AIC .= 0.0 @@ -191,21 +191,28 @@ Returns: nothing """ @inline function calculate_AIC_matrices!(body_aero::BodyAerodynamics, model::Model, core_radius_fraction, - va_norm_array, + va_norm_array, va_unit_array) # Determine evaluation point based on model evaluation_point = model == VSM ? :control_point : :aero_center evaluation_point_on_bound = model == LLT - - # Initialize AIC matrices - velocity_induced, tempvel, va_unit, U_2D = zeros(MVec3), zeros(MVec3), zeros(MVec3), zeros(MVec3) + + # Allocate work vectors for this function (separate from those used by child functions) + velocity_induced = @MVector zeros(3) + tempvel = @MVector zeros(3) + va_unit = @MVector zeros(3) + U_2D = @MVector zeros(3) # Calculate influence coefficients for icp in eachindex(body_aero.panels) - ep = getproperty(body_aero.panels[icp], evaluation_point) + panel_icp = body_aero.panels[icp] + ep = evaluation_point == :control_point ? panel_icp.control_point : panel_icp.aero_center for jring in eachindex(body_aero.panels) - va_unit .= @views va_unit_array[jring, :] - filaments = body_aero.panels[jring].filaments + panel_jring = body_aero.panels[jring] + @inbounds for k in 1:3 + va_unit[k] = va_unit_array[jring, k] + end + filaments = panel_jring.filaments va_norm = va_norm_array[jring] calculate_velocity_induced_single_ring_semiinfinite!( velocity_induced, @@ -222,8 +229,8 @@ Returns: nothing # Subtract 2D induced velocity for VSM if icp == jring && model == VSM - calculate_velocity_induced_bound_2D!(U_2D, body_aero.panels[jring], ep, body_aero.work_vectors) - velocity_induced .-= U_2D + calculate_velocity_induced_bound_2D!(U_2D, panel_jring, ep, body_aero.work_vectors) + velocity_induced .-= U_2D end body_aero.AIC[:, icp, jring] .= velocity_induced end @@ -276,19 +283,34 @@ function calculate_stall_angle_list(panels::Vector{Panel}; step_aoa=1.0, stall_angle_if_none_detected=50.0, cl_initial=-10.0) - - aoa_range = deg2rad.(range(begin_aoa, end_aoa, step=step_aoa)) - stall_angles = Float64[] - - for panel in panels + stall_angles = Vector{Float64}(undef, length(panels)) + calculate_stall_angle_list!(stall_angles, panels; + begin_aoa, end_aoa, step_aoa, + stall_angle_if_none_detected, cl_initial) + return stall_angles +end + +function calculate_stall_angle_list!(stall_angles::AbstractVector{Float64}, + panels::Vector{Panel}; + begin_aoa=9.0, + end_aoa=22.0, + step_aoa=1.0, + stall_angle_if_none_detected=50.0, + cl_initial=-10.0) + + # Pre-compute range values to avoid allocation + n_steps = Int(floor((end_aoa - begin_aoa) / step_aoa)) + 1 + + for (idx, panel) in enumerate(panels) # Default stall angle if none found panel_stall = stall_angle_if_none_detected - + # Start with minimum cl cl_old = cl_initial - + # Find stall angle - for aoa in aoa_range + for i in 0:(n_steps-1) + aoa = deg2rad(begin_aoa + i * step_aoa) cl = calculate_cl(panel, aoa) if cl < cl_old panel_stall = aoa @@ -296,11 +318,11 @@ function calculate_stall_angle_list(panels::Vector{Panel}; end cl_old = cl end - - push!(stall_angles, panel_stall) + + stall_angles[idx] = panel_stall end - - return stall_angles + + return nothing end """ From e28c0ced61539c023e5c2e74c22e9efbf407d42b Mon Sep 17 00:00:00 2001 From: 1-Bart-1 Date: Wed, 29 Oct 2025 15:10:21 +0100 Subject: [PATCH 05/53] Working grouping --- README.md | 4 +- bin/install | 2 +- docs/make.jl | 6 -- docs/src/glossary.md | 1 + docs/src/index.md | 4 +- docs/src/tips_and_tricks.md | 32 +++++++++ docs/src/types.md | 1 + examples/Project.toml | 7 ++ examples/bench.jl | 6 -- examples/menu.jl | 5 -- examples/stall_model.jl | 8 +-- scripts/Project.toml | 4 ++ scripts/build_docu.jl | 4 -- src/VortexStepMethod.jl | 12 ++++ src/obj_geometry.jl | 3 +- src/solver.jl | 24 +++++-- src/wing_geometry.jl | 114 +++++++++++++++++++++++++++++---- src/yaml_geometry.jl | 12 ++-- test/Aqua.jl | 5 -- test/Project.toml | 10 +++ test/bench.jl | 5 -- test/bench_solve.jl | 6 -- test/settings/test_settings.jl | 5 -- 23 files changed, 204 insertions(+), 76 deletions(-) create mode 100644 examples/Project.toml create mode 100644 scripts/Project.toml create mode 100644 test/Project.toml diff --git a/README.md b/README.md index de01a449..d93c3a0a 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,9 @@ if you haven't already. On Linux, make sure that Python3 and Matplotlib are inst ``` sudo apt install python3-matplotlib ``` -Furthermore, the packages `TestEnv` and `ControlPlots` must be installed globally: +Furthermore, the package `ControlPlots` must be installed globally: ``` -julia -e 'using Pkg; Pkg.add("TestEnv"); Pkg.add("ControlPlots")' +julia -e 'using Pkg; Pkg.add("ControlPlots")' ``` Before installing this software it is suggested to create a new project, for example like this: diff --git a/bin/install b/bin/install index e4609513..49b359ad 100755 --- a/bin/install +++ b/bin/install @@ -20,7 +20,7 @@ fi export JULIA_PKG_SERVER_REGISTRY_PREFERENCE=eager -julia -e 'using Pkg; Pkg.add("TestEnv"); Pkg.add("ControlPlots")' +julia -e 'using Pkg; Pkg.add("ControlPlots")' julia --project -e 'include("bin/install.jl")' diff --git a/docs/make.jl b/docs/make.jl index 17d42209..677baf72 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,9 +1,3 @@ -using Pkg -if ("TestEnv" ∈ keys(Pkg.project().dependencies)) - if ! ("Documents" ∈ keys(Pkg.project().dependencies)) - using TestEnv; TestEnv.activate() - end -end using ControlPlots using VortexStepMethod using Documenter diff --git a/docs/src/glossary.md b/docs/src/glossary.md index a67c577d..940c6b5d 100644 --- a/docs/src/glossary.md +++ b/docs/src/glossary.md @@ -5,6 +5,7 @@ | AIC | Aerodynamic Influence Coefficient (AIC). The AIC matrix represents the relationship between the induced velocities or pressures on aerodynamic surfaces and the circulation strength or modal deformations of the lifting surfaces.| | inviscid | A fluid flow in which viscosity is considered negligible or zero. This means that there is no internal friction between the fluid layers, and the effects of viscosity on the flow are assumed to be insignificant. | | Panel | Flat surface element in 3D that approximate the contour of the aerodynamic body being studied.| +| Panel Group | A collection of panels whose aerodynamic forces and moments are summed together. Groups can be defined using EQUAL_SIZE (sequential grouping) or REFINE (based on original unrefined structure) methods.| | Section |A wing section, also known as an airfoil or aerofoil, is the cross-sectional shape of an aircraft wing.| | Span | Distance from one wing tip to the other wing tip. | | Polar | The polar typically plots the coefficient of lift (CL) against the coefficient of drag (CD), with the angle of attack as a parameter along the curve. | diff --git a/docs/src/index.md b/docs/src/index.md index fa6d54c1..3310d72c 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -18,9 +18,9 @@ if you haven't already. On Linux, make sure that Python3 and Matplotlib are inst ``` sudo apt install python3-matplotlib ``` -Furthermore, the packages `TestEnv` and `ControlPlots` must be installed globally: +Furthermore, the package `ControlPlots` must be installed globally: ``` -julia -e 'using Pkg; Pkg.add("TestEnv"); Pkg.add("ControlPlots")' +julia -e 'using Pkg; Pkg.add("ControlPlots")' ``` Before installing this software it is suggested to create a new project, for example like this: diff --git a/docs/src/tips_and_tricks.md b/docs/src/tips_and_tricks.md index 64995f34..63cf87d3 100644 --- a/docs/src/tips_and_tricks.md +++ b/docs/src/tips_and_tricks.md @@ -10,6 +10,38 @@ The following bodies can be simulated: To build the geometry of a RAM-air kite, a 3D .obj file can be used as input. In addition a `.dat` file is needed. It should have two columns, one for the `x` and one for the `y` coordinate of the 2D polar that is used. +## Panel Grouping Methods +When creating a wing, you can specify how panels should be grouped for moment and force calculations using the `grouping_method` parameter. Two methods are available: + +### EQUAL_SIZE (Default) +Divides refined panels into equally-sized sequential groups. This is the original behavior. + +```julia +wing = Wing(40; n_groups=4, grouping_method=EQUAL_SIZE) +``` + +In this example, with 40 panels and 4 groups, each group will contain 10 consecutive panels (panels 1-10, 11-20, 21-30, 31-40). + +### REFINE +Groups refined panels back to their original unrefined section. This is useful when you want group moments and forces to represent the original wing structure, regardless of panel refinement. + +```julia +# Create wing with 4 unrefined sections (3 panels) +wing = Wing(40; n_groups=3, grouping_method=REFINE) +add_section!(wing, [0, 5, 0], [1, 5, 0], INVISCID) # Section 1 +add_section!(wing, [0, 2.5, 0], [1, 2.5, 0], INVISCID) # Section 2 +add_section!(wing, [0, 0, 0], [1, 0, 0], INVISCID) # Section 3 +add_section!(wing, [0, -5, 0], [1, -5, 0], INVISCID) # Section 4 +``` + +**Important:** When using `REFINE`, `n_groups` must equal the number of unrefined panels (number of sections - 1). The solver will automatically map each refined panel to its closest original unrefined panel and sum their moments and forces accordingly. + +This is particularly useful for: +- LEI kites where you want loads per rib +- Wings with discrete control surfaces +- Cases where physical structure doesn't align with uniform panel distribution +- Dynamic simulations where you have fewer structural segments than panels needed for accurate VSM aerodynamics. For example, a 6-segment structural model can be combined with 40-panel aerodynamics by using `n_groups=6` and `grouping_method=REFINE` to map aerodynamic loads back to the structural segments. + ## RAM-air kite model If running the example `ram_air_kite.jl` fails, try to run the `cleanup.jl` script and then try again. Background: this example caches the calculated polars. Reading cached polars can fail after an update. diff --git a/docs/src/types.md b/docs/src/types.md index de083b35..9b76a7d0 100644 --- a/docs/src/types.md +++ b/docs/src/types.md @@ -7,6 +7,7 @@ Model WingType AeroModel PanelDistribution +PanelGroupingMethod InitialGammaDistribution SolverStatus ``` diff --git a/examples/Project.toml b/examples/Project.toml new file mode 100644 index 00000000..b94307b9 --- /dev/null +++ b/examples/Project.toml @@ -0,0 +1,7 @@ +[deps] +CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" +ControlPlots = "23c2ee80-7a9e-4350-b264-8e670f12517c" +DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" +VortexStepMethod = "ed3cd733-9f0f-46a9-93e0-89b8d4998dd9" diff --git a/examples/bench.jl b/examples/bench.jl index 90c41ff1..6c2f8726 100644 --- a/examples/bench.jl +++ b/examples/bench.jl @@ -2,12 +2,6 @@ using LinearAlgebra using ControlPlots using VortexStepMethod -using Pkg - -if !("CSV" ∈ keys(Pkg.project().dependencies)) - using TestEnv - TestEnv.activate() -end # Step 1: Define wing parameters n_panels = 20 # Number of panels diff --git a/examples/menu.jl b/examples/menu.jl index 4454ccee..147990c3 100644 --- a/examples/menu.jl +++ b/examples/menu.jl @@ -1,8 +1,3 @@ -using Pkg -if ! ("ControlPlots" ∈ keys(Pkg.project().dependencies)) - using TestEnv; TestEnv.activate() -end - using ControlPlots using VortexStepMethod using REPL.TerminalMenus diff --git a/examples/stall_model.jl b/examples/stall_model.jl index 6253faca..89f3c0f4 100644 --- a/examples/stall_model.jl +++ b/examples/stall_model.jl @@ -2,10 +2,6 @@ using ControlPlots using LinearAlgebra using VortexStepMethod -using Pkg -if ! ("CSV" ∈ keys(Pkg.project().dependencies)) - using TestEnv; TestEnv.activate() -end using CSV using DataFrames @@ -38,7 +34,9 @@ for row in eachrow(df) end # Create wing geometry -CAD_wing = Wing(n_panels; spanwise_distribution) +# Using REFINE grouping method: n_groups should equal number of unrefined panels (18 sections = 18 panels) +n_groups = length(rib_list) - 1 +CAD_wing = Wing(n_panels; spanwise_distribution, n_groups, grouping_method=REFINE) for rib in rib_list add_section!(CAD_wing, rib[1], rib[2], rib[3], rib[4]) end diff --git a/scripts/Project.toml b/scripts/Project.toml new file mode 100644 index 00000000..f020312d --- /dev/null +++ b/scripts/Project.toml @@ -0,0 +1,4 @@ +[deps] +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +LiveServer = "16fef848-5104-11e9-1b77-fb7a48bbb589" +VortexStepMethod = "ed3cd733-9f0f-46a9-93e0-89b8d4998dd9" diff --git a/scripts/build_docu.jl b/scripts/build_docu.jl index f88f437c..f9398642 100644 --- a/scripts/build_docu.jl +++ b/scripts/build_docu.jl @@ -17,8 +17,4 @@ if !("LiveServer" in globaldependencies()) run(`julia -e 'using Pkg; Pkg.add("LiveServer")'`) end -if !("Documenter" ∈ keys(Pkg.project().dependencies)) - using TestEnv - TestEnv.activate() -end using LiveServer; servedocs(launch_browser=true) diff --git a/src/VortexStepMethod.jl b/src/VortexStepMethod.jl index 64a4a657..f3cb531a 100644 --- a/src/VortexStepMethod.jl +++ b/src/VortexStepMethod.jl @@ -37,6 +37,7 @@ export MVec3 export Model, VSM, LLT export AeroModel, LEI_AIRFOIL_BREUKELS, POLAR_VECTORS, POLAR_MATRICES, INVISCID export PanelDistribution, LINEAR, COSINE, COSINE_VAN_GARREL, SPLIT_PROVIDED, UNCHANGED +export PanelGroupingMethod, EQUAL_SIZE, REFINE export InitialGammaDistribution, ELLIPTIC, ZEROS export SolverStatus, FEASIBLE, INFEASIBLE, FAILURE export SolverType, LOOP, NONLIN @@ -138,6 +139,17 @@ Enumeration of the implemented panel distributions. UNCHANGED # Keep original sections end +""" + PanelGroupingMethod EQUAL_SIZE REFINE + +Enumeration of methods for grouping panels. + +# Elements +- EQUAL_SIZE: Divide panels into equally-sized sequential groups +- REFINE: Group refined panels back to their original unrefined section +""" +@enum PanelGroupingMethod EQUAL_SIZE REFINE + """ InitialGammaDistribution ELLIPTIC ZEROS diff --git a/src/obj_geometry.jl b/src/obj_geometry.jl index dec38cb7..1dae4cbc 100644 --- a/src/obj_geometry.jl +++ b/src/obj_geometry.jl @@ -366,7 +366,7 @@ function ObjWing( n_panels=56, n_sections=n_panels+1, n_groups=4, spanwise_distribution=UNCHANGED, spanwise_direction=[0.0, 1.0, 0.0], remove_nan=true, align_to_principal=false, alpha_range=deg2rad.(-5:1:20), delta_range=deg2rad.(-5:1:20), prn=true, - interp_steps=n_sections + interp_steps=n_sections, grouping_method::PanelGroupingMethod=EQUAL_SIZE ) !(n_groups == 0 || n_panels % n_groups == 0) && throw(ArgumentError("Number of panels should be divisible by number of groups")) !isapprox(spanwise_direction, [0.0, 1.0, 0.0]) && throw(ArgumentError("Spanwise direction has to be [0.0, 1.0, 0.0], not $spanwise_direction")) @@ -431,6 +431,7 @@ function ObjWing( Wing(n_panels, n_groups, spanwise_distribution, panel_props, MVec3(spanwise_direction), sections, refined_sections, remove_nan, + grouping_method, Int16[], non_deformed_sections, zeros(n_panels), zeros(n_panels), mass, gamma_tip, inertia_tensor, T_cad_body, R_cad_body, radius, le_interp, te_interp, area_interp, cache) diff --git a/src/solver.jl b/src/solver.jl index 9bce66ae..7f4daec3 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -302,14 +302,26 @@ function solve!(solver::Solver, body_aero::BodyAerodynamics, gamma_distribution= group_idx = 1 for wing in body_aero.wings if wing.n_groups > 0 - panels_per_group = wing.n_panels ÷ wing.n_groups - for _ in 1:wing.n_groups - for _ in 1:panels_per_group - group_moment_dist[group_idx] += moment_dist[panel_idx] - group_moment_coeff_dist[group_idx] += moment_coeff_dist[panel_idx] + if wing.grouping_method == EQUAL_SIZE + # Original method: divide panels into equally-sized sequential groups + panels_per_group = wing.n_panels ÷ wing.n_groups + for _ in 1:wing.n_groups + for _ in 1:panels_per_group + group_moment_dist[group_idx] += moment_dist[panel_idx] + group_moment_coeff_dist[group_idx] += moment_coeff_dist[panel_idx] + panel_idx += 1 + end + group_idx += 1 + end + elseif wing.grouping_method == REFINE + # REFINE method: group refined panels by their original unrefined section + for local_panel_idx in 1:wing.n_panels + original_section_idx = wing.refined_panel_mapping[local_panel_idx] + group_moment_dist[group_idx + original_section_idx - 1] += moment_dist[panel_idx] + group_moment_coeff_dist[group_idx + original_section_idx - 1] += moment_coeff_dist[panel_idx] panel_idx += 1 end - group_idx += 1 + group_idx += wing.n_groups end else # Skip panels for wings with n_groups=0 diff --git a/src/wing_geometry.jl b/src/wing_geometry.jl index 02f7a308..9536107f 100644 --- a/src/wing_geometry.jl +++ b/src/wing_geometry.jl @@ -229,6 +229,10 @@ mutable struct Wing <: AbstractWing refined_sections::Vector{Section} remove_nan::Bool + # Grouping + grouping_method::PanelGroupingMethod + refined_panel_mapping::Vector{Int16} # Maps each refined panel to its original unrefined section index + # Deformation fields non_deformed_sections::Vector{Section} theta_dist::Vector{Float64} @@ -252,7 +256,8 @@ end n_groups=n_panels, spanwise_distribution::PanelDistribution=LINEAR, spanwise_direction::PosVector=MVec3([0.0, 1.0, 0.0]), - remove_nan::Bool=true) + remove_nan::Bool=true, + grouping_method::PanelGroupingMethod=EQUAL_SIZE) Constructor for a [Wing](@ref) struct with default values that initializes the sections and refined sections as empty arrays. Creates a basic wing suitable for YAML-based construction. @@ -263,19 +268,28 @@ and refined sections as empty arrays. Creates a basic wing suitable for YAML-bas - `spanwise_distribution`::PanelDistribution = LINEAR: [PanelDistribution](@ref) - `spanwise_direction::MVec3` = MVec3([0.0, 1.0, 0.0]): Wing span direction vector - `remove_nan::Bool`: Wether to remove the NaNs from interpolations or not +- `grouping_method::PanelGroupingMethod` = EQUAL_SIZE: Method for grouping panels (EQUAL_SIZE or REFINE) """ function Wing(n_panels::Int; n_groups = n_panels, spanwise_distribution::PanelDistribution=LINEAR, spanwise_direction::PosVector=MVec3([0.0, 1.0, 0.0]), - remove_nan=true) - !(n_groups == 0 || n_panels % n_groups == 0) && throw(ArgumentError("Number of panels should be divisible by number of groups")) + remove_nan=true, + grouping_method::PanelGroupingMethod=EQUAL_SIZE) + # Validate grouping parameters + if grouping_method == EQUAL_SIZE + !(n_groups == 0 || n_panels % n_groups == 0) && throw(ArgumentError("With EQUAL_SIZE grouping, number of panels should be divisible by number of groups")) + end + # Note: For REFINE grouping, validation happens after refinement when we know the number of unrefined sections + panel_props = PanelProperties{n_panels}() # Initialize with default/empty values for optional fields Wing( n_panels, n_groups, spanwise_distribution, panel_props, spanwise_direction, Section[], Section[], remove_nan, + # Grouping + grouping_method, Int16[], # Deformation fields Section[], zeros(n_panels), zeros(n_panels), # Physical properties (defaults for non-OBJ wings) @@ -556,23 +570,25 @@ function refine_aerodynamic_mesh!(wing::AbstractWing) for i in eachindex(wing.sections) reinit!(wing.refined_sections[i], wing.sections[i]) end + compute_refined_panel_mapping!(wing) return nothing end @debug "Refining aerodynamic mesh from $(length(wing.sections)) sections to $n_sections sections." - + # Handle two-section case if n_sections == 2 reinit!(wing.refined_sections[1], LE[1,:], TE[1,:], aero_model[1], aero_data[1]) reinit!(wing.refined_sections[2], LE[end,:], TE[end,:], aero_model[end], aero_data[end]) + compute_refined_panel_mapping!(wing) return nothing end - + # Handle different distribution types if wing.spanwise_distribution == SPLIT_PROVIDED - return refine_mesh_by_splitting_provided_sections!(wing) + refine_mesh_by_splitting_provided_sections!(wing) elseif wing.spanwise_distribution in (LINEAR, COSINE, COSINE_VAN_GARREL) - return refine_mesh_for_linear_cosine_distribution!( + refine_mesh_for_linear_cosine_distribution!( wing, 1, wing.spanwise_distribution, @@ -585,6 +601,80 @@ function refine_aerodynamic_mesh!(wing::AbstractWing) else throw(ArgumentError("Unsupported spanwise panel distribution: $(wing.spanwise_distribution)")) end + + # Compute panel mapping by finding closest unrefined panel for each refined panel + compute_refined_panel_mapping!(wing) + + # Validate REFINE grouping method + if wing.grouping_method == REFINE && wing.n_groups > 0 + n_unrefined_panels = length(wing.sections) - 1 + if wing.n_groups != n_unrefined_panels + throw(ArgumentError( + "With REFINE grouping method, n_groups ($(wing.n_groups)) must equal " * + "the number of unrefined panels ($n_unrefined_panels). " * + "The wing has $(length(wing.sections)) unrefined sections, forming $n_unrefined_panels panels." + )) + end + end + + return nothing +end + + +""" + compute_refined_panel_mapping!(wing::AbstractWing) + +Compute the mapping from refined panels to unrefined panels by finding +the closest unrefined panel for each refined panel (based on panel center distance). +This is non-allocating and works after refinement is complete. +""" +function compute_refined_panel_mapping!(wing::AbstractWing) + n_unrefined_sections = length(wing.sections) + n_refined_panels = wing.n_panels + + # Handle case where no refinement occurred + if n_unrefined_sections == n_refined_panels + 1 + wing.refined_panel_mapping = Int16[i for i in 1:n_refined_panels] + return nothing + end + + # Ensure mapping array is allocated + if length(wing.refined_panel_mapping) != n_refined_panels + wing.refined_panel_mapping = zeros(Int16, n_refined_panels) + end + + # Compute centers of unrefined panels + n_unrefined_panels = n_unrefined_sections - 1 + unrefined_centers = Vector{MVec3}(undef, n_unrefined_panels) + for i in 1:n_unrefined_panels + le_mid = (wing.sections[i].LE_point + wing.sections[i+1].LE_point) / 2 + te_mid = (wing.sections[i].TE_point + wing.sections[i+1].TE_point) / 2 + unrefined_centers[i] = MVec3((le_mid + te_mid) / 2) + end + + # For each refined panel, find closest unrefined panel + for refined_panel_idx in 1:n_refined_panels + le_mid = (wing.refined_sections[refined_panel_idx].LE_point + + wing.refined_sections[refined_panel_idx+1].LE_point) / 2 + te_mid = (wing.refined_sections[refined_panel_idx].TE_point + + wing.refined_sections[refined_panel_idx+1].TE_point) / 2 + refined_center = MVec3((le_mid + te_mid) / 2) + + # Find closest unrefined panel + min_dist = Inf + closest_idx = 1 + for unrefined_panel_idx in 1:n_unrefined_panels + dist = norm(refined_center - unrefined_centers[unrefined_panel_idx]) + if dist < min_dist + min_dist = dist + closest_idx = unrefined_panel_idx + end + end + + wing.refined_panel_mapping[refined_panel_idx] = Int16(closest_idx) + end + + return nothing end @@ -736,7 +826,7 @@ function refine_mesh_for_linear_cosine_distribution!( target_length = target_lengths[i] # Find segment index - section_index = searchsortedlast(qc_cum_length, target_length) + section_index = searchsortedlast(qc_cum_length, target_length) section_index = clamp(section_index, 1, length(qc_cum_length)-1) # 4. Calculate weights @@ -747,7 +837,7 @@ function refine_mesh_for_linear_cosine_distribution!( right_weight = t # 5. Calculate quarter chord point - new_quarter_chord[i,:] = quarter_chord[section_index,:] + + new_quarter_chord[i,:] = quarter_chord[section_index,:] + t .* (quarter_chord[section_index+1,:] - quarter_chord[section_index,:]) # 6. Calculate chord vectors @@ -899,10 +989,10 @@ function refine_mesh_by_splitting_provided_sections!(wing::AbstractWing) # Add left section of pair reinit!(wing.refined_sections[idx], wing.sections[left_section_index]) idx += 1 - + # Calculate new sections for this pair num_new_sections = new_sections_per_pair + (left_section_index <= remaining ? 1 : 0) - + if num_new_sections > 0 # Prepare pair data LE_pair = hcat(LE[left_section_index], LE[left_section_index + 1])' @@ -915,7 +1005,7 @@ function refine_mesh_by_splitting_provided_sections!(wing::AbstractWing) aero_data[left_section_index], aero_data[left_section_index + 1] ] - + # Generate sections for this pair idx = refine_mesh_for_linear_cosine_distribution!( wing, diff --git a/src/yaml_geometry.jl b/src/yaml_geometry.jl index 3ba618f5..42a82199 100644 --- a/src/yaml_geometry.jl +++ b/src/yaml_geometry.jl @@ -188,7 +188,8 @@ function Wing( spanwise_distribution=LINEAR, spanwise_direction=[0.0, 1.0, 0.0], remove_nan=true, - prn=false + prn=false, + grouping_method::PanelGroupingMethod=EQUAL_SIZE ) !(n_groups == 0 || n_panels % n_groups == 0) && throw(ArgumentError("Number of panels should be divisible by number of groups")) !isapprox(spanwise_direction, [0.0, 1.0, 0.0]) && throw(ArgumentError("Spanwise direction has to be [0.0, 1.0, 0.0], not $spanwise_direction")) @@ -236,11 +237,12 @@ function Wing( end # Create Wing using the standard constructor - wing = Wing(n_panels; - n_groups=n_groups, + wing = Wing(n_panels; + n_groups=n_groups, spanwise_distribution=spanwise_distribution, - spanwise_direction=MVec3(spanwise_direction), - remove_nan=remove_nan + spanwise_direction=MVec3(spanwise_direction), + remove_nan=remove_nan, + grouping_method=grouping_method ) # Parse sections and populate wing diff --git a/test/Aqua.jl b/test/Aqua.jl index e16c0cc9..7a885062 100644 --- a/test/Aqua.jl +++ b/test/Aqua.jl @@ -1,8 +1,3 @@ -using Pkg -if ! ("Aqua" ∈ keys(Pkg.project().dependencies)) - using TestEnv; TestEnv.activate() -end - using Aqua, VortexStepMethod, Test @testset "Aqua.jl" begin Aqua.test_all( diff --git a/test/Project.toml b/test/Project.toml new file mode 100644 index 00000000..5354caa3 --- /dev/null +++ b/test/Project.toml @@ -0,0 +1,10 @@ +[deps] +Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" +BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" +CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" +ControlPlots = "23c2ee80-7a9e-4350-b264-8e670f12517c" +DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +VortexStepMethod = "ed3cd733-9f0f-46a9-93e0-89b8d4998dd9" diff --git a/test/bench.jl b/test/bench.jl index 5acfb95f..88172d69 100644 --- a/test/bench.jl +++ b/test/bench.jl @@ -1,8 +1,3 @@ -using Pkg -if !("BenchmarkTools" ∈ keys(Pkg.project().dependencies)) - using TestEnv - TestEnv.activate() -end using BenchmarkTools using StaticArrays using VortexStepMethod diff --git a/test/bench_solve.jl b/test/bench_solve.jl index 1abe47d7..dc8c4590 100644 --- a/test/bench_solve.jl +++ b/test/bench_solve.jl @@ -6,12 +6,6 @@ using VortexStepMethod using BenchmarkTools using Test -using Pkg - -if !("CSV" ∈ keys(Pkg.project().dependencies)) - using TestEnv - TestEnv.activate() -end # Step 1: Define wing parameters n_panels = 20 # Number of panels diff --git a/test/settings/test_settings.jl b/test/settings/test_settings.jl index c7412894..33ccd7a7 100644 --- a/test/settings/test_settings.jl +++ b/test/settings/test_settings.jl @@ -1,8 +1,3 @@ -using Pkg -if ! ("Test" ∈ keys(Pkg.project().dependencies)) - using TestEnv; TestEnv.activate() -end - using VortexStepMethod using Test From c27d1572e3da954dfa43459bceec52c69a4e6070 Mon Sep 17 00:00:00 2001 From: 1-Bart-1 Date: Wed, 29 Oct 2025 15:46:51 +0100 Subject: [PATCH 06/53] Working tests except aqua --- Project.toml | 22 ---- examples/stall_model.jl | 16 +-- test/Project.toml | 23 +++- test/wing_geometry/test_wing_geometry.jl | 153 +++++++++++++++++++++++ 4 files changed, 184 insertions(+), 30 deletions(-) diff --git a/Project.toml b/Project.toml index 6841b218..29000957 100644 --- a/Project.toml +++ b/Project.toml @@ -38,50 +38,28 @@ VortexStepMethodControlPlotsExt = "ControlPlots" VortexStepMethodMakieExt = "Makie" [compat] -Aqua = "0.8" -BenchmarkTools = "1" -CSV = "0.10" Colors = "0.13" -ControlPlots = "0.2.5" -DataFrames = "1.7" DefaultApplication = "1" DelimitedFiles = "1" DifferentiationInterface = "0.7.4" -Documenter = "1.8" FiniteDiff = "2.27.0" Interpolations = "0.15, 0.16" LaTeXStrings = "1" LinearAlgebra = "1" Logging = "1" -Makie = "0.24.6" Measures = "0.3" NonlinearSolve = "4.8.0" Parameters = "0.12" Pkg = "1" PreallocationTools = "0.4.31" PrecompileTools = "1.2.1" -Random = "1.10.0" RecursiveArrayTools = "3 - 3.36.0" SciMLBase = "2.77.0" Serialization = "1" StaticArrays = "1" Statistics = "1" StructMapping = "0.2.3" -Test = "1" Timers = "0.1" Xfoil = "1.1.0" YAML = "0.4.13" julia = "1.10, 1.11" - -[extras] -Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" -BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" -CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" -ControlPlots = "23c2ee80-7a9e-4350-b264-8e670f12517c" -DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" -Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" -Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" - -[targets] -test = ["Test", "DataFrames", "CSV", "Documenter", "BenchmarkTools", "ControlPlots", "Aqua", "Random"] diff --git a/examples/stall_model.jl b/examples/stall_model.jl index 89f3c0f4..78fb28fe 100644 --- a/examples/stall_model.jl +++ b/examples/stall_model.jl @@ -107,21 +107,23 @@ path_cfd_lebesque = joinpath( "V3_CL_CD_RANS_Lebesque_2024_Rey_300e4.csv" ) +# Only include literature data if file exists +literature_paths = isfile(path_cfd_lebesque) ? [path_cfd_lebesque] : String[] +labels = isfile(path_cfd_lebesque) ? + ["VSM CAD 19ribs", "VSM CAD 19ribs , with stall correction", "CFD_Lebesque Rey 30e5"] : + ["VSM CAD 19ribs", "VSM CAD 19ribs , with stall correction"] + PLOT && plot_polars( [vsm_solver, VSM_with_stall_correction], [body_aero, body_aero], - [ - "VSM CAD 19ribs", - "VSM CAD 19ribs , with stall correction", - "CFD_Lebesque Rey 30e5" - ]; - literature_path_list=[path_cfd_lebesque], + labels; + literature_path_list=literature_paths, angle_range=range(0, 25, length=25), angle_type="angle_of_attack", angle_of_attack=0, side_slip=0, v_a=10, - title="tutorial_testing_stall_model_n_panels_$(n_panels)_distribution_$(spanwise_distribution)", + title="tutorial_testing_stall_model_n_panels_$(n_panels)_distribution_$(spanwise_distribution)_grouping_$(CAD_wing.grouping_method)", data_type=".pdf", save_path=joinpath(save_folder, "polars"), is_save=true, diff --git a/test/Project.toml b/test/Project.toml index 5354caa3..32eda93f 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -5,6 +5,27 @@ CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" ControlPlots = "23c2ee80-7a9e-4350-b264-8e670f12517c" DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +Interpolations = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b" +StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" +Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" -VortexStepMethod = "ed3cd733-9f0f-46a9-93e0-89b8d4998dd9" +YAML = "ddb6d928-2868-570f-bddf-ab3f9cf99eb6" + +[compat] +Aqua = "0.8" +BenchmarkTools = "1" +CSV = "0.10" +ControlPlots = "0.2.5" +DataFrames = "1.7" +Documenter = "1.8" +LinearAlgebra = "1" +Logging = "1" +Random = "1.10.0" +StaticArrays = "1" +Statistics = "1" +Test = "1" +YAML = "0.4.13" diff --git a/test/wing_geometry/test_wing_geometry.jl b/test/wing_geometry/test_wing_geometry.jl index d2d01f0f..ddb8ae25 100644 --- a/test/wing_geometry/test_wing_geometry.jl +++ b/test/wing_geometry/test_wing_geometry.jl @@ -332,4 +332,157 @@ end @test section.aero_model == INVISCID end end + + @testset "REFINE grouping panel mapping" begin + # Test that refined panel mapping actually maps each panel to its closest unrefined panel + + @testset "LINEAR distribution" begin + n_panels = 20 + span = 10.0 + + wing = Wing(n_panels; spanwise_distribution=LINEAR, n_groups=2, grouping_method=REFINE) + # 3 sections = 2 unrefined panels + add_section!(wing, [0.0, span/2, 0.0], [1.0, span/2, 0.0], INVISCID) + add_section!(wing, [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], INVISCID) + add_section!(wing, [0.0, -span/2, 0.0], [1.0, -span/2, 0.0], INVISCID) + + refine_aerodynamic_mesh!(wing) + + @test length(wing.refined_panel_mapping) == n_panels + + # Manually verify each refined panel is mapped to its closest unrefined panel + n_unrefined_panels = length(wing.sections) - 1 + for refined_panel_idx in 1:n_panels + # Calculate refined panel center + le_mid = (wing.refined_sections[refined_panel_idx].LE_point + + wing.refined_sections[refined_panel_idx+1].LE_point) / 2 + te_mid = (wing.refined_sections[refined_panel_idx].TE_point + + wing.refined_sections[refined_panel_idx+1].TE_point) / 2 + refined_center = (le_mid + te_mid) / 2 + + # Find closest unrefined panel manually + min_dist = Inf + closest_idx = 1 + for unrefined_panel_idx in 1:n_unrefined_panels + le_mid_unref = (wing.sections[unrefined_panel_idx].LE_point + + wing.sections[unrefined_panel_idx+1].LE_point) / 2 + te_mid_unref = (wing.sections[unrefined_panel_idx].TE_point + + wing.sections[unrefined_panel_idx+1].TE_point) / 2 + unrefined_center = (le_mid_unref + te_mid_unref) / 2 + + dist = norm(refined_center - unrefined_center) + if dist < min_dist + min_dist = dist + closest_idx = unrefined_panel_idx + end + end + + # Verify mapping is correct + @test wing.refined_panel_mapping[refined_panel_idx] == closest_idx + end + end + + @testset "COSINE distribution" begin + n_panels = 30 + span = 20.0 + + wing = Wing(n_panels; spanwise_distribution=COSINE, n_groups=3, grouping_method=REFINE) + # 4 sections = 3 unrefined panels + add_section!(wing, [0.0, span/2, 0.0], [1.0, span/2, 0.0], INVISCID) + add_section!(wing, [0.0, span/6, 0.0], [1.0, span/6, 0.0], INVISCID) + add_section!(wing, [0.0, -span/6, 0.0], [1.0, -span/6, 0.0], INVISCID) + add_section!(wing, [0.0, -span/2, 0.0], [1.0, -span/2, 0.0], INVISCID) + + refine_aerodynamic_mesh!(wing) + + @test length(wing.refined_panel_mapping) == n_panels + + # Verify each panel is mapped to its closest unrefined panel + n_unrefined_panels = length(wing.sections) - 1 + for refined_panel_idx in 1:n_panels + # Calculate refined panel center + le_mid = (wing.refined_sections[refined_panel_idx].LE_point + + wing.refined_sections[refined_panel_idx+1].LE_point) / 2 + te_mid = (wing.refined_sections[refined_panel_idx].TE_point + + wing.refined_sections[refined_panel_idx+1].TE_point) / 2 + refined_center = (le_mid + te_mid) / 2 + + # Find closest unrefined panel manually + min_dist = Inf + closest_idx = 1 + for unrefined_panel_idx in 1:n_unrefined_panels + le_mid_unref = (wing.sections[unrefined_panel_idx].LE_point + + wing.sections[unrefined_panel_idx+1].LE_point) / 2 + te_mid_unref = (wing.sections[unrefined_panel_idx].TE_point + + wing.sections[unrefined_panel_idx+1].TE_point) / 2 + unrefined_center = (le_mid_unref + te_mid_unref) / 2 + + dist = norm(refined_center - unrefined_center) + if dist < min_dist + min_dist = dist + closest_idx = unrefined_panel_idx + end + end + + # Verify mapping is correct + @test wing.refined_panel_mapping[refined_panel_idx] == closest_idx + end + end + + @testset "SPLIT_PROVIDED distribution" begin + n_panels = 12 + + wing = Wing(n_panels; spanwise_distribution=SPLIT_PROVIDED, n_groups=3, grouping_method=REFINE) + # 4 sections = 3 unrefined panels + add_section!(wing, [0.0, 6.0, 0.0], [1.0, 6.0, 0.0], INVISCID) + add_section!(wing, [0.0, 2.0, 0.0], [1.0, 2.0, 0.0], INVISCID) + add_section!(wing, [0.0, -2.0, 0.0], [1.0, -2.0, 0.0], INVISCID) + add_section!(wing, [0.0, -6.0, 0.0], [1.0, -6.0, 0.0], INVISCID) + + refine_aerodynamic_mesh!(wing) + + @test length(wing.refined_panel_mapping) == n_panels + + # Verify each panel is mapped to its closest unrefined panel + n_unrefined_panels = length(wing.sections) - 1 + for refined_panel_idx in 1:n_panels + # Calculate refined panel center + le_mid = (wing.refined_sections[refined_panel_idx].LE_point + + wing.refined_sections[refined_panel_idx+1].LE_point) / 2 + te_mid = (wing.refined_sections[refined_panel_idx].TE_point + + wing.refined_sections[refined_panel_idx+1].TE_point) / 2 + refined_center = (le_mid + te_mid) / 2 + + # Find closest unrefined panel manually + min_dist = Inf + closest_idx = 1 + for unrefined_panel_idx in 1:n_unrefined_panels + le_mid_unref = (wing.sections[unrefined_panel_idx].LE_point + + wing.sections[unrefined_panel_idx+1].LE_point) / 2 + te_mid_unref = (wing.sections[unrefined_panel_idx].TE_point + + wing.sections[unrefined_panel_idx+1].TE_point) / 2 + unrefined_center = (le_mid_unref + te_mid_unref) / 2 + + dist = norm(refined_center - unrefined_center) + if dist < min_dist + min_dist = dist + closest_idx = unrefined_panel_idx + end + end + + # Verify mapping is correct + @test wing.refined_panel_mapping[refined_panel_idx] == closest_idx + end + end + + @testset "Validation: n_groups must equal unrefined panels" begin + wing = Wing(20; spanwise_distribution=LINEAR, n_groups=5, grouping_method=REFINE) + add_section!(wing, [0.0, 5.0, 0.0], [1.0, 5.0, 0.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, 0.0], [1.0, -5.0, 0.0], INVISCID) + + # Should throw error: 5 groups but only 2 unrefined panels + @test_throws ArgumentError refine_aerodynamic_mesh!(wing) + end + end end \ No newline at end of file From 35356430c0ef65814501f2fba2be71ea1a43a595 Mon Sep 17 00:00:00 2001 From: 1-Bart-1 Date: Wed, 29 Oct 2025 15:57:35 +0100 Subject: [PATCH 07/53] Working tests with aqua --- Project.toml | 2 ++ test/Project.toml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/Project.toml b/Project.toml index 29000957..7609394f 100644 --- a/Project.toml +++ b/Project.toml @@ -39,6 +39,7 @@ VortexStepMethodMakieExt = "Makie" [compat] Colors = "0.13" +ControlPlots = "0.2.5" DefaultApplication = "1" DelimitedFiles = "1" DifferentiationInterface = "0.7.4" @@ -47,6 +48,7 @@ Interpolations = "0.15, 0.16" LaTeXStrings = "1" LinearAlgebra = "1" Logging = "1" +Makie = "0.24.6" Measures = "0.3" NonlinearSolve = "4.8.0" Parameters = "0.12" diff --git a/test/Project.toml b/test/Project.toml index 32eda93f..9ede8c7a 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -22,9 +22,11 @@ CSV = "0.10" ControlPlots = "0.2.5" DataFrames = "1.7" Documenter = "1.8" +Interpolations = "0.15, 0.16" LinearAlgebra = "1" Logging = "1" Random = "1.10.0" +Serialization = "1" StaticArrays = "1" Statistics = "1" Test = "1" From 2b0301504a3ac6a2c14054df13f28bc6ee5713e8 Mon Sep 17 00:00:00 2001 From: 1-Bart-1 Date: Sat, 1 Nov 2025 23:31:49 +0100 Subject: [PATCH 08/53] Don't deform tuple --- examples/Project.toml | 3 + src/wing_geometry.jl | 8 +- test/yaml_geometry/test_yaml_geometry.jl | 1 + .../test_yaml_wing_deformation.jl | 175 ++++++++++++++++++ 4 files changed, 184 insertions(+), 3 deletions(-) create mode 100644 examples/Project.toml create mode 100644 test/yaml_geometry/test_yaml_wing_deformation.jl diff --git a/examples/Project.toml b/examples/Project.toml new file mode 100644 index 00000000..24966cd6 --- /dev/null +++ b/examples/Project.toml @@ -0,0 +1,3 @@ +[deps] +ControlPlots = "23c2ee80-7a9e-4350-b264-8e670f12517c" +VortexStepMethod = "ed3cd733-9f0f-46a9-93e0-89b8d4998dd9" diff --git a/src/wing_geometry.jl b/src/wing_geometry.jl index 02f7a308..4f00d2d4 100644 --- a/src/wing_geometry.jl +++ b/src/wing_geometry.jl @@ -44,10 +44,12 @@ function reinit!(section::Section, LE_point, TE_point, aero_model=nothing, aero_ section.TE_point .= TE_point (!isnothing(aero_model)) && (section.aero_model = aero_model) if !isnothing(aero_data) - if !isnothing(section.aero_data) - section.aero_data .= aero_data - else + # NTuple is immutable, so we must assign directly + # For mutable types (Vector, Matrix), we can broadcast for efficiency + if aero_data isa NTuple || isnothing(section.aero_data) section.aero_data = aero_data + else + section.aero_data .= aero_data end end nothing diff --git a/test/yaml_geometry/test_yaml_geometry.jl b/test/yaml_geometry/test_yaml_geometry.jl index 12daa54f..58924a91 100644 --- a/test/yaml_geometry/test_yaml_geometry.jl +++ b/test/yaml_geometry/test_yaml_geometry.jl @@ -4,4 +4,5 @@ using Test # Include specific test files for better organization include("test_load_polar_data.jl") include("test_wing_constructor.jl") + include("test_yaml_wing_deformation.jl") end diff --git a/test/yaml_geometry/test_yaml_wing_deformation.jl b/test/yaml_geometry/test_yaml_wing_deformation.jl new file mode 100644 index 00000000..da931177 --- /dev/null +++ b/test/yaml_geometry/test_yaml_wing_deformation.jl @@ -0,0 +1,175 @@ +using VortexStepMethod +using LinearAlgebra +using Test + +@testset "YAML Wing Deformation Tests" begin + @testset "Simple Wing Deformation" begin + # Load existing simple_wing.yaml + simple_wing_file = test_data_path("yaml_geometry", "simple_wing.yaml") + wing = Wing(simple_wing_file; n_panels=4, n_groups=2) + body_aero = BodyAerodynamics([wing]) + + # Store original TE point for comparison + i = length(body_aero.panels) ÷ 2 + original_te_point = copy(body_aero.panels[i].TE_point_1) + original_le_point = copy(body_aero.panels[i].LE_point_1) + + # Apply deformation with non-zero angles + theta_dist = fill(deg2rad(30.0), wing.n_panels) # 30 degrees twist + delta_dist = fill(deg2rad(5.0), wing.n_panels) # 5 degrees trailing edge deflection + + VortexStepMethod.deform!(wing, theta_dist, delta_dist) + VortexStepMethod.reinit!(body_aero) + + # Check if TE point changed after deformation + deformed_te_point = copy(body_aero.panels[i].TE_point_1) + deformed_le_point = copy(body_aero.panels[i].LE_point_1) + + # TE point should change significantly due to twist and deflection + @test !isapprox(original_te_point, deformed_te_point, atol=1e-2) + @test deformed_te_point[3] < original_te_point[3] # TE should move down with positive twist + + # LE point should also change due to twist + @test !isapprox(original_le_point, deformed_le_point, atol=1e-2) + + # Check delta is set correctly + @test body_aero.panels[i].delta ≈ deg2rad(5.0) + + # Reset deformation with zero angles + zero_theta_dist = zeros(wing.n_panels) + zero_delta_dist = zeros(wing.n_panels) + + VortexStepMethod.deform!(wing, zero_theta_dist, zero_delta_dist) + VortexStepMethod.reinit!(body_aero) + + # Check if TE point returned to original position + reset_te_point = copy(body_aero.panels[i].TE_point_1) + reset_le_point = copy(body_aero.panels[i].LE_point_1) + @test original_te_point ≈ reset_te_point atol=1e-4 + @test original_le_point ≈ reset_le_point atol=1e-4 + @test body_aero.panels[i].delta ≈ 0.0 atol=1e-4 + end + + @testset "Complex Wing Deformation" begin + # Load existing complex_wing.yaml with multiple sections + complex_wing_file = test_data_path("yaml_geometry", "complex_wing.yaml") + wing = Wing(complex_wing_file; n_panels=12, n_groups=3) + body_aero = BodyAerodynamics([wing]) + + # Store original points for multiple panels + original_points = [] + test_indices = [1, length(body_aero.panels) ÷ 2, length(body_aero.panels)] + for i in test_indices + push!(original_points, ( + LE=copy(body_aero.panels[i].LE_point_1), + TE=copy(body_aero.panels[i].TE_point_1) + )) + end + + # Apply spanwise-varying deformation + theta_dist = [deg2rad(10.0 * i / wing.n_panels) for i in 1:wing.n_panels] # Linear twist distribution + delta_dist = [deg2rad(-5.0 + 10.0 * i / wing.n_panels) for i in 1:wing.n_panels] # Varying deflection + + VortexStepMethod.deform!(wing, theta_dist, delta_dist) + VortexStepMethod.reinit!(body_aero) + + # Check that different panels have different deformations + for (idx, i) in enumerate(test_indices) + deformed_te = body_aero.panels[i].TE_point_1 + deformed_le = body_aero.panels[i].LE_point_1 + + # Points should have changed + @test !isapprox(original_points[idx].TE, deformed_te, atol=1e-2) + @test !isapprox(original_points[idx].LE, deformed_le, atol=1e-2) + end + + # Check that the deformation is applied correctly + # First panel should have smaller theta, last panel should have larger theta + @test body_aero.panels[1].delta < body_aero.panels[end].delta + + # Reset and verify + VortexStepMethod.deform!(wing, zeros(wing.n_panels), zeros(wing.n_panels)) + VortexStepMethod.reinit!(body_aero) + + for (idx, i) in enumerate(test_indices) + reset_te = body_aero.panels[i].TE_point_1 + reset_le = body_aero.panels[i].LE_point_1 + @test original_points[idx].TE ≈ reset_te atol=1e-4 + @test original_points[idx].LE ≈ reset_le atol=1e-4 + @test body_aero.panels[i].delta ≈ 0.0 atol=1e-4 + end + end + + @testset "Multiple Reinit Calls with NTuple aero_data" begin + # This test specifically checks the NTuple handling fix + simple_wing_file = test_data_path("yaml_geometry", "simple_wing.yaml") + wing = Wing(simple_wing_file; n_panels=4, n_groups=2) + + # Verify that sections have NTuple aero_data (for wings with simple polars) + # or other valid AeroData types + @test wing.sections[1].aero_data !== nothing + + # Perform multiple reinit! calls to ensure NTuple handling works + for _ in 1:5 + VortexStepMethod.reinit!(wing) + end + + # Wing should still be valid after multiple reinits + @test wing.sections[1].aero_data !== nothing + @test length(wing.sections) == 2 + end + + @testset "Deformation with BodyAerodynamics Reinit" begin + # Test that reinit! on BodyAerodynamics properly handles deformed wings + simple_wing_file = test_data_path("yaml_geometry", "simple_wing.yaml") + wing = Wing(simple_wing_file; n_panels=4, n_groups=2) + body_aero = BodyAerodynamics([wing]) + + # Apply deformation + theta_dist = fill(deg2rad(15.0), wing.n_panels) + delta_dist = fill(deg2rad(3.0), wing.n_panels) + VortexStepMethod.deform!(wing, theta_dist, delta_dist) + + # Store state after deformation + i = length(body_aero.panels) ÷ 2 + + # Multiple reinit calls should work without errors + for _ in 1:3 + VortexStepMethod.reinit!(body_aero; + va=zeros(3), + omega=zeros(3), + init_aero=true + ) + end + + # Panel should maintain deformation + @test body_aero.panels[i].delta ≈ deg2rad(3.0) atol=1e-6 + end + + @testset "Edge Cases" begin + simple_wing_file = test_data_path("yaml_geometry", "simple_wing.yaml") + wing = Wing(simple_wing_file; n_panels=2, n_groups=1) + body_aero = BodyAerodynamics([wing]) + + # Test zero deformation + VortexStepMethod.deform!(wing, zeros(wing.n_panels), zeros(wing.n_panels)) + VortexStepMethod.reinit!(body_aero) + @test all(p.delta ≈ 0.0 for p in body_aero.panels) + + # Test large deformation angles + theta_dist = fill(deg2rad(60.0), wing.n_panels) + delta_dist = fill(deg2rad(30.0), wing.n_panels) + + # Should not error even with large angles + VortexStepMethod.deform!(wing, theta_dist, delta_dist) + VortexStepMethod.reinit!(body_aero) + @test all(p.delta ≈ deg2rad(30.0) for p in body_aero.panels) + + # Test negative angles + theta_dist = fill(deg2rad(-20.0), wing.n_panels) + delta_dist = fill(deg2rad(-10.0), wing.n_panels) + VortexStepMethod.deform!(wing, theta_dist, delta_dist) + VortexStepMethod.reinit!(body_aero) + @test all(p.delta ≈ deg2rad(-10.0) for p in body_aero.panels) + end +end From 4ca52ac9ead629999d2260ce12f639ba9fd576db Mon Sep 17 00:00:00 2001 From: 1-Bart-1 Date: Thu, 6 Nov 2025 15:20:03 +0100 Subject: [PATCH 09/53] Option to not use data prefix --- src/settings.jl | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/settings.jl b/src/settings.jl index fe538ed9..3448802d 100644 --- a/src/settings.jl +++ b/src/settings.jl @@ -40,9 +40,13 @@ end solver_settings::SolverSettings = SolverSettings() end -function VSMSettings(filename) +function VSMSettings(filename; data_prefix=true) # Uwe's suggested 3-line approach using StructMapping.jl (adapted) - data = YAML.load_file(joinpath("data", filename)) + if data_prefix + data = YAML.load_file(joinpath("data", filename)) + else + data = YAML.load_file(filename) + end # Use StructMapping for basic structure conversion # But handle special fields manually due to enum conversion needs @@ -110,4 +114,4 @@ function Base.show(io::IO, vsm_settings::VSMSettings) print(io, replace(repr(wing), "\n" => "\n ")) end print(io, replace(repr(vsm_settings.solver_settings), "\n" => "\n ")) -end \ No newline at end of file +end From 128f720fae4b8584642a08577a36cb58d6bf103b Mon Sep 17 00:00:00 2001 From: 1-Bart-1 Date: Fri, 7 Nov 2025 11:11:58 +0100 Subject: [PATCH 10/53] Add obj set --- src/settings.jl | 12 +++- src/yaml_geometry.jl | 71 ++++++++++++++++--- test/settings/test_settings.jl | 2 +- .../test_yaml_wing_deformation.jl | 12 ++-- 4 files changed, 80 insertions(+), 17 deletions(-) diff --git a/src/settings.jl b/src/settings.jl index 3448802d..ad3fb75f 100644 --- a/src/settings.jl +++ b/src/settings.jl @@ -8,8 +8,10 @@ end @with_kw mutable struct WingSettings name::String = "main_wing" geometry_file::String = "" # path to wing geometry YAML file + obj_file::String = "" # path to .obj geometry file + dat_file::String = "" # path to .dat airfoil file n_panels::Int64 = 40 - n_groups::Int64 = 40 + n_groups::Int64 = 40 spanwise_panel_distribution::PanelDistribution = LINEAR spanwise_direction::MVec3 = [0.0, 1.0, 0.0] remove_nan = true @@ -68,12 +70,18 @@ function VSMSettings(filename; data_prefix=true) if haskey(wing_data, "geometry_file") wing.geometry_file = wing_data["geometry_file"] end + if haskey(wing_data, "obj_file") + wing.obj_file = wing_data["obj_file"] + end + if haskey(wing_data, "dat_file") + wing.dat_file = wing_data["dat_file"] + end wing.n_panels = wing_data["n_panels"] wing.n_groups = wing_data["n_groups"] wing.spanwise_panel_distribution = eval(Symbol(wing_data["spanwise_panel_distribution"])) wing.spanwise_direction = MVec3(wing_data["spanwise_direction"]) wing.remove_nan = wing_data["remove_nan"] - + push!(vsm_settings.wings, wing) n_panels += wing.n_panels n_groups += wing.n_groups diff --git a/src/yaml_geometry.jl b/src/yaml_geometry.jl index 3ba618f5..8279d525 100644 --- a/src/yaml_geometry.jl +++ b/src/yaml_geometry.jl @@ -275,9 +275,13 @@ end Create a wing model from VSM settings configuration. -This constructor is a convenience wrapper that extracts wing configuration -from VSMSettings and creates a Wing using the YAML geometry file path and -parameters specified in the settings. +This constructor is a convenience wrapper that extracts wing configuration +from VSMSettings and creates a Wing using either: +- YAML geometry file (geometry_file field), or +- OBJ + DAT files (obj_file and dat_file fields) + +The constructor automatically determines which path to use based on which +fields are populated in the settings. # Arguments - `settings`: VSMSettings object containing wing configuration @@ -287,15 +291,64 @@ A fully initialized `Wing` instance ready for aerodynamic simulation. # Example ```julia -# Load settings and create wing in one step +# Using YAML geometry settings = VSMSettings("path/to/vsm_settings.yaml") wing = Wing(settings) + +# Settings can specify either: +# - geometry_file: "path/to/wing.yaml" # YAML-based +# - obj_file + dat_file # OBJ-based ``` """ function Wing(settings::VSMSettings) - Wing(settings.wings[1].geometry_file; - n_panels=settings.wings[1].n_panels, - n_groups=settings.wings[1].n_groups, - spanwise_distribution=settings.wings[1].spanwise_panel_distribution - ) + wing_settings = settings.wings[1] + + # Check which geometry format to use + has_yaml = !isempty(wing_settings.geometry_file) + has_obj = !isempty(wing_settings.obj_file) + has_dat = !isempty(wing_settings.dat_file) + + if has_yaml && (has_obj || has_dat) + throw(ArgumentError( + "Cannot specify both geometry_file and obj_file/dat_file" + )) + end + + if has_obj && !has_dat + throw(ArgumentError( + "obj_file requires dat_file to be specified" + )) + end + + if has_dat && !has_obj + throw(ArgumentError( + "dat_file requires obj_file to be specified" + )) + end + + if has_yaml + # Use YAML geometry constructor + Wing(wing_settings.geometry_file; + n_panels=wing_settings.n_panels, + n_groups=wing_settings.n_groups, + spanwise_distribution=wing_settings.spanwise_panel_distribution, + remove_nan=wing_settings.remove_nan + ) + elseif has_obj && has_dat + # Use ObjWing constructor + ObjWing( + wing_settings.obj_file, + wing_settings.dat_file; + n_panels=wing_settings.n_panels, + n_groups=wing_settings.n_groups, + spanwise_distribution=wing_settings.spanwise_panel_distribution, + spanwise_direction=wing_settings.spanwise_direction, + remove_nan=wing_settings.remove_nan + ) + else + throw(ArgumentError( + "WingSettings must specify either geometry_file or " * + "both obj_file and dat_file" + )) + end end diff --git a/test/settings/test_settings.jl b/test/settings/test_settings.jl index c7412894..80126bcf 100644 --- a/test/settings/test_settings.jl +++ b/test/settings/test_settings.jl @@ -16,6 +16,6 @@ using Test @test vss.wings isa Vector{WingSettings} @test length(vss.wings) == 2 io = IOBuffer(repr(vss)) - @test countlines(io) == 40 # Updated to match new output format + @test countlines(io) == 44 # Updated to match new output format end nothing diff --git a/test/yaml_geometry/test_yaml_wing_deformation.jl b/test/yaml_geometry/test_yaml_wing_deformation.jl index da931177..2631f2cc 100644 --- a/test/yaml_geometry/test_yaml_wing_deformation.jl +++ b/test/yaml_geometry/test_yaml_wing_deformation.jl @@ -29,8 +29,8 @@ using Test @test !isapprox(original_te_point, deformed_te_point, atol=1e-2) @test deformed_te_point[3] < original_te_point[3] # TE should move down with positive twist - # LE point should also change due to twist - @test !isapprox(original_le_point, deformed_le_point, atol=1e-2) + # LE point stays fixed (deform! rotates TE around LE) + @test isapprox(original_le_point, deformed_le_point, atol=1e-4) # Check delta is set correctly @test body_aero.panels[i].delta ≈ deg2rad(5.0) @@ -78,9 +78,10 @@ using Test deformed_te = body_aero.panels[i].TE_point_1 deformed_le = body_aero.panels[i].LE_point_1 - # Points should have changed + # TE points should have changed due to deformation @test !isapprox(original_points[idx].TE, deformed_te, atol=1e-2) - @test !isapprox(original_points[idx].LE, deformed_le, atol=1e-2) + # LE points stay fixed (deform! rotates TE around LE) + @test isapprox(original_points[idx].LE, deformed_le, atol=1e-4) end # Check that the deformation is applied correctly @@ -116,7 +117,8 @@ using Test # Wing should still be valid after multiple reinits @test wing.sections[1].aero_data !== nothing - @test length(wing.sections) == 2 + # Note: For deformation support, wing.sections now points to refined_sections + @test length(wing.sections) == wing.n_panels + 1 end @testset "Deformation with BodyAerodynamics Reinit" begin From 7a7e2038095fe8daeb4ca8d11dbaf0e03b61a19a Mon Sep 17 00:00:00 2001 From: 1-Bart-1 Date: Fri, 7 Nov 2025 13:09:45 +0100 Subject: [PATCH 11/53] Old tests not failing --- src/wing_geometry.jl | 32 +++++++++++++++++-- .../test_yaml_wing_deformation.jl | 11 +++++-- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/src/wing_geometry.jl b/src/wing_geometry.jl index a1efc422..4ce34568 100644 --- a/src/wing_geometry.jl +++ b/src/wing_geometry.jl @@ -450,7 +450,7 @@ function deform!(wing::Wing) local_y .= normalize(section1.LE_point - section2.LE_point) chord .= section1.TE_point .- section1.LE_point normal .= chord × local_y - @. wing.sections[i].TE_point = section1.LE_point + cos(wing.theta_dist[i]) * chord - sin(wing.theta_dist[i]) * normal + @. wing.refined_sections[i].TE_point = section1.LE_point + cos(wing.theta_dist[i]) * chord - sin(wing.theta_dist[i]) * normal end return nothing end @@ -528,6 +528,28 @@ function flip_created_coord_in_pairs_if_needed!(coord::Matrix{Float64}) end +""" + update_non_deformed_sections!(wing::AbstractWing) + +Create non_deformed_sections to match refined_sections. +This enables deformation support for all wings (YAML and OBJ). +Should be called after refined_sections are populated for the FIRST time only. +Once populated, non_deformed_sections serves as the undeformed reference geometry. +""" +function update_non_deformed_sections!(wing::AbstractWing) + n_sections = wing.n_panels + 1 + + # Only populate non_deformed_sections if it's empty (initial setup) + # Once populated, it serves as the undeformed reference and should not be overwritten + if isempty(wing.non_deformed_sections) + wing.non_deformed_sections = [Section() for _ in 1:n_sections] + for i in 1:n_sections + reinit!(wing.non_deformed_sections[i], wing.refined_sections[i]) + end + end + return nothing +end + """ refine_aerodynamic_mesh!(wing::AbstractWing) @@ -542,6 +564,7 @@ function refine_aerodynamic_mesh!(wing::AbstractWing) if length(wing.refined_sections) == 0 if wing.spanwise_distribution == UNCHANGED || length(wing.sections) == n_sections wing.refined_sections = wing.sections + update_non_deformed_sections!(wing) return nothing else wing.refined_sections = Section[Section() for _ in 1:wing.n_panels+1] @@ -573,6 +596,7 @@ function refine_aerodynamic_mesh!(wing::AbstractWing) reinit!(wing.refined_sections[i], wing.sections[i]) end compute_refined_panel_mapping!(wing) + update_non_deformed_sections!(wing) return nothing end @@ -583,6 +607,7 @@ function refine_aerodynamic_mesh!(wing::AbstractWing) reinit!(wing.refined_sections[1], LE[1,:], TE[1,:], aero_model[1], aero_data[1]) reinit!(wing.refined_sections[2], LE[end,:], TE[end,:], aero_model[end], aero_data[end]) compute_refined_panel_mapping!(wing) + update_non_deformed_sections!(wing) return nothing end @@ -619,6 +644,9 @@ function refine_aerodynamic_mesh!(wing::AbstractWing) end end + # Create/update non_deformed_sections to match refined_sections + update_non_deformed_sections!(wing) + return nothing end @@ -1116,4 +1144,4 @@ function Base.getproperty(w::AbstractWing, s::Symbol) else return getfield(w, s) end -end \ No newline at end of file +end diff --git a/test/yaml_geometry/test_yaml_wing_deformation.jl b/test/yaml_geometry/test_yaml_wing_deformation.jl index 2631f2cc..75d64642 100644 --- a/test/yaml_geometry/test_yaml_wing_deformation.jl +++ b/test/yaml_geometry/test_yaml_wing_deformation.jl @@ -2,6 +2,12 @@ using VortexStepMethod using LinearAlgebra using Test +# Load TestSupport if not already loaded (for standalone execution) +if !@isdefined(TestSupport) + include(joinpath(@__DIR__, "..", "TestSupport.jl")) + using .TestSupport +end + @testset "YAML Wing Deformation Tests" begin @testset "Simple Wing Deformation" begin # Load existing simple_wing.yaml @@ -117,8 +123,9 @@ using Test # Wing should still be valid after multiple reinits @test wing.sections[1].aero_data !== nothing - # Note: For deformation support, wing.sections now points to refined_sections - @test length(wing.sections) == wing.n_panels + 1 + # Verify refined_sections and non_deformed_sections are properly populated + @test length(wing.refined_sections) == wing.n_panels + 1 + @test length(wing.non_deformed_sections) == wing.n_panels + 1 end @testset "Deformation with BodyAerodynamics Reinit" begin From 2ef5c3ed581cbeec08ca1e614c71d0386e61e1ba Mon Sep 17 00:00:00 2001 From: 1-Bart-1 Date: Fri, 7 Nov 2025 15:03:13 +0100 Subject: [PATCH 12/53] Passing tests --- src/body_aerodynamics.jl | 9 ++++++--- src/wing_geometry.jl | 16 ++++++++++++++-- .../test_yaml_wing_deformation.jl | 18 ++++++++++-------- 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/src/body_aerodynamics.jl b/src/body_aerodynamics.jl index 9e09f54c..103745dc 100644 --- a/src/body_aerodynamics.jl +++ b/src/body_aerodynamics.jl @@ -114,7 +114,7 @@ function Base.setproperty!(obj::BodyAerodynamics, sym::Symbol, val) end """ - reinit!(body_aero::BodyAerodynamics; init_aero, va, omega) + reinit!(body_aero::BodyAerodynamics; init_aero, va, omega, refine_mesh) Initialize a BodyAerodynamics struct in-place by setting up panels and coefficients. @@ -125,6 +125,8 @@ Initialize a BodyAerodynamics struct in-place by setting up panels and coefficie - `init_aero::Bool`: Wether to initialize the aero data or not - `va=[15.0, 0.0, 0.0]`: Apparent wind vector - `omega=zeros(3)`: Turn rate in kite body frame x y and z +- `refine_mesh=true`: Whether to refine wing meshes. Set to `false` after + `deform!()` to preserve deformed geometry. # Returns nothing @@ -132,12 +134,13 @@ nothing function reinit!(body_aero::BodyAerodynamics; init_aero=true, va=[15.0, 0.0, 0.0], - omega=zeros(MVec3) + omega=zeros(MVec3), + refine_mesh=true ) idx = 1 vec = @MVector zeros(3) for wing in body_aero.wings - reinit!(wing) + reinit!(wing; refine_mesh=refine_mesh) panel_props = wing.panel_props # Create panels diff --git a/src/wing_geometry.jl b/src/wing_geometry.jl index 4ce34568..79554cb4 100644 --- a/src/wing_geometry.jl +++ b/src/wing_geometry.jl @@ -301,8 +301,20 @@ function Wing(n_panels::Int; ) end -function reinit!(wing::AbstractWing) - refine_aerodynamic_mesh!(wing) +""" + reinit!(wing::AbstractWing; refine_mesh=true) + +Reinitialize wing geometry and panel properties. + +# Keyword Arguments +- `refine_mesh::Bool=true`: Whether to refine the mesh. Set to `false` after + `deform!()` to preserve deformed geometry while updating panel properties. +""" +function reinit!(wing::AbstractWing; refine_mesh=true) + # Refine mesh unless explicitly disabled (e.g., to preserve deformation) + if refine_mesh + refine_aerodynamic_mesh!(wing) + end # Calculate panel properties update_panel_properties!( diff --git a/test/yaml_geometry/test_yaml_wing_deformation.jl b/test/yaml_geometry/test_yaml_wing_deformation.jl index 75d64642..580e3dc9 100644 --- a/test/yaml_geometry/test_yaml_wing_deformation.jl +++ b/test/yaml_geometry/test_yaml_wing_deformation.jl @@ -25,7 +25,7 @@ end delta_dist = fill(deg2rad(5.0), wing.n_panels) # 5 degrees trailing edge deflection VortexStepMethod.deform!(wing, theta_dist, delta_dist) - VortexStepMethod.reinit!(body_aero) + VortexStepMethod.reinit!(body_aero; refine_mesh=false) # Check if TE point changed after deformation deformed_te_point = copy(body_aero.panels[i].TE_point_1) @@ -46,7 +46,7 @@ end zero_delta_dist = zeros(wing.n_panels) VortexStepMethod.deform!(wing, zero_theta_dist, zero_delta_dist) - VortexStepMethod.reinit!(body_aero) + VortexStepMethod.reinit!(body_aero; refine_mesh=false) # Check if TE point returned to original position reset_te_point = copy(body_aero.panels[i].TE_point_1) @@ -77,7 +77,7 @@ end delta_dist = [deg2rad(-5.0 + 10.0 * i / wing.n_panels) for i in 1:wing.n_panels] # Varying deflection VortexStepMethod.deform!(wing, theta_dist, delta_dist) - VortexStepMethod.reinit!(body_aero) + VortexStepMethod.reinit!(body_aero; refine_mesh=false) # Check that different panels have different deformations for (idx, i) in enumerate(test_indices) @@ -96,7 +96,7 @@ end # Reset and verify VortexStepMethod.deform!(wing, zeros(wing.n_panels), zeros(wing.n_panels)) - VortexStepMethod.reinit!(body_aero) + VortexStepMethod.reinit!(body_aero; refine_mesh=false) for (idx, i) in enumerate(test_indices) reset_te = body_aero.panels[i].TE_point_1 @@ -138,6 +138,7 @@ end theta_dist = fill(deg2rad(15.0), wing.n_panels) delta_dist = fill(deg2rad(3.0), wing.n_panels) VortexStepMethod.deform!(wing, theta_dist, delta_dist) + VortexStepMethod.reinit!(body_aero; refine_mesh=false) # Store state after deformation i = length(body_aero.panels) ÷ 2 @@ -147,7 +148,8 @@ end VortexStepMethod.reinit!(body_aero; va=zeros(3), omega=zeros(3), - init_aero=true + init_aero=true, + refine_mesh=false ) end @@ -162,7 +164,7 @@ end # Test zero deformation VortexStepMethod.deform!(wing, zeros(wing.n_panels), zeros(wing.n_panels)) - VortexStepMethod.reinit!(body_aero) + VortexStepMethod.reinit!(body_aero; refine_mesh=false) @test all(p.delta ≈ 0.0 for p in body_aero.panels) # Test large deformation angles @@ -171,14 +173,14 @@ end # Should not error even with large angles VortexStepMethod.deform!(wing, theta_dist, delta_dist) - VortexStepMethod.reinit!(body_aero) + VortexStepMethod.reinit!(body_aero; refine_mesh=false) @test all(p.delta ≈ deg2rad(30.0) for p in body_aero.panels) # Test negative angles theta_dist = fill(deg2rad(-20.0), wing.n_panels) delta_dist = fill(deg2rad(-10.0), wing.n_panels) VortexStepMethod.deform!(wing, theta_dist, delta_dist) - VortexStepMethod.reinit!(body_aero) + VortexStepMethod.reinit!(body_aero; refine_mesh=false) @test all(p.delta ≈ deg2rad(-10.0) for p in body_aero.panels) end end From dad4e72a9ae82d6456b077379b9f0d9bc9a32214 Mon Sep 17 00:00:00 2001 From: 1-Bart-1 Date: Fri, 7 Nov 2025 15:50:13 +0100 Subject: [PATCH 13/53] Add cl cd cm group array --- src/solver.jl | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/src/solver.jl b/src/solver.jl index 7f4daec3..742d921b 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -51,6 +51,9 @@ Struct for storing the solution of the [solve!](@ref) function. Must contain all moment_coeff_dist::MVector{P, Float64} = zeros(P) group_moment_dist::MVector{G, Float64} = zeros(G) group_moment_coeff_dist::MVector{G, Float64} = zeros(G) + cl_group_array::MVector{G, Float64} = zeros(G) + cd_group_array::MVector{G, Float64} = zeros(G) + cm_group_array::MVector{G, Float64} = zeros(G) solver_status::SolverStatus = FAILURE end @@ -296,8 +299,14 @@ function solve!(solver::Solver, body_aero::BodyAerodynamics, gamma_distribution= if length(solver.sol.group_moment_dist) > 0 group_moment_dist = solver.sol.group_moment_dist group_moment_coeff_dist = solver.sol.group_moment_coeff_dist + cl_group_array = solver.sol.cl_group_array + cd_group_array = solver.sol.cd_group_array + cm_group_array = solver.sol.cm_group_array group_moment_dist .= 0.0 group_moment_coeff_dist .= 0.0 + cl_group_array .= 0.0 + cd_group_array .= 0.0 + cm_group_array .= 0.0 panel_idx = 1 group_idx = 1 for wing in body_aero.wings @@ -306,21 +315,46 @@ function solve!(solver::Solver, body_aero::BodyAerodynamics, gamma_distribution= # Original method: divide panels into equally-sized sequential groups panels_per_group = wing.n_panels ÷ wing.n_groups for _ in 1:wing.n_groups + panel_count = 0 for _ in 1:panels_per_group group_moment_dist[group_idx] += moment_dist[panel_idx] group_moment_coeff_dist[group_idx] += moment_coeff_dist[panel_idx] + cl_group_array[group_idx] += solver.sol.cl_array[panel_idx] + cd_group_array[group_idx] += solver.sol.cd_array[panel_idx] + cm_group_array[group_idx] += solver.sol.cm_array[panel_idx] panel_idx += 1 + panel_count += 1 end + # Average the coefficients over panels in the group + cl_group_array[group_idx] /= panel_count + cd_group_array[group_idx] /= panel_count + cm_group_array[group_idx] /= panel_count group_idx += 1 end elseif wing.grouping_method == REFINE # REFINE method: group refined panels by their original unrefined section + # First pass: accumulate values + group_panel_counts = zeros(Int, wing.n_groups) for local_panel_idx in 1:wing.n_panels original_section_idx = wing.refined_panel_mapping[local_panel_idx] - group_moment_dist[group_idx + original_section_idx - 1] += moment_dist[panel_idx] - group_moment_coeff_dist[group_idx + original_section_idx - 1] += moment_coeff_dist[panel_idx] + target_group_idx = group_idx + original_section_idx - 1 + group_moment_dist[target_group_idx] += moment_dist[panel_idx] + group_moment_coeff_dist[target_group_idx] += moment_coeff_dist[panel_idx] + cl_group_array[target_group_idx] += solver.sol.cl_array[panel_idx] + cd_group_array[target_group_idx] += solver.sol.cd_array[panel_idx] + cm_group_array[target_group_idx] += solver.sol.cm_array[panel_idx] + group_panel_counts[original_section_idx] += 1 panel_idx += 1 end + # Second pass: average coefficients + for i in 1:wing.n_groups + target_group_idx = group_idx + i - 1 + if group_panel_counts[i] > 0 + cl_group_array[target_group_idx] /= group_panel_counts[i] + cd_group_array[target_group_idx] /= group_panel_counts[i] + cm_group_array[target_group_idx] /= group_panel_counts[i] + end + end group_idx += wing.n_groups end else From 1438b5572c521b247a564cfb70d8205f3b5b2c92 Mon Sep 17 00:00:00 2001 From: 1-Bart-1 Date: Fri, 7 Nov 2025 16:23:17 +0100 Subject: [PATCH 14/53] Added tests and settings --- data/ram_air_kite/vsm_settings.yaml | 4 + src/settings.jl | 4 + test/runtests.jl | 1 + test/solver/test_group_coefficients.jl | 184 +++++++++++++++++++++++++ 4 files changed, 193 insertions(+) create mode 100644 test/solver/test_group_coefficients.jl diff --git a/data/ram_air_kite/vsm_settings.yaml b/data/ram_air_kite/vsm_settings.yaml index 5ae66dc9..8f726240 100644 --- a/data/ram_air_kite/vsm_settings.yaml +++ b/data/ram_air_kite/vsm_settings.yaml @@ -10,6 +10,9 @@ PanelDistribution: InitialGammaDistribution: ELLIPTIC: Elliptic distribution ZEROS: Constant distribution +PanelGroupingMethod: + EQUAL_SIZE: Divide panels into equally-sized sequential groups + REFINE: Group refined panels by their original unrefined section wings: - name: main_wing @@ -17,6 +20,7 @@ wings: n_groups: 40 spanwise_panel_distribution: LINEAR spanwise_direction: [0.0, 1.0, 0.0] + grouping_method: EQUAL_SIZE remove_nan: true solver_settings: n_panels: 40 diff --git a/src/settings.jl b/src/settings.jl index ad3fb75f..5916da52 100644 --- a/src/settings.jl +++ b/src/settings.jl @@ -14,6 +14,7 @@ end n_groups::Int64 = 40 spanwise_panel_distribution::PanelDistribution = LINEAR spanwise_direction::MVec3 = [0.0, 1.0, 0.0] + grouping_method::PanelGroupingMethod = EQUAL_SIZE remove_nan = true end @@ -80,6 +81,9 @@ function VSMSettings(filename; data_prefix=true) wing.n_groups = wing_data["n_groups"] wing.spanwise_panel_distribution = eval(Symbol(wing_data["spanwise_panel_distribution"])) wing.spanwise_direction = MVec3(wing_data["spanwise_direction"]) + if haskey(wing_data, "grouping_method") + wing.grouping_method = eval(Symbol(wing_data["grouping_method"])) + end wing.remove_nan = wing_data["remove_nan"] push!(vsm_settings.wings, wing) diff --git a/test/runtests.jl b/test/runtests.jl index 6c6f92cd..520eeab3 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -28,6 +28,7 @@ end::Bool include("ram_geometry/test_kite_geometry.jl") include("settings/test_settings.jl") include("solver/test_solver.jl") + include("solver/test_group_coefficients.jl") include("VortexStepMethod/test_VortexStepMethod.jl") include("wake/test_wake.jl") include("wing_geometry/test_wing_geometry.jl") diff --git a/test/solver/test_group_coefficients.jl b/test/solver/test_group_coefficients.jl new file mode 100644 index 00000000..7eae60ef --- /dev/null +++ b/test/solver/test_group_coefficients.jl @@ -0,0 +1,184 @@ +using VortexStepMethod +using LinearAlgebra +using Test + +# Load test support +include("../TestSupport.jl") +using .TestSupport + +@testset "Group Coefficient Arrays Tests" begin + @testset "Group coefficients with EQUAL_SIZE method" begin + # Create a simple wing with groups + n_panels = 20 + n_groups = 4 + + # Create a test wing settings file + settings_file = create_temp_wing_settings("solver", "solver_test_wing.yaml"; alpha=5.0, beta=0.0, wind_speed=10.0) + + try + # Modify settings to use specific panel/group configuration + settings = VSMSettings(settings_file) + settings.wings[1].n_panels = n_panels + settings.wings[1].n_groups = n_groups + settings.wings[1].grouping_method = EQUAL_SIZE + settings.solver_settings.n_panels = n_panels + settings.solver_settings.n_groups = n_groups + + # Create wing and solver + wing = Wing(settings) + body_aero = BodyAerodynamics([wing]) + solver = Solver(body_aero, settings) + + # Set conditions and solve + va = [10.0, 0.0, 0.0] + set_va!(body_aero, va) + sol = solve!(solver, body_aero) + + # Test 1: Group arrays exist and have correct size + @test length(sol.cl_group_array) == n_groups + @test length(sol.cd_group_array) == n_groups + @test length(sol.cm_group_array) == n_groups + + # Test 2: Group arrays are not all zeros (solver computed them) + @test !all(sol.cl_group_array .== 0.0) + @test !all(sol.cd_group_array .== 0.0) + + # Test 3: Verify group coefficients are averages of panel coefficients + panels_per_group = n_panels ÷ n_groups + for group_idx in 1:n_groups + panel_start = (group_idx - 1) * panels_per_group + 1 + panel_end = group_idx * panels_per_group + + # Calculate expected average from panel coefficients + expected_cl = sum(sol.cl_array[panel_start:panel_end]) / panels_per_group + expected_cd = sum(sol.cd_array[panel_start:panel_end]) / panels_per_group + expected_cm = sum(sol.cm_array[panel_start:panel_end]) / panels_per_group + + # Check if group coefficients match expected averages + # Handle NaN values that can occur in INVISCID models + if isnan(expected_cl) + @test isnan(sol.cl_group_array[group_idx]) + else + @test isapprox(sol.cl_group_array[group_idx], expected_cl, rtol=1e-10) + end + if isnan(expected_cd) + @test isnan(sol.cd_group_array[group_idx]) + else + @test isapprox(sol.cd_group_array[group_idx], expected_cd, rtol=1e-10) + end + if isnan(expected_cm) + @test isnan(sol.cm_group_array[group_idx]) + else + @test isapprox(sol.cm_group_array[group_idx], expected_cm, rtol=1e-10) + end + end + + # Test 4: Verify physical consistency (lift coefficients should be positive at positive AoA) + # Skip test if values are NaN + if !any(isnan.(sol.cl_group_array)) + @test all(sol.cl_group_array .> 0.0) + end + + finally + rm(settings_file; force=true) + end + end + + @testset "Group coefficients with n_groups=0 (no grouping)" begin + # Create a wing with no groups + n_panels = 20 + n_groups = 0 + + settings_file = create_temp_wing_settings("solver", "solver_test_wing.yaml"; alpha=5.0, beta=0.0, wind_speed=10.0) + + try + settings = VSMSettings(settings_file) + settings.wings[1].n_panels = n_panels + settings.wings[1].n_groups = n_groups + settings.solver_settings.n_panels = n_panels + settings.solver_settings.n_groups = n_groups + + wing = Wing(settings) + body_aero = BodyAerodynamics([wing]) + solver = Solver(body_aero, settings) + + va = [10.0, 0.0, 0.0] + set_va!(body_aero, va) + sol = solve!(solver, body_aero) + + # Test: Group arrays should be empty when n_groups=0 + @test length(sol.cl_group_array) == 0 + @test length(sol.cd_group_array) == 0 + @test length(sol.cm_group_array) == 0 + + finally + rm(settings_file; force=true) + end + end + + @testset "Group coefficients with different group sizes" begin + # Test with various panel/group combinations + test_cases = [ + (n_panels=40, n_groups=8), + (n_panels=30, n_groups=5), + (n_panels=24, n_groups=6), + ] + + for (n_panels, n_groups) in test_cases + settings_file = create_temp_wing_settings("solver", "solver_test_wing.yaml"; alpha=5.0, beta=0.0, wind_speed=10.0) + + try + settings = VSMSettings(settings_file) + settings.wings[1].n_panels = n_panels + settings.wings[1].n_groups = n_groups + settings.wings[1].grouping_method = EQUAL_SIZE + settings.solver_settings.n_panels = n_panels + settings.solver_settings.n_groups = n_groups + + wing = Wing(settings) + body_aero = BodyAerodynamics([wing]) + solver = Solver(body_aero, settings) + + va = [10.0, 0.0, 0.0] + set_va!(body_aero, va) + sol = solve!(solver, body_aero) + + # Verify arrays have correct size + @test length(sol.cl_group_array) == n_groups + @test length(sol.cd_group_array) == n_groups + @test length(sol.cm_group_array) == n_groups + + # Verify group coefficients are computed correctly + panels_per_group = n_panels ÷ n_groups + for group_idx in 1:n_groups + panel_start = (group_idx - 1) * panels_per_group + 1 + panel_end = group_idx * panels_per_group + + expected_cl = sum(sol.cl_array[panel_start:panel_end]) / panels_per_group + expected_cd = sum(sol.cd_array[panel_start:panel_end]) / panels_per_group + expected_cm = sum(sol.cm_array[panel_start:panel_end]) / panels_per_group + + # Handle NaN for all coefficients + if isnan(expected_cl) + @test isnan(sol.cl_group_array[group_idx]) + else + @test isapprox(sol.cl_group_array[group_idx], expected_cl, rtol=1e-10) + end + if isnan(expected_cd) + @test isnan(sol.cd_group_array[group_idx]) + else + @test isapprox(sol.cd_group_array[group_idx], expected_cd, rtol=1e-10) + end + if isnan(expected_cm) + @test isnan(sol.cm_group_array[group_idx]) + else + @test isapprox(sol.cm_group_array[group_idx], expected_cm, rtol=1e-10) + end + end + + finally + rm(settings_file; force=true) + end + end + end +end From 80fc35760d0c3c95923f74b25679f5a85eb292d3 Mon Sep 17 00:00:00 2001 From: 1-Bart-1 Date: Sat, 8 Nov 2025 13:59:55 +0100 Subject: [PATCH 15/53] Improved obj file reading --- src/obj_geometry.jl | 103 ++++++++++++++---- src/wing_geometry.jl | 3 +- test/Project.toml | 1 + test/TestSupport.jl | 5 - test/ram_geometry/test_kite_geometry.jl | 58 ++++++++-- test/runtests.jl | 3 +- test/settings/test_settings.jl | 2 +- test/solver/test_group_coefficients.jl | 4 - .../test_yaml_wing_deformation.jl | 6 - 9 files changed, 136 insertions(+), 49 deletions(-) delete mode 100644 test/TestSupport.jl diff --git a/src/obj_geometry.jl b/src/obj_geometry.jl index 1dae4cbc..dcf2a2e3 100644 --- a/src/obj_geometry.jl +++ b/src/obj_geometry.jl @@ -112,7 +112,8 @@ Create interpolation functions for leading/trailing edges and area. - Where le_interp and te_interp are tuples themselves, containing the x, y and z interpolations """ function create_interpolations(vertices, circle_center_z, radius, gamma_tip, R=I(3); interp_steps=40) - gamma_range = range(-gamma_tip+1e-6, gamma_tip-1e-6, interp_steps) + gamma_range = range(-gamma_tip+gamma_tip/interp_steps*2, + gamma_tip-gamma_tip/interp_steps*2, interp_steps) stepsize = gamma_range.step.hi vz_centered = [v[3] - circle_center_z for v in vertices] @@ -122,33 +123,91 @@ function create_interpolations(vertices, circle_center_z, radius, gamma_tip, R=I leading_edges = zeros(3, length(gamma_range)) areas = zeros(length(gamma_range)) + n_slices = length(gamma_range) for (j, gamma) in enumerate(gamma_range) trailing_edges[1, j] = -Inf leading_edges[1, j] = Inf - for (i, v) in enumerate(vertices) - # Rotate y coordinate to check box containment - # rotated_y = v[2] * cos(-gamma) - vz_centered[i] * sin(-gamma) - gamma_v = atan(-v[2], vz_centered[i]) - if gamma ≤ 0 && gamma - stepsize ≤ gamma_v ≤ gamma - if v[1] > trailing_edges[1, j] - trailing_edges[:, j] .= v - te_gammas[j] = gamma_v - end - if v[1] < leading_edges[1, j] - leading_edges[:, j] .= v - le_gammas[j] = gamma_v + + # Determine if this is a tip slice and get search parameters + is_first_tip = (j == 1) + is_last_tip = (j == n_slices) + + if is_first_tip || is_last_tip + # Tip slices: use directional search within adjacent slice region + gamma_search = is_first_tip ? gamma_range[1] : gamma_range[end] + max_te_score = -Inf + max_le_score = -Inf + + for (i, v) in enumerate(vertices) + gamma_v = atan(-v[2], vz_centered[i]) + + # Check if vertex is in the adjacent slice region + in_range = if gamma_search ≤ 0 + gamma_search - stepsize ≤ gamma_v ≤ gamma_search + else + gamma_search ≤ gamma_v ≤ gamma_search + stepsize end - elseif gamma > 0 && gamma ≤ gamma_v ≤ gamma + stepsize - if v[1] > trailing_edges[1, j] - trailing_edges[:, j] .= v - te_gammas[j] = gamma_v + + if in_range + if is_first_tip + # TE: furthest in [X, Y, -Z] direction + te_score = v[1] + v[2] - v[3] + if te_score > max_te_score + trailing_edges[:, j] .= v + te_gammas[j] = gamma_v + max_te_score = te_score + end + # LE: furthest in [-X, Y, -Z] direction + le_score = -v[1] + v[2] - v[3] + if le_score > max_le_score + leading_edges[:, j] .= v + le_gammas[j] = gamma_v + max_le_score = le_score + end + else # is_last_tip + # TE: furthest in [X, -Y, -Z] direction + te_score = v[1] - v[2] - v[3] + if te_score > max_te_score + trailing_edges[:, j] .= v + te_gammas[j] = gamma_v + max_te_score = te_score + end + # LE: furthest in [-X, -Y, -Z] direction + le_score = -v[1] - v[2] - v[3] + if le_score > max_le_score + leading_edges[:, j] .= v + le_gammas[j] = gamma_v + max_le_score = le_score + end + end end - if v[1] < leading_edges[1, j] - leading_edges[:, j] .= v - le_gammas[j] = gamma_v + end + else + # Interior slices: use standard min/max x-coordinate search + for (i, v) in enumerate(vertices) + gamma_v = atan(-v[2], vz_centered[i]) + if gamma ≤ 0 && gamma - stepsize ≤ gamma_v ≤ gamma + if v[1] > trailing_edges[1, j] + trailing_edges[:, j] .= v + te_gammas[j] = gamma_v + end + if v[1] < leading_edges[1, j] + leading_edges[:, j] .= v + le_gammas[j] = gamma_v + end + elseif gamma > 0 && gamma ≤ gamma_v ≤ gamma + stepsize + if v[1] > trailing_edges[1, j] + trailing_edges[:, j] .= v + te_gammas[j] = gamma_v + end + if v[1] < leading_edges[1, j] + leading_edges[:, j] .= v + le_gammas[j] = gamma_v + end end end end + area = norm(leading_edges[:, j] - trailing_edges[:, j]) * stepsize * radius last_area = j > 1 ? areas[j-1] : 0.0 areas[j] = last_area + area @@ -159,9 +218,9 @@ function create_interpolations(vertices, circle_center_z, radius, gamma_tip, R=I trailing_edges[:, j] .= R * trailing_edges[:, j] end - le_interp = ntuple(i -> linear_interpolation(te_gammas, leading_edges[i, :], + le_interp = ntuple(i -> linear_interpolation(le_gammas, leading_edges[i, :], extrapolation_bc=Line()), 3) - te_interp = ntuple(i -> linear_interpolation(le_gammas, trailing_edges[i, :], + te_interp = ntuple(i -> linear_interpolation(te_gammas, trailing_edges[i, :], extrapolation_bc=Line()), 3) area_interp = linear_interpolation(gamma_range, areas, extrapolation_bc=Line()) diff --git a/src/wing_geometry.jl b/src/wing_geometry.jl index 79554cb4..6ac62f48 100644 --- a/src/wing_geometry.jl +++ b/src/wing_geometry.jl @@ -462,7 +462,8 @@ function deform!(wing::Wing) local_y .= normalize(section1.LE_point - section2.LE_point) chord .= section1.TE_point .- section1.LE_point normal .= chord × local_y - @. wing.refined_sections[i].TE_point = section1.LE_point + cos(wing.theta_dist[i]) * chord - sin(wing.theta_dist[i]) * normal + @. wing.refined_sections[i].TE_point = section1.LE_point + + cos(wing.theta_dist[i]) * chord - sin(wing.theta_dist[i]) * normal end return nothing end diff --git a/test/Project.toml b/test/Project.toml index 9ede8c7a..7b362c6d 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -13,6 +13,7 @@ Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +VortexStepMethod = "ed3cd733-9f0f-46a9-93e0-89b8d4998dd9" YAML = "ddb6d928-2868-570f-bddf-ab3f9cf99eb6" [compat] diff --git a/test/TestSupport.jl b/test/TestSupport.jl deleted file mode 100644 index 4b428502..00000000 --- a/test/TestSupport.jl +++ /dev/null @@ -1,5 +0,0 @@ -module TestSupport -export suppress_warnings, test_data_path, create_temp_wing_settings, - get_standard_wing_file, get_complete_settings_file -include("test_data_utils.jl") -end \ No newline at end of file diff --git a/test/ram_geometry/test_kite_geometry.jl b/test/ram_geometry/test_kite_geometry.jl index 47dee1d4..861bc880 100644 --- a/test/ram_geometry/test_kite_geometry.jl +++ b/test/ram_geometry/test_kite_geometry.jl @@ -189,18 +189,18 @@ using Serialization # Create an ObjWing for testing wing = ObjWing(test_obj_path, test_dat_path; remove_nan=true) body_aero = BodyAerodynamics([wing]) - + # Store original TE point for comparison i = length(body_aero.panels) ÷ 2 original_te_point = copy(body_aero.panels[i].TE_point_1) - + # Apply deformation with non-zero angles theta_dist = fill(deg2rad(30.0), wing.n_panels) # 10 degrees twist delta_dist = fill(deg2rad(5.0), wing.n_panels) # 5 degrees trailing edge deflection - + VortexStepMethod.deform!(wing, theta_dist, delta_dist) VortexStepMethod.reinit!(body_aero) - + # Check if TE point changed after deformation deformed_te_point = copy(body_aero.panels[i].TE_point_1) @test !isapprox(original_te_point, deformed_te_point, atol=1e-2) @@ -208,19 +208,61 @@ using Serialization @test deformed_te_point[2] ≈ original_te_point[2] atol=1e-2 # right hand rule @test deformed_te_point[1] < original_te_point[1] # right hand rule @test body_aero.panels[i].delta ≈ deg2rad(5.0) - + # Reset deformation with zero angles zero_theta_dist = zeros(wing.n_panels) zero_delta_dist = zeros(wing.n_panels) - + VortexStepMethod.deform!(wing, zero_theta_dist, zero_delta_dist) VortexStepMethod.reinit!(body_aero) - + # Check if TE point returned to original position reset_te_point = copy(body_aero.panels[i].TE_point_1) @test original_te_point ≈ reset_te_point atol=1e-4 end + + @testset "First and Last Section Deformation with group_deform!" begin + # Create an ObjWing with a small number of panels and groups + wing = ObjWing(test_obj_path, test_dat_path; + n_panels=4, n_groups=2, remove_nan=true) + + # Store original TE points from all refined_sections + # Wing has n_panels+1 sections (5 sections for 4 panels) + n_sections = wing.n_panels + 1 + original_te_points = [copy(wing.refined_sections[i].TE_point) + for i in 1:n_sections] + + @show wing.refined_sections[1].LE_point + @show wing.refined_sections[1].TE_point + @show wing.refined_sections[end].LE_point + @show wing.refined_sections[end].TE_point + + # Apply group_deform! with non-zero angles (2 groups, each controlling 2 panels) + theta_angles = [deg2rad(15.0), deg2rad(20.0)] + delta_angles = [deg2rad(5.0), deg2rad(10.0)] + + VortexStepMethod.group_deform!(wing, theta_angles, delta_angles; smooth=false) + + # Check that all sections' TE points have been deformed + for i in 1:n_sections + deformed_te = wing.refined_sections[i].TE_point + original_te = original_te_points[i] + + if i == 1 + # First section should be deformed + @test !isapprox(original_te, deformed_te, atol=1e-6) + @info "Section 1 (first): original=$original_te, deformed=$deformed_te" + elseif i == n_sections + # Last section (n_panels+1) should be deformed + @test !isapprox(original_te, deformed_te, atol=1e-6) + @info "Section $(n_sections) (last): original=$original_te, deformed=$deformed_te" + else + # Intermediate sections should also be deformed + @test !isapprox(original_te, deformed_te, atol=1e-6) + end + end + end rm(test_obj_path) rm(test_dat_path) -end \ No newline at end of file +end diff --git a/test/runtests.jl b/test/runtests.jl index 520eeab3..f89bea03 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2,8 +2,7 @@ using Test, VortexStepMethod # Make paths robust (avoid cd("..")) cd(@__DIR__) # ensure we're in test/ no matter how tests are launched -include("TestSupport.jl") -using .TestSupport +include("test_data_utils.jl") println("Running tests...") diff --git a/test/settings/test_settings.jl b/test/settings/test_settings.jl index 37b3d920..e3200766 100644 --- a/test/settings/test_settings.jl +++ b/test/settings/test_settings.jl @@ -11,6 +11,6 @@ using Test @test vss.wings isa Vector{WingSettings} @test length(vss.wings) == 2 io = IOBuffer(repr(vss)) - @test countlines(io) == 44 # Updated to match new output format + @test countlines(io) == 46 # Updated to match new output format end nothing diff --git a/test/solver/test_group_coefficients.jl b/test/solver/test_group_coefficients.jl index 7eae60ef..ede46b2a 100644 --- a/test/solver/test_group_coefficients.jl +++ b/test/solver/test_group_coefficients.jl @@ -2,10 +2,6 @@ using VortexStepMethod using LinearAlgebra using Test -# Load test support -include("../TestSupport.jl") -using .TestSupport - @testset "Group Coefficient Arrays Tests" begin @testset "Group coefficients with EQUAL_SIZE method" begin # Create a simple wing with groups diff --git a/test/yaml_geometry/test_yaml_wing_deformation.jl b/test/yaml_geometry/test_yaml_wing_deformation.jl index 580e3dc9..578528b2 100644 --- a/test/yaml_geometry/test_yaml_wing_deformation.jl +++ b/test/yaml_geometry/test_yaml_wing_deformation.jl @@ -2,12 +2,6 @@ using VortexStepMethod using LinearAlgebra using Test -# Load TestSupport if not already loaded (for standalone execution) -if !@isdefined(TestSupport) - include(joinpath(@__DIR__, "..", "TestSupport.jl")) - using .TestSupport -end - @testset "YAML Wing Deformation Tests" begin @testset "Simple Wing Deformation" begin # Load existing simple_wing.yaml From b3ef2b0a1b597d5e5d65136ec15c9896a53e4b6c Mon Sep 17 00:00:00 2001 From: 1-Bart-1 Date: Sat, 8 Nov 2025 14:11:12 +0100 Subject: [PATCH 16/53] Fixed deform --- src/wing_geometry.jl | 37 ++++++++++++++---- test/ram_geometry/test_kite_geometry.jl | 5 --- test/runtests.jl | 52 +++++++++++++++++-------- 3 files changed, 65 insertions(+), 29 deletions(-) diff --git a/src/wing_geometry.jl b/src/wing_geometry.jl index 6ac62f48..c212e8ab 100644 --- a/src/wing_geometry.jl +++ b/src/wing_geometry.jl @@ -456,14 +456,37 @@ function deform!(wing::Wing) chord = zeros(MVec3) normal = zeros(MVec3) - for i in 1:wing.n_panels - section1 = wing.non_deformed_sections[i] - section2 = wing.non_deformed_sections[i+1] - local_y .= normalize(section1.LE_point - section2.LE_point) - chord .= section1.TE_point .- section1.LE_point + # Process all sections (n_panels + 1) + # Each section gets angle(s) from adjacent panel(s) + for i in 1:(wing.n_panels + 1) + section = wing.non_deformed_sections[i] + + # Determine the angle for this section + if i == 1 + # First section: use angle from first panel + theta = wing.theta_dist[1] + elseif i == wing.n_panels + 1 + # Last section: use angle from last panel + theta = wing.theta_dist[wing.n_panels] + else + # Middle sections: average of adjacent panels + theta = 0.5 * (wing.theta_dist[i-1] + wing.theta_dist[i]) + end + + # Compute local coordinate system + if i < wing.n_panels + 1 + section2 = wing.non_deformed_sections[i+1] + local_y .= normalize(section.LE_point - section2.LE_point) + else + # For last section, use same local_y as previous + section_prev = wing.non_deformed_sections[i-1] + local_y .= normalize(section_prev.LE_point - section.LE_point) + end + + chord .= section.TE_point .- section.LE_point normal .= chord × local_y - @. wing.refined_sections[i].TE_point = section1.LE_point + - cos(wing.theta_dist[i]) * chord - sin(wing.theta_dist[i]) * normal + @. wing.refined_sections[i].TE_point = section.LE_point + + cos(theta) * chord - sin(theta) * normal end return nothing end diff --git a/test/ram_geometry/test_kite_geometry.jl b/test/ram_geometry/test_kite_geometry.jl index 861bc880..4eb07823 100644 --- a/test/ram_geometry/test_kite_geometry.jl +++ b/test/ram_geometry/test_kite_geometry.jl @@ -232,11 +232,6 @@ using Serialization original_te_points = [copy(wing.refined_sections[i].TE_point) for i in 1:n_sections] - @show wing.refined_sections[1].LE_point - @show wing.refined_sections[1].TE_point - @show wing.refined_sections[end].LE_point - @show wing.refined_sections[end].TE_point - # Apply group_deform! with non-zero angles (2 groups, each controlling 2 panels) theta_angles = [deg2rad(15.0), deg2rad(20.0)] delta_angles = [deg2rad(5.0), deg2rad(10.0)] diff --git a/test/runtests.jl b/test/runtests.jl index f89bea03..7a2abdd5 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -4,7 +4,25 @@ using Test, VortexStepMethod cd(@__DIR__) # ensure we're in test/ no matter how tests are launched include("test_data_utils.jl") +# Support selective test execution via ]test test_args=["pattern"] +const test_patterns = isempty(ARGS) ? String[] : ARGS + println("Running tests...") +if !isempty(test_patterns) + println("Filtering tests matching: ", test_patterns) +end + +# Helper to check if a test file matches any pattern +function should_run_test(test_path::String) + isempty(test_patterns) && return true + for pattern in test_patterns + # Match directory (e.g., "solver") or specific file (e.g., "test_group_coefficients") + if occursin(pattern, test_path) + return true + end + end + return false +end # keep your env check as-is... const build_is_production_build_env_name = "BUILD_IS_PRODUCTION_BUILD" @@ -14,25 +32,25 @@ const build_is_production_build = let v = get(ENV, build_is_production_build_env end::Bool @testset verbose = true "Testing VortexStepMethod..." begin - if build_is_production_build + if build_is_production_build && should_run_test("bench") include("bench.jl") end - include("body_aerodynamics/test_body_aerodynamics.jl") - include("body_aerodynamics/test_results.jl") - include("filament/test_bound_filament.jl") - include("filament/test_semi_infinite_filament.jl") - include("panel/test_panel.jl") - include("plotting/test_plotting.jl") - include("polars/test_polars.jl") - include("ram_geometry/test_kite_geometry.jl") - include("settings/test_settings.jl") - include("solver/test_solver.jl") - include("solver/test_group_coefficients.jl") - include("VortexStepMethod/test_VortexStepMethod.jl") - include("wake/test_wake.jl") - include("wing_geometry/test_wing_geometry.jl") - include("yaml_geometry/test_yaml_geometry.jl") - include("Aqua.jl") + should_run_test("body_aerodynamics/test_body_aerodynamics.jl") && include("body_aerodynamics/test_body_aerodynamics.jl") + should_run_test("body_aerodynamics/test_results.jl") && include("body_aerodynamics/test_results.jl") + should_run_test("filament/test_bound_filament.jl") && include("filament/test_bound_filament.jl") + should_run_test("filament/test_semi_infinite_filament.jl") && include("filament/test_semi_infinite_filament.jl") + should_run_test("panel/test_panel.jl") && include("panel/test_panel.jl") + should_run_test("plotting/test_plotting.jl") && include("plotting/test_plotting.jl") + should_run_test("polars/test_polars.jl") && include("polars/test_polars.jl") + should_run_test("ram_geometry/test_kite_geometry.jl") && include("ram_geometry/test_kite_geometry.jl") + should_run_test("settings/test_settings.jl") && include("settings/test_settings.jl") + should_run_test("solver/test_solver.jl") && include("solver/test_solver.jl") + should_run_test("solver/test_group_coefficients.jl") && include("solver/test_group_coefficients.jl") + should_run_test("VortexStepMethod/test_VortexStepMethod.jl") && include("VortexStepMethod/test_VortexStepMethod.jl") + should_run_test("wake/test_wake.jl") && include("wake/test_wake.jl") + should_run_test("wing_geometry/test_wing_geometry.jl") && include("wing_geometry/test_wing_geometry.jl") + should_run_test("yaml_geometry/test_yaml_geometry.jl") && include("yaml_geometry/test_yaml_geometry.jl") + should_run_test("Aqua.jl") && include("Aqua.jl") end nothing From 8e28721446031a55ec1b6cfd2e523d165d37c065 Mon Sep 17 00:00:00 2001 From: 1-Bart-1 Date: Sat, 8 Nov 2025 14:19:28 +0100 Subject: [PATCH 17/53] Test on 1.10 and 1.11 --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 6407e6c9..48fdc83d 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -23,7 +23,7 @@ jobs: matrix: version: - '1.10' - - '1' + - '1.11' os: - ubuntu-latest build_is_production_build: From a6a8337f85ab814ed86a85dcd0bd084cd217e842 Mon Sep 17 00:00:00 2001 From: 1-Bart-1 Date: Sat, 8 Nov 2025 17:00:37 +0100 Subject: [PATCH 18/53] Disable mapping kwarg --- src/body_aerodynamics.jl | 9 ++++++--- src/wing_geometry.jl | 22 ++++++++++++++-------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/body_aerodynamics.jl b/src/body_aerodynamics.jl index 103745dc..a05a289b 100644 --- a/src/body_aerodynamics.jl +++ b/src/body_aerodynamics.jl @@ -114,7 +114,7 @@ function Base.setproperty!(obj::BodyAerodynamics, sym::Symbol, val) end """ - reinit!(body_aero::BodyAerodynamics; init_aero, va, omega, refine_mesh) + reinit!(body_aero::BodyAerodynamics; init_aero, va, omega, refine_mesh, recompute_mapping) Initialize a BodyAerodynamics struct in-place by setting up panels and coefficients. @@ -127,6 +127,8 @@ Initialize a BodyAerodynamics struct in-place by setting up panels and coefficie - `omega=zeros(3)`: Turn rate in kite body frame x y and z - `refine_mesh=true`: Whether to refine wing meshes. Set to `false` after `deform!()` to preserve deformed geometry. +- `recompute_mapping=true`: Whether to recompute the refined panel mapping. + Set to `false` to skip mapping computation when it hasn't changed. # Returns nothing @@ -135,12 +137,13 @@ function reinit!(body_aero::BodyAerodynamics; init_aero=true, va=[15.0, 0.0, 0.0], omega=zeros(MVec3), - refine_mesh=true + refine_mesh=true, + recompute_mapping=true ) idx = 1 vec = @MVector zeros(3) for wing in body_aero.wings - reinit!(wing; refine_mesh=refine_mesh) + reinit!(wing; refine_mesh, recompute_mapping) panel_props = wing.panel_props # Create panels diff --git a/src/wing_geometry.jl b/src/wing_geometry.jl index c212e8ab..a5bcdbce 100644 --- a/src/wing_geometry.jl +++ b/src/wing_geometry.jl @@ -302,18 +302,20 @@ function Wing(n_panels::Int; end """ - reinit!(wing::AbstractWing; refine_mesh=true) + reinit!(wing::AbstractWing; refine_mesh=true, recompute_mapping=true) Reinitialize wing geometry and panel properties. # Keyword Arguments - `refine_mesh::Bool=true`: Whether to refine the mesh. Set to `false` after `deform!()` to preserve deformed geometry while updating panel properties. +- `recompute_mapping::Bool=true`: Whether to recompute the refined panel mapping. + Set to `false` to skip mapping computation when it hasn't changed. """ -function reinit!(wing::AbstractWing; refine_mesh=true) +function reinit!(wing::AbstractWing; refine_mesh=true, recompute_mapping=true) # Refine mesh unless explicitly disabled (e.g., to preserve deformation) if refine_mesh - refine_aerodynamic_mesh!(wing) + refine_aerodynamic_mesh!(wing; recompute_mapping) end # Calculate panel properties @@ -587,14 +589,18 @@ function update_non_deformed_sections!(wing::AbstractWing) end """ - refine_aerodynamic_mesh!(wing::AbstractWing) + refine_aerodynamic_mesh!(wing::AbstractWing; recompute_mapping=true) Refine the aerodynamic mesh of the wing based on spanwise panel distribution. +# Keyword Arguments +- `recompute_mapping::Bool=true`: Whether to recompute the refined panel mapping. + Set to `false` to skip mapping computation when it hasn't changed. + Returns: Vector{Section}: List of refined sections """ -function refine_aerodynamic_mesh!(wing::AbstractWing) +function refine_aerodynamic_mesh!(wing::AbstractWing; recompute_mapping=true) sort!(wing.sections, by=s -> s.LE_point[2], rev=true) n_sections = wing.n_panels + 1 if length(wing.refined_sections) == 0 @@ -631,7 +637,7 @@ function refine_aerodynamic_mesh!(wing::AbstractWing) for i in eachindex(wing.sections) reinit!(wing.refined_sections[i], wing.sections[i]) end - compute_refined_panel_mapping!(wing) + recompute_mapping && compute_refined_panel_mapping!(wing) update_non_deformed_sections!(wing) return nothing end @@ -642,7 +648,7 @@ function refine_aerodynamic_mesh!(wing::AbstractWing) if n_sections == 2 reinit!(wing.refined_sections[1], LE[1,:], TE[1,:], aero_model[1], aero_data[1]) reinit!(wing.refined_sections[2], LE[end,:], TE[end,:], aero_model[end], aero_data[end]) - compute_refined_panel_mapping!(wing) + recompute_mapping && compute_refined_panel_mapping!(wing) update_non_deformed_sections!(wing) return nothing end @@ -666,7 +672,7 @@ function refine_aerodynamic_mesh!(wing::AbstractWing) end # Compute panel mapping by finding closest unrefined panel for each refined panel - compute_refined_panel_mapping!(wing) + recompute_mapping && compute_refined_panel_mapping!(wing) # Validate REFINE grouping method if wing.grouping_method == REFINE && wing.n_groups > 0 From 3502599eb0f7c2f1d1a54473ae519d5c5de78ed5 Mon Sep 17 00:00:00 2001 From: 1-Bart-1 Date: Sat, 8 Nov 2025 17:33:57 +0100 Subject: [PATCH 19/53] Add kwarg to sort sections --- src/body_aerodynamics.jl | 9 ++++++--- src/wing_geometry.jl | 17 +++++++++++------ 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/body_aerodynamics.jl b/src/body_aerodynamics.jl index a05a289b..5d5ca33f 100644 --- a/src/body_aerodynamics.jl +++ b/src/body_aerodynamics.jl @@ -114,7 +114,7 @@ function Base.setproperty!(obj::BodyAerodynamics, sym::Symbol, val) end """ - reinit!(body_aero::BodyAerodynamics; init_aero, va, omega, refine_mesh, recompute_mapping) + reinit!(body_aero::BodyAerodynamics; init_aero, va, omega, refine_mesh, recompute_mapping, sort_sections) Initialize a BodyAerodynamics struct in-place by setting up panels and coefficients. @@ -129,6 +129,8 @@ Initialize a BodyAerodynamics struct in-place by setting up panels and coefficie `deform!()` to preserve deformed geometry. - `recompute_mapping=true`: Whether to recompute the refined panel mapping. Set to `false` to skip mapping computation when it hasn't changed. +- `sort_sections=true`: Whether to sort sections by spanwise position. + Set to `false` for REFINE wings where section order is determined by structural connectivity. # Returns nothing @@ -138,12 +140,13 @@ function reinit!(body_aero::BodyAerodynamics; va=[15.0, 0.0, 0.0], omega=zeros(MVec3), refine_mesh=true, - recompute_mapping=true + recompute_mapping=true, + sort_sections=true ) idx = 1 vec = @MVector zeros(3) for wing in body_aero.wings - reinit!(wing; refine_mesh, recompute_mapping) + reinit!(wing; refine_mesh, recompute_mapping, sort_sections) panel_props = wing.panel_props # Create panels diff --git a/src/wing_geometry.jl b/src/wing_geometry.jl index a5bcdbce..83889eff 100644 --- a/src/wing_geometry.jl +++ b/src/wing_geometry.jl @@ -302,7 +302,7 @@ function Wing(n_panels::Int; end """ - reinit!(wing::AbstractWing; refine_mesh=true, recompute_mapping=true) + reinit!(wing::AbstractWing; refine_mesh=true, recompute_mapping=true, sort_sections=true) Reinitialize wing geometry and panel properties. @@ -311,11 +311,13 @@ Reinitialize wing geometry and panel properties. `deform!()` to preserve deformed geometry while updating panel properties. - `recompute_mapping::Bool=true`: Whether to recompute the refined panel mapping. Set to `false` to skip mapping computation when it hasn't changed. +- `sort_sections::Bool=true`: Whether to sort sections by spanwise position. + Set to `false` for REFINE wings where section order is determined by structural connectivity. """ -function reinit!(wing::AbstractWing; refine_mesh=true, recompute_mapping=true) +function reinit!(wing::AbstractWing; refine_mesh=true, recompute_mapping=true, sort_sections=true) # Refine mesh unless explicitly disabled (e.g., to preserve deformation) if refine_mesh - refine_aerodynamic_mesh!(wing; recompute_mapping) + refine_aerodynamic_mesh!(wing; recompute_mapping, sort_sections) end # Calculate panel properties @@ -589,19 +591,22 @@ function update_non_deformed_sections!(wing::AbstractWing) end """ - refine_aerodynamic_mesh!(wing::AbstractWing; recompute_mapping=true) + refine_aerodynamic_mesh!(wing::AbstractWing; recompute_mapping=true, sort_sections=true) Refine the aerodynamic mesh of the wing based on spanwise panel distribution. # Keyword Arguments - `recompute_mapping::Bool=true`: Whether to recompute the refined panel mapping. Set to `false` to skip mapping computation when it hasn't changed. +- `sort_sections::Bool=true`: Whether to sort sections by spanwise position (y-coordinate). + Set to `false` for REFINE wings where section order is determined by structural connectivity. Returns: Vector{Section}: List of refined sections """ -function refine_aerodynamic_mesh!(wing::AbstractWing; recompute_mapping=true) - sort!(wing.sections, by=s -> s.LE_point[2], rev=true) +function refine_aerodynamic_mesh!(wing::AbstractWing; recompute_mapping=true, sort_sections=true) + # Only sort sections if requested (skip for REFINE wings with fixed structural order) + sort_sections && sort!(wing.sections, by=s -> s.LE_point[2], rev=true) n_sections = wing.n_panels + 1 if length(wing.refined_sections) == 0 if wing.spanwise_distribution == UNCHANGED || length(wing.sections) == n_sections From f55597f729d9755cd44cdd017ef5de2e7f397467 Mon Sep 17 00:00:00 2001 From: 1-Bart-1 Date: Sat, 8 Nov 2025 18:36:12 +0100 Subject: [PATCH 20/53] Add groups --- src/body_aerodynamics.jl | 12 +++++++-- src/solver.jl | 53 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 61 insertions(+), 4 deletions(-) diff --git a/src/body_aerodynamics.jl b/src/body_aerodynamics.jl index 5d5ca33f..b96ff57d 100644 --- a/src/body_aerodynamics.jl +++ b/src/body_aerodynamics.jl @@ -24,6 +24,7 @@ Main structure for calculating aerodynamic properties of bodies. Use the constru @with_kw mutable struct BodyAerodynamics{P} panels::Vector{Panel} wings::Vector{Wing} + groups::Vector{Panel} = Panel[] _va::MVec3 = zeros(MVec3) omega::MVec3 = zeros(MVec3) gamma_distribution::MVector{P, Float64} = zeros(P) @@ -73,6 +74,7 @@ function BodyAerodynamics( ) where T <: AbstractWing # Initialize panels panels = Panel[] + n_groups = 0 for wing in wings for section in wing.sections section.LE_point .-= kite_body_origin @@ -80,7 +82,7 @@ function BodyAerodynamics( end if wing.spanwise_distribution == UNCHANGED wing.refined_sections = wing.sections - !(wing.n_panels == length(wing.sections) - 1) && + !(wing.n_panels == length(wing.sections) - 1) && throw(ArgumentError("(wing.n_panels = $(wing.n_panels)) != (length(wing.sections) - 1 = $(length(wing.sections) - 1))")) else wing.refined_sections = Section[Section() for _ in 1:wing.n_panels+1] @@ -91,9 +93,15 @@ function BodyAerodynamics( panel = Panel() push!(panels, panel) end + + # Count total groups + n_groups += wing.n_groups end - body_aero = BodyAerodynamics{length(panels)}(; panels, wings) + # Initialize groups (unrefined panel representatives) + groups = [Panel() for _ in 1:n_groups] + + body_aero = BodyAerodynamics{length(panels)}(; panels, wings, groups) reinit!(body_aero; va, omega) return body_aero end diff --git a/src/solver.jl b/src/solver.jl index 742d921b..ed6f524b 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -316,43 +316,92 @@ function solve!(solver::Solver, body_aero::BodyAerodynamics, gamma_distribution= panels_per_group = wing.n_panels ÷ wing.n_groups for _ in 1:wing.n_groups panel_count = 0 + group_panel = body_aero.groups[group_idx] + # Zero out accumulated fields + group_panel.x_airf .= 0.0 + group_panel.y_airf .= 0.0 + group_panel.z_airf .= 0.0 + group_panel.va .= 0.0 + group_panel.chord = 0.0 + group_panel.width = 0.0 for _ in 1:panels_per_group + panel = body_aero.panels[panel_idx] group_moment_dist[group_idx] += moment_dist[panel_idx] group_moment_coeff_dist[group_idx] += moment_coeff_dist[panel_idx] cl_group_array[group_idx] += solver.sol.cl_array[panel_idx] cd_group_array[group_idx] += solver.sol.cd_array[panel_idx] cm_group_array[group_idx] += solver.sol.cm_array[panel_idx] + # Accumulate geometry for averaging + group_panel.x_airf .+= panel.x_airf + group_panel.y_airf .+= panel.y_airf + group_panel.z_airf .+= panel.z_airf + group_panel.va .+= panel.va + group_panel.chord += panel.chord + group_panel.width += panel.width panel_idx += 1 panel_count += 1 end - # Average the coefficients over panels in the group + # Average the coefficients and geometry over panels in the group cl_group_array[group_idx] /= panel_count cd_group_array[group_idx] /= panel_count cm_group_array[group_idx] /= panel_count + group_panel.x_airf ./= panel_count + group_panel.y_airf ./= panel_count + group_panel.z_airf ./= panel_count + group_panel.va ./= panel_count + group_panel.chord /= panel_count + group_panel.width /= panel_count group_idx += 1 end elseif wing.grouping_method == REFINE # REFINE method: group refined panels by their original unrefined section + # Initialize group panels + for i in 1:wing.n_groups + target_group_idx = group_idx + i - 1 + group_panel = body_aero.groups[target_group_idx] + group_panel.x_airf .= 0.0 + group_panel.y_airf .= 0.0 + group_panel.z_airf .= 0.0 + group_panel.va .= 0.0 + group_panel.chord = 0.0 + group_panel.width = 0.0 + end # First pass: accumulate values group_panel_counts = zeros(Int, wing.n_groups) for local_panel_idx in 1:wing.n_panels + panel = body_aero.panels[panel_idx] original_section_idx = wing.refined_panel_mapping[local_panel_idx] target_group_idx = group_idx + original_section_idx - 1 + group_panel = body_aero.groups[target_group_idx] group_moment_dist[target_group_idx] += moment_dist[panel_idx] group_moment_coeff_dist[target_group_idx] += moment_coeff_dist[panel_idx] cl_group_array[target_group_idx] += solver.sol.cl_array[panel_idx] cd_group_array[target_group_idx] += solver.sol.cd_array[panel_idx] cm_group_array[target_group_idx] += solver.sol.cm_array[panel_idx] + # Accumulate geometry + group_panel.x_airf .+= panel.x_airf + group_panel.y_airf .+= panel.y_airf + group_panel.z_airf .+= panel.z_airf + group_panel.va .+= panel.va + group_panel.chord += panel.chord + group_panel.width += panel.width group_panel_counts[original_section_idx] += 1 panel_idx += 1 end - # Second pass: average coefficients + # Second pass: average coefficients and geometry for i in 1:wing.n_groups target_group_idx = group_idx + i - 1 if group_panel_counts[i] > 0 + group_panel = body_aero.groups[target_group_idx] cl_group_array[target_group_idx] /= group_panel_counts[i] cd_group_array[target_group_idx] /= group_panel_counts[i] cm_group_array[target_group_idx] /= group_panel_counts[i] + group_panel.x_airf ./= group_panel_counts[i] + group_panel.y_airf ./= group_panel_counts[i] + group_panel.z_airf ./= group_panel_counts[i] + group_panel.va ./= group_panel_counts[i] + group_panel.chord /= group_panel_counts[i] + group_panel.width /= group_panel_counts[i] end end group_idx += wing.n_groups From 872bdfd7e4df92ff75c4493edd8128b38bbb6a6c Mon Sep 17 00:00:00 2001 From: 1-Bart-1 Date: Sun, 9 Nov 2025 14:00:50 +0100 Subject: [PATCH 21/53] Correct width --- src/solver.jl | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/solver.jl b/src/solver.jl index ed6f524b..cb7cc6be 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -54,6 +54,7 @@ Struct for storing the solution of the [solve!](@ref) function. Must contain all cl_group_array::MVector{G, Float64} = zeros(G) cd_group_array::MVector{G, Float64} = zeros(G) cm_group_array::MVector{G, Float64} = zeros(G) + alpha_group_array::MVector{G, Float64} = zeros(G) solver_status::SolverStatus = FAILURE end @@ -302,11 +303,13 @@ function solve!(solver::Solver, body_aero::BodyAerodynamics, gamma_distribution= cl_group_array = solver.sol.cl_group_array cd_group_array = solver.sol.cd_group_array cm_group_array = solver.sol.cm_group_array + alpha_group_array = solver.sol.alpha_group_array group_moment_dist .= 0.0 group_moment_coeff_dist .= 0.0 cl_group_array .= 0.0 cd_group_array .= 0.0 cm_group_array .= 0.0 + alpha_group_array .= 0.0 panel_idx = 1 group_idx = 1 for wing in body_aero.wings @@ -331,6 +334,7 @@ function solve!(solver::Solver, body_aero::BodyAerodynamics, gamma_distribution= cl_group_array[group_idx] += solver.sol.cl_array[panel_idx] cd_group_array[group_idx] += solver.sol.cd_array[panel_idx] cm_group_array[group_idx] += solver.sol.cm_array[panel_idx] + alpha_group_array[group_idx] += solver.sol.alpha_array[panel_idx] # Accumulate geometry for averaging group_panel.x_airf .+= panel.x_airf group_panel.y_airf .+= panel.y_airf @@ -345,12 +349,12 @@ function solve!(solver::Solver, body_aero::BodyAerodynamics, gamma_distribution= cl_group_array[group_idx] /= panel_count cd_group_array[group_idx] /= panel_count cm_group_array[group_idx] /= panel_count + alpha_group_array[group_idx] /= panel_count group_panel.x_airf ./= panel_count group_panel.y_airf ./= panel_count group_panel.z_airf ./= panel_count group_panel.va ./= panel_count group_panel.chord /= panel_count - group_panel.width /= panel_count group_idx += 1 end elseif wing.grouping_method == REFINE @@ -378,6 +382,7 @@ function solve!(solver::Solver, body_aero::BodyAerodynamics, gamma_distribution= cl_group_array[target_group_idx] += solver.sol.cl_array[panel_idx] cd_group_array[target_group_idx] += solver.sol.cd_array[panel_idx] cm_group_array[target_group_idx] += solver.sol.cm_array[panel_idx] + alpha_group_array[target_group_idx] += solver.sol.alpha_array[panel_idx] # Accumulate geometry group_panel.x_airf .+= panel.x_airf group_panel.y_airf .+= panel.y_airf @@ -396,6 +401,7 @@ function solve!(solver::Solver, body_aero::BodyAerodynamics, gamma_distribution= cl_group_array[target_group_idx] /= group_panel_counts[i] cd_group_array[target_group_idx] /= group_panel_counts[i] cm_group_array[target_group_idx] /= group_panel_counts[i] + alpha_group_array[target_group_idx] /= group_panel_counts[i] group_panel.x_airf ./= group_panel_counts[i] group_panel.y_airf ./= group_panel_counts[i] group_panel.z_airf ./= group_panel_counts[i] From a1df71207653e22e5b716a6824659d2ffefc9dd4 Mon Sep 17 00:00:00 2001 From: 1-Bart-1 Date: Mon, 10 Nov 2025 16:15:32 +0100 Subject: [PATCH 22/53] Add makie plotting --- examples/Project.toml | 1 + examples/ram_air_kite.jl | 4 +- ext/VortexStepMethodControlPlotsExt.jl | 920 ++++++++++++++++++++++++- ext/VortexStepMethodMakieExt.jl | 690 ++++++++++++++++++- src/panel.jl | 2 +- src/plotting.jl | 920 ------------------------- 6 files changed, 1612 insertions(+), 925 deletions(-) delete mode 100644 src/plotting.jl diff --git a/examples/Project.toml b/examples/Project.toml index b94307b9..fea9b2fd 100644 --- a/examples/Project.toml +++ b/examples/Project.toml @@ -2,6 +2,7 @@ CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" ControlPlots = "23c2ee80-7a9e-4350-b264-8e670f12517c" DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" +GLMakie = "e9467ef8-e4e7-5192-8a1a-b1aee30e663a" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" VortexStepMethod = "ed3cd733-9f0f-46a9-93e0-89b8d4998dd9" diff --git a/examples/ram_air_kite.jl b/examples/ram_air_kite.jl index 13e60c62..bbf1480b 100644 --- a/examples/ram_air_kite.jl +++ b/examples/ram_air_kite.jl @@ -1,4 +1,4 @@ -using ControlPlots +using GLMakie using VortexStepMethod using LinearAlgebra @@ -118,4 +118,4 @@ PLOT && plot_polars( is_show=true, use_tex=USE_TEX ) -nothing \ No newline at end of file +nothing diff --git a/ext/VortexStepMethodControlPlotsExt.jl b/ext/VortexStepMethodControlPlotsExt.jl index ce9494be..b2d09e9f 100644 --- a/ext/VortexStepMethodControlPlotsExt.jl +++ b/ext/VortexStepMethodControlPlotsExt.jl @@ -5,6 +5,924 @@ import VortexStepMethod: calculate_filaments_for_plotting export plot_wing, plot_circulation_distribution, plot_geometry, plot_distribution, plot_polars, save_plot, show_plot, plot_polar_data -include("../src/plotting.jl") +""" + set_plot_style(titel_size=16; use_tex=false) + +Set the default style for plots using LaTeX. +`` +# Arguments: +- `titel_size`: size of the plot title in points (default: 16) +- `ùse_tex`: if the external `pdflatex` command shall be used +""" +function set_plot_style(titel_size=16; use_tex=false) + rcParams = plt.PyDict(plt.matplotlib."rcParams") + rcParams["text.usetex"] = use_tex + rcParams["font.family"] = "serif" + if use_tex + rcParams["font.serif"] = ["Computer Modern Roman"] + end + rcParams["axes.titlesize"] = titel_size + rcParams["axes.labelsize"] = 12 + rcParams["axes.linewidth"] = 1 + rcParams["lines.linewidth"] = 1 + rcParams["lines.markersize"] = 6 + rcParams["xtick.labelsize"] = 10 + rcParams["ytick.labelsize"] = 10 + rcParams["legend.fontsize"] = 10 + rcParams["figure.titlesize"] = 16 + if use_tex + rcParams["pgf.texsystem"] = "pdflatex" # Use pdflatex + end + rcParams["pgf.rcfonts"] = false + rcParams["figure.figsize"] = (10, 6) # Default figure size +end + + +""" + save_plot(fig, save_path, title; data_type=".pdf") + +Save a plot to a file. + +# Arguments +- `fig`: Plots figure object +- `save_path`: Path to save the plot +- `title`: Title of the plot + +# Keyword arguments +- `data_type`: File extension (default: ".pdf") +""" +function VortexStepMethod.save_plot(fig, save_path, title; data_type=".pdf") + isnothing(save_path) && throw(ArgumentError("save_path should be provided")) + + !isdir(save_path) && mkpath(save_path) + full_path = joinpath(save_path, title * data_type) + + @debug "Attempting to save figure to: $full_path" + @debug "Current working directory: $(pwd())" + + try + fig.savefig(full_path) + @debug "Figure saved as $data_type" + + if isfile(full_path) + @debug "File successfully saved to $full_path" + @debug "File size: $(filesize(full_path)) bytes" + else + @info "File does not exist after save attempt: $full_path" + end + catch e + @error "Error saving figure: $e" + @error "Error type: $(typeof(e))" + rethrow(e) + end +end + +""" + show_plot(fig; dpi=130) + +Display a plot at specified DPI. + +# Arguments +- `fig`: Plots figure object + +# Keyword arguments +- `dpi`: Dots per inch for the figure (default: 130) +""" +function VortexStepMethod.show_plot(fig; dpi=130) + plt.display(fig) +end + +""" + plot_line_segment!(ax, segment, color, label; width=3) + +Plot a line segment in 3D with arrow. + +# Arguments +- `ax`: Plot axis +- `segment`: Array of two points defining the segment +- `color`: Color of the segment +- `label`: Label for the legend + +# Keyword Arguments +- `width`: Line width (default: 3) +""" +function plot_line_segment!(ax, segment, color, label; width=3) + ax.plot( + [segment[1][1], segment[2][1]], + [segment[1][2], segment[2][2]], + [segment[1][3], segment[2][3]], + color=color, label=label, linewidth=width + ) + + dir = segment[2] - segment[1] + ax.quiver( + [segment[1][1]], [segment[1][2]], [segment[1][3]], + [dir[1]], [dir[2]], [dir[3]], + color=color + ) +end + +""" + set_axes_equal!(ax; zoom=1.8) + +Set 3D plot axes to equal scale. + +# Arguments +- ax: 3D plot axis + +# Keyword arguments +zoom: zoom factor (default: 1.8) +""" +function set_axes_equal!(ax; zoom=1.8) + x_lims = ax.get_xlim3d() ./ zoom + y_lims = ax.get_ylim3d() ./ zoom + z_lims = ax.get_zlim3d() ./ zoom + + x_range = abs(x_lims[2] - x_lims[1]) + y_range = abs(y_lims[2] - y_lims[1]) + z_range = abs(z_lims[2] - z_lims[1]) + + max_range = max(x_range, y_range, z_range) + + x_mid = mean(x_lims) + y_mid = mean(y_lims) + z_mid = mean(z_lims) + + ax.set_xlim3d((x_mid - max_range / 2, x_mid + max_range / 2)) + ax.set_ylim3d((y_mid - max_range / 2, y_mid + max_range / 2)) + ax.set_zlim3d((z_mid - max_range / 2, z_mid + max_range / 2)) +end + +""" + create_geometry_plot(body_aero::BodyAerodynamics, title, view_elevation, view_azimuth; + zoom=1.8, use_tex=false) + +Create a 3D plot of wing geometry including panels and filaments. + +# Arguments +- body_aero: struct of type BodyAerodynamics +- title: plot title +- view_elevation: initial view elevation angle [°] +- view_azimuth: initial view azimuth angle [°] + +# Keyword arguments +- zoom: zoom factor (default: 1.8) +""" +function create_geometry_plot(body_aero::BodyAerodynamics, title, view_elevation, view_azimuth; + zoom=1.8, use_tex=false) + set_plot_style(28; use_tex) + + panels = body_aero.panels + va = isa(body_aero.va, Tuple) ? body_aero.va[1] : body_aero.va + + # Extract geometric data + corner_points = [panel.corner_points for panel in panels] + control_points = [panel.control_point for panel in panels] + aero_centers = [panel.aero_center for panel in panels] + + # Create plot + fig = plt.figure(figsize=(14, 14)) + ax = fig.add_subplot(111, projection="3d") + ax.set_title(title) + + # Plot panels + legend_used = Dict{String,Bool}() + for (i, panel) in enumerate(panels) + # Plot panel edges and surfaces + corners = Matrix{Float64}(panel.corner_points) + x_corners = corners[1, :] + y_corners = corners[2, :] + z_corners = corners[3, :] + + push!(x_corners, x_corners[1]) + push!(y_corners, y_corners[1]) + push!(z_corners, z_corners[1]) + + ax.plot(x_corners, + y_corners, + z_corners, + color=:grey, + linewidth=1, + label=i == 1 ? "Panel Edges" : "") + + # Plot control points and aerodynamic centers + ax.scatter([control_points[i][1]], [control_points[i][2]], [control_points[i][3]], + color=:green, label=i == 1 ? "Control Points" : "") + ax.scatter([aero_centers[i][1]], [aero_centers[i][2]], [aero_centers[i][3]], + color=:blue, label=i == 1 ? "Aerodynamic Centers" : "") + + # Plot filaments + filaments = calculate_filaments_for_plotting(panel) + legends = ["Bound Vortex", "side1", "side2", "wake_1", "wake_2"] + + for (filament, legend) in zip(filaments, legends) + x1, x2, color = filament + @debug "Legend: $legend" + show_legend = !get(legend_used, legend, false) + plot_line_segment!(ax, [x1, x2], color, show_legend ? legend : "") + legend_used[legend] = true + end + end + + # Plot velocity vector + max_chord = maximum(panel.chord for panel in panels) + va_mag = norm(va) + va_vector_begin = -2 * max_chord * va / va_mag + va_vector_end = va_vector_begin + 1.5 * va / va_mag + plot_line_segment!(ax, [va_vector_begin, va_vector_end], :lightblue, "va") + + # Add legends for the first occurrence of each label + handles, labels = ax.get_legend_handles_labels() + # by_label = Dict(zip(labels, handles)) + # ax.legend(values(by_label), keys(by_label), bbox_to_anchor=(0, 0, 1.1, 1)) + + # Set labels and make axes equal + ax.set_xlabel("x") + ax.set_ylabel("y") + ax.set_zlabel("z") + set_axes_equal!(ax; zoom) + + # Set the initial view + ax.view_init(elev=view_elevation, azim=view_azimuth) + + # Ensure the figure is fully rendered + # fig.canvas.draw() + plt.tight_layout(rect=(0, 0, 1, 0.97)) + + return fig +end + +""" + plot_geometry(body_aero::BodyAerodynamics, title; + data_type=".pdf", save_path=nothing, + is_save=false, is_show=false, + view_elevation=15, view_azimuth=-120, use_tex=false) + +Plot wing geometry from different viewpoints and optionally save/show plots. + +# Arguments: +- `body_aero`: the [BodyAerodynamics](@ref) to plot +- title: plot title + +# Keyword arguments: +- `data_type``: string with the file type postfix (default: ".pdf") +- `save_path`: path for saving the graphic (default: `nothing`) +- `is_save`: boolean value, indicates if the graphic shall be saved (default: `false`) +- `is_show`: boolean value, indicates if the graphic shall be displayed (default: `false`) +- `view_elevation`: initial view elevation angle (default: 15) [°] +- `view_azimuth`: initial view azimuth angle (default: -120) [°] +- `use_tex`: if the external `pdflatex` command shall be used (default: false) + +""" +function VortexStepMethod.plot_geometry(body_aero::BodyAerodynamics, title; + data_type=".pdf", + save_path=nothing, + is_save=false, + is_show=false, + view_elevation=15, + view_azimuth=-120, + use_tex=false) + + if is_save + plt.ioff() + # Angled view + fig = create_geometry_plot(body_aero, "$(title)_angled_view", 15, -120; use_tex) + save_plot(fig, save_path, "$(title)_angled_view", data_type=data_type) + + # Top view + fig = create_geometry_plot(body_aero, "$(title)_top_view", 90, 0; use_tex) + save_plot(fig, save_path, "$(title)_top_view", data_type=data_type) + + # Front view + fig = create_geometry_plot(body_aero, "$(title)_front_view", 0, 0; use_tex) + save_plot(fig, save_path, "$(title)_front_view", data_type=data_type) + + # Side view + fig = create_geometry_plot(body_aero, "$(title)_side_view", 0, -90; use_tex) + save_plot(fig, save_path, "$(title)_side_view", data_type=data_type) + end + + if is_show + plt.ion() + fig = create_geometry_plot(body_aero, title, view_elevation, view_azimuth; use_tex) + plt.display(fig) + else + fig = create_geometry_plot(body_aero, title, view_elevation, view_azimuth; use_tex) + end + fig +end + +""" + plot_distribution(y_coordinates_list, results_list, label_list; + title="spanwise_distribution", data_type=".pdf", + save_path=nothing, is_save=false, is_show=true, use_tex=false) + +Plot spanwise distributions of aerodynamic properties. + +# Arguments +- `y_coordinates_list`: List of spanwise coordinates +- `results_list`: List of result dictionaries +- `label_list`: List of labels for different results + +# Keyword arguments +- `title`: Plot title (default: "spanwise_distribution") +- `data_type`: File extension for saving (default: ".pdf") +- `save_path`: Path to save plots (default: nothing) +- `is_save`: Whether to save plots (default: false) +- `is_show`: Whether to display plots (default: true) +- `use_tex`: if the external `pdflatex` command shall be used +""" +function VortexStepMethod.plot_distribution(y_coordinates_list, results_list, label_list; + title="spanwise_distribution", + data_type=".pdf", + save_path=nothing, + is_save=false, + is_show=true, + use_tex=false) + + length(results_list) == length(label_list) || throw(ArgumentError( + "Number of results ($(length(results_list))) must match number of labels ($(length(label_list)))" + )) + + # Set the plot style + set_plot_style(; use_tex) + + # Initializing plot + fig, axs = plt.subplots(3, 3, figsize=(16, 10)) + fig.suptitle(title, fontsize=16) + + # CL plot + for (y_coordinates_i, result_i, label_i) in zip(y_coordinates_list, results_list, label_list) + value = "$(round(result_i["cl"], digits=2))" + if label_i == "LLT" + label = label_i * L" $~C_\mathrm{L}$: " * value + else + label = label_i * L" $C_\mathrm{L}$: " * value + end + axs[1, 1].plot( + y_coordinates_i, + result_i["cl_distribution"], + label=label + ) + end + axs[1, 1].set_title(L"$C_\mathrm{L}$ Distribution", size=16) + axs[1, 1].set_xlabel(L"Spanwise Position $y/b$") + axs[1, 1].set_ylabel(L"Lift Coefficient $C_\mathrm{L}$") + axs[1, 1].legend() + + # CD plot + for (y_coordinates_i, result_i, label_i) in zip(y_coordinates_list, results_list, label_list) + value = "$(round(result_i["cl"], digits=2))" + if label_i == "LLT" + label = label_i * L" $~C_\mathrm{D}$: " * value + else + label = label_i * L" $C_\mathrm{D}$: " * value + end + axs[1, 2].plot( + y_coordinates_i, + result_i["cd_distribution"], + label=label + ) + end + axs[1, 2].set_title(L"$C_\mathrm{D}$ Distribution", size=16) + axs[1, 2].set_xlabel(L"Spanwise Position $y/b$") + axs[1, 2].set_ylabel(L"Drag Coefficient $C_\mathrm{D}$") + axs[1, 2].legend() + + # Gamma Distribution + for (y_coordinates_i, result_i, label_i) in zip(y_coordinates_list, results_list, label_list) + axs[1, 3].plot( + y_coordinates_i, + result_i["gamma_distribution"], + label=label_i + ) + end + axs[1, 3].set_title(L"\Gamma~Distribution", size=16) + axs[1, 3].set_xlabel(L"Spanwise Position $y/b$") + axs[1, 3].set_ylabel(L"Circulation~\Gamma") + axs[1, 3].legend() + + # Geometric Alpha + for (y_coordinates_i, result_i, label_i) in zip(y_coordinates_list, results_list, label_list) + axs[2, 1].plot( + y_coordinates_i, + result_i["alpha_geometric"], + label=label_i + ) + end + axs[2, 1].set_title(L"$\alpha$ Geometric", size=16) + axs[2, 1].set_xlabel(L"Spanwise Position $y/b$") + axs[2, 1].set_ylabel(L"Angle of Attack $\alpha$ (deg)") + axs[2, 1].legend() + + # Calculated/ Corrected Alpha + for (y_coordinates_i, result_i, label_i) in zip(y_coordinates_list, results_list, label_list) + axs[2, 2].plot( + y_coordinates_i, + result_i["alpha_at_ac"], + label=label_i + ) + end + axs[2, 2].set_title(L"$\alpha$ result (corrected to aerodynamic center)", size=16) + axs[2, 2].set_xlabel(L"Spanwise Position $y/b$") + axs[2, 2].set_ylabel(L"Angle of Attack $\alpha$ (deg)") + axs[2, 2].legend() + + # Uncorrected Alpha plot + for (y_coordinates_i, result_i, label_i) in zip(y_coordinates_list, results_list, label_list) + axs[2, 3].plot( + y_coordinates_i, + result_i["alpha_uncorrected"], + label=label_i + ) + end + axs[2, 3].set_title(L"$\alpha$ Uncorrected (if VSM, at the control point)", size=16) + axs[2, 3].set_xlabel(L"Spanwise Position $y/b$") + axs[2, 3].set_ylabel(L"Angle of Attack $\alpha$ (deg)") + axs[2, 3].legend() + + # Force Components + for (idx, component) in enumerate(["x", "y", "z"]) + axs[3, idx].set_title("Force in $component direction", size=16) + axs[3, idx].set_xlabel(L"Spanwise Position $y/b$") + axs[3, idx].set_ylabel(raw"$F_\mathrm" * "{$component}" * raw"$") + for (y_coords, results, label) in zip(y_coordinates_list, results_list, label_list) + # Extract force components for the current direction (idx) + forces = results["F_distribution"][idx, :] + # Verify dimensions match + if length(y_coords) != length(forces) + @warn "Dimension mismatch in force plotting" length(y_coords) length(forces) component + continue # Skip this component instead of throwing error + end + space = "" + if label == "LLT" + space = "~" + end + axs[3, idx].plot( + y_coords, + forces, + label="$label" * space * raw"$~\Sigma~F_\mathrm" * "{$component}:~" * + raw"$" * "$(round(results["F$component"], digits=2)) N" + ) + axs[3, idx].legend() + end + end + + fig.tight_layout() + + # Save and show plot + if is_save + save_plot(fig, save_path, title, data_type=data_type) + end + + if is_show + show_plot(fig) + end + + return fig +end + +""" + generate_polar_data(solver, body_aero::BodyAerodynamics, angle_range; + angle_type="angle_of_attack", angle_of_attack=0.0, + side_slip=0.0, v_a=10.0, use_latex=false) + +Generate polar data for aerodynamic analysis over a range of angles. + +# Arguments +- `solver`: Aerodynamic solver object +- `body_aero`: Wing aerodynamics struct +- `angle_range`: Range of angles to analyze + +# Keyword arguments +- `angle_type`: Type of angle variation ("angle_of_attack" or "side_slip") +- `angle_of_attack`: Initial angle of attack [rad] +- `side_slip`: Initial side slip angle in [rad] +- `v_a`: norm of apparent wind speed [m/s] + +# Returns +- Tuple of polar data array and Reynolds number +""" +function generate_polar_data( + solver, + body_aero::BodyAerodynamics, + angle_range; + angle_type="angle_of_attack", + angle_of_attack=0.0, + side_slip=0.0, + v_a=10.0, + use_latex=false +) + n_panels = length(body_aero.panels) + n_angles = length(angle_range) + + # Initialize arrays + cl = zeros(n_angles) + cd = zeros(n_angles) + cs = zeros(n_angles) + gamma_distribution = zeros(n_angles, n_panels) + cl_distribution = zeros(n_angles, n_panels) + cd_distribution = zeros(n_angles, n_panels) + cs_distribution = zeros(n_angles, n_panels) + reynolds_number = zeros(n_angles) + + # Previous gamma for initialization + gamma = nothing + + for (i, angle_i) in enumerate(angle_range) + # Set angle based on type + if angle_type == "angle_of_attack" + α = deg2rad(angle_i) + β = side_slip + elseif angle_type == raw"side_slip" + α = angle_of_attack + β = deg2rad(angle_i) + else + throw(ArgumentError("angle_type must be 'angle_of_attack' or 'side_slip'")) + end + + # Update inflow conditions + set_va!( + body_aero, + [ + cos(α) * cos(β), + sin(β), + sin(α) + ] * v_a + ) + + # Solve and store results + results = solve(solver, body_aero, gamma_distribution[i, :]) + + cl[i] = results["cl"] + cd[i] = results["cd"] + cs[i] = results["cs"] + gamma_distribution[i, :] = results["gamma_distribution"] + cl_distribution[i, :] = results["cl_distribution"] + cd_distribution[i, :] = results["cd_distribution"] + cs_distribution[i, :] = results["cs_distribution"] + reynolds_number[i] = results["Rey"] + + # Store gamma for next iteration + gamma = gamma_distribution[i, :] + end + + polar_data = [ + angle_range, + cl, + cd, + cs, + gamma_distribution, + cl_distribution, + cd_distribution, + cs_distribution, + reynolds_number + ] + + return polar_data, reynolds_number[1] +end + +""" + plot_polars(solver_list, body_aero_list, label_list; + literature_path_list=String[], + angle_range=range(0, 20, 2), angle_type="angle_of_attack", + angle_of_attack=0.0, side_slip=0.0, v_a=10.0, + title="polar", data_type=".pdf", save_path=nothing, + is_save=true, is_show=true, use_tex=false) + +Plot polar data comparing different solvers and configurations. + +# Arguments +- `solver_list`: List of aerodynamic solvers +- `body_aero_list`: List of wing aerodynamics objects +- `label_list`: List of labels for each configuration + +# Keyword arguments +- `literature_path_list`: Optional paths to literature data files +- `angle_range`: Range of angles to analyze [°] +- `angle_type`: "`angle_of_attack`" or "`side_slip`"; (default: `angle_of_attack`) +- `angle_of_attack:` AoA to be used for plotting the polars (default: 0.0) [rad] +- `side_slip`: side slip angle (default: 0.0) [rad] +- v_a: norm of apparent wind speed (default: 10.0) [m/s] +- title: plot title +- `data_type`: File extension for saving (default: ".pdf") +- `save_path`: Path to save plots (default: nothing) +- `is_save`: Whether to save plots (default: true) +- `is_show`: Whether to display plots (default: true) +- `use_tex`: if the external `pdflatex` command shall be used (default: false) +""" +function VortexStepMethod.plot_polars( + solver_list, + body_aero_list, + label_list; + literature_path_list=String[], + angle_range=range(0, 20, 2), + angle_type="angle_of_attack", + angle_of_attack=0.0, + side_slip=0.0, + v_a=10.0, + title="polar", + data_type=".pdf", + save_path=nothing, + is_save=true, + is_show=true, + use_tex=false +) + # Validate inputs + total_cases = length(body_aero_list) + length(literature_path_list) + if total_cases != length(label_list) || length(solver_list) != length(body_aero_list) + throw(ArgumentError("Mismatch in number of solvers ($(length(solver_list))), " * + "cases ($total_cases), and labels ($(length(label_list)))")) + end + main_title = replace(title, " " => "_") + set_plot_style(; use_tex) + + # Generate polar data + polar_data_list = [] + for (i, (solver, body_aero)) in enumerate(zip(solver_list, body_aero_list)) + polar_data, rey = generate_polar_data( + solver, body_aero, angle_range; + angle_type, + angle_of_attack, + side_slip, + v_a + ) + push!(polar_data_list, polar_data) + # Update label with Reynolds number + label_list[i] = "$(label_list[i]) Re = $(round(Int64, rey*1e-5))e5" + end + # Load literature data if provided + if !isempty(literature_path_list) + for path in literature_path_list + data = readdlm(path, ',') + header = lowercase.(string.(data[1, :])) + # Find column indices for alpha, CL, CD, CS (case-insensitive, allow common variants) + alpha_idx = findfirst(x -> occursin("alpha", x), header) + cl_idx = findfirst(x -> occursin("cl", x), header) + cd_idx = findfirst(x -> occursin("cd", x), header) + cs_idx = findfirst(x -> occursin("cs", x), header) + # Fallback: if CS not found, fill with zeros + cs_col = cs_idx === nothing ? zeros(size(data, 1)-1) : data[2:end, cs_idx] + # Push as [alpha, CL, CD, CS] + push!(polar_data_list, [ + data[2:end, alpha_idx], + data[2:end, cl_idx], + data[2:end, cd_idx], + cs_col + ]) + end + end + + # Initializing plot + fig, axs = plt.subplots(2, 2, figsize=(14, 14)) + + # Number of computational results (excluding literature) + n_solvers = length(solver_list) + for (i, (polar_data, label)) in enumerate(zip(polar_data_list, label_list)) + if i < n_solvers + linestyle = "-" + marker = "*" + markersize = 7 + else + linestyle = "-" + marker = "." + markersize = 5 + end + if contains(label, "LLT") + label = replace(label, "e5" => raw"\cdot10^5") + label = replace(label, " " => raw"~") + label = replace(label, "LLT" => raw"\mathrm{LLT}{~\,}") + label = raw"$" * label * raw"$" + else + label = replace(label, "e5" => raw"\cdot10^5") + label = replace(label, " " => "~") + label = replace(label, "VSM" => raw"\mathrm{VSM}") + label = raw"$" * label * raw"$" + end + axs[1, 1].plot( + polar_data[1], + polar_data[2], + label=label, + linestyle=linestyle, + marker=marker, + markersize=markersize, + ) + # Limit y-range if CL > 10 + if maximum(polar_data[2]) > 10 + axs[1, 1].set_ylim([-0.5, 2]) + end + title = raw"$C_\mathrm{L}" * raw"$" * " vs $angle_type [°]" + axs[1, 1].set_title(title) + axs[1, 1].set_xlabel("$angle_type [°]") + axs[1, 1].set_ylabel(L"$C_\mathrm{L}$") + axs[1, 1].legend() + end + + for (i, (polar_data, label)) in enumerate(zip(polar_data_list, label_list)) + if i < n_solvers + linestyle = "-" + marker = "*" + markersize = 7 + else + linestyle = "-" + marker = "." + markersize = 5 + end + if contains(label, "LLT") + label = replace(label, "e5" => raw"\cdot10^5") + label = replace(label, " " => raw"~") + label = replace(label, "LLT" => raw"\mathrm{LLT}{~\,}") + label = raw"$" * label * raw"$" + else + label = replace(label, "e5" => raw"\cdot10^5") + label = replace(label, " " => "~") + label = replace(label, "VSM" => raw"\mathrm{VSM}") + label = raw"$" * label * raw"$" + end + axs[1, 2].plot( + polar_data[1], + polar_data[3], + label=label, + linestyle=linestyle, + marker=marker, + markersize=markersize, + ) + # Limit y-range if CL > 10 + if maximum(polar_data[2]) > 10 + axs[1, 2].set_ylim([-0.5, 2]) + end + title = raw"$C_\mathrm{D}" * raw"$" * " vs $angle_type [°]" + axs[1, 2].set_title(title) + axs[1, 2].set_xlabel("$angle_type [°]") + axs[1, 2].set_ylabel(L"$C_\mathrm{D}$") + axs[1, 2].legend() + end + + + for (i, (polar_data, label)) in enumerate(zip(polar_data_list, label_list)) + if i < n_solvers + linestyle = "-" + marker = "*" + markersize = 7 + else + linestyle = "-" + marker = "." + markersize = 5 + end + if contains(label, "LLT") + label = replace(label, "e5" => raw"\cdot10^5") + label = replace(label, " " => raw"~") + label = replace(label, "LLT" => raw"\mathrm{LLT}{~\,}") + label = raw"$" * label * raw"$" + else + label = replace(label, "e5" => raw"\cdot10^5") + label = replace(label, " " => "~") + label = replace(label, "VSM" => raw"\mathrm{VSM}") + label = raw"$" * label * raw"$" + end + axs[2, 1].plot( + polar_data[1], + polar_data[4], + label=label, + linestyle=linestyle, + marker=marker, + markersize=markersize, + ) + # Limit y-range if CL > 10 + if maximum(polar_data[2]) > 10 + axs[2, 1].set_ylim([-0.5, 2]) + end + title = raw"$C_\mathrm{S}" * raw"$" * " vs $angle_type [°]" + axs[2, 1].set_title(title) + axs[2, 1].set_xlabel("$angle_type [°]") + axs[2, 1].set_ylabel(L"$C_\mathrm{S}$") + axs[2, 1].legend() + end + + for (i, (polar_data, label)) in enumerate(zip(polar_data_list, label_list)) + if i < n_solvers + linestyle = "-" + marker = "*" + markersize = 7 + else + linestyle = "-" + marker = "." + markersize = 5 + end + if contains(label, "LLT") + label = replace(label, "e5" => raw"\cdot10^5") + label = replace(label, " " => raw"~") + label = replace(label, "LLT" => raw"\mathrm{LLT}{~\,}") + label = raw"$" * label * raw"$" + else + label = replace(label, "e5" => raw"\cdot10^5") + label = replace(label, " " => "~") + label = replace(label, "VSM" => raw"\mathrm{VSM}") + label = raw"$" * label * raw"$" + end + axs[2, 2].plot( + polar_data[3], + polar_data[2], + label=label, + linestyle=linestyle, + marker=marker, + markersize=markersize, + ) + # Limit y-range if CL > 10 + if maximum(polar_data[2]) > 10 || maximum(polar_data[3]) > 10 + axs[2, 2].set_ylim([-0.5, 2]) + axs[2, 2].set_xlim([-0.5, 2]) + end + title = raw"$C_\mathrm{L}" * raw"$" * " vs " * raw"$C_\mathrm{D}" * raw"$" + axs[2, 2].set_title(title) + axs[2, 2].set_xlabel(L"$C_\mathrm{D}$") + axs[2, 2].set_ylabel(L"$C_\mathrm{L}$") + axs[2, 2].legend() + end + + fig.tight_layout(h_pad=3.5, rect=(0.01, 0.01, 0.99, 0.99)) + + # Save and show plot + if is_save && !isnothing(save_path) + save_plot(fig, save_path, main_title; data_type) + end + + if is_show + show_plot(fig) + end + + return fig +end + +""" + plot_polar_data(body_aero::BodyAerodynamics; alphas=collect(deg2rad.(-5:0.3:25)), delta_tes=collect(deg2rad.(-5:0.3:25))) + +Plot polar data (Cl, Cd, Cm) as 3D surfaces against alpha and delta_te angles. delta_te is the trailing edge deflection angle +relative to the 2d airfoil or panel chord line. + +# Arguments +- `body_aero`: Wing aerodynamics struct + +# Keyword arguments +- `alphas`: Range of angle of attack values in radians (default: -5° to 25° in 0.3° steps) +- `delta_tes`: Range of trailing edge angles in radians (default: -5° to 25° in 0.3° steps) +- `is_show`: Whether to display plots (default: true) +- `use_tex`: if the external `pdflatex` command shall be used +""" +function VortexStepMethod.plot_polar_data(body_aero::BodyAerodynamics; + alphas=collect(deg2rad.(-5:0.3:25)), + delta_tes = collect(deg2rad.(-5:0.3:25)), + is_show = true, + use_tex = false + ) + if body_aero.panels[1].aero_model == POLAR_MATRICES + set_plot_style() + + # Create figure with subplots + fig = plt.figure(figsize=(15, 6)) + + # Get interpolation functions and labels + interp_data = [ + (body_aero.panels[1].cl_interp, L"$C_l$"), + (body_aero.panels[1].cd_interp, L"$C_d$"), + (body_aero.panels[1].cm_interp, L"$C_m$") + ] + + # Create each subplot + for (idx, (interp, label)) in enumerate(interp_data) + ax = fig.add_subplot(1, 3, idx, projection="3d") + + # Create interpolation matrix + interp_matrix = zeros(length(alphas), length(delta_tes)) + interp_matrix .= [interp(alpha, delta_te) for alpha in alphas, delta_te in delta_tes] + X = collect(delta_tes) .+ zeros(length(alphas))' + Y = collect(alphas)' .+ zeros(length(delta_tes)) + + # Plot surface + ax.plot_wireframe(X, Y, interp_matrix, + edgecolor="blue", + lw=0.5, + rstride=5, + cstride=5, + alpha=0.6) + + # Set labels and title + ax.set_xlabel(L"$\delta$ [rad]") + ax.set_ylabel(L"$\alpha$ [rad]") + ax.set_zlabel(label) + ax.set_title(label * L" vs $\alpha$ and $\delta$") + ax.grid(true) + end + + # Adjust layout and display + plt.tight_layout(rect=(0.01, 0.01, 0.99, 0.99)) + if is_show + show_plot(fig) + end + return fig + else + throw(ArgumentError("Plotting polar data for $(body_aero.panels[1].aero_model) is not implemented.")) + end +end end \ No newline at end of file diff --git a/ext/VortexStepMethodMakieExt.jl b/ext/VortexStepMethodMakieExt.jl index f72ea3d8..69819d2a 100644 --- a/ext/VortexStepMethodMakieExt.jl +++ b/ext/VortexStepMethodMakieExt.jl @@ -1,5 +1,8 @@ module VortexStepMethodMakieExt -using Makie, VortexStepMethod +using Makie, VortexStepMethod, LinearAlgebra, Statistics, DelimitedFiles +import VortexStepMethod: calculate_filaments_for_plotting + +export plot_geometry, plot_distribution, plot_polars, save_plot, show_plot, plot_polar_data """ plot!(ax, panel::VortexStepMethod.Panel; kwargs...) @@ -62,4 +65,689 @@ function Makie.plot(body_aero::VortexStepMethod.BodyAerodynamics; size = (1200, return fig end +""" + save_plot(fig, save_path, title; data_type=".pdf") + +Save a Makie figure to a file. + +# Arguments +- `fig`: Makie Figure object +- `save_path`: Path to save the plot +- `title`: Title of the plot + +# Keyword arguments +- `data_type`: File extension (default: ".pdf") +""" +function VortexStepMethod.save_plot(fig, save_path, title; data_type=".pdf") + isnothing(save_path) && throw(ArgumentError("save_path should be provided")) + + !isdir(save_path) && mkpath(save_path) + full_path = joinpath(save_path, title * data_type) + + @debug "Attempting to save figure to: $full_path" + @debug "Current working directory: $(pwd())" + + try + save(full_path, fig) + @debug "Figure saved as $data_type" + + if isfile(full_path) + @debug "File successfully saved to $full_path" + @debug "File size: $(filesize(full_path)) bytes" + else + @info "File does not exist after save attempt: $full_path" + end + catch e + @error "Error saving figure: $e" + @error "Error type: $(typeof(e))" + rethrow(e) + end +end + +""" + show_plot(fig; dpi=130) + +Display a Makie figure. + +# Arguments +- `fig`: Makie Figure object + +# Keyword arguments +- `dpi`: Dots per inch for the figure (default: 130) - currently unused in Makie +""" +function VortexStepMethod.show_plot(fig; dpi=130) + display(fig) +end + +""" + plot_line_segment_makie!(ax, segment, color, label; width=3) + +Plot a line segment in 3D with arrow using Makie. + +# Arguments +- `ax`: Makie Axis3 +- `segment`: Array of two points defining the segment +- `color`: Color of the segment +- `label`: Label for the legend + +# Keyword Arguments +- `width`: Line width (default: 3) +""" +function plot_line_segment_makie!(ax, segment, color, label; width=3) + # Plot line + lines!(ax, [Point3f(segment[1]), Point3f(segment[2])]; + color=color, linewidth=width, label=label) + + # Plot arrow + dir = segment[2] - segment[1] + arrows!(ax, [Point3f(segment[1])], [Point3f(dir)]; + color=color, arrowsize=0.1) +end + +""" + set_axes_equal_makie!(ax, panels; zoom=1.8) + +Set 3D Makie axis to equal scale based on panel data. + +# Arguments +- `ax`: Makie Axis3 +- `panels`: Array of panels +- `zoom`: zoom factor (default: 1.8) +""" +function set_axes_equal_makie!(ax, panels; zoom=1.8) + # Calculate bounds from all panels + all_x = Float64[] + all_y = Float64[] + all_z = Float64[] + + for panel in panels + for i in 1:4 + push!(all_x, panel.corner_points[1, i]) + push!(all_y, panel.corner_points[2, i]) + push!(all_z, panel.corner_points[3, i]) + end + end + + x_range = (maximum(all_x) - minimum(all_x)) / zoom + y_range = (maximum(all_y) - minimum(all_y)) / zoom + z_range = (maximum(all_z) - minimum(all_z)) / zoom + + max_range = max(x_range, y_range, z_range) + + x_mid = mean([maximum(all_x), minimum(all_x)]) + y_mid = mean([maximum(all_y), minimum(all_y)]) + z_mid = mean([maximum(all_z), minimum(all_z)]) + + limits!(ax, + x_mid - max_range/2, x_mid + max_range/2, + y_mid - max_range/2, y_mid + max_range/2, + z_mid - max_range/2, z_mid + max_range/2) +end + +""" + create_geometry_plot_makie(body_aero::BodyAerodynamics, title, + view_elevation, view_azimuth; zoom=1.8) + +Create a 3D Makie plot of wing geometry including panels and filaments. + +# Arguments +- `body_aero`: struct of type BodyAerodynamics +- `title`: plot title +- `view_elevation`: initial view elevation angle [°] +- `view_azimuth`: initial view azimuth angle [°] + +# Keyword arguments +- `zoom`: zoom factor (default: 1.8) +""" +function create_geometry_plot_makie(body_aero::BodyAerodynamics, title, + view_elevation, view_azimuth; zoom=1.8) + panels = body_aero.panels + va = isa(body_aero.va, Tuple) ? body_aero.va[1] : body_aero.va + + # Create figure + fig = Figure(size=(1400, 1400)) + ax = Axis3(fig[1, 1]; + title=title, + xlabel="x", ylabel="y", zlabel="z", + aspect=:data, + azimuth=deg2rad(view_azimuth), + elevation=deg2rad(view_elevation)) + + # Plot panels + legend_used = Dict{String,Bool}() + for (i, panel) in enumerate(panels) + # Panel edges + corners = [Point3f(panel.corner_points[:, j]) for j in 1:4] + push!(corners, corners[1]) + lines!(ax, corners; color=:grey, linewidth=1, + label = i == 1 ? "Panel Edges" : nothing) + + # Control points + scatter!(ax, [Point3f(panel.control_point)]; + color=:green, markersize=10, + label = i == 1 ? "Control Points" : nothing) + + # Aerodynamic centers + scatter!(ax, [Point3f(panel.aero_center)]; + color=:blue, markersize=10, + label = i == 1 ? "Aerodynamic Centers" : nothing) + + # Plot filaments + filaments = calculate_filaments_for_plotting(panel) + legends = ["Bound Vortex", "side1", "side2", "wake_1", "wake_2"] + + for (filament, legend) in zip(filaments, legends) + x1, x2, color = filament + show_legend = !get(legend_used, legend, false) + plot_line_segment_makie!(ax, [x1, x2], color, + show_legend ? legend : nothing) + legend_used[legend] = true + end + end + + # Plot velocity vector + max_chord = maximum(panel.chord for panel in panels) + va_mag = norm(va) + va_vector_begin = -2 * max_chord * va / va_mag + va_vector_end = va_vector_begin + 1.5 * va / va_mag + plot_line_segment_makie!(ax, [va_vector_begin, va_vector_end], :lightblue, "va") + + # Set equal axes + set_axes_equal_makie!(ax, panels; zoom) + + # Add legend + axislegend(ax; position=:lt) + + return fig +end + +""" + plot_geometry(body_aero::BodyAerodynamics, title; + data_type=".pdf", save_path=nothing, + is_save=false, is_show=false, + view_elevation=15, view_azimuth=-120, use_tex=false) + +Plot wing geometry from different viewpoints using Makie. + +# Arguments: +- `body_aero`: the BodyAerodynamics to plot +- `title`: plot title + +# Keyword arguments: +- `data_type`: File extension (default: ".pdf") +- `save_path`: Path for saving (default: nothing) +- `is_save`: Whether to save (default: false) +- `is_show`: Whether to display (default: false) +- `view_elevation`: View elevation angle [°] (default: 15) +- `view_azimuth`: View azimuth angle [°] (default: -120) +- `use_tex`: Ignored for Makie (default: false) +""" +function VortexStepMethod.plot_geometry(body_aero::BodyAerodynamics, title; + data_type=".pdf", + save_path=nothing, + is_save=false, + is_show=false, + view_elevation=15, + view_azimuth=-120, + use_tex=false) + + if is_save + # Angled view + fig = create_geometry_plot_makie(body_aero, "$(title)_angled_view", 15, -120) + save_plot(fig, save_path, "$(title)_angled_view", data_type=data_type) + + # Top view + fig = create_geometry_plot_makie(body_aero, "$(title)_top_view", 90, 0) + save_plot(fig, save_path, "$(title)_top_view", data_type=data_type) + + # Front view + fig = create_geometry_plot_makie(body_aero, "$(title)_front_view", 0, 0) + save_plot(fig, save_path, "$(title)_front_view", data_type=data_type) + + # Side view + fig = create_geometry_plot_makie(body_aero, "$(title)_side_view", 0, -90) + save_plot(fig, save_path, "$(title)_side_view", data_type=data_type) + end + + fig = create_geometry_plot_makie(body_aero, title, view_elevation, view_azimuth) + + if is_show + display(fig) + end + + return fig +end + +""" + plot_distribution(y_coordinates_list, results_list, label_list; + title="spanwise_distribution", data_type=".pdf", + save_path=nothing, is_save=false, is_show=true, use_tex=false) + +Plot spanwise distributions of aerodynamic properties using Makie. + +# Arguments +- `y_coordinates_list`: List of spanwise coordinates +- `results_list`: List of result dictionaries +- `label_list`: List of labels for different results + +# Keyword arguments +- `title`: Plot title (default: "spanwise_distribution") +- `data_type`: File extension (default: ".pdf") +- `save_path`: Path to save plots (default: nothing) +- `is_save`: Whether to save (default: false) +- `is_show`: Whether to display (default: true) +- `use_tex`: Ignored for Makie (default: false) +""" +function VortexStepMethod.plot_distribution(y_coordinates_list, results_list, label_list; + title="spanwise_distribution", + data_type=".pdf", + save_path=nothing, + is_save=false, + is_show=true, + use_tex=false) + + length(results_list) == length(label_list) || throw(ArgumentError( + "Number of results ($(length(results_list))) must match labels ($(length(label_list)))" + )) + + # Create figure with 3x3 grid + fig = Figure(size=(1600, 1000)) + Label(fig[0, :], title, fontsize=20) + + # Row 1: CL, CD, Gamma + ax_cl = Axis(fig[1, 1], title="CL Distribution", + xlabel="Spanwise Position y/b", ylabel="Lift Coefficient CL") + ax_cd = Axis(fig[1, 2], title="CD Distribution", + xlabel="Spanwise Position y/b", ylabel="Drag Coefficient CD") + ax_gamma = Axis(fig[1, 3], title="Γ Distribution", + xlabel="Spanwise Position y/b", ylabel="Circulation Γ") + + # Row 2: Alpha geometric, alpha at ac, alpha uncorrected + ax_alpha_geo = Axis(fig[2, 1], title="α Geometric", + xlabel="Spanwise Position y/b", ylabel="Angle of Attack α (deg)") + ax_alpha_ac = Axis(fig[2, 2], title="α result (corrected to aerodynamic center)", + xlabel="Spanwise Position y/b", ylabel="Angle of Attack α (deg)") + ax_alpha_unc = Axis(fig[2, 3], title="α Uncorrected (if VSM, at control point)", + xlabel="Spanwise Position y/b", ylabel="Angle of Attack α (deg)") + + # Row 3: Force components + ax_fx = Axis(fig[3, 1], title="Force in x direction", + xlabel="Spanwise Position y/b", ylabel="Fx") + ax_fy = Axis(fig[3, 2], title="Force in y direction", + xlabel="Spanwise Position y/b", ylabel="Fy") + ax_fz = Axis(fig[3, 3], title="Force in z direction", + xlabel="Spanwise Position y/b", ylabel="Fz") + + # Plot CL + for (y_coords, results, label) in zip(y_coordinates_list, results_list, label_list) + value = round(results["cl"], digits=2) + lines!(ax_cl, Vector(y_coords), Vector(results["cl_distribution"]), + label="$label CL: $value") + end + axislegend(ax_cl, position=:lt) + + # Plot CD + for (y_coords, results, label) in zip(y_coordinates_list, results_list, label_list) + value = round(results["cd"], digits=2) + lines!(ax_cd, Vector(y_coords), Vector(results["cd_distribution"]), + label="$label CD: $value") + end + axislegend(ax_cd, position=:lt) + + # Plot Gamma + for (y_coords, results, label) in zip(y_coordinates_list, results_list, label_list) + lines!(ax_gamma, Vector(y_coords), Vector(results["gamma_distribution"]), + label=label) + end + axislegend(ax_gamma, position=:lt) + + # Plot alpha geometric + for (y_coords, results, label) in zip(y_coordinates_list, results_list, label_list) + lines!(ax_alpha_geo, Vector(y_coords), Vector(results["alpha_geometric"]), + label=label) + end + axislegend(ax_alpha_geo, position=:lt) + + # Plot alpha at ac + for (y_coords, results, label) in zip(y_coordinates_list, results_list, label_list) + lines!(ax_alpha_ac, Vector(y_coords), Vector(results["alpha_at_ac"]), + label=label) + end + axislegend(ax_alpha_ac, position=:lt) + + # Plot alpha uncorrected + for (y_coords, results, label) in zip(y_coordinates_list, results_list, label_list) + lines!(ax_alpha_unc, Vector(y_coords), Vector(results["alpha_uncorrected"]), + label=label) + end + axislegend(ax_alpha_unc, position=:lt) + + # Plot force components + force_axes = [ax_fx, ax_fy, ax_fz] + components = ["x", "y", "z"] + for (idx, (ax, comp)) in enumerate(zip(force_axes, components)) + for (y_coords, results, label) in zip(y_coordinates_list, results_list, label_list) + forces = results["F_distribution"][idx, :] + if length(y_coords) != length(forces) + @warn "Dimension mismatch" length(y_coords) length(forces) comp + continue + end + total_force = round(results["F$comp"], digits=2) + lines!(ax, Vector(y_coords), Vector(forces), + label="$label ΣF$comp: $total_force N") + end + axislegend(ax, position=:lt) + end + + # Save and show + if is_save + save_plot(fig, save_path, title, data_type=data_type) + end + + if is_show + display(fig) + end + + return fig +end + +""" + generate_polar_data(solver, body_aero::BodyAerodynamics, angle_range; + angle_type="angle_of_attack", angle_of_attack=0.0, + side_slip=0.0, v_a=10.0) + +Generate polar data for aerodynamic analysis over a range of angles. + +# Arguments +- `solver`: Aerodynamic solver object +- `body_aero`: Wing aerodynamics struct +- `angle_range`: Range of angles to analyze + +# Keyword arguments +- `angle_type`: Type of angle variation ("angle_of_attack" or "side_slip") +- `angle_of_attack`: Initial angle of attack [rad] +- `side_slip`: Initial side slip angle [rad] +- `v_a`: norm of apparent wind speed [m/s] + +# Returns +- Tuple of polar data array and Reynolds number +""" +function generate_polar_data_makie( + solver, + body_aero::BodyAerodynamics, + angle_range; + angle_type="angle_of_attack", + angle_of_attack=0.0, + side_slip=0.0, + v_a=10.0 +) + n_panels = length(body_aero.panels) + n_angles = length(angle_range) + + # Initialize arrays + cl = zeros(n_angles) + cd = zeros(n_angles) + cs = zeros(n_angles) + gamma_distribution = zeros(n_angles, n_panels) + reynolds_number = zeros(n_angles) + + for (i, angle_i) in enumerate(angle_range) + # Set angle based on type + if angle_type == "angle_of_attack" + α = deg2rad(angle_i) + β = side_slip + elseif angle_type == "side_slip" + α = angle_of_attack + β = deg2rad(angle_i) + else + throw(ArgumentError("angle_type must be 'angle_of_attack' or 'side_slip'")) + end + + # Update inflow conditions + set_va!( + body_aero, + [ + cos(α) * cos(β), + sin(β), + sin(α) + ] * v_a + ) + + # Solve and store results + results = solve(solver, body_aero, gamma_distribution[i, :]) + + cl[i] = results["cl"] + cd[i] = results["cd"] + cs[i] = results["cs"] + gamma_distribution[i, :] = results["gamma_distribution"] + reynolds_number[i] = results["Rey"] + end + + polar_data = [angle_range, cl, cd, cs] + return polar_data, reynolds_number[1] +end + +""" + plot_polars(solver_list, body_aero_list, label_list; + literature_path_list=String[], + angle_range=range(0, 20, 2), angle_type="angle_of_attack", + angle_of_attack=0.0, side_slip=0.0, v_a=10.0, + title="polar", data_type=".pdf", save_path=nothing, + is_save=true, is_show=true, use_tex=false) + +Plot polar data comparing different solvers using Makie. + +# Arguments +- `solver_list`: List of aerodynamic solvers +- `body_aero_list`: List of wing aerodynamics objects +- `label_list`: List of labels for each configuration + +# Keyword arguments +- `literature_path_list`: Optional paths to literature data files +- `angle_range`: Range of angles [°] +- `angle_type`: "angle_of_attack" or "side_slip" (default: angle_of_attack) +- `angle_of_attack`: AoA [rad] (default: 0.0) +- `side_slip`: Side slip angle [rad] (default: 0.0) +- `v_a`: Wind speed [m/s] (default: 10.0) +- `title`: Plot title +- `data_type`: File extension (default: ".pdf") +- `save_path`: Path to save (default: nothing) +- `is_save`: Whether to save (default: true) +- `is_show`: Whether to display (default: true) +- `use_tex`: Ignored for Makie (default: false) +""" +function VortexStepMethod.plot_polars( + solver_list, + body_aero_list, + label_list; + literature_path_list=String[], + angle_range=range(0, 20, 2), + angle_type="angle_of_attack", + angle_of_attack=0.0, + side_slip=0.0, + v_a=10.0, + title="polar", + data_type=".pdf", + save_path=nothing, + is_save=true, + is_show=true, + use_tex=false +) + # Validate inputs + total_cases = length(body_aero_list) + length(literature_path_list) + if total_cases != length(label_list) || length(solver_list) != length(body_aero_list) + throw(ArgumentError("Mismatch in solvers/cases/labels")) + end + main_title = replace(title, " " => "_") + + # Generate polar data + polar_data_list = [] + labels_with_re = copy(label_list) + for (i, (solver, body_aero)) in enumerate(zip(solver_list, body_aero_list)) + polar_data, rey = generate_polar_data_makie( + solver, body_aero, angle_range; + angle_type, angle_of_attack, side_slip, v_a + ) + push!(polar_data_list, polar_data) + labels_with_re[i] = "$(label_list[i]) Re = $(round(Int64, rey*1e-5))e5" + end + + # Load literature data if provided + if !isempty(literature_path_list) + for path in literature_path_list + data = readdlm(path, ',') + header = lowercase.(string.(data[1, :])) + alpha_idx = findfirst(x -> occursin("alpha", x), header) + cl_idx = findfirst(x -> occursin("cl", x), header) + cd_idx = findfirst(x -> occursin("cd", x), header) + cs_idx = findfirst(x -> occursin("cs", x), header) + cs_col = cs_idx === nothing ? zeros(size(data, 1)-1) : data[2:end, cs_idx] + push!(polar_data_list, [ + data[2:end, alpha_idx], + data[2:end, cl_idx], + data[2:end, cd_idx], + cs_col + ]) + end + end + + # Create figure with 2x2 grid + fig = Figure(size=(1400, 1400)) + + ax_cl = Axis(fig[1, 1], title="CL vs $angle_type [°]", + xlabel="$angle_type [°]", ylabel="CL") + ax_cd = Axis(fig[1, 2], title="CD vs $angle_type [°]", + xlabel="$angle_type [°]", ylabel="CD") + ax_cs = Axis(fig[2, 1], title="CS vs $angle_type [°]", + xlabel="$angle_type [°]", ylabel="CS") + ax_polar = Axis(fig[2, 2], title="CL vs CD", + xlabel="CD", ylabel="CL") + + # Number of computational results + n_solvers = length(solver_list) + + # Plot CL vs angle + for (i, (polar_data, label)) in enumerate(zip(polar_data_list, labels_with_re)) + marker = i <= n_solvers ? :star5 : :circle + markersize = i <= n_solvers ? 12 : 8 + scatterlines!(ax_cl, polar_data[1], polar_data[2]; + label=label, marker=marker, markersize=markersize) + if maximum(polar_data[2]) > 10 + ylims!(ax_cl, -0.5, 2) + end + end + axislegend(ax_cl, position=:lt) + + # Plot CD vs angle + for (i, (polar_data, label)) in enumerate(zip(polar_data_list, labels_with_re)) + marker = i <= n_solvers ? :star5 : :circle + markersize = i <= n_solvers ? 12 : 8 + scatterlines!(ax_cd, polar_data[1], polar_data[3]; + label=label, marker=marker, markersize=markersize) + if maximum(polar_data[2]) > 10 + ylims!(ax_cd, -0.5, 2) + end + end + axislegend(ax_cd, position=:lt) + + # Plot CS vs angle + for (i, (polar_data, label)) in enumerate(zip(polar_data_list, labels_with_re)) + marker = i <= n_solvers ? :star5 : :circle + markersize = i <= n_solvers ? 12 : 8 + scatterlines!(ax_cs, polar_data[1], polar_data[4]; + label=label, marker=marker, markersize=markersize) + if maximum(polar_data[2]) > 10 + ylims!(ax_cs, -0.5, 2) + end + end + axislegend(ax_cs, position=:lt) + + # Plot CL vs CD + for (i, (polar_data, label)) in enumerate(zip(polar_data_list, labels_with_re)) + marker = i <= n_solvers ? :star5 : :circle + markersize = i <= n_solvers ? 12 : 8 + scatterlines!(ax_polar, polar_data[3], polar_data[2]; + label=label, marker=marker, markersize=markersize) + if maximum(polar_data[2]) > 10 || maximum(polar_data[3]) > 10 + ylims!(ax_polar, -0.5, 2) + xlims!(ax_polar, -0.5, 2) + end + end + axislegend(ax_polar, position=:lt) + + # Save and show + if is_save && !isnothing(save_path) + save_plot(fig, save_path, main_title; data_type) + end + + if is_show + display(fig) + end + + return fig +end + +""" + plot_polar_data(body_aero::BodyAerodynamics; + alphas=collect(deg2rad.(-5:0.3:25)), + delta_tes=collect(deg2rad.(-5:0.3:25)), + is_show=true, use_tex=false) + +Plot polar data (Cl, Cd, Cm) as 3D surfaces using Makie. + +# Arguments +- `body_aero`: Wing aerodynamics struct + +# Keyword arguments +- `alphas`: Range of AoA values [rad] (default: -5° to 25° in 0.3° steps) +- `delta_tes`: Range of trailing edge angles [rad] (default: -5° to 25° in 0.3° steps) +- `is_show`: Whether to display (default: true) +- `use_tex`: Ignored for Makie (default: false) +""" +function VortexStepMethod.plot_polar_data(body_aero::BodyAerodynamics; + alphas=collect(deg2rad.(-5:0.3:25)), + delta_tes=collect(deg2rad.(-5:0.3:25)), + is_show=true, + use_tex=false) + + if body_aero.panels[1].aero_model == POLAR_MATRICES + # Create figure with 3 subplots + fig = Figure(size=(1500, 600)) + + # Get interpolation functions + interp_data = [ + (body_aero.panels[1].cl_interp, "Cl"), + (body_aero.panels[1].cd_interp, "Cd"), + (body_aero.panels[1].cm_interp, "Cm") + ] + + # Create each subplot + for (idx, (interp, label)) in enumerate(interp_data) + ax = Axis3(fig[1, idx]; + title="$label vs α and δ", + xlabel="δ [rad]", + ylabel="α [rad]", + zlabel=label, + azimuth=1.275*π) + + # Create interpolation matrix + interp_matrix = [interp(alpha, delta_te) + for alpha in alphas, delta_te in delta_tes] + + # Create wireframe + wireframe!(ax, delta_tes, alphas, interp_matrix; + color=:blue, linewidth=0.5, transparency=true) + end + + if is_show + display(fig) + end + return fig + else + throw(ArgumentError( + "Plotting polar data for $(body_aero.panels[1].aero_model) not implemented." + )) + end +end + end diff --git a/src/panel.jl b/src/panel.jl index 23886f4d..ba2e2448 100644 --- a/src/panel.jl +++ b/src/panel.jl @@ -535,4 +535,4 @@ function calculate_velocity_induced_bound_2D!( cross_square .= cross_.^2 U_2D .= (cross_ ./ sum(cross_square) ./ 2π) .* norm(r0) return nothing -end \ No newline at end of file +end diff --git a/src/plotting.jl b/src/plotting.jl deleted file mode 100644 index 3c602edb..00000000 --- a/src/plotting.jl +++ /dev/null @@ -1,920 +0,0 @@ - -""" - set_plot_style(titel_size=16; use_tex=false) - -Set the default style for plots using LaTeX. -`` -# Arguments: -- `titel_size`: size of the plot title in points (default: 16) -- `ùse_tex`: if the external `pdflatex` command shall be used -""" -function set_plot_style(titel_size=16; use_tex=false) - rcParams = plt.PyDict(plt.matplotlib."rcParams") - rcParams["text.usetex"] = use_tex - rcParams["font.family"] = "serif" - if use_tex - rcParams["font.serif"] = ["Computer Modern Roman"] - end - rcParams["axes.titlesize"] = titel_size - rcParams["axes.labelsize"] = 12 - rcParams["axes.linewidth"] = 1 - rcParams["lines.linewidth"] = 1 - rcParams["lines.markersize"] = 6 - rcParams["xtick.labelsize"] = 10 - rcParams["ytick.labelsize"] = 10 - rcParams["legend.fontsize"] = 10 - rcParams["figure.titlesize"] = 16 - if use_tex - rcParams["pgf.texsystem"] = "pdflatex" # Use pdflatex - end - rcParams["pgf.rcfonts"] = false - rcParams["figure.figsize"] = (10, 6) # Default figure size -end - - -""" - save_plot(fig, save_path, title; data_type=".pdf") - -Save a plot to a file. - -# Arguments -- `fig`: Plots figure object -- `save_path`: Path to save the plot -- `title`: Title of the plot - -# Keyword arguments -- `data_type`: File extension (default: ".pdf") -""" -function VortexStepMethod.save_plot(fig, save_path, title; data_type=".pdf") - isnothing(save_path) && throw(ArgumentError("save_path should be provided")) - - !isdir(save_path) && mkpath(save_path) - full_path = joinpath(save_path, title * data_type) - - @debug "Attempting to save figure to: $full_path" - @debug "Current working directory: $(pwd())" - - try - fig.savefig(full_path) - @debug "Figure saved as $data_type" - - if isfile(full_path) - @debug "File successfully saved to $full_path" - @debug "File size: $(filesize(full_path)) bytes" - else - @info "File does not exist after save attempt: $full_path" - end - catch e - @error "Error saving figure: $e" - @error "Error type: $(typeof(e))" - rethrow(e) - end -end - -""" - show_plot(fig; dpi=130) - -Display a plot at specified DPI. - -# Arguments -- `fig`: Plots figure object - -# Keyword arguments -- `dpi`: Dots per inch for the figure (default: 130) -""" -function VortexStepMethod.show_plot(fig; dpi=130) - plt.display(fig) -end - -""" - plot_line_segment!(ax, segment, color, label; width=3) - -Plot a line segment in 3D with arrow. - -# Arguments -- `ax`: Plot axis -- `segment`: Array of two points defining the segment -- `color`: Color of the segment -- `label`: Label for the legend - -# Keyword Arguments -- `width`: Line width (default: 3) -""" -function plot_line_segment!(ax, segment, color, label; width=3) - ax.plot( - [segment[1][1], segment[2][1]], - [segment[1][2], segment[2][2]], - [segment[1][3], segment[2][3]], - color=color, label=label, linewidth=width - ) - - dir = segment[2] - segment[1] - ax.quiver( - [segment[1][1]], [segment[1][2]], [segment[1][3]], - [dir[1]], [dir[2]], [dir[3]], - color=color - ) -end - -""" - set_axes_equal!(ax; zoom=1.8) - -Set 3D plot axes to equal scale. - -# Arguments -- ax: 3D plot axis - -# Keyword arguments -zoom: zoom factor (default: 1.8) -""" -function set_axes_equal!(ax; zoom=1.8) - x_lims = ax.get_xlim3d() ./ zoom - y_lims = ax.get_ylim3d() ./ zoom - z_lims = ax.get_zlim3d() ./ zoom - - x_range = abs(x_lims[2] - x_lims[1]) - y_range = abs(y_lims[2] - y_lims[1]) - z_range = abs(z_lims[2] - z_lims[1]) - - max_range = max(x_range, y_range, z_range) - - x_mid = mean(x_lims) - y_mid = mean(y_lims) - z_mid = mean(z_lims) - - ax.set_xlim3d((x_mid - max_range / 2, x_mid + max_range / 2)) - ax.set_ylim3d((y_mid - max_range / 2, y_mid + max_range / 2)) - ax.set_zlim3d((z_mid - max_range / 2, z_mid + max_range / 2)) -end - -""" - create_geometry_plot(body_aero::BodyAerodynamics, title, view_elevation, view_azimuth; - zoom=1.8, use_tex=false) - -Create a 3D plot of wing geometry including panels and filaments. - -# Arguments -- body_aero: struct of type BodyAerodynamics -- title: plot title -- view_elevation: initial view elevation angle [°] -- view_azimuth: initial view azimuth angle [°] - -# Keyword arguments -- zoom: zoom factor (default: 1.8) -""" -function create_geometry_plot(body_aero::BodyAerodynamics, title, view_elevation, view_azimuth; - zoom=1.8, use_tex=false) - set_plot_style(28; use_tex) - - panels = body_aero.panels - va = isa(body_aero.va, Tuple) ? body_aero.va[1] : body_aero.va - - # Extract geometric data - corner_points = [panel.corner_points for panel in panels] - control_points = [panel.control_point for panel in panels] - aero_centers = [panel.aero_center for panel in panels] - - # Create plot - fig = plt.figure(figsize=(14, 14)) - ax = fig.add_subplot(111, projection="3d") - ax.set_title(title) - - # Plot panels - legend_used = Dict{String,Bool}() - for (i, panel) in enumerate(panels) - # Plot panel edges and surfaces - corners = Matrix{Float64}(panel.corner_points) - x_corners = corners[1, :] - y_corners = corners[2, :] - z_corners = corners[3, :] - - push!(x_corners, x_corners[1]) - push!(y_corners, y_corners[1]) - push!(z_corners, z_corners[1]) - - ax.plot(x_corners, - y_corners, - z_corners, - color=:grey, - linewidth=1, - label=i == 1 ? "Panel Edges" : "") - - # Plot control points and aerodynamic centers - ax.scatter([control_points[i][1]], [control_points[i][2]], [control_points[i][3]], - color=:green, label=i == 1 ? "Control Points" : "") - ax.scatter([aero_centers[i][1]], [aero_centers[i][2]], [aero_centers[i][3]], - color=:blue, label=i == 1 ? "Aerodynamic Centers" : "") - - # Plot filaments - filaments = calculate_filaments_for_plotting(panel) - legends = ["Bound Vortex", "side1", "side2", "wake_1", "wake_2"] - - for (filament, legend) in zip(filaments, legends) - x1, x2, color = filament - @debug "Legend: $legend" - show_legend = !get(legend_used, legend, false) - plot_line_segment!(ax, [x1, x2], color, show_legend ? legend : "") - legend_used[legend] = true - end - end - - # Plot velocity vector - max_chord = maximum(panel.chord for panel in panels) - va_mag = norm(va) - va_vector_begin = -2 * max_chord * va / va_mag - va_vector_end = va_vector_begin + 1.5 * va / va_mag - plot_line_segment!(ax, [va_vector_begin, va_vector_end], :lightblue, "va") - - # Add legends for the first occurrence of each label - handles, labels = ax.get_legend_handles_labels() - # by_label = Dict(zip(labels, handles)) - # ax.legend(values(by_label), keys(by_label), bbox_to_anchor=(0, 0, 1.1, 1)) - - # Set labels and make axes equal - ax.set_xlabel("x") - ax.set_ylabel("y") - ax.set_zlabel("z") - set_axes_equal!(ax; zoom) - - # Set the initial view - ax.view_init(elev=view_elevation, azim=view_azimuth) - - # Ensure the figure is fully rendered - # fig.canvas.draw() - plt.tight_layout(rect=(0, 0, 1, 0.97)) - - return fig -end - -""" - plot_geometry(body_aero::BodyAerodynamics, title; - data_type=".pdf", save_path=nothing, - is_save=false, is_show=false, - view_elevation=15, view_azimuth=-120, use_tex=false) - -Plot wing geometry from different viewpoints and optionally save/show plots. - -# Arguments: -- `body_aero`: the [BodyAerodynamics](@ref) to plot -- title: plot title - -# Keyword arguments: -- `data_type``: string with the file type postfix (default: ".pdf") -- `save_path`: path for saving the graphic (default: `nothing`) -- `is_save`: boolean value, indicates if the graphic shall be saved (default: `false`) -- `is_show`: boolean value, indicates if the graphic shall be displayed (default: `false`) -- `view_elevation`: initial view elevation angle (default: 15) [°] -- `view_azimuth`: initial view azimuth angle (default: -120) [°] -- `use_tex`: if the external `pdflatex` command shall be used (default: false) - -""" -function VortexStepMethod.plot_geometry(body_aero::BodyAerodynamics, title; - data_type=".pdf", - save_path=nothing, - is_save=false, - is_show=false, - view_elevation=15, - view_azimuth=-120, - use_tex=false) - - if is_save - plt.ioff() - # Angled view - fig = create_geometry_plot(body_aero, "$(title)_angled_view", 15, -120; use_tex) - save_plot(fig, save_path, "$(title)_angled_view", data_type=data_type) - - # Top view - fig = create_geometry_plot(body_aero, "$(title)_top_view", 90, 0; use_tex) - save_plot(fig, save_path, "$(title)_top_view", data_type=data_type) - - # Front view - fig = create_geometry_plot(body_aero, "$(title)_front_view", 0, 0; use_tex) - save_plot(fig, save_path, "$(title)_front_view", data_type=data_type) - - # Side view - fig = create_geometry_plot(body_aero, "$(title)_side_view", 0, -90; use_tex) - save_plot(fig, save_path, "$(title)_side_view", data_type=data_type) - end - - if is_show - plt.ion() - fig = create_geometry_plot(body_aero, title, view_elevation, view_azimuth; use_tex) - plt.display(fig) - else - fig = create_geometry_plot(body_aero, title, view_elevation, view_azimuth; use_tex) - end - fig -end - -""" - plot_distribution(y_coordinates_list, results_list, label_list; - title="spanwise_distribution", data_type=".pdf", - save_path=nothing, is_save=false, is_show=true, use_tex=false) - -Plot spanwise distributions of aerodynamic properties. - -# Arguments -- `y_coordinates_list`: List of spanwise coordinates -- `results_list`: List of result dictionaries -- `label_list`: List of labels for different results - -# Keyword arguments -- `title`: Plot title (default: "spanwise_distribution") -- `data_type`: File extension for saving (default: ".pdf") -- `save_path`: Path to save plots (default: nothing) -- `is_save`: Whether to save plots (default: false) -- `is_show`: Whether to display plots (default: true) -- `use_tex`: if the external `pdflatex` command shall be used -""" -function VortexStepMethod.plot_distribution(y_coordinates_list, results_list, label_list; - title="spanwise_distribution", - data_type=".pdf", - save_path=nothing, - is_save=false, - is_show=true, - use_tex=false) - - length(results_list) == length(label_list) || throw(ArgumentError( - "Number of results ($(length(results_list))) must match number of labels ($(length(label_list)))" - )) - - # Set the plot style - set_plot_style(; use_tex) - - # Initializing plot - fig, axs = plt.subplots(3, 3, figsize=(16, 10)) - fig.suptitle(title, fontsize=16) - - # CL plot - for (y_coordinates_i, result_i, label_i) in zip(y_coordinates_list, results_list, label_list) - value = "$(round(result_i["cl"], digits=2))" - if label_i == "LLT" - label = label_i * L" $~C_\mathrm{L}$: " * value - else - label = label_i * L" $C_\mathrm{L}$: " * value - end - axs[1, 1].plot( - y_coordinates_i, - result_i["cl_distribution"], - label=label - ) - end - axs[1, 1].set_title(L"$C_\mathrm{L}$ Distribution", size=16) - axs[1, 1].set_xlabel(L"Spanwise Position $y/b$") - axs[1, 1].set_ylabel(L"Lift Coefficient $C_\mathrm{L}$") - axs[1, 1].legend() - - # CD plot - for (y_coordinates_i, result_i, label_i) in zip(y_coordinates_list, results_list, label_list) - value = "$(round(result_i["cl"], digits=2))" - if label_i == "LLT" - label = label_i * L" $~C_\mathrm{D}$: " * value - else - label = label_i * L" $C_\mathrm{D}$: " * value - end - axs[1, 2].plot( - y_coordinates_i, - result_i["cd_distribution"], - label=label - ) - end - axs[1, 2].set_title(L"$C_\mathrm{D}$ Distribution", size=16) - axs[1, 2].set_xlabel(L"Spanwise Position $y/b$") - axs[1, 2].set_ylabel(L"Drag Coefficient $C_\mathrm{D}$") - axs[1, 2].legend() - - # Gamma Distribution - for (y_coordinates_i, result_i, label_i) in zip(y_coordinates_list, results_list, label_list) - axs[1, 3].plot( - y_coordinates_i, - result_i["gamma_distribution"], - label=label_i - ) - end - axs[1, 3].set_title(L"\Gamma~Distribution", size=16) - axs[1, 3].set_xlabel(L"Spanwise Position $y/b$") - axs[1, 3].set_ylabel(L"Circulation~\Gamma") - axs[1, 3].legend() - - # Geometric Alpha - for (y_coordinates_i, result_i, label_i) in zip(y_coordinates_list, results_list, label_list) - axs[2, 1].plot( - y_coordinates_i, - result_i["alpha_geometric"], - label=label_i - ) - end - axs[2, 1].set_title(L"$\alpha$ Geometric", size=16) - axs[2, 1].set_xlabel(L"Spanwise Position $y/b$") - axs[2, 1].set_ylabel(L"Angle of Attack $\alpha$ (deg)") - axs[2, 1].legend() - - # Calculated/ Corrected Alpha - for (y_coordinates_i, result_i, label_i) in zip(y_coordinates_list, results_list, label_list) - axs[2, 2].plot( - y_coordinates_i, - result_i["alpha_at_ac"], - label=label_i - ) - end - axs[2, 2].set_title(L"$\alpha$ result (corrected to aerodynamic center)", size=16) - axs[2, 2].set_xlabel(L"Spanwise Position $y/b$") - axs[2, 2].set_ylabel(L"Angle of Attack $\alpha$ (deg)") - axs[2, 2].legend() - - # Uncorrected Alpha plot - for (y_coordinates_i, result_i, label_i) in zip(y_coordinates_list, results_list, label_list) - axs[2, 3].plot( - y_coordinates_i, - result_i["alpha_uncorrected"], - label=label_i - ) - end - axs[2, 3].set_title(L"$\alpha$ Uncorrected (if VSM, at the control point)", size=16) - axs[2, 3].set_xlabel(L"Spanwise Position $y/b$") - axs[2, 3].set_ylabel(L"Angle of Attack $\alpha$ (deg)") - axs[2, 3].legend() - - # Force Components - for (idx, component) in enumerate(["x", "y", "z"]) - axs[3, idx].set_title("Force in $component direction", size=16) - axs[3, idx].set_xlabel(L"Spanwise Position $y/b$") - axs[3, idx].set_ylabel(raw"$F_\mathrm" * "{$component}" * raw"$") - for (y_coords, results, label) in zip(y_coordinates_list, results_list, label_list) - # Extract force components for the current direction (idx) - forces = results["F_distribution"][idx, :] - # Verify dimensions match - if length(y_coords) != length(forces) - @warn "Dimension mismatch in force plotting" length(y_coords) length(forces) component - continue # Skip this component instead of throwing error - end - space = "" - if label == "LLT" - space = "~" - end - axs[3, idx].plot( - y_coords, - forces, - label="$label" * space * raw"$~\Sigma~F_\mathrm" * "{$component}:~" * - raw"$" * "$(round(results["F$component"], digits=2)) N" - ) - axs[3, idx].legend() - end - end - - fig.tight_layout() - - # Save and show plot - if is_save - save_plot(fig, save_path, title, data_type=data_type) - end - - if is_show - show_plot(fig) - end - - return fig -end - -""" - generate_polar_data(solver, body_aero::BodyAerodynamics, angle_range; - angle_type="angle_of_attack", angle_of_attack=0.0, - side_slip=0.0, v_a=10.0, use_latex=false) - -Generate polar data for aerodynamic analysis over a range of angles. - -# Arguments -- `solver`: Aerodynamic solver object -- `body_aero`: Wing aerodynamics struct -- `angle_range`: Range of angles to analyze - -# Keyword arguments -- `angle_type`: Type of angle variation ("angle_of_attack" or "side_slip") -- `angle_of_attack`: Initial angle of attack [rad] -- `side_slip`: Initial side slip angle in [rad] -- `v_a`: norm of apparent wind speed [m/s] - -# Returns -- Tuple of polar data array and Reynolds number -""" -function generate_polar_data( - solver, - body_aero::BodyAerodynamics, - angle_range; - angle_type="angle_of_attack", - angle_of_attack=0.0, - side_slip=0.0, - v_a=10.0, - use_latex=false -) - n_panels = length(body_aero.panels) - n_angles = length(angle_range) - - # Initialize arrays - cl = zeros(n_angles) - cd = zeros(n_angles) - cs = zeros(n_angles) - gamma_distribution = zeros(n_angles, n_panels) - cl_distribution = zeros(n_angles, n_panels) - cd_distribution = zeros(n_angles, n_panels) - cs_distribution = zeros(n_angles, n_panels) - reynolds_number = zeros(n_angles) - - # Previous gamma for initialization - gamma = nothing - - for (i, angle_i) in enumerate(angle_range) - # Set angle based on type - if angle_type == "angle_of_attack" - α = deg2rad(angle_i) - β = side_slip - elseif angle_type == raw"side_slip" - α = angle_of_attack - β = deg2rad(angle_i) - else - throw(ArgumentError("angle_type must be 'angle_of_attack' or 'side_slip'")) - end - - # Update inflow conditions - set_va!( - body_aero, - [ - cos(α) * cos(β), - sin(β), - sin(α) - ] * v_a - ) - - # Solve and store results - results = solve(solver, body_aero, gamma_distribution[i, :]) - - cl[i] = results["cl"] - cd[i] = results["cd"] - cs[i] = results["cs"] - gamma_distribution[i, :] = results["gamma_distribution"] - cl_distribution[i, :] = results["cl_distribution"] - cd_distribution[i, :] = results["cd_distribution"] - cs_distribution[i, :] = results["cs_distribution"] - reynolds_number[i] = results["Rey"] - - # Store gamma for next iteration - gamma = gamma_distribution[i, :] - end - - polar_data = [ - angle_range, - cl, - cd, - cs, - gamma_distribution, - cl_distribution, - cd_distribution, - cs_distribution, - reynolds_number - ] - - return polar_data, reynolds_number[1] -end - -""" - plot_polars(solver_list, body_aero_list, label_list; - literature_path_list=String[], - angle_range=range(0, 20, 2), angle_type="angle_of_attack", - angle_of_attack=0.0, side_slip=0.0, v_a=10.0, - title="polar", data_type=".pdf", save_path=nothing, - is_save=true, is_show=true, use_tex=false) - -Plot polar data comparing different solvers and configurations. - -# Arguments -- `solver_list`: List of aerodynamic solvers -- `body_aero_list`: List of wing aerodynamics objects -- `label_list`: List of labels for each configuration - -# Keyword arguments -- `literature_path_list`: Optional paths to literature data files -- `angle_range`: Range of angles to analyze [°] -- `angle_type`: "`angle_of_attack`" or "`side_slip`"; (default: `angle_of_attack`) -- `angle_of_attack:` AoA to be used for plotting the polars (default: 0.0) [rad] -- `side_slip`: side slip angle (default: 0.0) [rad] -- v_a: norm of apparent wind speed (default: 10.0) [m/s] -- title: plot title -- `data_type`: File extension for saving (default: ".pdf") -- `save_path`: Path to save plots (default: nothing) -- `is_save`: Whether to save plots (default: true) -- `is_show`: Whether to display plots (default: true) -- `use_tex`: if the external `pdflatex` command shall be used (default: false) -""" -function VortexStepMethod.plot_polars( - solver_list, - body_aero_list, - label_list; - literature_path_list=String[], - angle_range=range(0, 20, 2), - angle_type="angle_of_attack", - angle_of_attack=0.0, - side_slip=0.0, - v_a=10.0, - title="polar", - data_type=".pdf", - save_path=nothing, - is_save=true, - is_show=true, - use_tex=false -) - # Validate inputs - total_cases = length(body_aero_list) + length(literature_path_list) - if total_cases != length(label_list) || length(solver_list) != length(body_aero_list) - throw(ArgumentError("Mismatch in number of solvers ($(length(solver_list))), " * - "cases ($total_cases), and labels ($(length(label_list)))")) - end - main_title = replace(title, " " => "_") - set_plot_style(; use_tex) - - # Generate polar data - polar_data_list = [] - for (i, (solver, body_aero)) in enumerate(zip(solver_list, body_aero_list)) - polar_data, rey = generate_polar_data( - solver, body_aero, angle_range; - angle_type, - angle_of_attack, - side_slip, - v_a - ) - push!(polar_data_list, polar_data) - # Update label with Reynolds number - label_list[i] = "$(label_list[i]) Re = $(round(Int64, rey*1e-5))e5" - end - # Load literature data if provided - if !isempty(literature_path_list) - for path in literature_path_list - data = readdlm(path, ',') - header = lowercase.(string.(data[1, :])) - # Find column indices for alpha, CL, CD, CS (case-insensitive, allow common variants) - alpha_idx = findfirst(x -> occursin("alpha", x), header) - cl_idx = findfirst(x -> occursin("cl", x), header) - cd_idx = findfirst(x -> occursin("cd", x), header) - cs_idx = findfirst(x -> occursin("cs", x), header) - # Fallback: if CS not found, fill with zeros - cs_col = cs_idx === nothing ? zeros(size(data, 1)-1) : data[2:end, cs_idx] - # Push as [alpha, CL, CD, CS] - push!(polar_data_list, [ - data[2:end, alpha_idx], - data[2:end, cl_idx], - data[2:end, cd_idx], - cs_col - ]) - end - end - - # Initializing plot - fig, axs = plt.subplots(2, 2, figsize=(14, 14)) - - # Number of computational results (excluding literature) - n_solvers = length(solver_list) - for (i, (polar_data, label)) in enumerate(zip(polar_data_list, label_list)) - if i < n_solvers - linestyle = "-" - marker = "*" - markersize = 7 - else - linestyle = "-" - marker = "." - markersize = 5 - end - if contains(label, "LLT") - label = replace(label, "e5" => raw"\cdot10^5") - label = replace(label, " " => raw"~") - label = replace(label, "LLT" => raw"\mathrm{LLT}{~\,}") - label = raw"$" * label * raw"$" - else - label = replace(label, "e5" => raw"\cdot10^5") - label = replace(label, " " => "~") - label = replace(label, "VSM" => raw"\mathrm{VSM}") - label = raw"$" * label * raw"$" - end - axs[1, 1].plot( - polar_data[1], - polar_data[2], - label=label, - linestyle=linestyle, - marker=marker, - markersize=markersize, - ) - # Limit y-range if CL > 10 - if maximum(polar_data[2]) > 10 - axs[1, 1].set_ylim([-0.5, 2]) - end - title = raw"$C_\mathrm{L}" * raw"$" * " vs $angle_type [°]" - axs[1, 1].set_title(title) - axs[1, 1].set_xlabel("$angle_type [°]") - axs[1, 1].set_ylabel(L"$C_\mathrm{L}$") - axs[1, 1].legend() - end - - for (i, (polar_data, label)) in enumerate(zip(polar_data_list, label_list)) - if i < n_solvers - linestyle = "-" - marker = "*" - markersize = 7 - else - linestyle = "-" - marker = "." - markersize = 5 - end - if contains(label, "LLT") - label = replace(label, "e5" => raw"\cdot10^5") - label = replace(label, " " => raw"~") - label = replace(label, "LLT" => raw"\mathrm{LLT}{~\,}") - label = raw"$" * label * raw"$" - else - label = replace(label, "e5" => raw"\cdot10^5") - label = replace(label, " " => "~") - label = replace(label, "VSM" => raw"\mathrm{VSM}") - label = raw"$" * label * raw"$" - end - axs[1, 2].plot( - polar_data[1], - polar_data[3], - label=label, - linestyle=linestyle, - marker=marker, - markersize=markersize, - ) - # Limit y-range if CL > 10 - if maximum(polar_data[2]) > 10 - axs[1, 2].set_ylim([-0.5, 2]) - end - title = raw"$C_\mathrm{D}" * raw"$" * " vs $angle_type [°]" - axs[1, 2].set_title(title) - axs[1, 2].set_xlabel("$angle_type [°]") - axs[1, 2].set_ylabel(L"$C_\mathrm{D}$") - axs[1, 2].legend() - end - - - for (i, (polar_data, label)) in enumerate(zip(polar_data_list, label_list)) - if i < n_solvers - linestyle = "-" - marker = "*" - markersize = 7 - else - linestyle = "-" - marker = "." - markersize = 5 - end - if contains(label, "LLT") - label = replace(label, "e5" => raw"\cdot10^5") - label = replace(label, " " => raw"~") - label = replace(label, "LLT" => raw"\mathrm{LLT}{~\,}") - label = raw"$" * label * raw"$" - else - label = replace(label, "e5" => raw"\cdot10^5") - label = replace(label, " " => "~") - label = replace(label, "VSM" => raw"\mathrm{VSM}") - label = raw"$" * label * raw"$" - end - axs[2, 1].plot( - polar_data[1], - polar_data[4], - label=label, - linestyle=linestyle, - marker=marker, - markersize=markersize, - ) - # Limit y-range if CL > 10 - if maximum(polar_data[2]) > 10 - axs[2, 1].set_ylim([-0.5, 2]) - end - title = raw"$C_\mathrm{S}" * raw"$" * " vs $angle_type [°]" - axs[2, 1].set_title(title) - axs[2, 1].set_xlabel("$angle_type [°]") - axs[2, 1].set_ylabel(L"$C_\mathrm{S}$") - axs[2, 1].legend() - end - - for (i, (polar_data, label)) in enumerate(zip(polar_data_list, label_list)) - if i < n_solvers - linestyle = "-" - marker = "*" - markersize = 7 - else - linestyle = "-" - marker = "." - markersize = 5 - end - if contains(label, "LLT") - label = replace(label, "e5" => raw"\cdot10^5") - label = replace(label, " " => raw"~") - label = replace(label, "LLT" => raw"\mathrm{LLT}{~\,}") - label = raw"$" * label * raw"$" - else - label = replace(label, "e5" => raw"\cdot10^5") - label = replace(label, " " => "~") - label = replace(label, "VSM" => raw"\mathrm{VSM}") - label = raw"$" * label * raw"$" - end - axs[2, 2].plot( - polar_data[3], - polar_data[2], - label=label, - linestyle=linestyle, - marker=marker, - markersize=markersize, - ) - # Limit y-range if CL > 10 - if maximum(polar_data[2]) > 10 || maximum(polar_data[3]) > 10 - axs[2, 2].set_ylim([-0.5, 2]) - axs[2, 2].set_xlim([-0.5, 2]) - end - title = raw"$C_\mathrm{L}" * raw"$" * " vs " * raw"$C_\mathrm{D}" * raw"$" - axs[2, 2].set_title(title) - axs[2, 2].set_xlabel(L"$C_\mathrm{D}$") - axs[2, 2].set_ylabel(L"$C_\mathrm{L}$") - axs[2, 2].legend() - end - - fig.tight_layout(h_pad=3.5, rect=(0.01, 0.01, 0.99, 0.99)) - - # Save and show plot - if is_save && !isnothing(save_path) - save_plot(fig, save_path, main_title; data_type) - end - - if is_show - show_plot(fig) - end - - return fig -end - -""" - plot_polar_data(body_aero::BodyAerodynamics; alphas=collect(deg2rad.(-5:0.3:25)), delta_tes=collect(deg2rad.(-5:0.3:25))) - -Plot polar data (Cl, Cd, Cm) as 3D surfaces against alpha and delta_te angles. delta_te is the trailing edge deflection angle -relative to the 2d airfoil or panel chord line. - -# Arguments -- `body_aero`: Wing aerodynamics struct - -# Keyword arguments -- `alphas`: Range of angle of attack values in radians (default: -5° to 25° in 0.3° steps) -- `delta_tes`: Range of trailing edge angles in radians (default: -5° to 25° in 0.3° steps) -- `is_show`: Whether to display plots (default: true) -- `use_tex`: if the external `pdflatex` command shall be used -""" -function VortexStepMethod.plot_polar_data(body_aero::BodyAerodynamics; - alphas=collect(deg2rad.(-5:0.3:25)), - delta_tes = collect(deg2rad.(-5:0.3:25)), - is_show = true, - use_tex = false - ) - if body_aero.panels[1].aero_model == POLAR_MATRICES - set_plot_style() - - # Create figure with subplots - fig = plt.figure(figsize=(15, 6)) - - # Get interpolation functions and labels - interp_data = [ - (body_aero.panels[1].cl_interp, L"$C_l$"), - (body_aero.panels[1].cd_interp, L"$C_d$"), - (body_aero.panels[1].cm_interp, L"$C_m$") - ] - - # Create each subplot - for (idx, (interp, label)) in enumerate(interp_data) - ax = fig.add_subplot(1, 3, idx, projection="3d") - - # Create interpolation matrix - interp_matrix = zeros(length(alphas), length(delta_tes)) - interp_matrix .= [interp(alpha, delta_te) for alpha in alphas, delta_te in delta_tes] - X = collect(delta_tes) .+ zeros(length(alphas))' - Y = collect(alphas)' .+ zeros(length(delta_tes)) - - # Plot surface - ax.plot_wireframe(X, Y, interp_matrix, - edgecolor="blue", - lw=0.5, - rstride=5, - cstride=5, - alpha=0.6) - - # Set labels and title - ax.set_xlabel(L"$\delta$ [rad]") - ax.set_ylabel(L"$\alpha$ [rad]") - ax.set_zlabel(label) - ax.set_title(label * L" vs $\alpha$ and $\delta$") - ax.grid(true) - end - - # Adjust layout and display - plt.tight_layout(rect=(0.01, 0.01, 0.99, 0.99)) - if is_show - show_plot(fig) - end - return fig - else - throw(ArgumentError("Plotting polar data for $(body_aero.panels[1].aero_model) is not implemented.")) - end -end From 084e7bb9368009ca7ef0924dd37b091bf41b14a5 Mon Sep 17 00:00:00 2001 From: 1-Bart-1 Date: Sun, 16 Nov 2025 22:09:09 +0100 Subject: [PATCH 23/53] Don't use corrected alpha --- src/VortexStepMethod.jl | 2 +- src/body_aerodynamics.jl | 23 ++++++++++++----------- src/settings.jl | 8 +++++++- src/solver.jl | 5 ++++- 4 files changed, 24 insertions(+), 14 deletions(-) diff --git a/src/VortexStepMethod.jl b/src/VortexStepMethod.jl index f3cb531a..8c1a407a 100644 --- a/src/VortexStepMethod.jl +++ b/src/VortexStepMethod.jl @@ -294,4 +294,4 @@ include("solver.jl") include("precompile.jl") -end # module \ No newline at end of file +end # module diff --git a/src/body_aerodynamics.jl b/src/body_aerodynamics.jl index b96ff57d..97803526 100644 --- a/src/body_aerodynamics.jl +++ b/src/body_aerodynamics.jl @@ -449,7 +449,8 @@ function calculate_results( va_norm_array, va_unit_array, panels::Vector{Panel}, - is_only_f_and_gamma_output::Bool, + is_only_f_and_gamma_output::Bool; + correct_aoa::Bool=false, ) # Initialize arrays @@ -473,7 +474,7 @@ function calculate_results( moment = reshape((cm_array .* 0.5 .* density .* v_a_array.^2 .* chord_array), :, 1) # Calculate alpha corrections based on model type - if aerodynamic_model_type == VSM + if correct_aoa update_effective_angle_of_attack!( alpha_corrected, body_aero, @@ -485,7 +486,7 @@ function calculate_results( va_norm_array, va_unit_array ) - elseif aerodynamic_model_type == LLT + else alpha_corrected .= alpha_array end @@ -661,8 +662,7 @@ Set velocity array and update wake filaments. - `va::VelVector`: Velocity vector of the apparent wind speed [m/s] - `omega::VelVector`: Turn rate vector around x y and z axis [rad/s] """ -function set_va!(body_aero::BodyAerodynamics, va::VelVector, omega=zeros(MVec3)) - +function set_va!(body_aero::BodyAerodynamics, va::AbstractVector, omega=zeros(MVec3)) # Calculate va_distribution based on input type va_distribution = if all(omega .== 0.0) repeat(reshape(va, 1, 3), length(body_aero.panels)) @@ -693,16 +693,17 @@ function set_va!(body_aero::BodyAerodynamics, va::VelVector, omega=zeros(MVec3)) return nothing end -function set_va!(body_aero::BodyAerodynamics, va_distribution::Vector{VelVector}, omega=zeros(MVec3)) - length(va) != length(body_aero.panels) && throw(ArgumentError("Length of va distribution should be equal to number of panels.")) - +function set_va!(body_aero::BodyAerodynamics, va_distribution::AbstractMatrix, omega=zeros(MVec3)) + size(va_distribution, 1) != length(body_aero.panels) && + throw(ArgumentError("Number of rows in va distribution should be equal to number of panels.")) + for (i, panel) in enumerate(body_aero.panels) - panel.va = va_distribution[i] + panel.va .= va_distribution[i, :] end - + # Update wake elements frozen_wake!(body_aero, va_distribution) - body_aero._va = va + body_aero._va .= [mean(va_distribution[:,i]) for i in 1:3] return nothing end diff --git a/src/settings.jl b/src/settings.jl index 5916da52..ea19b4d1 100644 --- a/src/settings.jl +++ b/src/settings.jl @@ -35,6 +35,7 @@ end core_radius_fraction::Float64 = 1e-20 mu::Float64 = 1.81e-5 # dynamic viscosity [N·s/m²] calc_only_f_and_gamma::Bool=false # whether to only output f and gamma + correct_aoa::Bool=false # perform aoa correction end @Base.kwdef mutable struct VSMSettings @@ -107,7 +108,12 @@ function VSMSettings(filename; data_prefix=true) # Handle enum conversions manually vsm_settings.solver_settings.aerodynamic_model_type = eval(Symbol(solver_data["aerodynamic_model_type"])) vsm_settings.solver_settings.type_initial_gamma_distribution = eval(Symbol(solver_data["type_initial_gamma_distribution"])) - + + # Set correct_aoa default based on model type if not explicitly provided + if !haskey(solver_data, "correct_aoa") + vsm_settings.solver_settings.correct_aoa = (vsm_settings.solver_settings.aerodynamic_model_type == VSM) + end + # Override with calculated totals vsm_settings.solver_settings.n_panels = n_panels vsm_settings.solver_settings.n_groups = n_groups diff --git a/src/solver.jl b/src/solver.jl index cb7cc6be..e273929c 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -123,6 +123,7 @@ sol::VSMSolution = VSMSolution(): The result of calling [solve!](@ref) core_radius_fraction::Float64 = 1e-20 mu::Float64 = 1.81e-5 is_only_f_and_gamma_output::Bool = false + correct_aoa::Bool = false # Intermediate results lr::LoopResult{P} = LoopResult{P}() @@ -149,6 +150,7 @@ function Solver(body_aero, settings::VSMSettings) rtol=settings.solver_settings.rtol, relaxation_factor=settings.solver_settings.relaxation_factor, core_radius_fraction=settings.solver_settings.core_radius_fraction, + correct_aoa=settings.solver_settings.correct_aoa, ) end @@ -485,7 +487,8 @@ function solve(solver::Solver, body_aero::BodyAerodynamics, gamma_distribution=n solver.br.va_norm_array, solver.br.va_unit_array, body_aero.panels, - solver.is_only_f_and_gamma_output + solver.is_only_f_and_gamma_output; + correct_aoa=solver.correct_aoa ) return results end From 112fcede60e9ae27535636bc3e5405ef850f7c92 Mon Sep 17 00:00:00 2001 From: 1-Bart-1 Date: Sun, 16 Nov 2025 22:09:24 +0100 Subject: [PATCH 24/53] Updated settings --- test/settings/test_settings.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/settings/test_settings.jl b/test/settings/test_settings.jl index e3200766..68566cae 100644 --- a/test/settings/test_settings.jl +++ b/test/settings/test_settings.jl @@ -11,6 +11,6 @@ using Test @test vss.wings isa Vector{WingSettings} @test length(vss.wings) == 2 io = IOBuffer(repr(vss)) - @test countlines(io) == 46 # Updated to match new output format + @test countlines(io) == 47 # Updated to match new output format end nothing From 2031982128e101f43c7dac20aa699800da3ee0aa Mon Sep 17 00:00:00 2001 From: 1-Bart-1 Date: Sun, 16 Nov 2025 22:09:40 +0100 Subject: [PATCH 25/53] Use plot and plot! with obs --- ext/VortexStepMethodMakieExt.jl | 204 +++++++++++++++++++++++++++++--- 1 file changed, 187 insertions(+), 17 deletions(-) diff --git a/ext/VortexStepMethodMakieExt.jl b/ext/VortexStepMethodMakieExt.jl index 69819d2a..203e7e1b 100644 --- a/ext/VortexStepMethodMakieExt.jl +++ b/ext/VortexStepMethodMakieExt.jl @@ -4,55 +4,193 @@ import VortexStepMethod: calculate_filaments_for_plotting export plot_geometry, plot_distribution, plot_polars, save_plot, show_plot, plot_polar_data +# Global storage for panel mesh observables (for dynamic plotting) +const PANEL_MESH_OBSERVABLES = Ref{Union{Nothing, Dict}}(nothing) + """ - plot!(ax, panel::VortexStepMethod.Panel; kwargs...) + plot!(ax, panel::VortexStepMethod.Panel; use_observables=false, kwargs...) Plot a single `Panel` as a `mesh`. The corner points are ordered as: LE1, TE1, TE2, LE2. This creates two triangles: (LE1, TE1, TE2) and (LE1, TE2, LE2). + +If `use_observables=true`, creates observables for dynamic updates. """ -function Makie.plot!(ax, panel::VortexStepMethod.Panel; color=(:red, 0.2), R_b_w=nothing, T_b_w=nothing, kwargs...) +function Makie.plot!(ax, panel::VortexStepMethod.Panel; color=(:red, 0.2), R_b_w=nothing, T_b_w=nothing, + use_observables=false, kwargs...) plots = [] points = [Point3f(panel.corner_points[:, i]) for i in 1:4] if !isnothing(R_b_w) && !isnothing(T_b_w) points = [Point3f(R_b_w * p + T_b_w) for p in points] end - faces = [Makie.GLTriangleFace(1, 2, 3), Makie.GLTriangleFace(1, 3, 4)] - p = mesh!(ax, points, faces; color, transparency=true, kwargs...) - push!(plots, p) - border_points = [points..., points[1]] - p = lines!(ax, border_points; color=:black, transparency=true, kwargs...) - push!(plots, p) + + if use_observables + # Create observables for dynamic updates + vertices_obs = Observable(points) + faces_obs = Observable([Makie.GLTriangleFace(1, 2, 3), Makie.GLTriangleFace(1, 3, 4)]) + border_obs = Observable([points..., points[1]]) + + p = mesh!(ax, vertices_obs, faces_obs; color, transparency=true, kwargs...) + push!(plots, p) + p = lines!(ax, border_obs; color=:black, transparency=true, kwargs...) + push!(plots, p) + + # Note: Observables are stored at the body level, not individual panel level + # Individual panels need their parent body for proper tracking + else + # Static plotting (original behavior) + faces = [Makie.GLTriangleFace(1, 2, 3), Makie.GLTriangleFace(1, 3, 4)] + p = mesh!(ax, points, faces; color, transparency=true, kwargs...) + push!(plots, p) + border_points = [points..., points[1]] + p = lines!(ax, border_points; color=:black, transparency=true, kwargs...) + push!(plots, p) + end + return plots end """ - plot!(ax, body::VortexStepMethod.BodyAerodynamics; kwargs...) + plot!(ax, body::VortexStepMethod.BodyAerodynamics; use_observables=false, kwargs...) Plot a `BodyAerodynamics` object by plotting each of its panels. + +If `use_observables=true`, creates observables for dynamic updates keyed by (body_id, panel_index). +Otherwise, creates static plots (original behavior). """ -function Makie.plot!(ax, body::VortexStepMethod.BodyAerodynamics; color=(:red, 0.2), R_b_w=nothing, T_b_w=nothing, kwargs...) +function Makie.plot!(ax, body::VortexStepMethod.BodyAerodynamics; color=(:red, 0.2), R_b_w=nothing, T_b_w=nothing, + use_observables=false, kwargs...) plots = [] - for panel in body.panels - p = Makie.plot!(ax, panel; color, R_b_w, T_b_w, kwargs...) - push!(plots, p) + + if use_observables + # Initialize global storage if needed + if isnothing(PANEL_MESH_OBSERVABLES[]) + PANEL_MESH_OBSERVABLES[] = Dict() + end + + body_id = objectid(body) + + # Create observables for each panel + for (panel_idx, panel) in enumerate(body.panels) + # Compute initial points + points = [Point3f(panel.corner_points[:, i]) for i in 1:4] + if !isnothing(R_b_w) && !isnothing(T_b_w) + points = [Point3f(R_b_w * p + T_b_w) for p in points] + end + + # Create observables + vertices_obs = Observable(points) + faces_obs = Observable([Makie.GLTriangleFace(1, 2, 3), Makie.GLTriangleFace(1, 3, 4)]) + border_obs = Observable([points..., points[1]]) + + # Plot using observables + p = mesh!(ax, vertices_obs, faces_obs; color, transparency=true, kwargs...) + push!(plots, p) + p = lines!(ax, border_obs; color=:black, transparency=true, kwargs...) + push!(plots, p) + + # Store observables with stable key + PANEL_MESH_OBSERVABLES[][(body_id, panel_idx)] = ( + vertices = vertices_obs, + border = border_obs, + faces = faces_obs + ) + end + else + # Static plotting (original behavior) + for panel in body.panels + p = Makie.plot!(ax, panel; color, R_b_w, T_b_w, use_observables=false, kwargs...) + push!(plots, p) + end end + return plots end -function Makie.plot(panel::VortexStepMethod.Panel; size = (1200, 800), kwargs...) + +""" + plot!(body::VortexStepMethod.BodyAerodynamics; R_b_w=nothing, T_b_w=nothing) + +Update existing body aerodynamics plot observables with current geometry. +This updates all panels in the body using their current corner_points. + +Requires that `plot(body; use_observables=true)` or `plot!(ax, body; use_observables=true)` +was called first to create the observables. +""" +function Makie.plot!(body::VortexStepMethod.BodyAerodynamics; R_b_w=nothing, T_b_w=nothing, kwargs...) + # Check if observables exist + if isnothing(PANEL_MESH_OBSERVABLES[]) + error("No panel observables found. Call plot(body; use_observables=true) first.") + end + + body_id = objectid(body) + + # Update each panel using stable (body_id, panel_idx) key + for (panel_idx, panel) in enumerate(body.panels) + key = (body_id, panel_idx) + if !haskey(PANEL_MESH_OBSERVABLES[], key) + error("No observables found for body $body_id panel $panel_idx. " * + "Call plot(body; use_observables=true) first.") + end + + # Get observables for this panel + obs = PANEL_MESH_OBSERVABLES[][key] + + # Recompute vertices from current panel.corner_points + points = [Point3f(panel.corner_points[:, i]) for i in 1:4] + if !isnothing(R_b_w) && !isnothing(T_b_w) + points = [Point3f(R_b_w * p + T_b_w) for p in points] + end + + # Update observables + obs.vertices[] = points + obs.border[] = [points..., points[1]] + end + + return nothing +end + +function Makie.plot(panel::VortexStepMethod.Panel; size = (1200, 800), + R_b_w=nothing, T_b_w=nothing, color=(:red, 0.2), kwargs...) fig = Figure(; size) ax = Axis3(fig[1, 1]; aspect = :data, xlabel = "X", ylabel = "Y", zlabel = "Z", azimuth = 9/8*π, zoommode = :cursor, viewmode = :fit, ) - plot!(ax, panel; kwargs...) + # Create observables for panel geometry + points = [Point3f(panel.corner_points[:, i]) for i in 1:4] + if !isnothing(R_b_w) && !isnothing(T_b_w) + points = [Point3f(R_b_w * p + T_b_w) for p in points] + end + + vertices_obs = Observable(points) + faces_obs = Observable([Makie.GLTriangleFace(1, 2, 3), Makie.GLTriangleFace(1, 3, 4)]) + + # Plot mesh using observables + mesh!(ax, vertices_obs, faces_obs; color, transparency=true, kwargs...) + + # Plot border + border_obs = Observable([points..., points[1]]) + lines!(ax, border_obs; color=:black, transparency=true, kwargs...) + + # Store observables globally for updates + panel_id = objectid(panel) + if isnothing(PANEL_MESH_OBSERVABLES[]) + PANEL_MESH_OBSERVABLES[] = Dict() + end + PANEL_MESH_OBSERVABLES[][panel_id] = ( + vertices = vertices_obs, + border = border_obs, + faces = faces_obs + ) + return fig end function Makie.plot(body_aero::VortexStepMethod.BodyAerodynamics; size = (1200, 800), - limitmargin = 0.1, kwargs...) + limitmargin = 0.1, R_b_w=nothing, T_b_w=nothing, color=(:red, 0.2), + kwargs...) fig = Figure(; size) ax = Axis3(fig[1, 1]; aspect = :data, xlabel = "X", ylabel = "Y", zlabel = "Z", @@ -61,7 +199,39 @@ function Makie.plot(body_aero::VortexStepMethod.BodyAerodynamics; size = (1200, yautolimitmargin=(limitmargin, limitmargin), zautolimitmargin=(limitmargin, limitmargin), ) - plot!(ax, body_aero; kwargs...) + + # Initialize global storage if needed + if isnothing(PANEL_MESH_OBSERVABLES[]) + PANEL_MESH_OBSERVABLES[] = Dict() + end + + body_id = objectid(body_aero) + + # Create observables for each panel using stable (body_id, panel_idx) key + for (panel_idx, panel) in enumerate(body_aero.panels) + # Compute initial points + points = [Point3f(panel.corner_points[:, i]) for i in 1:4] + if !isnothing(R_b_w) && !isnothing(T_b_w) + points = [Point3f(R_b_w * p + T_b_w) for p in points] + end + + # Create observables + vertices_obs = Observable(points) + faces_obs = Observable([Makie.GLTriangleFace(1, 2, 3), Makie.GLTriangleFace(1, 3, 4)]) + border_obs = Observable([points..., points[1]]) + + # Plot using observables + mesh!(ax, vertices_obs, faces_obs; color, transparency=true, kwargs...) + lines!(ax, border_obs; color=:black, transparency=true, kwargs...) + + # Store observables with stable key + PANEL_MESH_OBSERVABLES[][(body_id, panel_idx)] = ( + vertices = vertices_obs, + border = border_obs, + faces = faces_obs + ) + end + return fig end From e23eb99d9721fa3b54c6c57503bca299dc339ecf Mon Sep 17 00:00:00 2001 From: 1-Bart-1 Date: Fri, 21 Nov 2025 16:43:09 +0100 Subject: [PATCH 26/53] Use unrefined sections --- examples/stall_model.jl | 7 +- ext/VortexStepMethodMakieExt.jl | 6 +- src/VortexStepMethod.jl | 9 +- src/body_aerodynamics.jl | 19 +- src/obj_geometry.jl | 53 +++-- src/solver.jl | 201 +++++++----------- src/wing_geometry.jl | 53 ++--- src/yaml_geometry.jl | 28 ++- test/body_aerodynamics/complete_settings.yaml | 1 - test/ram_geometry/test_kite_geometry.jl | 2 +- test/solver/solver_settings.yaml | 1 - test/solver/test_group_coefficients.jl | 200 ++++++++--------- test/wing_geometry/test_wing_geometry.jl | 17 +- test/yaml_geometry/test_wing_constructor.jl | 30 +-- .../test_yaml_wing_deformation.jl | 10 +- 15 files changed, 303 insertions(+), 334 deletions(-) diff --git a/examples/stall_model.jl b/examples/stall_model.jl index 78fb28fe..a0ee1079 100644 --- a/examples/stall_model.jl +++ b/examples/stall_model.jl @@ -34,9 +34,8 @@ for row in eachrow(df) end # Create wing geometry -# Using REFINE grouping method: n_groups should equal number of unrefined panels (18 sections = 18 panels) -n_groups = length(rib_list) - 1 -CAD_wing = Wing(n_panels; spanwise_distribution, n_groups, grouping_method=REFINE) +# n_unrefined_sections will be automatically set to the number of ribs (18 sections) +CAD_wing = Wing(n_panels; spanwise_distribution) for rib in rib_list add_section!(CAD_wing, rib[1], rib[2], rib[3], rib[4]) end @@ -123,7 +122,7 @@ PLOT && plot_polars( angle_of_attack=0, side_slip=0, v_a=10, - title="tutorial_testing_stall_model_n_panels_$(n_panels)_distribution_$(spanwise_distribution)_grouping_$(CAD_wing.grouping_method)", + title="tutorial_testing_stall_model_n_panels_$(n_panels)_distribution_$(spanwise_distribution)_unrefined_$(CAD_wing.n_unrefined_sections)", data_type=".pdf", save_path=joinpath(save_folder, "polars"), is_save=true, diff --git a/ext/VortexStepMethodMakieExt.jl b/ext/VortexStepMethodMakieExt.jl index 203e7e1b..918402a9 100644 --- a/ext/VortexStepMethodMakieExt.jl +++ b/ext/VortexStepMethodMakieExt.jl @@ -310,8 +310,8 @@ function plot_line_segment_makie!(ax, segment, color, label; width=3) # Plot arrow dir = segment[2] - segment[1] - arrows!(ax, [Point3f(segment[1])], [Point3f(dir)]; - color=color, arrowsize=0.1) + arrows3d!(ax, [Point3f(segment[1])], [Point3f(dir)]; + color=color, shaftradius=0.01, tipradius=0.03, tiplength=0.1) end """ @@ -370,7 +370,7 @@ Create a 3D Makie plot of wing geometry including panels and filaments. - `zoom`: zoom factor (default: 1.8) """ function create_geometry_plot_makie(body_aero::BodyAerodynamics, title, - view_elevation, view_azimuth; zoom=1.8) + view_elevation, view_azimuth; zoom=0.5) panels = body_aero.panels va = isa(body_aero.va, Tuple) ? body_aero.va[1] : body_aero.va diff --git a/src/VortexStepMethod.jl b/src/VortexStepMethod.jl index 8c1a407a..fa2b9c70 100644 --- a/src/VortexStepMethod.jl +++ b/src/VortexStepMethod.jl @@ -142,11 +142,14 @@ end """ PanelGroupingMethod EQUAL_SIZE REFINE -Enumeration of methods for grouping panels. +**DEPRECATED**: This enum is deprecated and no longer used. +Grouping is now automatically handled via unrefined section mapping. + +Enumeration of methods for grouping panels (legacy). # Elements -- EQUAL_SIZE: Divide panels into equally-sized sequential groups -- REFINE: Group refined panels back to their original unrefined section +- EQUAL_SIZE: (Deprecated) Divide panels into equally-sized sequential groups +- REFINE: (Deprecated) Group refined panels back to their original unrefined section """ @enum PanelGroupingMethod EQUAL_SIZE REFINE diff --git a/src/body_aerodynamics.jl b/src/body_aerodynamics.jl index 97803526..1a0ae941 100644 --- a/src/body_aerodynamics.jl +++ b/src/body_aerodynamics.jl @@ -4,11 +4,12 @@ Main structure for calculating aerodynamic properties of bodies. Use the constructor to initialize. # Fields -- panels::Vector{Panel}: Vector of [Panel](@ref) structs +- panels::Vector{Panel}: Vector of refined [Panel](@ref) structs - wings::Vector{Wing}: A vector of wings; a body can have multiple wings +- unrefined::Vector{Panel}: Vector of unrefined panel representatives for aggregated results - `va::MVec3` = zeros(MVec3): A vector of the apparent wind speed, see: [MVec3](@ref) - `omega`::MVec3 = zeros(MVec3): A vector of the turn rates around the kite body axes -- `gamma_distribution`=zeros(Float64, P): A vector of the circulation +- `gamma_distribution`=zeros(Float64, P): A vector of the circulation of the velocity field; Length: Number of segments. [m²/s] - `alpha_uncorrected`=zeros(Float64, P): angles of attack per panel - `alpha_corrected`=zeros(Float64, P): corrected angles of attack per panel @@ -24,7 +25,7 @@ Main structure for calculating aerodynamic properties of bodies. Use the constru @with_kw mutable struct BodyAerodynamics{P} panels::Vector{Panel} wings::Vector{Wing} - groups::Vector{Panel} = Panel[] + unrefined::Vector{Panel} = Panel[] _va::MVec3 = zeros(MVec3) omega::MVec3 = zeros(MVec3) gamma_distribution::MVector{P, Float64} = zeros(P) @@ -74,7 +75,7 @@ function BodyAerodynamics( ) where T <: AbstractWing # Initialize panels panels = Panel[] - n_groups = 0 + n_unrefined_total = 0 for wing in wings for section in wing.sections section.LE_point .-= kite_body_origin @@ -94,14 +95,14 @@ function BodyAerodynamics( push!(panels, panel) end - # Count total groups - n_groups += wing.n_groups + # Count total unrefined panels (sections - 1) + n_unrefined_total += (wing.n_unrefined_sections - 1) end - # Initialize groups (unrefined panel representatives) - groups = [Panel() for _ in 1:n_groups] + # Initialize unrefined panels (representatives for unrefined sections) + unrefined = [Panel() for _ in 1:n_unrefined_total] - body_aero = BodyAerodynamics{length(panels)}(; panels, wings, groups) + body_aero = BodyAerodynamics{length(panels)}(; panels, wings, unrefined) reinit!(body_aero; va, omega) return body_aero end diff --git a/src/obj_geometry.jl b/src/obj_geometry.jl index dcf2a2e3..e074a402 100644 --- a/src/obj_geometry.jl +++ b/src/obj_geometry.jl @@ -422,12 +422,35 @@ group_deform!(wing, deg2rad.([5, 10, 5, 0]), deg2rad.([-5, 0, -5, 0])) function ObjWing( obj_path, dat_path; crease_frac=0.9, wind_vel=10., mass=1.0, - n_panels=56, n_sections=n_panels+1, n_groups=4, spanwise_distribution=UNCHANGED, + n_panels=56, n_sections=nothing, n_unrefined_sections=nothing, n_groups=nothing, + spanwise_distribution=LINEAR, spanwise_direction=[0.0, 1.0, 0.0], remove_nan=true, align_to_principal=false, alpha_range=deg2rad.(-5:1:20), delta_range=deg2rad.(-5:1:20), prn=true, - interp_steps=n_sections, grouping_method::PanelGroupingMethod=EQUAL_SIZE + interp_steps=n_panels+1, grouping_method::PanelGroupingMethod=EQUAL_SIZE ) - !(n_groups == 0 || n_panels % n_groups == 0) && throw(ArgumentError("Number of panels should be divisible by number of groups")) + # Handle deprecated parameters + if !isnothing(n_groups) + if !isnothing(n_unrefined_sections) + error("Cannot specify both n_groups and n_unrefined_sections. Use n_unrefined_sections only.") + end + @warn "Parameter n_groups is deprecated. Use n_unrefined_sections instead." maxlog=1 + n_unrefined_sections = n_groups + end + + if !isnothing(n_sections) + @warn "Parameter n_sections is deprecated. It is now always n_panels+1 for refined sections. Use n_unrefined_sections to control initial sections." maxlog=1 + end + + if grouping_method != EQUAL_SIZE + @warn "Parameter grouping_method is deprecated and ignored. Grouping is now always by unrefined sections." maxlog=1 + end + + # Set default: evenly spaced unrefined sections including both tips + if isnothing(n_unrefined_sections) + # Default to having same number of unrefined sections as refined (no refinement) + n_unrefined_sections = n_panels + 1 + end + !isapprox(spanwise_direction, [0.0, 1.0, 0.0]) && throw(ArgumentError("Spanwise direction has to be [0.0, 1.0, 0.0], not $spanwise_direction")) # Load or create polars @@ -472,29 +495,33 @@ function ObjWing( any(isnan.(cm_matrix)) && interpolate_matrix_nans!(cm_matrix; prn) end - # Create sections + # Create unrefined sections (evenly spaced including both tips) sections = Section[] - refined_sections = Section[] - non_deformed_sections = Section[] - for gamma in range(-gamma_tip, gamma_tip, n_sections) - aero_data = (collect(alpha_range), collect(delta_range), cl_matrix, cd_matrix, cm_matrix) + aero_data = (collect(alpha_range), collect(delta_range), cl_matrix, cd_matrix, cm_matrix) + for gamma in range(-gamma_tip, gamma_tip, n_unrefined_sections) LE_point = [le_interp[i](gamma) for i in 1:3] TE_point = [te_interp[i](gamma) for i in 1:3] push!(sections, Section(LE_point, TE_point, POLAR_MATRICES, aero_data)) - push!(refined_sections, Section(LE_point, TE_point, POLAR_MATRICES, aero_data)) - push!(non_deformed_sections, Section(LE_point, TE_point, POLAR_MATRICES, aero_data)) end + # Initialize refined_sections (will be populated by reinit!) + refined_sections = [Section() for _ in 1:(n_panels+1)] + panel_props = PanelProperties{n_panels}() cache = [PreallocationTools.LazyBufferCache()] - Wing(n_panels, n_groups, spanwise_distribution, panel_props, MVec3(spanwise_direction), + wing = Wing(n_panels, Int16(n_unrefined_sections), spanwise_distribution, panel_props, MVec3(spanwise_direction), sections, refined_sections, remove_nan, - grouping_method, Int16[], - non_deformed_sections, zeros(n_panels), zeros(n_panels), + Int16[], + Section[], zeros(n_panels), zeros(n_panels), mass, gamma_tip, inertia_tensor, T_cad_body, R_cad_body, radius, le_interp, te_interp, area_interp, cache) + # Refine mesh and update panel properties + reinit!(wing) + + wing + catch e if e isa BoundsError @error "Delete $cl_polar_path, $cd_polar_path and $cm_polar_path and try again." diff --git a/src/solver.jl b/src/solver.jl index e273929c..eed0cd4e 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -22,6 +22,12 @@ Struct for storing the solution of the [solve!](@ref) function. Must contain all - `moment_coeffs`::MVec3: Aerodynamic moment coefficients [CMx, CMy, CMz] [-] - `moment_dist`::Vector{Float64}: Pitching moments around the spanwise vector of each panel. [Nm] - `moment_coeff_dist`::Vector{Float64}: Pitching moment coefficient around the spanwise vector of each panel. [-] +- `unrefined_moment_dist`::MVector{G, Float64}: Aggregated moments for unrefined sections [Nm] +- `unrefined_moment_coeff_dist`::MVector{G, Float64}: Aggregated moment coefficients for unrefined sections [-] +- `cl_unrefined_array`::MVector{G, Float64}: Averaged lift coefficients for unrefined sections [-] +- `cd_unrefined_array`::MVector{G, Float64}: Averaged drag coefficients for unrefined sections [-] +- `cm_unrefined_array`::MVector{G, Float64}: Averaged moment coefficients for unrefined sections [-] +- `alpha_unrefined_array`::MVector{G, Float64}: Averaged angles of attack for unrefined sections [rad] - `solver_status`::SolverStatus: enum, see [SolverStatus](@ref) """ @with_kw mutable struct VSMSolution{P,G} @@ -49,12 +55,12 @@ Struct for storing the solution of the [solve!](@ref) function. Must contain all moment_coeffs::MVec3 = zeros(MVec3) moment_dist::MVector{P, Float64} = zeros(P) moment_coeff_dist::MVector{P, Float64} = zeros(P) - group_moment_dist::MVector{G, Float64} = zeros(G) - group_moment_coeff_dist::MVector{G, Float64} = zeros(G) - cl_group_array::MVector{G, Float64} = zeros(G) - cd_group_array::MVector{G, Float64} = zeros(G) - cm_group_array::MVector{G, Float64} = zeros(G) - alpha_group_array::MVector{G, Float64} = zeros(G) + unrefined_moment_dist::MVector{G, Float64} = zeros(G) + unrefined_moment_coeff_dist::MVector{G, Float64} = zeros(G) + cl_unrefined_array::MVector{G, Float64} = zeros(G) + cd_unrefined_array::MVector{G, Float64} = zeros(G) + cm_unrefined_array::MVector{G, Float64} = zeros(G) + alpha_unrefined_array::MVector{G, Float64} = zeros(G) solver_status::SolverStatus = FAILURE end @@ -138,7 +144,7 @@ end function Solver(body_aero; kwargs...) P = length(body_aero.panels) - G = sum([wing.n_groups for wing in body_aero.wings]) + G = sum([(wing.n_unrefined_sections - 1) for wing in body_aero.wings]) return Solver{P,G}(; kwargs...) end @@ -298,124 +304,79 @@ function solve!(solver::Solver, body_aero::BodyAerodynamics, gamma_distribution= moment_coeff_dist[i] = moment_dist[i] / (q_inf * projected_area) end - # Only compute group moments if there are groups - if length(solver.sol.group_moment_dist) > 0 - group_moment_dist = solver.sol.group_moment_dist - group_moment_coeff_dist = solver.sol.group_moment_coeff_dist - cl_group_array = solver.sol.cl_group_array - cd_group_array = solver.sol.cd_group_array - cm_group_array = solver.sol.cm_group_array - alpha_group_array = solver.sol.alpha_group_array - group_moment_dist .= 0.0 - group_moment_coeff_dist .= 0.0 - cl_group_array .= 0.0 - cd_group_array .= 0.0 - cm_group_array .= 0.0 - alpha_group_array .= 0.0 + # Only compute unrefined arrays if there are unrefined sections + if length(solver.sol.unrefined_moment_dist) > 0 + unrefined_moment_dist = solver.sol.unrefined_moment_dist + unrefined_moment_coeff_dist = solver.sol.unrefined_moment_coeff_dist + cl_unrefined_array = solver.sol.cl_unrefined_array + cd_unrefined_array = solver.sol.cd_unrefined_array + cm_unrefined_array = solver.sol.cm_unrefined_array + alpha_unrefined_array = solver.sol.alpha_unrefined_array + unrefined_moment_dist .= 0.0 + unrefined_moment_coeff_dist .= 0.0 + cl_unrefined_array .= 0.0 + cd_unrefined_array .= 0.0 + cm_unrefined_array .= 0.0 + alpha_unrefined_array .= 0.0 panel_idx = 1 - group_idx = 1 + unrefined_idx = 1 for wing in body_aero.wings - if wing.n_groups > 0 - if wing.grouping_method == EQUAL_SIZE - # Original method: divide panels into equally-sized sequential groups - panels_per_group = wing.n_panels ÷ wing.n_groups - for _ in 1:wing.n_groups - panel_count = 0 - group_panel = body_aero.groups[group_idx] - # Zero out accumulated fields - group_panel.x_airf .= 0.0 - group_panel.y_airf .= 0.0 - group_panel.z_airf .= 0.0 - group_panel.va .= 0.0 - group_panel.chord = 0.0 - group_panel.width = 0.0 - for _ in 1:panels_per_group - panel = body_aero.panels[panel_idx] - group_moment_dist[group_idx] += moment_dist[panel_idx] - group_moment_coeff_dist[group_idx] += moment_coeff_dist[panel_idx] - cl_group_array[group_idx] += solver.sol.cl_array[panel_idx] - cd_group_array[group_idx] += solver.sol.cd_array[panel_idx] - cm_group_array[group_idx] += solver.sol.cm_array[panel_idx] - alpha_group_array[group_idx] += solver.sol.alpha_array[panel_idx] - # Accumulate geometry for averaging - group_panel.x_airf .+= panel.x_airf - group_panel.y_airf .+= panel.y_airf - group_panel.z_airf .+= panel.z_airf - group_panel.va .+= panel.va - group_panel.chord += panel.chord - group_panel.width += panel.width - panel_idx += 1 - panel_count += 1 - end - # Average the coefficients and geometry over panels in the group - cl_group_array[group_idx] /= panel_count - cd_group_array[group_idx] /= panel_count - cm_group_array[group_idx] /= panel_count - alpha_group_array[group_idx] /= panel_count - group_panel.x_airf ./= panel_count - group_panel.y_airf ./= panel_count - group_panel.z_airf ./= panel_count - group_panel.va ./= panel_count - group_panel.chord /= panel_count - group_idx += 1 - end - elseif wing.grouping_method == REFINE - # REFINE method: group refined panels by their original unrefined section - # Initialize group panels - for i in 1:wing.n_groups - target_group_idx = group_idx + i - 1 - group_panel = body_aero.groups[target_group_idx] - group_panel.x_airf .= 0.0 - group_panel.y_airf .= 0.0 - group_panel.z_airf .= 0.0 - group_panel.va .= 0.0 - group_panel.chord = 0.0 - group_panel.width = 0.0 - end - # First pass: accumulate values - group_panel_counts = zeros(Int, wing.n_groups) - for local_panel_idx in 1:wing.n_panels - panel = body_aero.panels[panel_idx] - original_section_idx = wing.refined_panel_mapping[local_panel_idx] - target_group_idx = group_idx + original_section_idx - 1 - group_panel = body_aero.groups[target_group_idx] - group_moment_dist[target_group_idx] += moment_dist[panel_idx] - group_moment_coeff_dist[target_group_idx] += moment_coeff_dist[panel_idx] - cl_group_array[target_group_idx] += solver.sol.cl_array[panel_idx] - cd_group_array[target_group_idx] += solver.sol.cd_array[panel_idx] - cm_group_array[target_group_idx] += solver.sol.cm_array[panel_idx] - alpha_group_array[target_group_idx] += solver.sol.alpha_array[panel_idx] - # Accumulate geometry - group_panel.x_airf .+= panel.x_airf - group_panel.y_airf .+= panel.y_airf - group_panel.z_airf .+= panel.z_airf - group_panel.va .+= panel.va - group_panel.chord += panel.chord - group_panel.width += panel.width - group_panel_counts[original_section_idx] += 1 - panel_idx += 1 - end - # Second pass: average coefficients and geometry - for i in 1:wing.n_groups - target_group_idx = group_idx + i - 1 - if group_panel_counts[i] > 0 - group_panel = body_aero.groups[target_group_idx] - cl_group_array[target_group_idx] /= group_panel_counts[i] - cd_group_array[target_group_idx] /= group_panel_counts[i] - cm_group_array[target_group_idx] /= group_panel_counts[i] - alpha_group_array[target_group_idx] /= group_panel_counts[i] - group_panel.x_airf ./= group_panel_counts[i] - group_panel.y_airf ./= group_panel_counts[i] - group_panel.z_airf ./= group_panel_counts[i] - group_panel.va ./= group_panel_counts[i] - group_panel.chord /= group_panel_counts[i] - group_panel.width /= group_panel_counts[i] - end + n_unrefined_panels = wing.n_unrefined_sections - 1 + if n_unrefined_panels > 0 + # Initialize unrefined panels + for i in 1:n_unrefined_panels + target_unrefined_idx = unrefined_idx + i - 1 + unrefined_panel = body_aero.unrefined[target_unrefined_idx] + unrefined_panel.x_airf .= 0.0 + unrefined_panel.y_airf .= 0.0 + unrefined_panel.z_airf .= 0.0 + unrefined_panel.va .= 0.0 + unrefined_panel.chord = 0.0 + unrefined_panel.width = 0.0 + end + # Accumulate values from refined panels + unrefined_panel_counts = zeros(Int, n_unrefined_panels) + for local_panel_idx in 1:wing.n_panels + panel = body_aero.panels[panel_idx] + original_panel_idx = wing.refined_panel_mapping[local_panel_idx] + target_unrefined_idx = unrefined_idx + original_panel_idx - 1 + unrefined_panel = body_aero.unrefined[target_unrefined_idx] + unrefined_moment_dist[target_unrefined_idx] += moment_dist[panel_idx] + unrefined_moment_coeff_dist[target_unrefined_idx] += moment_coeff_dist[panel_idx] + cl_unrefined_array[target_unrefined_idx] += solver.sol.cl_array[panel_idx] + cd_unrefined_array[target_unrefined_idx] += solver.sol.cd_array[panel_idx] + cm_unrefined_array[target_unrefined_idx] += solver.sol.cm_array[panel_idx] + alpha_unrefined_array[target_unrefined_idx] += solver.sol.alpha_array[panel_idx] + # Accumulate geometry + unrefined_panel.x_airf .+= panel.x_airf + unrefined_panel.y_airf .+= panel.y_airf + unrefined_panel.z_airf .+= panel.z_airf + unrefined_panel.va .+= panel.va + unrefined_panel.chord += panel.chord + unrefined_panel.width += panel.width + unrefined_panel_counts[original_panel_idx] += 1 + panel_idx += 1 + end + # Average coefficients and geometry + for i in 1:n_unrefined_panels + target_unrefined_idx = unrefined_idx + i - 1 + if unrefined_panel_counts[i] > 0 + unrefined_panel = body_aero.unrefined[target_unrefined_idx] + cl_unrefined_array[target_unrefined_idx] /= unrefined_panel_counts[i] + cd_unrefined_array[target_unrefined_idx] /= unrefined_panel_counts[i] + cm_unrefined_array[target_unrefined_idx] /= unrefined_panel_counts[i] + alpha_unrefined_array[target_unrefined_idx] /= unrefined_panel_counts[i] + unrefined_panel.x_airf ./= unrefined_panel_counts[i] + unrefined_panel.y_airf ./= unrefined_panel_counts[i] + unrefined_panel.z_airf ./= unrefined_panel_counts[i] + unrefined_panel.va ./= unrefined_panel_counts[i] + unrefined_panel.chord /= unrefined_panel_counts[i] + unrefined_panel.width /= unrefined_panel_counts[i] end - group_idx += wing.n_groups end + unrefined_idx += n_unrefined_panels else - # Skip panels for wings with n_groups=0 + # Skip panels for wings with no unrefined sections panel_idx += wing.n_panels end end diff --git a/src/wing_geometry.jl b/src/wing_geometry.jl index 83889eff..ae7bcf46 100644 --- a/src/wing_geometry.jl +++ b/src/wing_geometry.jl @@ -196,7 +196,7 @@ Represents a wing composed of multiple sections with aerodynamic properties. # Core Fields (all wings) - `n_panels::Int16`: Number of panels in aerodynamic mesh -- `n_groups::Int16`: Number of panel groups +- `n_unrefined_sections::Int16`: Number of unrefined sections (sections before mesh refinement) - `spanwise_distribution`::PanelDistribution: [PanelDistribution](@ref) - `spanwise_direction::MVec3`: Wing span direction vector - `sections::Vector{Section}`: Vector of wing sections, see: [Section](@ref) @@ -223,7 +223,7 @@ Represents a wing composed of multiple sections with aerodynamic properties. """ mutable struct Wing <: AbstractWing n_panels::Int16 - n_groups::Int16 + n_unrefined_sections::Int16 spanwise_distribution::PanelDistribution panel_props::PanelProperties spanwise_direction::MVec3 @@ -232,7 +232,6 @@ mutable struct Wing <: AbstractWing remove_nan::Bool # Grouping - grouping_method::PanelGroupingMethod refined_panel_mapping::Vector{Int16} # Maps each refined panel to its original unrefined section index # Deformation fields @@ -255,7 +254,8 @@ end """ Wing(n_panels::Int; - n_groups=n_panels, + n_unrefined_sections=nothing, + n_groups=nothing, spanwise_distribution::PanelDistribution=LINEAR, spanwise_direction::PosVector=MVec3([0.0, 1.0, 0.0]), remove_nan::Bool=true, @@ -266,32 +266,46 @@ and refined sections as empty arrays. Creates a basic wing suitable for YAML-bas # Parameters - `n_panels::Int`: Number of panels in aerodynamic mesh -- `n_groups::Int`: Number of panel groups in aerodynamic mesh +- `n_unrefined_sections::Int`: Number of unrefined sections (inferred from added sections for YAML wings) +- `n_groups::Int`: DEPRECATED - use n_unrefined_sections instead - `spanwise_distribution`::PanelDistribution = LINEAR: [PanelDistribution](@ref) - `spanwise_direction::MVec3` = MVec3([0.0, 1.0, 0.0]): Wing span direction vector - `remove_nan::Bool`: Wether to remove the NaNs from interpolations or not -- `grouping_method::PanelGroupingMethod` = EQUAL_SIZE: Method for grouping panels (EQUAL_SIZE or REFINE) +- `grouping_method::PanelGroupingMethod` = EQUAL_SIZE: DEPRECATED - grouping is now always by unrefined sections """ function Wing(n_panels::Int; - n_groups = n_panels, + n_unrefined_sections=nothing, + n_groups=nothing, spanwise_distribution::PanelDistribution=LINEAR, spanwise_direction::PosVector=MVec3([0.0, 1.0, 0.0]), remove_nan=true, grouping_method::PanelGroupingMethod=EQUAL_SIZE) - # Validate grouping parameters - if grouping_method == EQUAL_SIZE - !(n_groups == 0 || n_panels % n_groups == 0) && throw(ArgumentError("With EQUAL_SIZE grouping, number of panels should be divisible by number of groups")) + + # Handle deprecated parameters + if !isnothing(n_groups) + if !isnothing(n_unrefined_sections) + error("Cannot specify both n_groups and n_unrefined_sections. Use n_unrefined_sections only.") + end + @warn "Parameter n_groups is deprecated. Use n_unrefined_sections instead." maxlog=1 + n_unrefined_sections = n_groups + end + + if grouping_method != EQUAL_SIZE + @warn "Parameter grouping_method is deprecated and ignored. Grouping is now always by unrefined sections." maxlog=1 end - # Note: For REFINE grouping, validation happens after refinement when we know the number of unrefined sections + + # For YAML wings, n_unrefined_sections will be set when sections are added + # Set to 0 as placeholder for now + n_unrefined_sections_value = isnothing(n_unrefined_sections) ? Int16(0) : Int16(n_unrefined_sections) panel_props = PanelProperties{n_panels}() # Initialize with default/empty values for optional fields Wing( - n_panels, n_groups, spanwise_distribution, panel_props, spanwise_direction, + n_panels, n_unrefined_sections_value, spanwise_distribution, panel_props, spanwise_direction, Section[], Section[], remove_nan, # Grouping - grouping_method, Int16[], + Int16[], # Deformation fields Section[], zeros(n_panels), zeros(n_panels), # Physical properties (defaults for non-OBJ wings) @@ -679,17 +693,8 @@ function refine_aerodynamic_mesh!(wing::AbstractWing; recompute_mapping=true, so # Compute panel mapping by finding closest unrefined panel for each refined panel recompute_mapping && compute_refined_panel_mapping!(wing) - # Validate REFINE grouping method - if wing.grouping_method == REFINE && wing.n_groups > 0 - n_unrefined_panels = length(wing.sections) - 1 - if wing.n_groups != n_unrefined_panels - throw(ArgumentError( - "With REFINE grouping method, n_groups ($(wing.n_groups)) must equal " * - "the number of unrefined panels ($n_unrefined_panels). " * - "The wing has $(length(wing.sections)) unrefined sections, forming $n_unrefined_panels panels." - )) - end - end + # Update n_unrefined_sections based on actual sections + wing.n_unrefined_sections = Int16(length(wing.sections)) # Create/update non_deformed_sections to match refined_sections update_non_deformed_sections!(wing) diff --git a/src/yaml_geometry.jl b/src/yaml_geometry.jl index 098f5c88..a24492e2 100644 --- a/src/yaml_geometry.jl +++ b/src/yaml_geometry.jl @@ -184,16 +184,33 @@ wing = Wing("wing_geometry.yaml"; n_panels=30, n_groups=2, prn=true) function Wing( geometry_file::String; n_panels=20, - n_groups=1, + n_unrefined_sections=nothing, + n_groups=nothing, spanwise_distribution=LINEAR, spanwise_direction=[0.0, 1.0, 0.0], remove_nan=true, prn=false, grouping_method::PanelGroupingMethod=EQUAL_SIZE ) - !(n_groups == 0 || n_panels % n_groups == 0) && throw(ArgumentError("Number of panels should be divisible by number of groups")) + # Handle deprecated parameters + if !isnothing(n_groups) + if !isnothing(n_unrefined_sections) + error("Cannot specify both n_groups and n_unrefined_sections. Use n_unrefined_sections only.") + end + @warn "Parameter n_groups is deprecated. For YAML wings, n_unrefined_sections is inferred from added sections." maxlog=1 + end + + if grouping_method != EQUAL_SIZE + @warn "Parameter grouping_method is deprecated and ignored. Grouping is now always by unrefined sections." maxlog=1 + end + + # For YAML wings, n_unrefined_sections is inferred from the number of sections added + if !isnothing(n_unrefined_sections) + @warn "For YAML wings, n_unrefined_sections is automatically inferred from the sections in the geometry file. The parameter is ignored." maxlog=1 + end + !isapprox(spanwise_direction, [0.0, 1.0, 0.0]) && throw(ArgumentError("Spanwise direction has to be [0.0, 1.0, 0.0], not $spanwise_direction")) - + prn && @info "Reading YAML wing configuration from $geometry_file" # Load YAML file following Uwe's suggestion @@ -237,12 +254,11 @@ function Wing( end # Create Wing using the standard constructor + # n_unrefined_sections will be set automatically after sections are added wing = Wing(n_panels; - n_groups=n_groups, spanwise_distribution=spanwise_distribution, spanwise_direction=MVec3(spanwise_direction), - remove_nan=remove_nan, - grouping_method=grouping_method + remove_nan=remove_nan ) # Parse sections and populate wing diff --git a/test/body_aerodynamics/complete_settings.yaml b/test/body_aerodynamics/complete_settings.yaml index a40ff608..214f7c30 100644 --- a/test/body_aerodynamics/complete_settings.yaml +++ b/test/body_aerodynamics/complete_settings.yaml @@ -2,7 +2,6 @@ wings: - name: "body_aero_test_wing" geometry_file: "test/body_aerodynamics/test_wing.yaml" n_panels: 4 - n_groups: 2 spanwise_panel_distribution: COSINE spanwise_direction: [0.0, 1.0, 0.0] remove_nan: true diff --git a/test/ram_geometry/test_kite_geometry.jl b/test/ram_geometry/test_kite_geometry.jl index 4eb07823..c4d35500 100644 --- a/test/ram_geometry/test_kite_geometry.jl +++ b/test/ram_geometry/test_kite_geometry.jl @@ -224,7 +224,7 @@ using Serialization @testset "First and Last Section Deformation with group_deform!" begin # Create an ObjWing with a small number of panels and groups wing = ObjWing(test_obj_path, test_dat_path; - n_panels=4, n_groups=2, remove_nan=true) + n_panels=4, remove_nan=true) # Store original TE points from all refined_sections # Wing has n_panels+1 sections (5 sections for 4 panels) diff --git a/test/solver/solver_settings.yaml b/test/solver/solver_settings.yaml index 689f915a..142c0c1f 100644 --- a/test/solver/solver_settings.yaml +++ b/test/solver/solver_settings.yaml @@ -2,7 +2,6 @@ wings: - name: "solver_test_wing" geometry_file: "test/solver/solver_test_wing.yaml" n_panels: 4 - n_groups: 2 spanwise_panel_distribution: COSINE spanwise_direction: [0.0, 1.0, 0.0] remove_nan: true diff --git a/test/solver/test_group_coefficients.jl b/test/solver/test_group_coefficients.jl index ede46b2a..6bc6745e 100644 --- a/test/solver/test_group_coefficients.jl +++ b/test/solver/test_group_coefficients.jl @@ -2,23 +2,20 @@ using VortexStepMethod using LinearAlgebra using Test -@testset "Group Coefficient Arrays Tests" begin - @testset "Group coefficients with EQUAL_SIZE method" begin - # Create a simple wing with groups +@testset "Unrefined Coefficient Arrays Tests" begin + @testset "Unrefined coefficients aggregation" begin + # Create a simple wing with unrefined sections n_panels = 20 - n_groups = 4 + n_unrefined_sections = 5 # This gives 4 unrefined panels # Create a test wing settings file settings_file = create_temp_wing_settings("solver", "solver_test_wing.yaml"; alpha=5.0, beta=0.0, wind_speed=10.0) try - # Modify settings to use specific panel/group configuration + # Modify settings to use specific panel configuration settings = VSMSettings(settings_file) settings.wings[1].n_panels = n_panels - settings.wings[1].n_groups = n_groups - settings.wings[1].grouping_method = EQUAL_SIZE settings.solver_settings.n_panels = n_panels - settings.solver_settings.n_groups = n_groups # Create wing and solver wing = Wing(settings) @@ -30,106 +27,75 @@ using Test set_va!(body_aero, va) sol = solve!(solver, body_aero) - # Test 1: Group arrays exist and have correct size - @test length(sol.cl_group_array) == n_groups - @test length(sol.cd_group_array) == n_groups - @test length(sol.cm_group_array) == n_groups - - # Test 2: Group arrays are not all zeros (solver computed them) - @test !all(sol.cl_group_array .== 0.0) - @test !all(sol.cd_group_array .== 0.0) - - # Test 3: Verify group coefficients are averages of panel coefficients - panels_per_group = n_panels ÷ n_groups - for group_idx in 1:n_groups - panel_start = (group_idx - 1) * panels_per_group + 1 - panel_end = group_idx * panels_per_group - - # Calculate expected average from panel coefficients - expected_cl = sum(sol.cl_array[panel_start:panel_end]) / panels_per_group - expected_cd = sum(sol.cd_array[panel_start:panel_end]) / panels_per_group - expected_cm = sum(sol.cm_array[panel_start:panel_end]) / panels_per_group - - # Check if group coefficients match expected averages - # Handle NaN values that can occur in INVISCID models - if isnan(expected_cl) - @test isnan(sol.cl_group_array[group_idx]) - else - @test isapprox(sol.cl_group_array[group_idx], expected_cl, rtol=1e-10) - end - if isnan(expected_cd) - @test isnan(sol.cd_group_array[group_idx]) - else - @test isapprox(sol.cd_group_array[group_idx], expected_cd, rtol=1e-10) - end - if isnan(expected_cm) - @test isnan(sol.cm_group_array[group_idx]) - else - @test isapprox(sol.cm_group_array[group_idx], expected_cm, rtol=1e-10) - end - end - - # Test 4: Verify physical consistency (lift coefficients should be positive at positive AoA) - # Skip test if values are NaN - if !any(isnan.(sol.cl_group_array)) - @test all(sol.cl_group_array .> 0.0) - end - - finally - rm(settings_file; force=true) - end - end + n_unrefined_panels = wing.n_unrefined_sections - 1 - @testset "Group coefficients with n_groups=0 (no grouping)" begin - # Create a wing with no groups - n_panels = 20 - n_groups = 0 + # Test 1: Unrefined arrays exist and have correct size + @test length(sol.cl_unrefined_array) == n_unrefined_panels + @test length(sol.cd_unrefined_array) == n_unrefined_panels + @test length(sol.cm_unrefined_array) == n_unrefined_panels - settings_file = create_temp_wing_settings("solver", "solver_test_wing.yaml"; alpha=5.0, beta=0.0, wind_speed=10.0) + # Test 2: Unrefined arrays are not all zeros (solver computed them) + @test !all(sol.cl_unrefined_array .== 0.0) + @test !all(sol.cd_unrefined_array .== 0.0) - try - settings = VSMSettings(settings_file) - settings.wings[1].n_panels = n_panels - settings.wings[1].n_groups = n_groups - settings.solver_settings.n_panels = n_panels - settings.solver_settings.n_groups = n_groups + # Test 3: Verify unrefined coefficients are aggregated from refined panels + # using refined_panel_mapping + for unrefined_idx in 1:n_unrefined_panels + # Find all refined panels that map to this unrefined panel + refined_panel_indices = findall(x -> x == unrefined_idx, wing.refined_panel_mapping) - wing = Wing(settings) - body_aero = BodyAerodynamics([wing]) - solver = Solver(body_aero, settings) + if !isempty(refined_panel_indices) + # Calculate expected average from refined panel coefficients + expected_cl = sum(sol.cl_array[refined_panel_indices]) / length(refined_panel_indices) + expected_cd = sum(sol.cd_array[refined_panel_indices]) / length(refined_panel_indices) + expected_cm = sum(sol.cm_array[refined_panel_indices]) / length(refined_panel_indices) - va = [10.0, 0.0, 0.0] - set_va!(body_aero, va) - sol = solve!(solver, body_aero) + # Check if unrefined coefficients match expected averages + # Handle NaN values that can occur in INVISCID models + if isnan(expected_cl) + @test isnan(sol.cl_unrefined_array[unrefined_idx]) + else + @test isapprox(sol.cl_unrefined_array[unrefined_idx], expected_cl, rtol=1e-10) + end + if isnan(expected_cd) + @test isnan(sol.cd_unrefined_array[unrefined_idx]) + else + @test isapprox(sol.cd_unrefined_array[unrefined_idx], expected_cd, rtol=1e-10) + end + if isnan(expected_cm) + @test isnan(sol.cm_unrefined_array[unrefined_idx]) + else + @test isapprox(sol.cm_unrefined_array[unrefined_idx], expected_cm, rtol=1e-10) + end + end + end - # Test: Group arrays should be empty when n_groups=0 - @test length(sol.cl_group_array) == 0 - @test length(sol.cd_group_array) == 0 - @test length(sol.cm_group_array) == 0 + # Test 4: Verify physical consistency (lift coefficients should be positive at positive AoA) + # Skip test if values are NaN + if !any(isnan.(sol.cl_unrefined_array)) + @test all(sol.cl_unrefined_array .> 0.0) + end finally rm(settings_file; force=true) end end - @testset "Group coefficients with different group sizes" begin - # Test with various panel/group combinations + @testset "Unrefined coefficients with different panel counts" begin + # Test with various panel/section combinations test_cases = [ - (n_panels=40, n_groups=8), - (n_panels=30, n_groups=5), - (n_panels=24, n_groups=6), + (n_panels=40, n_unrefined_expected=21), # From YAML file sections + (n_panels=30, n_unrefined_expected=21), + (n_panels=24, n_unrefined_expected=21), ] - for (n_panels, n_groups) in test_cases + for (n_panels, n_unrefined_expected) in test_cases settings_file = create_temp_wing_settings("solver", "solver_test_wing.yaml"; alpha=5.0, beta=0.0, wind_speed=10.0) try settings = VSMSettings(settings_file) settings.wings[1].n_panels = n_panels - settings.wings[1].n_groups = n_groups - settings.wings[1].grouping_method = EQUAL_SIZE settings.solver_settings.n_panels = n_panels - settings.solver_settings.n_groups = n_groups wing = Wing(settings) body_aero = BodyAerodynamics([wing]) @@ -139,36 +105,38 @@ using Test set_va!(body_aero, va) sol = solve!(solver, body_aero) - # Verify arrays have correct size - @test length(sol.cl_group_array) == n_groups - @test length(sol.cd_group_array) == n_groups - @test length(sol.cm_group_array) == n_groups - - # Verify group coefficients are computed correctly - panels_per_group = n_panels ÷ n_groups - for group_idx in 1:n_groups - panel_start = (group_idx - 1) * panels_per_group + 1 - panel_end = group_idx * panels_per_group - - expected_cl = sum(sol.cl_array[panel_start:panel_end]) / panels_per_group - expected_cd = sum(sol.cd_array[panel_start:panel_end]) / panels_per_group - expected_cm = sum(sol.cm_array[panel_start:panel_end]) / panels_per_group + n_unrefined_panels = wing.n_unrefined_sections - 1 - # Handle NaN for all coefficients - if isnan(expected_cl) - @test isnan(sol.cl_group_array[group_idx]) - else - @test isapprox(sol.cl_group_array[group_idx], expected_cl, rtol=1e-10) - end - if isnan(expected_cd) - @test isnan(sol.cd_group_array[group_idx]) - else - @test isapprox(sol.cd_group_array[group_idx], expected_cd, rtol=1e-10) - end - if isnan(expected_cm) - @test isnan(sol.cm_group_array[group_idx]) - else - @test isapprox(sol.cm_group_array[group_idx], expected_cm, rtol=1e-10) + # Verify arrays have correct size + @test length(sol.cl_unrefined_array) == n_unrefined_panels + @test length(sol.cd_unrefined_array) == n_unrefined_panels + @test length(sol.cm_unrefined_array) == n_unrefined_panels + + # Verify unrefined coefficients are computed correctly using mapping + for unrefined_idx in 1:n_unrefined_panels + refined_panel_indices = findall(x -> x == unrefined_idx, wing.refined_panel_mapping) + + if !isempty(refined_panel_indices) + expected_cl = sum(sol.cl_array[refined_panel_indices]) / length(refined_panel_indices) + expected_cd = sum(sol.cd_array[refined_panel_indices]) / length(refined_panel_indices) + expected_cm = sum(sol.cm_array[refined_panel_indices]) / length(refined_panel_indices) + + # Handle NaN for all coefficients + if isnan(expected_cl) + @test isnan(sol.cl_unrefined_array[unrefined_idx]) + else + @test isapprox(sol.cl_unrefined_array[unrefined_idx], expected_cl, rtol=1e-10) + end + if isnan(expected_cd) + @test isnan(sol.cd_unrefined_array[unrefined_idx]) + else + @test isapprox(sol.cd_unrefined_array[unrefined_idx], expected_cd, rtol=1e-10) + end + if isnan(expected_cm) + @test isnan(sol.cm_unrefined_array[unrefined_idx]) + else + @test isapprox(sol.cm_unrefined_array[unrefined_idx], expected_cm, rtol=1e-10) + end end end diff --git a/test/wing_geometry/test_wing_geometry.jl b/test/wing_geometry/test_wing_geometry.jl index ddb8ae25..05458a64 100644 --- a/test/wing_geometry/test_wing_geometry.jl +++ b/test/wing_geometry/test_wing_geometry.jl @@ -333,14 +333,14 @@ end end end - @testset "REFINE grouping panel mapping" begin + @testset "Refined panel mapping" begin # Test that refined panel mapping actually maps each panel to its closest unrefined panel @testset "LINEAR distribution" begin n_panels = 20 span = 10.0 - wing = Wing(n_panels; spanwise_distribution=LINEAR, n_groups=2, grouping_method=REFINE) + wing = Wing(n_panels; spanwise_distribution=LINEAR) # 3 sections = 2 unrefined panels add_section!(wing, [0.0, span/2, 0.0], [1.0, span/2, 0.0], INVISCID) add_section!(wing, [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], INVISCID) @@ -386,7 +386,7 @@ end n_panels = 30 span = 20.0 - wing = Wing(n_panels; spanwise_distribution=COSINE, n_groups=3, grouping_method=REFINE) + wing = Wing(n_panels; spanwise_distribution=COSINE) # 4 sections = 3 unrefined panels add_section!(wing, [0.0, span/2, 0.0], [1.0, span/2, 0.0], INVISCID) add_section!(wing, [0.0, span/6, 0.0], [1.0, span/6, 0.0], INVISCID) @@ -432,7 +432,7 @@ end @testset "SPLIT_PROVIDED distribution" begin n_panels = 12 - wing = Wing(n_panels; spanwise_distribution=SPLIT_PROVIDED, n_groups=3, grouping_method=REFINE) + wing = Wing(n_panels; spanwise_distribution=SPLIT_PROVIDED) # 4 sections = 3 unrefined panels add_section!(wing, [0.0, 6.0, 0.0], [1.0, 6.0, 0.0], INVISCID) add_section!(wing, [0.0, 2.0, 0.0], [1.0, 2.0, 0.0], INVISCID) @@ -475,14 +475,5 @@ end end end - @testset "Validation: n_groups must equal unrefined panels" begin - wing = Wing(20; spanwise_distribution=LINEAR, n_groups=5, grouping_method=REFINE) - add_section!(wing, [0.0, 5.0, 0.0], [1.0, 5.0, 0.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, 0.0], [1.0, -5.0, 0.0], INVISCID) - - # Should throw error: 5 groups but only 2 unrefined panels - @test_throws ArgumentError refine_aerodynamic_mesh!(wing) - end end end \ No newline at end of file diff --git a/test/yaml_geometry/test_wing_constructor.jl b/test/yaml_geometry/test_wing_constructor.jl index 19ec9869..87a1ab8e 100644 --- a/test/yaml_geometry/test_wing_constructor.jl +++ b/test/yaml_geometry/test_wing_constructor.jl @@ -37,11 +37,11 @@ using Logging # Use the actual YAML file from the test data cp(test_data_path("yaml_geometry", "simple_wing.yaml"), test_yaml_path; force=true) - wing = Wing(test_yaml_path; n_panels=4, n_groups=2) + wing = Wing(test_yaml_path; n_panels=4, # n_groups=2) @test wing isa Wing @test wing.n_panels == 4 - @test wing.n_groups == 2 + @test wing.n_unrefined_sections - 1 == 2 @test wing.spanwise_distribution == LINEAR @test wing.spanwise_direction ≈ [0.0, 1.0, 0.0] @test length(wing.sections) == 2 # simple_wing has 2 sections @@ -72,13 +72,13 @@ using Logging wing = Wing( test_yaml_path; n_panels=8, - n_groups=4, + # n_groups=4, spanwise_distribution=COSINE, remove_nan=false ) @test wing.n_panels == 8 - @test wing.n_groups == 4 + @test wing.n_unrefined_sections - 1 == 4 @test wing.spanwise_distribution == COSINE @test !wing.remove_nan end @@ -150,10 +150,10 @@ wing_airfoils: write(test_yaml_path, yaml_content) # Test invalid n_panels/n_groups combination - @test_throws ArgumentError Wing(test_yaml_path; n_panels=5, n_groups=2) + @test_throws ArgumentError Wing(test_yaml_path; n_panels=5, # n_groups=2) - # Test n_groups=0 (no grouping functionality) - wing_no_groups = Wing(test_yaml_path; n_panels=4, n_groups=0) + # Test # n_groups=0 (no grouping functionality) + wing_no_groups = Wing(test_yaml_path; n_panels=4, # n_groups=0) @test wing_no_groups.n_groups == 0 @test wing_no_groups.n_panels == 4 @@ -190,10 +190,10 @@ wing_airfoils: # Use the actual complex_wing.yaml file cp(test_data_path("yaml_geometry", "complex_wing.yaml"), test_yaml_path; force=true) - wing = Wing(test_yaml_path; n_panels=12, n_groups=3) + wing = Wing(test_yaml_path; n_panels=12, # n_groups=3) @test wing.n_panels == 12 - @test wing.n_groups == 3 + @test wing.n_unrefined_sections - 1 == 3 @test length(wing.sections) == 7 # Test that different airfoil_ids get different polar data @@ -216,7 +216,7 @@ wing_airfoils: settings.wings = [WingSettings( geometry_file=simple_wing_file, n_panels=6, - n_groups=3, + # n_groups=3, spanwise_panel_distribution=COSINE )] @@ -225,7 +225,7 @@ wing_airfoils: @test wing isa Wing @test wing.n_panels == 6 - @test wing.n_groups == 3 + @test wing.n_unrefined_sections - 1 == 3 @test wing.spanwise_distribution == COSINE @test length(wing.sections) == 2 @test wing.sections[1].aero_model == POLAR_VECTORS @@ -238,17 +238,17 @@ wing_airfoils: @test isfile(simple_wing_file) # Test basic Wing construction with shared data - wing = Wing(simple_wing_file; n_panels=4, n_groups=2) + wing = Wing(simple_wing_file; n_panels=4, # n_groups=2) @test wing isa Wing @test wing.n_panels == 4 - @test wing.n_groups == 2 + @test wing.n_unrefined_sections - 1 == 2 @test length(wing.sections) == 2 # Test complex wing construction complex_wing_file = test_data_path("yaml_geometry", "complex_wing.yaml") @test isfile(complex_wing_file) - complex_wing = Wing(complex_wing_file; n_panels=12, n_groups=3) + complex_wing = Wing(complex_wing_file; n_panels=12, # n_groups=3) @test complex_wing isa Wing @test complex_wing.n_panels == 12 @test complex_wing.n_groups == 3 @@ -262,7 +262,7 @@ wing_airfoils: standard_wing_file = simple_wing_file # Use simple_wing as our "standard" @test isfile(standard_wing_file) - standard_wing = Wing(standard_wing_file; n_panels=2, n_groups=1) + standard_wing = Wing(standard_wing_file; n_panels=2, # n_groups=1) @test standard_wing isa Wing @test length(standard_wing.sections) == 2 end diff --git a/test/yaml_geometry/test_yaml_wing_deformation.jl b/test/yaml_geometry/test_yaml_wing_deformation.jl index 578528b2..ac51e243 100644 --- a/test/yaml_geometry/test_yaml_wing_deformation.jl +++ b/test/yaml_geometry/test_yaml_wing_deformation.jl @@ -6,7 +6,7 @@ using Test @testset "Simple Wing Deformation" begin # Load existing simple_wing.yaml simple_wing_file = test_data_path("yaml_geometry", "simple_wing.yaml") - wing = Wing(simple_wing_file; n_panels=4, n_groups=2) + wing = Wing(simple_wing_file; n_panels=4, # n_groups=2) body_aero = BodyAerodynamics([wing]) # Store original TE point for comparison @@ -53,7 +53,7 @@ using Test @testset "Complex Wing Deformation" begin # Load existing complex_wing.yaml with multiple sections complex_wing_file = test_data_path("yaml_geometry", "complex_wing.yaml") - wing = Wing(complex_wing_file; n_panels=12, n_groups=3) + wing = Wing(complex_wing_file; n_panels=12, # n_groups=3) body_aero = BodyAerodynamics([wing]) # Store original points for multiple panels @@ -104,7 +104,7 @@ using Test @testset "Multiple Reinit Calls with NTuple aero_data" begin # This test specifically checks the NTuple handling fix simple_wing_file = test_data_path("yaml_geometry", "simple_wing.yaml") - wing = Wing(simple_wing_file; n_panels=4, n_groups=2) + wing = Wing(simple_wing_file; n_panels=4, # n_groups=2) # Verify that sections have NTuple aero_data (for wings with simple polars) # or other valid AeroData types @@ -125,7 +125,7 @@ using Test @testset "Deformation with BodyAerodynamics Reinit" begin # Test that reinit! on BodyAerodynamics properly handles deformed wings simple_wing_file = test_data_path("yaml_geometry", "simple_wing.yaml") - wing = Wing(simple_wing_file; n_panels=4, n_groups=2) + wing = Wing(simple_wing_file; n_panels=4, # n_groups=2) body_aero = BodyAerodynamics([wing]) # Apply deformation @@ -153,7 +153,7 @@ using Test @testset "Edge Cases" begin simple_wing_file = test_data_path("yaml_geometry", "simple_wing.yaml") - wing = Wing(simple_wing_file; n_panels=2, n_groups=1) + wing = Wing(simple_wing_file; n_panels=2, # n_groups=1) body_aero = BodyAerodynamics([wing]) # Test zero deformation From 988b1c91f37fb17158d7b771de1394d0041b1407 Mon Sep 17 00:00:00 2001 From: 1-Bart-1 Date: Thu, 4 Dec 2025 14:46:13 +0100 Subject: [PATCH 27/53] Use glmakie --- examples/V3_kite.jl | 2 +- examples/pyramid_model.jl | 2 +- examples/ram_air_kite.jl | 1 + examples/rectangular_wing.jl | 2 +- examples/stall_model.jl | 2 +- 5 files changed, 5 insertions(+), 4 deletions(-) diff --git a/examples/V3_kite.jl b/examples/V3_kite.jl index 9f50b6d4..4833c776 100644 --- a/examples/V3_kite.jl +++ b/examples/V3_kite.jl @@ -1,6 +1,6 @@ using LinearAlgebra using VortexStepMethod -using ControlPlots +using GLMakie project_dir = dirname(dirname(pathof(VortexStepMethod))) # Go up one level from src to project root# literature_paths = [ diff --git a/examples/pyramid_model.jl b/examples/pyramid_model.jl index 837e26ef..a67d8bb5 100644 --- a/examples/pyramid_model.jl +++ b/examples/pyramid_model.jl @@ -1,6 +1,6 @@ using LinearAlgebra using VortexStepMethod -using ControlPlots +using GLMakie project_dir = dirname(dirname(pathof(VortexStepMethod))) # Go up one level from src to project root diff --git a/examples/ram_air_kite.jl b/examples/ram_air_kite.jl index bbf1480b..f47e5938 100644 --- a/examples/ram_air_kite.jl +++ b/examples/ram_air_kite.jl @@ -12,6 +12,7 @@ LINEARIZE = false wing = ObjWing( joinpath("data", "ram_air_kite", "ram_air_kite_body.obj"), joinpath("data", "ram_air_kite", "ram_air_kite_foil.dat"); + n_unrefined_sections=4, prn=PRN ) body_aero = BodyAerodynamics([wing];) diff --git a/examples/rectangular_wing.jl b/examples/rectangular_wing.jl index 94dc5aec..360eb5ce 100644 --- a/examples/rectangular_wing.jl +++ b/examples/rectangular_wing.jl @@ -1,5 +1,5 @@ using LinearAlgebra -using ControlPlots +using GLMakie using VortexStepMethod PLOT = true diff --git a/examples/stall_model.jl b/examples/stall_model.jl index a0ee1079..510fe094 100644 --- a/examples/stall_model.jl +++ b/examples/stall_model.jl @@ -1,4 +1,4 @@ -using ControlPlots +using GLMakie using LinearAlgebra using VortexStepMethod From ab1a2822d5b5c07391ca8f85fb64a2e83c814793 Mon Sep 17 00:00:00 2001 From: 1-Bart-1 Date: Thu, 4 Dec 2025 14:46:56 +0100 Subject: [PATCH 28/53] Use unrefined segments and no groups --- src/body_aerodynamics.jl | 25 ++++++-- src/settings.jl | 9 ++- src/solver.jl | 28 ++++---- src/wing_geometry.jl | 134 +++++++++++++++++++-------------------- 4 files changed, 104 insertions(+), 92 deletions(-) diff --git a/src/body_aerodynamics.jl b/src/body_aerodynamics.jl index 1a0ae941..e4337c28 100644 --- a/src/body_aerodynamics.jl +++ b/src/body_aerodynamics.jl @@ -96,11 +96,11 @@ function BodyAerodynamics( end # Count total unrefined panels (sections - 1) - n_unrefined_total += (wing.n_unrefined_sections - 1) + n_unrefined_total += max(0, wing.n_unrefined_sections - 1) end # Initialize unrefined panels (representatives for unrefined sections) - unrefined = [Panel() for _ in 1:n_unrefined_total] + unrefined = [Panel() for _ in 1:max(0, n_unrefined_total)] body_aero = BodyAerodynamics{length(panels)}(; panels, wings, unrefined) reinit!(body_aero; va, omega) @@ -160,8 +160,10 @@ function reinit!(body_aero::BodyAerodynamics; # Create panels for i in 1:wing.n_panels - if !isnothing(wing.delta_dist) - delta = wing.delta_dist[i] + if !isnothing(wing.delta_dist) && length(wing.delta_dist) > 0 + # Map refined panel to unrefined section to get delta value + unrefined_idx = wing.refined_panel_mapping[i] + delta = wing.delta_dist[unrefined_idx] else delta = 0.0 end @@ -184,12 +186,23 @@ function reinit!(body_aero::BodyAerodynamics; idx += 1 end end - + + # Resize unrefined vector if needed (after wings are reinitialized and n_unrefined_sections is set) + n_unrefined_total = sum([max(0, wing.n_unrefined_sections - 1) for wing in body_aero.wings]) + if length(body_aero.unrefined) != n_unrefined_total + resize!(body_aero.unrefined, n_unrefined_total) + for i in 1:n_unrefined_total + if !isassigned(body_aero.unrefined, i) + body_aero.unrefined[i] = Panel() + end + end + end + # Initialize rest of the struct body_aero.projected_area = sum(calculate_projected_area, body_aero.wings) calculate_stall_angle_list!(body_aero.stall_angle_list, body_aero.panels) body_aero.alpha_array .= 0.0 - body_aero.v_a_array .= 0.0 + body_aero.v_a_array .= 0.0 body_aero.AIC .= 0.0 set_va!(body_aero, va, omega) return nothing diff --git a/src/settings.jl b/src/settings.jl index ea19b4d1..4010b8d9 100644 --- a/src/settings.jl +++ b/src/settings.jl @@ -79,17 +79,20 @@ function VSMSettings(filename; data_prefix=true) wing.dat_file = wing_data["dat_file"] end wing.n_panels = wing_data["n_panels"] - wing.n_groups = wing_data["n_groups"] + # Handle deprecated n_groups parameter + if haskey(wing_data, "n_groups") + @warn "n_groups in settings file is deprecated and ignored. Use n_unrefined_sections or let it be inferred automatically." maxlog=1 + end wing.spanwise_panel_distribution = eval(Symbol(wing_data["spanwise_panel_distribution"])) wing.spanwise_direction = MVec3(wing_data["spanwise_direction"]) if haskey(wing_data, "grouping_method") - wing.grouping_method = eval(Symbol(wing_data["grouping_method"])) + @warn "grouping_method in settings file is deprecated and ignored." maxlog=1 end wing.remove_nan = wing_data["remove_nan"] push!(vsm_settings.wings, wing) n_panels += wing.n_panels - n_groups += wing.n_groups + # n_unrefined_sections will be set when wing is created/initialized end end diff --git a/src/solver.jl b/src/solver.jl index eed0cd4e..a7137bfa 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -144,7 +144,7 @@ end function Solver(body_aero; kwargs...) P = length(body_aero.panels) - G = sum([(wing.n_unrefined_sections - 1) for wing in body_aero.wings]) + G = sum([max(0, wing.n_unrefined_sections - 1) for wing in body_aero.wings]) return Solver{P,G}(; kwargs...) end @@ -739,7 +739,7 @@ Compute the Jacobian matrix for a deformable wing around an operating point usin # Returns - `jac`: Jacobian matrix (∂outputs/∂inputs) -- `results`: Output vector at the operating point [Fx, Fy, Fz, Mx, My, Mz, group_moments...] +- `results`: Output vector at the operating point [Fx, Fy, Fz, Mx, My, Mz, unrefined_moments...] # Example ```julia @@ -773,17 +773,17 @@ function linearize(solver::Solver, body_aero::BodyAerodynamics, y::Vector{T}; !(length(body_aero.wings) == 1) && throw(ArgumentError("Linearization only works for a body_aero with one wing")) wing = body_aero.wings[1] - # Validate that theta_idxs and delta_idxs match the number of groups - if !isnothing(theta_idxs) && wing.n_groups > 0 - length(theta_idxs) != wing.n_groups && throw(ArgumentError( - "Length of theta_idxs ($(length(theta_idxs))) must match number of groups ($(wing.n_groups))")) + # Validate that theta_idxs and delta_idxs match the number of unrefined sections + if !isnothing(theta_idxs) && wing.n_unrefined_sections > 0 + length(theta_idxs) != wing.n_unrefined_sections && throw(ArgumentError( + "Length of theta_idxs ($(length(theta_idxs))) must match number of unrefined sections ($(wing.n_unrefined_sections))")) end - if !isnothing(delta_idxs) && wing.n_groups > 0 - length(delta_idxs) != wing.n_groups && throw(ArgumentError( - "Length of delta_idxs ($(length(delta_idxs))) must match number of groups ($(wing.n_groups))")) + if !isnothing(delta_idxs) && wing.n_unrefined_sections > 0 + length(delta_idxs) != wing.n_unrefined_sections && throw(ArgumentError( + "Length of delta_idxs ($(length(delta_idxs))) must match number of unrefined sections ($(wing.n_unrefined_sections))")) end - if wing.n_groups == 0 && (!isnothing(theta_idxs) || !isnothing(delta_idxs)) - throw(ArgumentError("Cannot use theta_idxs or delta_idxs when wing has n_groups=0 (no group functionality)")) + if wing.n_unrefined_sections == 0 && (!isnothing(theta_idxs) || !isnothing(delta_idxs)) + throw(ArgumentError("Cannot use theta_idxs or delta_idxs when wing has no unrefined sections")) end init_va = body_aero.cache[1][body_aero.va] @@ -835,16 +835,16 @@ function linearize(solver::Solver, body_aero::BodyAerodynamics, y::Vector{T}; if !aero_coeffs results[1:3] .= solver.sol.force results[4:6] .= solver.sol.moment - results[7:end] .= solver.sol.group_moment_dist + results[7:end] .= solver.sol.unrefined_moment_dist else results[1:3] .= solver.sol.force_coeffs results[4:6] .= solver.sol.moment_coeffs - results[7:end] .= solver.sol.group_moment_coeff_dist + results[7:end] .= solver.sol.unrefined_moment_coeff_dist end return nothing end - results = zeros(3+3+length(solver.sol.group_moment_dist)) + results = zeros(3+3+length(solver.sol.unrefined_moment_dist)) jac = zeros(length(results), length(y)) backend = AutoFiniteDiff(absstep=1e2solver.atol, relstep=1e2solver.rtol) prep = prepare_jacobian(calc_results!, results, backend, y) diff --git a/src/wing_geometry.jl b/src/wing_geometry.jl index ae7bcf46..7ac9e93c 100644 --- a/src/wing_geometry.jl +++ b/src/wing_geometry.jl @@ -236,8 +236,8 @@ mutable struct Wing <: AbstractWing # Deformation fields non_deformed_sections::Vector{Section} - theta_dist::Vector{Float64} - delta_dist::Vector{Float64} + theta_dist::Vector{Float64} # Length: n_unrefined_sections (section twist angles) + delta_dist::Vector{Float64} # Length: n_unrefined_sections (section TE deflection angles) # Physical properties (OBJ-based wings) mass::Float64 @@ -307,7 +307,7 @@ function Wing(n_panels::Int; # Grouping Int16[], # Deformation fields - Section[], zeros(n_panels), zeros(n_panels), + Section[], zeros(max(0, n_unrefined_sections_value)), zeros(max(0, n_unrefined_sections_value)), # Physical properties (defaults for non-OBJ wings) 0.0, 0.0, zeros(0, 0), zeros(MVec3), Matrix{Float64}(I, 3, 3), 0.0, nothing, nothing, nothing, @@ -346,27 +346,28 @@ end """ group_deform!(wing::Wing, theta_angles=nothing, delta_angles=nothing; smooth=false) -Distribute control angles across wing panels and optionally apply smoothing. +Apply deformation angles directly to unrefined wing sections. For wings that support deformation (OBJ-based wings with non_deformed_sections), this -distributes theta_angles and delta_angles to panel groups and applies deformation. +applies theta_angles and delta_angles directly to unrefined sections and then applies deformation. For wings without deformation support (YAML-based), this is a no-op that only succeeds if both angle inputs are nothing. # Arguments - `wing::Wing`: The wing to deform -- `theta_angles::AbstractVector`: Twist angles in radians for each control group (or nothing) -- `delta_angles::AbstractVector`: Trailing edge deflection angles in radians (or nothing) -- `smooth::Bool`: Whether to apply moving average smoothing +- `theta_angles::AbstractVector`: Twist angles in radians for each unrefined section (or nothing). + Length must be `n_unrefined_sections` +- `delta_angles::AbstractVector`: Trailing edge deflection angles in radians for each unrefined section (or nothing). + Length must be `n_unrefined_sections` +- `smooth::Bool`: DEPRECATED - no longer used. Apply smoothing to input angles if needed. # Algorithm -1. Distributes each control input to its corresponding group of panels -2. Optionally applies moving average smoothing with window based on group size -3. Calls deform! to update wing geometry +1. Copies theta_angles and delta_angles directly to wing.theta_dist and wing.delta_dist +2. Calls deform! to update wing geometry and propagate to refined sections # Errors - Throws `ArgumentError` if wing doesn't support deformation but angles are provided -- Throws `ArgumentError` if panel count is not divisible by number of control inputs +- Throws `ArgumentError` if angle vectors don't match n_unrefined_sections # Returns - `nothing` (modifies wing in-place) @@ -384,47 +385,25 @@ function group_deform!(wing::Wing, theta_angles=nothing, delta_angles=nothing; s end # Validate inputs - !isnothing(theta_angles) && !(wing.n_panels % length(theta_angles) == 0) && - throw(ArgumentError("Number of theta_angles has to be a divisor of number of panels")) - !isnothing(delta_angles) && !(wing.n_panels % length(delta_angles) == 0) && - throw(ArgumentError("Number of delta_angles has to be a divisor of number of panels")) - - n_panels = wing.n_panels - theta_dist = wing.theta_dist - delta_dist = wing.delta_dist - n_angles = isnothing(theta_angles) ? length(delta_angles) : length(theta_angles) - - # Distribute angles to panels - dist_idx = 0 - for angle_idx in 1:n_angles - for _ in 1:(wing.n_panels ÷ n_angles) - dist_idx += 1 - !isnothing(theta_angles) && (theta_dist[dist_idx] = theta_angles[angle_idx]) - !isnothing(delta_angles) && (delta_dist[dist_idx] = delta_angles[angle_idx]) - end - end - @assert (dist_idx == wing.n_panels) - - # Apply smoothing if requested - if smooth - window_size = wing.n_panels ÷ n_angles - if n_panels > window_size - smoothed = wing.cache[1][theta_dist] - - if !isnothing(theta_angles) - smoothed .= theta_dist - for i in (window_size÷2 + 1):(n_panels - window_size÷2) - @views smoothed[i] = mean(theta_dist[(i - window_size÷2):(i + window_size÷2)]) - end - theta_dist .= smoothed + !isnothing(theta_angles) && length(theta_angles) != wing.n_unrefined_sections && + throw(ArgumentError("theta_angles must have length n_unrefined_sections = $(wing.n_unrefined_sections), got $(length(theta_angles))")) + !isnothing(delta_angles) && length(delta_angles) != wing.n_unrefined_sections && + throw(ArgumentError("delta_angles must have length n_unrefined_sections = $(wing.n_unrefined_sections), got $(length(delta_angles))")) + + # Copy angles to theta_dist and delta_dist + !isnothing(theta_angles) && (wing.theta_dist .= theta_angles) + !isnothing(delta_angles) && (wing.delta_dist .= delta_angles) + + # Apply 3-point moving average smoothing if requested + if smooth && wing.n_unrefined_sections > 2 + if !isnothing(theta_angles) + for i in 2:(wing.n_unrefined_sections-1) + wing.theta_dist[i] = (wing.theta_dist[i-1] + wing.theta_dist[i] + wing.theta_dist[i+1]) / 3.0 end - - if !isnothing(delta_angles) - smoothed .= delta_dist - for i in (window_size÷2 + 1):(n_panels - window_size÷2) - @views smoothed[i] = mean(delta_dist[(i - window_size÷2):(i + window_size÷2)]) - end - delta_dist .= smoothed + end + if !isnothing(delta_angles) + for i in 2:(wing.n_unrefined_sections-1) + wing.delta_dist[i] = (wing.delta_dist[i-1] + wing.delta_dist[i] + wing.delta_dist[i+1]) / 3.0 end end end @@ -436,20 +415,20 @@ end """ deform!(wing::Wing, theta_dist::AbstractVector, delta_dist::AbstractVector) -Deform wing by applying theta and delta distributions directly. +Deform wing by applying theta and delta distributions directly to unrefined sections. # Arguments - `wing::Wing`: Wing to deform (must support deformation) -- `theta_dist::AbstractVector`: Twist angle in radians for each panel -- `delta_dist::AbstractVector`: Trailing edge deflection for each panel +- `theta_dist::AbstractVector`: Twist angle in radians for each unrefined section (length = n_unrefined_sections) +- `delta_dist::AbstractVector`: Trailing edge deflection for each unrefined section (length = n_unrefined_sections) # Effects Updates wing.sections with deformed geometry based on wing.non_deformed_sections """ function deform!(wing::Wing, theta_dist::AbstractVector, delta_dist::AbstractVector) !isempty(wing.non_deformed_sections) || throw(ArgumentError("Wing does not support deformation")) - !(length(theta_dist) == wing.n_panels) && throw(ArgumentError("theta_dist and panels are of different lengths")) - !(length(delta_dist) == wing.n_panels) && throw(ArgumentError("delta_dist and panels are of different lengths")) + !(length(theta_dist) == wing.n_unrefined_sections) && throw(ArgumentError("theta_dist must have length $(wing.n_unrefined_sections), got $(length(theta_dist))")) + !(length(delta_dist) == wing.n_unrefined_sections) && throw(ArgumentError("delta_dist must have length $(wing.n_unrefined_sections), got $(length(delta_dist))")) wing.theta_dist .= theta_dist wing.delta_dist .= delta_dist @@ -461,11 +440,16 @@ end Apply stored theta_dist and delta_dist to deform the wing geometry. +Deformation works by: +1. Applying theta/delta angles to unrefined sections (wing.sections) +2. Using refined_panel_mapping to determine which unrefined section each refined section came from +3. Applying the corresponding angle to each refined section + # Arguments - `wing::Wing`: Wing to deform (must have non_deformed_sections) # Effects -Updates wing.sections based on wing.non_deformed_sections and stored distributions +Updates wing.refined_sections based on wing.non_deformed_sections and stored distributions """ function deform!(wing::Wing) !isempty(wing.non_deformed_sections) || return nothing @@ -474,23 +458,25 @@ function deform!(wing::Wing) chord = zeros(MVec3) normal = zeros(MVec3) - # Process all sections (n_panels + 1) - # Each section gets angle(s) from adjacent panel(s) + # Process all refined sections (n_panels + 1) + # Each refined section gets the angle from its corresponding unrefined section for i in 1:(wing.n_panels + 1) - section = wing.non_deformed_sections[i] - - # Determine the angle for this section + # Determine which unrefined section this refined section belongs to if i == 1 - # First section: use angle from first panel - theta = wing.theta_dist[1] + # First section: use first unrefined section + unrefined_idx = 1 elseif i == wing.n_panels + 1 - # Last section: use angle from last panel - theta = wing.theta_dist[wing.n_panels] + # Last section: use last unrefined section + unrefined_idx = wing.n_unrefined_sections else - # Middle sections: average of adjacent panels - theta = 0.5 * (wing.theta_dist[i-1] + wing.theta_dist[i]) + # Middle sections: use the mapping from the panel to the left + unrefined_idx = wing.refined_panel_mapping[i-1] end + theta = wing.theta_dist[unrefined_idx] + + section = wing.non_deformed_sections[i] + # Compute local coordinate system if i < wing.n_panels + 1 section2 = wing.non_deformed_sections[i+1] @@ -696,6 +682,16 @@ function refine_aerodynamic_mesh!(wing::AbstractWing; recompute_mapping=true, so # Update n_unrefined_sections based on actual sections wing.n_unrefined_sections = Int16(length(wing.sections)) + # Resize theta_dist and delta_dist to match n_unrefined_sections + if length(wing.theta_dist) != wing.n_unrefined_sections + resize!(wing.theta_dist, wing.n_unrefined_sections) + fill!(wing.theta_dist, 0.0) + end + if length(wing.delta_dist) != wing.n_unrefined_sections + resize!(wing.delta_dist, wing.n_unrefined_sections) + fill!(wing.delta_dist, 0.0) + end + # Create/update non_deformed_sections to match refined_sections update_non_deformed_sections!(wing) From e500260ef0a15ed3eb61d1aedfe64420f33dcc79 Mon Sep 17 00:00:00 2001 From: 1-Bart-1 Date: Sat, 6 Dec 2025 10:14:16 +0100 Subject: [PATCH 29/53] Working ram example --- src/VortexStepMethod.jl | 6 ++- src/body_aerodynamics.jl | 6 ++- src/obj_geometry.jl | 24 ++++++--- src/solver.jl | 110 +++++++++++++++++++++++---------------- src/wing_geometry.jl | 17 ++++-- 5 files changed, 104 insertions(+), 59 deletions(-) diff --git a/src/VortexStepMethod.jl b/src/VortexStepMethod.jl index fa2b9c70..937dba01 100644 --- a/src/VortexStepMethod.jl +++ b/src/VortexStepMethod.jl @@ -129,14 +129,16 @@ Enumeration of the implemented panel distributions. - COSINE # Cosine distribution - `COSINE_VAN_GARREL` # van Garrel cosine distribution - `SPLIT_PROVIDED` # Split provided sections -- UNCHANGED # Keep original sections +- `UNCHANGED` # Keep original sections without interpolation +- `NONE` # No refinement - sections already refined """ @enum PanelDistribution begin LINEAR # Linear distribution COSINE # Cosine distribution COSINE_VAN_GARREL # van Garrel cosine distribution SPLIT_PROVIDED # Split provided sections - UNCHANGED # Keep original sections + UNCHANGED # Keep original sections without interpolation + NONE # No refinement - sections already refined end """ diff --git a/src/body_aerodynamics.jl b/src/body_aerodynamics.jl index e4337c28..831242a1 100644 --- a/src/body_aerodynamics.jl +++ b/src/body_aerodynamics.jl @@ -81,7 +81,11 @@ function BodyAerodynamics( section.LE_point .-= kite_body_origin section.TE_point .-= kite_body_origin end - if wing.spanwise_distribution == UNCHANGED + if wing.spanwise_distribution == NONE + # NONE distribution: refined_sections already populated in constructor + !(wing.n_panels == length(wing.refined_sections) - 1) && + throw(ArgumentError("(wing.n_panels = $(wing.n_panels)) != (length(wing.refined_sections) - 1 = $(length(wing.sections) - 1))")) + elseif wing.spanwise_distribution == UNCHANGED wing.refined_sections = wing.sections !(wing.n_panels == length(wing.sections) - 1) && throw(ArgumentError("(wing.n_panels = $(wing.n_panels)) != (length(wing.sections) - 1 = $(length(wing.sections) - 1))")) diff --git a/src/obj_geometry.jl b/src/obj_geometry.jl index e074a402..2a654893 100644 --- a/src/obj_geometry.jl +++ b/src/obj_geometry.jl @@ -381,7 +381,7 @@ This constructor builds a complete aerodynamic model by: 3. Computing inertial properties and coordinate transformations 4. Setting up control surfaces and panel distribution -The resulting Wing supports deformation through group_deform! and deform! functions. +The resulting Wing supports deformation through unrefined_deform! and deform! functions. # Arguments - `obj_path`: Path to .obj file containing 3D wing geometry @@ -395,7 +395,7 @@ The resulting Wing supports deformation through group_deform! and deform! functi - `n_groups=4`: Number of control groups for deformation - `n_sections=n_panels+1`: Number of spanwise cross-sections - `align_to_principal=false`: Align body frame to principal axes of inertia -- `spanwise_distribution=UNCHANGED`: Panel distribution type +- `spanwise_distribution=UNCHANGED`: Panel distribution type (forced to UNCHANGED for ObjWing) - `remove_nan=true`: Interpolate NaN values in aerodynamic data - `alpha_range=deg2rad.(-5:1:20)`: Angle of attack range for polars (rad) - `delta_range=deg2rad.(-5:1:20)`: Trailing edge deflection range for polars (rad) @@ -416,14 +416,14 @@ wing = ObjWing( ) # Apply deformation -group_deform!(wing, deg2rad.([5, 10, 5, 0]), deg2rad.([-5, 0, -5, 0])) +unrefined_deform!(wing, deg2rad.([5, 10, 5, 0]), deg2rad.([-5, 0, -5, 0])) ``` """ function ObjWing( obj_path, dat_path; crease_frac=0.9, wind_vel=10., mass=1.0, n_panels=56, n_sections=nothing, n_unrefined_sections=nothing, n_groups=nothing, - spanwise_distribution=LINEAR, + spanwise_distribution=UNCHANGED, spanwise_direction=[0.0, 1.0, 0.0], remove_nan=true, align_to_principal=false, alpha_range=deg2rad.(-5:1:20), delta_range=deg2rad.(-5:1:20), prn=true, interp_steps=n_panels+1, grouping_method::PanelGroupingMethod=EQUAL_SIZE @@ -445,6 +445,12 @@ function ObjWing( @warn "Parameter grouping_method is deprecated and ignored. Grouping is now always by unrefined sections." maxlog=1 end + # Force NONE distribution for ObjWing + if spanwise_distribution != NONE + @warn "ObjWing only supports spanwise_distribution=NONE. Overriding to NONE." maxlog=1 + spanwise_distribution = NONE + end + # Set default: evenly spaced unrefined sections including both tips if isnothing(n_unrefined_sections) # Default to having same number of unrefined sections as refined (no refinement) @@ -504,8 +510,12 @@ function ObjWing( push!(sections, Section(LE_point, TE_point, POLAR_MATRICES, aero_data)) end - # Initialize refined_sections (will be populated by reinit!) - refined_sections = [Section() for _ in 1:(n_panels+1)] + refined_sections = Section[] + for gamma in range(-gamma_tip, gamma_tip, n_panels+1) + LE_point = [le_interp[i](gamma) for i in 1:3] + TE_point = [te_interp[i](gamma) for i in 1:3] + push!(refined_sections, Section(LE_point, TE_point, POLAR_MATRICES, aero_data)) + end panel_props = PanelProperties{n_panels}() cache = [PreallocationTools.LazyBufferCache()] @@ -513,7 +523,7 @@ function ObjWing( wing = Wing(n_panels, Int16(n_unrefined_sections), spanwise_distribution, panel_props, MVec3(spanwise_direction), sections, refined_sections, remove_nan, Int16[], - Section[], zeros(n_panels), zeros(n_panels), + Section[], zeros(n_unrefined_sections), zeros(n_unrefined_sections), mass, gamma_tip, inertia_tensor, T_cad_body, R_cad_body, radius, le_interp, te_interp, area_interp, cache) diff --git a/src/solver.jl b/src/solver.jl index a7137bfa..7f25f354 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -22,15 +22,15 @@ Struct for storing the solution of the [solve!](@ref) function. Must contain all - `moment_coeffs`::MVec3: Aerodynamic moment coefficients [CMx, CMy, CMz] [-] - `moment_dist`::Vector{Float64}: Pitching moments around the spanwise vector of each panel. [Nm] - `moment_coeff_dist`::Vector{Float64}: Pitching moment coefficient around the spanwise vector of each panel. [-] -- `unrefined_moment_dist`::MVector{G, Float64}: Aggregated moments for unrefined sections [Nm] -- `unrefined_moment_coeff_dist`::MVector{G, Float64}: Aggregated moment coefficients for unrefined sections [-] -- `cl_unrefined_array`::MVector{G, Float64}: Averaged lift coefficients for unrefined sections [-] -- `cd_unrefined_array`::MVector{G, Float64}: Averaged drag coefficients for unrefined sections [-] -- `cm_unrefined_array`::MVector{G, Float64}: Averaged moment coefficients for unrefined sections [-] -- `alpha_unrefined_array`::MVector{G, Float64}: Averaged angles of attack for unrefined sections [rad] +- `unrefined_moment_dist`::MVector{U, Float64}: Aggregated moments for unrefined sections [Nm] +- `unrefined_moment_coeff_dist`::MVector{U, Float64}: Aggregated moment coefficients for unrefined sections [-] +- `cl_unrefined_array`::MVector{U, Float64}: Averaged lift coefficients for unrefined sections [-] +- `cd_unrefined_array`::MVector{U, Float64}: Averaged drag coefficients for unrefined sections [-] +- `cm_unrefined_array`::MVector{U, Float64}: Averaged moment coefficients for unrefined sections [-] +- `alpha_unrefined_array`::MVector{U, Float64}: Averaged angles of attack for unrefined sections [rad] - `solver_status`::SolverStatus: enum, see [SolverStatus](@ref) """ -@with_kw mutable struct VSMSolution{P,G} +@with_kw mutable struct VSMSolution{P,U} ### private vectors of solve_base! _x_airf_array::Matrix{Float64} = zeros(P, 3) _y_airf_array::Matrix{Float64} = zeros(P, 3) @@ -55,12 +55,12 @@ Struct for storing the solution of the [solve!](@ref) function. Must contain all moment_coeffs::MVec3 = zeros(MVec3) moment_dist::MVector{P, Float64} = zeros(P) moment_coeff_dist::MVector{P, Float64} = zeros(P) - unrefined_moment_dist::MVector{G, Float64} = zeros(G) - unrefined_moment_coeff_dist::MVector{G, Float64} = zeros(G) - cl_unrefined_array::MVector{G, Float64} = zeros(G) - cd_unrefined_array::MVector{G, Float64} = zeros(G) - cm_unrefined_array::MVector{G, Float64} = zeros(G) - alpha_unrefined_array::MVector{G, Float64} = zeros(G) + unrefined_moment_dist::MVector{U, Float64} = zeros(U) + unrefined_moment_coeff_dist::MVector{U, Float64} = zeros(U) + cl_unrefined_array::MVector{U, Float64} = zeros(U) + cd_unrefined_array::MVector{U, Float64} = zeros(U) + cm_unrefined_array::MVector{U, Float64} = zeros(U) + alpha_unrefined_array::MVector{U, Float64} = zeros(U) solver_status::SolverStatus = FAILURE end @@ -105,7 +105,7 @@ Main solver structure for the Vortex Step Method.See also: [solve](@ref) ## Solution sol::VSMSolution = VSMSolution(): The result of calling [solve!](@ref) """ -@with_kw mutable struct Solver{P,G} +@with_kw mutable struct Solver{P,U} # General settings solver_type::SolverType = LOOP aerodynamic_model_type::Model = VSM @@ -139,13 +139,13 @@ sol::VSMSolution = VSMSolution(): The result of calling [solve!](@ref) cache_lin::Vector{PreallocationTools.LazyBufferCache{typeof(identity), typeof(identity)}} = [LazyBufferCache() for _ in 1:4] # Solution - sol::VSMSolution{P,G} = VSMSolution{P,G}() + sol::VSMSolution{P,U} = VSMSolution{P,U}() end function Solver(body_aero; kwargs...) P = length(body_aero.panels) - G = sum([max(0, wing.n_unrefined_sections - 1) for wing in body_aero.wings]) - return Solver{P,G}(; kwargs...) + U = sum([wing.n_unrefined_sections for wing in body_aero.wings]) + return Solver{P,U}(; kwargs...) end function Solver(body_aero, settings::VSMSettings) @@ -719,47 +719,65 @@ function smooth_circulation!( end """ - linearize(solver::Solver, body_aero::BodyAerodynamics, wing::Wing, y::Vector{T}; - theta_idxs=1:4, delta_idxs=nothing, va_idxs=nothing, omega_idxs=nothing, kwargs...) where T + linearize(solver::Solver, body_aero::BodyAerodynamics, y::Vector{T}; + theta_idxs=1:4, delta_idxs=nothing, va_idxs=nothing, omega_idxs=nothing, + aero_coeffs=false, kwargs...) where T -Compute the Jacobian matrix for a deformable wing around an operating point using finite differences. +Compute Jacobian matrix of aerodynamic outputs with respect to control and kinematic inputs using +finite differences. Used for control system design and linear stability analysis. + +The function uses automatic differentiation with finite differences to compute ∂outputs/∂inputs. +Deformations are applied to the wing's unrefined sections (the original sections before mesh +refinement), with each control angle affecting one unrefined section. # Arguments -- `solver`: VSM solver instance (must be initialized) -- `body_aero`: Aerodynamic body representation -- `wing`: Wing model to linearize (must support deformation, i.e., created with ObjWing()) -- `y`: Input vector at operating point, containing a combination of control angles and velocities +- `solver::Solver`: Solver instance (must be configured for the wing) +- `body_aero::BodyAerodynamics`: Body aerodynamics with exactly one wing +- `y::Vector{T}`: Input vector at operating point containing control angles and/or kinematic states # Keyword Arguments -- `theta_idxs`: Indices of twist angles in input vector (default: 1:4) -- `delta_idxs`: Indices of trailing edge deflection angles (default: nothing) -- `va_idxs`: Indices of velocity components `[vx, vy, vz]` (default: nothing) -- `omega_idxs`: Indices of angular velocity components `[ωx, ωy, ωz]` (default: nothing) -- `kwargs...`: Additional arguments passed to the `solve!` function +- `theta_idxs`: Indices in `y` for twist angles (one per unrefined section, default: 1:4) +- `delta_idxs`: Indices in `y` for trailing edge deflections (one per unrefined section, default: nothing) +- `va_idxs`: Indices in `y` for apparent wind velocity [vx, vy, vz] (default: nothing) +- `omega_idxs`: Indices in `y` for angular velocity [ωx, ωy, ωz] (default: nothing) +- `aero_coeffs::Bool`: Return force/moment coefficients instead of dimensional values (default: false) +- `kwargs...`: Additional arguments passed to `solve!` + +# Index Validation +The lengths of `theta_idxs` and `delta_idxs` (if provided) must match `wing.n_unrefined_sections`. +Unrefined sections are the original wing sections before mesh refinement for aerodynamic analysis. + +# Caching +The function caches previous deformation angles to avoid redundant `unrefined_deform!` calls during +Jacobian computation. When the same angles are encountered, geometry deformation is skipped. # Returns -- `jac`: Jacobian matrix (∂outputs/∂inputs) -- `results`: Output vector at the operating point [Fx, Fy, Fz, Mx, My, Mz, unrefined_moments...] +- `jac::Matrix{Float64}`: Jacobian matrix (m×n) where m = 6 + n_unrefined_sections, n = length(y) +- `results::Vector{Float64}`: Output vector at operating point + - If `aero_coeffs=false`: [Fx, Fy, Fz, Mx, My, Mz, unrefined_moment_dist...] + - If `aero_coeffs=true`: [CFx, CFy, CFz, CMx, CMy, CMz, unrefined_moment_coeff_dist...] # Example ```julia -# Initialize wing and solver -wing = ObjWing("path/to/body.obj", "path/to/foil.dat") -body_aero = BodyAerodynamics([wing]) +# Create deformable wing with 4 unrefined sections +wing = ObjWing("kite.obj", "airfoil.dat"; n_unrefined_sections=4) +body_aero = BodyAerodynamics([wing], va=[15.0, 0, 0]) solver = Solver(body_aero) -# Define operating point with 4 control angles, velocity, and angular rates -y_op = [zeros(4); # 4 twist control angles (rad) - [15.0, 0.0, 0.0]; # Velocity vector (m/s) - zeros(3)] # Angular velocity (rad/s) +# Operating point: 4 twist angles + velocity + angular rates +y_op = [zeros(4); # theta angles [rad] + [15.0, 0.0, 0.0]; # va [m/s] + zeros(3)] # omega [rad/s] # Compute Jacobian -jac, results = linearize( - solver, body_aero, wing, y_op; - theta_idxs=1:4, # Twist angles - va_idxs=5:7, # Velocity components - omega_idxs=8:10 # Angular rates +jac, results = linearize(solver, body_aero, y_op; + theta_idxs=1:4, + va_idxs=5:7, + omega_idxs=8:10, + aero_coeffs=true ) + +# jac is (10×10): [6 force/moment coeffs + 4 unrefined moment coeffs] × [4 theta + 3 va + 3 omega] ``` """ function linearize(solver::Solver, body_aero::BodyAerodynamics, y::Vector{T}; @@ -804,20 +822,20 @@ function linearize(solver::Solver, body_aero::BodyAerodynamics, y::Vector{T}; if !isnothing(theta_angles) && isnothing(delta_angles) if !all(theta_angles .== last_theta) - VortexStepMethod.group_deform!(wing, theta_angles, nothing; smooth=false) + VortexStepMethod.unrefined_deform!(wing, theta_angles, nothing; smooth=false) VortexStepMethod.reinit!(body_aero; init_aero=false) last_theta .= theta_angles end elseif !isnothing(theta_angles) && !isnothing(delta_angles) if !all(delta_angles .== last_delta) || !all(theta_angles .== last_theta) - VortexStepMethod.group_deform!(wing, theta_angles, delta_angles; smooth=false) + VortexStepMethod.unrefined_deform!(wing, theta_angles, delta_angles; smooth=false) VortexStepMethod.reinit!(body_aero; init_aero=false) last_theta .= theta_angles last_delta .= delta_angles end elseif isnothing(theta_angles) && !isnothing(delta_angles) if !all(delta_angles .== last_delta) - VortexStepMethod.group_deform!(wing, nothing, delta_angles; smooth=false) + VortexStepMethod.unrefined_deform!(wing, nothing, delta_angles; smooth=false) VortexStepMethod.reinit!(body_aero; init_aero=false) last_delta .= delta_angles end diff --git a/src/wing_geometry.jl b/src/wing_geometry.jl index 7ac9e93c..eee799ca 100644 --- a/src/wing_geometry.jl +++ b/src/wing_geometry.jl @@ -46,7 +46,7 @@ function reinit!(section::Section, LE_point, TE_point, aero_model=nothing, aero_ if !isnothing(aero_data) # NTuple is immutable, so we must assign directly # For mutable types (Vector, Matrix), we can broadcast for efficiency - if aero_data isa NTuple || isnothing(section.aero_data) + if aero_data isa NTuple || aero_data isa Tuple || isnothing(section.aero_data) section.aero_data = aero_data else section.aero_data .= aero_data @@ -344,7 +344,7 @@ function reinit!(wing::AbstractWing; refine_mesh=true, recompute_mapping=true, s end """ - group_deform!(wing::Wing, theta_angles=nothing, delta_angles=nothing; smooth=false) + unrefined_deform!(wing::Wing, theta_angles=nothing, delta_angles=nothing; smooth=false) Apply deformation angles directly to unrefined wing sections. @@ -372,7 +372,7 @@ if both angle inputs are nothing. # Returns - `nothing` (modifies wing in-place) """ -function group_deform!(wing::Wing, theta_angles=nothing, delta_angles=nothing; smooth=false) +function unrefined_deform!(wing::Wing, theta_angles=nothing, delta_angles=nothing; smooth=false) # Check if deformation is supported can_deform = !isempty(wing.non_deformed_sections) @@ -608,6 +608,17 @@ function refine_aerodynamic_mesh!(wing::AbstractWing; recompute_mapping=true, so # Only sort sections if requested (skip for REFINE wings with fixed structural order) sort_sections && sort!(wing.sections, by=s -> s.LE_point[2], rev=true) n_sections = wing.n_panels + 1 + + # Handle NONE distribution - sections already refined, just compute mapping + if wing.spanwise_distribution == NONE + if length(wing.refined_sections) != n_sections + throw(ArgumentError("NONE distribution requires refined_sections to be pre-populated")) + end + recompute_mapping && compute_refined_panel_mapping!(wing) + update_non_deformed_sections!(wing) + return nothing + end + if length(wing.refined_sections) == 0 if wing.spanwise_distribution == UNCHANGED || length(wing.sections) == n_sections wing.refined_sections = wing.sections From a76454c3c66e6918d9bef846238764eeb4e06d80 Mon Sep 17 00:00:00 2001 From: 1-Bart-1 Date: Sat, 6 Dec 2025 10:49:19 +0100 Subject: [PATCH 30/53] Add combined plot --- examples/Project.toml | 3 + examples/ram_air_kite.jl | 70 +++------- ext/VortexStepMethodMakieExt.jl | 237 +++++++++++++++++++++++++++++++- 3 files changed, 261 insertions(+), 49 deletions(-) diff --git a/examples/Project.toml b/examples/Project.toml index fea9b2fd..f6082789 100644 --- a/examples/Project.toml +++ b/examples/Project.toml @@ -6,3 +6,6 @@ GLMakie = "e9467ef8-e4e7-5192-8a1a-b1aee30e663a" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" VortexStepMethod = "ed3cd733-9f0f-46a9-93e0-89b8d4998dd9" + +[sources] +VortexStepMethod = {path = ".."} diff --git a/examples/ram_air_kite.jl b/examples/ram_air_kite.jl index f47e5938..600acb7a 100644 --- a/examples/ram_air_kite.jl +++ b/examples/ram_air_kite.jl @@ -5,8 +5,8 @@ using LinearAlgebra PLOT = true PRN = true USE_TEX = false -DEFORM = false -LINEARIZE = false +DEFORM = true +LINEARIZE = true # Create wing geometry wing = ObjWing( @@ -22,7 +22,7 @@ println("First init") if DEFORM # Linear interpolation of alpha from 10° at one tip to 0° at the other println("Deform") - @time group_deform!(wing, deg2rad.([10,20,10,0]), deg2rad.([-10,0,-10,0]); smooth=true) + @time VortexStepMethod.unrefined_deform!(wing, deg2rad.([10,20,10,0]), deg2rad.([-10,0,-10,0]); smooth=true) println("Deform init") @time VortexStepMethod.reinit!(body_aero; init_aero=false) end @@ -68,55 +68,29 @@ if LINEARIZE moment_frac=0.1) end -# Plotting polar data -PLOT && plot_polar_data(body_aero) - -# Plotting geometry -PLOT && plot_geometry( - body_aero, - ""; - data_type=".svg", - save_path="", - is_save=false, - is_show=true, - view_elevation=15, - view_azimuth=-120, - use_tex=USE_TEX -) - -# Solving and plotting distributions +# Solving println("Solve") results = VortexStepMethod.solve(solver, body_aero; log=true) @time results = solve(solver, body_aero; log=true) body_y_coordinates = [panel.aero_center[2] for panel in body_aero.panels] -PLOT && plot_distribution( - [body_y_coordinates], - [results], - ["VSM"]; - title="CAD_spanwise_distributions_alpha_$(round(aoa, digits=1))_delta_$(round(side_slip, digits=1))_yaw_$(round(yaw_rate, digits=1))_v_a_$(round(v_a, digits=1))", - data_type=".pdf", - is_save=false, - is_show=true, - use_tex=USE_TEX -) - -PLOT && plot_polars( - [solver], - [body_aero], - [ - "VSM from Ram Air Kite OBJ and DAT file", - ]; - angle_range=range(0, 20, length=20), - angle_type="angle_of_attack", - angle_of_attack=0, - side_slip=0, - v_a=10, - title="ram_kite_panels_$(wing.n_panels)_distribution_$(wing.spanwise_distribution)", - data_type=".pdf", - is_save=false, - is_show=true, - use_tex=USE_TEX -) +if PLOT + plot_combined_analysis( + solver, + body_aero, + results; + solver_label="VSM", + angle_range=range(0, 20, length=20), + angle_type="angle_of_attack", + angle_of_attack=aoa, + side_slip=side_slip, + v_a=v_a, + title="Ram Air Kite (α=$(aoa)°, β=$(side_slip)°, v=$(v_a) m/s)", + view_elevation=15, + view_azimuth=-120, + is_show=true, + use_tex=USE_TEX + ) +end nothing diff --git a/ext/VortexStepMethodMakieExt.jl b/ext/VortexStepMethodMakieExt.jl index 918402a9..1f93c4f7 100644 --- a/ext/VortexStepMethodMakieExt.jl +++ b/ext/VortexStepMethodMakieExt.jl @@ -2,7 +2,8 @@ module VortexStepMethodMakieExt using Makie, VortexStepMethod, LinearAlgebra, Statistics, DelimitedFiles import VortexStepMethod: calculate_filaments_for_plotting -export plot_geometry, plot_distribution, plot_polars, save_plot, show_plot, plot_polar_data +export plot_geometry, plot_distribution, plot_polars, save_plot, show_plot, + plot_polar_data, plot_combined_analysis # Global storage for panel mesh observables (for dynamic plotting) const PANEL_MESH_OBSERVABLES = Ref{Union{Nothing, Dict}}(nothing) @@ -920,4 +921,238 @@ function VortexStepMethod.plot_polar_data(body_aero::BodyAerodynamics; end end +""" + plot_combined_analysis(solver, body_aero, results; + solver_label="VSM", + angle_range=range(0,20,length=20), + angle_type="angle_of_attack", + angle_of_attack=0.0, side_slip=0.0, v_a=10.0, + title="Combined Analysis", + view_elevation=15, view_azimuth=-120, + is_show=true, use_tex=false) + +Create combined multi-panel figure with geometry, polar data, distributions, and polars. + +# Arguments +- `solver`: Aerodynamic solver +- `body_aero`: BodyAerodynamics object +- `results`: Solution dictionary from solve() + +# Keyword arguments +- `solver_label`: Label for solver (default: "VSM") +- `angle_range`: Range of angles for polars (default: range(0,20,length=20)) +- `angle_type`: "angle_of_attack" or "side_slip" (default: "angle_of_attack") +- `angle_of_attack`: AoA in degrees (default: 0.0) +- `side_slip`: Side slip in degrees (default: 0.0) +- `v_a`: Wind speed in m/s (default: 10.0) +- `title`: Overall figure title (default: "Combined Analysis") +- `view_elevation`: Geometry view elevation [°] (default: 15) +- `view_azimuth`: Geometry view azimuth [°] (default: -120) +- `is_show`: Display figure (default: true) +- `use_tex`: Ignored for Makie (default: false) +""" +function VortexStepMethod.plot_combined_analysis( + solver, + body_aero::BodyAerodynamics, + results::Dict; + solver_label="VSM", + angle_range=range(0, 20, length=20), + angle_type="angle_of_attack", + angle_of_attack=0.0, + side_slip=0.0, + v_a=10.0, + title="Combined Analysis", + view_elevation=15, + view_azimuth=-120, + is_show=true, + use_tex=false +) + # Auto-detect screen size and use 80% of it + fig = try + screen_size = Makie.primary_resolution() + fig_width = round(Int, screen_size[1] * 0.8) + fig_height = round(Int, screen_size[2] * 0.8) + Figure(size=(fig_width, fig_height)) + catch + # Fallback if screen detection fails + Figure(size=(1800, 1200)) + end + Label(fig[0, :], title, fontsize=20, font=:bold) + + panels = body_aero.panels + va = isa(body_aero.va, Tuple) ? body_aero.va[1] : body_aero.va + + # [1,1] Wing Geometry + ax_geo = Axis3(fig[1, 1]; + title="Wing Geometry", + xlabel="x", ylabel="y", zlabel="z", + aspect=:data, + azimuth=deg2rad(view_azimuth), + elevation=deg2rad(view_elevation)) + + legend_used = Dict{String,Bool}() + for (i, panel) in enumerate(panels) + corners = [Point3f(panel.corner_points[:, j]) for j in 1:4] + push!(corners, corners[1]) + lines!(ax_geo, corners; color=:grey, linewidth=1, + label = i == 1 ? "Panel Edges" : nothing) + + scatter!(ax_geo, [Point3f(panel.control_point)]; + color=:green, markersize=10, + label = i == 1 ? "Control Points" : nothing) + + scatter!(ax_geo, [Point3f(panel.aero_center)]; + color=:blue, markersize=10, + label = i == 1 ? "Aerodynamic Centers" : nothing) + + filaments = calculate_filaments_for_plotting(panel) + legends = ["Bound Vortex", "side1", "side2", "wake_1", "wake_2"] + + for (filament, legend) in zip(filaments, legends) + x1, x2, color = filament + show_legend = !get(legend_used, legend, false) + plot_line_segment_makie!(ax_geo, [x1, x2], color, + show_legend ? legend : nothing) + legend_used[legend] = true + end + end + + max_chord = maximum(panel.chord for panel in panels) + va_mag = norm(va) + va_vector_begin = -2 * max_chord * va / va_mag + va_vector_end = va_vector_begin + 1.5 * va / va_mag + plot_line_segment_makie!(ax_geo, [va_vector_begin, va_vector_end], + :lightblue, "va") + + set_axes_equal_makie!(ax_geo, panels; zoom=0.5) + axislegend(ax_geo; position=:lt) + + # [1,2] Polar Data Surfaces + if body_aero.panels[1].aero_model == POLAR_MATRICES + alphas = collect(deg2rad.(-5:0.3:25)) + delta_tes = collect(deg2rad.(-5:0.3:25)) + + interp_data = [ + (body_aero.panels[1].cl_interp, "Cl"), + (body_aero.panels[1].cd_interp, "Cd"), + (body_aero.panels[1].cm_interp, "Cm") + ] + + for (idx, (interp, label)) in enumerate(interp_data) + ax = Axis3(fig[1, 2][1, idx]; + title="$label vs α and δ", + xlabel="δ [rad]", + ylabel="α [rad]", + zlabel=label, + azimuth=1.275*π) + + interp_matrix = [interp(alpha, delta_te) + for alpha in alphas, delta_te in delta_tes] + + wireframe!(ax, delta_tes, alphas, interp_matrix; + color=:blue, linewidth=0.5, transparency=true) + end + end + + # [2,1] Spanwise Distributions (3×3 grid) + y_coords = [panel.aero_center[2] for panel in body_aero.panels] + + ax_cl = Axis(fig[2, 1][1, 1], title="CL Distribution", + xlabel="Spanwise Position y/b", ylabel="CL") + ax_cd = Axis(fig[2, 1][1, 2], title="CD Distribution", + xlabel="Spanwise Position y/b", ylabel="CD") + ax_gamma = Axis(fig[2, 1][1, 3], title="Γ Distribution", + xlabel="Spanwise Position y/b", ylabel="Γ") + + ax_alpha_geo = Axis(fig[2, 1][2, 1], title="α Geometric", + xlabel="Spanwise Position y/b", ylabel="α (deg)") + ax_alpha_ac = Axis(fig[2, 1][2, 2], title="α at aero center", + xlabel="Spanwise Position y/b", ylabel="α (deg)") + ax_alpha_unc = Axis(fig[2, 1][2, 3], title="α Uncorrected", + xlabel="Spanwise Position y/b", ylabel="α (deg)") + + ax_fx = Axis(fig[2, 1][3, 1], title="Force x", + xlabel="Spanwise Position y/b", ylabel="Fx") + ax_fy = Axis(fig[2, 1][3, 2], title="Force y", + xlabel="Spanwise Position y/b", ylabel="Fy") + ax_fz = Axis(fig[2, 1][3, 3], title="Force z", + xlabel="Spanwise Position y/b", ylabel="Fz") + + cl_val = round(results["cl"], digits=2) + lines!(ax_cl, Vector(y_coords), Vector(results["cl_distribution"]), + label="$solver_label CL: $cl_val") + axislegend(ax_cl, position=:lt) + + cd_val = round(results["cd"], digits=2) + lines!(ax_cd, Vector(y_coords), Vector(results["cd_distribution"]), + label="$solver_label CD: $cd_val") + axislegend(ax_cd, position=:lt) + + lines!(ax_gamma, Vector(y_coords), Vector(results["gamma_distribution"]), + label=solver_label) + axislegend(ax_gamma, position=:lt) + + lines!(ax_alpha_geo, Vector(y_coords), Vector(results["alpha_geometric"]), + label=solver_label) + axislegend(ax_alpha_geo, position=:lt) + + lines!(ax_alpha_ac, Vector(y_coords), Vector(results["alpha_at_ac"]), + label=solver_label) + axislegend(ax_alpha_ac, position=:lt) + + lines!(ax_alpha_unc, Vector(y_coords), + Vector(results["alpha_uncorrected"]), label=solver_label) + axislegend(ax_alpha_unc, position=:lt) + + force_axes = [ax_fx, ax_fy, ax_fz] + components = ["x", "y", "z"] + for (idx, (ax, comp)) in enumerate(zip(force_axes, components)) + forces = results["F_distribution"][idx, :] + total_force = round(results["F$comp"], digits=2) + lines!(ax, Vector(y_coords), Vector(forces), + label="$solver_label ΣF$comp: $total_force N") + axislegend(ax, position=:lt) + end + + # [2,2] Polars (2×2 grid) + polar_data, rey = generate_polar_data_makie( + solver, body_aero, angle_range; + angle_type, angle_of_attack=deg2rad(angle_of_attack), + side_slip=deg2rad(side_slip), v_a + ) + + label_with_re = "$solver_label Re = $(round(Int64, rey*1e-5))e5" + + ax_cl_polar = Axis(fig[2, 2][1, 1], title="CL vs $angle_type [°]", + xlabel="$angle_type [°]", ylabel="CL") + ax_cd_polar = Axis(fig[2, 2][1, 2], title="CD vs $angle_type [°]", + xlabel="$angle_type [°]", ylabel="CD") + ax_cs_polar = Axis(fig[2, 2][2, 1], title="CS vs $angle_type [°]", + xlabel="$angle_type [°]", ylabel="CS") + ax_polar = Axis(fig[2, 2][2, 2], title="CL vs CD", + xlabel="CD", ylabel="CL") + + scatterlines!(ax_cl_polar, polar_data[1], polar_data[2]; + label=label_with_re, marker=:star5, markersize=12) + axislegend(ax_cl_polar, position=:lt) + + scatterlines!(ax_cd_polar, polar_data[1], polar_data[3]; + label=label_with_re, marker=:star5, markersize=12) + axislegend(ax_cd_polar, position=:lt) + + scatterlines!(ax_cs_polar, polar_data[1], polar_data[4]; + label=label_with_re, marker=:star5, markersize=12) + axislegend(ax_cs_polar, position=:lt) + + scatterlines!(ax_polar, polar_data[3], polar_data[2]; + label=label_with_re, marker=:star5, markersize=12) + axislegend(ax_polar, position=:lt) + + if is_show + display(fig) + end + + return fig +end + end From 55d2d0dc8126b3e33ba3005480495e8404064187 Mon Sep 17 00:00:00 2001 From: 1-Bart-1 Date: Sat, 6 Dec 2025 11:51:15 +0100 Subject: [PATCH 31/53] Dont use old vsm --- test/Project.toml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/Project.toml b/test/Project.toml index 7b362c6d..fca78da6 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -2,9 +2,9 @@ Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" -ControlPlots = "23c2ee80-7a9e-4350-b264-8e670f12517c" DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +GLMakie = "e9467ef8-e4e7-5192-8a1a-b1aee30e663a" Interpolations = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" @@ -13,16 +13,15 @@ Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" -VortexStepMethod = "ed3cd733-9f0f-46a9-93e0-89b8d4998dd9" YAML = "ddb6d928-2868-570f-bddf-ab3f9cf99eb6" [compat] Aqua = "0.8" BenchmarkTools = "1" CSV = "0.10" -ControlPlots = "0.2.5" DataFrames = "1.7" Documenter = "1.8" +GLMakie = "0" Interpolations = "0.15, 0.16" LinearAlgebra = "1" Logging = "1" From 3f0cf3dcd5631bf0b327957948ed86464d3742d0 Mon Sep 17 00:00:00 2001 From: 1-Bart-1 Date: Sat, 6 Dec 2025 11:51:44 +0100 Subject: [PATCH 32/53] Actually remove super confusing unrefined panels, simplify to use unrefined segments everywhere --- src/VortexStepMethod.jl | 4 +- src/body_aerodynamics.jl | 31 +++---------- src/solver.jl | 93 ++++++++++++++++++++++----------------- src/wing_geometry.jl | 95 ++++++++++++++++++++-------------------- 4 files changed, 111 insertions(+), 112 deletions(-) diff --git a/src/VortexStepMethod.jl b/src/VortexStepMethod.jl index 937dba01..92710573 100644 --- a/src/VortexStepMethod.jl +++ b/src/VortexStepMethod.jl @@ -43,7 +43,8 @@ export SolverStatus, FEASIBLE, INFEASIBLE, FAILURE export SolverType, LOOP, NONLIN export load_polar_data -export plot_geometry, plot_distribution, plot_circulation_distribution, plot_polars, save_plot, show_plot, plot_polar_data +export plot_geometry, plot_distribution, plot_circulation_distribution, plot_polars, + save_plot, show_plot, plot_polar_data, plot_combined_analysis # the following functions are defined in ext/VortexStepMethodExt.jl function plot_geometry end @@ -53,6 +54,7 @@ function plot_polars end function save_plot end function show_plot end function plot_polar_data end +function plot_combined_analysis end """ const MVec3 = MVector{3, Float64} diff --git a/src/body_aerodynamics.jl b/src/body_aerodynamics.jl index 831242a1..6b3ed591 100644 --- a/src/body_aerodynamics.jl +++ b/src/body_aerodynamics.jl @@ -6,7 +6,6 @@ Main structure for calculating aerodynamic properties of bodies. Use the constru # Fields - panels::Vector{Panel}: Vector of refined [Panel](@ref) structs - wings::Vector{Wing}: A vector of wings; a body can have multiple wings -- unrefined::Vector{Panel}: Vector of unrefined panel representatives for aggregated results - `va::MVec3` = zeros(MVec3): A vector of the apparent wind speed, see: [MVec3](@ref) - `omega`::MVec3 = zeros(MVec3): A vector of the turn rates around the kite body axes - `gamma_distribution`=zeros(Float64, P): A vector of the circulation @@ -25,7 +24,6 @@ Main structure for calculating aerodynamic properties of bodies. Use the constru @with_kw mutable struct BodyAerodynamics{P} panels::Vector{Panel} wings::Vector{Wing} - unrefined::Vector{Panel} = Panel[] _va::MVec3 = zeros(MVec3) omega::MVec3 = zeros(MVec3) gamma_distribution::MVector{P, Float64} = zeros(P) @@ -77,18 +75,18 @@ function BodyAerodynamics( panels = Panel[] n_unrefined_total = 0 for wing in wings - for section in wing.sections + for section in wing.unrefined_sections section.LE_point .-= kite_body_origin section.TE_point .-= kite_body_origin end if wing.spanwise_distribution == NONE # NONE distribution: refined_sections already populated in constructor !(wing.n_panels == length(wing.refined_sections) - 1) && - throw(ArgumentError("(wing.n_panels = $(wing.n_panels)) != (length(wing.refined_sections) - 1 = $(length(wing.sections) - 1))")) + throw(ArgumentError("(wing.n_panels = $(wing.n_panels)) != (length(wing.refined_sections) - 1 = $(length(wing.unrefined_sections) - 1))")) elseif wing.spanwise_distribution == UNCHANGED - wing.refined_sections = wing.sections - !(wing.n_panels == length(wing.sections) - 1) && - throw(ArgumentError("(wing.n_panels = $(wing.n_panels)) != (length(wing.sections) - 1 = $(length(wing.sections) - 1))")) + wing.refined_sections = wing.unrefined_sections + !(wing.n_panels == length(wing.unrefined_sections) - 1) && + throw(ArgumentError("(wing.n_panels = $(wing.n_panels)) != (length(wing.unrefined_sections) - 1 = $(length(wing.unrefined_sections) - 1))")) else wing.refined_sections = Section[Section() for _ in 1:wing.n_panels+1] end @@ -98,15 +96,9 @@ function BodyAerodynamics( panel = Panel() push!(panels, panel) end - - # Count total unrefined panels (sections - 1) - n_unrefined_total += max(0, wing.n_unrefined_sections - 1) end - # Initialize unrefined panels (representatives for unrefined sections) - unrefined = [Panel() for _ in 1:max(0, n_unrefined_total)] - - body_aero = BodyAerodynamics{length(panels)}(; panels, wings, unrefined) + body_aero = BodyAerodynamics{length(panels)}(; panels, wings) reinit!(body_aero; va, omega) return body_aero end @@ -191,17 +183,6 @@ function reinit!(body_aero::BodyAerodynamics; end end - # Resize unrefined vector if needed (after wings are reinitialized and n_unrefined_sections is set) - n_unrefined_total = sum([max(0, wing.n_unrefined_sections - 1) for wing in body_aero.wings]) - if length(body_aero.unrefined) != n_unrefined_total - resize!(body_aero.unrefined, n_unrefined_total) - for i in 1:n_unrefined_total - if !isassigned(body_aero.unrefined, i) - body_aero.unrefined[i] = Panel() - end - end - end - # Initialize rest of the struct body_aero.projected_area = sum(calculate_projected_area, body_aero.wings) calculate_stall_angle_list!(body_aero.stall_angle_list, body_aero.panels) diff --git a/src/solver.jl b/src/solver.jl index 7f25f354..111e2cea 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -61,6 +61,12 @@ Struct for storing the solution of the [solve!](@ref) function. Must contain all cd_unrefined_array::MVector{U, Float64} = zeros(U) cm_unrefined_array::MVector{U, Float64} = zeros(U) alpha_unrefined_array::MVector{U, Float64} = zeros(U) + x_airf_unrefined_array::Vector{MVec3} = [MVec3(0,0,0) for _ in 1:U] + y_airf_unrefined_array::Vector{MVec3} = [MVec3(0,0,0) for _ in 1:U] + z_airf_unrefined_array::Vector{MVec3} = [MVec3(0,0,0) for _ in 1:U] + va_unrefined_array::Vector{MVec3} = [MVec3(0,0,0) for _ in 1:U] + chord_unrefined_array::MVector{U, Float64} = zeros(U) + width_unrefined_array::MVector{U, Float64} = zeros(U) solver_status::SolverStatus = FAILURE end @@ -312,69 +318,78 @@ function solve!(solver::Solver, body_aero::BodyAerodynamics, gamma_distribution= cd_unrefined_array = solver.sol.cd_unrefined_array cm_unrefined_array = solver.sol.cm_unrefined_array alpha_unrefined_array = solver.sol.alpha_unrefined_array + x_airf_unrefined_array = solver.sol.x_airf_unrefined_array + y_airf_unrefined_array = solver.sol.y_airf_unrefined_array + z_airf_unrefined_array = solver.sol.z_airf_unrefined_array + va_unrefined_array = solver.sol.va_unrefined_array + chord_unrefined_array = solver.sol.chord_unrefined_array + width_unrefined_array = solver.sol.width_unrefined_array + + # Zero all unrefined arrays unrefined_moment_dist .= 0.0 unrefined_moment_coeff_dist .= 0.0 cl_unrefined_array .= 0.0 cd_unrefined_array .= 0.0 cm_unrefined_array .= 0.0 alpha_unrefined_array .= 0.0 + for i in eachindex(x_airf_unrefined_array) + x_airf_unrefined_array[i] .= 0.0 + y_airf_unrefined_array[i] .= 0.0 + z_airf_unrefined_array[i] .= 0.0 + va_unrefined_array[i] .= 0.0 + end + chord_unrefined_array .= 0.0 + width_unrefined_array .= 0.0 + panel_idx = 1 unrefined_idx = 1 for wing in body_aero.wings - n_unrefined_panels = wing.n_unrefined_sections - 1 - if n_unrefined_panels > 0 - # Initialize unrefined panels - for i in 1:n_unrefined_panels - target_unrefined_idx = unrefined_idx + i - 1 - unrefined_panel = body_aero.unrefined[target_unrefined_idx] - unrefined_panel.x_airf .= 0.0 - unrefined_panel.y_airf .= 0.0 - unrefined_panel.z_airf .= 0.0 - unrefined_panel.va .= 0.0 - unrefined_panel.chord = 0.0 - unrefined_panel.width = 0.0 - end - # Accumulate values from refined panels - unrefined_panel_counts = zeros(Int, n_unrefined_panels) + if wing.n_unrefined_sections > 0 + # Accumulate values from refined panels to unrefined sections + unrefined_section_counts = zeros(Int, wing.n_unrefined_sections) for local_panel_idx in 1:wing.n_panels panel = body_aero.panels[panel_idx] - original_panel_idx = wing.refined_panel_mapping[local_panel_idx] - target_unrefined_idx = unrefined_idx + original_panel_idx - 1 - unrefined_panel = body_aero.unrefined[target_unrefined_idx] + original_section_idx = wing.refined_panel_mapping[local_panel_idx] + target_unrefined_idx = unrefined_idx + original_section_idx - 1 + + # Accumulate coefficients and moments unrefined_moment_dist[target_unrefined_idx] += moment_dist[panel_idx] unrefined_moment_coeff_dist[target_unrefined_idx] += moment_coeff_dist[panel_idx] cl_unrefined_array[target_unrefined_idx] += solver.sol.cl_array[panel_idx] cd_unrefined_array[target_unrefined_idx] += solver.sol.cd_array[panel_idx] cm_unrefined_array[target_unrefined_idx] += solver.sol.cm_array[panel_idx] alpha_unrefined_array[target_unrefined_idx] += solver.sol.alpha_array[panel_idx] + # Accumulate geometry - unrefined_panel.x_airf .+= panel.x_airf - unrefined_panel.y_airf .+= panel.y_airf - unrefined_panel.z_airf .+= panel.z_airf - unrefined_panel.va .+= panel.va - unrefined_panel.chord += panel.chord - unrefined_panel.width += panel.width - unrefined_panel_counts[original_panel_idx] += 1 + x_airf_unrefined_array[target_unrefined_idx] .+= panel.x_airf + y_airf_unrefined_array[target_unrefined_idx] .+= panel.y_airf + z_airf_unrefined_array[target_unrefined_idx] .+= panel.z_airf + va_unrefined_array[target_unrefined_idx] .+= panel.va + chord_unrefined_array[target_unrefined_idx] += panel.chord + width_unrefined_array[target_unrefined_idx] += panel.width + + unrefined_section_counts[original_section_idx] += 1 panel_idx += 1 end + # Average coefficients and geometry - for i in 1:n_unrefined_panels + for i in 1:wing.n_unrefined_sections target_unrefined_idx = unrefined_idx + i - 1 - if unrefined_panel_counts[i] > 0 - unrefined_panel = body_aero.unrefined[target_unrefined_idx] - cl_unrefined_array[target_unrefined_idx] /= unrefined_panel_counts[i] - cd_unrefined_array[target_unrefined_idx] /= unrefined_panel_counts[i] - cm_unrefined_array[target_unrefined_idx] /= unrefined_panel_counts[i] - alpha_unrefined_array[target_unrefined_idx] /= unrefined_panel_counts[i] - unrefined_panel.x_airf ./= unrefined_panel_counts[i] - unrefined_panel.y_airf ./= unrefined_panel_counts[i] - unrefined_panel.z_airf ./= unrefined_panel_counts[i] - unrefined_panel.va ./= unrefined_panel_counts[i] - unrefined_panel.chord /= unrefined_panel_counts[i] - unrefined_panel.width /= unrefined_panel_counts[i] + if unrefined_section_counts[i] > 0 + count = unrefined_section_counts[i] + cl_unrefined_array[target_unrefined_idx] /= count + cd_unrefined_array[target_unrefined_idx] /= count + cm_unrefined_array[target_unrefined_idx] /= count + alpha_unrefined_array[target_unrefined_idx] /= count + x_airf_unrefined_array[target_unrefined_idx] ./= count + y_airf_unrefined_array[target_unrefined_idx] ./= count + z_airf_unrefined_array[target_unrefined_idx] ./= count + va_unrefined_array[target_unrefined_idx] ./= count + chord_unrefined_array[target_unrefined_idx] /= count + width_unrefined_array[target_unrefined_idx] /= count end end - unrefined_idx += n_unrefined_panels + unrefined_idx += wing.n_unrefined_sections else # Skip panels for wings with no unrefined sections panel_idx += wing.n_panels diff --git a/src/wing_geometry.jl b/src/wing_geometry.jl index eee799ca..857f5fe5 100644 --- a/src/wing_geometry.jl +++ b/src/wing_geometry.jl @@ -227,12 +227,12 @@ mutable struct Wing <: AbstractWing spanwise_distribution::PanelDistribution panel_props::PanelProperties spanwise_direction::MVec3 - sections::Vector{Section} + unrefined_sections::Vector{Section} refined_sections::Vector{Section} remove_nan::Bool # Grouping - refined_panel_mapping::Vector{Int16} # Maps each refined panel to its original unrefined section index + refined_panel_mapping::Vector{Int16} # Maps each refined panel index to unrefined section index (1 to n_unrefined_sections) # Deformation fields non_deformed_sections::Vector{Section} @@ -423,7 +423,7 @@ Deform wing by applying theta and delta distributions directly to unrefined sect - `delta_dist::AbstractVector`: Trailing edge deflection for each unrefined section (length = n_unrefined_sections) # Effects -Updates wing.sections with deformed geometry based on wing.non_deformed_sections +Updates wing.unrefined_sections with deformed geometry based on wing.non_deformed_sections """ function deform!(wing::Wing, theta_dist::AbstractVector, delta_dist::AbstractVector) !isempty(wing.non_deformed_sections) || throw(ArgumentError("Wing does not support deformation")) @@ -441,7 +441,7 @@ end Apply stored theta_dist and delta_dist to deform the wing geometry. Deformation works by: -1. Applying theta/delta angles to unrefined sections (wing.sections) +1. Applying theta/delta angles to unrefined sections (wing.unrefined_sections) 2. Using refined_panel_mapping to determine which unrefined section each refined section came from 3. Applying the corresponding angle to each refined section @@ -545,7 +545,7 @@ function add_section!(wing::Wing, LE_point, elseif aero_model == POLAR_MATRICES && wing.remove_nan interpolate_matrix_nans!.(aero_data[3:5]) end - push!(wing.sections, Section(LE_point, TE_point, aero_model, aero_data)) + push!(wing.unrefined_sections, Section(LE_point, TE_point, aero_model, aero_data)) return nothing end @@ -606,7 +606,7 @@ Returns: """ function refine_aerodynamic_mesh!(wing::AbstractWing; recompute_mapping=true, sort_sections=true) # Only sort sections if requested (skip for REFINE wings with fixed structural order) - sort_sections && sort!(wing.sections, by=s -> s.LE_point[2], rev=true) + sort_sections && sort!(wing.unrefined_sections, by=s -> s.LE_point[2], rev=true) n_sections = wing.n_panels + 1 # Handle NONE distribution - sections already refined, just compute mapping @@ -620,8 +620,8 @@ function refine_aerodynamic_mesh!(wing::AbstractWing; recompute_mapping=true, so end if length(wing.refined_sections) == 0 - if wing.spanwise_distribution == UNCHANGED || length(wing.sections) == n_sections - wing.refined_sections = wing.sections + if wing.spanwise_distribution == UNCHANGED || length(wing.unrefined_sections) == n_sections + wing.refined_sections = wing.unrefined_sections update_non_deformed_sections!(wing) return nothing else @@ -630,13 +630,13 @@ function refine_aerodynamic_mesh!(wing::AbstractWing; recompute_mapping=true, so end # Extract geometry data - n_current = length(wing.sections) + n_current = length(wing.unrefined_sections) LE = zeros(Float64, n_current, 3) TE = zeros(Float64, n_current, 3) - aero_model = Vector{typeof(wing.sections[1].aero_model)}() - aero_data = Vector{typeof(wing.sections[1].aero_data)}() + aero_model = Vector{typeof(wing.unrefined_sections[1].aero_model)}() + aero_data = Vector{typeof(wing.unrefined_sections[1].aero_data)}() - for (i, section) in enumerate(wing.sections) + for (i, section) in enumerate(wing.unrefined_sections) LE[i,:] = section.LE_point TE[i,:] = section.TE_point push!(aero_model, section.aero_model) @@ -649,16 +649,16 @@ function refine_aerodynamic_mesh!(wing::AbstractWing; recompute_mapping=true, so end # Handle special cases - if wing.spanwise_distribution == UNCHANGED || length(wing.sections) == n_sections - for i in eachindex(wing.sections) - reinit!(wing.refined_sections[i], wing.sections[i]) + if wing.spanwise_distribution == UNCHANGED || length(wing.unrefined_sections) == n_sections + for i in eachindex(wing.unrefined_sections) + reinit!(wing.refined_sections[i], wing.unrefined_sections[i]) end recompute_mapping && compute_refined_panel_mapping!(wing) update_non_deformed_sections!(wing) return nothing end - @debug "Refining aerodynamic mesh from $(length(wing.sections)) sections to $n_sections sections." + @debug "Refining aerodynamic mesh from $(length(wing.unrefined_sections)) sections to $n_sections sections." # Handle two-section case if n_sections == 2 @@ -687,11 +687,11 @@ function refine_aerodynamic_mesh!(wing::AbstractWing; recompute_mapping=true, so throw(ArgumentError("Unsupported spanwise panel distribution: $(wing.spanwise_distribution)")) end - # Compute panel mapping by finding closest unrefined panel for each refined panel + # Compute panel mapping by finding closest unrefined section for each refined panel recompute_mapping && compute_refined_panel_mapping!(wing) # Update n_unrefined_sections based on actual sections - wing.n_unrefined_sections = Int16(length(wing.sections)) + wing.n_unrefined_sections = Int16(length(wing.unrefined_sections)) # Resize theta_dist and delta_dist to match n_unrefined_sections if length(wing.theta_dist) != wing.n_unrefined_sections @@ -713,12 +713,14 @@ end """ compute_refined_panel_mapping!(wing::AbstractWing) -Compute the mapping from refined panels to unrefined panels by finding -the closest unrefined panel for each refined panel (based on panel center distance). +Compute the mapping from refined panels to unrefined sections by finding +the closest unrefined section for each refined panel (based on section center distance). +Maps each refined panel index to its corresponding unrefined section index +(1 to n_unrefined_sections). This is non-allocating and works after refinement is complete. """ function compute_refined_panel_mapping!(wing::AbstractWing) - n_unrefined_sections = length(wing.sections) + n_unrefined_sections = length(wing.unrefined_sections) n_refined_panels = wing.n_panels # Handle case where no refinement occurred @@ -732,16 +734,15 @@ function compute_refined_panel_mapping!(wing::AbstractWing) wing.refined_panel_mapping = zeros(Int16, n_refined_panels) end - # Compute centers of unrefined panels - n_unrefined_panels = n_unrefined_sections - 1 - unrefined_centers = Vector{MVec3}(undef, n_unrefined_panels) - for i in 1:n_unrefined_panels - le_mid = (wing.sections[i].LE_point + wing.sections[i+1].LE_point) / 2 - te_mid = (wing.sections[i].TE_point + wing.sections[i+1].TE_point) / 2 - unrefined_centers[i] = MVec3((le_mid + te_mid) / 2) + # Compute centers of unrefined sections + unrefined_centers = Vector{MVec3}(undef, n_unrefined_sections) + for i in 1:n_unrefined_sections + le_point = wing.unrefined_sections[i].LE_point + te_point = wing.unrefined_sections[i].TE_point + unrefined_centers[i] = MVec3((le_point + te_point) / 2) end - # For each refined panel, find closest unrefined panel + # For each refined panel, find closest unrefined section for refined_panel_idx in 1:n_refined_panels le_mid = (wing.refined_sections[refined_panel_idx].LE_point + wing.refined_sections[refined_panel_idx+1].LE_point) / 2 @@ -749,14 +750,14 @@ function compute_refined_panel_mapping!(wing::AbstractWing) wing.refined_sections[refined_panel_idx+1].TE_point) / 2 refined_center = MVec3((le_mid + te_mid) / 2) - # Find closest unrefined panel + # Find closest unrefined section min_dist = Inf closest_idx = 1 - for unrefined_panel_idx in 1:n_unrefined_panels - dist = norm(refined_center - unrefined_centers[unrefined_panel_idx]) + for unrefined_section_idx in 1:n_unrefined_sections + dist = norm(refined_center - unrefined_centers[unrefined_section_idx]) if dist < min_dist min_dist = dist - closest_idx = unrefined_panel_idx + closest_idx = unrefined_section_idx end end @@ -1039,7 +1040,7 @@ Returns: Vector{Section}: Refined sections """ function refine_mesh_by_splitting_provided_sections!(wing::AbstractWing) - n_sections_provided = length(wing.sections) + n_sections_provided = length(wing.unrefined_sections) n_panels_provided = n_sections_provided - 1 n_panels_desired = wing.n_panels @@ -1047,7 +1048,7 @@ function refine_mesh_by_splitting_provided_sections!(wing::AbstractWing) # Check if refinement is needed if n_panels_provided == n_panels_desired - for (refined_section, section) in zip(wing.refined_sections, wing.sections) + for (refined_section, section) in zip(wing.refined_sections, wing.unrefined_sections) reinit!(refined_section, section) end return nothing @@ -1067,16 +1068,16 @@ function refine_mesh_by_splitting_provided_sections!(wing::AbstractWing) new_sections_per_pair, remaining = divrem(n_new_sections, n_section_pairs) # Extract geometry data - LE = [section.LE_point for section in wing.sections] - TE = [section.TE_point for section in wing.sections] - aero_model = [section.aero_model for section in wing.sections] - aero_data = [section.aero_data for section in wing.sections] + LE = [section.LE_point for section in wing.unrefined_sections] + TE = [section.TE_point for section in wing.unrefined_sections] + aero_model = [section.aero_model for section in wing.unrefined_sections] + aero_data = [section.aero_data for section in wing.unrefined_sections] # Process each section pair idx = 1 for left_section_index in 1:n_section_pairs # Add left section of pair - reinit!(wing.refined_sections[idx], wing.sections[left_section_index]) + reinit!(wing.refined_sections[idx], wing.unrefined_sections[left_section_index]) idx += 1 # Calculate new sections for this pair @@ -1111,7 +1112,7 @@ function refine_mesh_by_splitting_provided_sections!(wing::AbstractWing) end # Add final section - reinit!(wing.refined_sections[idx], wing.sections[end]) + reinit!(wing.refined_sections[idx], wing.unrefined_sections[end]) idx += 1 # Validate result @@ -1137,7 +1138,7 @@ function calculate_span(wing::AbstractWing) # Get all points all_points = reduce(vcat, [[section.LE_point, section.TE_point] - for section in wing.sections]) + for section in wing.unrefined_sections]) # Project points and calculate span projections = [dot(point, vector_axis) for point in all_points] @@ -1170,12 +1171,12 @@ function calculate_projected_area(wing::AbstractWing, # Calculate area by summing trapezoid areas projected_area = 0.0 - for i in 1:(length(wing.sections)-1) + for i in 1:(length(wing.unrefined_sections)-1) # Get section points - LE_current = wing.sections[i].LE_point - TE_current = wing.sections[i].TE_point - LE_next = wing.sections[i+1].LE_point - TE_next = wing.sections[i+1].TE_point + LE_current = wing.unrefined_sections[i].LE_point + TE_current = wing.unrefined_sections[i].TE_point + LE_next = wing.unrefined_sections[i+1].LE_point + TE_next = wing.unrefined_sections[i+1].TE_point # Project points project_onto_plane!(LE_current_proj, LE_current, z_plane_vector) From c536ba936b473e429eb2b89c0496fdc1b40fd562 Mon Sep 17 00:00:00 2001 From: 1-Bart-1 Date: Sat, 6 Dec 2025 11:56:57 +0100 Subject: [PATCH 33/53] Adjust for using the unrefined segment approach --- .../test_body_aerodynamics.jl | 16 +- test/body_aerodynamics/test_results.jl | 22 +-- test/plotting/test_plotting.jl | 20 +-- test/ram_geometry/test_kite_geometry.jl | 28 ++-- test/solver/test_group_coefficients.jl | 148 ------------------ test/test_data_utils.jl | 2 - test/wing_geometry/test_wing_geometry.jl | 42 ++--- test/yaml_geometry/test_wing_constructor.jl | 106 ++++++------- .../test_yaml_wing_deformation.jl | 43 ++--- 9 files changed, 131 insertions(+), 296 deletions(-) delete mode 100644 test/solver/test_group_coefficients.jl diff --git a/test/body_aerodynamics/test_body_aerodynamics.jl b/test/body_aerodynamics/test_body_aerodynamics.jl index c022102f..239e5113 100644 --- a/test/body_aerodynamics/test_body_aerodynamics.jl +++ b/test/body_aerodynamics/test_body_aerodynamics.jl @@ -126,12 +126,12 @@ end body_aero = BodyAerodynamics([wing]; kite_body_origin=origin) # Check if sections are correctly translated - @test wing.sections[3].LE_point ≈ [-1.0, -2.0, -3.0] - @test wing.sections[3].TE_point ≈ [0.0, -2.0, -3.0] - @test wing.sections[2].LE_point ≈ [-1.0, -1.0, -3.0] - @test wing.sections[2].TE_point ≈ [0.0, -1.0, -3.0] - @test wing.sections[1].LE_point ≈ [-1.0, 0.0, -3.0] - @test wing.sections[1].TE_point ≈ [0.0, 0.0, -3.0] + @test wing.unrefined_sections[3].LE_point ≈ [-1.0, -2.0, -3.0] + @test wing.unrefined_sections[3].TE_point ≈ [0.0, -2.0, -3.0] + @test wing.unrefined_sections[2].LE_point ≈ [-1.0, -1.0, -3.0] + @test wing.unrefined_sections[2].TE_point ≈ [0.0, -1.0, -3.0] + @test wing.unrefined_sections[1].LE_point ≈ [-1.0, 0.0, -3.0] + @test wing.unrefined_sections[1].TE_point ≈ [0.0, 0.0, -3.0] end function create_geometry(; model=VSM, wing_type=:rectangular, plotting=false, N=40) @@ -351,8 +351,8 @@ end @test loop_sol.solver_status == FEASIBLE - @test sum(loop_sol.moment_dist) ≈ sum(loop_sol.group_moment_dist) - @test sum(nonlin_sol.moment_dist) ≈ sum(nonlin_sol.group_moment_dist) + @test sum(loop_sol.moment_dist) ≈ sum(loop_sol.unrefined_moment_dist) + @test sum(nonlin_sol.moment_dist) ≈ sum(nonlin_sol.unrefined_moment_dist) end diff --git a/test/body_aerodynamics/test_results.jl b/test/body_aerodynamics/test_results.jl index 60d2d4cf..80be8585 100644 --- a/test/body_aerodynamics/test_results.jl +++ b/test/body_aerodynamics/test_results.jl @@ -31,7 +31,7 @@ if !@isdefined ram_wing_results error("Required data files not found: $body_src or $foil_src") end - ram_wing = ObjWing(body_path, foil_path; alpha_range=deg2rad.(-1:1), delta_range=deg2rad.(-1:1)) + ram_wing = ObjWing(body_path, foil_path; alpha_range=deg2rad.(-1:1), delta_range=deg2rad.(-1:1), n_unrefined_sections=4) end @testset "Nonlinear vs Linear - Comprehensive Input Testing" begin @@ -48,7 +48,7 @@ end domega_magnitudes = [deg2rad(0.1), deg2rad(0.5), deg2rad(1.0)] # Angular rate perturbations (rad/s) # Create body aerodynamics and solver - VortexStepMethod.group_deform!(ram_wing, theta, delta; smooth=false) + VortexStepMethod.unrefined_deform!(ram_wing, theta, delta; smooth=false) body_aero = BodyAerodynamics([ram_wing]; va, omega) solver = Solver(body_aero; aerodynamic_model_type=VSM, @@ -85,8 +85,8 @@ end # Verify that linearization results match nonlinear results at operating point baseline_res = VortexStepMethod.solve!(solver, body_aero; log=false) - baseline_res = [solver.sol.force; solver.sol.moment; solver.sol.group_moment_dist] - coeff_baseline_res = [solver.sol.force_coeffs; solver.sol.moment_coeffs; solver.sol.group_moment_coeff_dist] + baseline_res = [solver.sol.force; solver.sol.moment; solver.sol.unrefined_moment_dist] + coeff_baseline_res = [solver.sol.force_coeffs; solver.sol.moment_coeffs; solver.sol.unrefined_moment_coeff_dist] @test baseline_res ≈ lin_res @test coeff_baseline_res ≈ coeff_lin_res @@ -137,13 +137,13 @@ end else throw(ArgumentError()) end - VortexStepMethod.group_deform!(ram_wing, reset_theta, reset_delta; smooth=false) + VortexStepMethod.unrefined_deform!(ram_wing, reset_theta, reset_delta; smooth=false) reinit!(body_aero; init_aero=false, va=reset_va, omega=reset_omega) # Get nonlinear solution nonlin_res = VortexStepMethod.solve!(solver, body_aero, nothing; log=false) - nonlin_res = [solver.sol.force; solver.sol.moment; solver.sol.group_moment_dist] - coeff_nonlin_res = [solver.sol.force_coeffs; solver.sol.moment_coeffs; solver.sol.group_moment_coeff_dist] + nonlin_res = [solver.sol.force; solver.sol.moment; solver.sol.unrefined_moment_dist] + coeff_nonlin_res = [solver.sol.force_coeffs; solver.sol.moment_coeffs; solver.sol.unrefined_moment_coeff_dist] @test nonlin_res ≉ baseline_res @test coeff_nonlin_res ≉ baseline_res @@ -220,7 +220,7 @@ end for (combo_name, active_indices, perturbation, idx_mappings) in combination_tests @testset "$combo_name" begin # Start with a fresh model for each combination test - VortexStepMethod.group_deform!(ram_wing, theta, delta; smooth=false) + VortexStepMethod.unrefined_deform!(ram_wing, theta, delta; smooth=false) reinit!(body_aero; init_aero=false, va, omega) # Create the appropriate input vector for this combination @@ -250,7 +250,7 @@ end # Get baseline results baseline_res = VortexStepMethod.solve!(solver, body_aero; log=false) - baseline_res = [solver.sol.force; solver.sol.moment; solver.sol.group_moment_dist] + baseline_res = [solver.sol.force; solver.sol.moment; solver.sol.unrefined_moment_dist] # Should match the linearization result @test baseline_res ≈ lin_res_combo @@ -272,12 +272,12 @@ end perturbed_input[idx_mappings.delta_idxs] : delta # Apply to nonlinear model - VortexStepMethod.group_deform!(ram_wing, perturbed_theta, perturbed_delta; smooth=false) + VortexStepMethod.unrefined_deform!(ram_wing, perturbed_theta, perturbed_delta; smooth=false) reinit!(body_aero; init_aero=false, va=perturbed_va, omega=perturbed_omega) # Get nonlinear solution with perturbation nonlin_res = VortexStepMethod.solve!(solver, body_aero; log=false) - nonlin_res = [solver.sol.force; solver.sol.moment; solver.sol.group_moment_dist] + nonlin_res = [solver.sol.force; solver.sol.moment; solver.sol.unrefined_moment_dist] # Compute linearized prediction using our specialized Jacobian lin_prediction = lin_res_combo + jac_combo * perturbation diff --git a/test/plotting/test_plotting.jl b/test/plotting/test_plotting.jl index e1f82959..eba66aa2 100644 --- a/test/plotting/test_plotting.jl +++ b/test/plotting/test_plotting.jl @@ -1,5 +1,5 @@ using VortexStepMethod -using ControlPlots +using GLMakie using Test # Resolve repo data directory for ram air kite assets @@ -63,17 +63,8 @@ function create_body_aero() body_aero end -plt.ioff() @testset "Plotting" begin - fig = plt.figure(figsize=(14, 14)) - res = plt.plot([1,2,3]) - @test fig isa plt.PyPlot.Figure - @test res isa Vector{plt.PyObject} save_dir = tempdir() - save_plot(fig, save_dir, "plot") - @test isfile(joinpath(save_dir, "plot.pdf")) - safe_rm(joinpath(save_dir, "plot.pdf")) - show_plot(fig) body_aero = create_body_aero() if Sys.islinux() fig = plot_geometry( @@ -83,7 +74,7 @@ plt.ioff() save_path=save_dir, is_save=true, is_show=false) - @test fig isa plt.PyPlot.Figure + @test fig isa Figure @test isfile(joinpath(save_dir, "Rectangular_wing_geometry_angled_view.pdf")) safe_rm(joinpath(save_dir, "Rectangular_wing_geometry_angled_view.pdf")) @test isfile(joinpath(save_dir, "Rectangular_wing_geometry_front_view.pdf")) @@ -110,7 +101,7 @@ plt.ioff() ["VSM", "LLT"], title="Spanwise Distributions" ) - @test fig isa plt.PyPlot.Figure + @test fig isa Figure # Step 8: Plot polar curves v_a = 20.0 # Magnitude of inflow velocity [m/s] @@ -128,15 +119,14 @@ plt.ioff() is_save=true, is_show=false ) - @test fig isa plt.PyPlot.Figure + @test fig isa Figure @test isfile(joinpath(save_dir, "Rectangular_Wing_Polars.pdf")) safe_rm(joinpath(save_dir, "Rectangular_Wing_Polars.pdf")) # Step 9: Test polar data plotting body_aero = BodyAerodynamics([ram_wing]) fig = plot_polar_data(body_aero; is_show=false) - @test fig isa plt.PyPlot.Figure + @test fig isa Figure end end -plt.ion() nothing \ No newline at end of file diff --git a/test/ram_geometry/test_kite_geometry.jl b/test/ram_geometry/test_kite_geometry.jl index c4d35500..d76cc533 100644 --- a/test/ram_geometry/test_kite_geometry.jl +++ b/test/ram_geometry/test_kite_geometry.jl @@ -171,18 +171,18 @@ using Serialization @test wing.n_panels == 56 # Default value @test wing.spanwise_distribution == UNCHANGED @test wing.spanwise_direction ≈ [0.0, 1.0, 0.0] - @test length(wing.sections) > 0 # Should have sections now + @test length(wing.unrefined_sections) > 0 # Should have sections now @test wing.mass ≈ 1.0 @test wing.radius ≈ r rtol=1e-2 @test wing.gamma_tip ≈ π/4 rtol=1e-2 - @test !isnan(wing.sections[1].aero_data[3][end]) - @test !isnan(wing.sections[1].aero_data[4][end]) - @test !isnan(wing.sections[1].aero_data[5][end]) + @test !isnan(wing.unrefined_sections[1].aero_data[3][end]) + @test !isnan(wing.unrefined_sections[1].aero_data[4][end]) + @test !isnan(wing.unrefined_sections[1].aero_data[5][end]) wing = ObjWing(test_obj_path, test_dat_path; remove_nan=false) - @test isnan(wing.sections[1].aero_data[3][end]) - @test isnan(wing.sections[1].aero_data[4][end]) - @test isnan(wing.sections[1].aero_data[5][end]) + @test isnan(wing.unrefined_sections[1].aero_data[3][end]) + @test isnan(wing.unrefined_sections[1].aero_data[4][end]) + @test isnan(wing.unrefined_sections[1].aero_data[5][end]) end @testset "Wing Deformation" begin @@ -195,8 +195,8 @@ using Serialization original_te_point = copy(body_aero.panels[i].TE_point_1) # Apply deformation with non-zero angles - theta_dist = fill(deg2rad(30.0), wing.n_panels) # 10 degrees twist - delta_dist = fill(deg2rad(5.0), wing.n_panels) # 5 degrees trailing edge deflection + theta_dist = fill(deg2rad(30.0), wing.n_unrefined_sections) # 30 degrees twist for all sections + delta_dist = fill(deg2rad(5.0), wing.n_unrefined_sections) # 5 degrees trailing edge deflection for all sections VortexStepMethod.deform!(wing, theta_dist, delta_dist) VortexStepMethod.reinit!(body_aero) @@ -221,10 +221,10 @@ using Serialization @test original_te_point ≈ reset_te_point atol=1e-4 end - @testset "First and Last Section Deformation with group_deform!" begin - # Create an ObjWing with a small number of panels and groups + @testset "First and Last Section Deformation with unrefined_deform!" begin + # Create an ObjWing with a small number of panels and unrefined sections wing = ObjWing(test_obj_path, test_dat_path; - n_panels=4, remove_nan=true) + n_panels=4, n_unrefined_sections=2, remove_nan=true) # Store original TE points from all refined_sections # Wing has n_panels+1 sections (5 sections for 4 panels) @@ -232,11 +232,11 @@ using Serialization original_te_points = [copy(wing.refined_sections[i].TE_point) for i in 1:n_sections] - # Apply group_deform! with non-zero angles (2 groups, each controlling 2 panels) + # Apply unrefined_deform! with non-zero angles (2 groups, each controlling 2 panels) theta_angles = [deg2rad(15.0), deg2rad(20.0)] delta_angles = [deg2rad(5.0), deg2rad(10.0)] - VortexStepMethod.group_deform!(wing, theta_angles, delta_angles; smooth=false) + VortexStepMethod.unrefined_deform!(wing, theta_angles, delta_angles; smooth=false) # Check that all sections' TE points have been deformed for i in 1:n_sections diff --git a/test/solver/test_group_coefficients.jl b/test/solver/test_group_coefficients.jl deleted file mode 100644 index 6bc6745e..00000000 --- a/test/solver/test_group_coefficients.jl +++ /dev/null @@ -1,148 +0,0 @@ -using VortexStepMethod -using LinearAlgebra -using Test - -@testset "Unrefined Coefficient Arrays Tests" begin - @testset "Unrefined coefficients aggregation" begin - # Create a simple wing with unrefined sections - n_panels = 20 - n_unrefined_sections = 5 # This gives 4 unrefined panels - - # Create a test wing settings file - settings_file = create_temp_wing_settings("solver", "solver_test_wing.yaml"; alpha=5.0, beta=0.0, wind_speed=10.0) - - try - # Modify settings to use specific panel configuration - settings = VSMSettings(settings_file) - settings.wings[1].n_panels = n_panels - settings.solver_settings.n_panels = n_panels - - # Create wing and solver - wing = Wing(settings) - body_aero = BodyAerodynamics([wing]) - solver = Solver(body_aero, settings) - - # Set conditions and solve - va = [10.0, 0.0, 0.0] - set_va!(body_aero, va) - sol = solve!(solver, body_aero) - - n_unrefined_panels = wing.n_unrefined_sections - 1 - - # Test 1: Unrefined arrays exist and have correct size - @test length(sol.cl_unrefined_array) == n_unrefined_panels - @test length(sol.cd_unrefined_array) == n_unrefined_panels - @test length(sol.cm_unrefined_array) == n_unrefined_panels - - # Test 2: Unrefined arrays are not all zeros (solver computed them) - @test !all(sol.cl_unrefined_array .== 0.0) - @test !all(sol.cd_unrefined_array .== 0.0) - - # Test 3: Verify unrefined coefficients are aggregated from refined panels - # using refined_panel_mapping - for unrefined_idx in 1:n_unrefined_panels - # Find all refined panels that map to this unrefined panel - refined_panel_indices = findall(x -> x == unrefined_idx, wing.refined_panel_mapping) - - if !isempty(refined_panel_indices) - # Calculate expected average from refined panel coefficients - expected_cl = sum(sol.cl_array[refined_panel_indices]) / length(refined_panel_indices) - expected_cd = sum(sol.cd_array[refined_panel_indices]) / length(refined_panel_indices) - expected_cm = sum(sol.cm_array[refined_panel_indices]) / length(refined_panel_indices) - - # Check if unrefined coefficients match expected averages - # Handle NaN values that can occur in INVISCID models - if isnan(expected_cl) - @test isnan(sol.cl_unrefined_array[unrefined_idx]) - else - @test isapprox(sol.cl_unrefined_array[unrefined_idx], expected_cl, rtol=1e-10) - end - if isnan(expected_cd) - @test isnan(sol.cd_unrefined_array[unrefined_idx]) - else - @test isapprox(sol.cd_unrefined_array[unrefined_idx], expected_cd, rtol=1e-10) - end - if isnan(expected_cm) - @test isnan(sol.cm_unrefined_array[unrefined_idx]) - else - @test isapprox(sol.cm_unrefined_array[unrefined_idx], expected_cm, rtol=1e-10) - end - end - end - - # Test 4: Verify physical consistency (lift coefficients should be positive at positive AoA) - # Skip test if values are NaN - if !any(isnan.(sol.cl_unrefined_array)) - @test all(sol.cl_unrefined_array .> 0.0) - end - - finally - rm(settings_file; force=true) - end - end - - @testset "Unrefined coefficients with different panel counts" begin - # Test with various panel/section combinations - test_cases = [ - (n_panels=40, n_unrefined_expected=21), # From YAML file sections - (n_panels=30, n_unrefined_expected=21), - (n_panels=24, n_unrefined_expected=21), - ] - - for (n_panels, n_unrefined_expected) in test_cases - settings_file = create_temp_wing_settings("solver", "solver_test_wing.yaml"; alpha=5.0, beta=0.0, wind_speed=10.0) - - try - settings = VSMSettings(settings_file) - settings.wings[1].n_panels = n_panels - settings.solver_settings.n_panels = n_panels - - wing = Wing(settings) - body_aero = BodyAerodynamics([wing]) - solver = Solver(body_aero, settings) - - va = [10.0, 0.0, 0.0] - set_va!(body_aero, va) - sol = solve!(solver, body_aero) - - n_unrefined_panels = wing.n_unrefined_sections - 1 - - # Verify arrays have correct size - @test length(sol.cl_unrefined_array) == n_unrefined_panels - @test length(sol.cd_unrefined_array) == n_unrefined_panels - @test length(sol.cm_unrefined_array) == n_unrefined_panels - - # Verify unrefined coefficients are computed correctly using mapping - for unrefined_idx in 1:n_unrefined_panels - refined_panel_indices = findall(x -> x == unrefined_idx, wing.refined_panel_mapping) - - if !isempty(refined_panel_indices) - expected_cl = sum(sol.cl_array[refined_panel_indices]) / length(refined_panel_indices) - expected_cd = sum(sol.cd_array[refined_panel_indices]) / length(refined_panel_indices) - expected_cm = sum(sol.cm_array[refined_panel_indices]) / length(refined_panel_indices) - - # Handle NaN for all coefficients - if isnan(expected_cl) - @test isnan(sol.cl_unrefined_array[unrefined_idx]) - else - @test isapprox(sol.cl_unrefined_array[unrefined_idx], expected_cl, rtol=1e-10) - end - if isnan(expected_cd) - @test isnan(sol.cd_unrefined_array[unrefined_idx]) - else - @test isapprox(sol.cd_unrefined_array[unrefined_idx], expected_cd, rtol=1e-10) - end - if isnan(expected_cm) - @test isnan(sol.cm_unrefined_array[unrefined_idx]) - else - @test isapprox(sol.cm_unrefined_array[unrefined_idx], expected_cm, rtol=1e-10) - end - end - end - - finally - rm(settings_file; force=true) - end - end - end -end diff --git a/test/test_data_utils.jl b/test/test_data_utils.jl index 23b54c59..13fc97ce 100644 --- a/test/test_data_utils.jl +++ b/test/test_data_utils.jl @@ -68,7 +68,6 @@ rm(settings_file) function create_temp_wing_settings(module_name, wing_file; name="test_wing", n_panels=4, - n_groups=2, spanwise_panel_distribution="COSINE", spanwise_direction=[0.0, 1.0, 0.0], remove_nan=true, @@ -87,7 +86,6 @@ function create_temp_wing_settings(module_name, wing_file; "name" => name, "geometry_file" => wing_file_path, "n_panels" => n_panels, - "n_groups" => n_groups, "spanwise_panel_distribution" => spanwise_panel_distribution, "spanwise_direction" => spanwise_direction, "remove_nan" => remove_nan, diff --git a/test/wing_geometry/test_wing_geometry.jl b/test/wing_geometry/test_wing_geometry.jl index 05458a64..5e458c2a 100644 --- a/test/wing_geometry/test_wing_geometry.jl +++ b/test/wing_geometry/test_wing_geometry.jl @@ -25,15 +25,15 @@ end @test example_wing.n_panels == 10 @test example_wing.spanwise_distribution == LINEAR @test example_wing.spanwise_direction ≈ [0.0, 1.0, 0.0] - @test length(example_wing.sections) == 0 + @test length(example_wing.unrefined_sections) == 0 end @testset "Add section" begin example_wing = Wing(10) add_section!(example_wing, [0.0, 0.0, 0.0], [-1.0, 0.0, 0.0], INVISCID) - @test length(example_wing.sections) == 1 + @test length(example_wing.unrefined_sections) == 1 - section = example_wing.sections[1] + section = example_wing.unrefined_sections[1] @test section.LE_point ≈ [0.0, 0.0, 0.0] @test section.TE_point ≈ [-1.0, 0.0, 0.0] @test section.aero_model === INVISCID @@ -57,7 +57,7 @@ end add_section!(wing, [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], POLAR_VECTORS, aero_data) # Check if NaNs were removed consistently - cleaned_data = wing.sections[1].aero_data + cleaned_data = wing.unrefined_sections[1].aero_data @test length(cleaned_data[1]) == 18 # 21 - 3 NaN positions @test !any(isnan, cleaned_data[2]) # cl @test !any(isnan, cleaned_data[3]) # cd @@ -84,7 +84,7 @@ end add_section!(wing, [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], POLAR_MATRICES, aero_data) # Check if NaNs were removed consistently - cleaned_data = wing.sections[1].aero_data + cleaned_data = wing.unrefined_sections[1].aero_data @test !any(isnan, cleaned_data[3]) # cl @test !any(isnan, cleaned_data[4]) # cd @test !any(isnan, cleaned_data[5]) # cm @@ -174,7 +174,7 @@ end add_section!(wing, [0.0, -span/2, 0.0], [-1.0, -span/2, 0.0], INVISCID) refine_aerodynamic_mesh!(wing) - sections = wing.sections + sections = wing.unrefined_sections @test length(sections) == wing.n_panels + 1 @test sections[1].LE_point ≈ [0.0, span/2, 0.0] @test sections[1].TE_point ≈ [-1.0, span/2, 0.0] @@ -351,7 +351,7 @@ end @test length(wing.refined_panel_mapping) == n_panels # Manually verify each refined panel is mapped to its closest unrefined panel - n_unrefined_panels = length(wing.sections) - 1 + n_unrefined_panels = length(wing.unrefined_sections) - 1 for refined_panel_idx in 1:n_panels # Calculate refined panel center le_mid = (wing.refined_sections[refined_panel_idx].LE_point + @@ -364,10 +364,10 @@ end min_dist = Inf closest_idx = 1 for unrefined_panel_idx in 1:n_unrefined_panels - le_mid_unref = (wing.sections[unrefined_panel_idx].LE_point + - wing.sections[unrefined_panel_idx+1].LE_point) / 2 - te_mid_unref = (wing.sections[unrefined_panel_idx].TE_point + - wing.sections[unrefined_panel_idx+1].TE_point) / 2 + le_mid_unref = (wing.unrefined_sections[unrefined_panel_idx].LE_point + + wing.unrefined_sections[unrefined_panel_idx+1].LE_point) / 2 + te_mid_unref = (wing.unrefined_sections[unrefined_panel_idx].TE_point + + wing.unrefined_sections[unrefined_panel_idx+1].TE_point) / 2 unrefined_center = (le_mid_unref + te_mid_unref) / 2 dist = norm(refined_center - unrefined_center) @@ -398,7 +398,7 @@ end @test length(wing.refined_panel_mapping) == n_panels # Verify each panel is mapped to its closest unrefined panel - n_unrefined_panels = length(wing.sections) - 1 + n_unrefined_panels = length(wing.unrefined_sections) - 1 for refined_panel_idx in 1:n_panels # Calculate refined panel center le_mid = (wing.refined_sections[refined_panel_idx].LE_point + @@ -411,10 +411,10 @@ end min_dist = Inf closest_idx = 1 for unrefined_panel_idx in 1:n_unrefined_panels - le_mid_unref = (wing.sections[unrefined_panel_idx].LE_point + - wing.sections[unrefined_panel_idx+1].LE_point) / 2 - te_mid_unref = (wing.sections[unrefined_panel_idx].TE_point + - wing.sections[unrefined_panel_idx+1].TE_point) / 2 + le_mid_unref = (wing.unrefined_sections[unrefined_panel_idx].LE_point + + wing.unrefined_sections[unrefined_panel_idx+1].LE_point) / 2 + te_mid_unref = (wing.unrefined_sections[unrefined_panel_idx].TE_point + + wing.unrefined_sections[unrefined_panel_idx+1].TE_point) / 2 unrefined_center = (le_mid_unref + te_mid_unref) / 2 dist = norm(refined_center - unrefined_center) @@ -444,7 +444,7 @@ end @test length(wing.refined_panel_mapping) == n_panels # Verify each panel is mapped to its closest unrefined panel - n_unrefined_panels = length(wing.sections) - 1 + n_unrefined_panels = length(wing.unrefined_sections) - 1 for refined_panel_idx in 1:n_panels # Calculate refined panel center le_mid = (wing.refined_sections[refined_panel_idx].LE_point + @@ -457,10 +457,10 @@ end min_dist = Inf closest_idx = 1 for unrefined_panel_idx in 1:n_unrefined_panels - le_mid_unref = (wing.sections[unrefined_panel_idx].LE_point + - wing.sections[unrefined_panel_idx+1].LE_point) / 2 - te_mid_unref = (wing.sections[unrefined_panel_idx].TE_point + - wing.sections[unrefined_panel_idx+1].TE_point) / 2 + le_mid_unref = (wing.unrefined_sections[unrefined_panel_idx].LE_point + + wing.unrefined_sections[unrefined_panel_idx+1].LE_point) / 2 + te_mid_unref = (wing.unrefined_sections[unrefined_panel_idx].TE_point + + wing.unrefined_sections[unrefined_panel_idx+1].TE_point) / 2 unrefined_center = (le_mid_unref + te_mid_unref) / 2 dist = norm(refined_center - unrefined_center) diff --git a/test/yaml_geometry/test_wing_constructor.jl b/test/yaml_geometry/test_wing_constructor.jl index 87a1ab8e..e0acf4a9 100644 --- a/test/yaml_geometry/test_wing_constructor.jl +++ b/test/yaml_geometry/test_wing_constructor.jl @@ -37,31 +37,31 @@ using Logging # Use the actual YAML file from the test data cp(test_data_path("yaml_geometry", "simple_wing.yaml"), test_yaml_path; force=true) - wing = Wing(test_yaml_path; n_panels=4, # n_groups=2) - + wing = Wing(test_yaml_path; n_panels=4) + @test wing isa Wing @test wing.n_panels == 4 - @test wing.n_unrefined_sections - 1 == 2 + @test wing.n_unrefined_sections == 2 @test wing.spanwise_distribution == LINEAR @test wing.spanwise_direction ≈ [0.0, 1.0, 0.0] - @test length(wing.sections) == 2 # simple_wing has 2 sections + @test length(wing.unrefined_sections) == 2 # simple_wing has 2 sections # Test section coordinates (sections are sorted by spanwise position) # simple_wing.yaml has: [1, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0] and [1, 0.0, -1.0, 0.0, 1.0, -1.0, 0.0] # Sorted by y-coordinate: y=1.0, y=-1.0 - @test wing.sections[1].LE_point ≈ [0.0, 1.0, 0.0] - @test wing.sections[1].TE_point ≈ [1.0, 1.0, 0.0] - @test wing.sections[2].LE_point ≈ [0.0, -1.0, 0.0] - @test wing.sections[2].TE_point ≈ [1.0, -1.0, 0.0] + @test wing.unrefined_sections[1].LE_point ≈ [0.0, 1.0, 0.0] + @test wing.unrefined_sections[1].TE_point ≈ [1.0, 1.0, 0.0] + @test wing.unrefined_sections[2].LE_point ≈ [0.0, -1.0, 0.0] + @test wing.unrefined_sections[2].TE_point ≈ [1.0, -1.0, 0.0] # Test that sections have polar data - @test wing.sections[1].aero_model == POLAR_VECTORS - @test wing.sections[2].aero_model == POLAR_VECTORS + @test wing.unrefined_sections[1].aero_model == POLAR_VECTORS + @test wing.unrefined_sections[2].aero_model == POLAR_VECTORS # Test polar data is loaded - @test wing.sections[1].aero_data isa Tuple - @test length(wing.sections[1].aero_data) == 4 - @test length(wing.sections[1].aero_data[1]) >= 3 # at least 3 alpha points + @test wing.unrefined_sections[1].aero_data isa Tuple + @test length(wing.unrefined_sections[1].aero_data) == 4 + @test length(wing.unrefined_sections[1].aero_data[1]) >= 3 # at least 3 alpha points end @testset "Wing Constructor Parameters" begin @@ -72,13 +72,12 @@ using Logging wing = Wing( test_yaml_path; n_panels=8, - # n_groups=4, spanwise_distribution=COSINE, remove_nan=false ) - + @test wing.n_panels == 8 - @test wing.n_unrefined_sections - 1 == 4 + @test wing.n_unrefined_sections == 2 @test wing.spanwise_distribution == COSINE @test !wing.remove_nan end @@ -104,8 +103,8 @@ wing_airfoils: wing = suppress_warnings(() -> Wing(test_yaml_path; n_panels=2)) # Should fall back to INVISCID model - @test wing.sections[1].aero_model == INVISCID - @test wing.sections[1].aero_data === nothing + @test wing.unrefined_sections[1].aero_model == INVISCID + @test wing.unrefined_sections[1].aero_data === nothing end @testset "Sections Without Polar Files" begin @@ -129,7 +128,7 @@ wing_airfoils: wing = suppress_warnings(() -> Wing(test_yaml_path; n_panels=2)) # Should fall back to INVISCID model - @test wing.sections[1].aero_model == INVISCID + @test wing.unrefined_sections[1].aero_model == INVISCID end @testset "Invalid Parameters" begin @@ -148,13 +147,9 @@ wing_airfoils: - [1, polars, {csv_file_path: "polars/1.csv"}] """ write(test_yaml_path, yaml_content) - - # Test invalid n_panels/n_groups combination - @test_throws ArgumentError Wing(test_yaml_path; n_panels=5, # n_groups=2) - # Test # n_groups=0 (no grouping functionality) - wing_no_groups = Wing(test_yaml_path; n_panels=4, # n_groups=0) - @test wing_no_groups.n_groups == 0 + # Test # n_groups=0 (no grouping functionality) - backward compatibility + wing_no_groups = Wing(test_yaml_path; n_panels=4) @test wing_no_groups.n_panels == 4 # Test invalid spanwise direction @@ -177,10 +172,10 @@ wing_airfoils: wing = Wing(subdir_yaml_path; n_panels=2) # Should successfully load polar data with relative path - @test wing.sections[1].aero_model == POLAR_VECTORS - @test wing.sections[1].aero_data isa Tuple - @test wing.sections[2].aero_model == POLAR_VECTORS - @test wing.sections[2].aero_data isa Tuple + @test wing.unrefined_sections[1].aero_model == POLAR_VECTORS + @test wing.unrefined_sections[1].aero_data isa Tuple + @test wing.unrefined_sections[2].aero_model == POLAR_VECTORS + @test wing.unrefined_sections[2].aero_data isa Tuple # Cleanup rm(subdir; recursive=true) @@ -189,21 +184,21 @@ wing_airfoils: @testset "Complex Wing Geometry" begin # Use the actual complex_wing.yaml file cp(test_data_path("yaml_geometry", "complex_wing.yaml"), test_yaml_path; force=true) - - wing = Wing(test_yaml_path; n_panels=12, # n_groups=3) - + + wing = Wing(test_yaml_path; n_panels=12) + @test wing.n_panels == 12 - @test wing.n_unrefined_sections - 1 == 3 - @test length(wing.sections) == 7 + @test wing.n_unrefined_sections == 7 + @test length(wing.unrefined_sections) == 7 # Test that different airfoil_ids get different polar data - @test wing.sections[1].aero_model == POLAR_VECTORS - @test wing.sections[2].aero_model == POLAR_VECTORS + @test wing.unrefined_sections[1].aero_model == POLAR_VECTORS + @test wing.unrefined_sections[2].aero_model == POLAR_VECTORS # Verify geometric progression along wingspan - @test wing.sections[1].LE_point[2] == 5.0 # First section at y=5 - @test wing.sections[4].LE_point[2] == 0.0 # Middle section at y=0 - @test wing.sections[7].LE_point[2] == -5.0 # Last section at y=-5 + @test wing.unrefined_sections[1].LE_point[2] == 5.0 # First section at y=5 + @test wing.unrefined_sections[4].LE_point[2] == 0.0 # Middle section at y=0 + @test wing.unrefined_sections[7].LE_point[2] == -5.0 # Last section at y=-5 end @testset "VSMSettings Constructor" begin @@ -216,20 +211,19 @@ wing_airfoils: settings.wings = [WingSettings( geometry_file=simple_wing_file, n_panels=6, - # n_groups=3, spanwise_panel_distribution=COSINE )] - + # Test Wing constructor with VSMSettings wing = Wing(settings) - + @test wing isa Wing @test wing.n_panels == 6 - @test wing.n_unrefined_sections - 1 == 3 + @test wing.n_unrefined_sections == 2 @test wing.spanwise_distribution == COSINE - @test length(wing.sections) == 2 - @test wing.sections[1].aero_model == POLAR_VECTORS - @test wing.sections[2].aero_model == POLAR_VECTORS + @test length(wing.unrefined_sections) == 2 + @test wing.unrefined_sections[1].aero_model == POLAR_VECTORS + @test wing.unrefined_sections[2].aero_model == POLAR_VECTORS end @testset "Shared Test Data Usage" begin @@ -238,33 +232,33 @@ wing_airfoils: @test isfile(simple_wing_file) # Test basic Wing construction with shared data - wing = Wing(simple_wing_file; n_panels=4, # n_groups=2) + wing = Wing(simple_wing_file; n_panels=4) @test wing isa Wing @test wing.n_panels == 4 - @test wing.n_unrefined_sections - 1 == 2 - @test length(wing.sections) == 2 + @test wing.n_unrefined_sections == 2 + @test length(wing.unrefined_sections) == 2 # Test complex wing construction complex_wing_file = test_data_path("yaml_geometry", "complex_wing.yaml") @test isfile(complex_wing_file) - complex_wing = Wing(complex_wing_file; n_panels=12, # n_groups=3) + complex_wing = Wing(complex_wing_file; n_panels=12) @test complex_wing isa Wing @test complex_wing.n_panels == 12 - @test complex_wing.n_groups == 3 - @test length(complex_wing.sections) == 7 + @test complex_wing.n_unrefined_sections == 7 + @test length(complex_wing.unrefined_sections) == 7 # Verify polar data is loaded from shared files - @test complex_wing.sections[1].aero_model == POLAR_VECTORS - @test complex_wing.sections[1].aero_data isa Tuple + @test complex_wing.unrefined_sections[1].aero_model == POLAR_VECTORS + @test complex_wing.unrefined_sections[1].aero_data isa Tuple # Test with module-specific convenience function - create a standard wing for this test standard_wing_file = simple_wing_file # Use simple_wing as our "standard" @test isfile(standard_wing_file) - standard_wing = Wing(standard_wing_file; n_panels=2, # n_groups=1) + standard_wing = Wing(standard_wing_file; n_panels=2) @test standard_wing isa Wing - @test length(standard_wing.sections) == 2 + @test length(standard_wing.unrefined_sections) == 2 end # Cleanup after all tests diff --git a/test/yaml_geometry/test_yaml_wing_deformation.jl b/test/yaml_geometry/test_yaml_wing_deformation.jl index ac51e243..4b434de7 100644 --- a/test/yaml_geometry/test_yaml_wing_deformation.jl +++ b/test/yaml_geometry/test_yaml_wing_deformation.jl @@ -6,7 +6,7 @@ using Test @testset "Simple Wing Deformation" begin # Load existing simple_wing.yaml simple_wing_file = test_data_path("yaml_geometry", "simple_wing.yaml") - wing = Wing(simple_wing_file; n_panels=4, # n_groups=2) + wing = Wing(simple_wing_file; n_panels=4) body_aero = BodyAerodynamics([wing]) # Store original TE point for comparison @@ -15,8 +15,8 @@ using Test original_le_point = copy(body_aero.panels[i].LE_point_1) # Apply deformation with non-zero angles - theta_dist = fill(deg2rad(30.0), wing.n_panels) # 30 degrees twist - delta_dist = fill(deg2rad(5.0), wing.n_panels) # 5 degrees trailing edge deflection + theta_dist = fill(deg2rad(30.0), wing.n_unrefined_sections) # 30 degrees twist + delta_dist = fill(deg2rad(5.0), wing.n_unrefined_sections) # 5 degrees trailing edge deflection VortexStepMethod.deform!(wing, theta_dist, delta_dist) VortexStepMethod.reinit!(body_aero; refine_mesh=false) @@ -36,8 +36,8 @@ using Test @test body_aero.panels[i].delta ≈ deg2rad(5.0) # Reset deformation with zero angles - zero_theta_dist = zeros(wing.n_panels) - zero_delta_dist = zeros(wing.n_panels) + zero_theta_dist = zeros(wing.n_unrefined_sections) + zero_delta_dist = zeros(wing.n_unrefined_sections) VortexStepMethod.deform!(wing, zero_theta_dist, zero_delta_dist) VortexStepMethod.reinit!(body_aero; refine_mesh=false) @@ -53,7 +53,7 @@ using Test @testset "Complex Wing Deformation" begin # Load existing complex_wing.yaml with multiple sections complex_wing_file = test_data_path("yaml_geometry", "complex_wing.yaml") - wing = Wing(complex_wing_file; n_panels=12, # n_groups=3) + wing = Wing(complex_wing_file; n_panels=12) body_aero = BodyAerodynamics([wing]) # Store original points for multiple panels @@ -67,8 +67,9 @@ using Test end # Apply spanwise-varying deformation - theta_dist = [deg2rad(10.0 * i / wing.n_panels) for i in 1:wing.n_panels] # Linear twist distribution - delta_dist = [deg2rad(-5.0 + 10.0 * i / wing.n_panels) for i in 1:wing.n_panels] # Varying deflection + n = wing.n_unrefined_sections + theta_dist = [deg2rad(10.0 * i / n) for i in 1:n] # Linear twist distribution + delta_dist = [deg2rad(-5.0 + 10.0 * i / n) for i in 1:n] # Varying deflection VortexStepMethod.deform!(wing, theta_dist, delta_dist) VortexStepMethod.reinit!(body_aero; refine_mesh=false) @@ -89,7 +90,7 @@ using Test @test body_aero.panels[1].delta < body_aero.panels[end].delta # Reset and verify - VortexStepMethod.deform!(wing, zeros(wing.n_panels), zeros(wing.n_panels)) + VortexStepMethod.deform!(wing, zeros(wing.n_unrefined_sections), zeros(wing.n_unrefined_sections)) VortexStepMethod.reinit!(body_aero; refine_mesh=false) for (idx, i) in enumerate(test_indices) @@ -104,11 +105,11 @@ using Test @testset "Multiple Reinit Calls with NTuple aero_data" begin # This test specifically checks the NTuple handling fix simple_wing_file = test_data_path("yaml_geometry", "simple_wing.yaml") - wing = Wing(simple_wing_file; n_panels=4, # n_groups=2) + wing = Wing(simple_wing_file; n_panels=4) # Verify that sections have NTuple aero_data (for wings with simple polars) # or other valid AeroData types - @test wing.sections[1].aero_data !== nothing + @test wing.unrefined_sections[1].aero_data !== nothing # Perform multiple reinit! calls to ensure NTuple handling works for _ in 1:5 @@ -116,7 +117,7 @@ using Test end # Wing should still be valid after multiple reinits - @test wing.sections[1].aero_data !== nothing + @test wing.unrefined_sections[1].aero_data !== nothing # Verify refined_sections and non_deformed_sections are properly populated @test length(wing.refined_sections) == wing.n_panels + 1 @test length(wing.non_deformed_sections) == wing.n_panels + 1 @@ -125,12 +126,12 @@ using Test @testset "Deformation with BodyAerodynamics Reinit" begin # Test that reinit! on BodyAerodynamics properly handles deformed wings simple_wing_file = test_data_path("yaml_geometry", "simple_wing.yaml") - wing = Wing(simple_wing_file; n_panels=4, # n_groups=2) + wing = Wing(simple_wing_file; n_panels=4) body_aero = BodyAerodynamics([wing]) # Apply deformation - theta_dist = fill(deg2rad(15.0), wing.n_panels) - delta_dist = fill(deg2rad(3.0), wing.n_panels) + theta_dist = fill(deg2rad(15.0), wing.n_unrefined_sections) + delta_dist = fill(deg2rad(3.0), wing.n_unrefined_sections) VortexStepMethod.deform!(wing, theta_dist, delta_dist) VortexStepMethod.reinit!(body_aero; refine_mesh=false) @@ -153,17 +154,17 @@ using Test @testset "Edge Cases" begin simple_wing_file = test_data_path("yaml_geometry", "simple_wing.yaml") - wing = Wing(simple_wing_file; n_panels=2, # n_groups=1) + wing = Wing(simple_wing_file; n_panels=2) body_aero = BodyAerodynamics([wing]) # Test zero deformation - VortexStepMethod.deform!(wing, zeros(wing.n_panels), zeros(wing.n_panels)) + VortexStepMethod.deform!(wing, zeros(wing.n_unrefined_sections), zeros(wing.n_unrefined_sections)) VortexStepMethod.reinit!(body_aero; refine_mesh=false) @test all(p.delta ≈ 0.0 for p in body_aero.panels) # Test large deformation angles - theta_dist = fill(deg2rad(60.0), wing.n_panels) - delta_dist = fill(deg2rad(30.0), wing.n_panels) + theta_dist = fill(deg2rad(60.0), wing.n_unrefined_sections) + delta_dist = fill(deg2rad(30.0), wing.n_unrefined_sections) # Should not error even with large angles VortexStepMethod.deform!(wing, theta_dist, delta_dist) @@ -171,8 +172,8 @@ using Test @test all(p.delta ≈ deg2rad(30.0) for p in body_aero.panels) # Test negative angles - theta_dist = fill(deg2rad(-20.0), wing.n_panels) - delta_dist = fill(deg2rad(-10.0), wing.n_panels) + theta_dist = fill(deg2rad(-20.0), wing.n_unrefined_sections) + delta_dist = fill(deg2rad(-10.0), wing.n_unrefined_sections) VortexStepMethod.deform!(wing, theta_dist, delta_dist) VortexStepMethod.reinit!(body_aero; refine_mesh=false) @test all(p.delta ≈ deg2rad(-10.0) for p in body_aero.panels) From 860f38d0550d140584d70b1b852eec2e6538d3db Mon Sep 17 00:00:00 2001 From: 1-Bart-1 Date: Sat, 6 Dec 2025 13:15:35 +0100 Subject: [PATCH 34/53] Dist for per panel, unrefined_dist for per unrefined section --- src/body_aerodynamics.jl | 32 ++-- src/solver.jl | 320 +++++++++++++++++++-------------------- 2 files changed, 176 insertions(+), 176 deletions(-) diff --git a/src/body_aerodynamics.jl b/src/body_aerodynamics.jl index 6b3ed591..539c95f9 100644 --- a/src/body_aerodynamics.jl +++ b/src/body_aerodynamics.jl @@ -13,8 +13,8 @@ Main structure for calculating aerodynamic properties of bodies. Use the constru - `alpha_uncorrected`=zeros(Float64, P): angles of attack per panel - `alpha_corrected`=zeros(Float64, P): corrected angles of attack per panel - `stall_angle_list`=zeros(Float64, P): stall angle per panel -- `alpha_array::MVector{P, Float64}` = zeros(Float64, P) -- `v_a_array::MVector{P, Float64}` = zeros(Float64, P) +- `alpha_dist::MVector{P, Float64}` = zeros(Float64, P) +- `v_a_dist::MVector{P, Float64}` = zeros(Float64, P) - `work_vectors`::NTuple{10, MVec3} = ntuple(_ -> zeros(MVec3), 10) - `AIC::Array{Float64, 3}` = zeros(3, P, P) - `projected_area::Float64` = 1.0: The area projected onto the xy-plane of the kite body reference frame [m²] @@ -30,8 +30,8 @@ Main structure for calculating aerodynamic properties of bodies. Use the constru alpha_uncorrected::MVector{P, Float64} = zeros(P) alpha_corrected::MVector{P, Float64} = zeros(P) stall_angle_list::MVector{P, Float64} = zeros(P) - alpha_array::MVector{P, Float64} = zeros(P) - v_a_array::MVector{P, Float64} = zeros(P) + alpha_dist::MVector{P, Float64} = zeros(P) + v_a_dist::MVector{P, Float64} = zeros(P) work_vectors::NTuple{10,MVec3} = ntuple(_ -> zeros(MVec3), 10) AIC::Array{Float64, 3} = zeros(3, P, P) projected_area::Float64 = one(Float64) @@ -186,8 +186,8 @@ function reinit!(body_aero::BodyAerodynamics; # Initialize rest of the struct body_aero.projected_area = sum(calculate_projected_area, body_aero.wings) calculate_stall_angle_list!(body_aero.stall_angle_list, body_aero.panels) - body_aero.alpha_array .= 0.0 - body_aero.v_a_array .= 0.0 + body_aero.alpha_dist .= 0.0 + body_aero.v_a_dist .= 0.0 body_aero.AIC .= 0.0 set_va!(body_aero, va, omega) return nothing @@ -418,7 +418,7 @@ end calculate_results(body_aero::BodyAerodynamics, gamma_new, density, aerodynamic_model_type::Model, core_radius_fraction, mu, - alpha_array, v_a_array, + alpha_dist, v_a_dist, chord_array, x_airf_array, y_airf_array, z_airf_array, va_array, va_norm_array, @@ -438,8 +438,8 @@ function calculate_results( aerodynamic_model_type::Model, core_radius_fraction, mu, - alpha_array, - v_a_array, + alpha_dist, + v_a_dist, chord_array, x_airf_array, y_airf_array, @@ -462,15 +462,15 @@ function calculate_results( # Calculate coefficients for each panel for (i, panel) in enumerate(panels) - cl_array[i] = calculate_cl(panel, alpha_array[i]) - cd_array[i], cm_array[i] = calculate_cd_cm(panel, alpha_array[i]) + cl_array[i] = calculate_cl(panel, alpha_dist[i]) + cd_array[i], cm_array[i] = calculate_cd_cm(panel, alpha_dist[i]) panel_width_array[i] = panel.width end # Calculate forces - lift = reshape((cl_array .* 0.5 .* density .* v_a_array.^2 .* chord_array), :, 1) - drag = reshape((cd_array .* 0.5 .* density .* v_a_array.^2 .* chord_array), :, 1) - moment = reshape((cm_array .* 0.5 .* density .* v_a_array.^2 .* chord_array), :, 1) + lift = reshape((cl_array .* 0.5 .* density .* v_a_dist.^2 .* chord_array), :, 1) + drag = reshape((cd_array .* 0.5 .* density .* v_a_dist.^2 .* chord_array), :, 1) + moment = reshape((cm_array .* 0.5 .* density .* v_a_dist.^2 .* chord_array), :, 1) # Calculate alpha corrections based on model type if correct_aoa @@ -486,7 +486,7 @@ function calculate_results( va_unit_array ) else - alpha_corrected .= alpha_array + alpha_corrected .= alpha_dist end # Verify va is not distributed @@ -632,7 +632,7 @@ function calculate_results( "cfy" => (sum(f_body_3D[2,:]) / (q_inf * projected_area)), "cfz" => (sum(f_body_3D[3,:]) / (q_inf * projected_area)), "alpha_at_ac" => alpha_corrected, - "alpha_uncorrected" => alpha_array, + "alpha_uncorrected" => alpha_dist, "alpha_geometric" => alpha_geometric, "gamma_distribution" => gamma_new, "area_all_panels" => area_all_panels, diff --git a/src/solver.jl b/src/solver.jl index 111e2cea..3ec3c376 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -4,15 +4,19 @@ Struct for storing the solution of the [solve!](@ref) function. Must contain all info needed by `KiteModels.jl`. +# Naming Convention +- Variables ending in `_dist`: Per-panel distributions (length P, one value per panel) +- Variables ending in `_unrefined_dist`: Per-unrefined-section distributions (length U, averaged values per unrefined section) + # Attributes -- `panel_width_array`::Vector{Float64}: Width of the panels [m] -- `alpha_array`::Vector{Float64}: Angle of attack of each panel relative to the apparent wind [rad] -- cl_array::Vector{Float64}: Lift coefficients of the panels [-] -- cd_array::Vector{Float64}: Drag coefficients of the panels [-] -- cm_array::Vector{Float64}: Pitching moment coefficients of the panels [-] -- panel_lift::Vector{Float64}: Lift force of the panels [N] -- panel_drag::Vector{Float64}: Drag force of the panels [N] -- panel_moment::Vector{Float64}: Pitching moment around the spanwise vector of the panels [Nm] +- `width_dist`::Vector{Float64}: Width of the panels [m] +- `alpha_dist`::Vector{Float64}: Angle of attack of each panel relative to the apparent wind [rad] +- cl_dist::Vector{Float64}: Lift coefficients of the panels [-] +- cd_dist::Vector{Float64}: Drag coefficients of the panels [-] +- cm_dist::Vector{Float64}: Pitching moment coefficients of the panels [-] +- lift_dist::Vector{Float64}: Lift force of the panels [N] +- drag_dist::Vector{Float64}: Drag force of the panels [N] +- panel_moment_dist::Vector{Float64}: Pitching moment around the spanwise vector of the panels [Nm] - `f_body_3D`::Matrix{Float64}: Matrix of the aerodynamic forces (x, y, z vectors) [N] - `m_body_3D`::Matrix{Float64}: Matrix of the aerodynamic moments [Nm] - `gamma_distribution`::Union{Nothing, Vector{Float64}}: Vector containing the panel circulations. @@ -22,30 +26,29 @@ Struct for storing the solution of the [solve!](@ref) function. Must contain all - `moment_coeffs`::MVec3: Aerodynamic moment coefficients [CMx, CMy, CMz] [-] - `moment_dist`::Vector{Float64}: Pitching moments around the spanwise vector of each panel. [Nm] - `moment_coeff_dist`::Vector{Float64}: Pitching moment coefficient around the spanwise vector of each panel. [-] -- `unrefined_moment_dist`::MVector{U, Float64}: Aggregated moments for unrefined sections [Nm] -- `unrefined_moment_coeff_dist`::MVector{U, Float64}: Aggregated moment coefficients for unrefined sections [-] -- `cl_unrefined_array`::MVector{U, Float64}: Averaged lift coefficients for unrefined sections [-] -- `cd_unrefined_array`::MVector{U, Float64}: Averaged drag coefficients for unrefined sections [-] -- `cm_unrefined_array`::MVector{U, Float64}: Averaged moment coefficients for unrefined sections [-] -- `alpha_unrefined_array`::MVector{U, Float64}: Averaged angles of attack for unrefined sections [rad] +- `moment_unrefined_dist`::MVector{U, Float64}: Averaged moments for unrefined sections [Nm] +- `cl_unrefined_dist`::MVector{U, Float64}: Averaged lift coefficients for unrefined sections [-] +- `cd_unrefined_dist`::MVector{U, Float64}: Averaged drag coefficients for unrefined sections [-] +- `cm_unrefined_dist`::MVector{U, Float64}: Averaged moment coefficients for unrefined sections [-] +- `alpha_unrefined_dist`::MVector{U, Float64}: Averaged angles of attack for unrefined sections [rad] - `solver_status`::SolverStatus: enum, see [SolverStatus](@ref) """ @with_kw mutable struct VSMSolution{P,U} ### private vectors of solve_base! - _x_airf_array::Matrix{Float64} = zeros(P, 3) - _y_airf_array::Matrix{Float64} = zeros(P, 3) - _z_airf_array::Matrix{Float64} = zeros(P, 3) - _va_array::Matrix{Float64} = zeros(P, 3) - _chord_array::Vector{Float64} = zeros(P) + _x_airf_dist::Matrix{Float64} = zeros(P, 3) + _y_airf_dist::Matrix{Float64} = zeros(P, 3) + _z_airf_dist::Matrix{Float64} = zeros(P, 3) + _va_dist::Matrix{Float64} = zeros(P, 3) + _chord_dist::Vector{Float64} = zeros(P) ### end of private vectors - panel_width_array::Vector{Float64} = zeros(P) - alpha_array::Vector{Float64} = zeros(P) - cl_array::Vector{Float64} = zeros(P) - cd_array::Vector{Float64} = zeros(P) - cm_array::Vector{Float64} = zeros(P) - panel_lift::Vector{Float64} = zeros(P) - panel_drag::Vector{Float64} = zeros(P) - panel_moment::Vector{Float64} = zeros(P) + width_dist::Vector{Float64} = zeros(P) + alpha_dist::Vector{Float64} = zeros(P) + cl_dist::Vector{Float64} = zeros(P) + cd_dist::Vector{Float64} = zeros(P) + cm_dist::Vector{Float64} = zeros(P) + lift_dist::Vector{Float64} = zeros(P) + drag_dist::Vector{Float64} = zeros(P) + panel_moment_dist::Vector{Float64} = zeros(P) f_body_3D::Matrix{Float64} = zeros(3, P) m_body_3D::Matrix{Float64} = zeros(3, P) gamma_distribution::Union{Nothing, Vector{Float64}} = nothing @@ -55,18 +58,17 @@ Struct for storing the solution of the [solve!](@ref) function. Must contain all moment_coeffs::MVec3 = zeros(MVec3) moment_dist::MVector{P, Float64} = zeros(P) moment_coeff_dist::MVector{P, Float64} = zeros(P) - unrefined_moment_dist::MVector{U, Float64} = zeros(U) - unrefined_moment_coeff_dist::MVector{U, Float64} = zeros(U) - cl_unrefined_array::MVector{U, Float64} = zeros(U) - cd_unrefined_array::MVector{U, Float64} = zeros(U) - cm_unrefined_array::MVector{U, Float64} = zeros(U) - alpha_unrefined_array::MVector{U, Float64} = zeros(U) - x_airf_unrefined_array::Vector{MVec3} = [MVec3(0,0,0) for _ in 1:U] - y_airf_unrefined_array::Vector{MVec3} = [MVec3(0,0,0) for _ in 1:U] - z_airf_unrefined_array::Vector{MVec3} = [MVec3(0,0,0) for _ in 1:U] - va_unrefined_array::Vector{MVec3} = [MVec3(0,0,0) for _ in 1:U] - chord_unrefined_array::MVector{U, Float64} = zeros(U) - width_unrefined_array::MVector{U, Float64} = zeros(U) + moment_unrefined_dist::MVector{U, Float64} = zeros(U) + cl_unrefined_dist::MVector{U, Float64} = zeros(U) + cd_unrefined_dist::MVector{U, Float64} = zeros(U) + cm_unrefined_dist::MVector{U, Float64} = zeros(U) + alpha_unrefined_dist::MVector{U, Float64} = zeros(U) + x_airf_unrefined_dist::Vector{MVec3} = [MVec3(0,0,0) for _ in 1:U] + y_airf_unrefined_dist::Vector{MVec3} = [MVec3(0,0,0) for _ in 1:U] + z_airf_unrefined_dist::Vector{MVec3} = [MVec3(0,0,0) for _ in 1:U] + va_unrefined_dist::Vector{MVec3} = [MVec3(0,0,0) for _ in 1:U] + chord_unrefined_dist::MVector{U, Float64} = zeros(U) + width_unrefined_dist::MVector{U, Float64} = zeros(U) solver_status::SolverStatus = FAILURE end @@ -74,13 +76,13 @@ end @with_kw mutable struct LoopResult{P} converged::Bool = false gamma_new::MVector{P, Float64} = zeros(P) - alpha_array::MVector{P, Float64} = zeros(P) # TODO: Is this different from BodyAerodynamics.alpha_array ? - v_a_array::MVector{P, Float64} = zeros(P) + alpha_dist::MVector{P, Float64} = zeros(P) # TODO: Is this different from BodyAerodynamics.alpha_dist ? + v_a_dist::MVector{P, Float64} = zeros(P) end @with_kw struct BaseResult{P} - va_norm_array::MVector{P, Float64} = zeros(P) - va_unit_array::Matrix{Float64} = zeros(P, 3) + va_norm_dist::MVector{P, Float64} = zeros(P) + va_unit_dist::Matrix{Float64} = zeros(P, 3) end """ @@ -200,16 +202,16 @@ function solve!(solver::Solver, body_aero::BodyAerodynamics, gamma_distribution= end # Initialize arrays - cl_array = solver.sol.cl_array - cd_array = solver.sol.cd_array - cm_array = solver.sol.cm_array + cl_dist = solver.sol.cl_dist + cd_dist = solver.sol.cd_dist + cm_dist = solver.sol.cm_dist converged = solver.lr.converged - alpha_array = solver.lr.alpha_array - alpha_corrected = solver.sol.alpha_array - v_a_array = solver.lr.v_a_array + alpha_dist = solver.lr.alpha_dist + alpha_corrected = solver.sol.alpha_dist + v_a_dist = solver.lr.v_a_dist panels = body_aero.panels - panel_width_array = solver.sol.panel_width_array + width_dist = solver.sol.width_dist solver.sol.moment_dist .= 0 solver.sol.moment_coeff_dist .= 0 moment_dist = solver.sol.moment_dist @@ -219,20 +221,20 @@ function solve!(solver::Solver, body_aero::BodyAerodynamics, gamma_distribution= # Calculate coefficients for each panel for (i, panel) in enumerate(panels) # zero bytes - cl_array[i] = calculate_cl(panel, alpha_array[i]) - cd_array[i], cm_array[i] = calculate_cd_cm(panel, alpha_array[i]) - panel_width_array[i] = panel.width + cl_dist[i] = calculate_cl(panel, alpha_dist[i]) + cd_dist[i], cm_dist[i] = calculate_cd_cm(panel, alpha_dist[i]) + width_dist[i] = panel.width end # create an alias for the three vertical output vectors - lift = solver.sol.panel_lift - drag = solver.sol.panel_drag - panel_moment = solver.sol.panel_moment + lift = solver.sol.lift_dist + drag = solver.sol.drag_dist + panel_moment_dist = solver.sol.panel_moment_dist # Compute using fused broadcasting (no intermediate allocations) - @. lift = cl_array * 0.5 * density * v_a_array^2 * solver.sol._chord_array - @. drag = cd_array * 0.5 * density * v_a_array^2 * solver.sol._chord_array - @. panel_moment = cm_array * 0.5 * density * v_a_array^2 * solver.sol._chord_array + @. lift = cl_dist * 0.5 * density * v_a_dist^2 * solver.sol._chord_dist + @. drag = cd_dist * 0.5 * density * v_a_dist^2 * solver.sol._chord_dist + @. panel_moment_dist = cm_dist * 0.5 * density * v_a_dist^2 * solver.sol._chord_dist # Calculate alpha corrections based on model type if aerodynamic_model_type == VSM # 64 bytes @@ -241,14 +243,14 @@ function solve!(solver::Solver, body_aero::BodyAerodynamics, gamma_distribution= body_aero, gamma_new, solver.core_radius_fraction, - solver.sol._z_airf_array, - solver.sol._x_airf_array, - solver.sol._va_array, - solver.br.va_norm_array, - solver.br.va_unit_array + solver.sol._z_airf_dist, + solver.sol._x_airf_dist, + solver.sol._va_dist, + solver.br.va_norm_dist, + solver.br.va_unit_dist ) elseif aerodynamic_model_type == LLT - alpha_corrected .= alpha_array + alpha_corrected .= alpha_dist end # Initialize result arrays @@ -296,7 +298,7 @@ function solve!(solver::Solver, body_aero::BodyAerodynamics, gamma_distribution= # Use the axis around which the moment is defined, # which is the y-axis pointing "spanwise" moment_axis_body = panel.y_airf - M_local_3D = panel_moment[i] * moment_axis_body * panel.width + M_local_3D = panel_moment_dist[i] * moment_axis_body * panel.width # Vector from panel AC to the chosen reference point: r_vector = panel_ac_body - reference_point # e.g. CG, wing root, etc. # Cross product to shift the force from panel AC to ref. point: @@ -306,40 +308,38 @@ function solve!(solver::Solver, body_aero::BodyAerodynamics, gamma_distribution= # Calculate the moment distribution (moment on each panel) arm = (moment_frac - 0.25) * panel.chord - moment_dist[i] = ((ftotal_induced_va ⋅ panel.z_airf) * arm + panel_moment[i]) * panel.width + moment_dist[i] = ((ftotal_induced_va ⋅ panel.z_airf) * arm + panel_moment_dist[i]) * panel.width moment_coeff_dist[i] = moment_dist[i] / (q_inf * projected_area) end # Only compute unrefined arrays if there are unrefined sections - if length(solver.sol.unrefined_moment_dist) > 0 - unrefined_moment_dist = solver.sol.unrefined_moment_dist - unrefined_moment_coeff_dist = solver.sol.unrefined_moment_coeff_dist - cl_unrefined_array = solver.sol.cl_unrefined_array - cd_unrefined_array = solver.sol.cd_unrefined_array - cm_unrefined_array = solver.sol.cm_unrefined_array - alpha_unrefined_array = solver.sol.alpha_unrefined_array - x_airf_unrefined_array = solver.sol.x_airf_unrefined_array - y_airf_unrefined_array = solver.sol.y_airf_unrefined_array - z_airf_unrefined_array = solver.sol.z_airf_unrefined_array - va_unrefined_array = solver.sol.va_unrefined_array - chord_unrefined_array = solver.sol.chord_unrefined_array - width_unrefined_array = solver.sol.width_unrefined_array + if length(solver.sol.moment_unrefined_dist) > 0 + moment_unrefined_dist = solver.sol.moment_unrefined_dist + cl_unrefined_dist = solver.sol.cl_unrefined_dist + cd_unrefined_dist = solver.sol.cd_unrefined_dist + cm_unrefined_dist = solver.sol.cm_unrefined_dist + alpha_unrefined_dist = solver.sol.alpha_unrefined_dist + x_airf_unrefined_dist = solver.sol.x_airf_unrefined_dist + y_airf_unrefined_dist = solver.sol.y_airf_unrefined_dist + z_airf_unrefined_dist = solver.sol.z_airf_unrefined_dist + va_unrefined_dist = solver.sol.va_unrefined_dist + chord_unrefined_dist = solver.sol.chord_unrefined_dist + width_unrefined_dist = solver.sol.width_unrefined_dist # Zero all unrefined arrays - unrefined_moment_dist .= 0.0 - unrefined_moment_coeff_dist .= 0.0 - cl_unrefined_array .= 0.0 - cd_unrefined_array .= 0.0 - cm_unrefined_array .= 0.0 - alpha_unrefined_array .= 0.0 - for i in eachindex(x_airf_unrefined_array) - x_airf_unrefined_array[i] .= 0.0 - y_airf_unrefined_array[i] .= 0.0 - z_airf_unrefined_array[i] .= 0.0 - va_unrefined_array[i] .= 0.0 + moment_unrefined_dist .= 0.0 + cl_unrefined_dist .= 0.0 + cd_unrefined_dist .= 0.0 + cm_unrefined_dist .= 0.0 + alpha_unrefined_dist .= 0.0 + for i in eachindex(x_airf_unrefined_dist) + x_airf_unrefined_dist[i] .= 0.0 + y_airf_unrefined_dist[i] .= 0.0 + z_airf_unrefined_dist[i] .= 0.0 + va_unrefined_dist[i] .= 0.0 end - chord_unrefined_array .= 0.0 - width_unrefined_array .= 0.0 + chord_unrefined_dist .= 0.0 + width_unrefined_dist .= 0.0 panel_idx = 1 unrefined_idx = 1 @@ -353,20 +353,19 @@ function solve!(solver::Solver, body_aero::BodyAerodynamics, gamma_distribution= target_unrefined_idx = unrefined_idx + original_section_idx - 1 # Accumulate coefficients and moments - unrefined_moment_dist[target_unrefined_idx] += moment_dist[panel_idx] - unrefined_moment_coeff_dist[target_unrefined_idx] += moment_coeff_dist[panel_idx] - cl_unrefined_array[target_unrefined_idx] += solver.sol.cl_array[panel_idx] - cd_unrefined_array[target_unrefined_idx] += solver.sol.cd_array[panel_idx] - cm_unrefined_array[target_unrefined_idx] += solver.sol.cm_array[panel_idx] - alpha_unrefined_array[target_unrefined_idx] += solver.sol.alpha_array[panel_idx] + moment_unrefined_dist[target_unrefined_idx] += moment_dist[panel_idx] + cl_unrefined_dist[target_unrefined_idx] += solver.sol.cl_dist[panel_idx] + cd_unrefined_dist[target_unrefined_idx] += solver.sol.cd_dist[panel_idx] + cm_unrefined_dist[target_unrefined_idx] += solver.sol.cm_dist[panel_idx] + alpha_unrefined_dist[target_unrefined_idx] += solver.sol.alpha_dist[panel_idx] # Accumulate geometry - x_airf_unrefined_array[target_unrefined_idx] .+= panel.x_airf - y_airf_unrefined_array[target_unrefined_idx] .+= panel.y_airf - z_airf_unrefined_array[target_unrefined_idx] .+= panel.z_airf - va_unrefined_array[target_unrefined_idx] .+= panel.va - chord_unrefined_array[target_unrefined_idx] += panel.chord - width_unrefined_array[target_unrefined_idx] += panel.width + x_airf_unrefined_dist[target_unrefined_idx] .+= panel.x_airf + y_airf_unrefined_dist[target_unrefined_idx] .+= panel.y_airf + z_airf_unrefined_dist[target_unrefined_idx] .+= panel.z_airf + va_unrefined_dist[target_unrefined_idx] .+= panel.va + chord_unrefined_dist[target_unrefined_idx] += panel.chord + width_unrefined_dist[target_unrefined_idx] += panel.width unrefined_section_counts[original_section_idx] += 1 panel_idx += 1 @@ -377,16 +376,17 @@ function solve!(solver::Solver, body_aero::BodyAerodynamics, gamma_distribution= target_unrefined_idx = unrefined_idx + i - 1 if unrefined_section_counts[i] > 0 count = unrefined_section_counts[i] - cl_unrefined_array[target_unrefined_idx] /= count - cd_unrefined_array[target_unrefined_idx] /= count - cm_unrefined_array[target_unrefined_idx] /= count - alpha_unrefined_array[target_unrefined_idx] /= count - x_airf_unrefined_array[target_unrefined_idx] ./= count - y_airf_unrefined_array[target_unrefined_idx] ./= count - z_airf_unrefined_array[target_unrefined_idx] ./= count - va_unrefined_array[target_unrefined_idx] ./= count - chord_unrefined_array[target_unrefined_idx] /= count - width_unrefined_array[target_unrefined_idx] /= count + moment_unrefined_dist[target_unrefined_idx] /= count + cl_unrefined_dist[target_unrefined_idx] /= count + cd_unrefined_dist[target_unrefined_idx] /= count + cm_unrefined_dist[target_unrefined_idx] /= count + alpha_unrefined_dist[target_unrefined_idx] /= count + x_airf_unrefined_dist[target_unrefined_idx] ./= count + y_airf_unrefined_dist[target_unrefined_idx] ./= count + z_airf_unrefined_dist[target_unrefined_idx] ./= count + va_unrefined_dist[target_unrefined_idx] ./= count + chord_unrefined_dist[target_unrefined_idx] /= count + width_unrefined_dist[target_unrefined_idx] /= count end end unrefined_idx += wing.n_unrefined_sections @@ -453,15 +453,15 @@ function solve(solver::Solver, body_aero::BodyAerodynamics, gamma_distribution=n solver.aerodynamic_model_type, solver.core_radius_fraction, solver.mu, - solver.lr.alpha_array, - solver.lr.v_a_array, - solver.sol._chord_array, - solver.sol._x_airf_array, - solver.sol._y_airf_array, - solver.sol._z_airf_array, - solver.sol._va_array, - solver.br.va_norm_array, - solver.br.va_unit_array, + solver.lr.alpha_dist, + solver.lr.v_a_dist, + solver.sol._chord_dist, + solver.sol._x_airf_dist, + solver.sol._y_airf_dist, + solver.sol._z_airf_dist, + solver.sol._va_dist, + solver.br.va_norm_dist, + solver.br.va_unit_dist, body_aero.panels, solver.is_only_f_and_gamma_output; correct_aoa=solver.correct_aoa @@ -469,9 +469,9 @@ function solve(solver::Solver, body_aero::BodyAerodynamics, gamma_distribution=n return results end -@inline @inbounds function calc_norm_array!(va_norm_array, va_array) +@inline @inbounds function calc_norm_array!(va_norm_dist, va_array) for i in 1:size(va_array, 1) - va_norm_array[i] = norm(MVec3(view(va_array, i, :))) + va_norm_dist[i] = norm(MVec3(view(va_array, i, :))) end end @@ -487,31 +487,31 @@ function solve_base!(solver::Solver, body_aero::BodyAerodynamics, gamma_distribu relaxation_factor = solver.relaxation_factor # Clear arrays - solver.sol._x_airf_array .= 0 - solver.sol._y_airf_array .= 0 - solver.sol._z_airf_array .= 0 - solver.sol._va_array .= 0 - solver.sol._chord_array .= 0 + solver.sol._x_airf_dist .= 0 + solver.sol._y_airf_dist .= 0 + solver.sol._z_airf_dist .= 0 + solver.sol._va_dist .= 0 + solver.sol._chord_dist .= 0 # Fill arrays from panels for (i, panel) in enumerate(panels) - solver.sol._x_airf_array[i, :] .= panel.x_airf - solver.sol._y_airf_array[i, :] .= panel.y_airf - solver.sol._z_airf_array[i, :] .= panel.z_airf - solver.sol._va_array[i, :] .= panel.va - solver.sol._chord_array[i] = panel.chord + solver.sol._x_airf_dist[i, :] .= panel.x_airf + solver.sol._y_airf_dist[i, :] .= panel.y_airf + solver.sol._z_airf_dist[i, :] .= panel.z_airf + solver.sol._va_dist[i, :] .= panel.va + solver.sol._chord_dist[i] = panel.chord end # Calculate unit vectors - calc_norm_array!(solver.br.va_norm_array, solver.sol._va_array) - solver.br.va_unit_array .= solver.sol._va_array ./ solver.br.va_norm_array + calc_norm_array!(solver.br.va_norm_dist, solver.sol._va_dist) + solver.br.va_unit_dist .= solver.sol._va_dist ./ solver.br.va_norm_dist # Calculate AIC matrices - calculate_AIC_matrices!(body_aero, solver.aerodynamic_model_type, solver.core_radius_fraction, solver.br.va_norm_array, - solver.br.va_unit_array) + calculate_AIC_matrices!(body_aero, solver.aerodynamic_model_type, solver.core_radius_fraction, solver.br.va_norm_dist, + solver.br.va_unit_dist) # Initialize gamma distribution - gamma_initial = solver.cache_base[1][solver.sol._chord_array] + gamma_initial = solver.cache_base[1][solver.sol._chord_dist] if isnothing(gamma_distribution) if solver.type_initial_gamma_distribution == ELLIPTIC calculate_circulation_distribution_elliptical_wing(gamma_initial, body_aero) @@ -552,24 +552,24 @@ function gamma_loop!( relaxation_factor::Float64; log::Bool = true ) - va_array = solver.sol._va_array - chord_array = solver.sol._chord_array - x_airf_array = solver.sol._x_airf_array - y_airf_array = solver.sol._y_airf_array - z_airf_array = solver.sol._z_airf_array + va_array = solver.sol._va_dist + chord_array = solver.sol._chord_dist + x_airf_array = solver.sol._x_airf_dist + y_airf_array = solver.sol._y_airf_dist + z_airf_array = solver.sol._z_airf_dist solver.lr.converged = false n_panels = length(body_aero.panels) - solver.lr.alpha_array .= body_aero.alpha_array - solver.lr.v_a_array .= body_aero.v_a_array + solver.lr.alpha_dist .= body_aero.alpha_dist + solver.lr.v_a_dist .= body_aero.v_a_dist - va_magw_array = solver.cache[1][solver.lr.v_a_array] + va_magw_array = solver.cache[1][solver.lr.v_a_dist] gamma = solver.cache[2][solver.lr.gamma_new] abs_gamma_new = solver.cache[3][solver.lr.gamma_new] induced_velocity_all = solver.cache[4][va_array] relative_velocity_array = solver.cache[5][va_array] relative_velocity_crossz = solver.cache[6][va_array] v_acrossz_array = solver.cache[7][va_array] - cl_array = solver.cache[8][solver.lr.gamma_new] + cl_dist = solver.cache[8][solver.lr.gamma_new] damp = solver.cache[9][solver.lr.gamma_new] v_normal_array = solver.cache[10][solver.lr.gamma_new] v_tangential_array = solver.cache[11][solver.lr.gamma_new] @@ -600,17 +600,17 @@ function gamma_loop!( v_normal_array[i] = view(z_airf_array, i, :) ⋅ view(relative_velocity_array, i, :) v_tangential_array[i] = view(x_airf_array, i, :) ⋅ view(relative_velocity_array, i, :) end - solver.lr.alpha_array .= atan.(v_normal_array, v_tangential_array) + solver.lr.alpha_dist .= atan.(v_normal_array, v_tangential_array) for i in 1:n_panels - @views solver.lr.v_a_array[i] = norm(relative_velocity_crossz[i, :]) + @views solver.lr.v_a_dist[i] = norm(relative_velocity_crossz[i, :]) @views va_magw_array[i] = norm(v_acrossz_array[i, :]) end - for (i, (panel, alpha)) in enumerate(zip(panels, solver.lr.alpha_array)) - cl_array[i] = calculate_cl(panel, alpha) + for (i, (panel, alpha)) in enumerate(zip(panels, solver.lr.alpha_dist)) + cl_dist[i] = calculate_cl(panel, alpha) end - gamma_new .= 0.5 .* solver.lr.v_a_array.^2 ./ va_magw_array .* cl_array .* chord_array + gamma_new .= 0.5 .* solver.lr.v_a_dist.^2 ./ va_magw_array .* cl_dist .* chord_array nothing end @@ -769,8 +769,8 @@ Jacobian computation. When the same angles are encountered, geometry deformation # Returns - `jac::Matrix{Float64}`: Jacobian matrix (m×n) where m = 6 + n_unrefined_sections, n = length(y) - `results::Vector{Float64}`: Output vector at operating point - - If `aero_coeffs=false`: [Fx, Fy, Fz, Mx, My, Mz, unrefined_moment_dist...] - - If `aero_coeffs=true`: [CFx, CFy, CFz, CMx, CMy, CMz, unrefined_moment_coeff_dist...] + - If `aero_coeffs=false`: [Fx, Fy, Fz, Mx, My, Mz, moment_unrefined_dist...] + - If `aero_coeffs=true`: [CFx, CFy, CFz, CMx, CMy, CMz, cm_unrefined_dist...] # Example ```julia @@ -868,16 +868,16 @@ function linearize(solver::Solver, body_aero::BodyAerodynamics, y::Vector{T}; if !aero_coeffs results[1:3] .= solver.sol.force results[4:6] .= solver.sol.moment - results[7:end] .= solver.sol.unrefined_moment_dist + results[7:end] .= solver.sol.moment_unrefined_dist else results[1:3] .= solver.sol.force_coeffs results[4:6] .= solver.sol.moment_coeffs - results[7:end] .= solver.sol.unrefined_moment_coeff_dist + results[7:end] .= solver.sol.cm_unrefined_dist end return nothing end - results = zeros(3+3+length(solver.sol.unrefined_moment_dist)) + results = zeros(3+3+length(solver.sol.moment_unrefined_dist)) jac = zeros(length(results), length(y)) backend = AutoFiniteDiff(absstep=1e2solver.atol, relstep=1e2solver.rtol) prep = prepare_jacobian(calc_results!, results, backend, y) From a97157468d0c3747c3362f1a119ac1de46cf5783 Mon Sep 17 00:00:00 2001 From: 1-Bart-1 Date: Sat, 6 Dec 2025 16:28:39 +0100 Subject: [PATCH 35/53] Update the deform functions to make more sense --- src/body_aerodynamics.jl | 5 +- src/obj_geometry.jl | 2 +- src/wing_geometry.jl | 169 +++++++++++++++++++++++++++------------ 3 files changed, 121 insertions(+), 55 deletions(-) diff --git a/src/body_aerodynamics.jl b/src/body_aerodynamics.jl index 539c95f9..1ca1506f 100644 --- a/src/body_aerodynamics.jl +++ b/src/body_aerodynamics.jl @@ -157,9 +157,8 @@ function reinit!(body_aero::BodyAerodynamics; # Create panels for i in 1:wing.n_panels if !isnothing(wing.delta_dist) && length(wing.delta_dist) > 0 - # Map refined panel to unrefined section to get delta value - unrefined_idx = wing.refined_panel_mapping[i] - delta = wing.delta_dist[unrefined_idx] + # Panel i gets its delta directly from delta_dist[i] + delta = wing.delta_dist[i] else delta = 0.0 end diff --git a/src/obj_geometry.jl b/src/obj_geometry.jl index 2a654893..c77e122e 100644 --- a/src/obj_geometry.jl +++ b/src/obj_geometry.jl @@ -523,7 +523,7 @@ function ObjWing( wing = Wing(n_panels, Int16(n_unrefined_sections), spanwise_distribution, panel_props, MVec3(spanwise_direction), sections, refined_sections, remove_nan, Int16[], - Section[], zeros(n_unrefined_sections), zeros(n_unrefined_sections), + Section[], zeros(n_panels), zeros(n_panels), mass, gamma_tip, inertia_tensor, T_cad_body, R_cad_body, radius, le_interp, te_interp, area_interp, cache) diff --git a/src/wing_geometry.jl b/src/wing_geometry.jl index 857f5fe5..2345bd46 100644 --- a/src/wing_geometry.jl +++ b/src/wing_geometry.jl @@ -236,8 +236,8 @@ mutable struct Wing <: AbstractWing # Deformation fields non_deformed_sections::Vector{Section} - theta_dist::Vector{Float64} # Length: n_unrefined_sections (section twist angles) - delta_dist::Vector{Float64} # Length: n_unrefined_sections (section TE deflection angles) + theta_dist::Vector{Float64} # Length: n_panels (panel twist angles) + delta_dist::Vector{Float64} # Length: n_panels (panel TE deflection angles) # Physical properties (OBJ-based wings) mass::Float64 @@ -307,7 +307,7 @@ function Wing(n_panels::Int; # Grouping Int16[], # Deformation fields - Section[], zeros(max(0, n_unrefined_sections_value)), zeros(max(0, n_unrefined_sections_value)), + Section[], zeros(max(0, n_panels)), zeros(max(0, n_panels)), # Physical properties (defaults for non-OBJ wings) 0.0, 0.0, zeros(0, 0), zeros(MVec3), Matrix{Float64}(I, 3, 3), 0.0, nothing, nothing, nothing, @@ -372,7 +372,8 @@ if both angle inputs are nothing. # Returns - `nothing` (modifies wing in-place) """ -function unrefined_deform!(wing::Wing, theta_angles=nothing, delta_angles=nothing; smooth=false) +function unrefined_deform!(wing::Wing, theta_angles=nothing, delta_angles=nothing; + smooth=false, smooth_window=nothing) # Check if deformation is supported can_deform = !isempty(wing.non_deformed_sections) @@ -390,91 +391,156 @@ function unrefined_deform!(wing::Wing, theta_angles=nothing, delta_angles=nothin !isnothing(delta_angles) && length(delta_angles) != wing.n_unrefined_sections && throw(ArgumentError("delta_angles must have length n_unrefined_sections = $(wing.n_unrefined_sections), got $(length(delta_angles))")) - # Copy angles to theta_dist and delta_dist - !isnothing(theta_angles) && (wing.theta_dist .= theta_angles) - !isnothing(delta_angles) && (wing.delta_dist .= delta_angles) + # Map unrefined sections → panels → sections (no smoothing yet) + if !isnothing(theta_angles) + map_unrefined_to_sections!(wing.theta_dist, theta_angles, wing.refined_panel_mapping, wing.n_panels) + end + if !isnothing(delta_angles) + map_unrefined_to_sections!(wing.delta_dist, delta_angles, wing.refined_panel_mapping, wing.n_panels) + end - # Apply 3-point moving average smoothing if requested - if smooth && wing.n_unrefined_sections > 2 - if !isnothing(theta_angles) - for i in 2:(wing.n_unrefined_sections-1) - wing.theta_dist[i] = (wing.theta_dist[i-1] + wing.theta_dist[i] + wing.theta_dist[i+1]) / 3.0 - end - end - if !isnothing(delta_angles) - for i in 2:(wing.n_unrefined_sections-1) - wing.delta_dist[i] = (wing.delta_dist[i-1] + wing.delta_dist[i] + wing.delta_dist[i+1]) / 3.0 - end - end + # Apply deformation with optional smoothing + deform!(wing, smooth=smooth, smooth_window=smooth_window) + return nothing +end + +""" + map_unrefined_to_sections!(panel_dist, unrefined_angles, panel_mapping, n_panels) + +Map angles from unrefined sections to panels. +Steps: unrefined[1:n_unrefined] → panels[1:n_panels] + +# Arguments +- `panel_dist::Vector{Float64}`: Output panel angles (length n_panels) +- `unrefined_angles::Vector{Float64}`: Input unrefined section angles +- `panel_mapping::Vector{Int16}`: Maps panel index to unrefined section index +- `n_panels::Int`: Number of panels +""" +function map_unrefined_to_sections!(panel_dist, + unrefined_angles, + panel_mapping, + n_panels) + # Map unrefined sections to panels + for i in 1:n_panels + unrefined_idx = panel_mapping[i] + panel_dist[i] = unrefined_angles[unrefined_idx] end - deform!(wing) return nothing end """ - deform!(wing::Wing, theta_dist::AbstractVector, delta_dist::AbstractVector) + smooth_distribution!(dist, window_size) -Deform wing by applying theta and delta distributions directly to unrefined sections. +Apply moving average smoothing to a distribution in-place. +Uses a centered window of size `window_size` (must be odd). + +# Arguments +- `dist::Vector{Float64}`: Distribution to smooth (modified in-place) +- `window_size::Int`: Size of smoothing window (must be odd) +""" +function smooth_distribution!(dist::Vector{Float64}, window_size::Int) + n = length(dist) + window_size <= 1 && return nothing + n <= window_size && return nothing + + # Create temporary copy + dist_copy = copy(dist) + half_window = div(window_size, 2) + + # Apply moving average + for i in 1:n + start_idx = max(1, i - half_window) + end_idx = min(n, i + half_window) + dist[i] = sum(dist_copy[start_idx:end_idx]) / (end_idx - start_idx + 1) + end + + return nothing +end + +""" + deform!(wing::Wing, theta_dist::AbstractVector, delta_dist::AbstractVector; + smooth=false, smooth_window=nothing) + +Deform wing by applying theta and delta distributions at the panel level. # Arguments - `wing::Wing`: Wing to deform (must support deformation) -- `theta_dist::AbstractVector`: Twist angle in radians for each unrefined section (length = n_unrefined_sections) -- `delta_dist::AbstractVector`: Trailing edge deflection for each unrefined section (length = n_unrefined_sections) +- `theta_dist::AbstractVector`: Twist angles for each panel (length = n_panels) +- `delta_dist::AbstractVector`: TE deflections for each panel (length = n_panels) +- `smooth::Bool`: Whether to apply smoothing (default: false) +- `smooth_window::Union{Nothing, Int}`: Smoothing window size (default: auto-calculated) # Effects -Updates wing.unrefined_sections with deformed geometry based on wing.non_deformed_sections +Updates wing.refined_sections with deformed geometry based on wing.non_deformed_sections """ -function deform!(wing::Wing, theta_dist::AbstractVector, delta_dist::AbstractVector) +function deform!(wing::Wing, theta_dist::AbstractVector, delta_dist::AbstractVector; + smooth=false, smooth_window=nothing) !isempty(wing.non_deformed_sections) || throw(ArgumentError("Wing does not support deformation")) - !(length(theta_dist) == wing.n_unrefined_sections) && throw(ArgumentError("theta_dist must have length $(wing.n_unrefined_sections), got $(length(theta_dist))")) - !(length(delta_dist) == wing.n_unrefined_sections) && throw(ArgumentError("delta_dist must have length $(wing.n_unrefined_sections), got $(length(delta_dist))")) + + expected_len = wing.n_panels + !(length(theta_dist) == expected_len) && throw(ArgumentError("theta_dist must have length $(expected_len), got $(length(theta_dist))")) + !(length(delta_dist) == expected_len) && throw(ArgumentError("delta_dist must have length $(expected_len), got $(length(delta_dist))")) + wing.theta_dist .= theta_dist wing.delta_dist .= delta_dist - deform!(wing) + deform!(wing, smooth=smooth, smooth_window=smooth_window) end """ - deform!(wing::Wing) + deform!(wing::Wing; smooth=false, smooth_window=nothing) Apply stored theta_dist and delta_dist to deform the wing geometry. - -Deformation works by: -1. Applying theta/delta angles to unrefined sections (wing.unrefined_sections) -2. Using refined_panel_mapping to determine which unrefined section each refined section came from -3. Applying the corresponding angle to each refined section +Converts panel angles (n_panels) to section angles (n_panels+1) by averaging adjacent panels. # Arguments - `wing::Wing`: Wing to deform (must have non_deformed_sections) +- `smooth::Bool`: Whether to apply smoothing to theta_dist and delta_dist (default: false) +- `smooth_window::Union{Nothing, Int}`: Smoothing window size (default: auto-calculated) # Effects Updates wing.refined_sections based on wing.non_deformed_sections and stored distributions """ -function deform!(wing::Wing) +function deform!(wing::Wing; smooth=false, smooth_window=nothing) !isempty(wing.non_deformed_sections) || return nothing + # Apply smoothing if requested + if smooth + # Default window size based on refinement ratio + if isnothing(smooth_window) + smooth_window = max(3, round(Int, wing.n_panels / wing.n_unrefined_sections)) + end + + # Ensure window is odd for symmetric averaging + if smooth_window % 2 == 0 + smooth_window += 1 + end + + # Apply moving average to theta_dist and delta_dist (panel-level) + smooth_distribution!(wing.theta_dist, smooth_window) + smooth_distribution!(wing.delta_dist, smooth_window) + end + local_y = zeros(MVec3) chord = zeros(MVec3) normal = zeros(MVec3) # Process all refined sections (n_panels + 1) - # Each refined section gets the angle from its corresponding unrefined section + # Convert panel angles to section angles by averaging for i in 1:(wing.n_panels + 1) - # Determine which unrefined section this refined section belongs to + # Determine theta for this section by averaging adjacent panels if i == 1 - # First section: use first unrefined section - unrefined_idx = 1 + # First section: use panel 1 angle + theta = wing.theta_dist[1] elseif i == wing.n_panels + 1 - # Last section: use last unrefined section - unrefined_idx = wing.n_unrefined_sections + # Last section: use last panel angle + theta = wing.theta_dist[wing.n_panels] else - # Middle sections: use the mapping from the panel to the left - unrefined_idx = wing.refined_panel_mapping[i-1] + # Middle sections: average of panels (i-1) and i + theta = (wing.theta_dist[i-1] + wing.theta_dist[i]) / 2.0 end - theta = wing.theta_dist[unrefined_idx] - section = wing.non_deformed_sections[i] # Compute local coordinate system @@ -693,13 +759,14 @@ function refine_aerodynamic_mesh!(wing::AbstractWing; recompute_mapping=true, so # Update n_unrefined_sections based on actual sections wing.n_unrefined_sections = Int16(length(wing.unrefined_sections)) - # Resize theta_dist and delta_dist to match n_unrefined_sections - if length(wing.theta_dist) != wing.n_unrefined_sections - resize!(wing.theta_dist, wing.n_unrefined_sections) + # Resize theta_dist and delta_dist to match n_panels + target_size = wing.n_panels + if length(wing.theta_dist) != target_size + resize!(wing.theta_dist, target_size) fill!(wing.theta_dist, 0.0) end - if length(wing.delta_dist) != wing.n_unrefined_sections - resize!(wing.delta_dist, wing.n_unrefined_sections) + if length(wing.delta_dist) != target_size + resize!(wing.delta_dist, target_size) fill!(wing.delta_dist, 0.0) end From 4a6a37170b590461e3a3f98b06aa706d9b296b60 Mon Sep 17 00:00:00 2001 From: 1-Bart-1 Date: Sat, 6 Dec 2025 16:29:19 +0100 Subject: [PATCH 36/53] Update tests for recent changes --- test/bench.jl | 43 +++++++--- .../test_body_aerodynamics.jl | 3 - test/body_aerodynamics/test_results.jl | 12 +-- test/ram_geometry/test_kite_geometry.jl | 4 +- test/runtests.jl | 4 +- test/wing_geometry/test_wing_geometry.jl | 60 ++++++------- .../test_yaml_wing_deformation.jl | 85 ++++++++++++++++--- 7 files changed, 141 insertions(+), 70 deletions(-) diff --git a/test/bench.jl b/test/bench.jl index 88172d69..b5d1ba27 100644 --- a/test/bench.jl +++ b/test/bench.jl @@ -14,6 +14,9 @@ using VortexStepMethod: calculate_AIC_matrices!, gamma_loop!, calculate_results, using Test using LinearAlgebra +# Check Julia version for known allocation issues +const IS_JULIA_1_12_OR_NEWER = VERSION >= v"1.12" + @testset "Function Allocation Tests" begin # Define wing parameters n_panels = 20 # Number of panels @@ -85,7 +88,11 @@ using LinearAlgebra for frac in core_radius_fractions @testset "Model $model Core Radius Fraction $frac" begin result = @benchmark calculate_AIC_matrices!($body_aero, $model, $frac, $va_norm_array, $va_unit_array) samples=1 evals=1 - @test result.allocs ≤ 30 + if IS_JULIA_1_12_OR_NEWER + @test_broken result.allocs ≤ 30 + else + @test result.allocs ≤ 30 + end @info "Model: $(model) \t Core radius fraction: $(frac) \t Allocations: $(result.allocs) \t Memory: $(result.memory)" end end @@ -134,11 +141,11 @@ using LinearAlgebra solver = Solver(body_aero; aerodynamic_model_type=model ) - solver.sol._va_array .= va_array - solver.sol._chord_array .= chord_array - solver.sol._x_airf_array .= x_airf_array - solver.sol._y_airf_array .= y_airf_array - solver.sol._z_airf_array .= z_airf_array + solver.sol._va_dist .= va_array + solver.sol._chord_dist .= chord_array + solver.sol._x_airf_dist .= x_airf_array + solver.sol._y_airf_dist .= y_airf_array + solver.sol._z_airf_dist .= z_airf_array result = @benchmark gamma_loop!( $solver, $body_aero, @@ -203,17 +210,33 @@ using LinearAlgebra @testset "Allocation Tests for solve() and solve!()" begin result = @benchmark solve_base!($solver, $body_aero, nothing) samples=1 evals=1 # 51 allocations - @test result.allocs <= 55 + if IS_JULIA_1_12_OR_NEWER + @test_broken result.allocs <= 55 + else + @test result.allocs <= 55 + end # time Python: 32.0 ms Ryzen 7950x # time Julia: 0.45 ms Ryzen 7950x result = @benchmark sol = solve!($solver, $body_aero, nothing) samples=1 evals=1 # 85 allocations - @test result.allocs <= 89 + if IS_JULIA_1_12_OR_NEWER + @test_broken result.allocs <= 89 + else + @test result.allocs <= 89 + end # Step 5: Solve using both methods result = @benchmark solve_base!($nonlin_solver, $body_aero, nothing) samples=1 evals=1 # 51 allocations - @test result.allocs <= 55 + if IS_JULIA_1_12_OR_NEWER + @test_broken result.allocs <= 55 + else + @test result.allocs <= 55 + end result = @benchmark sol = solve!($nonlin_solver, $body_aero, nothing) samples=1 evals=1 # 85 allocations - @test result.allocs <= 89 + if IS_JULIA_1_12_OR_NEWER + @test_broken result.allocs <= 89 + else + @test result.allocs <= 89 + end end end diff --git a/test/body_aerodynamics/test_body_aerodynamics.jl b/test/body_aerodynamics/test_body_aerodynamics.jl index 239e5113..6e61857c 100644 --- a/test/body_aerodynamics/test_body_aerodynamics.jl +++ b/test/body_aerodynamics/test_body_aerodynamics.jl @@ -351,9 +351,6 @@ end @test loop_sol.solver_status == FEASIBLE - @test sum(loop_sol.moment_dist) ≈ sum(loop_sol.unrefined_moment_dist) - @test sum(nonlin_sol.moment_dist) ≈ sum(nonlin_sol.unrefined_moment_dist) - end # Calculate forces using uncorrected alpha diff --git a/test/body_aerodynamics/test_results.jl b/test/body_aerodynamics/test_results.jl index 80be8585..5378bedf 100644 --- a/test/body_aerodynamics/test_results.jl +++ b/test/body_aerodynamics/test_results.jl @@ -85,8 +85,8 @@ end # Verify that linearization results match nonlinear results at operating point baseline_res = VortexStepMethod.solve!(solver, body_aero; log=false) - baseline_res = [solver.sol.force; solver.sol.moment; solver.sol.unrefined_moment_dist] - coeff_baseline_res = [solver.sol.force_coeffs; solver.sol.moment_coeffs; solver.sol.unrefined_moment_coeff_dist] + baseline_res = [solver.sol.force; solver.sol.moment; solver.sol.moment_unrefined_dist] + coeff_baseline_res = [solver.sol.force_coeffs; solver.sol.moment_coeffs; solver.sol.cm_unrefined_dist] @test baseline_res ≈ lin_res @test coeff_baseline_res ≈ coeff_lin_res @@ -142,8 +142,8 @@ end # Get nonlinear solution nonlin_res = VortexStepMethod.solve!(solver, body_aero, nothing; log=false) - nonlin_res = [solver.sol.force; solver.sol.moment; solver.sol.unrefined_moment_dist] - coeff_nonlin_res = [solver.sol.force_coeffs; solver.sol.moment_coeffs; solver.sol.unrefined_moment_coeff_dist] + nonlin_res = [solver.sol.force; solver.sol.moment; solver.sol.moment_unrefined_dist] + coeff_nonlin_res = [solver.sol.force_coeffs; solver.sol.moment_coeffs; solver.sol.cm_unrefined_dist] @test nonlin_res ≉ baseline_res @test coeff_nonlin_res ≉ baseline_res @@ -250,7 +250,7 @@ end # Get baseline results baseline_res = VortexStepMethod.solve!(solver, body_aero; log=false) - baseline_res = [solver.sol.force; solver.sol.moment; solver.sol.unrefined_moment_dist] + baseline_res = [solver.sol.force; solver.sol.moment; solver.sol.moment_unrefined_dist] # Should match the linearization result @test baseline_res ≈ lin_res_combo @@ -277,7 +277,7 @@ end # Get nonlinear solution with perturbation nonlin_res = VortexStepMethod.solve!(solver, body_aero; log=false) - nonlin_res = [solver.sol.force; solver.sol.moment; solver.sol.unrefined_moment_dist] + nonlin_res = [solver.sol.force; solver.sol.moment; solver.sol.moment_unrefined_dist] # Compute linearized prediction using our specialized Jacobian lin_prediction = lin_res_combo + jac_combo * perturbation diff --git a/test/ram_geometry/test_kite_geometry.jl b/test/ram_geometry/test_kite_geometry.jl index d76cc533..3058af89 100644 --- a/test/ram_geometry/test_kite_geometry.jl +++ b/test/ram_geometry/test_kite_geometry.jl @@ -195,8 +195,8 @@ using Serialization original_te_point = copy(body_aero.panels[i].TE_point_1) # Apply deformation with non-zero angles - theta_dist = fill(deg2rad(30.0), wing.n_unrefined_sections) # 30 degrees twist for all sections - delta_dist = fill(deg2rad(5.0), wing.n_unrefined_sections) # 5 degrees trailing edge deflection for all sections + theta_dist = fill(deg2rad(30.0), wing.n_panels) # 30 degrees twist for all panels + delta_dist = fill(deg2rad(5.0), wing.n_panels) # 5 degrees TE deflection for all panels VortexStepMethod.deform!(wing, theta_dist, delta_dist) VortexStepMethod.reinit!(body_aero) diff --git a/test/runtests.jl b/test/runtests.jl index 7a2abdd5..d0b770d4 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -16,7 +16,7 @@ end function should_run_test(test_path::String) isempty(test_patterns) && return true for pattern in test_patterns - # Match directory (e.g., "solver") or specific file (e.g., "test_group_coefficients") + # Match directory (e.g., "solver") or specific file (e.g., "test_unrefined_dist") if occursin(pattern, test_path) return true end @@ -45,7 +45,7 @@ end::Bool should_run_test("ram_geometry/test_kite_geometry.jl") && include("ram_geometry/test_kite_geometry.jl") should_run_test("settings/test_settings.jl") && include("settings/test_settings.jl") should_run_test("solver/test_solver.jl") && include("solver/test_solver.jl") - should_run_test("solver/test_group_coefficients.jl") && include("solver/test_group_coefficients.jl") + should_run_test("solver/test_unrefined_dist.jl") && include("solver/test_unrefined_dist.jl") should_run_test("VortexStepMethod/test_VortexStepMethod.jl") && include("VortexStepMethod/test_VortexStepMethod.jl") should_run_test("wake/test_wake.jl") && include("wake/test_wake.jl") should_run_test("wing_geometry/test_wing_geometry.jl") && include("wing_geometry/test_wing_geometry.jl") diff --git a/test/wing_geometry/test_wing_geometry.jl b/test/wing_geometry/test_wing_geometry.jl index 5e458c2a..8c4ba69c 100644 --- a/test/wing_geometry/test_wing_geometry.jl +++ b/test/wing_geometry/test_wing_geometry.jl @@ -341,7 +341,7 @@ end span = 10.0 wing = Wing(n_panels; spanwise_distribution=LINEAR) - # 3 sections = 2 unrefined panels + # 3 sections add_section!(wing, [0.0, span/2, 0.0], [1.0, span/2, 0.0], INVISCID) add_section!(wing, [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], INVISCID) add_section!(wing, [0.0, -span/2, 0.0], [1.0, -span/2, 0.0], INVISCID) @@ -350,8 +350,8 @@ end @test length(wing.refined_panel_mapping) == n_panels - # Manually verify each refined panel is mapped to its closest unrefined panel - n_unrefined_panels = length(wing.unrefined_sections) - 1 + # Manually verify each refined panel is mapped to its closest unrefined section + n_unrefined_sections = length(wing.unrefined_sections) for refined_panel_idx in 1:n_panels # Calculate refined panel center le_mid = (wing.refined_sections[refined_panel_idx].LE_point + @@ -360,20 +360,18 @@ end wing.refined_sections[refined_panel_idx+1].TE_point) / 2 refined_center = (le_mid + te_mid) / 2 - # Find closest unrefined panel manually + # Find closest unrefined section manually min_dist = Inf closest_idx = 1 - for unrefined_panel_idx in 1:n_unrefined_panels - le_mid_unref = (wing.unrefined_sections[unrefined_panel_idx].LE_point + - wing.unrefined_sections[unrefined_panel_idx+1].LE_point) / 2 - te_mid_unref = (wing.unrefined_sections[unrefined_panel_idx].TE_point + - wing.unrefined_sections[unrefined_panel_idx+1].TE_point) / 2 - unrefined_center = (le_mid_unref + te_mid_unref) / 2 + for unrefined_section_idx in 1:n_unrefined_sections + le_point = wing.unrefined_sections[unrefined_section_idx].LE_point + te_point = wing.unrefined_sections[unrefined_section_idx].TE_point + unrefined_center = (le_point + te_point) / 2 dist = norm(refined_center - unrefined_center) if dist < min_dist min_dist = dist - closest_idx = unrefined_panel_idx + closest_idx = unrefined_section_idx end end @@ -387,7 +385,7 @@ end span = 20.0 wing = Wing(n_panels; spanwise_distribution=COSINE) - # 4 sections = 3 unrefined panels + # 4 sections add_section!(wing, [0.0, span/2, 0.0], [1.0, span/2, 0.0], INVISCID) add_section!(wing, [0.0, span/6, 0.0], [1.0, span/6, 0.0], INVISCID) add_section!(wing, [0.0, -span/6, 0.0], [1.0, -span/6, 0.0], INVISCID) @@ -397,8 +395,8 @@ end @test length(wing.refined_panel_mapping) == n_panels - # Verify each panel is mapped to its closest unrefined panel - n_unrefined_panels = length(wing.unrefined_sections) - 1 + # Verify each panel is mapped to its closest unrefined section + n_unrefined_sections = length(wing.unrefined_sections) for refined_panel_idx in 1:n_panels # Calculate refined panel center le_mid = (wing.refined_sections[refined_panel_idx].LE_point + @@ -407,20 +405,18 @@ end wing.refined_sections[refined_panel_idx+1].TE_point) / 2 refined_center = (le_mid + te_mid) / 2 - # Find closest unrefined panel manually + # Find closest unrefined section manually min_dist = Inf closest_idx = 1 - for unrefined_panel_idx in 1:n_unrefined_panels - le_mid_unref = (wing.unrefined_sections[unrefined_panel_idx].LE_point + - wing.unrefined_sections[unrefined_panel_idx+1].LE_point) / 2 - te_mid_unref = (wing.unrefined_sections[unrefined_panel_idx].TE_point + - wing.unrefined_sections[unrefined_panel_idx+1].TE_point) / 2 - unrefined_center = (le_mid_unref + te_mid_unref) / 2 + for unrefined_section_idx in 1:n_unrefined_sections + le_point = wing.unrefined_sections[unrefined_section_idx].LE_point + te_point = wing.unrefined_sections[unrefined_section_idx].TE_point + unrefined_center = (le_point + te_point) / 2 dist = norm(refined_center - unrefined_center) if dist < min_dist min_dist = dist - closest_idx = unrefined_panel_idx + closest_idx = unrefined_section_idx end end @@ -433,7 +429,7 @@ end n_panels = 12 wing = Wing(n_panels; spanwise_distribution=SPLIT_PROVIDED) - # 4 sections = 3 unrefined panels + # 4 sections add_section!(wing, [0.0, 6.0, 0.0], [1.0, 6.0, 0.0], INVISCID) add_section!(wing, [0.0, 2.0, 0.0], [1.0, 2.0, 0.0], INVISCID) add_section!(wing, [0.0, -2.0, 0.0], [1.0, -2.0, 0.0], INVISCID) @@ -443,8 +439,8 @@ end @test length(wing.refined_panel_mapping) == n_panels - # Verify each panel is mapped to its closest unrefined panel - n_unrefined_panels = length(wing.unrefined_sections) - 1 + # Verify each panel is mapped to its closest unrefined section + n_unrefined_sections = length(wing.unrefined_sections) for refined_panel_idx in 1:n_panels # Calculate refined panel center le_mid = (wing.refined_sections[refined_panel_idx].LE_point + @@ -453,20 +449,18 @@ end wing.refined_sections[refined_panel_idx+1].TE_point) / 2 refined_center = (le_mid + te_mid) / 2 - # Find closest unrefined panel manually + # Find closest unrefined section manually min_dist = Inf closest_idx = 1 - for unrefined_panel_idx in 1:n_unrefined_panels - le_mid_unref = (wing.unrefined_sections[unrefined_panel_idx].LE_point + - wing.unrefined_sections[unrefined_panel_idx+1].LE_point) / 2 - te_mid_unref = (wing.unrefined_sections[unrefined_panel_idx].TE_point + - wing.unrefined_sections[unrefined_panel_idx+1].TE_point) / 2 - unrefined_center = (le_mid_unref + te_mid_unref) / 2 + for unrefined_section_idx in 1:n_unrefined_sections + le_point = wing.unrefined_sections[unrefined_section_idx].LE_point + te_point = wing.unrefined_sections[unrefined_section_idx].TE_point + unrefined_center = (le_point + te_point) / 2 dist = norm(refined_center - unrefined_center) if dist < min_dist min_dist = dist - closest_idx = unrefined_panel_idx + closest_idx = unrefined_section_idx end end diff --git a/test/yaml_geometry/test_yaml_wing_deformation.jl b/test/yaml_geometry/test_yaml_wing_deformation.jl index 4b434de7..d483835a 100644 --- a/test/yaml_geometry/test_yaml_wing_deformation.jl +++ b/test/yaml_geometry/test_yaml_wing_deformation.jl @@ -14,9 +14,9 @@ using Test original_te_point = copy(body_aero.panels[i].TE_point_1) original_le_point = copy(body_aero.panels[i].LE_point_1) - # Apply deformation with non-zero angles - theta_dist = fill(deg2rad(30.0), wing.n_unrefined_sections) # 30 degrees twist - delta_dist = fill(deg2rad(5.0), wing.n_unrefined_sections) # 5 degrees trailing edge deflection + # Apply deformation with non-zero angles (panel-level) + theta_dist = fill(deg2rad(30.0), wing.n_panels) # 30 degrees twist per panel + delta_dist = fill(deg2rad(5.0), wing.n_panels) # 5 degrees TE deflection per panel VortexStepMethod.deform!(wing, theta_dist, delta_dist) VortexStepMethod.reinit!(body_aero; refine_mesh=false) @@ -36,8 +36,8 @@ using Test @test body_aero.panels[i].delta ≈ deg2rad(5.0) # Reset deformation with zero angles - zero_theta_dist = zeros(wing.n_unrefined_sections) - zero_delta_dist = zeros(wing.n_unrefined_sections) + zero_theta_dist = zeros(wing.n_panels) + zero_delta_dist = zeros(wing.n_panels) VortexStepMethod.deform!(wing, zero_theta_dist, zero_delta_dist) VortexStepMethod.reinit!(body_aero; refine_mesh=false) @@ -66,8 +66,8 @@ using Test )) end - # Apply spanwise-varying deformation - n = wing.n_unrefined_sections + # Apply spanwise-varying deformation (panel-level) + n = wing.n_panels theta_dist = [deg2rad(10.0 * i / n) for i in 1:n] # Linear twist distribution delta_dist = [deg2rad(-5.0 + 10.0 * i / n) for i in 1:n] # Varying deflection @@ -130,8 +130,8 @@ using Test body_aero = BodyAerodynamics([wing]) # Apply deformation - theta_dist = fill(deg2rad(15.0), wing.n_unrefined_sections) - delta_dist = fill(deg2rad(3.0), wing.n_unrefined_sections) + theta_dist = fill(deg2rad(15.0), wing.n_panels) + delta_dist = fill(deg2rad(3.0), wing.n_panels) VortexStepMethod.deform!(wing, theta_dist, delta_dist) VortexStepMethod.reinit!(body_aero; refine_mesh=false) @@ -158,13 +158,13 @@ using Test body_aero = BodyAerodynamics([wing]) # Test zero deformation - VortexStepMethod.deform!(wing, zeros(wing.n_unrefined_sections), zeros(wing.n_unrefined_sections)) + VortexStepMethod.deform!(wing, zeros(wing.n_panels), zeros(wing.n_panels)) VortexStepMethod.reinit!(body_aero; refine_mesh=false) @test all(p.delta ≈ 0.0 for p in body_aero.panels) # Test large deformation angles - theta_dist = fill(deg2rad(60.0), wing.n_unrefined_sections) - delta_dist = fill(deg2rad(30.0), wing.n_unrefined_sections) + theta_dist = fill(deg2rad(60.0), wing.n_panels) + delta_dist = fill(deg2rad(30.0), wing.n_panels) # Should not error even with large angles VortexStepMethod.deform!(wing, theta_dist, delta_dist) @@ -172,10 +172,67 @@ using Test @test all(p.delta ≈ deg2rad(30.0) for p in body_aero.panels) # Test negative angles - theta_dist = fill(deg2rad(-20.0), wing.n_unrefined_sections) - delta_dist = fill(deg2rad(-10.0), wing.n_unrefined_sections) + theta_dist = fill(deg2rad(-20.0), wing.n_panels) + delta_dist = fill(deg2rad(-10.0), wing.n_panels) VortexStepMethod.deform!(wing, theta_dist, delta_dist) VortexStepMethod.reinit!(body_aero; refine_mesh=false) @test all(p.delta ≈ deg2rad(-10.0) for p in body_aero.panels) end + + @testset "Panel to Section Angle Mapping" begin + # Test that panel angles are correctly averaged to section angles + simple_wing_file = test_data_path("yaml_geometry", "simple_wing.yaml") + wing = Wing(simple_wing_file; n_panels=4) + body_aero = BodyAerodynamics([wing]) + + # Create varying panel angles + theta_panel = [10.0, 20.0, 30.0, 40.0] # degrees per panel + delta_panel = [5.0, 10.0, 15.0, 20.0] # degrees per panel + + # Apply deformation + VortexStepMethod.deform!(wing, deg2rad.(theta_panel), deg2rad.(delta_panel)) + + # Verify section angles by checking the geometry + # Section 1: should use panel 1 angle = 10° + # Section 2: should avg panels 1,2 = (10+20)/2 = 15° + # Section 3: should avg panels 2,3 = (20+30)/2 = 25° + # Section 4: should avg panels 3,4 = (30+40)/2 = 35° + # Section 5: should use panel 4 angle = 40° + + # We can verify this by checking that delta values are correct + VortexStepMethod.reinit!(body_aero; refine_mesh=false) + + # Each panel gets its delta directly + @test body_aero.panels[1].delta ≈ deg2rad(5.0) atol=1e-6 + @test body_aero.panels[2].delta ≈ deg2rad(10.0) atol=1e-6 + @test body_aero.panels[3].delta ≈ deg2rad(15.0) atol=1e-6 + @test body_aero.panels[4].delta ≈ deg2rad(20.0) atol=1e-6 + end + + @testset "unrefined_deform! Maps to Panels" begin + # Test that unrefined_deform! correctly maps unrefined sections to panels + # Use complex_wing which has 7 unrefined sections + complex_wing_file = test_data_path("yaml_geometry", "complex_wing.yaml") + wing = Wing(complex_wing_file; n_panels=12) + body_aero = BodyAerodynamics([wing]) + + # Verify we have 7 unrefined sections + @test wing.n_unrefined_sections == 7 + + # Create unrefined section angles (7 sections) + # These will be mapped to panels via refined_panel_mapping + theta_unrefined = deg2rad.([10.0, 15.0, 20.0, 25.0, 20.0, 15.0, 10.0]) + delta_unrefined = deg2rad.([5.0, 7.5, 10.0, 12.5, 10.0, 7.5, 5.0]) + + # Apply using unrefined_deform! + VortexStepMethod.unrefined_deform!(wing, theta_unrefined, delta_unrefined) + VortexStepMethod.reinit!(body_aero; refine_mesh=false) + + # Each panel should have the delta from its mapped unrefined section + for i in 1:wing.n_panels + unrefined_idx = wing.refined_panel_mapping[i] + expected_delta = delta_unrefined[unrefined_idx] + @test body_aero.panels[i].delta ≈ expected_delta atol=1e-6 + end + end end From 6a7aca53df8e04a6260305f67b97441cc290f9cf Mon Sep 17 00:00:00 2001 From: 1-Bart-1 Date: Sat, 6 Dec 2025 16:29:58 +0100 Subject: [PATCH 37/53] Rename to more logical name --- test/solver/test_unrefined_dist.jl | 144 +++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 test/solver/test_unrefined_dist.jl diff --git a/test/solver/test_unrefined_dist.jl b/test/solver/test_unrefined_dist.jl new file mode 100644 index 00000000..219cc65e --- /dev/null +++ b/test/solver/test_unrefined_dist.jl @@ -0,0 +1,144 @@ +using VortexStepMethod +using LinearAlgebra +using Test + +@testset "Unrefined Arrays Tests" begin + @testset "Unrefined section array aggregation" begin + # Create a simple wing with unrefined sections + n_panels = 20 + n_unrefined_sections = 5 # 5 unrefined sections + + # Create a test wing settings file + settings_file = create_temp_wing_settings("solver", "solver_test_wing.yaml"; alpha=5.0, beta=0.0, wind_speed=10.0) + + try + # Modify settings to use specific panel configuration + settings = VSMSettings(settings_file) + settings.wings[1].n_panels = n_panels + settings.solver_settings.n_panels = n_panels + + # Create wing and solver + wing = Wing(settings) + body_aero = BodyAerodynamics([wing]) + solver = Solver(body_aero, settings) + + # Set conditions and solve + va = [10.0, 0.0, 0.0] + set_va!(body_aero, va) + sol = solve!(solver, body_aero) + + # Test 1: Unrefined arrays exist and have correct size + @test length(sol.cl_unrefined_dist) == wing.n_unrefined_sections + @test length(sol.cd_unrefined_dist) == wing.n_unrefined_sections + @test length(sol.cm_unrefined_dist) == wing.n_unrefined_sections + + # Test 2: Unrefined arrays are not all zeros (solver computed them) + @test !all(sol.cl_unrefined_dist .== 0.0) + @test !all(sol.cd_unrefined_dist .== 0.0) + + # Test 3: Verify unrefined coefficients are averaged from refined panels + # refined_panel_mapping maps each refined panel to its unrefined section index + for unrefined_idx in 1:wing.n_unrefined_sections + # Find all refined panels that map to this unrefined section + refined_panel_indices = findall(x -> x == unrefined_idx, wing.refined_panel_mapping) + + if !isempty(refined_panel_indices) + # Calculate expected average from refined panel coefficients + expected_cl = sum(sol.cl_dist[refined_panel_indices]) / length(refined_panel_indices) + expected_cd = sum(sol.cd_dist[refined_panel_indices]) / length(refined_panel_indices) + expected_cm = sum(sol.cm_dist[refined_panel_indices]) / length(refined_panel_indices) + + # Check if unrefined coefficients match expected averages + # Handle NaN values that can occur in INVISCID models + if isnan(expected_cl) + @test isnan(sol.cl_unrefined_dist[unrefined_idx]) + else + @test isapprox(sol.cl_unrefined_dist[unrefined_idx], expected_cl, rtol=1e-10) + end + if isnan(expected_cd) + @test isnan(sol.cd_unrefined_dist[unrefined_idx]) + else + @test isapprox(sol.cd_unrefined_dist[unrefined_idx], expected_cd, rtol=1e-10) + end + if isnan(expected_cm) + @test isnan(sol.cm_unrefined_dist[unrefined_idx]) + else + @test isapprox(sol.cm_unrefined_dist[unrefined_idx], expected_cm, rtol=1e-10) + end + end + end + + # Test 4: Verify physical consistency (lift coefficients should be positive at positive AoA) + # Skip test if values are NaN + if !any(isnan.(sol.cl_unrefined_dist)) + @test all(sol.cl_unrefined_dist .> 0.0) + end + + finally + rm(settings_file; force=true) + end + end + + @testset "Unrefined arrays with different panel counts" begin + # Test with various panel/section combinations + test_cases = [ + (n_panels=40, n_unrefined_expected=21), # From YAML file sections + (n_panels=30, n_unrefined_expected=21), + (n_panels=24, n_unrefined_expected=21), + ] + + for (n_panels, n_unrefined_expected) in test_cases + settings_file = create_temp_wing_settings("solver", "solver_test_wing.yaml"; alpha=5.0, beta=0.0, wind_speed=10.0) + + try + settings = VSMSettings(settings_file) + settings.wings[1].n_panels = n_panels + settings.solver_settings.n_panels = n_panels + + wing = Wing(settings) + body_aero = BodyAerodynamics([wing]) + solver = Solver(body_aero, settings) + + va = [10.0, 0.0, 0.0] + set_va!(body_aero, va) + sol = solve!(solver, body_aero) + + # Verify arrays have correct size + @test length(sol.cl_unrefined_dist) == wing.n_unrefined_sections + @test length(sol.cd_unrefined_dist) == wing.n_unrefined_sections + @test length(sol.cm_unrefined_dist) == wing.n_unrefined_sections + + # Verify unrefined coefficients are computed correctly using mapping + for unrefined_idx in 1:wing.n_unrefined_sections + refined_panel_indices = findall(x -> x == unrefined_idx, wing.refined_panel_mapping) + + if !isempty(refined_panel_indices) + expected_cl = sum(sol.cl_dist[refined_panel_indices]) / length(refined_panel_indices) + expected_cd = sum(sol.cd_dist[refined_panel_indices]) / length(refined_panel_indices) + expected_cm = sum(sol.cm_dist[refined_panel_indices]) / length(refined_panel_indices) + + # Handle NaN for all coefficients + if isnan(expected_cl) + @test isnan(sol.cl_unrefined_dist[unrefined_idx]) + else + @test isapprox(sol.cl_unrefined_dist[unrefined_idx], expected_cl, rtol=1e-10) + end + if isnan(expected_cd) + @test isnan(sol.cd_unrefined_dist[unrefined_idx]) + else + @test isapprox(sol.cd_unrefined_dist[unrefined_idx], expected_cd, rtol=1e-10) + end + if isnan(expected_cm) + @test isnan(sol.cm_unrefined_dist[unrefined_idx]) + else + @test isapprox(sol.cm_unrefined_dist[unrefined_idx], expected_cm, rtol=1e-10) + end + end + end + + finally + rm(settings_file; force=true) + end + end + end +end From f13015779bde05e1ee79b4448d3f22e046fd003d Mon Sep 17 00:00:00 2001 From: 1-Bart-1 Date: Sat, 6 Dec 2025 16:30:37 +0100 Subject: [PATCH 38/53] Add claude md --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 04fba002..6232db14 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -Manifest.toml +Manifest*.toml .vscode/settings.json venv results/TUDELFT_V3_LEI_KITE/polars/$C_L$ vs $C_D$.pdf @@ -9,3 +9,4 @@ results/TUDELFT_V3_LEI_KITE/polars/tutorial_testing_stall_model_n_panels_54_dist !test/data/*.bin Manifest-v1.11.toml Manifest-v1.10.toml +CLAUDE.md From ddfc633fc6a4f212bf3435022c59a059a314c44d Mon Sep 17 00:00:00 2001 From: 1-Bart-1 Date: Sat, 6 Dec 2025 16:30:59 +0100 Subject: [PATCH 39/53] Rename --- docs/src/private_functions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/private_functions.md b/docs/src/private_functions.md index d99dc45b..3646579c 100644 --- a/docs/src/private_functions.md +++ b/docs/src/private_functions.md @@ -7,5 +7,5 @@ CurrentModule = VortexStepMethod calculate_AIC_matrices! update_panel_properties! calculate_inertia_tensor -group_deform! +unrefined_deform! ``` \ No newline at end of file From 4c16cde3b539c40c5ed21e444db0cf6f2b2829cf Mon Sep 17 00:00:00 2001 From: 1-Bart-1 Date: Sat, 6 Dec 2025 16:31:22 +0100 Subject: [PATCH 40/53] Simpler example, angle at wingtips --- examples/ram_air_kite.jl | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/examples/ram_air_kite.jl b/examples/ram_air_kite.jl index 600acb7a..cc180023 100644 --- a/examples/ram_air_kite.jl +++ b/examples/ram_air_kite.jl @@ -6,13 +6,13 @@ PLOT = true PRN = true USE_TEX = false DEFORM = true -LINEARIZE = true +LINEARIZE = false # Create wing geometry wing = ObjWing( joinpath("data", "ram_air_kite", "ram_air_kite_body.obj"), joinpath("data", "ram_air_kite", "ram_air_kite_foil.dat"); - n_unrefined_sections=4, + n_unrefined_sections=2, prn=PRN ) body_aero = BodyAerodynamics([wing];) @@ -20,9 +20,8 @@ println("First init") @time VortexStepMethod.reinit!(body_aero) if DEFORM - # Linear interpolation of alpha from 10° at one tip to 0° at the other println("Deform") - @time VortexStepMethod.unrefined_deform!(wing, deg2rad.([10,20,10,0]), deg2rad.([-10,0,-10,0]); smooth=true) + @time VortexStepMethod.unrefined_deform!(wing, deg2rad.([-10,0]), deg2rad.([0,0]); smooth=true) println("Deform init") @time VortexStepMethod.reinit!(body_aero; init_aero=false) end From cd1cc5b0272f4da595a475c05477db0b88b6c489 Mon Sep 17 00:00:00 2001 From: 1-Bart-1 Date: Sat, 6 Dec 2025 17:00:47 +0100 Subject: [PATCH 41/53] Add unreleased news --- NEWS.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/NEWS.md b/NEWS.md index d368160a..e7493c1b 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,27 @@ +## VortexStepMethod [Unreleased] +### Changed +- Unified `Wing` and `RamAirWing` into single `Wing` type (`RamAirWing` now alias for `ObjWing`) +- Renamed `ram_geometry.jl` to `obj_geometry.jl` +- Wing geometry uses unrefined sections with automatic panel-to-section mapping +- Consistent naming: variables ending in `_dist` are per-panel, `_unrefined_dist` per unrefined section +- `VSMSolution` field names: `panel_width_array` → `width_dist`, `alpha_array` → `alpha_dist`, etc. +- Enhanced Makie extension with `plot_combined_analysis` for combined plotting + +### Added +- `n_unrefined_sections` field in `Wing` for tracking pre-refinement sections +- `refined_panel_mapping` for automatic panel-to-section association +- Unrefined distribution fields in `VSMSolution`: `cl_unrefined_dist`, `cd_unrefined_dist`, `cm_unrefined_dist`, `alpha_unrefined_dist`, `moment_unrefined_dist` +- `PanelDistribution.NONE` for wings already refined +- Kwarg `sort_sections` for section ordering +- YAML wing deformation tests +- Unrefined distribution tests + +### Removed +- Panel grouping (replaced with unrefined section mapping) +- `PanelGroupingMethod` enum (deprecated, grouping automatic via mapping) +- `n_groups` and `grouping_method` from settings files and structs +- `n_groups` field from `WingSettings` and `SolverSettings` + ## VortexStepMethod v2.3.0 2025-10-16 ### Added - A Makie plotting extension. From 135a71394f5852a30b756923dcf3d3ff33ef34d6 Mon Sep 17 00:00:00 2001 From: 1-Bart-1 Date: Sat, 6 Dec 2025 17:01:28 +0100 Subject: [PATCH 42/53] Remove last instances of groups --- data/TUDELFT_V3_KITE/vsm_settings.yaml | 2 -- data/pyramid_model/vsm_settings.yaml | 2 -- data/ram_air_kite/vsm_settings.yaml | 7 ----- data/ram_air_kite/vsm_settings_dual.yaml | 2 -- docs/src/tips_and_tricks.md | 27 ++++++----------- examples/V3_kite.jl | 4 +-- examples/pyramid_model.jl | 4 +-- examples/rectangular_wing.jl | 2 +- examples/stall_model.jl | 4 +-- ext/VortexStepMethodMakieExt.jl | 24 +++++++-------- src/VortexStepMethod.jl | 17 +---------- src/obj_geometry.jl | 25 +++------------- src/settings.jl | 8 +---- src/wing_geometry.jl | 23 ++------------ src/yaml_geometry.jl | 30 +++---------------- test/plotting/test_plotting.jl | 24 +++++++-------- test/ram_geometry/test_kite_geometry.jl | 2 +- .../test_yaml_wing_deformation.jl | 2 +- 18 files changed, 54 insertions(+), 155 deletions(-) diff --git a/data/TUDELFT_V3_KITE/vsm_settings.yaml b/data/TUDELFT_V3_KITE/vsm_settings.yaml index cef41285..9f185704 100644 --- a/data/TUDELFT_V3_KITE/vsm_settings.yaml +++ b/data/TUDELFT_V3_KITE/vsm_settings.yaml @@ -30,7 +30,6 @@ # NEWTON: Newton-Raphson nonlinear solver # # USAGE NOTES: -# - n_panels should be divisible by n_groups for proper load balancing # - Higher n_panels improves accuracy but increases computation time # - Lower relaxation_factor if convergence issues occur @@ -46,7 +45,6 @@ wings: - name: V3_Kite # Wing identifier for output labeling geometry_file: data/TUDELFT_V3_KITE/aero_geometry.yaml n_panels: 36 # Total number of panels along wingspan - n_groups: 1 # Number of panel groups (must divide n_panels) spanwise_panel_distribution: LINEAR # Panel spacing algorithm spanwise_direction: [0.0, 1.0, 0.0] # Unit vector defining wingspan direction remove_nan: true # Remove NaN values from polar data diff --git a/data/pyramid_model/vsm_settings.yaml b/data/pyramid_model/vsm_settings.yaml index 93ba2ca8..5ab7053d 100644 --- a/data/pyramid_model/vsm_settings.yaml +++ b/data/pyramid_model/vsm_settings.yaml @@ -30,7 +30,6 @@ # NEWTON: Newton-Raphson nonlinear solver # # USAGE NOTES: -# - n_panels should be divisible by n_groups for proper load balancing # - Higher n_panels improves accuracy but increases computation time # - Lower relaxation_factor if convergence issues occur @@ -46,7 +45,6 @@ wings: - name: V3_Kite # Wing identifier for output labeling geometry_file: data/pyramid_model/wing_geometry.yaml n_panels: 2 # Total number of panels along wingspan - n_groups: 1 # Number of panel groups (must divide n_panels) spanwise_panel_distribution: LINEAR # Panel spacing algorithm spanwise_direction: [0.0, 1.0, 0.0] # Unit vector defining wingspan direction remove_nan: true # Remove NaN values from polar data diff --git a/data/ram_air_kite/vsm_settings.yaml b/data/ram_air_kite/vsm_settings.yaml index 8f726240..0726e7f8 100644 --- a/data/ram_air_kite/vsm_settings.yaml +++ b/data/ram_air_kite/vsm_settings.yaml @@ -10,21 +10,14 @@ PanelDistribution: InitialGammaDistribution: ELLIPTIC: Elliptic distribution ZEROS: Constant distribution -PanelGroupingMethod: - EQUAL_SIZE: Divide panels into equally-sized sequential groups - REFINE: Group refined panels by their original unrefined section - wings: - name: main_wing n_panels: 40 - n_groups: 40 spanwise_panel_distribution: LINEAR spanwise_direction: [0.0, 1.0, 0.0] - grouping_method: EQUAL_SIZE remove_nan: true solver_settings: n_panels: 40 - n_groups: 40 aerodynamic_model_type: VSM density: 1.225 # air density [kg/m³] max_iterations: 1500 diff --git a/data/ram_air_kite/vsm_settings_dual.yaml b/data/ram_air_kite/vsm_settings_dual.yaml index 307cf14d..5c4a9359 100644 --- a/data/ram_air_kite/vsm_settings_dual.yaml +++ b/data/ram_air_kite/vsm_settings_dual.yaml @@ -14,13 +14,11 @@ InitialGammaDistribution: wings: - name: main_wing n_panels: 40 - n_groups: 40 spanwise_panel_distribution: LINEAR spanwise_direction: [0.0, 1.0, 0.0] remove_nan: true - name: tail n_panels: 20 - n_groups: 20 spanwise_panel_distribution: COSINE spanwise_direction: [0.0, 1.1, 0.0] remove_nan: false diff --git a/docs/src/tips_and_tricks.md b/docs/src/tips_and_tricks.md index 63cf87d3..6e8d532f 100644 --- a/docs/src/tips_and_tricks.md +++ b/docs/src/tips_and_tricks.md @@ -10,37 +10,28 @@ The following bodies can be simulated: To build the geometry of a RAM-air kite, a 3D .obj file can be used as input. In addition a `.dat` file is needed. It should have two columns, one for the `x` and one for the `y` coordinate of the 2D polar that is used. -## Panel Grouping Methods -When creating a wing, you can specify how panels should be grouped for moment and force calculations using the `grouping_method` parameter. Two methods are available: +## Unrefined Section Distribution +When creating a wing, panel forces and moments are automatically computed for each unrefined section. The unrefined sections correspond to the original geometry sections you define, while panels represent the refined mesh used for aerodynamic calculations. -### EQUAL_SIZE (Default) -Divides refined panels into equally-sized sequential groups. This is the original behavior. +### How It Works +The solver automatically tracks which panels belong to which original unrefined section. After refinement (e.g., splitting each section into multiple panels), the aerodynamic forces and moments are aggregated back to the unrefined section level. ```julia -wing = Wing(40; n_groups=4, grouping_method=EQUAL_SIZE) -``` - -In this example, with 40 panels and 4 groups, each group will contain 10 consecutive panels (panels 1-10, 11-20, 21-30, 31-40). - -### REFINE -Groups refined panels back to their original unrefined section. This is useful when you want group moments and forces to represent the original wing structure, regardless of panel refinement. - -```julia -# Create wing with 4 unrefined sections (3 panels) -wing = Wing(40; n_groups=3, grouping_method=REFINE) +# Create wing with 4 sections, refined to 40 panels +wing = Wing(40) add_section!(wing, [0, 5, 0], [1, 5, 0], INVISCID) # Section 1 add_section!(wing, [0, 2.5, 0], [1, 2.5, 0], INVISCID) # Section 2 add_section!(wing, [0, 0, 0], [1, 0, 0], INVISCID) # Section 3 add_section!(wing, [0, -5, 0], [1, -5, 0], INVISCID) # Section 4 ``` -**Important:** When using `REFINE`, `n_groups` must equal the number of unrefined panels (number of sections - 1). The solver will automatically map each refined panel to its closest original unrefined panel and sum their moments and forces accordingly. +The 40 panels are distributed across the 3 unrefined panels (sections 1-2, 2-3, 3-4). Forces and moments are computed per panel during the solve, then automatically aggregated to the 3 unrefined panels for output. -This is particularly useful for: +This approach is useful for: - LEI kites where you want loads per rib - Wings with discrete control surfaces - Cases where physical structure doesn't align with uniform panel distribution -- Dynamic simulations where you have fewer structural segments than panels needed for accurate VSM aerodynamics. For example, a 6-segment structural model can be combined with 40-panel aerodynamics by using `n_groups=6` and `grouping_method=REFINE` to map aerodynamic loads back to the structural segments. +- Dynamic simulations where you have fewer structural segments than panels needed for accurate VSM aerodynamics. For example, a 6-segment structural model can be combined with 40-panel aerodynamics, with loads automatically mapped back to the 6 structural segments. ## RAM-air kite model If running the example `ram_air_kite.jl` fails, try to run the `cleanup.jl` script and then try again. Background: this example caches the calculated polars. Reading cached polars can fail after an update. diff --git a/examples/V3_kite.jl b/examples/V3_kite.jl index 4833c776..560fd98d 100644 --- a/examples/V3_kite.jl +++ b/examples/V3_kite.jl @@ -50,7 +50,7 @@ PLOT && plot_polars( side_slip=sideslip_deg, v_a=wind_speed, title="$(wing.n_panels)_panels_$(wing.spanwise_distribution)_from_yaml_settings", - data_type=".pdf", + data_type=".png", is_save=false, is_show=true, use_tex=USE_TEX @@ -80,7 +80,7 @@ PLOT && plot_distribution( [results], ["VSM"]; title="CAD_spanwise_distributions_alpha_$(round(angle_of_attack_deg, digits=1))_delta_$(round(sideslip_deg, digits=1))_yaw_$(round(yaw_rate, digits=1))_v_a_$(round(wind_speed, digits=1))", - data_type=".pdf", + data_type=".png", is_save=false, is_show=true, use_tex=USE_TEX diff --git a/examples/pyramid_model.jl b/examples/pyramid_model.jl index a67d8bb5..43d4220a 100644 --- a/examples/pyramid_model.jl +++ b/examples/pyramid_model.jl @@ -39,7 +39,7 @@ PLOT && plot_polars( side_slip=sideslip_deg, v_a=wind_speed, title="$(wing.n_panels)_panels_$(wing.spanwise_distribution)_pyramid_model", - data_type=".pdf", + data_type=".png", is_save=false, is_show=true, use_tex=USE_TEX @@ -67,7 +67,7 @@ PLOT && plot_distribution( [results], ["VSM"]; title="pyramid_spanwise_distributions_alpha_$(round(angle_of_attack_deg, digits=1))_delta_$(round(sideslip_deg, digits=1))_yaw_$(round(yaw_rate, digits=1))_v_a_$(round(wind_speed, digits=1))", - data_type=".pdf", + data_type=".png", is_save=false, is_show=true, use_tex=USE_TEX diff --git a/examples/rectangular_wing.jl b/examples/rectangular_wing.jl index 360eb5ce..330de483 100644 --- a/examples/rectangular_wing.jl +++ b/examples/rectangular_wing.jl @@ -57,7 +57,7 @@ println("Projected area = $(round(results_vsm["projected_area"], digits=4)) m²" PLOT && plot_geometry( body_aero, "Rectangular_wing_geometry"; - data_type=".pdf", + data_type=".png", save_path=".", is_save=false, is_show=true, diff --git a/examples/stall_model.jl b/examples/stall_model.jl index 510fe094..b6d01e92 100644 --- a/examples/stall_model.jl +++ b/examples/stall_model.jl @@ -89,7 +89,7 @@ PLOT && plot_distribution( [results, results_with_stall], ["VSM", "VSM with stall correction"]; title="CAD_spanwise_distributions_alpha_$(round(aoa, digits=1))_delta_$(round(side_slip, digits=1))_yaw_$(round(yaw_rate, digits=1))_v_a_$(round(v_a, digits=1))", - data_type=".pdf", + data_type=".png", save_path=joinpath(save_folder, "spanwise_distributions"), is_save=false, is_show=true, @@ -123,7 +123,7 @@ PLOT && plot_polars( side_slip=0, v_a=10, title="tutorial_testing_stall_model_n_panels_$(n_panels)_distribution_$(spanwise_distribution)_unrefined_$(CAD_wing.n_unrefined_sections)", - data_type=".pdf", + data_type=".png", save_path=joinpath(save_folder, "polars"), is_save=true, is_show=true, diff --git a/ext/VortexStepMethodMakieExt.jl b/ext/VortexStepMethodMakieExt.jl index 1f93c4f7..badf35c2 100644 --- a/ext/VortexStepMethodMakieExt.jl +++ b/ext/VortexStepMethodMakieExt.jl @@ -237,7 +237,7 @@ function Makie.plot(body_aero::VortexStepMethod.BodyAerodynamics; size = (1200, end """ - save_plot(fig, save_path, title; data_type=".pdf") + save_plot(fig, save_path, title; data_type=".png") Save a Makie figure to a file. @@ -247,9 +247,9 @@ Save a Makie figure to a file. - `title`: Title of the plot # Keyword arguments -- `data_type`: File extension (default: ".pdf") +- `data_type`: File extension (default: ".png", also supports ".jpeg") """ -function VortexStepMethod.save_plot(fig, save_path, title; data_type=".pdf") +function VortexStepMethod.save_plot(fig, save_path, title; data_type=".png") isnothing(save_path) && throw(ArgumentError("save_path should be provided")) !isdir(save_path) && mkpath(save_path) @@ -434,7 +434,7 @@ end """ plot_geometry(body_aero::BodyAerodynamics, title; - data_type=".pdf", save_path=nothing, + data_type=".png", save_path=nothing, is_save=false, is_show=false, view_elevation=15, view_azimuth=-120, use_tex=false) @@ -445,7 +445,7 @@ Plot wing geometry from different viewpoints using Makie. - `title`: plot title # Keyword arguments: -- `data_type`: File extension (default: ".pdf") +- `data_type`: File extension (default: ".png", also supports ".jpeg") - `save_path`: Path for saving (default: nothing) - `is_save`: Whether to save (default: false) - `is_show`: Whether to display (default: false) @@ -454,7 +454,7 @@ Plot wing geometry from different viewpoints using Makie. - `use_tex`: Ignored for Makie (default: false) """ function VortexStepMethod.plot_geometry(body_aero::BodyAerodynamics, title; - data_type=".pdf", + data_type=".png", save_path=nothing, is_save=false, is_show=false, @@ -491,7 +491,7 @@ end """ plot_distribution(y_coordinates_list, results_list, label_list; - title="spanwise_distribution", data_type=".pdf", + title="spanwise_distribution", data_type=".png", save_path=nothing, is_save=false, is_show=true, use_tex=false) Plot spanwise distributions of aerodynamic properties using Makie. @@ -503,7 +503,7 @@ Plot spanwise distributions of aerodynamic properties using Makie. # Keyword arguments - `title`: Plot title (default: "spanwise_distribution") -- `data_type`: File extension (default: ".pdf") +- `data_type`: File extension (default: ".png", also supports ".jpeg") - `save_path`: Path to save plots (default: nothing) - `is_save`: Whether to save (default: false) - `is_show`: Whether to display (default: true) @@ -511,7 +511,7 @@ Plot spanwise distributions of aerodynamic properties using Makie. """ function VortexStepMethod.plot_distribution(y_coordinates_list, results_list, label_list; title="spanwise_distribution", - data_type=".pdf", + data_type=".png", save_path=nothing, is_save=false, is_show=true, @@ -703,7 +703,7 @@ end literature_path_list=String[], angle_range=range(0, 20, 2), angle_type="angle_of_attack", angle_of_attack=0.0, side_slip=0.0, v_a=10.0, - title="polar", data_type=".pdf", save_path=nothing, + title="polar", data_type=".png", save_path=nothing, is_save=true, is_show=true, use_tex=false) Plot polar data comparing different solvers using Makie. @@ -721,7 +721,7 @@ Plot polar data comparing different solvers using Makie. - `side_slip`: Side slip angle [rad] (default: 0.0) - `v_a`: Wind speed [m/s] (default: 10.0) - `title`: Plot title -- `data_type`: File extension (default: ".pdf") +- `data_type`: File extension (default: ".png", also supports ".jpeg") - `save_path`: Path to save (default: nothing) - `is_save`: Whether to save (default: true) - `is_show`: Whether to display (default: true) @@ -738,7 +738,7 @@ function VortexStepMethod.plot_polars( side_slip=0.0, v_a=10.0, title="polar", - data_type=".pdf", + data_type=".png", save_path=nothing, is_save=true, is_show=true, diff --git a/src/VortexStepMethod.jl b/src/VortexStepMethod.jl index 92710573..e4c730cd 100644 --- a/src/VortexStepMethod.jl +++ b/src/VortexStepMethod.jl @@ -36,8 +36,7 @@ export calculate_span, calculate_projected_area export MVec3 export Model, VSM, LLT export AeroModel, LEI_AIRFOIL_BREUKELS, POLAR_VECTORS, POLAR_MATRICES, INVISCID -export PanelDistribution, LINEAR, COSINE, COSINE_VAN_GARREL, SPLIT_PROVIDED, UNCHANGED -export PanelGroupingMethod, EQUAL_SIZE, REFINE +export PanelDistribution, LINEAR, COSINE, COSINE_VAN_GARREL, SPLIT_PROVIDED, UNCHANGED, NONE export InitialGammaDistribution, ELLIPTIC, ZEROS export SolverStatus, FEASIBLE, INFEASIBLE, FAILURE export SolverType, LOOP, NONLIN @@ -143,20 +142,6 @@ Enumeration of the implemented panel distributions. NONE # No refinement - sections already refined end -""" - PanelGroupingMethod EQUAL_SIZE REFINE - -**DEPRECATED**: This enum is deprecated and no longer used. -Grouping is now automatically handled via unrefined section mapping. - -Enumeration of methods for grouping panels (legacy). - -# Elements -- EQUAL_SIZE: (Deprecated) Divide panels into equally-sized sequential groups -- REFINE: (Deprecated) Group refined panels back to their original unrefined section -""" -@enum PanelGroupingMethod EQUAL_SIZE REFINE - """ InitialGammaDistribution ELLIPTIC ZEROS diff --git a/src/obj_geometry.jl b/src/obj_geometry.jl index c77e122e..b89874e5 100644 --- a/src/obj_geometry.jl +++ b/src/obj_geometry.jl @@ -392,8 +392,7 @@ The resulting Wing supports deformation through unrefined_deform! and deform! fu - `wind_vel=10.0`: Reference wind velocity for XFoil analysis (m/s) - `mass=1.0`: Wing mass (kg) - `n_panels=56`: Number of aerodynamic panels across wingspan -- `n_groups=4`: Number of control groups for deformation -- `n_sections=n_panels+1`: Number of spanwise cross-sections +- `n_unrefined_sections`: Number of unrefined sections for deformation control (default: inferred from geometry) - `align_to_principal=false`: Align body frame to principal axes of inertia - `spanwise_distribution=UNCHANGED`: Panel distribution type (forced to UNCHANGED for ObjWing) - `remove_nan=true`: Interpolate NaN values in aerodynamic data @@ -412,7 +411,7 @@ wing = ObjWing( "path/to/airfoil.dat"; mass=1.5, n_panels=40, - n_groups=4 + n_unrefined_sections=4 ) # Apply deformation @@ -422,28 +421,12 @@ unrefined_deform!(wing, deg2rad.([5, 10, 5, 0]), deg2rad.([-5, 0, -5, 0])) function ObjWing( obj_path, dat_path; crease_frac=0.9, wind_vel=10., mass=1.0, - n_panels=56, n_sections=nothing, n_unrefined_sections=nothing, n_groups=nothing, + n_panels=56, n_unrefined_sections=nothing, spanwise_distribution=UNCHANGED, spanwise_direction=[0.0, 1.0, 0.0], remove_nan=true, align_to_principal=false, alpha_range=deg2rad.(-5:1:20), delta_range=deg2rad.(-5:1:20), prn=true, - interp_steps=n_panels+1, grouping_method::PanelGroupingMethod=EQUAL_SIZE + interp_steps=n_panels+1 ) - # Handle deprecated parameters - if !isnothing(n_groups) - if !isnothing(n_unrefined_sections) - error("Cannot specify both n_groups and n_unrefined_sections. Use n_unrefined_sections only.") - end - @warn "Parameter n_groups is deprecated. Use n_unrefined_sections instead." maxlog=1 - n_unrefined_sections = n_groups - end - - if !isnothing(n_sections) - @warn "Parameter n_sections is deprecated. It is now always n_panels+1 for refined sections. Use n_unrefined_sections to control initial sections." maxlog=1 - end - - if grouping_method != EQUAL_SIZE - @warn "Parameter grouping_method is deprecated and ignored. Grouping is now always by unrefined sections." maxlog=1 - end # Force NONE distribution for ObjWing if spanwise_distribution != NONE diff --git a/src/settings.jl b/src/settings.jl index 4010b8d9..9c6bd81f 100644 --- a/src/settings.jl +++ b/src/settings.jl @@ -11,16 +11,13 @@ end obj_file::String = "" # path to .obj geometry file dat_file::String = "" # path to .dat airfoil file n_panels::Int64 = 40 - n_groups::Int64 = 40 spanwise_panel_distribution::PanelDistribution = LINEAR spanwise_direction::MVec3 = [0.0, 1.0, 0.0] - grouping_method::PanelGroupingMethod = EQUAL_SIZE remove_nan = true end @with_kw mutable struct SolverSettings n_panels::Int64 = 40 - n_groups::Int64 = 40 aerodynamic_model_type::Model = VSM solver_type::String = "LOOP" # type of solver density::Float64 = 1.225 # air density [kg/m³] @@ -63,8 +60,7 @@ function VSMSettings(filename; data_prefix=true) # Convert wing settings manually due to enum conversions n_panels = 0 - n_groups = 0 - + if haskey(data, "wings") for wing_data in data["wings"] wing = WingSettings() @@ -92,7 +88,6 @@ function VSMSettings(filename; data_prefix=true) push!(vsm_settings.wings, wing) n_panels += wing.n_panels - # n_unrefined_sections will be set when wing is created/initialized end end @@ -119,7 +114,6 @@ function VSMSettings(filename; data_prefix=true) # Override with calculated totals vsm_settings.solver_settings.n_panels = n_panels - vsm_settings.solver_settings.n_groups = n_groups end return vsm_settings diff --git a/src/wing_geometry.jl b/src/wing_geometry.jl index 2345bd46..3a11fb08 100644 --- a/src/wing_geometry.jl +++ b/src/wing_geometry.jl @@ -255,11 +255,9 @@ end """ Wing(n_panels::Int; n_unrefined_sections=nothing, - n_groups=nothing, spanwise_distribution::PanelDistribution=LINEAR, spanwise_direction::PosVector=MVec3([0.0, 1.0, 0.0]), - remove_nan::Bool=true, - grouping_method::PanelGroupingMethod=EQUAL_SIZE) + remove_nan::Bool=true) Constructor for a [Wing](@ref) struct with default values that initializes the sections and refined sections as empty arrays. Creates a basic wing suitable for YAML-based construction. @@ -267,32 +265,15 @@ and refined sections as empty arrays. Creates a basic wing suitable for YAML-bas # Parameters - `n_panels::Int`: Number of panels in aerodynamic mesh - `n_unrefined_sections::Int`: Number of unrefined sections (inferred from added sections for YAML wings) -- `n_groups::Int`: DEPRECATED - use n_unrefined_sections instead - `spanwise_distribution`::PanelDistribution = LINEAR: [PanelDistribution](@ref) - `spanwise_direction::MVec3` = MVec3([0.0, 1.0, 0.0]): Wing span direction vector - `remove_nan::Bool`: Wether to remove the NaNs from interpolations or not -- `grouping_method::PanelGroupingMethod` = EQUAL_SIZE: DEPRECATED - grouping is now always by unrefined sections """ function Wing(n_panels::Int; n_unrefined_sections=nothing, - n_groups=nothing, spanwise_distribution::PanelDistribution=LINEAR, spanwise_direction::PosVector=MVec3([0.0, 1.0, 0.0]), - remove_nan=true, - grouping_method::PanelGroupingMethod=EQUAL_SIZE) - - # Handle deprecated parameters - if !isnothing(n_groups) - if !isnothing(n_unrefined_sections) - error("Cannot specify both n_groups and n_unrefined_sections. Use n_unrefined_sections only.") - end - @warn "Parameter n_groups is deprecated. Use n_unrefined_sections instead." maxlog=1 - n_unrefined_sections = n_groups - end - - if grouping_method != EQUAL_SIZE - @warn "Parameter grouping_method is deprecated and ignored. Grouping is now always by unrefined sections." maxlog=1 - end + remove_nan=true) # For YAML wings, n_unrefined_sections will be set when sections are added # Set to 0 as placeholder for now diff --git a/src/yaml_geometry.jl b/src/yaml_geometry.jl index a24492e2..60571026 100644 --- a/src/yaml_geometry.jl +++ b/src/yaml_geometry.jl @@ -148,7 +148,7 @@ function load_polar_data(csv_file_path::String) end """ - Wing(geometry_file::String; n_panels=20, n_groups=1, spanwise_distribution=LINEAR, + Wing(geometry_file::String; n_panels=20, spanwise_distribution=LINEAR, spanwise_direction=[0.0, 1.0, 0.0], remove_nan=true, prn=false) Constructs a `Wing` object from a YAML geometry file. @@ -158,7 +158,6 @@ Constructs a `Wing` object from a YAML geometry file. # Keyword Arguments - `n_panels::Int`: Number of spanwise panels (default: 20). -- `n_groups::Int`: Number of grouped sections across the span (default: 1). Must divide `n_panels`. - `spanwise_distribution`: Spanwise panel distribution type (default: `LINEAR`). - `spanwise_direction::Vector{Float64}`: Direction of the spanwise axis (default: `[0.0, 1.0, 0.0]`). Must be the global Y axis. - `remove_nan::Bool`: Remove NaN values from the geometry (default: `true`). @@ -171,43 +170,24 @@ Constructs a `Wing` object from a YAML geometry file. This function reads a YAML configuration file to define the geometry and airfoil data for a multi-section wing. Each section and corresponding airfoil is parsed from the YAML file, polar data is loaded, and each section is added to the wing. The geometry logic currently assumes the spanwise direction is `[0.0, 1.0, 0.0]` (aligned with the global Y axis). +The number of unrefined sections is automatically inferred from the sections in the geometry file. # Errors -- Throws an `ArgumentError` if `n_panels` is not divisible by `n_groups`. - Throws an `ArgumentError` if `spanwise_direction` is not `[0.0, 1.0, 0.0]`. # Example ```julia -wing = Wing("wing_geometry.yaml"; n_panels=30, n_groups=2, prn=true) +wing = Wing("wing_geometry.yaml"; n_panels=30, prn=true) ``` """ function Wing( geometry_file::String; n_panels=20, - n_unrefined_sections=nothing, - n_groups=nothing, spanwise_distribution=LINEAR, spanwise_direction=[0.0, 1.0, 0.0], remove_nan=true, - prn=false, - grouping_method::PanelGroupingMethod=EQUAL_SIZE + prn=false ) - # Handle deprecated parameters - if !isnothing(n_groups) - if !isnothing(n_unrefined_sections) - error("Cannot specify both n_groups and n_unrefined_sections. Use n_unrefined_sections only.") - end - @warn "Parameter n_groups is deprecated. For YAML wings, n_unrefined_sections is inferred from added sections." maxlog=1 - end - - if grouping_method != EQUAL_SIZE - @warn "Parameter grouping_method is deprecated and ignored. Grouping is now always by unrefined sections." maxlog=1 - end - - # For YAML wings, n_unrefined_sections is inferred from the number of sections added - if !isnothing(n_unrefined_sections) - @warn "For YAML wings, n_unrefined_sections is automatically inferred from the sections in the geometry file. The parameter is ignored." maxlog=1 - end !isapprox(spanwise_direction, [0.0, 1.0, 0.0]) && throw(ArgumentError("Spanwise direction has to be [0.0, 1.0, 0.0], not $spanwise_direction")) @@ -348,7 +328,6 @@ function Wing(settings::VSMSettings) # Use YAML geometry constructor Wing(wing_settings.geometry_file; n_panels=wing_settings.n_panels, - n_groups=wing_settings.n_groups, spanwise_distribution=wing_settings.spanwise_panel_distribution, remove_nan=wing_settings.remove_nan ) @@ -358,7 +337,6 @@ function Wing(settings::VSMSettings) wing_settings.obj_file, wing_settings.dat_file; n_panels=wing_settings.n_panels, - n_groups=wing_settings.n_groups, spanwise_distribution=wing_settings.spanwise_panel_distribution, spanwise_direction=wing_settings.spanwise_direction, remove_nan=wing_settings.remove_nan diff --git a/test/plotting/test_plotting.jl b/test/plotting/test_plotting.jl index eba66aa2..0bcfc6cc 100644 --- a/test/plotting/test_plotting.jl +++ b/test/plotting/test_plotting.jl @@ -70,19 +70,19 @@ end fig = plot_geometry( body_aero, "Rectangular_wing_geometry"; - data_type=".pdf", + data_type=".png", save_path=save_dir, is_save=true, is_show=false) @test fig isa Figure - @test isfile(joinpath(save_dir, "Rectangular_wing_geometry_angled_view.pdf")) - safe_rm(joinpath(save_dir, "Rectangular_wing_geometry_angled_view.pdf")) - @test isfile(joinpath(save_dir, "Rectangular_wing_geometry_front_view.pdf")) - safe_rm(joinpath(save_dir, "Rectangular_wing_geometry_front_view.pdf")) - @test isfile(joinpath(save_dir, "Rectangular_wing_geometry_side_view.pdf")) - safe_rm(joinpath(save_dir, "Rectangular_wing_geometry_side_view.pdf")) - @test isfile(joinpath(save_dir, "Rectangular_wing_geometry_top_view.pdf")) - safe_rm(joinpath(save_dir, "Rectangular_wing_geometry_top_view.pdf")) + @test isfile(joinpath(save_dir, "Rectangular_wing_geometry_angled_view.png")) + safe_rm(joinpath(save_dir, "Rectangular_wing_geometry_angled_view.png")) + @test isfile(joinpath(save_dir, "Rectangular_wing_geometry_front_view.png")) + safe_rm(joinpath(save_dir, "Rectangular_wing_geometry_front_view.png")) + @test isfile(joinpath(save_dir, "Rectangular_wing_geometry_side_view.png")) + safe_rm(joinpath(save_dir, "Rectangular_wing_geometry_side_view.png")) + @test isfile(joinpath(save_dir, "Rectangular_wing_geometry_top_view.png")) + safe_rm(joinpath(save_dir, "Rectangular_wing_geometry_top_view.png")) # Step 5: Initialize the solvers vsm_solver = Solver(body_aero; aerodynamic_model_type=VSM) @@ -114,14 +114,14 @@ end angle_type="angle_of_attack", v_a=v_a, title="Rectangular Wing Polars", - data_type=".pdf", + data_type=".png", save_path=save_dir, is_save=true, is_show=false ) @test fig isa Figure - @test isfile(joinpath(save_dir, "Rectangular_Wing_Polars.pdf")) - safe_rm(joinpath(save_dir, "Rectangular_Wing_Polars.pdf")) + @test isfile(joinpath(save_dir, "Rectangular_Wing_Polars.png")) + safe_rm(joinpath(save_dir, "Rectangular_Wing_Polars.png")) # Step 9: Test polar data plotting body_aero = BodyAerodynamics([ram_wing]) diff --git a/test/ram_geometry/test_kite_geometry.jl b/test/ram_geometry/test_kite_geometry.jl index 3058af89..894664d2 100644 --- a/test/ram_geometry/test_kite_geometry.jl +++ b/test/ram_geometry/test_kite_geometry.jl @@ -169,7 +169,7 @@ using Serialization wing = ObjWing(test_obj_path, test_dat_path; remove_nan=true) @test wing.n_panels == 56 # Default value - @test wing.spanwise_distribution == UNCHANGED + @test wing.spanwise_distribution == NONE @test wing.spanwise_direction ≈ [0.0, 1.0, 0.0] @test length(wing.unrefined_sections) > 0 # Should have sections now @test wing.mass ≈ 1.0 diff --git a/test/yaml_geometry/test_yaml_wing_deformation.jl b/test/yaml_geometry/test_yaml_wing_deformation.jl index d483835a..199d6bc9 100644 --- a/test/yaml_geometry/test_yaml_wing_deformation.jl +++ b/test/yaml_geometry/test_yaml_wing_deformation.jl @@ -90,7 +90,7 @@ using Test @test body_aero.panels[1].delta < body_aero.panels[end].delta # Reset and verify - VortexStepMethod.deform!(wing, zeros(wing.n_unrefined_sections), zeros(wing.n_unrefined_sections)) + VortexStepMethod.deform!(wing, zeros(wing.n_panels), zeros(wing.n_panels)) VortexStepMethod.reinit!(body_aero; refine_mesh=false) for (idx, i) in enumerate(test_indices) From c9febb966eb21a60a67195c0ddbfa37566be6700 Mon Sep 17 00:00:00 2001 From: 1-Bart-1 Date: Sat, 6 Dec 2025 17:05:41 +0100 Subject: [PATCH 43/53] Removed groups --- test/settings/test_settings.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/settings/test_settings.jl b/test/settings/test_settings.jl index 68566cae..42fb5e5b 100644 --- a/test/settings/test_settings.jl +++ b/test/settings/test_settings.jl @@ -11,6 +11,6 @@ using Test @test vss.wings isa Vector{WingSettings} @test length(vss.wings) == 2 io = IOBuffer(repr(vss)) - @test countlines(io) == 47 # Updated to match new output format + @test countlines(io) == 42 # Updated to match new output format end nothing From b4a90f8caaeffdd9b286dbd5e70c2788688c1f64 Mon Sep 17 00:00:00 2001 From: 1-Bart-1 Date: Sat, 6 Dec 2025 19:53:16 +0100 Subject: [PATCH 44/53] Add tests for smoothing and non smoothing deform --- data/ram_air_kite/vsm_settings.yaml | 2 +- .../test_yaml_wing_deformation.jl | 66 +++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/data/ram_air_kite/vsm_settings.yaml b/data/ram_air_kite/vsm_settings.yaml index 0726e7f8..92372c10 100644 --- a/data/ram_air_kite/vsm_settings.yaml +++ b/data/ram_air_kite/vsm_settings.yaml @@ -13,7 +13,7 @@ InitialGammaDistribution: wings: - name: main_wing n_panels: 40 - spanwise_panel_distribution: LINEAR + spanwise_panel_distribution: NONE spanwise_direction: [0.0, 1.0, 0.0] remove_nan: true solver_settings: diff --git a/test/yaml_geometry/test_yaml_wing_deformation.jl b/test/yaml_geometry/test_yaml_wing_deformation.jl index 199d6bc9..bf263cc5 100644 --- a/test/yaml_geometry/test_yaml_wing_deformation.jl +++ b/test/yaml_geometry/test_yaml_wing_deformation.jl @@ -235,4 +235,70 @@ using Test @test body_aero.panels[i].delta ≈ expected_delta atol=1e-6 end end + + @testset "Smooth vs Non-Smooth Deformation" begin + # Create test wing with 2 unrefined sections, refined to 40 panels + simple_wing_file = test_data_path("yaml_geometry", "simple_wing.yaml") + wing = Wing(simple_wing_file; n_panels=40) + @test wing.n_unrefined_sections == 2 + + # Define varying input angles at unrefined section level + delta_input = deg2rad.([0.0, 10.0]) + + # Test 1: Non-smooth deformation has step-wise discontinuities + VortexStepMethod.unrefined_deform!(wing, nothing, delta_input; smooth=false) + delta_nonsmooth = copy(wing.delta_dist) + + # Verify step-wise pattern: panels in same unrefined section have identical angles + for i in 1:wing.n_panels + unrefined_idx = wing.refined_panel_mapping[i] + @test delta_nonsmooth[i] ≈ delta_input[unrefined_idx] atol=1e-10 + end + + # Verify discontinuities exist at unrefined section boundaries + # Find boundary indices (where panel mapping changes) + max_gradient_nonsmooth = 0.0 + for i in 1:(wing.n_panels-1) + if wing.refined_panel_mapping[i] != wing.refined_panel_mapping[i+1] + gradient = abs(delta_nonsmooth[i+1] - delta_nonsmooth[i]) + max_gradient_nonsmooth = max(max_gradient_nonsmooth, gradient) + end + end + @test max_gradient_nonsmooth > deg2rad(5.0) # Should have large jumps + + # Test 2: Smooth deformation is continuous + VortexStepMethod.unrefined_deform!(wing, nothing, delta_input; smooth=true) + delta_smooth = copy(wing.delta_dist) + + # Verify gradients between adjacent panels are small + max_gradient_smooth = maximum(abs.(diff(delta_smooth))) + @test max_gradient_smooth < deg2rad(3.0) # Should be smooth + + # Verify no sharp discontinuities + for i in 1:(wing.n_panels-1) + @test abs(delta_smooth[i+1] - delta_smooth[i]) < deg2rad(3.0) + end + + # Test 3: Smoothing reduces maximum gradient + @test max_gradient_smooth < max_gradient_nonsmooth + + # Test 4: Angles match input at unrefined section centers (both modes) + # For non-smooth: extract angle at center panel of each unrefined section + VortexStepMethod.unrefined_deform!(wing, nothing, delta_input; smooth=false) + for i in 1:wing.n_unrefined_sections + # Find panels belonging to this unrefined section + panel_indices = findall(==(i), wing.refined_panel_mapping) + center_panel_idx = panel_indices[div(length(panel_indices), 2) + 1] + @test wing.delta_dist[center_panel_idx] ≈ delta_input[i] atol=1e-10 + end + + # For smooth: angles at center should be close to input (tolerance larger due to smoothing) + VortexStepMethod.unrefined_deform!(wing, nothing, delta_input; smooth=true) + for i in 1:wing.n_unrefined_sections + panel_indices = findall(==(i), wing.refined_panel_mapping) + center_panel_idx = panel_indices[div(length(panel_indices), 2) + 1] + # Smoothing may shift values slightly, use absolute tolerance for small angles + @test wing.delta_dist[center_panel_idx] ≈ delta_input[i] atol=deg2rad(2.0) + end + end end From cbbc31162ba985c05fd5fdaac4ecb99c5f50ae8f Mon Sep 17 00:00:00 2001 From: 1-Bart-1 Date: Sun, 7 Dec 2025 20:03:53 +0100 Subject: [PATCH 45/53] Add everything plotting function --- ext/VortexStepMethodControlPlotsExt.jl | 92 +++++++++++++++++++++++++- ext/VortexStepMethodMakieExt.jl | 14 +++- 2 files changed, 103 insertions(+), 3 deletions(-) diff --git a/ext/VortexStepMethodControlPlotsExt.jl b/ext/VortexStepMethodControlPlotsExt.jl index b2d09e9f..bbf985c7 100644 --- a/ext/VortexStepMethodControlPlotsExt.jl +++ b/ext/VortexStepMethodControlPlotsExt.jl @@ -3,7 +3,8 @@ using ControlPlots, LaTeXStrings, VortexStepMethod, LinearAlgebra, Statistics, D import ControlPlots: plt import VortexStepMethod: calculate_filaments_for_plotting -export plot_wing, plot_circulation_distribution, plot_geometry, plot_distribution, plot_polars, save_plot, show_plot, plot_polar_data +export plot_wing, plot_circulation_distribution, plot_geometry, plot_distribution, + plot_polars, save_plot, show_plot, plot_polar_data, plot_combined_analysis """ set_plot_style(titel_size=16; use_tex=false) @@ -925,4 +926,93 @@ function VortexStepMethod.plot_polar_data(body_aero::BodyAerodynamics; end end +""" + plot_combined_analysis(solver, body_aero, results; kwargs...) + +Create combined analysis by calling plot_geometry, plot_distribution, +and plot_polars sequentially. Each creates a separate matplotlib window. + +# Arguments +- `solver`: Solver or array of solvers +- `body_aero`: BodyAerodynamics object or array +- `results`: Results dict or array of results dicts + +See individual functions for detailed parameter descriptions. +""" +function VortexStepMethod.plot_combined_analysis( + solver, + body_aero, + results; + solver_label="VSM", + angle_range=range(0, 20, length=20), + angle_type="angle_of_attack", + angle_of_attack=0.0, + side_slip=0.0, + v_a=10.0, + title="Combined Analysis", + data_type=".pdf", + save_path=nothing, + is_save=false, + is_show=true, + view_elevation=15, + view_azimuth=-120, + use_tex=false, + literature_path_list=String[] +) + # Normalize inputs to arrays for consistent handling + solvers = solver isa Vector ? solver : [solver] + body_aeros = body_aero isa Vector ? body_aero : [body_aero] + results_list = results isa Vector ? results : [results] + labels = solver_label isa Vector ? solver_label : [solver_label] + + # Extract y-coordinates for distribution plot (use first body_aero) + body_y_coordinates = [panel.aero_center[2] for panel in body_aeros[1].panels] + y_coords_list = [body_y_coordinates for _ in 1:length(solvers)] + + # Plot geometry (only use first body_aero) + plot_geometry( + body_aeros[1], + title; + data_type=data_type, + save_path=save_path, + is_save=is_save, + is_show=is_show, + view_elevation=view_elevation, + view_azimuth=view_azimuth, + use_tex=use_tex + ) + + # Plot spanwise distributions + plot_distribution( + y_coords_list, + results_list, + labels; + title=title * " - Distributions", + data_type=data_type, + save_path=save_path, + is_save=is_save, + is_show=is_show, + use_tex=use_tex + ) + + # Plot polars + plot_polars( + solvers, + body_aeros, + labels; + literature_path_list=literature_path_list, + angle_range=angle_range, + angle_type=angle_type, + angle_of_attack=angle_of_attack, + side_slip=side_slip, + v_a=v_a, + title=title * " - Polars", + data_type=data_type, + save_path=save_path, + is_save=is_save, + is_show=is_show, + use_tex=use_tex + ) +end + end \ No newline at end of file diff --git a/ext/VortexStepMethodMakieExt.jl b/ext/VortexStepMethodMakieExt.jl index badf35c2..0ec5e5b5 100644 --- a/ext/VortexStepMethodMakieExt.jl +++ b/ext/VortexStepMethodMakieExt.jl @@ -929,7 +929,9 @@ end angle_of_attack=0.0, side_slip=0.0, v_a=10.0, title="Combined Analysis", view_elevation=15, view_azimuth=-120, - is_show=true, use_tex=false) + is_show=true, use_tex=false, + literature_path_list=String[], + data_type=".png", save_path=nothing, is_save=false) Create combined multi-panel figure with geometry, polar data, distributions, and polars. @@ -950,6 +952,10 @@ Create combined multi-panel figure with geometry, polar data, distributions, and - `view_azimuth`: Geometry view azimuth [°] (default: -120) - `is_show`: Display figure (default: true) - `use_tex`: Ignored for Makie (default: false) +- `literature_path_list`: Paths to literature CSV files (default: String[]) +- `data_type`: File extension (default: ".png", also supports ".jpeg") +- `save_path`: Directory path to save files (default: nothing) +- `is_save`: Save plots to files (default: false) """ function VortexStepMethod.plot_combined_analysis( solver, @@ -965,7 +971,11 @@ function VortexStepMethod.plot_combined_analysis( view_elevation=15, view_azimuth=-120, is_show=true, - use_tex=false + use_tex=false, + literature_path_list=String[], + data_type=".png", + save_path=nothing, + is_save=false ) # Auto-detect screen size and use 80% of it fig = try From 66ac77da825212e7526745df3d05361f2fed8bdf Mon Sep 17 00:00:00 2001 From: 1-Bart-1 Date: Sun, 7 Dec 2025 20:06:58 +0100 Subject: [PATCH 46/53] Manual refine --- src/VortexStepMethod.jl | 10 ++-- src/body_aerodynamics.jl | 47 +++++++-------- src/obj_geometry.jl | 22 ++++--- src/precompile.jl | 1 + src/wing_geometry.jl | 120 ++++++++++++++++++++++++++------------- src/yaml_geometry.jl | 11 ++-- 6 files changed, 126 insertions(+), 85 deletions(-) diff --git a/src/VortexStepMethod.jl b/src/VortexStepMethod.jl index e4c730cd..9a3c7462 100644 --- a/src/VortexStepMethod.jl +++ b/src/VortexStepMethod.jl @@ -27,7 +27,7 @@ using Xfoil # Export public interface export VSMSettings, WingSettings, SolverSettings -export Wing, Section, ObjWing, reinit! +export Wing, Section, ObjWing, reinit!, refine! export BodyAerodynamics export Solver, solve, solve_base!, solve!, VSMSolution, linearize export calculate_results @@ -36,7 +36,7 @@ export calculate_span, calculate_projected_area export MVec3 export Model, VSM, LLT export AeroModel, LEI_AIRFOIL_BREUKELS, POLAR_VECTORS, POLAR_MATRICES, INVISCID -export PanelDistribution, LINEAR, COSINE, COSINE_VAN_GARREL, SPLIT_PROVIDED, UNCHANGED, NONE +export PanelDistribution, LINEAR, COSINE, COSINE_VAN_GARREL, SPLIT_PROVIDED, UNCHANGED export InitialGammaDistribution, ELLIPTIC, ZEROS export SolverStatus, FEASIBLE, INFEASIBLE, FAILURE export SolverType, LOOP, NONLIN @@ -130,16 +130,14 @@ Enumeration of the implemented panel distributions. - COSINE # Cosine distribution - `COSINE_VAN_GARREL` # van Garrel cosine distribution - `SPLIT_PROVIDED` # Split provided sections -- `UNCHANGED` # Keep original sections without interpolation -- `NONE` # No refinement - sections already refined +- `UNCHANGED` # 1:1 copy of unrefined to refined sections (no interpolation) """ @enum PanelDistribution begin LINEAR # Linear distribution COSINE # Cosine distribution COSINE_VAN_GARREL # van Garrel cosine distribution SPLIT_PROVIDED # Split provided sections - UNCHANGED # Keep original sections without interpolation - NONE # No refinement - sections already refined + UNCHANGED # 1:1 copy of unrefined to refined sections end """ diff --git a/src/body_aerodynamics.jl b/src/body_aerodynamics.jl index 1ca1506f..b40be910 100644 --- a/src/body_aerodynamics.jl +++ b/src/body_aerodynamics.jl @@ -71,6 +71,27 @@ function BodyAerodynamics( va=[15.0, 0.0, 0.0], omega=zeros(MVec3) ) where T <: AbstractWing + # Validate all wings are refined + for (i, wing) in enumerate(wings) + if isempty(wing.refined_sections) || + length(wing.refined_sections) != wing.n_panels + 1 + throw(ArgumentError( + "Wing $i has not been refined. " * + "Call refine!(wing) before creating BodyAerodynamics.\n\n" * + "Expected workflow:\n" * + " wing = Wing(...)\n" * + " refine!(wing)\n" * + " body_aero = BodyAerodynamics([wing])" + )) + end + + if isempty(wing.non_deformed_sections) + @warn "Wing $i has no non_deformed_sections. " * + "Deformation (unrefined_deform!) will not work. " * + "This should have been created by refine!." maxlog=1 + end + end + # Initialize panels panels = Panel[] n_unrefined_total = 0 @@ -79,17 +100,6 @@ function BodyAerodynamics( section.LE_point .-= kite_body_origin section.TE_point .-= kite_body_origin end - if wing.spanwise_distribution == NONE - # NONE distribution: refined_sections already populated in constructor - !(wing.n_panels == length(wing.refined_sections) - 1) && - throw(ArgumentError("(wing.n_panels = $(wing.n_panels)) != (length(wing.refined_sections) - 1 = $(length(wing.unrefined_sections) - 1))")) - elseif wing.spanwise_distribution == UNCHANGED - wing.refined_sections = wing.unrefined_sections - !(wing.n_panels == length(wing.unrefined_sections) - 1) && - throw(ArgumentError("(wing.n_panels = $(wing.n_panels)) != (length(wing.unrefined_sections) - 1 = $(length(wing.unrefined_sections) - 1))")) - else - wing.refined_sections = Section[Section() for _ in 1:wing.n_panels+1] - end # Create panels for _ in 1:wing.n_panels @@ -127,15 +137,9 @@ Initialize a BodyAerodynamics struct in-place by setting up panels and coefficie - `body_aero::BodyAerodynamics`: The structure to initialize # Keyword Arguments -- `init_aero::Bool`: Wether to initialize the aero data or not +- `init_aero::Bool`: Whether to initialize the aero data or not - `va=[15.0, 0.0, 0.0]`: Apparent wind vector - `omega=zeros(3)`: Turn rate in kite body frame x y and z -- `refine_mesh=true`: Whether to refine wing meshes. Set to `false` after - `deform!()` to preserve deformed geometry. -- `recompute_mapping=true`: Whether to recompute the refined panel mapping. - Set to `false` to skip mapping computation when it hasn't changed. -- `sort_sections=true`: Whether to sort sections by spanwise position. - Set to `false` for REFINE wings where section order is determined by structural connectivity. # Returns nothing @@ -143,15 +147,12 @@ nothing function reinit!(body_aero::BodyAerodynamics; init_aero=true, va=[15.0, 0.0, 0.0], - omega=zeros(MVec3), - refine_mesh=true, - recompute_mapping=true, - sort_sections=true + omega=zeros(MVec3) ) idx = 1 vec = @MVector zeros(3) for wing in body_aero.wings - reinit!(wing; refine_mesh, recompute_mapping, sort_sections) + reinit!(wing) panel_props = wing.panel_props # Create panels diff --git a/src/obj_geometry.jl b/src/obj_geometry.jl index b89874e5..bd0a56e6 100644 --- a/src/obj_geometry.jl +++ b/src/obj_geometry.jl @@ -428,15 +428,9 @@ function ObjWing( interp_steps=n_panels+1 ) - # Force NONE distribution for ObjWing - if spanwise_distribution != NONE - @warn "ObjWing only supports spanwise_distribution=NONE. Overriding to NONE." maxlog=1 - spanwise_distribution = NONE - end - # Set default: evenly spaced unrefined sections including both tips if isnothing(n_unrefined_sections) - # Default to having same number of unrefined sections as refined (no refinement) + # Default to having same number of unrefined sections as refined (no interpolation needed) n_unrefined_sections = n_panels + 1 end @@ -493,6 +487,7 @@ function ObjWing( push!(sections, Section(LE_point, TE_point, POLAR_MATRICES, aero_data)) end + # Create refined sections (evenly spaced including both tips) refined_sections = Section[] for gamma in range(-gamma_tip, gamma_tip, n_panels+1) LE_point = [le_interp[i](gamma) for i in 1:3] @@ -500,17 +495,26 @@ function ObjWing( push!(refined_sections, Section(LE_point, TE_point, POLAR_MATRICES, aero_data)) end + # Create non_deformed_sections as copy of refined_sections for deformation support + non_deformed_sections = [Section() for _ in 1:n_panels+1] + for i in 1:n_panels+1 + reinit!(non_deformed_sections[i], refined_sections[i]) + end + panel_props = PanelProperties{n_panels}() cache = [PreallocationTools.LazyBufferCache()] wing = Wing(n_panels, Int16(n_unrefined_sections), spanwise_distribution, panel_props, MVec3(spanwise_direction), sections, refined_sections, remove_nan, Int16[], - Section[], zeros(n_panels), zeros(n_panels), + non_deformed_sections, zeros(n_panels), zeros(n_panels), mass, gamma_tip, inertia_tensor, T_cad_body, R_cad_body, radius, le_interp, te_interp, area_interp, cache) - # Refine mesh and update panel properties + # Compute panel mapping for deformation support + VortexStepMethod.compute_refined_panel_mapping!(wing) + + # Update panel properties reinit!(wing) wing diff --git a/src/precompile.jl b/src/precompile.jl index cd9a44ab..aefd41f6 100644 --- a/src/precompile.jl +++ b/src/precompile.jl @@ -26,6 +26,7 @@ INVISCID) # Step 3: Initialize aerodynamics (simplified) + refine!(wing) body_aero = BodyAerodynamics([wing]) nothing diff --git a/src/wing_geometry.jl b/src/wing_geometry.jl index 3a11fb08..5f2acfa4 100644 --- a/src/wing_geometry.jl +++ b/src/wing_geometry.jl @@ -297,25 +297,20 @@ function Wing(n_panels::Int; end """ - reinit!(wing::AbstractWing; refine_mesh=true, recompute_mapping=true, sort_sections=true) + reinit!(wing::AbstractWing) -Reinitialize wing geometry and panel properties. +Reinitialize wing panel properties based on current refined_sections geometry. -# Keyword Arguments -- `refine_mesh::Bool=true`: Whether to refine the mesh. Set to `false` after - `deform!()` to preserve deformed geometry while updating panel properties. -- `recompute_mapping::Bool=true`: Whether to recompute the refined panel mapping. - Set to `false` to skip mapping computation when it hasn't changed. -- `sort_sections::Bool=true`: Whether to sort sections by spanwise position. - Set to `false` for REFINE wings where section order is determined by structural connectivity. -""" -function reinit!(wing::AbstractWing; refine_mesh=true, recompute_mapping=true, sort_sections=true) - # Refine mesh unless explicitly disabled (e.g., to preserve deformation) - if refine_mesh - refine_aerodynamic_mesh!(wing; recompute_mapping, sort_sections) - end +This function only updates panel properties (chord, area, etc.) from the existing +refined_sections. It does NOT refine the mesh - call refine_aerodynamic_mesh!(wing) +first if needed. - # Calculate panel properties +# Note +After deformation via `unrefined_deform!()` or `deform!()`, call `reinit!` to update +panel properties while preserving the deformed geometry. +""" +function reinit!(wing::AbstractWing) + # Calculate panel properties from refined sections update_panel_properties!( wing.panel_props, wing.refined_sections, @@ -620,54 +615,99 @@ end Create non_deformed_sections to match refined_sections. This enables deformation support for all wings (YAML and OBJ). -Should be called after refined_sections are populated for the FIRST time only. +Should be called after refined_sections are populated. Once populated, non_deformed_sections serves as the undeformed reference geometry. """ function update_non_deformed_sections!(wing::AbstractWing) n_sections = wing.n_panels + 1 - # Only populate non_deformed_sections if it's empty (initial setup) - # Once populated, it serves as the undeformed reference and should not be overwritten + # Populate or update non_deformed_sections if isempty(wing.non_deformed_sections) + @show length(wing.refined_sections) n_sections + # Initial setup wing.non_deformed_sections = [Section() for _ in 1:n_sections] for i in 1:n_sections reinit!(wing.non_deformed_sections[i], wing.refined_sections[i]) end + elseif length(wing.non_deformed_sections) != n_sections + # Size mismatch - error + throw(ArgumentError( + "non_deformed_sections has incorrect size. " * + "Expected $(n_sections) sections (n_panels+1), got $(length(wing.non_deformed_sections)). " * + "This indicates an inconsistent wing state." + )) + else + # Correct size - update all sections + for i in 1:n_sections + reinit!(wing.non_deformed_sections[i], wing.refined_sections[i]) + end end return nothing end """ - refine_aerodynamic_mesh!(wing::AbstractWing; recompute_mapping=true, sort_sections=true) - -Refine the aerodynamic mesh of the wing based on spanwise panel distribution. + refine!(wing::AbstractWing; recompute_mapping=true, sort_sections=true) + +Refine the wing aerodynamic mesh from unrefined sections to refined sections. + +This function interpolates the wing geometry from a coarse set of unrefined sections +to a fine mesh of refined sections (n_panels+1 sections) based on the wing's +spanwise_distribution setting. It also populates non_deformed_sections which +enables deformation support via `unrefined_deform!`. + +# Required Workflow +Must be called after wing construction and before creating `BodyAerodynamics`: +```julia +wing = Wing("wing.yaml"; n_panels=40) # or ObjWing(...) or manual Wing +refine!(wing) # Refine mesh +body_aero = BodyAerodynamics([wing]) # Create aerodynamics +``` + +# Distribution Methods +- `LINEAR`: Linear interpolation between sections +- `COSINE`: Cosine spacing (more panels near tips) +- `COSINE_VAN_GARREL`: van Garrel cosine distribution +- `SPLIT_PROVIDED`: Split each unrefined section into sub-panels +- `UNCHANGED`: 1:1 copy when n_unrefined_sections == n_panels+1 # Keyword Arguments -- `recompute_mapping::Bool=true`: Whether to recompute the refined panel mapping. - Set to `false` to skip mapping computation when it hasn't changed. -- `sort_sections::Bool=true`: Whether to sort sections by spanwise position (y-coordinate). - Set to `false` for REFINE wings where section order is determined by structural connectivity. +- `recompute_mapping::Bool=true`: Recompute the mapping from refined panels to unrefined sections +- `sort_sections::Bool=true`: Sort sections by spanwise position (disable for structural ordering) -Returns: - Vector{Section}: List of refined sections +# Effects +1. Populates `wing.refined_sections` (n_panels+1 sections) +2. Populates `wing.non_deformed_sections` (copy of refined_sections for deformation reference) +3. Computes `wing.refined_panel_mapping` (panel → unrefined section mapping) +4. Resizes `wing.theta_dist` and `wing.delta_dist` to n_panels + +# Example +```julia +# YAML wing +wing = Wing("wing.yaml"; n_panels=40) +refine!(wing) +body_aero = BodyAerodynamics([wing]) + +# After refinement, deformation is supported +unrefined_deform!(wing, theta_angles, delta_angles) +``` """ -function refine_aerodynamic_mesh!(wing::AbstractWing; recompute_mapping=true, sort_sections=true) +function refine!(wing::AbstractWing; recompute_mapping=true, sort_sections=true) + # Validate unrefined_sections exist + if isempty(wing.unrefined_sections) + throw(ArgumentError( + "Cannot refine mesh: wing has no unrefined_sections. " * + "Add sections using add_section! or check wing construction." + )) + end + # Only sort sections if requested (skip for REFINE wings with fixed structural order) sort_sections && sort!(wing.unrefined_sections, by=s -> s.LE_point[2], rev=true) n_sections = wing.n_panels + 1 - # Handle NONE distribution - sections already refined, just compute mapping - if wing.spanwise_distribution == NONE - if length(wing.refined_sections) != n_sections - throw(ArgumentError("NONE distribution requires refined_sections to be pre-populated")) - end - recompute_mapping && compute_refined_panel_mapping!(wing) - update_non_deformed_sections!(wing) - return nothing - end - if length(wing.refined_sections) == 0 - if wing.spanwise_distribution == UNCHANGED || length(wing.unrefined_sections) == n_sections + @show wing.spanwise_distribution + if wing.spanwise_distribution == UNCHANGED || + length(wing.unrefined_sections) == n_sections wing.refined_sections = wing.unrefined_sections update_non_deformed_sections!(wing) return nothing diff --git a/src/yaml_geometry.jl b/src/yaml_geometry.jl index 60571026..4df42382 100644 --- a/src/yaml_geometry.jl +++ b/src/yaml_geometry.jl @@ -246,7 +246,7 @@ function Wing( # Get coordinates directly from struct fields le_coord = [section.LE_x, section.LE_y, section.LE_z] te_coord = [section.TE_x, section.TE_y, section.TE_z] - + # Load polar data and create section csv_file_path = get(airfoil_csv_map, section.airfoil_id, "") if !isempty(csv_file_path) && !isabspath(csv_file_path) @@ -256,15 +256,12 @@ function Wing( csv_file_path = joinpath(dirname(geometry_file), csv_file_path) end aero_data, aero_model = load_polar_data(csv_file_path) - + prn && println("Section airfoil_id $(section.airfoil_id): Using $aero_model model") - + add_section!(wing, le_coord, te_coord, aero_model, aero_data) end - - # Initialize the wing after adding all sections - reinit!(wing) - + return wing end From ec46e3bc4f22e2d7b3b08cf8a548b0586d067c35 Mon Sep 17 00:00:00 2001 From: 1-Bart-1 Date: Sun, 7 Dec 2025 20:34:15 +0100 Subject: [PATCH 47/53] Improved makie plotting --- ext/VortexStepMethodMakieExt.jl | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/ext/VortexStepMethodMakieExt.jl b/ext/VortexStepMethodMakieExt.jl index 0ec5e5b5..62d5343c 100644 --- a/ext/VortexStepMethodMakieExt.jl +++ b/ext/VortexStepMethodMakieExt.jl @@ -1037,7 +1037,7 @@ function VortexStepMethod.plot_combined_analysis( set_axes_equal_makie!(ax_geo, panels; zoom=0.5) axislegend(ax_geo; position=:lt) - # [1,2] Polar Data Surfaces + # [1,2] Polar Data Surfaces or Curves if body_aero.panels[1].aero_model == POLAR_MATRICES alphas = collect(deg2rad.(-5:0.3:25)) delta_tes = collect(deg2rad.(-5:0.3:25)) @@ -1062,6 +1062,30 @@ function VortexStepMethod.plot_combined_analysis( wireframe!(ax, delta_tes, alphas, interp_matrix; color=:blue, linewidth=0.5, transparency=true) end + elseif body_aero.panels[1].aero_model == POLAR_VECTORS + alphas_deg = collect(-5:0.5:25) + alphas = deg2rad.(alphas_deg) + + ax_cl_curve = Axis(fig[1, 2][1, 1]; + title="Cl vs α", + xlabel="α [°]", + ylabel="Cl") + ax_cd_curve = Axis(fig[1, 2][1, 2]; + title="Cd vs α", + xlabel="α [°]", + ylabel="Cd") + ax_cm_curve = Axis(fig[1, 2][1, 3]; + title="Cm vs α", + xlabel="α [°]", + ylabel="Cm") + + cl_vals = [body_aero.panels[1].cl_interp(a) for a in alphas] + cd_vals = [body_aero.panels[1].cd_interp(a) for a in alphas] + cm_vals = [body_aero.panels[1].cm_interp(a) for a in alphas] + + lines!(ax_cl_curve, alphas_deg, cl_vals; color=:blue, linewidth=2) + lines!(ax_cd_curve, alphas_deg, cd_vals; color=:red, linewidth=2) + lines!(ax_cm_curve, alphas_deg, cm_vals; color=:green, linewidth=2) end # [2,1] Spanwise Distributions (3×3 grid) @@ -1158,6 +1182,10 @@ function VortexStepMethod.plot_combined_analysis( label=label_with_re, marker=:star5, markersize=12) axislegend(ax_polar, position=:lt) + # Set column widths: left column wider for 3x3 grid + colsize!(fig.layout, 1, Relative(0.6)) + colsize!(fig.layout, 2, Relative(0.4)) + if is_show display(fig) end From 9e535dab4775f4c93bb972ba10b3fb58a1a0af80 Mon Sep 17 00:00:00 2001 From: 1-Bart-1 Date: Sun, 7 Dec 2025 20:34:29 +0100 Subject: [PATCH 48/53] Deformable yaml kite --- examples/V3_kite.jl | 69 +++++++++++++++--------------------- examples/pyramid_model.jl | 44 +++++------------------ examples/rectangular_wing.jl | 45 ++++++++--------------- examples/stall_model.jl | 53 ++++++--------------------- 4 files changed, 61 insertions(+), 150 deletions(-) diff --git a/examples/V3_kite.jl b/examples/V3_kite.jl index 560fd98d..9dafd212 100644 --- a/examples/V3_kite.jl +++ b/examples/V3_kite.jl @@ -2,6 +2,10 @@ using LinearAlgebra using VortexStepMethod using GLMakie +PLOT = true +USE_TEX = false +DEFORM = false + project_dir = dirname(dirname(pathof(VortexStepMethod))) # Go up one level from src to project root# literature_paths = [ joinpath(project_dir, "data", "TUDELFT_V3_KITE", "literature_results","CFD_RANS_Rey_5e5_Poland2025_alpha_sweep_beta_0_NoStruts.csv"), @@ -11,8 +15,8 @@ literature_paths = [ ] labels= [ "Julia VSM 2D CFD PCHIP", - "CFD RANS Re=5e5", - "CFD RANS Re=10e5 (With Struts)", + "CFD RANS Re=5e5", + "CFD RANS Re=10e5 (With Struts)", "Python VSM 2D CFD PCHIP Re=5e5", "Wind Tunnel Re=5e5 (With Struts)" ] @@ -22,7 +26,20 @@ settings = VSMSettings("TUDELFT_V3_KITE/vsm_settings.yaml") # Create wing, body_aero, and solver objects using settings wing = Wing(settings) +refine!(wing) body_aero = BodyAerodynamics([wing]) +VortexStepMethod.reinit!(body_aero) + +if DEFORM + VortexStepMethod.unrefined_deform!( + wing, + deg2rad.(range(-10, 10, length=wing.n_unrefined_sections)), + deg2rad.(range(0, 0, length=wing.n_unrefined_sections)); + smooth=true + ) + VortexStepMethod.reinit!(body_aero; init_aero=false) +end + solver = Solver(body_aero, settings) # Set flight conditions from settings @@ -38,53 +55,23 @@ yaw_rate = settings.condition.yaw_rate PLOT = true USE_TEX = false -# Plotting polars -PLOT && plot_polars( - [solver], - [body_aero], - labels, +# Solve and plot combined analysis +results = VortexStepMethod.solve(solver, body_aero; log=true) +PLOT && plot_combined_analysis( + solver, + body_aero, + results; + solver_label="VSM", literature_path_list=literature_paths, angle_range=range(-5, 25, length=30), angle_type="angle_of_attack", angle_of_attack=angle_of_attack_deg, side_slip=sideslip_deg, v_a=wind_speed, - title="$(wing.n_panels)_panels_$(wing.spanwise_distribution)_from_yaml_settings", - data_type=".png", - is_save=false, - is_show=true, - use_tex=USE_TEX -) - - -# Plotting geometry -results = VortexStepMethod.solve(solver, body_aero; log=true) -PLOT && plot_geometry( - body_aero, - ""; - data_type=".svg", - save_path="", - is_save=false, - is_show=true, - view_elevation=15, - view_azimuth=-120, - use_tex=USE_TEX -) - - -# Plotting spanwise distributions -body_y_coordinates = [panel.aero_center[2] for panel in body_aero.panels] - -PLOT && plot_distribution( - [body_y_coordinates], - [results], - ["VSM"]; - title="CAD_spanwise_distributions_alpha_$(round(angle_of_attack_deg, digits=1))_delta_$(round(sideslip_deg, digits=1))_yaw_$(round(yaw_rate, digits=1))_v_a_$(round(wind_speed, digits=1))", - data_type=".png", - is_save=false, + title="TU Delft V3 Kite", is_show=true, use_tex=USE_TEX ) -nothing \ No newline at end of file +nothing diff --git a/examples/pyramid_model.jl b/examples/pyramid_model.jl index 43d4220a..bc2f0d2a 100644 --- a/examples/pyramid_model.jl +++ b/examples/pyramid_model.jl @@ -9,6 +9,7 @@ vsm_settings = VSMSettings("pyramid_model/vsm_settings.yaml") # Create wing, body_aero, and solver objects using vsm_settings wing = Wing(vsm_settings) +refine!(wing) body_aero = BodyAerodynamics([wing]) solver = Solver(body_aero, vsm_settings) @@ -28,47 +29,18 @@ results = VortexStepMethod.solve(solver, body_aero; log=true) PLOT = true USE_TEX = false -# Plotting polars -PLOT && plot_polars( - [solver], - [body_aero], - ["VSM Pyramid Model"], +# Plotting combined analysis +PLOT && plot_combined_analysis( + solver, + body_aero, + results; + solver_label="VSM", angle_range=range(-5, 25, length=30), angle_type="angle_of_attack", angle_of_attack=angle_of_attack_deg, side_slip=sideslip_deg, v_a=wind_speed, - title="$(wing.n_panels)_panels_$(wing.spanwise_distribution)_pyramid_model", - data_type=".png", - is_save=false, - is_show=true, - use_tex=USE_TEX -) - -# Plotting geometry -results = VortexStepMethod.solve(solver, body_aero; log=true) -PLOT && plot_geometry( - body_aero, - ""; - data_type=".svg", - save_path="", - is_save=false, - is_show=true, - view_elevation=15, - view_azimuth=-120, - use_tex=USE_TEX -) - -# Plotting spanwise distributions -body_y_coordinates = [panel.aero_center[2] for panel in body_aero.panels] - -PLOT && plot_distribution( - [body_y_coordinates], - [results], - ["VSM"]; - title="pyramid_spanwise_distributions_alpha_$(round(angle_of_attack_deg, digits=1))_delta_$(round(sideslip_deg, digits=1))_yaw_$(round(yaw_rate, digits=1))_v_a_$(round(wind_speed, digits=1))", - data_type=".png", - is_save=false, + title="Pyramid Model", is_show=true, use_tex=USE_TEX ) diff --git a/examples/rectangular_wing.jl b/examples/rectangular_wing.jl index 330de483..7053337d 100644 --- a/examples/rectangular_wing.jl +++ b/examples/rectangular_wing.jl @@ -18,15 +18,18 @@ alpha = deg2rad(alpha_deg) wing = Wing(n_panels, spanwise_distribution=LINEAR) # Add wing sections - defining only tip sections with inviscid airfoil model -add_section!(wing, - [0.0, span/2, 0.0], # Left tip LE +add_section!(wing, + [0.0, span/2, 0.0], # Left tip LE [chord, span/2, 0.0], # Left tip TE INVISCID) -add_section!(wing, +add_section!(wing, [0.0, -span/2, 0.0], # Right tip LE [chord, -span/2, 0.0], # Right tip TE INVISCID) +# Refine mesh +refine!(wing) + # Step 3: Initialize aerodynamics body_aero = BodyAerodynamics([wing]) @@ -53,38 +56,18 @@ println("CL = $(round(results_vsm["cl"], digits=4))") println("CD = $(round(results_vsm["cd"], digits=4))") println("Projected area = $(round(results_vsm["projected_area"], digits=4)) m²") -# Step 6: Plot geometry -PLOT && plot_geometry( - body_aero, - "Rectangular_wing_geometry"; - data_type=".png", - save_path=".", - is_save=false, - is_show=true, - use_tex=USE_TEX -) - -# Step 7: Plot spanwise distributions -y_coordinates = [panel.aero_center[2] for panel in body_aero.panels] - -PLOT && plot_distribution( - [y_coordinates, y_coordinates], - [results_vsm, results_llt], - ["VSM", "LLT"], - title="Spanwise Distributions", - use_tex=USE_TEX -) - -# Step 8: Plot polar curves +# Step 6: Plot combined analysis angle_range = range(0, 20, 20) -PLOT && plot_polars( +PLOT && plot_combined_analysis( [llt_solver, vsm_solver], [body_aero, body_aero], - ["LLT", "VSM"]; - angle_range, + [results_llt, results_vsm]; + solver_label=["LLT", "VSM"], + angle_range=angle_range, angle_type="angle_of_attack", - v_a, - title="Rectangular Wing Polars", + v_a=v_a, + title="Rectangular Wing", + is_show=true, use_tex=USE_TEX ) nothing diff --git a/examples/stall_model.jl b/examples/stall_model.jl index b6d01e92..fe83e718 100644 --- a/examples/stall_model.jl +++ b/examples/stall_model.jl @@ -39,6 +39,7 @@ CAD_wing = Wing(n_panels; spanwise_distribution) for rib in rib_list add_section!(CAD_wing, rib[1], rib[2], rib[3], rib[4]) end +refine!(CAD_wing) body_aero = BodyAerodynamics([CAD_wing]) # Create solvers @@ -64,40 +65,12 @@ vel_app = [ ] * v_a set_va!(body_aero, vel_app) -# Plotting geometry -PLOT && plot_geometry( - body_aero, - ""; - data_type=".svg", - save_path="", - is_save=false, - is_show=true, - view_elevation=15, - view_azimuth=-120, - use_tex=USE_TEX -) - -# Solving and plotting distributions +# Solve both configurations results = solve(vsm_solver, body_aero) @time results_with_stall = solve(VSM_with_stall_correction, body_aero) @time results_with_stall = solve(VSM_with_stall_correction, body_aero) -CAD_y_coordinates = [panel.aero_center[2] for panel in body_aero.panels] - -PLOT && plot_distribution( - [CAD_y_coordinates, CAD_y_coordinates], - [results, results_with_stall], - ["VSM", "VSM with stall correction"]; - title="CAD_spanwise_distributions_alpha_$(round(aoa, digits=1))_delta_$(round(side_slip, digits=1))_yaw_$(round(yaw_rate, digits=1))_v_a_$(round(v_a, digits=1))", - data_type=".png", - save_path=joinpath(save_folder, "spanwise_distributions"), - is_save=false, - is_show=true, - use_tex=USE_TEX -) - -# Plotting polar -save_path = joinpath(root_dir, "results", "TUD_V3_LEI_KITE") +# Setup literature data paths path_cfd_lebesque = joinpath( root_dir, "data", @@ -108,24 +81,20 @@ path_cfd_lebesque = joinpath( # Only include literature data if file exists literature_paths = isfile(path_cfd_lebesque) ? [path_cfd_lebesque] : String[] -labels = isfile(path_cfd_lebesque) ? - ["VSM CAD 19ribs", "VSM CAD 19ribs , with stall correction", "CFD_Lebesque Rey 30e5"] : - ["VSM CAD 19ribs", "VSM CAD 19ribs , with stall correction"] -PLOT && plot_polars( +# Plot combined analysis +PLOT && plot_combined_analysis( [vsm_solver, VSM_with_stall_correction], [body_aero, body_aero], - labels; + [results, results_with_stall]; + solver_label=["VSM", "VSM (with stall)"], literature_path_list=literature_paths, angle_range=range(0, 25, length=25), angle_type="angle_of_attack", - angle_of_attack=0, - side_slip=0, - v_a=10, - title="tutorial_testing_stall_model_n_panels_$(n_panels)_distribution_$(spanwise_distribution)_unrefined_$(CAD_wing.n_unrefined_sections)", - data_type=".png", - save_path=joinpath(save_folder, "polars"), - is_save=true, + angle_of_attack=aoa, + side_slip=side_slip, + v_a=v_a, + title="Stall Model Comparison", is_show=true, use_tex=USE_TEX ) From 56f219ec18992dd06b8fb024b673cadd3f3313da Mon Sep 17 00:00:00 2001 From: 1-Bart-1 Date: Sun, 7 Dec 2025 21:29:44 +0100 Subject: [PATCH 49/53] Fix for updated separate refine --- test/bench.jl | 8 +++-- test/bench_solve.jl | 4 ++- .../test_body_aerodynamics.jl | 15 ++++++++-- test/plotting/test_plotting.jl | 2 ++ test/ram_geometry/test_kite_geometry.jl | 4 +-- test/runtests.jl | 1 + test/solver/test_solver.jl | 4 ++- test/solver/test_unrefined_dist.jl | 8 +++-- test/wake/test_wake.jl | 1 + test/wing_geometry/test_wing_geometry.jl | 30 +++++++++---------- test/yaml_geometry/test_wing_constructor.jl | 10 ++----- .../test_yaml_wing_deformation.jl | 30 ++++++++++++------- 12 files changed, 72 insertions(+), 45 deletions(-) diff --git a/test/bench.jl b/test/bench.jl index b5d1ba27..42c256f4 100644 --- a/test/bench.jl +++ b/test/bench.jl @@ -51,10 +51,12 @@ const IS_JULIA_1_12_OR_NEWER = VERSION >= v"1.12" [chord, -span/2, 0.0], # Right tip TE INVISCID) + refine!(wing) body_aero = BodyAerodynamics([wing]) + refine!(unchanged_wing) unchanged_body_aero = BodyAerodynamics([unchanged_wing]) reinit!(unchanged_body_aero) - + @testset "Re-initialization" begin result = @benchmark reinit!($unchanged_body_aero; init_aero=false) samples=1 evals=1 @info "Re-initializing Allocations: $(result.allocs) \t Memory: $(result.memory)" @@ -136,7 +138,9 @@ const IS_JULIA_1_12_OR_NEWER = VERSION >= v"1.12" [chord, -span/2, 0.0], # Right tip TE aero_model, aero_data) - body_aero = BodyAerodynamics([wing]) + refine!(wing) + refine!(wing) + body_aero = BodyAerodynamics([wing]) solver = Solver(body_aero; aerodynamic_model_type=model diff --git a/test/bench_solve.jl b/test/bench_solve.jl index dc8c4590..1f3b934f 100644 --- a/test/bench_solve.jl +++ b/test/bench_solve.jl @@ -30,7 +30,9 @@ add_section!(wing, INVISCID) # Step 3: Initialize aerodynamics -wa = BodyAerodynamics([wing]) +wa = refine!(wing) + refine!(wing) + body_aero = BodyAerodynamics([wing]) # Set inflow conditions vel_app = [cos(alpha), 0.0, sin(alpha)] .* v_a diff --git a/test/body_aerodynamics/test_body_aerodynamics.jl b/test/body_aerodynamics/test_body_aerodynamics.jl index 6e61857c..c89415aa 100644 --- a/test/body_aerodynamics/test_body_aerodynamics.jl +++ b/test/body_aerodynamics/test_body_aerodynamics.jl @@ -37,6 +37,8 @@ include("../utils.jl") ) end + refine!(wing) + refine!(wing) body_aero = BodyAerodynamics([wing]) set_va!(body_aero, v_a) @@ -120,7 +122,8 @@ end add_section!(wing, [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], INVISCID) add_section!(wing, [0.0, 1.0, 0.0], [1.0, 1.0, 0.0], INVISCID) add_section!(wing, [0.0, 2.0, 0.0], [1.0, 2.0, 0.0], INVISCID) - + refine!(wing) + # Test non-zero origin translation origin = MVec3(1.0, 2.0, 3.0) body_aero = BodyAerodynamics([wing]; kite_body_origin=origin) @@ -174,7 +177,9 @@ end INVISCID ) end - body_aero = BodyAerodynamics([wing]) + refine!(wing) + refine!(wing) + body_aero = BodyAerodynamics([wing]) set_va!(body_aero, v_a) return body_aero, coord, v_a, model @@ -304,6 +309,8 @@ end ) end + refine!(wing) + refine!(wing) body_aero = BodyAerodynamics([wing]) set_va!(body_aero, v_a) @@ -416,7 +423,9 @@ end try settings = VSMSettings(settings_file) wing = Wing(settings) - body_aero = BodyAerodynamics([wing]) + body_aero = refine!(wing) + refine!(wing) + body_aero = BodyAerodynamics([wing]) set_va!(body_aero, settings) diff --git a/test/plotting/test_plotting.jl b/test/plotting/test_plotting.jl index 0bcfc6cc..29fe4a20 100644 --- a/test/plotting/test_plotting.jl +++ b/test/plotting/test_plotting.jl @@ -56,6 +56,7 @@ function create_body_aero() INVISCID) # Step 3: Initialize aerodynamics + refine!(wing) body_aero = BodyAerodynamics([wing]) # Set inflow conditions vel_app = [cos(alpha), 0.0, sin(alpha)] .* v_a @@ -124,6 +125,7 @@ end safe_rm(joinpath(save_dir, "Rectangular_Wing_Polars.png")) # Step 9: Test polar data plotting + # ram_wing is an ObjWing - no refine! needed body_aero = BodyAerodynamics([ram_wing]) fig = plot_polar_data(body_aero; is_show=false) @test fig isa Figure diff --git a/test/ram_geometry/test_kite_geometry.jl b/test/ram_geometry/test_kite_geometry.jl index 894664d2..7df146dd 100644 --- a/test/ram_geometry/test_kite_geometry.jl +++ b/test/ram_geometry/test_kite_geometry.jl @@ -169,7 +169,7 @@ using Serialization wing = ObjWing(test_obj_path, test_dat_path; remove_nan=true) @test wing.n_panels == 56 # Default value - @test wing.spanwise_distribution == NONE + @test wing.spanwise_distribution == UNCHANGED @test wing.spanwise_direction ≈ [0.0, 1.0, 0.0] @test length(wing.unrefined_sections) > 0 # Should have sections now @test wing.mass ≈ 1.0 @@ -186,7 +186,7 @@ using Serialization end @testset "Wing Deformation" begin - # Create an ObjWing for testing + # Create an ObjWing for testing (no refine! needed - fully complete) wing = ObjWing(test_obj_path, test_dat_path; remove_nan=true) body_aero = BodyAerodynamics([wing]) diff --git a/test/runtests.jl b/test/runtests.jl index d0b770d4..593b9c81 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -37,6 +37,7 @@ end::Bool end should_run_test("body_aerodynamics/test_body_aerodynamics.jl") && include("body_aerodynamics/test_body_aerodynamics.jl") should_run_test("body_aerodynamics/test_results.jl") && include("body_aerodynamics/test_results.jl") + should_run_test("test_refinement_validation.jl") && include("test_refinement_validation.jl") should_run_test("filament/test_bound_filament.jl") && include("filament/test_bound_filament.jl") should_run_test("filament/test_semi_infinite_filament.jl") && include("filament/test_semi_infinite_filament.jl") should_run_test("panel/test_panel.jl") && include("panel/test_panel.jl") diff --git a/test/solver/test_solver.jl b/test/solver/test_solver.jl index 1d60061a..5004986b 100644 --- a/test/solver/test_solver.jl +++ b/test/solver/test_solver.jl @@ -11,7 +11,9 @@ using Test # Test Solver constructor with VSMSettings settings = VSMSettings(settings_file) wing = Wing(settings) - body_aero = BodyAerodynamics([wing]) + refine!(wing) + refine!(wing) + body_aero = BodyAerodynamics([wing]) solver = Solver(body_aero, settings) # Verify solver properties match settings diff --git a/test/solver/test_unrefined_dist.jl b/test/solver/test_unrefined_dist.jl index 219cc65e..5e2d131a 100644 --- a/test/solver/test_unrefined_dist.jl +++ b/test/solver/test_unrefined_dist.jl @@ -19,7 +19,9 @@ using Test # Create wing and solver wing = Wing(settings) - body_aero = BodyAerodynamics([wing]) + refine!(wing) + refine!(wing) + body_aero = BodyAerodynamics([wing]) solver = Solver(body_aero, settings) # Set conditions and solve @@ -96,7 +98,9 @@ using Test settings.solver_settings.n_panels = n_panels wing = Wing(settings) - body_aero = BodyAerodynamics([wing]) + refine!(wing) + refine!(wing) + body_aero = BodyAerodynamics([wing]) solver = Solver(body_aero, settings) va = [10.0, 0.0, 0.0] diff --git a/test/wake/test_wake.jl b/test/wake/test_wake.jl index b1c45f87..e6d604f5 100644 --- a/test/wake/test_wake.jl +++ b/test/wake/test_wake.jl @@ -20,6 +20,7 @@ using VortexStepMethod try # Create wing and body aerodynamics with known good geometry + # ObjWing is fully complete - no refine! needed wing = ObjWing(body_path, foil_path; n_panels=56) # Use default panels body_aero = BodyAerodynamics([wing]) diff --git a/test/wing_geometry/test_wing_geometry.jl b/test/wing_geometry/test_wing_geometry.jl index 8c4ba69c..74704048 100644 --- a/test/wing_geometry/test_wing_geometry.jl +++ b/test/wing_geometry/test_wing_geometry.jl @@ -1,7 +1,7 @@ using Test using LinearAlgebra using VortexStepMethod -using VortexStepMethod: Wing, Section, add_section!, refine_mesh_by_splitting_provided_sections!, refine_aerodynamic_mesh! +using VortexStepMethod: Wing, Section, add_section!, refine_mesh_by_splitting_provided_sections!, refine! import Base: == """ @@ -98,7 +98,7 @@ end add_section!(example_wing, [0.0, 1.0, 0.0], [0.0, 1.0, 0.0], INVISCID) add_section!(example_wing, [0.0, -1.0, 0.0], [0.0, -1.0, 0.0], INVISCID) add_section!(example_wing, [0.0, -1.5, 0.0], [0.0, -1.5, 0.0], INVISCID) - refine_aerodynamic_mesh!(example_wing) + refine!(example_wing) sections = example_wing.refined_sections # Test right to left order @@ -106,7 +106,7 @@ end add_section!(example_wing_1, [0.0, -1.5, 0.0], [0.0, -1.5, 0.0], INVISCID) add_section!(example_wing_1, [0.0, -1.0, 0.0], [0.0, -1.0, 0.0], INVISCID) add_section!(example_wing_1, [0.0, 1.0, 0.0], [0.0, 1.0, 0.0], INVISCID) - refine_aerodynamic_mesh!(example_wing_1) + refine!(example_wing_1) sections_1 = example_wing_1.refined_sections # Test random order @@ -114,7 +114,7 @@ end add_section!(example_wing_2, [0.0, 1.0, 0.0], [0.0, 1.0, 0.0], INVISCID) add_section!(example_wing_2, [0.0, -1.5, 0.0], [0.0, -1.5, 0.0], INVISCID) add_section!(example_wing_2, [0.0, -1.0, 0.0], [0.0, -1.0, 0.0], INVISCID) - refine_aerodynamic_mesh!(example_wing_2) + refine!(example_wing_2) sections_2 = example_wing_2.refined_sections for i in eachindex(sections) @@ -133,7 +133,7 @@ end wing = Wing(n_panels; spanwise_distribution=LINEAR) add_section!(wing, [0.0, span/2, 0.0], [-1.0, span/2, 0.0], INVISCID) add_section!(wing, [0.0, -span/2, 0.0], [-1.0, -span/2, 0.0], INVISCID) - refine_aerodynamic_mesh!(wing) + refine!(wing) sections = wing.refined_sections @test length(sections) == wing.n_panels + 1 @@ -149,7 +149,7 @@ end wing = Wing(n_panels; spanwise_distribution=COSINE) add_section!(wing, [0.0, span/2, 0.0], [-1.0, span/2, 0.0], INVISCID) add_section!(wing, [0.0, -span/2, 0.0], [-1.0, -span/2, 0.0], INVISCID) - refine_aerodynamic_mesh!(wing) + refine!(wing) sections = wing.refined_sections @test length(sections) == wing.n_panels + 1 @@ -173,7 +173,7 @@ end add_section!(wing, [0.0, span/2, 0.0], [-1.0, span/2, 0.0], INVISCID) add_section!(wing, [0.0, -span/2, 0.0], [-1.0, -span/2, 0.0], INVISCID) - refine_aerodynamic_mesh!(wing) + refine!(wing) sections = wing.unrefined_sections @test length(sections) == wing.n_panels + 1 @test sections[1].LE_point ≈ [0.0, span/2, 0.0] @@ -188,7 +188,7 @@ end add_section!(wing, [0.0, span/2, 0.0], [-1.0, span/2, 0.0], INVISCID) add_section!(wing, [0.0, -span/2, 0.0], [-1.0, -span/2, 0.0], INVISCID) - refine_aerodynamic_mesh!(wing) + refine!(wing) sections = wing.refined_sections @test length(sections) == wing.n_panels + 1 @test sections[1].LE_point ≈ [0.0, span/2, 0.0] @@ -206,7 +206,7 @@ end add_section!(wing, [0.0, y, 0.0], [-1.0, y, 0.0], INVISCID) end - refine_aerodynamic_mesh!(wing) + refine!(wing) sections = wing.refined_sections @test length(sections) == wing.n_panels + 1 @@ -226,7 +226,7 @@ end add_section!(wing, [0.0, 5.0, 0.0], [-1.0, 5.0, 0.0], INVISCID) add_section!(wing, [0.0, -5.0, 0.0], [-1.0, -5.0, 0.0], INVISCID) - refine_aerodynamic_mesh!(wing) + refine!(wing) sections = wing.refined_sections # Calculate expected quarter-chord points @@ -284,7 +284,7 @@ end add_section!(wing, [0.0, 0.0, 0.0], [-1.0, 0.0, 0.0], LEI_AIRFOIL_BREUKELS, (2.0, 0.5)) add_section!(wing, [0.0, -span/2, 0.0], [-1.0, -span/2, 0.0], LEI_AIRFOIL_BREUKELS, (4.0, 1.0)) - refine_aerodynamic_mesh!(wing) + refine!(wing) sections = wing.refined_sections @test length(sections) == wing.n_panels + 1 @@ -316,7 +316,7 @@ end add_section!(wing, [0.0, -1.0, 0.0], [1.0, -1.0, 0.0], INVISCID) add_section!(wing, [0.0, -2.0, 0.0], [1.0, -2.0, 0.0], INVISCID) - refine_aerodynamic_mesh!(wing) + refine!(wing) new_sections = wing.refined_sections @test length(new_sections) - 1 == 6 @@ -346,7 +346,7 @@ end add_section!(wing, [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], INVISCID) add_section!(wing, [0.0, -span/2, 0.0], [1.0, -span/2, 0.0], INVISCID) - refine_aerodynamic_mesh!(wing) + refine!(wing) @test length(wing.refined_panel_mapping) == n_panels @@ -391,7 +391,7 @@ end add_section!(wing, [0.0, -span/6, 0.0], [1.0, -span/6, 0.0], INVISCID) add_section!(wing, [0.0, -span/2, 0.0], [1.0, -span/2, 0.0], INVISCID) - refine_aerodynamic_mesh!(wing) + refine!(wing) @test length(wing.refined_panel_mapping) == n_panels @@ -435,7 +435,7 @@ end add_section!(wing, [0.0, -2.0, 0.0], [1.0, -2.0, 0.0], INVISCID) add_section!(wing, [0.0, -6.0, 0.0], [1.0, -6.0, 0.0], INVISCID) - refine_aerodynamic_mesh!(wing) + refine!(wing) @test length(wing.refined_panel_mapping) == n_panels diff --git a/test/yaml_geometry/test_wing_constructor.jl b/test/yaml_geometry/test_wing_constructor.jl index e0acf4a9..bdd19f5d 100644 --- a/test/yaml_geometry/test_wing_constructor.jl +++ b/test/yaml_geometry/test_wing_constructor.jl @@ -41,7 +41,6 @@ using Logging @test wing isa Wing @test wing.n_panels == 4 - @test wing.n_unrefined_sections == 2 @test wing.spanwise_distribution == LINEAR @test wing.spanwise_direction ≈ [0.0, 1.0, 0.0] @test length(wing.unrefined_sections) == 2 # simple_wing has 2 sections @@ -77,7 +76,6 @@ using Logging ) @test wing.n_panels == 8 - @test wing.n_unrefined_sections == 2 @test wing.spanwise_distribution == COSINE @test !wing.remove_nan end @@ -188,7 +186,6 @@ wing_airfoils: wing = Wing(test_yaml_path; n_panels=12) @test wing.n_panels == 12 - @test wing.n_unrefined_sections == 7 @test length(wing.unrefined_sections) == 7 # Test that different airfoil_ids get different polar data @@ -219,7 +216,6 @@ wing_airfoils: @test wing isa Wing @test wing.n_panels == 6 - @test wing.n_unrefined_sections == 2 @test wing.spanwise_distribution == COSINE @test length(wing.unrefined_sections) == 2 @test wing.unrefined_sections[1].aero_model == POLAR_VECTORS @@ -235,17 +231,15 @@ wing_airfoils: wing = Wing(simple_wing_file; n_panels=4) @test wing isa Wing @test wing.n_panels == 4 - @test wing.n_unrefined_sections == 2 @test length(wing.unrefined_sections) == 2 - + # Test complex wing construction complex_wing_file = test_data_path("yaml_geometry", "complex_wing.yaml") @test isfile(complex_wing_file) - + complex_wing = Wing(complex_wing_file; n_panels=12) @test complex_wing isa Wing @test complex_wing.n_panels == 12 - @test complex_wing.n_unrefined_sections == 7 @test length(complex_wing.unrefined_sections) == 7 # Verify polar data is loaded from shared files diff --git a/test/yaml_geometry/test_yaml_wing_deformation.jl b/test/yaml_geometry/test_yaml_wing_deformation.jl index bf263cc5..6194e676 100644 --- a/test/yaml_geometry/test_yaml_wing_deformation.jl +++ b/test/yaml_geometry/test_yaml_wing_deformation.jl @@ -7,6 +7,7 @@ using Test # Load existing simple_wing.yaml simple_wing_file = test_data_path("yaml_geometry", "simple_wing.yaml") wing = Wing(simple_wing_file; n_panels=4) + refine!(wing) body_aero = BodyAerodynamics([wing]) # Store original TE point for comparison @@ -19,7 +20,7 @@ using Test delta_dist = fill(deg2rad(5.0), wing.n_panels) # 5 degrees TE deflection per panel VortexStepMethod.deform!(wing, theta_dist, delta_dist) - VortexStepMethod.reinit!(body_aero; refine_mesh=false) + VortexStepMethod.reinit!(body_aero) # Check if TE point changed after deformation deformed_te_point = copy(body_aero.panels[i].TE_point_1) @@ -40,7 +41,7 @@ using Test zero_delta_dist = zeros(wing.n_panels) VortexStepMethod.deform!(wing, zero_theta_dist, zero_delta_dist) - VortexStepMethod.reinit!(body_aero; refine_mesh=false) + VortexStepMethod.reinit!(body_aero) # Check if TE point returned to original position reset_te_point = copy(body_aero.panels[i].TE_point_1) @@ -54,6 +55,7 @@ using Test # Load existing complex_wing.yaml with multiple sections complex_wing_file = test_data_path("yaml_geometry", "complex_wing.yaml") wing = Wing(complex_wing_file; n_panels=12) + refine!(wing) body_aero = BodyAerodynamics([wing]) # Store original points for multiple panels @@ -72,7 +74,7 @@ using Test delta_dist = [deg2rad(-5.0 + 10.0 * i / n) for i in 1:n] # Varying deflection VortexStepMethod.deform!(wing, theta_dist, delta_dist) - VortexStepMethod.reinit!(body_aero; refine_mesh=false) + VortexStepMethod.reinit!(body_aero) # Check that different panels have different deformations for (idx, i) in enumerate(test_indices) @@ -91,7 +93,7 @@ using Test # Reset and verify VortexStepMethod.deform!(wing, zeros(wing.n_panels), zeros(wing.n_panels)) - VortexStepMethod.reinit!(body_aero; refine_mesh=false) + VortexStepMethod.reinit!(body_aero) for (idx, i) in enumerate(test_indices) reset_te = body_aero.panels[i].TE_point_1 @@ -106,6 +108,7 @@ using Test # This test specifically checks the NTuple handling fix simple_wing_file = test_data_path("yaml_geometry", "simple_wing.yaml") wing = Wing(simple_wing_file; n_panels=4) + refine!(wing) # Verify that sections have NTuple aero_data (for wings with simple polars) # or other valid AeroData types @@ -127,13 +130,14 @@ using Test # Test that reinit! on BodyAerodynamics properly handles deformed wings simple_wing_file = test_data_path("yaml_geometry", "simple_wing.yaml") wing = Wing(simple_wing_file; n_panels=4) + refine!(wing) body_aero = BodyAerodynamics([wing]) # Apply deformation theta_dist = fill(deg2rad(15.0), wing.n_panels) delta_dist = fill(deg2rad(3.0), wing.n_panels) VortexStepMethod.deform!(wing, theta_dist, delta_dist) - VortexStepMethod.reinit!(body_aero; refine_mesh=false) + VortexStepMethod.reinit!(body_aero) # Store state after deformation i = length(body_aero.panels) ÷ 2 @@ -144,7 +148,7 @@ using Test va=zeros(3), omega=zeros(3), init_aero=true, - refine_mesh=false + ) end @@ -155,11 +159,12 @@ using Test @testset "Edge Cases" begin simple_wing_file = test_data_path("yaml_geometry", "simple_wing.yaml") wing = Wing(simple_wing_file; n_panels=2) + refine!(wing) body_aero = BodyAerodynamics([wing]) # Test zero deformation VortexStepMethod.deform!(wing, zeros(wing.n_panels), zeros(wing.n_panels)) - VortexStepMethod.reinit!(body_aero; refine_mesh=false) + VortexStepMethod.reinit!(body_aero) @test all(p.delta ≈ 0.0 for p in body_aero.panels) # Test large deformation angles @@ -168,14 +173,14 @@ using Test # Should not error even with large angles VortexStepMethod.deform!(wing, theta_dist, delta_dist) - VortexStepMethod.reinit!(body_aero; refine_mesh=false) + VortexStepMethod.reinit!(body_aero) @test all(p.delta ≈ deg2rad(30.0) for p in body_aero.panels) # Test negative angles theta_dist = fill(deg2rad(-20.0), wing.n_panels) delta_dist = fill(deg2rad(-10.0), wing.n_panels) VortexStepMethod.deform!(wing, theta_dist, delta_dist) - VortexStepMethod.reinit!(body_aero; refine_mesh=false) + VortexStepMethod.reinit!(body_aero) @test all(p.delta ≈ deg2rad(-10.0) for p in body_aero.panels) end @@ -183,6 +188,7 @@ using Test # Test that panel angles are correctly averaged to section angles simple_wing_file = test_data_path("yaml_geometry", "simple_wing.yaml") wing = Wing(simple_wing_file; n_panels=4) + refine!(wing) body_aero = BodyAerodynamics([wing]) # Create varying panel angles @@ -200,7 +206,7 @@ using Test # Section 5: should use panel 4 angle = 40° # We can verify this by checking that delta values are correct - VortexStepMethod.reinit!(body_aero; refine_mesh=false) + VortexStepMethod.reinit!(body_aero) # Each panel gets its delta directly @test body_aero.panels[1].delta ≈ deg2rad(5.0) atol=1e-6 @@ -214,6 +220,7 @@ using Test # Use complex_wing which has 7 unrefined sections complex_wing_file = test_data_path("yaml_geometry", "complex_wing.yaml") wing = Wing(complex_wing_file; n_panels=12) + refine!(wing) body_aero = BodyAerodynamics([wing]) # Verify we have 7 unrefined sections @@ -226,7 +233,7 @@ using Test # Apply using unrefined_deform! VortexStepMethod.unrefined_deform!(wing, theta_unrefined, delta_unrefined) - VortexStepMethod.reinit!(body_aero; refine_mesh=false) + VortexStepMethod.reinit!(body_aero) # Each panel should have the delta from its mapped unrefined section for i in 1:wing.n_panels @@ -240,6 +247,7 @@ using Test # Create test wing with 2 unrefined sections, refined to 40 panels simple_wing_file = test_data_path("yaml_geometry", "simple_wing.yaml") wing = Wing(simple_wing_file; n_panels=40) + refine!(wing) @test wing.n_unrefined_sections == 2 # Define varying input angles at unrefined section level From d3bc096eb8fe7938c2fe87bc96eb3ca2e08d4c70 Mon Sep 17 00:00:00 2001 From: 1-Bart-1 Date: Sun, 7 Dec 2025 21:30:10 +0100 Subject: [PATCH 50/53] Move Obj to refinement method --- src/obj_geometry.jl | 114 +++++++++++++++++++++++++++++++++++-------- src/wing_geometry.jl | 6 +-- 2 files changed, 96 insertions(+), 24 deletions(-) diff --git a/src/obj_geometry.jl b/src/obj_geometry.jl index bd0a56e6..ea9863ca 100644 --- a/src/obj_geometry.jl +++ b/src/obj_geometry.jl @@ -227,6 +227,94 @@ function create_interpolations(vertices, circle_center_z, radius, gamma_tip, R=I return (le_interp, te_interp, area_interp) end +""" + refine_obj_wing!(wing::AbstractWing; recompute_mapping=true) + +Refine OBJ wing by computing position deltas and applying them to refined sections. + +This method enables deformation support for OBJ wings by: +1. Recalculating evenly-spaced gammas for unrefined sections +2. Computing what unrefined sections SHOULD be (from interpolations) +3. Computing deltas between current and interpolated positions +4. Creating refined sections from interpolations + interpolated deltas +5. Computing panel mapping + +# Arguments +- `wing::AbstractWing`: OBJ wing with le_interp/te_interp +- `recompute_mapping::Bool=true`: Whether to recompute refined_panel_mapping + +# Effects +Updates wing.refined_sections and wing.non_deformed_sections in-place. +""" +function refine_obj_wing!(wing::AbstractWing; recompute_mapping=true) + n_unrefined = wing.n_unrefined_sections + n_refined = wing.n_panels + 1 + + # 1. Calculate evenly spaced gammas for unrefined sections + unrefined_gammas = range(-wing.gamma_tip, wing.gamma_tip, n_unrefined) + + # 2. Recalculate what unrefined sections SHOULD be from interpolations + interpolated_unrefined_le = Matrix{Float64}(undef, n_unrefined, 3) + interpolated_unrefined_te = Matrix{Float64}(undef, n_unrefined, 3) + for (i, gamma) in enumerate(unrefined_gammas) + interpolated_unrefined_le[i, :] .= [wing.le_interp[j](gamma) for j in 1:3] + interpolated_unrefined_te[i, :] .= [wing.te_interp[j](gamma) for j in 1:3] + end + + # 3. Compute deltas: current - interpolated + deltas_le = Matrix{Float64}(undef, n_unrefined, 3) + deltas_te = Matrix{Float64}(undef, n_unrefined, 3) + for i in 1:n_unrefined + deltas_le[i, :] .= wing.unrefined_sections[i].LE_point - + interpolated_unrefined_le[i, :] + deltas_te[i, :] .= wing.unrefined_sections[i].TE_point - + interpolated_unrefined_te[i, :] + end + + # 4. Create refined sections with interpolated deltas + refined_gammas = range(-wing.gamma_tip, wing.gamma_tip, n_refined) + if isempty(wing.refined_sections) + wing.refined_sections = [Section() for _ in 1:n_refined] + end + + for (idx, gamma) in enumerate(refined_gammas) + # Get base position from interpolation + base_le = [wing.le_interp[i](gamma) for i in 1:3] + base_te = [wing.te_interp[i](gamma) for i in 1:3] + + # Find surrounding unrefined sections for delta interpolation + unrefined_idx = searchsortedlast(collect(unrefined_gammas), gamma) + unrefined_idx = clamp(unrefined_idx, 1, n_unrefined - 1) + + # Linear interpolation weight + gamma_left = unrefined_gammas[unrefined_idx] + gamma_right = unrefined_gammas[unrefined_idx + 1] + t = (gamma - gamma_left) / (gamma_right - gamma_left) + + # Interpolate deltas + delta_le = (1 - t) * deltas_le[unrefined_idx, :] + + t * deltas_le[unrefined_idx + 1, :] + delta_te = (1 - t) * deltas_te[unrefined_idx, :] + + t * deltas_te[unrefined_idx + 1, :] + + # Apply deltas to get final position + final_le = base_le + delta_le + final_te = base_te + delta_te + + # Update refined section + aero_model = wing.unrefined_sections[1].aero_model + aero_data = wing.unrefined_sections[1].aero_data + VortexStepMethod.reinit!(wing.refined_sections[idx], final_le, final_te, + aero_model, aero_data) + end + + # 5. Compute panel mapping and update non_deformed_sections + recompute_mapping && VortexStepMethod.compute_refined_panel_mapping!(wing) + VortexStepMethod.update_non_deformed_sections!(wing) + + return nothing +end + """ center_to_com!(vertices, faces) @@ -487,34 +575,18 @@ function ObjWing( push!(sections, Section(LE_point, TE_point, POLAR_MATRICES, aero_data)) end - # Create refined sections (evenly spaced including both tips) - refined_sections = Section[] - for gamma in range(-gamma_tip, gamma_tip, n_panels+1) - LE_point = [le_interp[i](gamma) for i in 1:3] - TE_point = [te_interp[i](gamma) for i in 1:3] - push!(refined_sections, Section(LE_point, TE_point, POLAR_MATRICES, aero_data)) - end - - # Create non_deformed_sections as copy of refined_sections for deformation support - non_deformed_sections = [Section() for _ in 1:n_panels+1] - for i in 1:n_panels+1 - reinit!(non_deformed_sections[i], refined_sections[i]) - end - panel_props = PanelProperties{n_panels}() cache = [PreallocationTools.LazyBufferCache()] wing = Wing(n_panels, Int16(n_unrefined_sections), spanwise_distribution, panel_props, MVec3(spanwise_direction), - sections, refined_sections, remove_nan, - Int16[], - non_deformed_sections, zeros(n_panels), zeros(n_panels), + sections, Section[], remove_nan, # refined_sections empty + Int16[], # refined_panel_mapping empty + Section[], zeros(n_panels), zeros(n_panels), # non_deformed, theta, delta mass, gamma_tip, inertia_tensor, T_cad_body, R_cad_body, radius, le_interp, te_interp, area_interp, cache) - # Compute panel mapping for deformation support - VortexStepMethod.compute_refined_panel_mapping!(wing) - - # Update panel properties + # Auto-refine for backward compatibility + refine_obj_wing!(wing; recompute_mapping=true) reinit!(wing) wing diff --git a/src/wing_geometry.jl b/src/wing_geometry.jl index 5f2acfa4..241b2bc3 100644 --- a/src/wing_geometry.jl +++ b/src/wing_geometry.jl @@ -580,7 +580,7 @@ Add a new section to the wing. - `aero_model`::AeroModel: [AeroModel](@ref) - `aero_data`::AeroData: See [AeroData](@ref) """ -function add_section!(wing::Wing, LE_point, +function add_section!(wing::Wing, LE_point, TE_point, aero_model::AeroModel, aero_data::AeroData=nothing) if aero_model == POLAR_VECTORS && wing.remove_nan aero_data = remove_vector_nans(aero_data) @@ -588,6 +588,7 @@ function add_section!(wing::Wing, LE_point, interpolate_matrix_nans!.(aero_data[3:5]) end push!(wing.unrefined_sections, Section(LE_point, TE_point, aero_model, aero_data)) + wing.n_unrefined_sections = Int16(length(wing.unrefined_sections)) return nothing end @@ -623,7 +624,6 @@ function update_non_deformed_sections!(wing::AbstractWing) # Populate or update non_deformed_sections if isempty(wing.non_deformed_sections) - @show length(wing.refined_sections) n_sections # Initial setup wing.non_deformed_sections = [Section() for _ in 1:n_sections] for i in 1:n_sections @@ -705,10 +705,10 @@ function refine!(wing::AbstractWing; recompute_mapping=true, sort_sections=true) n_sections = wing.n_panels + 1 if length(wing.refined_sections) == 0 - @show wing.spanwise_distribution if wing.spanwise_distribution == UNCHANGED || length(wing.unrefined_sections) == n_sections wing.refined_sections = wing.unrefined_sections + recompute_mapping && compute_refined_panel_mapping!(wing) update_non_deformed_sections!(wing) return nothing else From 662c3ebdeceefef025b308d5b10897d8011055b1 Mon Sep 17 00:00:00 2001 From: 1-Bart-1 Date: Sun, 7 Dec 2025 21:31:22 +0100 Subject: [PATCH 51/53] Add obj wing refinement test --- test/test_refinement_validation.jl | 86 ++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 test/test_refinement_validation.jl diff --git a/test/test_refinement_validation.jl b/test/test_refinement_validation.jl new file mode 100644 index 00000000..0555b707 --- /dev/null +++ b/test/test_refinement_validation.jl @@ -0,0 +1,86 @@ +using VortexStepMethod +using Test + +@testset "Refinement Validation" begin + @testset "Error when refinement forgotten" begin + wing = Wing(20; spanwise_distribution=LINEAR) + add_section!(wing, [0,10,0], [1,10,0], INVISCID) + add_section!(wing, [0,-10,0], [1,-10,0], INVISCID) + + # Should error without refinement + @test_throws ArgumentError BodyAerodynamics([wing]) + + # Should work after refinement + refine!(wing) + body_aero = BodyAerodynamics([wing]) + @test length(body_aero.panels) == 20 + end + + @testset "Multiple refine! calls work correctly" begin + wing = Wing(20; spanwise_distribution=LINEAR) + add_section!(wing, [0,10,0], [1,10,0], INVISCID) + add_section!(wing, [0,-10,0], [1,-10,0], INVISCID) + + # Multiple calls should work (not idempotent - re-refines each time) + refine!(wing) + n1 = length(wing.refined_sections) + + refine!(wing) + n2 = length(wing.refined_sections) + + @test n1 == n2 == 21 + end + + @testset "YAML wing deformation support after refinement" begin + simple_wing_file = test_data_path("yaml_geometry", "simple_wing.yaml") + wing = Wing(simple_wing_file; n_panels=4) + + # After refinement, should have non_deformed_sections + refine!(wing) + @test !isempty(wing.non_deformed_sections) + @test length(wing.non_deformed_sections) == 5 + + # unrefined_deform! should work + @test_nowarn VortexStepMethod.unrefined_deform!(wing, [0.1, 0.2], nothing) + end + + @testset "ObjWing deformation support" begin + data_dir = joinpath(dirname(@__DIR__), "data", "ram_air_kite") + wing = ObjWing( + joinpath(data_dir, "ram_air_kite_body.obj"), + joinpath(data_dir, "ram_air_kite_foil.dat"); + n_panels=20, + n_unrefined_sections=2, + prn=false + ) + + # ObjWing creates non_deformed_sections in constructor (no refine! needed) + @test !isempty(wing.non_deformed_sections) + @test length(wing.non_deformed_sections) == 21 + + # unrefined_deform! should work + @test_nowarn VortexStepMethod.unrefined_deform!(wing, deg2rad.([5.0, -5.0]), nothing) + end + + @testset "Refinement populates all required fields" begin + wing = Wing(10; spanwise_distribution=COSINE) + add_section!(wing, [0,5,0], [1,5,0], INVISCID) + add_section!(wing, [0,-5,0], [1,-5,0], INVISCID) + + refine!(wing) + + # Check refined_sections populated + @test length(wing.refined_sections) == 11 + + # Check non_deformed_sections populated + @test length(wing.non_deformed_sections) == 11 + + # Check theta_dist and delta_dist resized + @test length(wing.theta_dist) == 10 + @test length(wing.delta_dist) == 10 + + # Check all zeros initially + @test all(wing.theta_dist .== 0.0) + @test all(wing.delta_dist .== 0.0) + end +end From 56687a4d7beddf14057c17e15ce51916e3b6aec7 Mon Sep 17 00:00:00 2001 From: 1-Bart-1 Date: Sun, 7 Dec 2025 21:55:08 +0100 Subject: [PATCH 52/53] Auto refine yaml wing --- src/yaml_geometry.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/yaml_geometry.jl b/src/yaml_geometry.jl index 4df42382..e6c39514 100644 --- a/src/yaml_geometry.jl +++ b/src/yaml_geometry.jl @@ -262,6 +262,7 @@ function Wing( add_section!(wing, le_coord, te_coord, aero_model, aero_data) end + refine!(wing) return wing end From 25fb6f0bb4979da801a1b3c5b7b4709f442cec61 Mon Sep 17 00:00:00 2001 From: 1-Bart-1 Date: Wed, 10 Dec 2025 21:25:26 +0100 Subject: [PATCH 53/53] Width is sum not average --- src/solver.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/solver.jl b/src/solver.jl index 3ec3c376..88e997a2 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -386,7 +386,6 @@ function solve!(solver::Solver, body_aero::BodyAerodynamics, gamma_distribution= z_airf_unrefined_dist[target_unrefined_idx] ./= count va_unrefined_dist[target_unrefined_idx] ./= count chord_unrefined_dist[target_unrefined_idx] /= count - width_unrefined_dist[target_unrefined_idx] /= count end end unrefined_idx += wing.n_unrefined_sections