Skip to content

Commit c146a9e

Browse files
authored
Add an option to align body frame with principal axes (#134)
* Add option to align frames * Read file better * Succesful alignment * Test alignment * Fix example * Dont use expensive rotate * Error on non-triangular face
1 parent 257b525 commit c146a9e

File tree

2 files changed

+140
-65
lines changed

2 files changed

+140
-65
lines changed

src/kite_geometry.jl

Lines changed: 124 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -111,10 +111,13 @@ Create interpolation functions for leading/trailing edges and area.
111111
- Tuple of (le_interp, te_interp, area_interp) interpolation functions
112112
- Where le_interp and te_interp are tuples themselves, containing the x, y and z interpolations
113113
"""
114-
function create_interpolations(vertices, circle_center_z, radius, gamma_tip)
115-
gamma_range = range(-gamma_tip+1e-6, gamma_tip-1e-6, 100)
114+
function create_interpolations(vertices, circle_center_z, radius, gamma_tip, R; interp_steps=40)
115+
gamma_range = range(-gamma_tip+1e-6, gamma_tip-1e-6, interp_steps)
116+
stepsize = gamma_range.step.hi
116117
vz_centered = [v[3] - circle_center_z for v in vertices]
117118

119+
te_gammas = zeros(length(gamma_range))
120+
le_gammas = zeros(length(gamma_range))
118121
trailing_edges = zeros(3, length(gamma_range))
119122
leading_edges = zeros(3, length(gamma_range))
120123
areas = zeros(length(gamma_range))
@@ -124,59 +127,90 @@ function create_interpolations(vertices, circle_center_z, radius, gamma_tip)
124127
leading_edges[1, j] = Inf
125128
for (i, v) in enumerate(vertices)
126129
# Rotate y coordinate to check box containment
127-
rotated_y = v[2] * cos(-gamma) - vz_centered[i] * sin(-gamma)
128-
if gamma 0.0 && 0.0 rotated_y 0.5
130+
# rotated_y = v[2] * cos(-gamma) - vz_centered[i] * sin(-gamma)
131+
gamma_v = atan(-v[2], vz_centered[i])
132+
if gamma < 0 && gamma - stepsize gamma_v gamma
129133
if v[1] > trailing_edges[1, j]
130134
trailing_edges[:, j] .= v
135+
te_gammas[j] = gamma_v
131136
end
132137
if v[1] < leading_edges[1, j]
133138
leading_edges[:, j] .= v
139+
le_gammas[j] = gamma_v
134140
end
135-
elseif gamma > 0.0 && -0.5 rotated_y 0.0
141+
elseif gamma > 0 && gamma gamma_v gamma + stepsize
136142
if v[1] > trailing_edges[1, j]
137143
trailing_edges[:, j] .= v
144+
te_gammas[j] = gamma_v
138145
end
139146
if v[1] < leading_edges[1, j]
140147
leading_edges[:, j] .= v
148+
le_gammas[j] = gamma_v
141149
end
142150
end
143151
end
144-
area = norm(leading_edges[:, j] - trailing_edges[:, j]) * gamma_range.step * radius
152+
area = norm(leading_edges[:, j] - trailing_edges[:, j]) * stepsize * radius
145153
last_area = j > 1 ? areas[j-1] : 0.0
146154
areas[j] = last_area + area
147155
end
148156

149-
le_interp = ntuple(i -> linear_interpolation(gamma_range, leading_edges[i, :],
157+
for j in eachindex(gamma_range)
158+
leading_edges[:, j] .= R * leading_edges[:, j]
159+
trailing_edges[:, j] .= R * trailing_edges[:, j]
160+
end
161+
162+
le_interp = ntuple(i -> linear_interpolation(te_gammas, leading_edges[i, :],
150163
extrapolation_bc=Line()), 3)
151-
te_interp = ntuple(i -> linear_interpolation(gamma_range, trailing_edges[i, :],
164+
te_interp = ntuple(i -> linear_interpolation(le_gammas, trailing_edges[i, :],
152165
extrapolation_bc=Line()), 3)
153166
area_interp = linear_interpolation(gamma_range, areas, extrapolation_bc=Line())
154167

155168
return (le_interp, te_interp, area_interp)
156169
end
157170

158-
# Makes the zero coordinates lie on the com
171+
"""
172+
center_to_com!(vertices, faces)
173+
174+
Calculate center of mass of a mesh and translate vertices so that COM is at origin.
175+
176+
# Arguments
177+
- `vertices`: Vector of 3D point coordinates
178+
- `faces`: Vector of vertex indices for each face (can be triangular or non-triangular)
179+
180+
# Returns
181+
- Vector representing the original center of mass before translation
182+
183+
# Notes
184+
- Non-triangular faces are automatically triangulated into triangles
185+
- Assumes uniform surface density
186+
"""
159187
function center_to_com!(vertices, faces)
160188
area_total = 0.0
161189
com = zeros(3)
162190

163191
for face in faces
164-
v1 = vertices[face[1]]
165-
v2 = vertices[face[2]]
166-
v3 = vertices[face[3]]
167-
168-
# Calculate triangle area and centroid
169-
normal = cross(v2 - v1, v3 - v1)
170-
area = norm(normal) / 2
171-
centroid = (v1 + v2 + v3) / 3
172-
173-
area_total += area
174-
com += area * centroid
192+
if length(face) == 3
193+
# Triangle case
194+
v1 = vertices[face[1]]
195+
v2 = vertices[face[2]]
196+
v3 = vertices[face[3]]
197+
198+
# Calculate triangle area and centroid
199+
normal = cross(v2 - v1, v3 - v1)
200+
area = norm(normal) / 2
201+
centroid = (v1 + v2 + v3) / 3
202+
203+
area_total += area
204+
com += area * centroid
205+
else
206+
throw(ArgumentError("Triangulate faces in a CAD program first"))
207+
end
175208
end
176209

177210
com = com / area_total
178211
!(abs(com[2]) < 0.01) && throw(ArgumentError("Center of mass $com of .obj file has to lie on the xz-plane."))
179212
@info "Centering vertices of .obj file to the center of mass: $com"
213+
com[2] = 0.0
180214
for v in vertices
181215
v .-= com
182216
end
@@ -245,6 +279,40 @@ function calculate_inertia_tensor(vertices, faces, mass, com)
245279
return (mass / total_area) * I / 3
246280
end
247281

282+
function calc_inertia_y_rotation(I_b_tensor)
283+
# Function for nonlinear solver - off-diagonal element should be zero
284+
function eq!(F, theta, _)
285+
# Rotation matrix around y-axis
286+
R_y = [
287+
cos(theta[1]) 0 sin(theta[1]);
288+
0 1 0;
289+
-sin(theta[1]) 0 cos(theta[1])
290+
]
291+
# Transform inertia tensor
292+
I_rotated = R_y * I_b_tensor * R_y'
293+
# We want the off-diagonal xz elements to be zero
294+
F[1] = I_rotated[1,3]
295+
end
296+
297+
theta0 = [0.0]
298+
prob = NonlinearProblem(eq!, theta0, nothing)
299+
sol = NonlinearSolve.solve(prob, NewtonRaphson())
300+
theta_opt = sol.u[1]
301+
302+
R_b_p = [
303+
cos(theta_opt) 0 sin(theta_opt);
304+
0 1 0;
305+
-sin(theta_opt) 0 cos(theta_opt)
306+
]
307+
@show theta_opt
308+
# Calculate diagonalized inertia tensor
309+
I_diag = R_b_p * I_b_tensor * R_b_p'
310+
@show I_diag
311+
@assert isapprox(I_diag[1,3], 0.0, atol=1e-5)
312+
@show R_b_p
313+
return I_diag, R_b_p
314+
end
315+
248316
"""
249317
interpolate_matrix_nans!(matrix::Matrix{Float64})
250318
@@ -330,7 +398,6 @@ mutable struct RamAirWing <: AbstractWing
330398
# Additional fields for RamAirWing
331399
non_deformed_sections::Vector{Section}
332400
mass::Float64
333-
circle_center_z::Float64
334401
gamma_tip::Float64
335402
inertia_tensor::Matrix{Float64}
336403
center_of_mass::Vector{Float64}
@@ -367,8 +434,8 @@ Constructor for a [RamAirWing](@ref) that allows to use an `.obj` and a `.dat` f
367434
"""
368435
function RamAirWing(obj_path, dat_path; alpha=0.0, crease_frac=0.75, wind_vel=10., mass=1.0,
369436
n_panels=54, n_sections=n_panels+1, spanwise_panel_distribution=UNCHANGED,
370-
spanwise_direction=[0.0, 1.0, 0.0], remove_nan=true,
371-
alpha_range=deg2rad.(-5:1:20), delta_range=deg2rad.(-5:1:20)
437+
spanwise_direction=[0.0, 1.0, 0.0], remove_nan=true, align_to_principal=false,
438+
alpha_range=deg2rad.(-5:1:20), delta_range=deg2rad.(-5:1:20), interp_steps=40
372439
)
373440

374441
!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"))
@@ -382,38 +449,48 @@ function RamAirWing(obj_path, dat_path; alpha=0.0, crease_frac=0.75, wind_vel=10
382449
(!isfile(obj_path)) && error("OBJ file not found: $obj_path")
383450
info_path = obj_path[1:end-4] * "_info.bin"
384451

385-
if !ispath(polar_path) || !ispath(info_path)
452+
if true || !ispath(info_path)
386453
@info "Reading $obj_path"
387454
vertices, faces = read_faces(obj_path)
388455
center_of_mass = center_to_com!(vertices, faces)
389456
inertia_tensor = calculate_inertia_tensor(vertices, faces, mass, zeros(3))
390457

391-
circle_center_z, radius, gamma_tip = find_circle_center_and_radius(vertices)
392-
le_interp, te_interp, area_interp = create_interpolations(vertices, circle_center_z, radius, gamma_tip)
458+
if align_to_principal
459+
inertia_tensor, R_b_p = calc_inertia_y_rotation(inertia_tensor)
460+
circle_center_z, radius, gamma_tip = find_circle_center_and_radius(vertices)
461+
le_interp, te_interp, area_interp = create_interpolations(vertices, circle_center_z, radius, gamma_tip, R_b_p; interp_steps)
462+
else
463+
circle_center_z, radius, gamma_tip = find_circle_center_and_radius(vertices)
464+
le_interp, te_interp, area_interp = create_interpolations(vertices, circle_center_z, radius, gamma_tip, I(3); interp_steps)
465+
end
466+
393467
@info "Writing $info_path"
394-
serialize(info_path, (inertia_tensor, center_of_mass, circle_center_z, radius, gamma_tip,
468+
serialize(info_path, (inertia_tensor, center_of_mass, radius, gamma_tip,
395469
le_interp, te_interp, area_interp))
396-
397-
width = 2gamma_tip * radius
398-
area = area_interp(gamma_tip)
399-
@eval Main begin
400-
foil_path, polar_path, v_wind, area, width, x_turn, alpha_range, delta_range =
401-
$dat_path, $polar_path, $wind_vel, $gamma_tip, $width, $crease_frac, $alpha_range, $delta_range
402-
include(joinpath(dirname(@__FILE__), "../scripts/polars.jl"))
403-
end
404470
end
405471

406-
@info "Loading polars and kite info from $polar_path and $info_path"
472+
@info "Loading kite info from $info_path and polars from $polar_path"
407473
try
474+
(inertia_tensor::Matrix, center_of_mass::Vector,
475+
radius::Real, gamma_tip::Real, le_interp, te_interp, area_interp) = deserialize(info_path)
476+
477+
if !ispath(polar_path)
478+
width = 2gamma_tip * radius
479+
area = area_interp(gamma_tip)
480+
@eval Main begin
481+
foil_path, polar_path, v_wind, area, width, x_turn, alpha_range, delta_range =
482+
$dat_path, $polar_path, $wind_vel, $gamma_tip, $width, $crease_frac, $alpha_range, $delta_range
483+
include(joinpath(dirname(@__FILE__), "../scripts/polars.jl"))
484+
end
485+
end
486+
408487
(alpha_range, delta_range, cl_matrix::Matrix, cd_matrix::Matrix, cm_matrix::Matrix) = deserialize(polar_path)
409488
if remove_nan
410489
interpolate_matrix_nans!(cl_matrix)
411490
interpolate_matrix_nans!(cd_matrix)
412491
interpolate_matrix_nans!(cm_matrix)
413492
end
414-
(inertia_tensor::Matrix, center_of_mass::Vector, circle_center_z::Real,
415-
radius::Real, gamma_tip::Real, le_interp, te_interp, area_interp) = deserialize(info_path)
416-
493+
417494
# Create sections
418495
sections = Section[]
419496
refined_sections = Section[]
@@ -426,14 +503,15 @@ function RamAirWing(obj_path, dat_path; alpha=0.0, crease_frac=0.75, wind_vel=10
426503
push!(refined_sections, Section(LE_point, TE_point, POLAR_MATRICES, aero_data))
427504
push!(non_deformed_sections, Section(LE_point, TE_point, POLAR_MATRICES, aero_data))
428505
end
429-
506+
430507
RamAirWing(n_panels, spanwise_panel_distribution, spanwise_direction, sections,
431508
refined_sections, remove_nan, non_deformed_sections,
432-
mass, circle_center_z, gamma_tip, inertia_tensor, center_of_mass, radius,
509+
mass, gamma_tip, inertia_tensor, center_of_mass, radius,
433510
le_interp, te_interp, area_interp, zeros(n_panels), zeros(n_panels))
434-
catch e
511+
512+
catch
435513
if e isa BoundsError
436-
@error "Delete $polar_path and $info_path and try again."
514+
@error "Delete $info_path and $polar_path and try again."
437515
end
438516
rethrow(e)
439517
end
@@ -460,32 +538,15 @@ function deform!(wing::RamAirWing, theta_dist::AbstractVector, delta_dist::Abstr
460538

461539
local_y = zeros(MVec3)
462540
chord = zeros(MVec3)
541+
normal = zeros(MVec3)
463542

464543
for i in 1:wing.n_panels
465544
section1 = wing.non_deformed_sections[i]
466545
section2 = wing.non_deformed_sections[i+1]
467546
local_y .= normalize(section1.LE_point - section2.LE_point)
468547
chord .= section1.TE_point .- section1.LE_point
469-
wing.sections[i].TE_point .= section1.LE_point .+ rotate_v_around_k(chord, local_y, wing.theta_dist[i])
548+
normal .= chord × local_y
549+
@. wing.sections[i].TE_point = section1.LE_point + cos(wing.theta_dist[i]) * chord - sin(wing.theta_dist[i]) * normal
470550
end
471551
return nothing
472552
end
473-
474-
"""
475-
rotate_v_around_k(v, k, θ)
476-
477-
Rotate vector v around axis k by angle θ using Rodrigues' rotation formula.
478-
479-
# Arguments
480-
- `v`: Vector to rotate
481-
- `k`: Rotation axis (will be normalized)
482-
- `θ`: Rotation angle in radians
483-
484-
# Returns
485-
- Rotated vector
486-
"""
487-
function rotate_v_around_k(v, k, θ)
488-
k = normalize(k)
489-
v_rot = v * cos(θ) + (k × v) * sin(θ) + k * (k v) * (1 - cos(θ))
490-
return v_rot
491-
end

test/test_kite_geometry.jl

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11

22
using Test
33
using VortexStepMethod
4-
using VortexStepMethod: create_interpolations, find_circle_center_and_radius, calculate_inertia_tensor, center_to_com!, read_faces
4+
using VortexStepMethod: create_interpolations, find_circle_center_and_radius, calculate_inertia_tensor, center_to_com!, read_faces, calc_inertia_y_rotation
55
using LinearAlgebra
66
using Interpolations
77
using Serialization
@@ -126,7 +126,7 @@ using Serialization
126126

127127
# Create info file
128128
info_path = test_obj_path[1:end-4] * "_info.bin"
129-
le_interp, te_interp, area_interp = create_interpolations(vertices, z_center, r, π/4)
129+
le_interp, te_interp, area_interp = create_interpolations(vertices, z_center, r, π/4, I(3))
130130
center_of_mass = center_to_com!(vertices, faces)
131131
inertia_tensor = calculate_inertia_tensor(vertices, faces, 1.0, zeros(3))
132132

@@ -147,6 +147,20 @@ using Serialization
147147
@test isapprox([le_interp[i](0.0) for i in 1:3], [0.0, 0.0, r+z_center], atol=0.03)
148148
@test isapprox([te_interp[i](0.0) for i in 1:3], [1.0, 0.0, r+z_center], atol=0.03)
149149
end
150+
151+
@testset "Alignment to principal frame" begin
152+
vertices, faces = read_faces(test_obj_path)
153+
center_of_mass = center_to_com!(vertices, faces)
154+
inertia_tensor_b = calculate_inertia_tensor(vertices, faces, 1.0, zeros(3))
155+
inertia_tensor_p, R_b_p = calc_inertia_y_rotation(inertia_tensor_b)
156+
for v in vertices
157+
v .= R_b_p * v
158+
end
159+
inertia_tensor_b2 = calculate_inertia_tensor(vertices, faces, 1.0, zeros(3))
160+
inertia_tensor_p2, R_b_p2 = calc_inertia_y_rotation(inertia_tensor_b2)
161+
@test inertia_tensor_p inertia_tensor_p2
162+
@test R_b_p2 I(3)
163+
end
150164

151165
@testset "RamAirWing Construction" begin
152166
wing = RamAirWing(test_obj_path, test_dat_path; remove_nan=true)

0 commit comments

Comments
 (0)