Skip to content

Commit 367728c

Browse files
committed
Add linearization tests
1 parent 619044e commit 367728c

File tree

5 files changed

+200
-27
lines changed

5 files changed

+200
-27
lines changed

examples/ram_air_kite.jl

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ using ControlPlots
22
using VortexStepMethod
33
using LinearAlgebra
44

5-
PLOT = false
5+
PLOT = true
66
USE_TEX = false
77
DEFORM = false
8-
LINEARIZE = true
8+
LINEARIZE = false
99

1010
# Create wing geometry
1111
wing = RamAirWing("data/ram_air_kite_body.obj", "data/ram_air_kite_foil.dat")
@@ -47,16 +47,14 @@ if LINEARIZE
4747
jac, res = VortexStepMethod.linearize(
4848
solver,
4949
body_aero,
50-
wing,
51-
[zeros(4); vel_app; zeros(3)];
52-
theta_idxs=1:4,
53-
va_idxs=5:7,
50+
[zeros(4); vel_app; zeros(3)];
51+
theta_idxs=1:4,
52+
va_idxs=5:7,
5453
omega_idxs=8:10,
5554
moment_frac=0.1)
5655
@time jac, res = VortexStepMethod.linearize(
5756
solver,
5857
body_aero,
59-
wing,
6058
[zeros(4); vel_app; zeros(3)];
6159
theta_idxs=1:4,
6260
va_idxs=5:7,
@@ -68,7 +66,7 @@ end
6866
PLOT && plot_polar_data(body_aero)
6967

7068
# Plotting geometry
71-
plot_geometry(
69+
PLOT && plot_geometry(
7270
body_aero,
7371
"";
7472
data_type=".svg",

src/kite_geometry.jl

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -455,7 +455,7 @@ function RamAirWing(
455455
(!isfile(obj_path)) && error("OBJ file not found: $obj_path")
456456
info_path = obj_path[1:end-4] * "_info.bin"
457457

458-
if true || !ispath(info_path)
458+
if !ispath(info_path)
459459
@info "Reading $obj_path"
460460
vertices, faces = read_faces(obj_path)
461461
center_of_mass = center_to_com!(vertices, faces)
@@ -467,7 +467,6 @@ function RamAirWing(
467467
le_interp, te_interp, area_interp = create_interpolations(vertices, circle_center_z, radius, gamma_tip, R_b_p; interp_steps)
468468
else
469469
circle_center_z, radius, gamma_tip = find_circle_center_and_radius(vertices)
470-
@show circle_center_z radius gamma_tip interp_steps
471470
le_interp, te_interp, area_interp = create_interpolations(vertices, circle_center_z, radius, gamma_tip, I(3); interp_steps)
472471
end
473472

src/solver.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -676,14 +676,15 @@ jac, results = linearize(
676676
)
677677
```
678678
"""
679-
function linearize(solver::Solver, body_aero::BodyAerodynamics, wing::RamAirWing, y::Vector{T};
679+
function linearize(solver::Solver, body_aero::BodyAerodynamics, y::Vector{T};
680680
theta_idxs=1:4,
681681
delta_idxs=nothing,
682682
va_idxs=nothing,
683683
omega_idxs=nothing,
684684
kwargs...) where T
685685

686686
!(length(body_aero.wings) == 1) && throw(ArgumentError("Linearization only works for a body_aero with one wing"))
687+
wing = body_aero.wings[1]
687688

688689
init_va = body_aero.cache[1][body_aero.va]
689690
init_va .= body_aero.va

test/bench.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ using LinearAlgebra
6060
@testset "Re-initialization" begin
6161
result = @benchmark init!($unchanged_body_aero; init_aero=false) samples=1 evals=1
6262
@info "Re-initializing Allocations: $(result.allocs) \t Memory: $(result.memory)"
63-
@test result.allocs < 100
63+
@test result.allocs 50
6464
end
6565

6666
vel_app = [cos(alpha), 0.0, sin(alpha)] .* v_a

test/test_results.jl

Lines changed: 190 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,218 @@
11
using VortexStepMethod
2-
using VortexStepMethod: calculate_cl, calculate_cd_cm, calculate_projected_area, calculate_AIC_matrices!
2+
using VortexStepMethod: calculate_cl, calculate_cd_cm, calculate_projected_area, calculate_AIC_matrices!, init!
33
using LinearAlgebra
44
using Test
55
using Logging
66

7-
# if !@isdefined ram_wing
7+
if !@isdefined ram_wing
88
cp("data/ram_air_kite_body.obj", "/tmp/ram_air_kite_body.obj"; force=true)
99
cp("data/ram_air_kite_foil.dat", "/tmp/ram_air_kite_foil.dat"; force=true)
10-
ram_wing = RamAirWing("/tmp/ram_air_kite_body.obj", "/tmp/ram_air_kite_foil.dat")
11-
# end
10+
ram_wing = RamAirWing("/tmp/ram_air_kite_body.obj", "/tmp/ram_air_kite_foil.dat"; alpha_range=deg2rad.(-1:1), delta_range=deg2rad.(-1:1))
11+
end
12+
13+
# @testset "Nonlinear vs Linear" begin
14+
# va = [15.0, 0.0, 0.0]
15+
# theta = zeros(4)
16+
# delta = zeros(4)
17+
# omega = zeros(3)
18+
19+
# body_aero = BodyAerodynamics([ram_wing]; va)
20+
# solver = Solver(body_aero;
21+
# aerodynamic_model_type=VSM,
22+
# is_with_artificial_damping=false,
23+
# solver_type=NONLIN
24+
# )
1225

13-
# ram_wing = RamAirWing("data/ram_air_kite_body.obj", "data/ram_air_kite_foil.dat")
26+
# jac, lin_res = VortexStepMethod.linearize(
27+
# solver,
28+
# body_aero,
29+
# ram_wing,
30+
# [theta; va; omega; delta];
31+
# theta_idxs=1:4,
32+
# va_idxs=5:7,
33+
# omega_idxs=8:10,
34+
# delta_idxs=11:14,
35+
# moment_frac=0.1
36+
# )
37+
# nonlin_res = VortexStepMethod.solve!(solver, body_aero; log=true)
38+
# nonlin_res = [solver.sol.force; solver.sol.moment; solver.sol.group_moment_dist]
1439

15-
@testset "Nonlinear vs Linear" begin
40+
# @test nonlin_res ≈ lin_res
41+
42+
# dva = [0.01, 0.01, 0.01]
43+
# init!(body_aero; init_aero=false, va=va+dva)
44+
# nonlin_res = VortexStepMethod.solve!(solver, body_aero; log=true)
45+
# nonlin_res = [solver.sol.force; solver.sol.moment; solver.sol.group_moment_dist]
46+
# @test lin_res ≉ nonlin_res atol=1e-2
47+
# @test lin_res + jac * [zeros(4); dva; zeros(3); zeros(4)] ≈ nonlin_res atol=1e-2
48+
# # Test accuracy
49+
# @test norm(lin_res + jac * [zeros(4); dva; zeros(3); zeros(4)] - nonlin_res) /
50+
# norm(lin_res - nonlin_res) < 2e-3
51+
# end
52+
53+
@testset "Nonlinear vs Linear - Comprehensive Input Testing" begin
54+
# Initialize with base parameters
1655
va = [15.0, 0.0, 0.0]
1756
theta = zeros(4)
1857
delta = zeros(4)
1958
omega = zeros(3)
20-
21-
body_aero = BodyAerodynamics([ram_wing]; va)
59+
60+
# Define perturbation magnitudes
61+
dva_magnitudes = [0.01, 0.01, 0.01] # Velocity perturbations (m/s)
62+
dtheta_magnitudes = [deg2rad(0.1), deg2rad(0.5), deg2rad(1.0), deg2rad(2.0)] # Angle perturbations (rad)
63+
ddelta_magnitudes = [deg2rad(0.1), deg2rad(0.5), deg2rad(1.0), deg2rad(2.0)] # Trailing edge perturbations (rad)
64+
domega_magnitudes = [deg2rad(0.1), deg2rad(0.5), deg2rad(1.0)] # Angular rate perturbations (rad/s)
65+
66+
# Create body aerodynamics and solver
67+
VortexStepMethod.group_deform!(ram_wing, theta, delta; smooth=false)
68+
body_aero = BodyAerodynamics([ram_wing]; va, omega)
2269
solver = Solver(body_aero;
2370
aerodynamic_model_type=VSM,
2471
is_with_artificial_damping=false,
72+
atol=1e-8,
73+
rtol=1e-8,
2574
solver_type=NONLIN
2675
)
27-
28-
jac, res = VortexStepMethod.linearize(
76+
77+
# Get initial Jacobian and linearization result
78+
base_inputs = [theta; va; omega; delta]
79+
jac, lin_res = VortexStepMethod.linearize(
2980
solver,
3081
body_aero,
31-
ram_wing,
32-
[theta; va; omega; delta];
82+
base_inputs;
3383
theta_idxs=1:4,
3484
va_idxs=5:7,
3585
omega_idxs=8:10,
3686
delta_idxs=11:14,
3787
moment_frac=0.1
3888
)
39-
results = VortexStepMethod.solve!(solver, body_aero; log=true)
40-
41-
@show jac res
89+
90+
# Verify that linearization results match nonlinear results at operating point
91+
baseline_res = VortexStepMethod.solve!(solver, body_aero; log=false)
92+
baseline_res = [solver.sol.force; solver.sol.moment; solver.sol.group_moment_dist]
93+
@test baseline_res lin_res
94+
95+
# Define test cases
96+
test_cases = [
97+
("va", 5:7, dva_magnitudes),
98+
("theta", 1:4, dtheta_magnitudes),
99+
("omega", 8:10, domega_magnitudes),
100+
("delta", 11:14, ddelta_magnitudes)
101+
]
102+
103+
# For each test case
104+
for (input_name, indices, magnitudes) in test_cases
105+
@testset "Input: $input_name" begin
106+
max_error_ratio = 0.0
107+
108+
# Select one index at a time for focused testing
109+
for idx in indices
110+
# Test each magnitude
111+
for mag in magnitudes
112+
# Prepare perturbation vector
113+
perturbation = zeros(length(base_inputs))
114+
115+
if length(indices) > 1 && input_name != "theta" && input_name != "delta"
116+
# For vector quantities (va, omega), perturb all components together
117+
perturbation[indices] .= mag
118+
else
119+
# For control angles, perturb one at a time
120+
perturbation[idx] = mag
121+
end
122+
123+
# Reset to baseline
124+
reset_va = copy(va)
125+
reset_theta = copy(theta)
126+
reset_delta = copy(delta)
127+
reset_omega = zeros(3)
128+
129+
# Apply the perturbation to the nonlinear model
130+
if input_name == "va"
131+
reset_va = va + perturbation[5:7]
132+
elseif input_name == "theta"
133+
reset_theta = theta + perturbation[1:4]
134+
elseif input_name == "delta"
135+
reset_delta = delta + perturbation[11:14]
136+
elseif input_name == "omega"
137+
reset_omega = perturbation[8:10]
138+
else
139+
throw(ArgumentError())
140+
end
141+
VortexStepMethod.group_deform!(ram_wing, reset_theta, reset_delta; smooth=false)
142+
init!(body_aero; init_aero=false, va=reset_va, omega=reset_omega)
143+
144+
# Get nonlinear solution
145+
nonlin_res = VortexStepMethod.solve!(solver, body_aero, nothing; log=false)
146+
nonlin_res = [solver.sol.force; solver.sol.moment; solver.sol.group_moment_dist]
147+
@test nonlin_res baseline_res
148+
149+
# Compute linearized prediction
150+
lin_prediction = lin_res + jac * perturbation
151+
152+
# Calculate error ratio for this case
153+
prediction_error = norm(lin_prediction - nonlin_res)
154+
baseline_difference = norm(baseline_res - nonlin_res)
155+
error_ratio = prediction_error / baseline_difference
42156

157+
# Update max error ratio
158+
max_error_ratio = max(max_error_ratio, error_ratio)
159+
160+
# For small perturbations, test that error ratio is small
161+
if idx == first(indices) && mag == first(magnitudes)
162+
@test error_ratio < 2e-3
163+
end
164+
end
165+
end
166+
167+
@info "Max error ratio for $input_name: $max_error_ratio"
168+
end
169+
end
170+
171+
# Test combinations of input variations
172+
@testset "Combined Input Variations" begin
173+
# Sample some key combinations
174+
combination_tests = [
175+
("Theta + VA", [dtheta_magnitudes[1] * ones(4); dva_magnitudes[1] * ones(3); zeros(3); zeros(4)]),
176+
("Theta + Omega", [dtheta_magnitudes[1] * ones(4); zeros(3); domega_magnitudes[1] * ones(3); zeros(4)]),
177+
("VA + Omega", [zeros(4); dva_magnitudes[1] * ones(3); domega_magnitudes[1] * ones(3); zeros(4)]),
178+
("Delta + VA", [zeros(4); dva_magnitudes[1] * ones(3); zeros(3); ddelta_magnitudes[1] * ones(4)]),
179+
("All Inputs", [dtheta_magnitudes[1] * ones(4); dva_magnitudes[1] * ones(3);
180+
domega_magnitudes[1] * ones(3); ddelta_magnitudes[1] * ones(4)])
181+
]
182+
183+
for (combo_name, perturbation) in combination_tests
184+
# Reset to baseline
185+
VortexStepMethod.group_deform!(ram_wing, theta, delta; smooth=false)
186+
init!(body_aero; init_aero=false, va=va, omega=zeros(3))
187+
188+
# Extract components
189+
perturbed_theta = theta + perturbation[1:4]
190+
perturbed_va = va + perturbation[5:7]
191+
perturbed_omega = perturbation[8:10]
192+
perturbed_delta = delta + perturbation[11:14]
193+
194+
# Apply to nonlinear model
195+
VortexStepMethod.group_deform!(ram_wing, perturbed_theta, perturbed_delta; smooth=false)
196+
init!(body_aero; init_aero=false, va=perturbed_va, omega=perturbed_omega)
197+
198+
# Get nonlinear solution
199+
nonlin_res = VortexStepMethod.solve!(solver, body_aero; log=false)
200+
nonlin_res = [solver.sol.force; solver.sol.moment; solver.sol.group_moment_dist]
201+
202+
# Compute linearized prediction
203+
lin_prediction = lin_res + jac * perturbation
204+
205+
# Calculate error ratio
206+
prediction_error = norm(lin_prediction - nonlin_res)
207+
baseline_difference = norm(lin_res - nonlin_res)
208+
error_ratio = prediction_error / baseline_difference
209+
210+
@info "$combo_name error ratio: $error_ratio"
211+
212+
# Test combinations
213+
@test lin_res nonlin_res atol=1e-2
214+
@test lin_prediction nonlin_res rtol=0.2 atol=0.05
215+
@test error_ratio < 2e-3
216+
end
217+
end
43218
end

0 commit comments

Comments
 (0)