File size: 22,154 Bytes
0b9b583
 
 
 
 
9e28fe5
 
 
0b9b583
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9e28fe5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2d67447
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9e28fe5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0b9b583
9e28fe5
0b9b583
 
 
 
 
9e28fe5
 
 
 
 
 
0b9b583
 
 
 
 
 
 
 
9e28fe5
0b9b583
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9e28fe5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0b9b583
 
 
 
 
 
 
9e28fe5
 
 
0b9b583
9e28fe5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0b9b583
 
9e28fe5
 
 
 
 
0b9b583
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9e28fe5
 
0b9b583
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9e28fe5
0b9b583
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9e28fe5
 
 
0b9b583
 
 
9e28fe5
 
0b9b583
 
 
 
 
 
 
 
 
 
 
 
 
9e28fe5
0b9b583
 
 
 
 
 
 
 
 
 
 
9e28fe5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0b9b583
9e28fe5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
import gradio as gr
import neuralfoil as nf
import numpy as np
import json
from pathlib import Path
import matplotlib.pyplot as plt
import matplotlib
matplotlib.use('Agg')  # Use non-interactive backend

def load_airfoil_from_dat(filepath):
    """Load airfoil coordinates from a .dat file"""
    with open(filepath, 'r') as f:
        lines = f.readlines()
    
    # Skip the first line (airfoil name)
    coords = []
    for line in lines[1:]:
        line = line.strip()
        if line:
            parts = line.split()
            if len(parts) >= 2:
                try:
                    x, y = float(parts[0]), float(parts[1])
                    coords.append([x, y])
                except ValueError:
                    continue
    
    return np.array(coords)

def format_coordinates(coords_array):
    """Format coordinates array as a string for display"""
    if coords_array is None or len(coords_array) == 0:
        return ""
    
    lines = []
    for x, y in coords_array:
        lines.append(f"{x:.6f}     {y:.6f}")
    return "\n".join(lines)

def parse_coordinates(coords_text):
    """Parse coordinates from text input"""
    lines = coords_text.strip().split('\n')
    coords = []
    
    for line in lines:
        line = line.strip()
        if line:
            parts = line.split()
            if len(parts) >= 2:
                try:
                    x, y = float(parts[0]), float(parts[1])
                    coords.append([x, y])
                except ValueError:
                    continue
    
    return np.array(coords)

def create_pressure_plot(coords, result, alpha, reynolds):
    """Create a plot of the airfoil with pressure distribution"""
    try:
        fig, ax = plt.subplots(figsize=(12, 6))
        
        # Plot airfoil shape
        ax.plot(coords[:, 0], coords[:, 1], 'k-', linewidth=2, label='Airfoil')
        ax.fill(coords[:, 0], coords[:, 1], color='lightgray', alpha=0.3)
        
        # Calculate pressure coefficient from edge velocity
        # Cp = 1 - (ue/vinf)^2
        upper_ue = np.array([result[f'upper_bl_ue/vinf_{i}'][0] for i in range(32) if f'upper_bl_ue/vinf_{i}' in result])
        lower_ue = np.array([result[f'lower_bl_ue/vinf_{i}'][0] for i in range(32) if f'lower_bl_ue/vinf_{i}' in result])
        
        upper_cp_32 = 1 - upper_ue**2
        lower_cp_32 = 1 - lower_ue**2
        
        # Create x positions for 32 points (from leading edge to trailing edge)
        x_bl_upper = np.linspace(0, 1, len(upper_cp_32))
        x_bl_lower = np.linspace(0, 1, len(lower_cp_32))
        
        # Find upper and lower surface points
        # Typically airfoil coords go: TE (top) -> LE -> TE (bottom)
        le_idx = np.argmin(coords[:, 0])  # Leading edge is minimum x
        
        upper_surface = coords[:le_idx+1]  # From TE to LE (top)
        lower_surface = coords[le_idx:]     # From LE to TE (bottom)
        
        # Get high-resolution x-coordinates and interpolate Cp
        if len(upper_surface) > 1:
            x_upper_hires = upper_surface[::-1, 0]  # Reverse to go LE to TE
            y_upper_hires = upper_surface[::-1, 1]
            upper_cp_hires = np.interp(x_upper_hires, x_bl_upper, upper_cp_32)
        else:
            x_upper_hires = np.array([])
            y_upper_hires = np.array([])
            upper_cp_hires = np.array([])
            
        if len(lower_surface) > 1:
            x_lower_hires = lower_surface[:, 0]  # Already LE to TE
            y_lower_hires = lower_surface[:, 1]
            lower_cp_hires = np.interp(x_lower_hires, x_bl_lower, lower_cp_32)
        else:
            x_lower_hires = np.array([])
            y_lower_hires = np.array([])
            lower_cp_hires = np.array([])
        
        # Also get y-coordinates for 32-point data
        if len(upper_surface) > 1:
            y_upper_32 = np.interp(x_bl_upper, upper_surface[::-1, 0], upper_surface[::-1, 1])
        else:
            y_upper_32 = np.zeros_like(x_bl_upper)
            
        if len(lower_surface) > 1:
            y_lower_32 = np.interp(x_bl_lower, lower_surface[:, 0], lower_surface[:, 1])
        else:
            y_lower_32 = np.zeros_like(x_bl_lower)
        
        # Scale factor for pressure lines
        scale = 0.15 * np.max(coords[:, 1] - np.min(coords[:, 1]))
        
        # Calculate pressure line coordinates for high-resolution data
        y_upper_pressure_hires = y_upper_hires - upper_cp_hires * scale
        y_lower_pressure_hires = y_lower_hires + lower_cp_hires * scale
        
        # Calculate pressure line coordinates for 32-point data
        y_upper_pressure_32 = y_upper_32 - upper_cp_32 * scale
        y_lower_pressure_32 = y_lower_32 + lower_cp_32 * scale
        
        # Plot high-resolution pressure distribution
        ax.plot(x_upper_hires, y_upper_pressure_hires, 'b-', linewidth=2, label='Upper Surface Cp (interpolated)', alpha=0.8)
        ax.plot(x_lower_hires, y_lower_pressure_hires, 'r-', linewidth=2, label='Lower Surface Cp (interpolated)', alpha=0.8)
        
        # Plot 32-point data with markers
        ax.plot(x_bl_upper, y_upper_pressure_32, 'bo', markersize=4, label='Upper Surface Cp (32 pts)', alpha=0.6)
        ax.plot(x_bl_lower, y_lower_pressure_32, 'ro', markersize=4, label='Lower Surface Cp (32 pts)', alpha=0.6)
        
        # Fill the area between airfoil surface and pressure line
        ax.fill_between(x_upper_hires, y_upper_hires, y_upper_pressure_hires, color='blue', alpha=0.2)
        ax.fill_between(x_lower_hires, y_lower_hires, y_lower_pressure_hires, color='red', alpha=0.2)
        
        ax.legend(loc='upper right')
        
        ax.set_xlabel('x/c', fontsize=12)
        ax.set_ylabel('y/c', fontsize=12)
        ax.set_title(f'Airfoil with Pressure Distribution (α={alpha}°, Re={reynolds:.1e})', fontsize=14)
        ax.grid(True, alpha=0.3)
        ax.set_aspect('equal')
        ax.axhline(y=0, color='k', linestyle='--', alpha=0.3, linewidth=0.5)
        
        plt.tight_layout()
        return fig
        
    except Exception as e:
        # Return a simple error plot
        fig, ax = plt.subplots(figsize=(12, 6))
        ax.text(0.5, 0.5, f'Error creating plot: {str(e)}', 
                ha='center', va='center', fontsize=12)
        ax.set_xlim(0, 1)
        ax.set_ylim(0, 1)
        return fig

def run_neuralfoil_prediction_api(coordinates, alpha, reynolds, model_size):
    """Run NeuralFoil prediction from numpy array/list - for API usage"""
    try:
        # Convert to numpy array if it's a list
        if isinstance(coordinates, list):
            coords = np.array(coordinates)
        else:
            coords = coordinates
        
        if len(coords) < 3:
            return {"error": "Invalid coordinates. Please provide at least 3 coordinate pairs."}
        
        # Run NeuralFoil analysis directly from coordinates
        result = nf.get_aero_from_coordinates(
            coordinates=coords,
            alpha=alpha,
            Re=reynolds,
            model_size=model_size
        )
        
        # Convert result to a serializable dictionary
        output = {}
        
        # Check if result is a dictionary or has attributes
        if isinstance(result, dict):
            # Result is already a dictionary - extract scalar values
            for key, val in result.items():
                if isinstance(val, np.ndarray):
                    output[key] = float(val[0]) if val.size == 1 else val.tolist()
                else:
                    output[key] = val
        else:
            # Result has attributes (older API style)
            # Standard outputs
            if hasattr(result, 'CL'):
                output['CL'] = float(result.CL) if not np.isnan(result.CL) else None
            if hasattr(result, 'CD'):
                output['CD'] = float(result.CD) if not np.isnan(result.CD) else None
            if hasattr(result, 'CM'):
                output['CM'] = float(result.CM) if not np.isnan(result.CM) else None
            
            # Transition locations (if available)
            if hasattr(result, 'Top_Xtr'):
                output['Top_Xtr'] = float(result.Top_Xtr) if not np.isnan(result.Top_Xtr) else None
            if hasattr(result, 'Bot_Xtr'):
                output['Bot_Xtr'] = float(result.Bot_Xtr) if not np.isnan(result.Bot_Xtr) else None
            
            # Confidence metric
            if hasattr(result, 'analysis_confidence'):
                output['analysis_confidence'] = float(result.analysis_confidence) if not np.isnan(result.analysis_confidence) else None
            
            # Include all other attributes
            for attr in dir(result):
                if not attr.startswith('_') and attr not in output:
                    val = getattr(result, attr)
                    if isinstance(val, (int, float, str, bool)):
                        output[attr] = val
                    elif isinstance(val, np.ndarray):
                        output[attr] = val.tolist()
        
        # Calculate pressure coefficients from edge velocity
        # Cp = 1 - (ue/vinf)^2
        upper_ue = np.array([result[f'upper_bl_ue/vinf_{i}'][0] for i in range(32) if f'upper_bl_ue/vinf_{i}' in result])
        lower_ue = np.array([result[f'lower_bl_ue/vinf_{i}'][0] for i in range(32) if f'lower_bl_ue/vinf_{i}' in result])
        
        upper_cp_32 = 1 - upper_ue**2
        lower_cp_32 = 1 - lower_ue**2
        
        # Create x positions for the 32 boundary layer stations
        x_bl_upper = np.linspace(0, 1, len(upper_cp_32))
        x_bl_lower = np.linspace(0, 1, len(lower_cp_32))
        
        # Split airfoil coordinates into upper and lower surfaces
        le_idx = np.argmin(coords[:, 0])  # Leading edge is minimum x
        upper_surface = coords[:le_idx+1]  # From TE to LE (top)
        lower_surface = coords[le_idx:]     # From LE to TE (bottom)
        
        # Get x-coordinates for upper and lower surfaces (in 0-1 range)
        if len(upper_surface) > 1:
            x_upper_coords = upper_surface[::-1, 0]  # Reverse to go LE to TE
            # Interpolate Cp from 32 points to airfoil coordinate resolution
            upper_cp_interp = np.interp(x_upper_coords, x_bl_upper, upper_cp_32)
        else:
            x_upper_coords = np.array([])
            upper_cp_interp = np.array([])
        
        if len(lower_surface) > 1:
            x_lower_coords = lower_surface[:, 0]  # Already LE to TE
            # Interpolate Cp from 32 points to airfoil coordinate resolution
            lower_cp_interp = np.interp(x_lower_coords, x_bl_lower, lower_cp_32)
        else:
            x_lower_coords = np.array([])
            lower_cp_interp = np.array([])
        
        # Add pressure coefficient arrays (both 32-point and interpolated)
        output['pressure_coefficients'] = {
            'upper_surface_cp': upper_cp_interp.tolist(),
            'lower_surface_cp': lower_cp_interp.tolist(),
            'x_upper': x_upper_coords.tolist(),
            'x_lower': x_lower_coords.tolist(),
            'upper_surface_cp_32': upper_cp_32.tolist(),
            'lower_surface_cp_32': lower_cp_32.tolist(),
            'x_upper_32': x_bl_upper.tolist(),
            'x_lower_32': x_bl_lower.tolist()
        }
        
        # Add input parameters for reference
        output['input_parameters'] = {
            'alpha_deg': alpha,
            'reynolds_number': reynolds,
            'model_size': model_size,
            'num_coordinates': len(coords)
        }
        
        return output
        
    except Exception as e:
        return {"error": str(e)}

def run_neuralfoil_prediction(coords_text, alpha, reynolds, model_size):
    """Run NeuralFoil prediction and return full JSON output and plot - for UI usage"""
    try:
        # Parse coordinates
        coords = parse_coordinates(coords_text)
        
        if len(coords) < 3:
            error_fig, ax = plt.subplots(figsize=(12, 6))
            ax.text(0.5, 0.5, 'Invalid coordinates. Please provide at least 3 coordinate pairs.', 
                   ha='center', va='center', fontsize=12)
            ax.set_xlim(0, 1)
            ax.set_ylim(0, 1)
            return json.dumps({"error": "Invalid coordinates. Please provide at least 3 coordinate pairs."}, indent=2), error_fig
        
        # Run NeuralFoil analysis directly from coordinates
        result = nf.get_aero_from_coordinates(
            coordinates=coords,
            alpha=alpha,
            Re=reynolds,
            model_size=model_size
        )
        # print(result)
        
        # Convert result to a serializable dictionary
        output = {}
        
        # Standard outputs
        if hasattr(result, 'CL'):
            output['CL'] = float(result.CL) if not np.isnan(result.CL) else None
        if hasattr(result, 'CD'):
            output['CD'] = float(result.CD) if not np.isnan(result.CD) else None
        if hasattr(result, 'CM'):
            output['CM'] = float(result.CM) if not np.isnan(result.CM) else None
        
        # Transition locations (if available)
        if hasattr(result, 'Top_Xtr'):
            output['Top_Xtr'] = float(result.Top_Xtr) if not np.isnan(result.Top_Xtr) else None
        if hasattr(result, 'Bot_Xtr'):
            output['Bot_Xtr'] = float(result.Bot_Xtr) if not np.isnan(result.Bot_Xtr) else None
        
        # Confidence metric
        if hasattr(result, 'analysis_confidence'):
            output['analysis_confidence'] = float(result.analysis_confidence) if not np.isnan(result.analysis_confidence) else None
        
        # Include all other attributes
        for attr in dir(result):
            if not attr.startswith('_') and attr not in output:
                val = getattr(result, attr)
                if isinstance(val, (int, float, str, bool)):
                    output[attr] = val
                elif isinstance(val, np.ndarray):
                    output[attr] = val.tolist()
        
        # Calculate pressure coefficients from edge velocity
        # Cp = 1 - (ue/vinf)^2
        upper_ue = np.array([result[f'upper_bl_ue/vinf_{i}'][0] for i in range(32) if f'upper_bl_ue/vinf_{i}' in result])
        lower_ue = np.array([result[f'lower_bl_ue/vinf_{i}'][0] for i in range(32) if f'lower_bl_ue/vinf_{i}' in result])
        
        upper_cp_32 = 1 - upper_ue**2
        lower_cp_32 = 1 - lower_ue**2
        
        # Create x positions for the 32 boundary layer stations
        x_bl_upper = np.linspace(0, 1, len(upper_cp_32))
        x_bl_lower = np.linspace(0, 1, len(lower_cp_32))
        
        # Split airfoil coordinates into upper and lower surfaces
        le_idx = np.argmin(coords[:, 0])  # Leading edge is minimum x
        upper_surface = coords[:le_idx+1]  # From TE to LE (top)
        lower_surface = coords[le_idx:]     # From LE to TE (bottom)
        
        # Get x-coordinates for upper and lower surfaces (in 0-1 range)
        if len(upper_surface) > 1:
            x_upper_coords = upper_surface[::-1, 0]  # Reverse to go LE to TE
            # Interpolate Cp from 32 points to airfoil coordinate resolution
            upper_cp_interp = np.interp(x_upper_coords, x_bl_upper, upper_cp_32)
        else:
            x_upper_coords = np.array([])
            upper_cp_interp = np.array([])
        
        if len(lower_surface) > 1:
            x_lower_coords = lower_surface[:, 0]  # Already LE to TE
            # Interpolate Cp from 32 points to airfoil coordinate resolution
            lower_cp_interp = np.interp(x_lower_coords, x_bl_lower, lower_cp_32)
        else:
            x_lower_coords = np.array([])
            lower_cp_interp = np.array([])
        
        # Add pressure coefficient arrays (both 32-point and interpolated)
        output['pressure_coefficients'] = {
            'upper_surface_cp': upper_cp_interp.tolist(),
            'lower_surface_cp': lower_cp_interp.tolist(),
            'x_upper': x_upper_coords.tolist(),
            'x_lower': x_lower_coords.tolist(),
            'upper_surface_cp_32': upper_cp_32.tolist(),
            'lower_surface_cp_32': lower_cp_32.tolist(),
            'x_upper_32': x_bl_upper.tolist(),
            'x_lower_32': x_bl_lower.tolist()
        }
        
        # Add input parameters for reference
        output['input_parameters'] = {
            'alpha_deg': alpha,
            'reynolds_number': reynolds,
            'model_size': model_size,
            'num_coordinates': len(coords)
        }

        # Use the API function to get results
        output = run_neuralfoil_prediction_api(coords, alpha, reynolds, model_size)
        
        if "error" in output:
            error_fig, ax = plt.subplots(figsize=(12, 6))
            ax.text(0.5, 0.5, output["error"], ha='center', va='center', fontsize=12)
            ax.set_xlim(0, 1)
            ax.set_ylim(0, 1)
            return json.dumps(output, indent=2), error_fig
        
        # Get result back for plotting
        result = nf.get_aero_from_coordinates(
            coordinates=coords,
            alpha=alpha,
            Re=reynolds,
            model_size=model_size
        )
        
        # Create the pressure plot
        fig = create_pressure_plot(coords, result, alpha, reynolds)
        
        return json.dumps(output, indent=2), fig
        
    except Exception as e:
        error_fig, ax = plt.subplots(figsize=(12, 6))
        ax.text(0.5, 0.5, f'Error: {str(e)}', ha='center', va='center', fontsize=12)
        ax.set_xlim(0, 1)
        ax.set_ylim(0, 1)
        return json.dumps({"error": str(e)}, indent=2), error_fig

def load_example(example_name):
    """Load an example airfoil"""
    example_files = {
        "NACA 4412": "examples/naca4412.dat",
        "Clark Y": "examples/clarky.dat",
        "RAE 2822": "examples/rae2822.dat"
    }
    
    filepath = example_files.get(example_name)
    if filepath and Path(filepath).exists():
        coords = load_airfoil_from_dat(filepath)
        return format_coordinates(coords)
    return ""

# Load default airfoil (RAE 2822)
default_coords = format_coordinates(load_airfoil_from_dat("examples/rae2822.dat"))

# Create Gradio interface
with gr.Blocks(title="NeuralFoil Airfoil Predictor") as demo:
    gr.Markdown("# NeuralFoil Airfoil Predictor")
    gr.Markdown("""
    This app uses [NeuralFoil](https://github.com/peterdsharpe/NeuralFoil) to predict airfoil aerodynamics.
    
    Provide airfoil coordinates (x, y pairs, one per line) and operating conditions to get predictions for CL, CD, CM, and more.
    """)
    
    with gr.Row():
        with gr.Column():
            gr.Markdown("### Airfoil Coordinates")
            gr.Markdown("Enter x,y coordinate pairs (one per line). Coordinates should trace the airfoil from trailing edge, over the top, to leading edge, then back along the bottom.")
            
            coords_input = gr.Textbox(
                label="Airfoil Coordinates",
                value=default_coords,
                lines=15,
                max_lines=30,
                placeholder="x     y\n1.0   0.0\n0.95  0.01\n..."
            )
            
            gr.Markdown("### Load Example")
            example_buttons = gr.Radio(
                choices=["NACA 4412", "Clark Y", "RAE 2822"],
                label="Example Airfoils",
                value="RAE 2822"
            )
            
            load_btn = gr.Button("Load Example")
            
            gr.Markdown("### Operating Conditions")
            alpha_input = gr.Slider(
                minimum=-10,
                maximum=20,
                value=5.0,
                step=0.5,
                label="Angle of Attack α [deg]"
            )
            
            reynolds_input = gr.Number(
                value=1e6,
                label="Reynolds Number Re [-]"
            )
            
            model_size_input = gr.Dropdown(
                choices=["xxsmall", "xsmall", "small", "medium", "large", "xlarge", "xxlarge", "xxxlarge"],
                value="large",
                label="Model Size"
            )
            
            predict_btn = gr.Button("Run Prediction", variant="primary")
        
        with gr.Column():
            gr.Markdown("### Airfoil with Pressure Distribution")
            output_plot = gr.Plot(label="Pressure Distribution")
            
            gr.Markdown("### Full NeuralFoil Output (JSON)")
            output_json = gr.Textbox(
                label="Prediction Results",
                lines=20,
                max_lines=30,
                placeholder="Results will appear here..."
            )
    
    # Event handlers
    load_btn.click(
        fn=load_example,
        inputs=[example_buttons],
        outputs=[coords_input]
    )
    
    predict_btn.click(
        fn=run_neuralfoil_prediction,
        inputs=[coords_input, alpha_input, reynolds_input, model_size_input],
        outputs=[output_json, output_plot]
    )
    
    gr.Markdown("""
    ---
    ### About
    
    **NeuralFoil** is a neural-network-based surrogate for XFoil that predicts airfoil aerodynamics much faster than traditional CFD.
    
    **Citation**: If you use NeuralFoil, please cite the [GitHub repository](https://github.com/peterdsharpe/NeuralFoil) and Peter Sharpe's PhD thesis.
    """)

# Create API endpoint that accepts numpy arrays
api = gr.Interface(
    fn=run_neuralfoil_prediction_api,
    inputs=[
        gr.JSON(label="Coordinates (2D array: [[x1,y1], [x2,y2], ...])"),
        gr.Number(label="Angle of Attack α [deg]"),
        gr.Number(label="Reynolds Number Re [-]"),
        gr.Dropdown(choices=["xxsmall", "xsmall", "small", "medium", "large", "xlarge", "xxlarge", "xxxlarge"], label="Model Size")
    ],
    outputs=gr.JSON(label="Prediction Results"),
    title="NeuralFoil API",
    description="API endpoint for programmatic access. Input coordinates as a 2D array."
)

# Combine both interfaces in tabs
app = gr.TabbedInterface(
    [demo, api],
    ["Interactive UI", "API"]
)

if __name__ == "__main__":
    app.launch()