Skip to content

Commit 90ff624

Browse files
committed
#186 test passing without errors/warnings
1 parent 1510f6b commit 90ff624

17 files changed

+640
-551
lines changed

examples/bench.jl

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,11 @@ println("Rectangular wing, solve:")
5656
@time solve(vsm_solver, body_aero, nothing)
5757

5858
# Create wing geometry
59-
wing = RamAirWing("data/ram_air_kite/ram_air_kite_body.obj", "data/ram_air_kite/ram_air_kite_foil.dat"; prn=false)
59+
wing = RamAirWing(
60+
joinpath("data", "ram_air_kite", "ram_air_kite_body.obj"),
61+
joinpath("data", "ram_air_kite", "ram_air_kite_foil.dat");
62+
prn=false
63+
)
6064
body_aero = BodyAerodynamics([wing])
6165

6266
# Create solvers

examples/ram_air_kite.jl

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ DEFORM = false
99
LINEARIZE = false
1010

1111
# Create wing geometry
12-
wing = RamAirWing("data/ram_air_kite/ram_air_kite_body.obj", "data/ram_air_kite/ram_air_kite_foil.dat"; prn=PRN)
12+
wing = RamAirWing(
13+
joinpath("data", "ram_air_kite", "ram_air_kite_body.obj"),
14+
joinpath("data", "ram_air_kite", "ram_air_kite_foil.dat");
15+
prn=PRN
16+
)
1317
body_aero = BodyAerodynamics([wing];)
1418
println("First init")
1519
@time VortexStepMethod.reinit!(body_aero)

src/VortexStepMethod.jl

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export PanelDistribution, LINEAR, COSINE, COSINE_VAN_GARREL, SPLIT_PROVIDED, UNC
4040
export InitialGammaDistribution, ELLIPTIC, ZEROS
4141
export SolverStatus, FEASIBLE, INFEASIBLE, FAILURE
4242
export SolverType, LOOP, NONLIN
43+
export load_polar_data
4344

4445
export plot_geometry, plot_distribution, plot_circulation_distribution, plot_polars, save_plot, show_plot, plot_polar_data
4546

@@ -202,7 +203,9 @@ const AeroData = Union{
202203
}
203204

204205
function menu()
205-
Main.include("examples/menu.jl")
206+
# Load the examples menu using a portable path
207+
ex = joinpath(dirname(pathof(@__MODULE__)), "..", "examples", "menu.jl")
208+
Base.include(Main, normpath(ex))
206209
end
207210

208211
"""
@@ -213,11 +216,11 @@ Copy all example scripts to the folder "examples"
213216
"""
214217
function copy_examples()
215218
PATH = "examples"
216-
if ! isdir(PATH)
219+
if ! isdir(PATH)
217220
mkdir(PATH)
218221
end
219222
src_path = joinpath(dirname(pathof(@__MODULE__)), "..", PATH)
220-
copy_files("examples", readdir(src_path))
223+
copy_files(PATH, readdir(src_path))
221224
end
222225

223226
function install_examples(add_packages=true)

src/yaml_geometry.jl

Lines changed: 139 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,9 @@
1-
2-
3-
"""
4-
load_polar_data(csv_file_path)
5-
6-
Load polar data from a CSV file using only `readlines`.
7-
Expected format: header row and columns alpha, cl, cd, cm (case-insensitive, order arbitrary).
8-
- alpha: Angle of attack in degrees
9-
- cl: Lift coefficient
10-
- cd: Drag coefficient
11-
- cm: Moment coefficient
12-
13-
Returns (aero_data, POLAR_VECTORS) or (nothing, INVISCID) if missing/invalid.
14-
"""
15-
16-
# Structs for YAML parsing using StructMapping.jl
17-
@kwdef struct WingAirfoilInfo
18-
csv_file_path::String = ""
1+
# Data structures for YAML wing geometry
2+
@with_kw struct WingAirfoilInfo
3+
csv_file_path::String
194
end
205

21-
@kwdef struct WingAirfoilData
22-
airfoil_id::Int64
23-
type::String
24-
info_dict::WingAirfoilInfo
25-
end
26-
27-
@kwdef struct WingAirfoils
28-
alpha_range::Vector{Float64}
29-
reynolds::Float64
30-
headers::Vector{String}
31-
data::Vector{WingAirfoilData}
32-
end
33-
34-
@kwdef struct WingSectionData
6+
@with_kw struct WingSectionData
357
airfoil_id::Int64
368
LE_x::Float64
379
LE_y::Float64
@@ -41,115 +13,162 @@ end
4113
TE_z::Float64
4214
end
4315

44-
@kwdef struct WingSections
45-
headers::Vector{String}
46-
data::Vector{WingSectionData}
16+
@with_kw struct WingAirfoilData
17+
airfoil_id::Int64
18+
type::String
19+
info_dict::WingAirfoilInfo
4720
end
4821

49-
@kwdef struct WingGeometry
50-
wing_sections::WingSections
51-
wing_airfoils::WingAirfoils
52-
end
22+
"""
23+
load_polar_data(csv_file_path::String) -> Tuple{Union{Nothing, Tuple{Vector{Float64}, Vector{Float64}, Vector{Float64}, Vector{Float64}}}, Symbol}
24+
25+
Load aerodynamic polar data from a CSV file using only `readlines`.
26+
27+
The CSV file must contain a header row with columns for `alpha`, `cl`, `cd`, and `cm` (case-insensitive, order arbitrary). Each subsequent row should contain numeric values for these columns.
28+
29+
# Arguments
30+
- `csv_file_path::String`: Path to the CSV file containing polar data.
31+
32+
# Returns
33+
- A tuple `(aero_data, model_type)` where:
34+
- `aero_data`: A tuple of vectors `(alpha, cl, cd, cm)` if the file is valid, or `nothing` if invalid or missing.
35+
- `alpha`: Angle of attack in degrees (converted to radians internally).
36+
- `cl`: Lift coefficient.
37+
- `cd`: Drag coefficient.
38+
- `cm`: Moment coefficient.
39+
- `model_type`: `POLAR_VECTORS` if data is loaded, or `INVISCID` if not.
40+
41+
# Behavior
42+
- If the file is missing, empty, or invalid, a warning is issued and `(nothing, INVISCID)` is returned.
5343
44+
# Example
45+
```julia
46+
# Create a YAML-based wing from configuration file
47+
wing = Wing(
48+
"path/to/wing_config.yaml";
49+
n_panels=40,
50+
n_groups=4
51+
)
52+
```
53+
"""
5454
function load_polar_data(csv_file_path::String)
55-
if isempty(csv_file_path) || !isfile(csv_file_path)
56-
@warn "Polar file not found or empty path: $csv_file_path. Using INVISCID model instead."
57-
return nothing, INVISCID
55+
# Return early for empty path
56+
if isempty(csv_file_path)
57+
@warn "Empty CSV file path provided"
58+
return (nothing, INVISCID)
5859
end
60+
61+
# Check if file exists
62+
if !isfile(csv_file_path)
63+
@warn "CSV file not found: $csv_file_path"
64+
return (nothing, INVISCID)
65+
end
66+
5967
try
68+
# Read all lines from the file
6069
lines = readlines(csv_file_path)
61-
isempty(lines) && throw(ArgumentError("File is empty"))
62-
header = lowercase.(split(strip(lines[1]), ','))
63-
64-
# Create a mapping from column name to index (1-based for Julia arrays)
65-
col_map = Dict(name => i for (i, name) in enumerate(header))
66-
required = ["alpha", "cl", "cd", "cm"]
67-
missing = filter(x -> !haskey(col_map, x), required)
68-
if !isempty(missing)
69-
@warn "Missing columns $missing in $csv_file_path. Using INVISCID model instead."
70-
return nothing, INVISCID
70+
71+
# Check if file is empty
72+
if isempty(lines)
73+
@warn "CSV file is empty: $csv_file_path"
74+
return (nothing, INVISCID)
7175
end
7276

73-
# Preallocate arrays
74-
n = length(lines) - 1
75-
alpha = Vector{Float64}(undef, n)
76-
cl = Vector{Float64}(undef, n)
77-
cd = Vector{Float64}(undef, n)
78-
cm = Vector{Float64}(undef, n)
77+
# Parse header - make case insensitive
78+
header_line = strip(lines[1])
79+
if isempty(header_line)
80+
@warn "CSV file has empty header: $csv_file_path"
81+
return (nothing, INVISCID)
82+
end
7983

84+
# Split header and normalize to lowercase
85+
header_parts = map(strip lowercase, split(header_line, ','))
86+
87+
# Find column indices for required columns
88+
required_cols = ["alpha", "cl", "cd", "cm"]
89+
col_indices = Dict{String, Int}()
90+
91+
for (i, col_name) in enumerate(header_parts)
92+
if col_name in required_cols
93+
col_indices[col_name] = i
94+
end
95+
end
96+
97+
# Check if all required columns are present
98+
missing_cols = setdiff(required_cols, keys(col_indices))
99+
if !isempty(missing_cols)
100+
@warn "CSV file missing required columns: $(join(missing_cols, ", ")) in $csv_file_path"
101+
return (nothing, INVISCID)
102+
end
103+
104+
# Parse data rows
105+
data_vectors = Dict{String, Vector{Float64}}()
106+
for col in required_cols
107+
data_vectors[col] = Float64[]
108+
end
80109

110+
for line_num in 2:length(lines)
111+
line = strip(lines[line_num])
112+
isempty(line) && continue # Skip empty lines
113+
114+
parts = split(line, ',')
115+
if length(parts) != length(header_parts)
116+
@warn "Line $line_num has incorrect number of columns in $csv_file_path"
117+
return (nothing, INVISCID) # Strict: any malformed data rejects the file
118+
end
119+
120+
try
121+
for col in required_cols
122+
value_str = strip(parts[col_indices[col]])
123+
value = parse(Float64, value_str)
124+
push!(data_vectors[col], value)
125+
end
126+
catch e
127+
@warn "Failed to parse line $line_num in $csv_file_path: $e"
128+
return (nothing, INVISCID) # Strict: any parsing error rejects the file
129+
end
130+
end
81131

82-
# Read each data line
83-
for (i, line) in enumerate(lines[2:end])
84-
fields = split(strip(line), ',')
85-
alpha[i] = parse(Float64, fields[col_map["alpha"]])
86-
cl[i] = parse(Float64, fields[col_map["cl"]])
87-
cd[i] = parse(Float64, fields[col_map["cd"]])
88-
cm[i] = parse(Float64, fields[col_map["cm"]])
132+
# Check if we got any valid data
133+
if isempty(data_vectors["alpha"])
134+
@warn "No valid data rows found in $csv_file_path"
135+
return (nothing, INVISCID)
89136
end
90137

91-
# Converting to radians
92-
alpha = deg2rad.(alpha)
138+
# Convert alpha from degrees to radians and create tuple
139+
alpha_rad = deg2rad.(data_vectors["alpha"])
140+
aero_data = (alpha_rad, data_vectors["cl"], data_vectors["cd"], data_vectors["cm"])
141+
142+
return (aero_data, POLAR_VECTORS)
93143

94-
return (alpha, cl, cd, cm), POLAR_VECTORS
95144
catch e
96-
@warn "Error reading polar file $csv_file_path: $e. Using INVISCID model instead."
97-
return nothing, INVISCID
145+
@warn "Error reading CSV file $csv_file_path: $e"
146+
return (nothing, INVISCID)
98147
end
99148
end
100149

101-
102150
"""
103-
Wing(geometry_file; kwargs...)
151+
# Module: yaml_geometry.jl
104152
105-
Create a wing model from YAML configuration file with CSV polar data.
153+
This module provides functionality for parsing and handling geometry data from YAML files.
154+
It is intended for use within the VortexStepMethod.jl package to facilitate the import,
155+
validation, and manipulation of geometric configurations specified in YAML format.
106156
107-
This constructor builds a complete aerodynamic model by:
108-
1. Loading wing geometry and airfoil configurations from YAML file
109-
2. Loading aerodynamic polars from CSV files specified in the YAML
110-
3. Creating wing sections with appropriate aerodynamic models
111-
4. Setting up panel distribution and geometric properties
157+
Functions and types in this module allow users to:
158+
- Load geometry definitions from YAML files.
159+
- Convert YAML data into Julia-native structures.
160+
- Validate the integrity and consistency of imported geometry data.
112161
113-
# Arguments
114-
- `geometry_file`: Path to YAML file containing wing geometry and airfoil specifications
162+
Typical use cases include reading wing or blade geometry for aerodynamic simulations.
115163
116-
# Keyword Arguments
117-
- `n_panels=20`: Number of aerodynamic panels across wingspan
118-
- `n_groups=4`: Number of control groups
119-
- `spanwise_distribution=LINEAR`: Panel distribution type
120-
- `spanwise_direction=[0.0, 1.0, 0.0]`: Spanwise direction vector
121-
- `remove_nan=true`: Interpolate NaN values in aerodynamic data
122-
- `prn=true`: Print info messages during construction
164+
# Example
165+
```julia
166+
geometry = load_yaml_geometry("geometry.yaml")
167+
```
123168
124-
# Returns
125-
A fully initialized `Wing` instance ready for aerodynamic simulation.
126-
127-
# Example YAML format
128-
```yaml
129-
wing_sections:
130-
headers: [airfoil_id, LE_x, LE_y, LE_z, TE_x, TE_y, TE_z]
131-
data:
132-
- [1, 0.0, 10.0, 0.0, 1.0, 10.0, 0.0]
133-
- [2, 0.0, -10.0, 0.0, 1.0, -10.0, 0.0]
134-
135-
wing_airfoils:
136-
alpha_range: [-10, 31, 0.5]
137-
reynolds: 1e6
138-
headers: [airfoil_id, type, info_dict]
139-
data:
140-
- [1, polars, {csv_file_path: "polars/1.csv"}]
141-
- [2, polars, {csv_file_path: "polars/2.csv"}]
142-
```
143-
144-
# Example
145-
```julia
146-
# Create a YAML-based wing from configuration file
147-
wing = Wing(
148-
"path/to/wing_config.yaml";
149-
n_panels=40,
150-
n_groups=4
151-
)
152-
```
169+
# See Also
170+
- [`YAML`](@ref)
171+
- [`VortexStepMethod`](@ref)
153172
"""
154173
function Wing(
155174
geometry_file::String;
@@ -167,7 +186,7 @@ function Wing(
167186

168187
# Load YAML file following Uwe's suggestion
169188
data = YAML.load_file(geometry_file)
170-
189+
171190
# Convert YAML data to our struct format
172191
# Convert wing sections
173192
wing_sections_data = data["wing_sections"]
@@ -222,7 +241,9 @@ function Wing(
222241
# Load polar data and create section
223242
csv_file_path = get(airfoil_csv_map, section.airfoil_id, "")
224243
if !isempty(csv_file_path) && !isabspath(csv_file_path)
225-
# Make relative paths relative to YAML file directory
244+
# NOTE: The spanwise direction is currently restricted to [0.0, 1.0, 0.0] (the global Y axis).
245+
# This is required because downstream geometry and panel generation code assumes the spanwise axis is aligned with Y.
246+
# If you need to support arbitrary spanwise directions, refactor the geometry logic accordingly.
226247
csv_file_path = joinpath(dirname(geometry_file), csv_file_path)
227248
end
228249
aero_data, aero_model = load_polar_data(csv_file_path)

test/TestSupport.jl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module TestSupport
2+
export suppress_warnings, test_data_path, create_temp_wing_settings,
3+
get_standard_wing_file, get_complete_settings_file
4+
include("test_data_utils.jl")
5+
end

test/body_aerodynamics/test_body_aerodynamics.jl

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ using Test
55
using Logging
66

77
include("../utils.jl")
8-
include("../test_data_utils.jl")
98

109
@testset "Induction Matrix Creation" begin
1110
# Setup

0 commit comments

Comments
 (0)