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 CHANGED
@@ -0,0 +1,3 @@
1
+ """GMSH-Airfoil-2D: 2D airfoil mesh generation with GMSH."""
2
+
3
+ __version__ = "1.0.0"
@@ -0,0 +1,6 @@
1
+ """Entry point for the gmshairfoil2d package."""
2
+
3
+ from gmshairfoil2d.gmshairfoil2d import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -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 at m-selig.ae.illinois.edu and stores it (if found) in the
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 : srt
43
- name of the airfoil
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
- url = f"https://m-selig.ae.illinois.edu/ads/coord/{airfoil_name}.dat"
50
-
51
- r = requests.get(url)
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
- r = requests.get(url, timeout=10) # Aggiungi sempre un timeout
55
- if r.status_code != 200:
56
- # Invece di raise Exception, print e exit pulito
57
- print(f"❌ Error: Could not find airfoil '{airfoil_name}' on UIUC database.")
58
- import sys
59
- sys.exit(1)
60
- except requests.exceptions.RequestException as e:
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
- airfoil_points = []
75
- upper_points = []
76
- lower_points = []
77
- upper_len = 0
78
- lower_len = 0
79
- reverse_lower = False
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 + ".dat")
153
+ airfoil_file = Path(database_dir, f"{airfoil_name}.dat")
83
154
 
155
+ airfoil_points = []
84
156
  with open(airfoil_file) as f:
85
- lines = f.readlines()
86
-
87
- for line in lines:
88
-
89
- # Catch the text lines
90
- try:
91
- x, y = map(float, line.strip("\n").split())
92
- except ValueError:
93
- continue
94
-
95
- # Catch the line with the upper and lower number of points
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
- if not upper_len or not lower_len:
107
-
108
- upper_len = n_points // 2
109
-
110
- for i, (x, y) in enumerate(airfoil_points):
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 reverse_lower:
180
+
181
+ if lower_points and lower_points[0][0] == 0:
121
182
  lower_points = lower_points[::-1]
122
183
 
123
- assert len(upper_points) + len(lower_points) == n_points
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[k], y[k], 0) for k in range(0, len(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)