diff --git a/CHANGELOG.md b/CHANGELOG.md index 532b0816..76b6fa40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +## Unreleased + +### Fixed + +- `ObjWing` fills the mesh-derived fields of the `Wing` it returns again: + `gamma_tip`, `radius`, `le_interp`, `te_interp`, `area_interp`, + `inertia_tensor` and `T_cad_body`. Building the wing through + `ObjAdapter.obj_to_yaml` + `Wing(yaml)` in v4.0.0 left all seven at their + empty defaults, so a caller reading the spanwise geometry off an OBJ wing — + `wing.le_interp[i](gamma)` is how a bridle is placed on one — hit + `nothing`, and the wing's inertia silently fell back to point masses. The + values are in the mesh's own coordinates, the frame `obj_to_yaml` cuts its + sections in, and `test/ram_geometry/test_kite_geometry.jl` asserts that. +- `ObjWing` hands the `Wing` it builds the `crease_frac` it generated the polars + about, instead of leaving it at the default 0.75, and its + `spanwise_distribution` defaults to `LINEAR`. `UNCHANGED` keeps whatever + sections the geometry file carries, so asking for more panels than it has + sections threw a `BoundsError` out of `compute_refined_panel_mapping!`. + ## VortexStepMethod v5.1.1 2026-09-12 ### Fixed diff --git a/docs/src/private_functions.md b/docs/src/private_functions.md index d7807c42..16d9ccc3 100644 --- a/docs/src/private_functions.md +++ b/docs/src/private_functions.md @@ -246,6 +246,7 @@ find_circle_center_and_radius march_edges calculate_inertia_tensor center_to_com! +seed_mesh_geometry! airfoils_from_yaml write_geometry_yaml resolve_aero_geometry diff --git a/src/obj_adapter/obj_geometry.jl b/src/obj_adapter/obj_geometry.jl index 0a5ac73a..4f49789c 100644 --- a/src/obj_adapter/obj_geometry.jl +++ b/src/obj_adapter/obj_geometry.jl @@ -370,3 +370,29 @@ function calc_inertia_y_rotation(I_b_tensor) end +""" + seed_mesh_geometry!(wing, obj_path; mass=1.0) + +Fill the mesh-derived fields of a [`Wing`](@ref VortexStepMethod.Wing) built from +`obj_path`: the `radius` and `gamma_tip` of the arc fitted through the mesh, the +`le_interp`, `te_interp` and `area_interp` interpolations over that arc, the +thin-shell `inertia_tensor` about the centre of mass for a wing of `mass` [kg], +and `T_cad_body`, the centre of mass negated. Positions are in the mesh's own +coordinates, the frame [`obj_to_yaml`](@ref) writes its sections in. +""" +function seed_mesh_geometry!(wing, obj_path; mass=1.0) + vertices, faces = read_faces(obj_path) + com = -center_to_com!(copy.(vertices), faces; prn=false) + 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) + wing.mass = mass + wing.inertia_tensor = calculate_inertia_tensor(vertices, faces, mass, com) + wing.T_cad_body .= -com + wing.gamma_tip = gamma_tip + wing.radius = radius + wing.le_interp = le_interp + wing.te_interp = te_interp + wing.area_interp = area_interp + return wing +end diff --git a/src/yaml_geometry.jl b/src/yaml_geometry.jl index 11da48ac..6a542626 100644 --- a/src/yaml_geometry.jl +++ b/src/yaml_geometry.jl @@ -443,6 +443,16 @@ polars; the default is `AirfoilAero.NeuralFoilSolver()`. By default (`remake=false`) an existing `geometry.yaml` in `output_dir` is reused, skipping the expensive polar generation. Set `remake=true` to force regeneration. + +`spanwise_distribution` defaults to `LINEAR`, so a mesh cut into fewer sections +than `n_panels + 1` is panelled into that many rather than kept as it is. +`crease_frac` is the chordwise hinge the polars are deflected about, and the +wing carries it. + +The mesh-derived fields a wing loaded from YAML alone leaves empty — `gamma_tip`, +`radius`, `le_interp`, `te_interp`, `area_interp`, `inertia_tensor` and +`T_cad_body` — are filled from the mesh by +[`ObjAdapter.seed_mesh_geometry!`](@ref). """ function ObjWing(obj_path, dat_path=nothing; n_panels::Int=56, @@ -451,13 +461,14 @@ function ObjWing(obj_path, dat_path=nothing; delta_range=-5:1:20, n_sections::Union{Nothing, Int}=nothing, spanwise_direction=[0.0, 1.0, 0.0], - spanwise_distribution=UNCHANGED, + spanwise_distribution=LINEAR, remove_nan::Bool=true, aero_solver=AirfoilAero.NeuralFoilSolver(), remake::Bool=false, output_dir::String=mktempdir(), crease_frac=0.75, verbose::Bool=false) + (!endswith(obj_path, ".obj")) && (obj_path *= ".obj") yaml_path = joinpath(output_dir, "geometry.yaml") if remake || !isfile(yaml_path) n_sec = isnothing(n_sections) ? n_panels + 1 : n_sections @@ -471,5 +482,7 @@ function ObjWing(obj_path, dat_path=nothing; crease_frac, verbose) end - return Wing(yaml_path; n_panels, spanwise_distribution, spanwise_direction, remove_nan) + wing = Wing(yaml_path; n_panels, spanwise_distribution, spanwise_direction, + remove_nan, crease_frac) + return ObjAdapter.seed_mesh_geometry!(wing, obj_path) end diff --git a/test/ram_geometry/test_kite_geometry.jl b/test/ram_geometry/test_kite_geometry.jl index 4c9fdc8d..37d269db 100644 --- a/test/ram_geometry/test_kite_geometry.jl +++ b/test/ram_geometry/test_kite_geometry.jl @@ -167,11 +167,45 @@ using Serialization @test R_b_p2 ≈ I(3) end - @testset "Converted-wing construction and deformation" begin - # TODO: redesign. These previously tested ObjWing internals (radius, - # gamma_tip, UNCHANGED distribution, obj deform\!) that were dropped when - # ObjWing was replaced by convert-then-load (obj_to_matrix_yaml -> Wing). - # Rebuild against ram_air_matrix_wing() geometry once its numerics are set. - @test_skip false + @testset "ObjWing carries the mesh geometry its sections are cut from" begin + gen_dir, _ = ram_air_matrix_dir() + obj = joinpath(dirname(dirname(@__DIR__)), "data", "ram_air_kite", + "ram_air_kite_body.obj") + wing = ObjWing(obj; n_panels=20, spanwise_distribution=LINEAR, + output_dir=gen_dir, verbose=false) + + @test wing.gamma_tip > 0 + @test wing.radius > 0 + @test !isnothing(wing.le_interp) + @test !isnothing(wing.te_interp) + @test !isnothing(wing.area_interp) + @test wing.area_interp(wing.gamma_tip) > 0 + + vertices, faces = read_faces(obj) + com = -center_to_com!(vertices, faces) + @test wing.T_cad_body ≈ -com + @test wing.inertia_tensor ≈ wing.inertia_tensor' + @test all(>(0), diag(wing.inertia_tensor)) + + # The interpolations must be in the frame the sections are cut in: at the + # centre they land on the centre section, not a centre-of-mass away from it. + le_points = [section.LE_point for section in wing.refined_sections] + spacing = maximum(abs(le_points[i + 1][2] - le_points[i][2]) + for i in 1:length(le_points) - 1) + le_center = [wing.le_interp[i](0.0) for i in 1:3] + nearest = argmin(le_point -> abs(le_point[2] - le_center[2]), le_points) + @test norm(nearest - le_center) < spacing + end + + @testset "ObjWing panels a coarser cut and flies the crease it generated" begin + gen_dir, _ = ram_air_matrix_dir() # four sections + obj = joinpath(dirname(dirname(@__DIR__)), "data", "ram_air_kite", + "ram_air_kite_body.obj") + wing = ObjWing(obj; n_panels=20, crease_frac=0.82, + output_dir=gen_dir, verbose=false) + + @test wing.n_unrefined_sections == 4 + @test length(wing.refined_sections) == 21 + @test wing.crease_frac == 0.82 end end