gmshairfoil2d 0.2.2__py3-none-any.whl → 0.2.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gmshairfoil2d/__init__.py +3 -0
- gmshairfoil2d/__main__.py +6 -0
- gmshairfoil2d/airfoil_func.py +126 -71
- gmshairfoil2d/config_handler.py +198 -0
- gmshairfoil2d/geometry_def.py +331 -277
- gmshairfoil2d/gmshairfoil2d.py +265 -29
- {gmshairfoil2d-0.2.2.dist-info → gmshairfoil2d-0.2.3.dist-info}/METADATA +104 -16
- gmshairfoil2d-0.2.3.dist-info/RECORD +16 -0
- tests/test_airfoil_func.py +52 -1
- tests/test_config_handler.py +260 -0
- tests/test_geometry_def.py +11 -7
- gmshairfoil2d-0.2.2.dist-info/RECORD +0 -13
- {gmshairfoil2d-0.2.2.dist-info → gmshairfoil2d-0.2.3.dist-info}/WHEEL +0 -0
- {gmshairfoil2d-0.2.2.dist-info → gmshairfoil2d-0.2.3.dist-info}/entry_points.txt +0 -0
- {gmshairfoil2d-0.2.2.dist-info → gmshairfoil2d-0.2.3.dist-info}/licenses/LICENSE +0 -0
- {gmshairfoil2d-0.2.2.dist-info → gmshairfoil2d-0.2.3.dist-info}/top_level.txt +0 -0
gmshairfoil2d/__init__.py
CHANGED
gmshairfoil2d/airfoil_func.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import sys
|
|
1
2
|
from pathlib import Path
|
|
2
3
|
|
|
3
4
|
import numpy as np
|
|
4
5
|
import requests
|
|
5
|
-
import sys
|
|
6
6
|
|
|
7
7
|
import gmshairfoil2d.__init__
|
|
8
8
|
|
|
@@ -10,6 +10,71 @@ LIB_DIR = Path(gmshairfoil2d.__init__.__file__).parents[1]
|
|
|
10
10
|
database_dir = Path(LIB_DIR, "database")
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
def read_airfoil_from_file(file_path):
|
|
14
|
+
"""Read airfoil coordinates from a .dat file.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
file_path : str or Path
|
|
19
|
+
Path to airfoil data file
|
|
20
|
+
|
|
21
|
+
Returns
|
|
22
|
+
-------
|
|
23
|
+
list
|
|
24
|
+
List of unique (x, y, 0) points sorted by original order
|
|
25
|
+
|
|
26
|
+
Raises
|
|
27
|
+
------
|
|
28
|
+
FileNotFoundError
|
|
29
|
+
If file does not exist
|
|
30
|
+
ValueError
|
|
31
|
+
If no valid airfoil points found
|
|
32
|
+
"""
|
|
33
|
+
file_path = Path(file_path)
|
|
34
|
+
if not file_path.exists():
|
|
35
|
+
raise FileNotFoundError(f"File {file_path} not found.")
|
|
36
|
+
|
|
37
|
+
airfoil_points = []
|
|
38
|
+
with open(file_path, 'r') as f:
|
|
39
|
+
for line in f:
|
|
40
|
+
line = line.strip()
|
|
41
|
+
if not line or line.startswith(('#', 'Airfoil')):
|
|
42
|
+
continue
|
|
43
|
+
parts = line.split()
|
|
44
|
+
if len(parts) != 2:
|
|
45
|
+
continue
|
|
46
|
+
try:
|
|
47
|
+
x, y = map(float, parts)
|
|
48
|
+
except ValueError:
|
|
49
|
+
continue
|
|
50
|
+
if x > 1 and y > 1:
|
|
51
|
+
continue
|
|
52
|
+
airfoil_points.append((x, y))
|
|
53
|
+
|
|
54
|
+
if not airfoil_points:
|
|
55
|
+
raise ValueError(f"No valid airfoil points found in {file_path}")
|
|
56
|
+
|
|
57
|
+
# Split upper and lower surfaces
|
|
58
|
+
try:
|
|
59
|
+
split_index = next(i for i, (x, y) in enumerate(airfoil_points) if x >= 1.0)
|
|
60
|
+
except StopIteration:
|
|
61
|
+
split_index = len(airfoil_points) // 2
|
|
62
|
+
|
|
63
|
+
upper_points = airfoil_points[:split_index + 1]
|
|
64
|
+
lower_points = airfoil_points[split_index + 1:]
|
|
65
|
+
|
|
66
|
+
# Ensure lower points start from trailing edge
|
|
67
|
+
if lower_points and lower_points[0][0] == 0.0:
|
|
68
|
+
lower_points = lower_points[::-1]
|
|
69
|
+
|
|
70
|
+
# Combine and remove duplicates
|
|
71
|
+
x_up, y_up = zip(*upper_points) if upper_points else ([], [])
|
|
72
|
+
x_lo, y_lo = zip(*lower_points) if lower_points else ([], [])
|
|
73
|
+
|
|
74
|
+
cloud_points = [(x, y, 0) for x, y in zip([*x_up, *x_lo], [*y_up, *y_lo])]
|
|
75
|
+
return sorted(set(cloud_points), key=cloud_points.index)
|
|
76
|
+
|
|
77
|
+
|
|
13
78
|
def get_all_available_airfoil_names():
|
|
14
79
|
"""
|
|
15
80
|
Request the airfoil list available at m-selig.ae.illinois.edu
|
|
@@ -34,102 +99,92 @@ def get_all_available_airfoil_names():
|
|
|
34
99
|
|
|
35
100
|
def get_airfoil_file(airfoil_name):
|
|
36
101
|
"""
|
|
37
|
-
Request the airfoil .dat file
|
|
38
|
-
database folder
|
|
102
|
+
Request the airfoil .dat file from m-selig.ae.illinois.edu and store it in database folder.
|
|
39
103
|
|
|
40
104
|
Parameters
|
|
41
105
|
----------
|
|
42
|
-
airfoil_name :
|
|
43
|
-
|
|
106
|
+
airfoil_name : str
|
|
107
|
+
Name of the airfoil
|
|
108
|
+
|
|
109
|
+
Raises
|
|
110
|
+
------
|
|
111
|
+
SystemExit
|
|
112
|
+
If airfoil not found or network error occurs
|
|
44
113
|
"""
|
|
45
|
-
|
|
46
114
|
if not database_dir.exists():
|
|
47
115
|
database_dir.mkdir()
|
|
48
116
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
117
|
+
file_path = Path(database_dir, f"{airfoil_name}.dat")
|
|
118
|
+
if file_path.exists():
|
|
119
|
+
return
|
|
52
120
|
|
|
121
|
+
url = f"https://m-selig.ae.illinois.edu/ads/coord/{airfoil_name}.dat"
|
|
53
122
|
try:
|
|
54
|
-
|
|
55
|
-
if
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
except requests.exceptions.RequestException
|
|
123
|
+
response = requests.get(url, timeout=10)
|
|
124
|
+
if response.status_code != 200:
|
|
125
|
+
print(f"❌ Error: Could not find airfoil '{airfoil_name}' on UIUC database.")
|
|
126
|
+
sys.exit(1)
|
|
127
|
+
with open(file_path, "wb") as f:
|
|
128
|
+
f.write(response.content)
|
|
129
|
+
except requests.exceptions.RequestException:
|
|
61
130
|
print(f"❌ Network Error: Could not connect to the database. Check your internet.")
|
|
62
|
-
import sys
|
|
63
131
|
sys.exit(1)
|
|
64
132
|
|
|
65
|
-
file_path = Path(database_dir, f"{airfoil_name}.dat")
|
|
66
|
-
|
|
67
|
-
if not file_path.exists():
|
|
68
|
-
with open(file_path, "wb") as f:
|
|
69
|
-
f.write(r.content)
|
|
70
|
-
|
|
71
133
|
|
|
72
134
|
def get_airfoil_points(airfoil_name):
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
135
|
+
"""Load airfoil points from the database.
|
|
136
|
+
|
|
137
|
+
Parameters
|
|
138
|
+
----------
|
|
139
|
+
airfoil_name : str
|
|
140
|
+
Name of the airfoil in the database
|
|
141
|
+
|
|
142
|
+
Returns
|
|
143
|
+
-------
|
|
144
|
+
list
|
|
145
|
+
List of unique (x, y, 0) points
|
|
146
|
+
|
|
147
|
+
Raises
|
|
148
|
+
------
|
|
149
|
+
ValueError
|
|
150
|
+
If no valid points found for the airfoil
|
|
151
|
+
"""
|
|
81
152
|
get_airfoil_file(airfoil_name)
|
|
82
|
-
airfoil_file = Path(database_dir, airfoil_name
|
|
153
|
+
airfoil_file = Path(database_dir, f"{airfoil_name}.dat")
|
|
83
154
|
|
|
155
|
+
airfoil_points = []
|
|
84
156
|
with open(airfoil_file) as f:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if x > 1 and y > 1:
|
|
97
|
-
upper_len = int(x)
|
|
98
|
-
lower_len = int(y)
|
|
99
|
-
continue
|
|
100
|
-
|
|
101
|
-
# Catch the x, y coordinates
|
|
102
|
-
airfoil_points.append((x, y))
|
|
157
|
+
for line in f:
|
|
158
|
+
try:
|
|
159
|
+
x, y = map(float, line.strip().split())
|
|
160
|
+
except ValueError:
|
|
161
|
+
continue
|
|
162
|
+
if x > 1 and y > 1:
|
|
163
|
+
continue
|
|
164
|
+
airfoil_points.append((x, y))
|
|
165
|
+
|
|
166
|
+
if not airfoil_points:
|
|
167
|
+
raise ValueError(f"No valid points found for airfoil {airfoil_name}")
|
|
103
168
|
|
|
104
169
|
n_points = len(airfoil_points)
|
|
170
|
+
upper_len = n_points // 2
|
|
105
171
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if x == y == 0:
|
|
112
|
-
upper_len = i
|
|
113
|
-
break
|
|
114
|
-
else:
|
|
115
|
-
reverse_lower = True
|
|
172
|
+
# Try to find split point at (0, 0)
|
|
173
|
+
for i, (x, y) in enumerate(airfoil_points):
|
|
174
|
+
if x == y == 0:
|
|
175
|
+
upper_len = i
|
|
176
|
+
break
|
|
116
177
|
|
|
117
178
|
upper_points = airfoil_points[:upper_len]
|
|
118
179
|
lower_points = airfoil_points[upper_len:]
|
|
119
|
-
|
|
120
|
-
if
|
|
180
|
+
|
|
181
|
+
if lower_points and lower_points[0][0] == 0:
|
|
121
182
|
lower_points = lower_points[::-1]
|
|
122
183
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
x_up, y_up = zip(*[points for points in upper_points])
|
|
126
|
-
x_lo, y_lo = zip(*[points for points in lower_points])
|
|
127
|
-
|
|
128
|
-
x = [*x_up, *x_lo]
|
|
129
|
-
y = [*y_up, *y_lo]
|
|
184
|
+
x_up, y_up = zip(*upper_points) if upper_points else ([], [])
|
|
185
|
+
x_lo, y_lo = zip(*lower_points) if lower_points else ([], [])
|
|
130
186
|
|
|
131
|
-
cloud_points = [(x
|
|
132
|
-
# remove duplicated points
|
|
187
|
+
cloud_points = [(x, y, 0) for x, y in zip([*x_up, *x_lo], [*y_up, *y_lo])]
|
|
133
188
|
return sorted(set(cloud_points), key=cloud_points.index)
|
|
134
189
|
|
|
135
190
|
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""Configuration file handler for gmshairfoil2d.
|
|
2
|
+
|
|
3
|
+
Supports reading and writing simple key=value configuration files.
|
|
4
|
+
Empty values are skipped.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _convert_value(value, key, string_params):
|
|
11
|
+
"""Convert string value to appropriate type.
|
|
12
|
+
|
|
13
|
+
Parameters
|
|
14
|
+
----------
|
|
15
|
+
value : str
|
|
16
|
+
String value to convert
|
|
17
|
+
key : str
|
|
18
|
+
Configuration key name
|
|
19
|
+
string_params : set
|
|
20
|
+
Set of keys that should remain as strings
|
|
21
|
+
|
|
22
|
+
Returns
|
|
23
|
+
-------
|
|
24
|
+
str, int, float, bool, or None
|
|
25
|
+
Converted value
|
|
26
|
+
"""
|
|
27
|
+
if key in string_params:
|
|
28
|
+
return value
|
|
29
|
+
|
|
30
|
+
value_lower = value.lower()
|
|
31
|
+
if value_lower == 'true':
|
|
32
|
+
return True
|
|
33
|
+
elif value_lower == 'false':
|
|
34
|
+
return False
|
|
35
|
+
elif value_lower == 'none':
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
# Try to convert to numeric
|
|
39
|
+
try:
|
|
40
|
+
if '.' in value or 'e' in value_lower:
|
|
41
|
+
return float(value)
|
|
42
|
+
return int(value)
|
|
43
|
+
except ValueError:
|
|
44
|
+
return value
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def read_config(config_path):
|
|
48
|
+
"""Read configuration from a simple config file (key=value format).
|
|
49
|
+
|
|
50
|
+
Empty values are skipped. Values are automatically converted to appropriate types
|
|
51
|
+
(int, float, bool) unless the key is in the string_params set.
|
|
52
|
+
|
|
53
|
+
Parameters
|
|
54
|
+
----------
|
|
55
|
+
config_path : str or Path
|
|
56
|
+
Path to the configuration file
|
|
57
|
+
|
|
58
|
+
Returns
|
|
59
|
+
-------
|
|
60
|
+
dict
|
|
61
|
+
Dictionary containing configuration parameters
|
|
62
|
+
|
|
63
|
+
Raises
|
|
64
|
+
------
|
|
65
|
+
FileNotFoundError
|
|
66
|
+
If the configuration file doesn't exist
|
|
67
|
+
Exception
|
|
68
|
+
If there's an error parsing the configuration file
|
|
69
|
+
"""
|
|
70
|
+
config_path = Path(config_path)
|
|
71
|
+
|
|
72
|
+
if not config_path.exists():
|
|
73
|
+
raise FileNotFoundError(f"Configuration file not found: {config_path}")
|
|
74
|
+
|
|
75
|
+
# Parameters that should always remain as strings
|
|
76
|
+
string_params = {'naca', 'airfoil', 'airfoil_path', 'flap_path', 'format', 'arg_struc', 'box'}
|
|
77
|
+
|
|
78
|
+
config = {}
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
with open(config_path, 'r') as f:
|
|
82
|
+
for line in f:
|
|
83
|
+
line = line.strip()
|
|
84
|
+
# Skip empty lines and comments
|
|
85
|
+
if not line or line.startswith('#'):
|
|
86
|
+
continue
|
|
87
|
+
|
|
88
|
+
# Split by first '=' only
|
|
89
|
+
if '=' not in line:
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
key, value = line.split('=', 1)
|
|
93
|
+
key = key.strip()
|
|
94
|
+
value = value.strip()
|
|
95
|
+
|
|
96
|
+
# Skip empty values
|
|
97
|
+
if not value:
|
|
98
|
+
continue
|
|
99
|
+
|
|
100
|
+
# Convert value to appropriate type
|
|
101
|
+
config[key] = _convert_value(value, key, string_params)
|
|
102
|
+
|
|
103
|
+
return config
|
|
104
|
+
|
|
105
|
+
except FileNotFoundError:
|
|
106
|
+
raise
|
|
107
|
+
except Exception as e:
|
|
108
|
+
raise Exception(f"Error parsing configuration file: {e}") from e
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def write_config(config_dict, output_path):
|
|
112
|
+
"""
|
|
113
|
+
Write configuration to a simple config file (key=value format).
|
|
114
|
+
|
|
115
|
+
Parameters
|
|
116
|
+
----------
|
|
117
|
+
config_dict : dict
|
|
118
|
+
Configuration dictionary to write
|
|
119
|
+
output_path : str or Path
|
|
120
|
+
Output path for the configuration file
|
|
121
|
+
"""
|
|
122
|
+
output_path = Path(output_path)
|
|
123
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
124
|
+
|
|
125
|
+
with open(output_path, 'w') as f:
|
|
126
|
+
for key, value in config_dict.items():
|
|
127
|
+
if value is None:
|
|
128
|
+
f.write(f"{key}=\n")
|
|
129
|
+
else:
|
|
130
|
+
f.write(f"{key}= {value}\n")
|
|
131
|
+
|
|
132
|
+
print(f"Configuration saved to: {output_path}")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def merge_config_with_args(config_dict, args):
|
|
136
|
+
"""Merge configuration file parameters with command-line arguments.
|
|
137
|
+
|
|
138
|
+
Command-line arguments take precedence over config file values. Only applies
|
|
139
|
+
config values when the command-line value is None or False (default).
|
|
140
|
+
|
|
141
|
+
Parameters
|
|
142
|
+
----------
|
|
143
|
+
config_dict : dict
|
|
144
|
+
Configuration dictionary from config file
|
|
145
|
+
args : argparse.Namespace
|
|
146
|
+
Command-line arguments
|
|
147
|
+
|
|
148
|
+
Returns
|
|
149
|
+
-------
|
|
150
|
+
argparse.Namespace
|
|
151
|
+
Merged arguments with config values applied
|
|
152
|
+
"""
|
|
153
|
+
args_dict = vars(args)
|
|
154
|
+
|
|
155
|
+
# For each key in config, update args only if not explicitly set on command line
|
|
156
|
+
for key, value in config_dict.items():
|
|
157
|
+
if key in args_dict:
|
|
158
|
+
# Only override with config value if current value is default/None/False
|
|
159
|
+
if args_dict[key] is None or args_dict[key] is False:
|
|
160
|
+
setattr(args, key, value)
|
|
161
|
+
|
|
162
|
+
return args
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def create_example_config(output_path="config_example.cfg"):
|
|
166
|
+
"""
|
|
167
|
+
Create an example configuration file with all available options.
|
|
168
|
+
|
|
169
|
+
Parameters
|
|
170
|
+
----------
|
|
171
|
+
output_path : str or Path
|
|
172
|
+
Output path for the example configuration file
|
|
173
|
+
"""
|
|
174
|
+
example_config = {
|
|
175
|
+
"naca": "0012",
|
|
176
|
+
"airfoil": None,
|
|
177
|
+
"airfoil_path": None,
|
|
178
|
+
"flap_path": None,
|
|
179
|
+
"aoa": "0.0",
|
|
180
|
+
"deflection": "0.0",
|
|
181
|
+
"farfield": "10",
|
|
182
|
+
"farfield_ctype": None,
|
|
183
|
+
"box": None,
|
|
184
|
+
"airfoil_mesh_size": "0.01",
|
|
185
|
+
"flap_mesh_size": None,
|
|
186
|
+
"ext_mesh_size": "0.2",
|
|
187
|
+
"no_bl": "False",
|
|
188
|
+
"first_layer": "3e-05",
|
|
189
|
+
"ratio": "1.2",
|
|
190
|
+
"nb_layers": "35",
|
|
191
|
+
"format": "su2",
|
|
192
|
+
"structured": "False",
|
|
193
|
+
"arg_struc": "10x10",
|
|
194
|
+
"output": None,
|
|
195
|
+
"ui": "False",
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
write_config(example_config, output_path)
|