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
194end
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
4214end
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
4720end
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+ """
5454function 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
99148end
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"""
154173function 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)
0 commit comments