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()