Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/src/private_functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 26 additions & 0 deletions src/obj_adapter/obj_geometry.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
17 changes: 15 additions & 2 deletions src/yaml_geometry.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -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
46 changes: 40 additions & 6 deletions test/ram_geometry/test_kite_geometry.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading