├── .gitattributes ├── .gitignore ├── README.md ├── Resources ├── Actor_Critic_Approach.png ├── Airfoil_Action_Reward.pdf ├── DetailedProposed_RL_Architecture.pdf ├── Original_RL_Idea.png ├── RL_Architecture.pdf └── SARS.png └── src ├── CFD ├── Aerodynamics │ ├── Aerodynamics.py │ ├── Airfoil_Coordinates │ │ └── Dummy.txt │ ├── Airfoil_Database │ │ ├── NACA0006.dat │ │ ├── NACA0009.dat │ │ ├── NACA0012.dat │ │ ├── NACA1408.dat │ │ ├── NACA2412.dat │ │ └── NACA4412.dat │ ├── __init__.py │ └── xfoil.exe ├── CFD_Explanation.py └── __init__.py ├── Idea_2 ├── Idea_2.png └── Main.py ├── Lift_to_Drag_Predictor ├── Dataset │ ├── Archive │ │ ├── Arrays_as_rows_1.txt │ │ └── Rewards_as_rows_1.txt │ ├── Arrays_as_rows - Copy.txt │ ├── Arrays_as_rows.txt │ ├── Arrays_as_rows_a_scaling_100.txt │ ├── Rewards_as_rows - Copy.txt │ ├── Rewards_as_rows.txt │ └── Rewards_as_rows_a_scaling_100.txt ├── Dataset_Generator.py ├── Dataset_Generator_High_Ascaling.py ├── High_L_by_D_Airfoil.py └── Visualize_Airfoils.py ├── ML_Modules ├── Main.ipynb ├── My_NN │ ├── NeuralNetwork_My.py │ └── Trial.py ├── NeuralNetwork.py └── Objective_function.py ├── Policy_Gradient ├── Generate_Airfoil.py ├── Helper.py ├── Policy_Gradient.py ├── Progress_Checkpoint │ ├── Checkpoint.pth │ ├── Dummy.txt │ ├── Total_Reward_vs_Epochs.png │ └── Trained_Model.pth └── Trajectory.py ├── Shape_Parametrization ├── Curves.py ├── Curves_Explanation.ipynb ├── Splines.py ├── Splines_Explanation.ipynb └── __init__.py └── StableBaselines ├── Average_Reward_NoTraining.py ├── CFD_Gym_Env.py ├── Check_Env.py ├── Dataset ├── Archive │ ├── Arrays_as_rows_1.txt │ └── Rewards_as_rows_1.txt ├── Arrays_as_rows - Copy.txt ├── Arrays_as_rows.txt ├── Arrays_as_rows_a_scaling_100.txt ├── Rewards_as_rows - Copy.txt ├── Rewards_as_rows.txt └── Rewards_as_rows_a_scaling_100.txt ├── Logs └── Dummy.txt ├── Models └── Dummy.txt └── Train_PPO.py /.gitattributes: -------------------------------------------------------------------------------- 1 | *.txt filter=lfs diff=lfs merge=lfs -text 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Airfoil Shape Optimization using Deep Reinforcement Learning 2 | 3 | ## Motivation 4 | Aircraft design methods used today to determine shape, structure and size begin by taking features from similar aircraft that have been built before. Since this data comes from already built aircraft, it often leads to configurations being stuck in a sub-optimal design space. Since the design space is huge, it is extremely difficult for a human to explore or even guess the initial design. A revolution in aircraft design can therefore perhaps be brought about by machine learning methods which can ‘intelligently’ search through the design space to reach globally optimal configurations. Aircraft design is a tremendously complex process, therefore in this study we begin by exploring methods to design optimal airfoils, which are fundamental shapes that underpin aircraft wing design, imparting them their aerodynamic properties like lift and drag. A measure of an airfoil's quality is its lift-to-drag ratio, which only depends on its shape. In this study, we work towards reaching the optimal shape that achieves the highest lift-to-drag ratio. 5 | 6 | 7 | ## Method 8 | The central part of our method is a reinforcement learning agent whose goal is to achieve an airfoil shape that maximizes the lift-to-drag ratio (its reward function). The RL agent holds a shape, which can be evaluated by a Computational Fluid Dynamics (CFD) solver (its environment) to give back its lift-to-drag ratio. The agent makes sequential changes to the shape (its actions) until an optimal shape is reached. The goal of the agent is to learn a policy that intelligently makes changes to efficiently reach the best design. This is represented in the figure below: 9 | 10 | 11 | 12 |

13 | 14 |
15 | Overview of RL as an approach to attack the problem of airfoil design. 16 |
17 |
18 |

19 | 20 |

21 | 22 |
23 | One step of the RL agent. Actions are taken to change the airfoil shape. 24 |
25 |
26 |

27 | 28 |

29 | 30 |
31 | Actor-Critic Deep Reinforcement Learning architecture to take actions given states. 32 |
33 |
34 |

35 | 36 | 37 | ## Contributors 38 | - Parth Prashant Lathi 39 | - Meenal Gupta 40 | - Atharva Aalok 41 | 42 | ## References 43 | - Viquerat, Jonathan, et al. "Direct shape optimization through deep reinforcement learning." Journal of Computational Physics 428 (2021): 110080. 44 | - Dussauge, Thomas P., et al. "A reinforcement learning approach to airfoil shape optimization." Scientific Reports 13.1 (2023): 9753. -------------------------------------------------------------------------------- /Resources/Actor_Critic_Approach.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atharvaaalok/Airfoil-Shape-Optimization-RL/55da5c55ce028480df2c440e74ca534717df898e/Resources/Actor_Critic_Approach.png -------------------------------------------------------------------------------- /Resources/Airfoil_Action_Reward.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atharvaaalok/Airfoil-Shape-Optimization-RL/55da5c55ce028480df2c440e74ca534717df898e/Resources/Airfoil_Action_Reward.pdf -------------------------------------------------------------------------------- /Resources/DetailedProposed_RL_Architecture.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atharvaaalok/Airfoil-Shape-Optimization-RL/55da5c55ce028480df2c440e74ca534717df898e/Resources/DetailedProposed_RL_Architecture.pdf -------------------------------------------------------------------------------- /Resources/Original_RL_Idea.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atharvaaalok/Airfoil-Shape-Optimization-RL/55da5c55ce028480df2c440e74ca534717df898e/Resources/Original_RL_Idea.png -------------------------------------------------------------------------------- /Resources/RL_Architecture.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atharvaaalok/Airfoil-Shape-Optimization-RL/55da5c55ce028480df2c440e74ca534717df898e/Resources/RL_Architecture.pdf -------------------------------------------------------------------------------- /Resources/SARS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atharvaaalok/Airfoil-Shape-Optimization-RL/55da5c55ce028480df2c440e74ca534717df898e/Resources/SARS.png -------------------------------------------------------------------------------- /src/CFD/Aerodynamics/Aerodynamics.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import numpy as np 3 | import os 4 | 5 | import time 6 | 7 | class Airfoil: 8 | def __init__(self, airfoil_coordinates, airfoil_name): 9 | self.coordinates = np.array(airfoil_coordinates) 10 | self.name = airfoil_name 11 | 12 | def get_aerodynamic_properties(self, Re, angle_of_attack = 0): 13 | aerodynamic_properties = CFD(self, Re, angle_of_attack) 14 | return aerodynamic_properties 15 | 16 | def get_L_by_D(self, Re, angle_of_attack = 0): 17 | aerodynamic_properties = self.get_aerodynamic_properties(Re, angle_of_attack) 18 | if aerodynamic_properties is None: 19 | return None 20 | try: 21 | CL_by_CD = aerodynamic_properties['CL'] / aerodynamic_properties['CD'] 22 | except ZeroDivisionError: 23 | CL_by_CD = None 24 | return CL_by_CD 25 | 26 | def get_CL(self, Re, angle_of_attack = 0): 27 | aerodynamic_properties = self.get_aerodynamic_properties(Re, angle_of_attack) 28 | if aerodynamic_properties is None: 29 | return None 30 | return aerodynamic_properties['CL'] 31 | 32 | def get_CD(self, Re, angle_of_attack = 0): 33 | aerodynamic_properties = self.get_aerodynamic_properties(Re, angle_of_attack) 34 | if aerodynamic_properties is None: 35 | return None 36 | return aerodynamic_properties['CD'] 37 | 38 | def visualize(self): 39 | CFD(self, 1e6, angle_of_attack = 0, visualize = True) 40 | 41 | 42 | 43 | def CFD(airfoil, Re, angle_of_attack = 0, visualize = False): 44 | 45 | coordinate_file_directory = '/Airfoil_Coordinates/' 46 | 47 | # Get relative path to the directory where to store the airfoil coordinate files 48 | top_level_script_directory = os.getcwd() 49 | aerodynamics_module_directory = os.path.dirname(__file__) 50 | relative_path_xfoil = aerodynamics_module_directory[len(top_level_script_directory) + 1:] + coordinate_file_directory 51 | 52 | # Get the different file paths necessary to save the coordinate file and then retrieve using airfoil 53 | airfoil_coord_filename = airfoil.name + '.dat' 54 | airfoil_save_path = os.path.dirname(__file__) + coordinate_file_directory + airfoil_coord_filename 55 | xfoil_file_path = relative_path_xfoil + airfoil_coord_filename 56 | 57 | # Save the airfoil coordinate file 58 | np.savetxt(airfoil_save_path, airfoil.coordinates, delimiter = ',') 59 | 60 | # Start Xfoil 61 | xfoil_path = os.path.dirname(__file__) + '/xfoil.exe' 62 | xfoil = subprocess.Popen(xfoil_path, stdin = subprocess.PIPE, stdout = subprocess.PIPE, stderr = subprocess.PIPE, text = True) 63 | 64 | # Set CFD evaluation parameters 65 | panel_count = 120 66 | LE_TE_panel_density_ratio = 1 67 | Reynolds_num = Re 68 | max_iter_count = 100 69 | 70 | # Define the sequence of commands to execute to calculate the aerodynamic properties 71 | command_list = ['PLOP\n', # Go to plotting options menu 72 | 'G\n', # Switch off graphical display 73 | '\n', 74 | f'load {xfoil_file_path}\n', 75 | f'{airfoil.name}\n', 76 | 'PPAR\n', # go to panel menu 77 | f'n {panel_count}\n', # set panel count 78 | f't {LE_TE_panel_density_ratio}\n', # set Leading Edge to Trailing Edge panel density 79 | '\n', 80 | '\n', 81 | 'OPER\n', # Go to operations menu to run the simulation 82 | 'visc\n' 83 | f'{Reynolds_num}\n' 84 | f'iter {max_iter_count}\n' 85 | 'PACC\n' # Turn on polar accumulation 86 | '\n', 87 | '\n', 88 | f'alfa {angle_of_attack}\n', 89 | 'PLIS\n', # List the polar values 90 | '\n', 91 | 'QUIT\n' 92 | ] 93 | 94 | if visualize == True: 95 | command_list = command_list[3:-1] 96 | 97 | # Execute the commands and close Xfoil 98 | xfoil.stdin.write(''.join(command_list)) 99 | xfoil.stdin.flush() 100 | xfoil.stdin.close() 101 | 102 | if visualize == True: 103 | time.sleep(2) 104 | 105 | # Get the outputs from xfoil - stop communication if xfoil is stuck in convergence issues 106 | try: 107 | xfoil_stdout, xfoil_stderr = xfoil.communicate(timeout = .3) 108 | except subprocess.TimeoutExpired: 109 | aerodynamic_properties = None 110 | else: 111 | # Extract the aerodynamic properties 112 | coefficients = xfoil_stdout.splitlines()[-4].split() 113 | # Check if the values are numbers, if not, then it did not converge and return None 114 | try: 115 | aerodynamic_properties = {'CL': float(coefficients[1]), 'CD': float(coefficients[2])} 116 | except: 117 | aerodynamic_properties = None 118 | finally: 119 | try: 120 | # Remove the extra airfoil coordinate file created 121 | os.remove(airfoil_save_path) 122 | except: 123 | pass 124 | return aerodynamic_properties -------------------------------------------------------------------------------- /src/CFD/Aerodynamics/Airfoil_Coordinates/Dummy.txt: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:85bc13b20a839cdedd2ae733825011c18f037b83438fc9700c0f162a8ca6a45b 3 | size 51 4 | -------------------------------------------------------------------------------- /src/CFD/Aerodynamics/Airfoil_Database/NACA0006.dat: -------------------------------------------------------------------------------- 1 | 1.0000 0.00063 2 | 0.9500 0.00403 3 | 0.9000 0.00724 4 | 0.8000 0.01312 5 | 0.7000 0.01832 6 | 0.6000 0.02282 7 | 0.5000 0.02647 8 | 0.4000 0.02902 9 | 0.3000 0.03001 10 | 0.2500 0.02971 11 | 0.2000 0.02869 12 | 0.1500 0.02673 13 | 0.1000 0.02341 14 | 0.0750 0.02100 15 | 0.0500 0.01777 16 | 0.0250 0.01307 17 | 0.0125 0.00947 18 | 0.0000 0.00000 19 | 0.0125 -0.00947 20 | 0.0250 -0.01307 21 | 0.0500 -0.01777 22 | 0.0750 -0.02100 23 | 0.1000 -0.02341 24 | 0.1500 -0.02673 25 | 0.2000 -0.02869 26 | 0.2500 -0.02971 27 | 0.3000 -0.03001 28 | 0.4000 -0.02902 29 | 0.5000 -0.02647 30 | 0.6000 -0.02282 31 | 0.7000 -0.01832 32 | 0.8000 -0.01312 33 | 0.9000 -0.00724 34 | 0.9500 -0.00403 35 | 1.0000 -0.00063 -------------------------------------------------------------------------------- /src/CFD/Aerodynamics/Airfoil_Database/NACA0009.dat: -------------------------------------------------------------------------------- 1 | 1.0000 0.00063 2 | 0.9500 0.00403 3 | 0.9000 0.00724 4 | 0.8000 0.01312 5 | 0.7000 0.01832 6 | 0.6000 0.02282 7 | 0.5000 0.02647 8 | 0.4000 0.02902 9 | 0.3000 0.03001 10 | 0.2500 0.02971 11 | 0.2000 0.02869 12 | 0.1500 0.02673 13 | 0.1000 0.02341 14 | 0.0750 0.02100 15 | 0.0500 0.01777 16 | 0.0250 0.01307 17 | 0.0125 0.00947 18 | 0.0000 0.00000 19 | 0.0125 -0.00947 20 | 0.0250 -0.01307 21 | 0.0500 -0.01777 22 | 0.0750 -0.02100 23 | 0.1000 -0.02341 24 | 0.1500 -0.02673 25 | 0.2000 -0.02869 26 | 0.2500 -0.02971 27 | 0.3000 -0.03001 28 | 0.4000 -0.02902 29 | 0.5000 -0.02647 30 | 0.6000 -0.02282 31 | 0.7000 -0.01832 32 | 0.8000 -0.01312 33 | 0.9000 -0.00724 34 | 0.9500 -0.00403 35 | 1.0000 -0.00063 -------------------------------------------------------------------------------- /src/CFD/Aerodynamics/Airfoil_Database/NACA0012.dat: -------------------------------------------------------------------------------- 1 | 1.000000 0.001260 2 | 0.993723 0.002137 3 | 0.982775 0.003651 4 | 0.969992 0.005394 5 | 0.955666 0.007315 6 | 0.940264 0.009344 7 | 0.924222 0.011418 8 | 0.907837 0.013497 9 | 0.891275 0.015558 10 | 0.874624 0.017591 11 | 0.857926 0.019590 12 | 0.841202 0.021554 13 | 0.824462 0.023482 14 | 0.807711 0.025373 15 | 0.790953 0.027228 16 | 0.774190 0.029046 17 | 0.757424 0.030826 18 | 0.740656 0.032570 19 | 0.723887 0.034276 20 | 0.707119 0.035943 21 | 0.690352 0.037571 22 | 0.673589 0.039159 23 | 0.656830 0.040706 24 | 0.640077 0.042211 25 | 0.623332 0.043672 26 | 0.606594 0.045088 27 | 0.589867 0.046458 28 | 0.573152 0.047778 29 | 0.556449 0.049048 30 | 0.539761 0.050265 31 | 0.523090 0.051426 32 | 0.506436 0.052530 33 | 0.489803 0.053572 34 | 0.473192 0.054551 35 | 0.456605 0.055463 36 | 0.440044 0.056305 37 | 0.423513 0.057072 38 | 0.407014 0.057761 39 | 0.390549 0.058368 40 | 0.374123 0.058888 41 | 0.357739 0.059317 42 | 0.341401 0.059648 43 | 0.325114 0.059878 44 | 0.308883 0.059999 45 | 0.292715 0.060006 46 | 0.276617 0.059891 47 | 0.260598 0.059649 48 | 0.244668 0.059270 49 | 0.228839 0.058747 50 | 0.213128 0.058070 51 | 0.197554 0.057232 52 | 0.182143 0.056222 53 | 0.166928 0.055030 54 | 0.151958 0.053649 55 | 0.137297 0.052071 56 | 0.123034 0.050294 57 | 0.109290 0.048323 58 | 0.096218 0.046177 59 | 0.083993 0.043888 60 | 0.072782 0.041503 61 | 0.062705 0.039079 62 | 0.053802 0.036668 63 | 0.046033 0.034310 64 | 0.039294 0.032026 65 | 0.033459 0.029826 66 | 0.028396 0.027706 67 | 0.023986 0.025657 68 | 0.020129 0.023668 69 | 0.016743 0.021726 70 | 0.013763 0.019819 71 | 0.011140 0.017934 72 | 0.008834 0.016059 73 | 0.006820 0.014186 74 | 0.005078 0.012305 75 | 0.003599 0.010412 76 | 0.002378 0.008506 77 | 0.001413 0.006590 78 | 0.000704 0.004674 79 | 0.000246 0.002774 80 | 0.000026 0.000910 81 | 0.000026 -0.000910 82 | 0.000246 -0.002774 83 | 0.000704 -0.004674 84 | 0.001413 -0.006590 85 | 0.002378 -0.008506 86 | 0.003599 -0.010412 87 | 0.005078 -0.012305 88 | 0.006820 -0.014186 89 | 0.008834 -0.016059 90 | 0.011140 -0.017934 91 | 0.013763 -0.019819 92 | 0.016743 -0.021726 93 | 0.020129 -0.023668 94 | 0.023986 -0.025657 95 | 0.028396 -0.027706 96 | 0.033459 -0.029826 97 | 0.039295 -0.032026 98 | 0.046033 -0.034310 99 | 0.053802 -0.036668 100 | 0.062705 -0.039079 101 | 0.072782 -0.041503 102 | 0.083993 -0.043888 103 | 0.096218 -0.046177 104 | 0.109290 -0.048323 105 | 0.123034 -0.050294 106 | 0.137297 -0.052071 107 | 0.151958 -0.053649 108 | 0.166928 -0.055030 109 | 0.182143 -0.056222 110 | 0.197554 -0.057232 111 | 0.213128 -0.058070 112 | 0.228839 -0.058747 113 | 0.244668 -0.059270 114 | 0.260598 -0.059649 115 | 0.276617 -0.059891 116 | 0.292715 -0.060006 117 | 0.308883 -0.059999 118 | 0.325114 -0.059878 119 | 0.341401 -0.059648 120 | 0.357739 -0.059317 121 | 0.374123 -0.058888 122 | 0.390549 -0.058368 123 | 0.407014 -0.057761 124 | 0.423513 -0.057072 125 | 0.440044 -0.056305 126 | 0.456605 -0.055463 127 | 0.473192 -0.054551 128 | 0.489803 -0.053572 129 | 0.506436 -0.052530 130 | 0.523090 -0.051426 131 | 0.539761 -0.050265 132 | 0.556449 -0.049048 133 | 0.573152 -0.047778 134 | 0.589867 -0.046458 135 | 0.606594 -0.045088 136 | 0.623332 -0.043672 137 | 0.640078 -0.042211 138 | 0.656830 -0.040706 139 | 0.673589 -0.039159 140 | 0.690352 -0.037571 141 | 0.707119 -0.035943 142 | 0.723887 -0.034276 143 | 0.740656 -0.032570 144 | 0.757424 -0.030826 145 | 0.774190 -0.029046 146 | 0.790953 -0.027228 147 | 0.807711 -0.025373 148 | 0.824462 -0.023482 149 | 0.841202 -0.021554 150 | 0.857926 -0.019590 151 | 0.874624 -0.017591 152 | 0.891275 -0.015558 153 | 0.907837 -0.013497 154 | 0.924222 -0.011418 155 | 0.940264 -0.009344 156 | 0.955666 -0.007315 157 | 0.969992 -0.005394 158 | 0.982775 -0.003651 159 | 0.993723 -0.002137 160 | 1.000000 -0.001260 -------------------------------------------------------------------------------- /src/CFD/Aerodynamics/Airfoil_Database/NACA1408.dat: -------------------------------------------------------------------------------- 1 | 1.00000 0.00084 2 | 0.95016 0.00698 3 | 0.90027 0.01271 4 | 0.80039 0.02305 5 | 0.70041 0.03193 6 | 0.60034 0.03931 7 | 0.50020 0.04502 8 | 0.40000 0.04869 9 | 0.29950 0.04939 10 | 0.24926 0.04819 11 | 0.19904 0.04574 12 | 0.14889 0.04171 13 | 0.09883 0.03558 14 | 0.07386 0.03138 15 | 0.04896 0.02602 16 | 0.02418 0.01862 17 | 0.01189 0.01324 18 | 0.00000 0.00000 19 | 0.01311 -0.01200 20 | 0.02582 -0.01620 21 | 0.05104 -0.02134 22 | 0.07614 -0.02458 23 | 0.10117 -0.02682 24 | 0.15111 -0.02953 25 | 0.20096 -0.03074 26 | 0.25074 -0.03101 27 | 0.30050 -0.03063 28 | 0.40000 -0.02869 29 | 0.49980 -0.02556 30 | 0.59966 -0.02153 31 | 0.69959 -0.01693 32 | 0.79961 -0.01193 33 | 0.89973 -0.00659 34 | 0.94984 -0.00378 35 | 1.00000 -0.00084 -------------------------------------------------------------------------------- /src/CFD/Aerodynamics/Airfoil_Database/NACA2412.dat: -------------------------------------------------------------------------------- 1 | 1.0000 0.0013 2 | 0.9500 0.0114 3 | 0.9000 0.0208 4 | 0.8000 0.0375 5 | 0.7000 0.0518 6 | 0.6000 0.0636 7 | 0.5000 0.0724 8 | 0.4000 0.0780 9 | 0.3000 0.0788 10 | 0.2500 0.0767 11 | 0.2000 0.0726 12 | 0.1500 0.0661 13 | 0.1000 0.0563 14 | 0.0750 0.0496 15 | 0.0500 0.0413 16 | 0.0250 0.0299 17 | 0.0125 0.0215 18 | 0.0000 0.0000 19 | 0.0125 -0.0165 20 | 0.0250 -0.0227 21 | 0.0500 -0.0301 22 | 0.0750 -0.0346 23 | 0.1000 -0.0375 24 | 0.1500 -0.0410 25 | 0.2000 -0.0423 26 | 0.2500 -0.0422 27 | 0.3000 -0.0412 28 | 0.4000 -0.0380 29 | 0.5000 -0.0334 30 | 0.6000 -0.0276 31 | 0.7000 -0.0214 32 | 0.8000 -0.0150 33 | 0.9000 -0.0082 34 | 0.9500 -0.0048 35 | 1.0000 -0.0013 -------------------------------------------------------------------------------- /src/CFD/Aerodynamics/Airfoil_Database/NACA4412.dat: -------------------------------------------------------------------------------- 1 | 1.0000 0.0013 2 | 0.9500 0.0147 3 | 0.9000 0.0271 4 | 0.8000 0.0489 5 | 0.7000 0.0669 6 | 0.6000 0.0814 7 | 0.5000 0.0919 8 | 0.4000 0.0980 9 | 0.3000 0.0976 10 | 0.2500 0.0941 11 | 0.2000 0.0880 12 | 0.1500 0.0789 13 | 0.1000 0.0659 14 | 0.0750 0.0576 15 | 0.0500 0.0473 16 | 0.0250 0.0339 17 | 0.0125 0.0244 18 | 0.0000 0.0000 19 | 0.0125 -0.0143 20 | 0.0250 -0.0195 21 | 0.0500 -0.0249 22 | 0.0750 -0.0274 23 | 0.1000 -0.0286 24 | 0.1500 -0.0288 25 | 0.2000 -0.0274 26 | 0.2500 -0.0250 27 | 0.3000 -0.0226 28 | 0.4000 -0.0180 29 | 0.5000 -0.0140 30 | 0.6000 -0.0100 31 | 0.7000 -0.0065 32 | 0.8000 -0.0039 33 | 0.9000 -0.0022 34 | 0.9500 -0.0016 35 | 1.0000 -0.0013 -------------------------------------------------------------------------------- /src/CFD/Aerodynamics/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atharvaaalok/Airfoil-Shape-Optimization-RL/55da5c55ce028480df2c440e74ca534717df898e/src/CFD/Aerodynamics/__init__.py -------------------------------------------------------------------------------- /src/CFD/Aerodynamics/xfoil.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atharvaaalok/Airfoil-Shape-Optimization-RL/55da5c55ce028480df2c440e74ca534717df898e/src/CFD/Aerodynamics/xfoil.exe -------------------------------------------------------------------------------- /src/CFD/CFD_Explanation.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | from Aerodynamics import Aerodynamics 4 | 5 | import time 6 | 7 | import concurrent.futures 8 | 9 | 10 | 11 | def L_by_D_func(airfoil_name): 12 | # Get coordinates of airfoil 13 | airfoil_coordinates = np.loadtxt('Aerodynamics/Airfoil_Database/' + airfoil_name + '.dat') 14 | 15 | # Create airfoil object to analyze properties 16 | airfoil = Aerodynamics.Airfoil(airfoil_coordinates, airfoil_name) 17 | 18 | # Get lift-to-drag ratio 19 | Reynolds_num = 1e6 20 | L_by_D_ratio = airfoil.get_L_by_D(Reynolds_num) 21 | return L_by_D_ratio 22 | 23 | 24 | airfoil_name_list = ['NACA0006', 'NACA0009', 'NACA0012', 'NACA1408', 'NACA2412', 'NACA4412'] 25 | 26 | 27 | if __name__ == '__main__': 28 | 29 | # Time sequential compute 30 | print('Sequential Compute' + '\n' + '-' * 30) 31 | start = time.perf_counter() 32 | 33 | for airfoil_name in airfoil_name_list: 34 | L_by_D_ratio = L_by_D_func(airfoil_name) 35 | print(L_by_D_ratio) 36 | 37 | finish = time.perf_counter() 38 | print() 39 | print(f'Finished in {round(finish - start, 2)} second(s)') 40 | 41 | 42 | # Time parallel compute 43 | print('\n' + 'Parallel Compute' + '\n' + '-' * 30) 44 | start = time.perf_counter() 45 | 46 | with concurrent.futures.ProcessPoolExecutor(max_workers = 60) as executor: 47 | results = executor.map(L_by_D_func, airfoil_name_list) 48 | # Map gives results in the order they were started 49 | 50 | for result in results: 51 | print(result) 52 | 53 | finish = time.perf_counter() 54 | print() 55 | print(f'Finished in {round(finish - start, 2)} second(s)') -------------------------------------------------------------------------------- /src/CFD/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atharvaaalok/Airfoil-Shape-Optimization-RL/55da5c55ce028480df2c440e74ca534717df898e/src/CFD/__init__.py -------------------------------------------------------------------------------- /src/Idea_2/Idea_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atharvaaalok/Airfoil-Shape-Optimization-RL/55da5c55ce028480df2c440e74ca534717df898e/src/Idea_2/Idea_2.png -------------------------------------------------------------------------------- /src/Idea_2/Main.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import torch 3 | from ..ML_Modules.NeuralNetwork import NeuralNetwork, Train_NN 4 | from ..CFD.Aerodynamics import Aerodynamics 5 | import matplotlib.pyplot as plt 6 | 7 | torch.manual_seed(42) 8 | 9 | # Create state and action lists 10 | s_list = [] 11 | a_list = [] 12 | 13 | # Generate initial state 14 | # s0 = torch.tensor([[1, 0], [0.75, 0.05], [0.5, 0.10], [0.25, 0.05], [0, 0], [0.25, -0.05], [0.5, -0.10], [0.75, -0.05], [1, 0]]) 15 | s0 = torch.tensor([[1, 0], [0.75, 0.05], [0.625, 0.075], [0.5, 0.1], [0.25, 0.05], [0, 0], [0.25, -0.05], [0.5, -0.1], [0.625, -0.075], [0.75, -0.05], [1, 0]]) 16 | idx_to_change = [1, 2, 3, 4, 6, 7, 8, 9] 17 | s_list.append(s0) 18 | 19 | # Total number of points to represent the shape 20 | pts_on_curve = s0.shape[0] 21 | # action_idx = [1, 2, 3, 5, 6, 7] 22 | action_idx = [1, 2, 3, 4, 6, 7, 8, 9] 23 | action_dim = len(action_idx) 24 | 25 | # Plot the initial airfoil 26 | airfoil_coordinates = s0.numpy() 27 | plt.plot(airfoil_coordinates[:, 0], airfoil_coordinates[:, 1], marker = 'o') 28 | ax = plt.gca() 29 | ax.set_aspect('equal', adjustable='box') 30 | # plt.show() 31 | 32 | 33 | # Define the experiment run count 34 | total_exp = 20 35 | print('Running Experiments\n' + 30 * '-') 36 | for i_exp in range(total_exp): 37 | print(f'Experiment Count: {i_exp + 1}') 38 | # Get the state 39 | s = s_list[i_exp] 40 | 41 | # Generate random actions to take 42 | num_actions = 20 43 | # Calculate the L/D ratio for each new state resulting from the actions and determine the best action 44 | Rewards = torch.zeros(num_actions + 1) 45 | a_temp = [] 46 | 47 | # Define action scaling properties 48 | step = 0.0075 49 | p = 0.1 50 | jump = step / ((i_exp + 1) ** p) 51 | 52 | for i_a in range(num_actions + 1): 53 | # Generate actions and make sure that actions are only taken for the movable points and the fixed points at (1, 0) and (0, 0) are untouched 54 | # Also generate an action that does not change the state (delta_x/y = 0) to ensure that if the current state is best, don't change it 55 | if i_a == num_actions: 56 | a = torch.zeros(pts_on_curve, 2) 57 | a_temp.append(a) 58 | else: 59 | a = torch.zeros(pts_on_curve, 2) 60 | a[action_idx, :] = jump * torch.rand(action_dim, 2) 61 | a_temp.append(a) 62 | 63 | # Get new states from current state and the generated action 64 | s_prime = s + a 65 | 66 | # Get the airfoil coordinates 67 | airfoil_coordinates = s_prime.numpy() 68 | 69 | # Create airfoil object to analyze properties 70 | airfoil_name = f'my_airfoil{i_exp}{i_a}' 71 | airfoil = Aerodynamics.Airfoil(airfoil_coordinates, airfoil_name) 72 | 73 | # Get L/D ratio 74 | Reynolds_num = 1e6 75 | reward = airfoil.get_L_by_D(Reynolds_num) 76 | # print(reward) 77 | if reward == None: 78 | # If xfoil doesn't converge give a large negative reward 79 | reward = -1000 80 | Rewards[i_a] = reward 81 | 82 | idx_max_reward = torch.argmax(Rewards) 83 | a_best = a_temp[idx_max_reward] 84 | max_reward = Rewards[idx_max_reward].item() 85 | print(f'Max reward: {max_reward}\n') 86 | 87 | # Get the new state corresponding to the best action 88 | s_new = s + a_best 89 | 90 | # Add the action and the new state to the state and action lists 91 | a_list.append(a_best) 92 | s_list.append(s_new) 93 | 94 | print() 95 | 96 | # Plot the new airfoil 97 | airfoil_coordinates = s_new.numpy() 98 | plt.plot(airfoil_coordinates[:, 0], airfoil_coordinates[:, 1], marker = 'o') 99 | ax = plt.gca() 100 | ax.set_aspect('equal', adjustable='box') 101 | plt.show() 102 | 103 | # Visualize the final airfoil in xfoil 104 | airfoil_name = 'final_airfoil' 105 | airfoil = Aerodynamics.Airfoil(airfoil_coordinates, airfoil_name) 106 | airfoil.visualize() 107 | 108 | S_input_list = [] 109 | A_input_list = [] 110 | 111 | for i in range(len(a_list)): 112 | S_input_list.append(torch.cat((s_list[i][:, 0], s_list[i][:, 1]))) 113 | A_input_list.append(torch.cat((a_list[i][:, 0], a_list[i][:, 1]))) 114 | 115 | # Train neural network using the states as input and the actions as the labeled outputs 116 | S = torch.stack(S_input_list) 117 | A = torch.stack(A_input_list) 118 | 119 | 120 | # Specify the size of the neural network and instantiate an object 121 | input_size = pts_on_curve * 2 122 | output_size = pts_on_curve * 2 123 | layer_size_list = [20, 20] 124 | 125 | NN_model = NeuralNetwork(input_size, output_size, layer_size_list) 126 | 127 | # Define hyperparameters 128 | learning_rate = 0.01 129 | training_epochs = 1000 130 | 131 | # Train the neural network 132 | Train_NN(S, A, NN_model, learning_rate, training_epochs) -------------------------------------------------------------------------------- /src/Lift_to_Drag_Predictor/Dataset/Archive/Arrays_as_rows_1.txt: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:88528dcc5c5bf1f4290246ba656dd151ff630979ea12891a0dce7d89fd2bb3b3 3 | size 111691346 4 | -------------------------------------------------------------------------------- /src/Lift_to_Drag_Predictor/Dataset/Archive/Rewards_as_rows_1.txt: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:e75c416ddfee24e90696589adafe6aa4455e287779ddb35a5e4d465ded01ff15 3 | size 5241840 4 | -------------------------------------------------------------------------------- /src/Lift_to_Drag_Predictor/Dataset/Arrays_as_rows - Copy.txt: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:26a97307ba0906d039b4b2a989639a2dc08575a3d07dded580707eebe9a82493 3 | size 27195 4 | -------------------------------------------------------------------------------- /src/Lift_to_Drag_Predictor/Dataset/Arrays_as_rows.txt: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:88528dcc5c5bf1f4290246ba656dd151ff630979ea12891a0dce7d89fd2bb3b3 3 | size 111691346 4 | -------------------------------------------------------------------------------- /src/Lift_to_Drag_Predictor/Dataset/Arrays_as_rows_a_scaling_100.txt: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:ecf73a37e026f77d5ab58a0dbfccb860d472fafa55a6f12ce7ddf7aabbda5279 3 | size 324095 4 | -------------------------------------------------------------------------------- /src/Lift_to_Drag_Predictor/Dataset/Rewards_as_rows - Copy.txt: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:3c296da0f544345bcb7b9749d5b093fa4bf2ec212af16769613eaeef54af14ba 3 | size 1274 4 | -------------------------------------------------------------------------------- /src/Lift_to_Drag_Predictor/Dataset/Rewards_as_rows.txt: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:e75c416ddfee24e90696589adafe6aa4455e287779ddb35a5e4d465ded01ff15 3 | size 5241840 4 | -------------------------------------------------------------------------------- /src/Lift_to_Drag_Predictor/Dataset/Rewards_as_rows_a_scaling_100.txt: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:775cc5307152f99c6675a19e7e52325d71b55fba72fbf91817b89778547db83d 3 | size 15213 4 | -------------------------------------------------------------------------------- /src/Lift_to_Drag_Predictor/Dataset_Generator.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from ..CFD.Aerodynamics import Aerodynamics 4 | 5 | import time 6 | import os 7 | 8 | NEGATIVE_REWARD = -50 9 | 10 | 11 | # Generate next state given the current state and action 12 | def generate_next_state(s_current, a_current): 13 | s_new = s_current + a_current 14 | return s_new 15 | 16 | # Generate reward corresponding to the state 17 | def generate_reward(s, a, s_new, airfoil_name = 'my_airfoil'): 18 | airfoil_name = str(np.random.rand(1))[3:-1] 19 | 20 | airfoil_name = 'air' + airfoil_name 21 | # Get coordinates of airfoil 22 | airfoil_coordinates = s_new 23 | 24 | # Create airfoil object to analyze properties 25 | airfoil = Aerodynamics.Airfoil(airfoil_coordinates, airfoil_name) 26 | 27 | # Get lift-to-drag ratio 28 | Reynolds_num = 1e6 29 | L_by_D_ratio = airfoil.get_L_by_D(Reynolds_num) 30 | 31 | # If Xfoil did not converge give large negative reward 32 | if L_by_D_ratio == None: 33 | L_by_D_ratio = NEGATIVE_REWARD 34 | 35 | return L_by_D_ratio 36 | 37 | 38 | # Function to read a random line from the file and convert it into a NumPy vector 39 | def read_random_line(file_path): 40 | with open(file_path, 'r') as file: 41 | # Count the total number of lines in the file 42 | num_lines = sum(1 for line in file) 43 | 44 | # Generate a random line number within the range of total lines 45 | random_line_number = np.random.randint(0, num_lines - 1) 46 | 47 | # Read the selected random line from the file 48 | with open(file_path, 'r') as file: 49 | for line_num, line in enumerate(file): 50 | if line_num == random_line_number: 51 | # Convert the line into a NumPy vector 52 | numpy_vector = np.fromstring(line, dtype = float, sep=' ') 53 | return numpy_vector # Return the NumPy vector 54 | 55 | 56 | 57 | # File paths 58 | directory_path = os.path.dirname(__file__) 59 | numpy_arr_file_path = directory_path + '/Dataset/Arrays_as_rows.txt' 60 | rewards_file_path = directory_path + '/Dataset/Rewards_as_rows.txt' 61 | 62 | # Trajectory parameters 63 | T = 25 64 | N = 10 65 | 66 | 67 | 68 | if __name__ == '__main__': 69 | 70 | start_time = time.perf_counter() 71 | 72 | # Initialize a state 73 | # s0 = np.array([[1, 0], [0.75, 0.05], [0.625, 0.075], [0.5, 0.1], [0.25, 0.05], [0, 0], [0.25, -0.05], [0.5, -0.1], [0.625, -0.075], [0.75, -0.05], [1, 0]]) 74 | idx_to_change = [1, 2, 3, 4, 6, 7, 8, 9] 75 | 76 | max_iterations = 50 77 | # s_new = s0 78 | 79 | for iteration in range(max_iterations): 80 | 81 | total_new_points = 0 82 | max_reward = 0 83 | state_list = [] 84 | reward_list = [] 85 | 86 | for i_N in range(N): 87 | # Get an initial state from file 88 | num_lines = sum(1 for _ in open(numpy_arr_file_path)) 89 | s_new = read_random_line(numpy_arr_file_path).reshape(-1, 2) 90 | 91 | for t in range(T): 92 | # Get state 93 | s = s_new 94 | # Generate an action 95 | a = np.zeros(s.shape) 96 | a[idx_to_change, :] = np.random.rand(len(idx_to_change), 2) / 1000 97 | 98 | # Get new state using this action 99 | s_new = generate_next_state(s, a) 100 | 101 | # Generate reward for the new state 102 | r = generate_reward(s, a, s_new) 103 | 104 | # If we achieved convergence record the state-reward pair 105 | if r > NEGATIVE_REWARD + 1: 106 | # Add state-reward tuple to list 107 | state_list.append(s_new.flatten()) 108 | reward_list.append(np.array([r])) 109 | # Increment counter for total valid states generated 110 | total_new_points += 1 111 | if r > max_reward: 112 | max_reward = r 113 | 114 | # Save data into a file and clear the arrays to save space 115 | # Save state-reward tuples to file 116 | with open(numpy_arr_file_path, 'a') as file1, open(rewards_file_path, 'a') as file2: 117 | np.savetxt(file1, state_list) 118 | np.savetxt(file2, reward_list) 119 | 120 | print(f'Total new points: {total_new_points}') 121 | print(f'Maximum reward: {max_reward}') 122 | print() 123 | 124 | 125 | # Print finish time and the time taken per iteration 126 | finish_time = time.perf_counter() 127 | print(f'Total time taken for {max_iterations}: {finish_time - start_time}') 128 | print(f'Time per iteration: {(finish_time - start_time) / (max_iterations * T * N)}') -------------------------------------------------------------------------------- /src/Lift_to_Drag_Predictor/Dataset_Generator_High_Ascaling.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from ..CFD.Aerodynamics import Aerodynamics 4 | 5 | import time 6 | import os 7 | 8 | NEGATIVE_REWARD = -50 9 | 10 | 11 | # Generate next state given the current state and action 12 | def generate_next_state(s_current, a_current): 13 | s_new = s_current + a_current 14 | return s_new 15 | 16 | # Generate reward corresponding to the state 17 | def generate_reward(s, a, s_new, airfoil_name = 'my_airfoil'): 18 | airfoil_name = str(np.random.rand(1))[3:-1] 19 | 20 | airfoil_name = 'air' + airfoil_name 21 | # Get coordinates of airfoil 22 | airfoil_coordinates = s_new 23 | 24 | # Create airfoil object to analyze properties 25 | airfoil = Aerodynamics.Airfoil(airfoil_coordinates, airfoil_name) 26 | 27 | # Get lift-to-drag ratio 28 | Reynolds_num = 1e6 29 | L_by_D_ratio = airfoil.get_L_by_D(Reynolds_num) 30 | 31 | # If Xfoil did not converge give large negative reward 32 | if L_by_D_ratio == None: 33 | L_by_D_ratio = NEGATIVE_REWARD 34 | 35 | return L_by_D_ratio 36 | 37 | 38 | # Function to read a random line from the file and convert it into a NumPy vector 39 | def read_random_line(file_path): 40 | with open(file_path, 'r') as file: 41 | # Count the total number of lines in the file 42 | num_lines = sum(1 for line in file) 43 | 44 | # Generate a random line number within the range of total lines 45 | random_line_number = np.random.randint(0, num_lines - 1) 46 | 47 | # Read the selected random line from the file 48 | with open(file_path, 'r') as file: 49 | for line_num, line in enumerate(file): 50 | if line_num == random_line_number: 51 | # Convert the line into a NumPy vector 52 | numpy_vector = np.fromstring(line, dtype = float, sep=' ') 53 | return numpy_vector # Return the NumPy vector 54 | 55 | 56 | 57 | # File paths 58 | directory_path = os.path.dirname(__file__) 59 | numpy_arr_file_path = directory_path + '/Dataset/Arrays_as_rows_a_scaling_100.txt' 60 | rewards_file_path = directory_path + '/Dataset/Rewards_as_rows_a_scaling_100.txt' 61 | 62 | # Trajectory parameters 63 | T = 15 64 | N = 10 65 | 66 | 67 | 68 | if __name__ == '__main__': 69 | 70 | start_time = time.perf_counter() 71 | 72 | # Initialize a state 73 | # s0 = np.array([[1, 0], [0.75, 0.05], [0.625, 0.075], [0.5, 0.1], [0.25, 0.05], [0, 0], [0.25, -0.05], [0.5, -0.1], [0.625, -0.075], [0.75, -0.05], [1, 0]]) 74 | idx_to_change = [1, 2, 3, 4, 6, 7, 8, 9] 75 | 76 | max_iterations = 5 77 | # s_new = s0 78 | 79 | for iteration in range(max_iterations): 80 | 81 | total_new_points = 0 82 | max_reward = 0 83 | state_list = [] 84 | reward_list = [] 85 | 86 | for i_N in range(N): 87 | # Get an initial state from file 88 | num_lines = sum(1 for _ in open(numpy_arr_file_path)) 89 | s_new = read_random_line(numpy_arr_file_path).reshape(-1, 2) 90 | 91 | for t in range(T): 92 | # Get state 93 | s = s_new 94 | # Generate an action 95 | a = np.zeros(s.shape) 96 | a[idx_to_change, :] = np.random.rand(len(idx_to_change), 2) / 100 97 | 98 | # Get new state using this action 99 | s_new = generate_next_state(s, a) 100 | 101 | # Generate reward for the new state 102 | r = generate_reward(s, a, s_new) 103 | 104 | # If we achieved convergence record the state-reward pair 105 | if r > NEGATIVE_REWARD + 1: 106 | # Add state-reward tuple to list 107 | state_list.append(s_new.flatten()) 108 | reward_list.append(np.array([r])) 109 | # Increment counter for total valid states generated 110 | total_new_points += 1 111 | if r > max_reward: 112 | max_reward = r 113 | 114 | # Save data into a file and clear the arrays to save space 115 | # Save state-reward tuples to file 116 | with open(numpy_arr_file_path, 'a') as file1, open(rewards_file_path, 'a') as file2: 117 | np.savetxt(file1, state_list) 118 | np.savetxt(file2, reward_list) 119 | 120 | print(f'Total new points: {total_new_points}') 121 | print(f'Maximum reward: {max_reward}') 122 | print() 123 | 124 | 125 | # Print finish time and the time taken per iteration 126 | finish_time = time.perf_counter() 127 | print(f'Total time taken for {max_iterations}: {finish_time - start_time}') 128 | print(f'Time per iteration: {(finish_time - start_time) / (max_iterations * T * N)}') -------------------------------------------------------------------------------- /src/Lift_to_Drag_Predictor/High_L_by_D_Airfoil.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from ..CFD.Aerodynamics import Aerodynamics 4 | 5 | 6 | # airfoil_coordinates = np.array([[1, 0], [0.75, 0.05], [0.5, 0.10], [0.25, 0.05], [0, 0], [0.25, -0.05], [0.5, -0.10], [0.75, -0.05], [1, 0]]) 7 | airfoil_coordinates = np.array([[1, 0], [0.75, 0.05], [0.625, 0.08], [0.5, 0.1], [0.25, 0.10], [0, 0], [0.25, -0.004], [0.5, 0.005], [0.625, 0.008], [0.75, 0.015], [1, 0]]) 8 | # Visualize the airfoil in xfoil 9 | airfoil_name = 'my_airfoil' 10 | airfoil = Aerodynamics.Airfoil(airfoil_coordinates, airfoil_name) 11 | airfoil.visualize() 12 | 13 | Reynolds_num = 1e6 14 | print(airfoil.get_L_by_D(Reynolds_num)) -------------------------------------------------------------------------------- /src/Lift_to_Drag_Predictor/Visualize_Airfoils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | 4 | from ..CFD.Aerodynamics import Aerodynamics 5 | 6 | import os 7 | 8 | # File paths 9 | directory_path = os.path.dirname(__file__) 10 | numpy_arr_file_path = directory_path + '/Dataset/Arrays_as_rows_a_scaling_100.txt' 11 | rewards_file_path = directory_path + '/Dataset/Rewards_as_rows_a_scaling_100.txt' 12 | 13 | 14 | # Function to read a random line from the file and convert it into a NumPy vector 15 | def read_random_line(file_path): 16 | with open(file_path, 'r') as file: 17 | # Count the total number of lines in the file 18 | num_lines = sum(1 for line in file) 19 | 20 | # Generate a random line number within the range of total lines 21 | random_line_number = np.random.randint(0, num_lines - 1) 22 | 23 | # Read the selected random line from the file 24 | with open(file_path, 'r') as file: 25 | for line_num, line in enumerate(file): 26 | if line_num == random_line_number: 27 | # Convert the line into a NumPy vector 28 | numpy_vector = np.fromstring(line, dtype = float, sep=' ') 29 | return numpy_vector # Return the NumPy vector 30 | 31 | 32 | plt.ion() 33 | plt.xlabel('x') 34 | plt.ylabel('y') 35 | plt.title('My Airfoil') 36 | plt.grid(True) 37 | ax = plt.gca() 38 | ax.set_aspect('equal', adjustable='box') 39 | plt.show() 40 | 41 | total_airfoils_visualize = 100 42 | 43 | for i in range(total_airfoils_visualize): 44 | airfoil_coordinates = read_random_line(numpy_arr_file_path).reshape(-1, 2) 45 | # Visualize the airfoil in xfoil 46 | airfoil_name = 'my_airfoil' 47 | airfoil = Aerodynamics.Airfoil(airfoil_coordinates, airfoil_name) 48 | # airfoil.visualize() 49 | plt.plot(airfoil_coordinates[:, 0], airfoil_coordinates[:, 1], marker = 'o') 50 | plt.pause(0.1) 51 | 52 | Reynolds_num = 1e6 53 | print(airfoil.get_L_by_D(Reynolds_num)) 54 | 55 | 56 | plt.ioff() -------------------------------------------------------------------------------- /src/ML_Modules/Main.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Import necessary libraries and modules" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": 1, 13 | "metadata": {}, 14 | "outputs": [], 15 | "source": [ 16 | "# Import libraries\n", 17 | "import torch\n", 18 | "\n", 19 | "# Import modules\n", 20 | "from NeuralNetwork import NeuralNetwork, Train_NN" 21 | ] 22 | }, 23 | { 24 | "cell_type": "markdown", 25 | "metadata": {}, 26 | "source": [ 27 | "# Import function and Produce Data\n", 28 | "Import the function that you are trying to model using the neural network.\n", 29 | "\n", 30 | "Generate labeled data to train the neural network." 31 | ] 32 | }, 33 | { 34 | "cell_type": "code", 35 | "execution_count": 2, 36 | "metadata": {}, 37 | "outputs": [ 38 | { 39 | "name": "stdout", 40 | "output_type": "stream", 41 | "text": [ 42 | "torch.Size([10000, 2])\n", 43 | "torch.Size([10000, 2])\n" 44 | ] 45 | } 46 | ], 47 | "source": [ 48 | "# Import the objective function that we are trying to fit\n", 49 | "from Objective_function import func_scalar, func_vector\n", 50 | "\n", 51 | "# Generate training data\n", 52 | "num_samples = 10000\n", 53 | "\n", 54 | "x1_train = torch.rand(num_samples) * 5\n", 55 | "x2_train = torch.rand(num_samples) * 5\n", 56 | "\n", 57 | "# REMEMBER: Input data to the neural network consists of the training examples as rows\n", 58 | "input_data = torch.stack((x1_train, x2_train), dim = 1)\n", 59 | "print(input_data.shape)\n", 60 | "\n", 61 | "# Generate labeled outputs. Evaluate your function at the above input samples and generate labeled dataset.\n", 62 | "# Tweak the function file accordingly as per requirement\n", 63 | "output_values = func_vector(input_data)\n", 64 | "print(output_values.shape)" 65 | ] 66 | }, 67 | { 68 | "cell_type": "markdown", 69 | "metadata": {}, 70 | "source": [ 71 | "# Make Neural Network\n", 72 | "Specify layer sizes and create neural network object." 73 | ] 74 | }, 75 | { 76 | "cell_type": "code", 77 | "execution_count": 3, 78 | "metadata": {}, 79 | "outputs": [], 80 | "source": [ 81 | "# Specify size of neural network\n", 82 | "input_size = 2\n", 83 | "output_size = 2\n", 84 | "# Two hidden layers of sizes 16 each\n", 85 | "layer_size_list = [25, 25]\n", 86 | "\n", 87 | "# Instantiate neural network\n", 88 | "model = NeuralNetwork(input_size, output_size, layer_size_list)" 89 | ] 90 | }, 91 | { 92 | "cell_type": "markdown", 93 | "metadata": {}, 94 | "source": [ 95 | "# Define Hyperparameters\n", 96 | "Define the learning rate and the number of gradient descent steps to take." 97 | ] 98 | }, 99 | { 100 | "cell_type": "code", 101 | "execution_count": 4, 102 | "metadata": {}, 103 | "outputs": [], 104 | "source": [ 105 | "learning_rate = 0.01\n", 106 | "epochs = 10000" 107 | ] 108 | }, 109 | { 110 | "cell_type": "markdown", 111 | "metadata": {}, 112 | "source": [ 113 | "# Train the Neural Network" 114 | ] 115 | }, 116 | { 117 | "cell_type": "code", 118 | "execution_count": 5, 119 | "metadata": {}, 120 | "outputs": [ 121 | { 122 | "name": "stdout", 123 | "output_type": "stream", 124 | "text": [ 125 | "Epoch [1/10000], Loss: 3373.7920\n", 126 | "Epoch [501/10000], Loss: 3.8239\n", 127 | "Epoch [1001/10000], Loss: 1.1904\n", 128 | "Epoch [1501/10000], Loss: 0.6880\n", 129 | "Epoch [2001/10000], Loss: 0.5866\n", 130 | "Epoch [2501/10000], Loss: 0.4829\n", 131 | "Epoch [3001/10000], Loss: 0.4038\n", 132 | "Epoch [3501/10000], Loss: 0.3364\n", 133 | "Epoch [4001/10000], Loss: 0.2791\n", 134 | "Epoch [4501/10000], Loss: 0.2441\n", 135 | "Epoch [5001/10000], Loss: 0.2198\n", 136 | "Epoch [5501/10000], Loss: 0.2046\n", 137 | "Epoch [6001/10000], Loss: 0.1963\n", 138 | "Epoch [6501/10000], Loss: 0.1862\n", 139 | "Epoch [7001/10000], Loss: 0.1787\n", 140 | "Epoch [7501/10000], Loss: 0.1710\n", 141 | "Epoch [8001/10000], Loss: 0.7073\n", 142 | "Epoch [8501/10000], Loss: 0.3043\n", 143 | "Epoch [9001/10000], Loss: 0.1485\n", 144 | "Epoch [9501/10000], Loss: 0.1320\n" 145 | ] 146 | } 147 | ], 148 | "source": [ 149 | "Train_NN(input_data, output_values, model, learning_rate, epochs)" 150 | ] 151 | }, 152 | { 153 | "cell_type": "markdown", 154 | "metadata": {}, 155 | "source": [ 156 | "# Test trained model on new data" 157 | ] 158 | }, 159 | { 160 | "cell_type": "code", 161 | "execution_count": 6, 162 | "metadata": {}, 163 | "outputs": [ 164 | { 165 | "name": "stdout", 166 | "output_type": "stream", 167 | "text": [ 168 | "True Output = tensor([[25., 91.]])\n", 169 | "Predicted value is: tensor([[24.9625, 91.4093]], grad_fn=)\n" 170 | ] 171 | } 172 | ], 173 | "source": [ 174 | "input_test = torch.Tensor([[3.0, 4.0]])\n", 175 | "output_test = func_vector(input_test)\n", 176 | "print(f'True Output = {output_test}')\n", 177 | "\n", 178 | "predicted_value = model(input_test)\n", 179 | "print(f\"Predicted value is: {predicted_value}\")" 180 | ] 181 | } 182 | ], 183 | "metadata": { 184 | "kernelspec": { 185 | "display_name": "Python 3", 186 | "language": "python", 187 | "name": "python3" 188 | }, 189 | "language_info": { 190 | "codemirror_mode": { 191 | "name": "ipython", 192 | "version": 3 193 | }, 194 | "file_extension": ".py", 195 | "mimetype": "text/x-python", 196 | "name": "python", 197 | "nbconvert_exporter": "python", 198 | "pygments_lexer": "ipython3", 199 | "version": "3.11.5" 200 | } 201 | }, 202 | "nbformat": 4, 203 | "nbformat_minor": 2 204 | } 205 | -------------------------------------------------------------------------------- /src/ML_Modules/My_NN/NeuralNetwork_My.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | class NeuralNetwork: 4 | def __init__(self, input_size, output_size, layer_size_list): 5 | self.input_size = input_size 6 | self.output_size = output_size 7 | self.layers = [] 8 | layer_size_list = [input_size] + layer_size_list + [output_size] 9 | for i in range(1, len(layer_size_list)): 10 | layer_size = layer_size_list[i] 11 | prev_layer_size = layer_size_list[i - 1] 12 | self.layers.append(Layer(layer_size, prev_layer_size)) 13 | 14 | def NN_eval(self, X): 15 | vec = X.copy() 16 | for layer in self.layers: 17 | vec = ReLU(((layer.W @ vec).T + layer.b).T) 18 | return vec.flatten() 19 | 20 | def NN_Loss(self, X, Y): 21 | Y_hat = self.NN_eval(X) 22 | err = Y_hat - Y 23 | Loss = (1 / 2) * err @ err 24 | return Loss 25 | 26 | def NN_train(self, X, Y, alpha): 27 | for k in range(1): 28 | for i in range(Y.shape[0]): 29 | x_i = X[:, i] 30 | y_i = Y[i] 31 | self.forward_pass(x_i) 32 | self.backward_pass(x_i, y_i) 33 | self.update_NN(alpha) 34 | Loss = self.NN_Loss(X, Y) 35 | print(f'Iteration: {k} Loss: {Loss}') 36 | # self.forward_pass(X) 37 | # self.backward_pass(X, Y) 38 | # self.update_NN(alpha) 39 | # Loss = self.NN_Loss(X, Y) 40 | # print(f'Iteration: {k} Loss: {Loss}') 41 | 42 | def forward_pass(self, X): 43 | vec = X.copy() 44 | for layer in self.layers: 45 | # vec = ReLU(((layer.W @ vec).T + layer.b).T) 46 | layer.z_eval(vec) 47 | layer.a_eval() 48 | vec = layer.a 49 | 50 | def backward_pass(self, X, Y): 51 | # Start with output layer 52 | layer = self.layers[-1] 53 | prev_layer = self.layers[-2] 54 | dJ_dz = - (Y - layer.z) 55 | dJ_dW = np.outer(dJ_dz, prev_layer.a) 56 | dJ_db = dJ_dz 57 | layer.dJdW = dJ_dW 58 | layer.dJdb = dJ_db 59 | 60 | for i in range(-2, -len(self.layers), -1): 61 | layer = self.layers[i] 62 | prev_layer = self.layers[i - 1] 63 | next_layer = self.layers[i + 1] 64 | dJ_da = (next_layer.W).T @ dJ_dz 65 | dJ_dz = dJ_da * dReLU(layer.z) 66 | dJ_dW = np.outer(dJ_dz, prev_layer.a) 67 | dJ_db = dJ_dz 68 | layer.dJdW = dJ_dW 69 | layer.dJdb = dJ_db 70 | 71 | # Update first layer 72 | layer = self.layers[-len(self.layers)] 73 | next_layer = self.layers[-len(self.layers) + 1] 74 | dJ_da = (next_layer.W).T @ dJ_dz 75 | dJ_dz = dJ_da * dReLU(layer.z) 76 | dJ_dW = np.outer(dJ_dz, ReLU(X)) 77 | dJ_db = dJ_dz 78 | layer.dJdW = dJ_dW 79 | layer.dJdb = dJ_db 80 | 81 | def update_NN(self, alpha): 82 | for layer in self.layers: 83 | a = layer.W.copy() 84 | layer.W -= alpha * layer.dJdW 85 | layer.b -= alpha * layer.dJdb 86 | 87 | 88 | class Layer: 89 | def __init__(self, layer_size, prev_layer_size): 90 | self.size = layer_size 91 | mean = 0 92 | std = 1 93 | self.W = mean + std * np.random.randn(layer_size, prev_layer_size) 94 | self.b = mean + std * np.zeros(layer_size) 95 | self.z = mean + std * np.zeros(layer_size) 96 | self.a = mean + std * np.zeros(layer_size) 97 | self.dJdW = mean + std * np.random.randn(layer_size, prev_layer_size) 98 | self.dJdb = mean + std * np.random.randn(layer_size) 99 | 100 | def z_eval(self, X): 101 | self.z = self.W @ X + self.b 102 | 103 | def a_eval(self): 104 | self.a = ReLU(self.z) 105 | 106 | 107 | def ReLU(X): 108 | return X * (X > 0) 109 | 110 | def dReLU(X): 111 | return 1.0 * (X > 0) 112 | -------------------------------------------------------------------------------- /src/ML_Modules/My_NN/Trial.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import NeuralNetwork 3 | from NeuralNetwork import * 4 | import matplotlib.pyplot as plt 5 | 6 | 7 | def my_func(x, y): 8 | return x ** 2 + y ** 2 9 | 10 | input_size = 2 11 | output_size = 1 12 | layer_size_list = [15, 15] 13 | 14 | my_nn = NeuralNetwork(input_size, output_size, layer_size_list) 15 | 16 | n = 10000 17 | x1 = np.linspace(-10, 10, num = n) 18 | x2 = np.linspace(-10, 10, num = n) 19 | np.random.shuffle(x1) 20 | np.random.shuffle(x2) 21 | 22 | X = np.vstack((x1, x2)) 23 | Y = my_func(x1, x2) 24 | 25 | # Evaluate the predictions and loss pre-training 26 | Y_pretrain = my_nn.NN_eval(X) 27 | L_pretrain = my_nn.NN_Loss(X, Y) 28 | print(f'{Y_pretrain=}') 29 | print(f'{L_pretrain=}') 30 | 31 | alpha = 0.01 32 | my_nn.NN_train(X, Y, alpha) 33 | 34 | Y_posttrain = my_nn.NN_eval(X) 35 | L_posttrain = my_nn.NN_Loss(X, Y) 36 | print(f'{Y_posttrain=}') 37 | print(f'{L_posttrain=}') 38 | 39 | quit() 40 | 41 | 42 | def my_func(x, y): 43 | return x **2 + y ** 2 44 | 45 | input_size = 2 46 | output_size = 1 47 | layer_size_list = [2, 2] 48 | 49 | my_nn = NeuralNetwork.NeuralNetwork(input_size, output_size, layer_size_list) 50 | 51 | n = 100 52 | x1 = np.linspace(-10, 10, n) 53 | x2 = np.linspace(-10, 10, n) 54 | np.random.shuffle(x1) 55 | np.random.shuffle(x2) 56 | 57 | X = np.vstack((x1, x2)).T 58 | Y = my_func(x1, x2) 59 | 60 | my_nn.NN_train(X, Y) 61 | 62 | Y_pred = np.zeros(n) 63 | for i in range(n): 64 | x_i = X[i, :] 65 | Y_pred[i] = my_nn.NN_eval(x_i)[0] 66 | 67 | 68 | my_Y = np.vstack((Y, Y_pred)).T 69 | print(my_Y) -------------------------------------------------------------------------------- /src/ML_Modules/NeuralNetwork.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import torch.nn as nn 3 | import torch.optim as optim 4 | 5 | 6 | class NeuralNetwork(nn.Module): 7 | def __init__(self, input_size, output_size, layer_size_list): 8 | super(NeuralNetwork, self).__init__() 9 | layer_size_list = [input_size] + layer_size_list + [output_size] 10 | self.fc = nn.ModuleList([ 11 | nn.Linear(layer_size_list[i], layer_size_list[i + 1]) 12 | for i in range(len(layer_size_list) - 1) 13 | ]) 14 | 15 | def forward(self, x): 16 | for layer in self.fc[:-1]: 17 | x = torch.relu(layer(x)) 18 | x = self.fc[-1](x) 19 | return x 20 | 21 | def Train_NN(input_data, output_values, model, learning_rate, epochs): 22 | criterion = nn.MSELoss() 23 | optimizer = optim.Adam(model.parameters(), lr = learning_rate) 24 | 25 | # Training loop 26 | print('Training the Neural Network\n' + 30 * '-') 27 | for epoch in range(epochs): 28 | optimizer.zero_grad() 29 | outputs = model(input_data) 30 | loss = criterion(outputs, output_values) 31 | loss.backward() 32 | optimizer.step() 33 | 34 | if epoch % int(epochs * (5 / 100)) == 0: 35 | print(f'Epoch [{epoch+1}/{epochs}], Loss: {loss.item():.4f}') -------------------------------------------------------------------------------- /src/ML_Modules/Objective_function.py: -------------------------------------------------------------------------------- 1 | import torch 2 | 3 | # Define the function x1^2 + x2^2 4 | def func_scalar(X): 5 | return torch.sum(X ** 2, dim = 1) 6 | 7 | def func_vector(X): 8 | a = torch.sum(X ** 2, dim = 1) 9 | b = torch.sum(X ** 3, dim = 1) 10 | return torch.stack((a, b), dim = 1) -------------------------------------------------------------------------------- /src/Policy_Gradient/Generate_Airfoil.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import numpy as np 3 | import matplotlib.pyplot as plt 4 | 5 | import os 6 | 7 | from .Policy_Gradient import * 8 | from .Helper import * 9 | from .Trajectory import Trajectory 10 | 11 | 12 | # Initialize the policy network with flexible hidden layers 13 | policy_net = PolicyNetwork(state_dim, action_dim, layer_size_list) 14 | 15 | # Define the covariance matrix Sigma as a trainable parameter 16 | Sigma = nn.Parameter(torch.randn(action_dim, action_dim), requires_grad = True) 17 | 18 | # Load progress if checkpoint is available 19 | if os.path.exists(trained_model_path): 20 | policy_net, Sigma = load_checkpoint(trained_model_path, policy_net, Sigma) 21 | else: 22 | print("Trained model doesn't exist. Train a model first by running Policy_Gradient.py") 23 | 24 | 25 | # Set policy parameters and the MDP functions required to generate trajectories 26 | policy_params = {'policy_net': policy_net, 'Sigma': Sigma} 27 | MDP_functions = {'generate_action': generate_action, 'generate_next_state': generate_next_state, 'generate_reward': generate_reward} 28 | 29 | 30 | 31 | 32 | # Now take actions according to the trained policy to generate an airfoil 33 | ### Change only the Total_improvements variable below in this file 34 | Total_improvements = 30 35 | # Run for long time to generate optimized airfoil - don't calculate rewards if the goal is just shape optimization 36 | airfoil_gen_trajectory = Trajectory(s0, a_params, Total_improvements, policy_params, MDP_functions, calculate_rewards = False) 37 | s_final = airfoil_gen_trajectory.SARS[-1].s_new 38 | 39 | 40 | # Plot initial airfoil 41 | airfoil_coordinates = s0.numpy() 42 | plt.plot(airfoil_coordinates[:, 0], airfoil_coordinates[:, 1], marker = 'o') 43 | 44 | # Plot final airfoil 45 | airfoil_coordinates = s_final.numpy() 46 | plt.plot(airfoil_coordinates[:, 0], airfoil_coordinates[:, 1], marker = 'o') 47 | ax = plt.gca() 48 | ax.set_aspect('equal', adjustable='box') 49 | plt.show() 50 | 51 | # Visualize the final airfoil in xfoil 52 | airfoil_name = 'final_airfoil' 53 | airfoil = Aerodynamics.Airfoil(airfoil_coordinates, airfoil_name) 54 | airfoil.visualize() -------------------------------------------------------------------------------- /src/Policy_Gradient/Helper.py: -------------------------------------------------------------------------------- 1 | import torch 2 | from torch import nn 3 | from torch import optim 4 | import numpy as np 5 | 6 | import random 7 | 8 | from ..CFD.Aerodynamics import Aerodynamics 9 | 10 | 11 | NEGATIVE_REWARD = -50 12 | 13 | # Generate next state given the current state and action 14 | def generate_next_state(s_current, a_current): 15 | s_new = s_current + a_current 16 | return s_new 17 | 18 | # Generate reward corresponding to the state 19 | def generate_reward(s, a, s_new, airfoil_name = 'my_airfoil'): 20 | airfoil_name = str(np.random.rand(1))[3:-1] 21 | 22 | airfoil_name = 'air' + airfoil_name 23 | # Get coordinates of airfoil 24 | airfoil_coordinates = s_new.cpu().numpy() 25 | 26 | # Create airfoil object to analyze properties 27 | airfoil = Aerodynamics.Airfoil(airfoil_coordinates, airfoil_name) 28 | 29 | # Get lift-to-drag ratio 30 | Reynolds_num = 1e6 31 | L_by_D_ratio = airfoil.get_L_by_D(Reynolds_num) 32 | 33 | # If Xfoil did not converge give large negative reward 34 | if L_by_D_ratio == None: 35 | L_by_D_ratio = NEGATIVE_REWARD 36 | 37 | return L_by_D_ratio 38 | 39 | 40 | # Generate action given the state and the policy 41 | def generate_action(s, a_params, policy_params): 42 | # Get the parameters 43 | policy_net = policy_params['policy_net'] 44 | Sigma = policy_params['Sigma'] 45 | 46 | idx_tochange = a_params['idx_tochange'] 47 | s_nn = s[idx_tochange, :] 48 | s_nn = torch.cat((s_nn[:, 0], s_nn[:, 1])) 49 | 50 | # Get the mean action from the policy network 51 | mu = policy_net(s_nn) 52 | 53 | # Sample action from a normal distribution with mean mu and covariance Sigma 54 | cov_matrix = torch.mm(Sigma, Sigma.t()) # Ensure covariance matrix is positive semi-definite 55 | distribution = torch.distributions.MultivariateNormal(mu, covariance_matrix = cov_matrix) 56 | 57 | # Sample an action from the distribution 58 | a_nn_orig = distribution.sample() 59 | 60 | # Calculate the log probability of taking this action 61 | log_prob = distribution.log_prob(a_nn_orig) 62 | 63 | a_scaling = a_params['a_scaling'] 64 | a_nn = a_scaling * a_nn_orig 65 | action_dim = a_nn.shape[0] 66 | a_nn = torch.stack((a_nn[:action_dim // 2], a_nn[action_dim // 2:]), dim = 1) 67 | 68 | a = torch.zeros_like(s) 69 | a[idx_tochange, :] = a_nn 70 | 71 | return (a, log_prob) 72 | 73 | 74 | 75 | # Define the neural network for the policy 76 | class PolicyNetwork(nn.Module): 77 | def __init__(self, input_size, output_size, layer_size_list): 78 | super(PolicyNetwork, self).__init__() 79 | layer_sizes = [input_size] + layer_size_list + [output_size] 80 | layers = [] 81 | for i in range(len(layer_sizes) - 1): 82 | layers.append(nn.Linear(layer_sizes[i], layer_sizes[i + 1])) 83 | if i < len(layer_sizes) - 2: 84 | layers.append(nn.ReLU()) 85 | self.layers = nn.Sequential(*layers) 86 | 87 | def forward(self, x): 88 | return self.layers(x) 89 | 90 | 91 | def calculate_gradient_objective(trajectory_list, causality = False, baseline = False): 92 | 93 | log_prob_mat = torch.stack([trajectory.action_log_prob for trajectory in trajectory_list]) 94 | reward_mat = torch.stack([trajectory.rewards for trajectory in trajectory_list]) 95 | 96 | N = len(trajectory_list) 97 | 98 | if causality == False and baseline == False: 99 | total_log_prob = log_prob_mat.sum(dim = 1) 100 | total_reward = reward_mat.sum(dim = 1) 101 | J = -1 * (1 / N) * (total_log_prob * total_reward).sum() 102 | elif causality == True and baseline == False: 103 | cumulative_reward = reward_mat.cumsum(dim = 1) 104 | J = -1 * (1 / N) * (log_prob_mat * cumulative_reward).sum() 105 | elif causality == False and baseline == True: 106 | total_log_prob = log_prob_mat.sum(dim = 1) 107 | total_reward = reward_mat.sum(dim = 1) 108 | average_reward = total_reward.mean() 109 | J = -1 * (1 / N) * (total_log_prob * (total_reward - average_reward)).sum() 110 | elif causality == True and baseline == True: 111 | cumulative_reward = reward_mat.cumsum(dim = 1) 112 | average_reward_step_t = reward_mat.mean(dim = 0) 113 | J = -1 * (1 / N) * (log_prob_mat * (cumulative_reward - average_reward_step_t.reshape(1, -1))).sum() 114 | 115 | return J 116 | 117 | 118 | def calculate_total_reward(trajectory_list): 119 | reward_mat = torch.stack([trajectory.rewards for trajectory in trajectory_list]) 120 | return reward_mat.sum() 121 | 122 | 123 | def get_trajectory_rewards(SAS_list): 124 | reward_list = [] 125 | for s, a, s_new in SAS_list: 126 | reward_list.append(generate_reward(s, a, s_new)) 127 | return reward_list 128 | 129 | 130 | 131 | def load_checkpoint(checkpoint_path, policy_net, Sigma, optimizer = None, learning_rate_policy_net = None, learning_rate_Sigma = None, Valid_initial_states = None, Epoch_list = None, Total_Reward_list = None): 132 | 133 | # Optimizer is None if trained model is to be loaded 134 | if optimizer == None: 135 | checkpoint = torch.load(checkpoint_path) 136 | policy_net.load_state_dict(checkpoint['policy_net_state_dict']) 137 | Sigma = checkpoint['Sigma'] 138 | return (policy_net, Sigma) 139 | 140 | checkpoint = torch.load(checkpoint_path) 141 | epoch = checkpoint['epoch'] 142 | policy_net.load_state_dict(checkpoint['policy_net_state_dict']) 143 | Sigma = checkpoint['Sigma'] 144 | optimizer = optim.Adam([ 145 | {'params': policy_net.parameters(), 'lr': learning_rate_policy_net}, 146 | {'params': Sigma, 'lr': learning_rate_Sigma} 147 | ]) 148 | optimizer.load_state_dict(checkpoint['optimizer_state_dict']) 149 | torch.set_rng_state(checkpoint['seed_state']) 150 | Valid_initial_states = checkpoint['Valid_initial_states'] 151 | random.setstate(checkpoint['random_module_state']) 152 | Epoch_list = checkpoint['Epoch_list'] 153 | Total_Reward_list = checkpoint['Total_Reward_list'] 154 | 155 | return (epoch, policy_net, Sigma, optimizer, Valid_initial_states, Epoch_list, Total_Reward_list) 156 | 157 | def save_checkpoint(checkpoint_path, epoch, policy_net, Sigma, optimizer, Valid_initial_states, Epoch_list, Total_Reward_list): 158 | checkpoint = { 159 | 'epoch': epoch + 1, 160 | 'policy_net_state_dict': policy_net.state_dict(), 161 | 'Sigma': Sigma, 162 | 'optimizer_state_dict': optimizer.state_dict(), 163 | 'seed_state': torch.get_rng_state(), 164 | 'Valid_initial_states': Valid_initial_states, 165 | 'random_module_state': random.getstate(), 166 | 'Epoch_list': Epoch_list, 167 | 'Total_Reward_list': Total_Reward_list 168 | } 169 | torch.save(checkpoint, checkpoint_path) -------------------------------------------------------------------------------- /src/Policy_Gradient/Policy_Gradient.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import torch 4 | from torch import nn 5 | from torch import optim 6 | 7 | import matplotlib.pyplot as plt 8 | 9 | import random 10 | 11 | from .Helper import * 12 | from .Trajectory import Trajectory, Generate_trajectories, add_valid_initial_states 13 | 14 | import datetime 15 | import time 16 | 17 | # Print start time 18 | current_time = datetime.datetime.now() 19 | formatted_time = current_time.strftime("%H:%M:%S") # Format as HH:MM:SS 20 | print("Formatted time:", formatted_time) 21 | 22 | # Define where to store training progress and the final trained model 23 | checkpoint_path = os.path.dirname(__file__) + '/Progress_Checkpoint/Checkpoint.pth' 24 | trained_model_path = os.path.dirname(__file__) + '/Progress_Checkpoint/Trained_Model.pth' 25 | training_performance_plot_path = os.path.dirname(__file__) + '/Progress_Checkpoint/Total_Reward_vs_Epochs.png' 26 | 27 | # Set seed for reproducibity 28 | torch.manual_seed(42) 29 | random.seed(42) 30 | 31 | 32 | 33 | # Define an initial state to start training and then final airfoil shape optimization from 34 | s0 = torch.tensor([[1, 0], [0.75, 0.05], [0.5, 0.10], [0.25, 0.05], [0, 0], [0.25, -0.05], [0.5, -0.10], [0.75, -0.05], [1, 0]]) 35 | a_params = {'idx_tochange': [1, 2, 3, 5, 6, 7], 'a_scaling': (1 / 1000)} 36 | 37 | # Define constants and hyperparameters 38 | state_dim = 6 * 2 39 | action_dim = 6 * 2 40 | layer_size_list = [100, 100] 41 | 42 | learning_rate_policy_net = 0.01 43 | learning_rate_Sigma = 0.01 44 | 45 | T = 5 # Episode length 46 | N = 5 # Batch size - number of trajectories each of length T - Set equal to number of parallel workers 47 | epochs = 100 # Total policy improvements - total training updates 48 | 49 | # Set parallel compute to true if you want to generate trajectories in parallel 50 | parallelize = False 51 | 52 | # Set reward function 53 | use_delta_LbyD = True 54 | 55 | # Define whether to use causality and baseline 56 | use_causality = True 57 | use_baseline = True 58 | 59 | 60 | 61 | if __name__ == '__main__': 62 | 63 | start = time.perf_counter() 64 | 65 | # Initialize the policy network with flexible hidden layers 66 | policy_net = PolicyNetwork(state_dim, action_dim, layer_size_list) 67 | 68 | # Define the covariance matrix Sigma as a trainable parameter 69 | Sigma = nn.Parameter(torch.randn(action_dim, action_dim), requires_grad = True) 70 | 71 | # Define the optimizer 72 | optimizer = optim.Adam([ 73 | {'params': policy_net.parameters(), 'lr': learning_rate_policy_net}, 74 | {'params': Sigma, 'lr': learning_rate_Sigma} 75 | ]) 76 | 77 | 78 | # Prepare for Training 79 | # Initialize epoch to 0 80 | epoch = 0 81 | # Keep track of valid initial states to start trajectory generation from 82 | Valid_initial_states = [s0] 83 | # Make reward and epoch lists to plot the training process 84 | Total_Reward_list = [] 85 | Epoch_list = [] 86 | 87 | # Load progress if checkpoint is available 88 | if os.path.exists(checkpoint_path): 89 | epoch, policy_net, Sigma, optimizer, Valid_initial_states, Epoch_list, Total_Reward_list = load_checkpoint(checkpoint_path, policy_net, Sigma, optimizer, learning_rate_policy_net, learning_rate_Sigma, Valid_initial_states, Epoch_list, Total_Reward_list) 90 | 91 | # Set policy parameters and the MDP functions required to generate trajectories 92 | policy_params = {'policy_net': policy_net, 'Sigma': Sigma} 93 | MDP_functions = {'generate_action': generate_action, 'generate_next_state': generate_next_state, 'generate_reward': generate_reward} 94 | 95 | 96 | # Prepare plot for dynamic updating 97 | plt.ion() # Turn on interactive mode 98 | plt.xlabel('Epochs') 99 | plt.ylabel('Total Reward') 100 | plt.title('Total Reward vs Epochs') 101 | plt.grid(True) 102 | plt.show() 103 | 104 | finish = time.perf_counter() 105 | print(f'Initial startup and loading time: {finish - start}') 106 | 107 | # print(f'Total valid initial states: {len(Valid_initial_states)}') 108 | while epoch < epochs: 109 | start = time.perf_counter() 110 | # Select initial states to start trajectory generation from. One s0 for each trajectory 111 | s0_list = random.choices(Valid_initial_states, k = N) 112 | 113 | # Generate trajectories - policy rollout 114 | trajectory_list = Generate_trajectories(s0_list, a_params, T, N, policy_params, MDP_functions, parallelize) 115 | 116 | finish = time.perf_counter() 117 | print(f'Trajectory generation time: {finish - start}') 118 | non_converged = 0 119 | for trajectory in trajectory_list: 120 | non_converged += (trajectory.rewards == -50).sum() 121 | print(f'Non converged trajectory count: {non_converged}') 122 | 123 | start = time.perf_counter() 124 | 125 | # Update list of valid initial states 126 | add_valid_initial_states(trajectory_list, Valid_initial_states) 127 | 128 | # Define if to set reward to delta L/D instead of L/D 129 | for trajectory in trajectory_list: 130 | trajectory.use_delta_r(use = use_delta_LbyD) 131 | 132 | finish = time.perf_counter() 133 | print(f'Adding valid initial states time: {finish - start}') 134 | 135 | # Get total reward for all the trajectories combined 136 | Total_Reward = calculate_total_reward(trajectory_list) 137 | 138 | start = time.perf_counter() 139 | 140 | # Compute the gradient loss function and define whether to use causality and baseline 141 | J = calculate_gradient_objective(trajectory_list, causality = use_causality, baseline = use_baseline) 142 | 143 | finish = time.perf_counter() 144 | print(f'J calculation time: {finish - start}') 145 | 146 | start = time.perf_counter() 147 | # Update the policy network and Sigma 148 | optimizer.zero_grad() 149 | J.backward() 150 | optimizer.step() 151 | 152 | finish = time.perf_counter() 153 | print(f'Policy Update time: {finish - start}') 154 | 155 | 156 | # Print progress and save models after every 5% progress 157 | if (epoch + 1) % (epochs // 10) == 0: 158 | # print(f"Episode {epoch + 1}/{epochs} | Policy Loss: {J.item()}") 159 | print(f"Episode {epoch + 1}/{epochs} | Total Reward: {Total_Reward.item()}") 160 | # print(f'Total valid initial states: {len(Valid_initial_states)}') 161 | Total_Reward_list.append(Total_Reward) 162 | Epoch_list.append(epoch + 1) 163 | save_checkpoint(checkpoint_path, epoch, policy_net, Sigma, optimizer, Valid_initial_states, Epoch_list, Total_Reward_list) 164 | plt.plot(Epoch_list, Total_Reward_list, '-o', color = 'b') 165 | plt.pause(0.1) 166 | 167 | # Update epoch 168 | epoch += 1 169 | print() 170 | 171 | # Upon finishing of training saved the trained model and delete the checkpoint file, also remove any pre-existing trained models 172 | if os.path.exists(trained_model_path): 173 | os.remove(trained_model_path) 174 | os.rename(checkpoint_path, trained_model_path) 175 | 176 | 177 | # Turn off plot interactive mode and save the plot 178 | plt.savefig(training_performance_plot_path) 179 | plt.ioff() 180 | 181 | # Print end time 182 | current_time = datetime.datetime.now() 183 | formatted_time = current_time.strftime("%H:%M:%S") # Format as HH:MM:SS 184 | print("Formatted time:", formatted_time) -------------------------------------------------------------------------------- /src/Policy_Gradient/Progress_Checkpoint/Checkpoint.pth: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atharvaaalok/Airfoil-Shape-Optimization-RL/55da5c55ce028480df2c440e74ca534717df898e/src/Policy_Gradient/Progress_Checkpoint/Checkpoint.pth -------------------------------------------------------------------------------- /src/Policy_Gradient/Progress_Checkpoint/Dummy.txt: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:85bc13b20a839cdedd2ae733825011c18f037b83438fc9700c0f162a8ca6a45b 3 | size 51 4 | -------------------------------------------------------------------------------- /src/Policy_Gradient/Progress_Checkpoint/Total_Reward_vs_Epochs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atharvaaalok/Airfoil-Shape-Optimization-RL/55da5c55ce028480df2c440e74ca534717df898e/src/Policy_Gradient/Progress_Checkpoint/Total_Reward_vs_Epochs.png -------------------------------------------------------------------------------- /src/Policy_Gradient/Progress_Checkpoint/Trained_Model.pth: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atharvaaalok/Airfoil-Shape-Optimization-RL/55da5c55ce028480df2c440e74ca534717df898e/src/Policy_Gradient/Progress_Checkpoint/Trained_Model.pth -------------------------------------------------------------------------------- /src/Policy_Gradient/Trajectory.py: -------------------------------------------------------------------------------- 1 | import torch 2 | 3 | from .Helper import * 4 | import concurrent.futures 5 | 6 | class Trajectory: 7 | def __init__(self, s0, a_params, T, policy_params, MDP_functions, parallelize = False, calculate_rewards = True): 8 | self.s0 = s0 9 | self.r0 = torch.tensor(0.0) 10 | self.T = T 11 | self.SARS = [] 12 | self.action_log_prob = torch.zeros(T) 13 | self.rewards = torch.zeros(T) 14 | 15 | # Generate the trajectory 16 | self.generate_trajectory(a_params, policy_params, MDP_functions, parallelize, calculate_rewards) 17 | 18 | def generate_trajectory(self, a_params, policy_params, MDP_functions, parallelize, calculate_rewards): 19 | # Get the MDP functions 20 | generate_action = MDP_functions['generate_action'] 21 | generate_next_state = MDP_functions['generate_next_state'] 22 | generate_reward = MDP_functions['generate_reward'] 23 | 24 | log_prob_list = [] 25 | reward_list = [] 26 | # Get the first state 27 | s = self.s0 28 | # Generate reward for the first state 29 | self.r0 = generate_reward(s, s, s) 30 | for t in range(self.T): 31 | # Generate action given the state 32 | a, action_log_prob = generate_action(s, a_params, policy_params) 33 | # Generate new state using this action 34 | s_new = generate_next_state(s, a) 35 | # Generate the reward 36 | if parallelize: 37 | r = 0 38 | else: 39 | if calculate_rewards: 40 | r = torch.tensor(generate_reward(s, a, s_new)) 41 | else: 42 | r = torch.tensor(0.0) 43 | 44 | # Create state-action-reward tuple 45 | self.SARS.append(SARS_tuple(s, a, r, s_new)) 46 | log_prob_list.append(action_log_prob) 47 | reward_list.append(r) 48 | 49 | # Update the state 50 | s = s_new 51 | 52 | self.action_log_prob = torch.stack(log_prob_list) 53 | self.rewards = torch.tensor(reward_list) 54 | 55 | def set_rewards(self, reward_list): 56 | for SARS, r in zip(self.SARS, reward_list): 57 | SARS.r = r 58 | self.rewards = torch.tensor(reward_list) 59 | 60 | def get_SAS_list(self): 61 | return [(SARS.s.detach(), SARS.a.detach(), SARS.s_new.detach()) for SARS in self.SARS] 62 | 63 | def use_delta_r(self, use = False): 64 | if use: 65 | self.rewards[1:] = torch.diff(self.rewards) 66 | self.rewards[0] = self.rewards[0] - self.r0 67 | 68 | 69 | 70 | class SARS_tuple: 71 | def __init__(self, s, a, r, s_new): 72 | self.s = s 73 | self.a = a 74 | self.r = r 75 | self.s_new = s_new 76 | 77 | 78 | def Generate_trajectories(s0_list, a_params, T, N, policy_params, MDP_functions, parallelize): 79 | trajectory_list = [] 80 | # Generate training batch 81 | for i_traj in range(N): 82 | rewards = [] 83 | s0 = s0_list[i_traj] 84 | trajectory = Trajectory(s0, a_params, T, policy_params, MDP_functions, parallelize = parallelize) 85 | trajectory_list.append(trajectory) 86 | 87 | # If running in parallel, calculate rewards for generated trajectory s, a, s_new pairs afterwards in parallel 88 | if parallelize: 89 | # Get s, a, s_new pairs to calculate corresponding rewards in parallel 90 | SAS_list = [trajectory.get_SAS_list() for trajectory in trajectory_list] 91 | 92 | # For all trajectories calculate rewards 93 | with concurrent.futures.ProcessPoolExecutor(max_workers = 60) as executor: 94 | reward_lists = executor.map(get_trajectory_rewards, SAS_list) 95 | 96 | # Update the trajectory rewards 97 | for trajectory, rewards in zip(trajectory_list, reward_lists): 98 | trajectory.set_rewards(rewards) 99 | 100 | return trajectory_list 101 | 102 | 103 | def add_valid_initial_states(trajectory_list, Valid_initial_states): 104 | for trajectory in trajectory_list: 105 | for SARS in trajectory.SARS: 106 | if SARS.r > NEGATIVE_REWARD + 1: 107 | # Add the new state in the transition to the list of valid initial states 108 | Valid_initial_states.append(SARS.s_new) -------------------------------------------------------------------------------- /src/Shape_Parametrization/Curves.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | class Quadratic_Bezier: 5 | 6 | Characteristic_mat = np.array([[1, 0, 0], [-2, 2, 0], [1, -2, 1]]) 7 | 8 | def __init__(self, P0, P1, P2): 9 | self.P0 = np.array(P0) 10 | self.P1 = np.array(P1) 11 | self.P2 = np.array(P2) 12 | self.ControlPoints_mat = np.vstack((self.P0, self.P1, self.P2)) 13 | 14 | def generate_points(self, total_points): 15 | t = np.linspace(0, 1, total_points) 16 | T_mat = np.vstack((t ** 0, t, t ** 2)).T 17 | generated_points = T_mat @ Quadratic_Bezier.Characteristic_mat @ self.ControlPoints_mat 18 | return generated_points 19 | 20 | def generate_points_at_tvals(self, t): 21 | T_mat = np.vstack((t ** 0, t, t ** 2)).T 22 | generated_points = T_mat @ Quadratic_Bezier.Characteristic_mat @ self.ControlPoints_mat 23 | return generated_points 24 | 25 | 26 | class Cubic_Bezier: 27 | 28 | Characteristic_mat = np.array([[1, 0, 0, 0], [-3, 3, 0, 0], [3, -6, 3, 0], [-1, 3, -3, 1]]) 29 | 30 | def __init__(self, P0, P1, P2, P3): 31 | self.P0 = np.array(P0) 32 | self.P1 = np.array(P1) 33 | self.P2 = np.array(P2) 34 | self.P3 = np.array(P3) 35 | self.ControlPoints_mat = np.vstack((self.P0, self.P1, self.P2, self.P3)) 36 | 37 | def generate_points(self, total_points): 38 | t = np.linspace(0, 1, total_points) 39 | T_mat = np.vstack((t ** 0, t, t ** 2, t ** 3)).T 40 | generated_points = T_mat @ Cubic_Bezier.Characteristic_mat @ self.ControlPoints_mat 41 | return generated_points 42 | 43 | def generate_points_at_tvals(self, t): 44 | T_mat = np.vstack((t ** 0, t, t ** 2, t ** 3)).T 45 | generated_points = T_mat @ Cubic_Bezier.Characteristic_mat @ self.ControlPoints_mat 46 | return generated_points 47 | 48 | 49 | class CatmullRom_curve: 50 | 51 | Characteristic_mat = (1 / 2) * np.array([[0, 2, 0, 0], [-1, 0, 1, 0], [2, -5, 4, -1], [-1, 3, -3, 1]]) 52 | 53 | def __init__(self, P0, P1, P2, P3): 54 | self.P0 = np.array(P0) 55 | self.P1 = np.array(P1) 56 | self.P2 = np.array(P2) 57 | self.P3 = np.array(P3) 58 | self.ControlPoints_mat = np.vstack((self.P0, self.P1, self.P2, self.P3)) 59 | 60 | def generate_points(self, total_points): 61 | t = np.linspace(0, 1, total_points) 62 | T_mat = np.vstack((t ** 0, t, t ** 2, t ** 3)).T 63 | generated_points = T_mat @ CatmullRom_curve.Characteristic_mat @ self.ControlPoints_mat 64 | return generated_points 65 | 66 | def generate_points_at_tvals(self, t): 67 | T_mat = np.vstack((t ** 0, t, t ** 2, t ** 3)).T 68 | generated_points = T_mat @ CatmullRom_curve.Characteristic_mat @ self.ControlPoints_mat 69 | return generated_points 70 | 71 | 72 | class B_Spline_curve: 73 | 74 | Characteristic_mat = (1 / 6) * np.array([[1, 4, 1, 0], [-3, 0, 3, 0], [3, -6, 3, 0], [-1, 3, -3, 1]]) 75 | 76 | def __init__(self, P0, P1, P2, P3): 77 | self.P0 = np.array(P0) 78 | self.P1 = np.array(P1) 79 | self.P2 = np.array(P2) 80 | self.P3 = np.array(P3) 81 | self.ControlPoints_mat = np.vstack((self.P0, self.P1, self.P2, self.P3)) 82 | 83 | def generate_points(self, total_points): 84 | t = np.linspace(0, 1, total_points) 85 | T_mat = np.vstack((t ** 0, t, t ** 2, t ** 3)).T 86 | generated_points = T_mat @ B_Spline_curve.Characteristic_mat @ self.ControlPoints_mat 87 | return generated_points 88 | 89 | def generate_points_at_tvals(self, t): 90 | T_mat = np.vstack((t ** 0, t, t ** 2, t ** 3)).T 91 | generated_points = T_mat @ B_Spline_curve.Characteristic_mat @ self.ControlPoints_mat 92 | return generated_points -------------------------------------------------------------------------------- /src/Shape_Parametrization/Curves_Explanation.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import numpy as np\n", 10 | "import matplotlib.pyplot as plt\n", 11 | "import Curves" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 2, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "p0 = (0, 0)\n", 21 | "p1 = (0, 2)\n", 22 | "p2 = (2, 0)\n", 23 | "p3 = (2, 2)" 24 | ] 25 | }, 26 | { 27 | "cell_type": "code", 28 | "execution_count": 3, 29 | "metadata": {}, 30 | "outputs": [ 31 | { 32 | "data": { 33 | "image/png": "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", 34 | "text/plain": [ 35 | "
" 36 | ] 37 | }, 38 | "metadata": {}, 39 | "output_type": "display_data" 40 | } 41 | ], 42 | "source": [ 43 | "# Quadratic Bezier curve\n", 44 | "quad_bezier = Curves.Quadratic_Bezier(p0, p1, p2)\n", 45 | "\n", 46 | "# Generate points\n", 47 | "n = 20\n", 48 | "points = quad_bezier.generate_points(n)\n", 49 | "x = points[:, 0]\n", 50 | "y = points[:, 1]\n", 51 | "\n", 52 | "# Plot the points\n", 53 | "plt.plot(x, y, marker = 'o')\n", 54 | "plt.title('Quadratic Bezier')\n", 55 | "plt.show()" 56 | ] 57 | }, 58 | { 59 | "cell_type": "code", 60 | "execution_count": 4, 61 | "metadata": {}, 62 | "outputs": [ 63 | { 64 | "data": { 65 | "image/png": "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amqqaZvRaERqaiqio6MtOobBYMAPP/wAf//6L++QkBBoNBqzY+r1ehw5csTiY0qJvsoAAIgd7IfXZg3DlofvxqGn72VYISIiagerV7pNSEjAvHnzMHLkSERGRmLNmjWoqKjAggULAABz585F7969kZycDAD429/+hrvvvhv9+/dHaWkpXnrpJZw7dw6LFi0CUD+DaNmyZXj++ecRGhqKkJAQrFixAgEBAYiPj++4ntqBEALpP10GAMy8KxD3DvJr5RVERERkCasDy8yZM3H58mWsXLkSWq0Ww4YNQ0pKimnQ7Pnz5yGX3zhxc/XqVTz88MPQarXo0aMHRowYgW+++QZhYTfGezz11FOoqKjAI488gtLSUowZMwYpKSmNFpiTurMllbhYeg2uChmiQno6uhwiIiKnIRNCdPqlV/V6PdRqNXQ6nUMH4G7KOIuVu04ium9PbHnkbofVQURE1BlY8/3Newl1oPSfigEAYwf4OLgSIiIi58LA0kFqDUZk5NYHlntCezm4GiIiIufCwNJBjp8vRUWNAd4ebgjzd/y6MERERM6EgaWDNMwOGtPfB3IuDkdERNShGFg6yH+z6wPL2FCOXyEiIupoDCwd4GpFDb6/qAMAjOX4FSIiog7HwNIBvs4thhDAQD9PaNSda+0YIiKizoCBpQP8t2E6My8HERER2QQDSzsJIW6MXxnAy0FERES2wMDSTrmXK3BJVwU3Fzkig70dXQ4REZFTYmBpp4azK5HB3nB3Uzi4GiIiIufEwNJODeuvcPwKERGR7TCwtEN1nQGHf74CALiH41eIiIhshoGlHY6eu4prtQb43KbEII2no8shIiJyWgws7fDf7IabHfpAJuNy/ERERLbCwNION6Yzc/wKERGRLTGwtFFJeTWyLuoBAKP7M7AQERHZEgNLGx3Kqb8cdIe/F3w9uRw/ERGRLTGwtFH6TzfGrxAREZFtMbC0wc3L8XM6MxERke0xsLTBT4XlKCqrhspVjhFBPRxdDhERkdNjYGmDhrMrUSE9oXLlcvxERES2xsDSBunX11/hcvxERET2wcBipapaA478XAKA41eIiIjshYHFCgajwKaMc6iuM6JHN1f09fFwdElERERdAgOLhVKyCjBm9X78397TAICrlbUY++IBpGQVOLgyIiIi58fAYoGUrAIs3nwMBboqs+1aXRUWbz7G0EJE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", 66 | "text/plain": [ 67 | "
" 68 | ] 69 | }, 70 | "metadata": {}, 71 | "output_type": "display_data" 72 | } 73 | ], 74 | "source": [ 75 | "# Cubic Bezier curve\n", 76 | "cubic_bezier = Curves.Cubic_Bezier(p0, p1, p2, p3)\n", 77 | "\n", 78 | "# Generate points\n", 79 | "n = 20\n", 80 | "points = cubic_bezier.generate_points(n)\n", 81 | "x = points[:, 0]\n", 82 | "y = points[:, 1]\n", 83 | "\n", 84 | "# Plot the points\n", 85 | "plt.plot(x, y, marker = 'o')\n", 86 | "plt.title('Cubic Bezier')\n", 87 | "plt.show()" 88 | ] 89 | }, 90 | { 91 | "cell_type": "code", 92 | "execution_count": 5, 93 | "metadata": {}, 94 | "outputs": [ 95 | { 96 | "data": { 97 | "image/png": "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", 98 | "text/plain": [ 99 | "
" 100 | ] 101 | }, 102 | "metadata": {}, 103 | "output_type": "display_data" 104 | } 105 | ], 106 | "source": [ 107 | "# Catmull Rom curve\n", 108 | "catmull_rom = Curves.CatmullRom_curve(p0, p1, p2, p3)\n", 109 | "\n", 110 | "# Generate points\n", 111 | "n = 20\n", 112 | "points = catmull_rom.generate_points(n)\n", 113 | "x = points[:, 0]\n", 114 | "y = points[:, 1]\n", 115 | "\n", 116 | "# Plot the points\n", 117 | "plt.plot(x, y, marker = 'o')\n", 118 | "plt.title('Catmull Rom curve')\n", 119 | "plt.show()" 120 | ] 121 | }, 122 | { 123 | "cell_type": "code", 124 | "execution_count": 6, 125 | "metadata": {}, 126 | "outputs": [ 127 | { 128 | "data": { 129 | "image/png": "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", 130 | "text/plain": [ 131 | "
" 132 | ] 133 | }, 134 | "metadata": {}, 135 | "output_type": "display_data" 136 | } 137 | ], 138 | "source": [ 139 | "# B Spline curve\n", 140 | "b_spline = Curves.B_Spline_curve(p0, p1, p2, p3)\n", 141 | "\n", 142 | "# Generate points\n", 143 | "n = 20\n", 144 | "points = b_spline.generate_points(n)\n", 145 | "x = points[:, 0]\n", 146 | "y = points[:, 1]\n", 147 | "\n", 148 | "# Plot the points\n", 149 | "plt.plot(x, y, marker = 'o')\n", 150 | "plt.title('B Spline curve')\n", 151 | "plt.show()" 152 | ] 153 | } 154 | ], 155 | "metadata": { 156 | "kernelspec": { 157 | "display_name": "Python 3", 158 | "language": "python", 159 | "name": "python3" 160 | }, 161 | "language_info": { 162 | "codemirror_mode": { 163 | "name": "ipython", 164 | "version": 3 165 | }, 166 | "file_extension": ".py", 167 | "mimetype": "text/x-python", 168 | "name": "python", 169 | "nbconvert_exporter": "python", 170 | "pygments_lexer": "ipython3", 171 | "version": "3.11.5" 172 | } 173 | }, 174 | "nbformat": 4, 175 | "nbformat_minor": 2 176 | } 177 | -------------------------------------------------------------------------------- /src/Shape_Parametrization/Splines.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | if __package__ is None or __package__ == '': 4 | # uses current directory visibility 5 | import Curves 6 | else: 7 | # uses current package visibility 8 | from . import Curves 9 | 10 | 11 | class Bezier_spline: 12 | 13 | def __init__(self, ControlPoints_list): 14 | self.Bezier_curves = [] 15 | self.total_curves = len(ControlPoints_list) 16 | 17 | for ControlPoints in ControlPoints_list: 18 | if len(ControlPoints) == 3: 19 | curve_i = Curves.Quadratic_Bezier(*ControlPoints) 20 | self.Bezier_curves.append(curve_i) 21 | elif len(ControlPoints) == 4: 22 | curve_i = Curves.Cubic_Bezier(*ControlPoints) 23 | self.Bezier_curves.append(curve_i) 24 | 25 | def generate_points(self, total_points): 26 | t = np.linspace(0, self.total_curves, total_points) 27 | 28 | generated_points = [] 29 | 30 | for i in range(self.total_curves): 31 | t_vals_curve_i = t[(t - i >= 0) & (t - i <= 1)] - i 32 | points_curve_i = self.Bezier_curves[i].generate_points_at_tvals(t_vals_curve_i) 33 | generated_points.append(points_curve_i) 34 | 35 | generated_points = np.vstack(generated_points) 36 | return generated_points 37 | 38 | 39 | class CatmullRom_spline: 40 | 41 | def __init__(self, ControlPoints_list): 42 | self.CatmullRom_curves = [] 43 | self.total_curves = len(ControlPoints_list) - 1 44 | 45 | cp_array = np.array(ControlPoints_list) 46 | GhostPoint_0 = cp_array[0] + (cp_array[0] - cp_array[1]) 47 | GhostPoint_1 = cp_array[-1] + (cp_array[-1] - cp_array[-2]) 48 | ControlPoints_list.insert(0, tuple(GhostPoint_0.tolist())) 49 | ControlPoints_list.append(tuple(GhostPoint_1.tolist())) 50 | 51 | for i in range(self.total_curves): 52 | curve_i = Curves.CatmullRom_curve(ControlPoints_list[i], ControlPoints_list[i + 1], ControlPoints_list[i + 2], ControlPoints_list[i + 3]) 53 | self.CatmullRom_curves.append(curve_i) 54 | 55 | def generate_points(self, total_points): 56 | t = np.linspace(0, self.total_curves, total_points) 57 | 58 | generated_points = [] 59 | 60 | for i in range(self.total_curves): 61 | t_vals_curve_i = t[(t - i >= 0) & (t - i <= 1)] - i 62 | points_curve_i = self.CatmullRom_curves[i].generate_points_at_tvals(t_vals_curve_i) 63 | generated_points.append(points_curve_i) 64 | 65 | generated_points = np.vstack(generated_points) 66 | return generated_points 67 | 68 | 69 | class B_spline: 70 | 71 | def __init__(self, ControlPoints_list): 72 | self.Bspline_curves = [] 73 | self.total_curves = len(ControlPoints_list) - 3 74 | 75 | for i in range(self.total_curves): 76 | curve_i = Curves.CatmullRom_curve(ControlPoints_list[i], ControlPoints_list[i + 1], ControlPoints_list[i + 2], ControlPoints_list[i + 3]) 77 | self.Bspline_curves.append(curve_i) 78 | 79 | def generate_points(self, total_points): 80 | t = np.linspace(0, self.total_curves, total_points) 81 | 82 | generated_points = [] 83 | 84 | for i in range(self.total_curves): 85 | t_vals_curve_i = t[(t - i >= 0) & (t - i <= 1)] - i 86 | points_curve_i = self.Bspline_curves[i].generate_points_at_tvals(t_vals_curve_i) 87 | generated_points.append(points_curve_i) 88 | 89 | generated_points = np.vstack(generated_points) 90 | return generated_points -------------------------------------------------------------------------------- /src/Shape_Parametrization/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atharvaaalok/Airfoil-Shape-Optimization-RL/55da5c55ce028480df2c440e74ca534717df898e/src/Shape_Parametrization/__init__.py -------------------------------------------------------------------------------- /src/StableBaselines/Average_Reward_NoTraining.py: -------------------------------------------------------------------------------- 1 | from stable_baselines3.common.env_checker import check_env 2 | from .CFD_Gym_Env import CFD_Env 3 | 4 | import numpy as np 5 | import os 6 | 7 | s0 = np.array([[1, 0], [0.75, 0.05], [0.625, 0.075], [0.5, 0.1], [0.25, 0.05], [0, 0], [0.25, -0.05], [0.5, -0.1], [0.625, -0.075], [0.75, -0.05], [1, 0]]) 8 | s0 = s0.astype(np.float32) 9 | idx_to_change = [1, 2, 3, 4, 6, 7, 8, 9] 10 | a_scaling = (1 / 1000) 11 | valid_states_file_path = os.path.dirname(__file__) + '/Dataset/Arrays_as_rows.txt' 12 | 13 | 14 | # Get the environment 15 | MAX_ITERATIONS = 50 16 | env = CFD_Env(s0, idx_to_change, MAX_ITERATIONS, a_scaling, valid_states_file_path) 17 | check_env(env) 18 | 19 | 20 | total_reward_list = [] 21 | 22 | # Also run the following 23 | 24 | episodes = 500 25 | for episode in range(episodes): 26 | terminated = False 27 | observation, info = env.reset() 28 | reward_list = [] 29 | while not terminated: 30 | random_action = env.action_space.sample() 31 | observation, reward, terminated, truncated, info = env.step(random_action) 32 | reward_list.append(reward) 33 | 34 | total_reward_list.append(sum(reward_list)) 35 | 36 | print(f'Average reward: {sum(total_reward_list) / len(total_reward_list)}') -------------------------------------------------------------------------------- /src/StableBaselines/CFD_Gym_Env.py: -------------------------------------------------------------------------------- 1 | import gymnasium as gym 2 | from gymnasium import spaces 3 | 4 | import numpy as np 5 | 6 | from ..CFD.Aerodynamics import Aerodynamics 7 | 8 | 9 | NEGATIVE_REWARD = -100.0 10 | 11 | 12 | class CFD_Env(gym.Env): 13 | """Custom Environment that follows gym interface.""" 14 | 15 | metadata = {"render_modes": ["human", "no_display"], "render_fps": 4} 16 | 17 | def __init__(self, s0, idx_to_change, max_iterations, a_scaling, valid_states_file_path, use_delta_r): 18 | super(CFD_Env, self).__init__() 19 | self.action_space = spaces.Box(low = -1, high = 1, shape = (len(idx_to_change) * 2,), dtype = np.float32) 20 | self.observation_space = spaces.Box(low = -1, high = 2, shape = s0.flatten().shape, dtype = np.float32) 21 | 22 | self.s = s0 23 | self.idx_to_change = idx_to_change 24 | self.a_scaling = a_scaling 25 | self.valid_states_file_path = valid_states_file_path 26 | self.iter = 0 27 | self.max_iterations = max_iterations 28 | 29 | self.use_delta_r = use_delta_r 30 | self.prev_reward = 0 31 | self.new_reward = 0 32 | 33 | 34 | def step(self, action): 35 | action = action.reshape(-1, 2) 36 | s_new = self.s 37 | s_new[self.idx_to_change, :] = s_new[self.idx_to_change, :] + action * self.a_scaling 38 | self.s = s_new 39 | 40 | terminated = False 41 | truncated = False 42 | 43 | # Generate reward 44 | self.new_reward = self._generate_reward(s_new) 45 | 46 | if self.use_delta_r: 47 | reward = self.new_reward - self.prev_reward 48 | self.prev_reward = self.new_reward 49 | else: 50 | reward = self.new_reward 51 | self.prev_reward = self.new_reward 52 | 53 | 54 | observation = s_new.flatten() 55 | info = {} 56 | 57 | self.iter += 1 58 | if self.iter == self.max_iterations: 59 | terminated = True 60 | else: 61 | terminated = False 62 | 63 | return observation, reward, terminated, truncated, info 64 | 65 | def reset(self, seed = None, options = None): 66 | np.random.seed(seed) 67 | # Choose a random valid initial state from file 68 | file_path = self.valid_states_file_path 69 | s_new = read_random_line(file_path).reshape(-1, 2).astype(np.float32) 70 | 71 | self.s = s_new 72 | 73 | # Set state's reward 74 | self.prev_reward = self._generate_reward(s_new) 75 | self.new_reward = 0 76 | 77 | observation = s_new.flatten() 78 | # observation = s_new[self.idx_to_change, :].flatten() 79 | info = {} 80 | 81 | self.iter = 0 82 | 83 | return observation, info 84 | 85 | def render(self): 86 | pass 87 | 88 | def close(self): 89 | pass 90 | 91 | def _generate_reward(self, s): 92 | # Generate reward 93 | airfoil_name = 'air' + str(np.random.rand(1))[3:-1] 94 | airfoil_coordinates = s 95 | airfoil = Aerodynamics.Airfoil(airfoil_coordinates, airfoil_name) 96 | Reynolds_num = 1e6 97 | L_by_D_ratio = airfoil.get_L_by_D(Reynolds_num) 98 | 99 | if L_by_D_ratio == None: 100 | L_by_D_ratio = NEGATIVE_REWARD 101 | 102 | return L_by_D_ratio 103 | 104 | 105 | 106 | 107 | # Function to read a random line from the file and convert it into a NumPy vector 108 | def read_random_line(file_path): 109 | with open(file_path, 'r') as file: 110 | # Count the total number of lines in the file 111 | num_lines = sum(1 for line in file) 112 | 113 | # Generate a random line number within the range of total lines 114 | random_line_number = np.random.randint(0, num_lines - 1) 115 | 116 | # Read the selected random line from the file 117 | with open(file_path, 'r') as file: 118 | for line_num, line in enumerate(file): 119 | if line_num == random_line_number: 120 | # Convert the line into a NumPy vector 121 | numpy_vector = np.fromstring(line, dtype = float, sep=' ') 122 | return numpy_vector # Return the NumPy vector -------------------------------------------------------------------------------- /src/StableBaselines/Check_Env.py: -------------------------------------------------------------------------------- 1 | from stable_baselines3.common.env_checker import check_env 2 | from .CFD_Gym_Env import CFD_Env 3 | 4 | import numpy as np 5 | import os 6 | 7 | s0 = np.array([[1, 0], [0.75, 0.05], [0.625, 0.075], [0.5, 0.1], [0.25, 0.05], [0, 0], [0.25, -0.05], [0.5, -0.1], [0.625, -0.075], [0.75, -0.05], [1, 0]]) 8 | s0 = s0.astype(np.float32) 9 | idx_to_change = [1, 2, 3, 4, 6, 7, 8, 9] 10 | a_scaling = (1 / 1000) 11 | valid_states_file_path = os.path.dirname(__file__) + '/Dataset/Arrays_as_rows.txt' 12 | 13 | 14 | # Get the environment 15 | MAX_ITERATIONS = 50 16 | env = CFD_Env(s0, idx_to_change, MAX_ITERATIONS, a_scaling, valid_states_file_path) 17 | check_env(env) 18 | 19 | 20 | # Also run the following 21 | 22 | episodes = 10 23 | for episode in range(episodes): 24 | terminated = False 25 | observation, info = env.reset() 26 | while not terminated: 27 | random_action = env.action_space.sample() 28 | print('action:', random_action) 29 | observation, reward, terminated, truncated, info = env.step(random_action) 30 | print('reward:', reward) 31 | print() 32 | -------------------------------------------------------------------------------- /src/StableBaselines/Dataset/Archive/Arrays_as_rows_1.txt: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:88528dcc5c5bf1f4290246ba656dd151ff630979ea12891a0dce7d89fd2bb3b3 3 | size 111691346 4 | -------------------------------------------------------------------------------- /src/StableBaselines/Dataset/Archive/Rewards_as_rows_1.txt: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:e75c416ddfee24e90696589adafe6aa4455e287779ddb35a5e4d465ded01ff15 3 | size 5241840 4 | -------------------------------------------------------------------------------- /src/StableBaselines/Dataset/Arrays_as_rows - Copy.txt: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:26a97307ba0906d039b4b2a989639a2dc08575a3d07dded580707eebe9a82493 3 | size 27195 4 | -------------------------------------------------------------------------------- /src/StableBaselines/Dataset/Arrays_as_rows.txt: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:88528dcc5c5bf1f4290246ba656dd151ff630979ea12891a0dce7d89fd2bb3b3 3 | size 111691346 4 | -------------------------------------------------------------------------------- /src/StableBaselines/Dataset/Arrays_as_rows_a_scaling_100.txt: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:ecf73a37e026f77d5ab58a0dbfccb860d472fafa55a6f12ce7ddf7aabbda5279 3 | size 324095 4 | -------------------------------------------------------------------------------- /src/StableBaselines/Dataset/Rewards_as_rows - Copy.txt: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:3c296da0f544345bcb7b9749d5b093fa4bf2ec212af16769613eaeef54af14ba 3 | size 1274 4 | -------------------------------------------------------------------------------- /src/StableBaselines/Dataset/Rewards_as_rows.txt: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:e75c416ddfee24e90696589adafe6aa4455e287779ddb35a5e4d465ded01ff15 3 | size 5241840 4 | -------------------------------------------------------------------------------- /src/StableBaselines/Dataset/Rewards_as_rows_a_scaling_100.txt: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:775cc5307152f99c6675a19e7e52325d71b55fba72fbf91817b89778547db83d 3 | size 15213 4 | -------------------------------------------------------------------------------- /src/StableBaselines/Logs/Dummy.txt: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:85bc13b20a839cdedd2ae733825011c18f037b83438fc9700c0f162a8ca6a45b 3 | size 51 4 | -------------------------------------------------------------------------------- /src/StableBaselines/Models/Dummy.txt: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:85bc13b20a839cdedd2ae733825011c18f037b83438fc9700c0f162a8ca6a45b 3 | size 51 4 | -------------------------------------------------------------------------------- /src/StableBaselines/Train_PPO.py: -------------------------------------------------------------------------------- 1 | import gymnasium as gym 2 | from stable_baselines3 import PPO 3 | from stable_baselines3.common.env_util import make_vec_env 4 | from stable_baselines3.common.vec_env import DummyVecEnv, SubprocVecEnv 5 | 6 | from .CFD_Gym_Env import CFD_Env 7 | 8 | import numpy as np 9 | import os 10 | 11 | 12 | # Function that returns gym environments to use with vectorized environments 13 | def make_env(s0, idx_to_change, max_iterations, a_scaling, valid_states_file_path, use_delta_r) -> gym.Env: 14 | 15 | # Get the environment 16 | env = CFD_Env(s0, idx_to_change, max_iterations, a_scaling, valid_states_file_path, use_delta_r) 17 | # Reset the environment 18 | observation, info = env.reset() 19 | 20 | return env 21 | 22 | 23 | 24 | if __name__ == '__main__': 25 | 26 | # Parameters to mess with 27 | algorithm_name = 'PPO' 28 | 29 | s0 = np.array([[1, 0], [0.75, 0.05], [0.625, 0.075], [0.5, 0.1], [0.25, 0.05], [0, 0], [0.25, -0.05], [0.5, -0.1], [0.625, -0.075], [0.75, -0.05], [1, 0]]) 30 | idx_to_change = [1, 2, 3, 4, 6, 7, 8, 9] 31 | a_scaling = (1 / 1000) 32 | valid_states_file_path = os.path.dirname(__file__) + '/Dataset/Arrays_as_rows.txt' 33 | MAX_ITERATIONS = 50 34 | 35 | parallelize = True 36 | num_cpu = 5 37 | 38 | use_custom_policy = True 39 | network_arch = [128, 64, 64] 40 | policy_kwargs = dict(net_arch = dict(pi = network_arch, vf = network_arch)) 41 | 42 | use_delta_r = False 43 | 44 | CHECKPOINT_TIMESTEPS = 5000 45 | EPOCHS = 50 46 | 47 | 48 | if use_delta_r: 49 | algorithm_name = algorithm_name + '_DeltaR' 50 | 51 | if parallelize: 52 | algorithm_name = algorithm_name + '_Parallel' 53 | training_env = make_vec_env(lambda: make_env(s0, idx_to_change, MAX_ITERATIONS, a_scaling, valid_states_file_path, use_delta_r), n_envs = num_cpu, vec_env_cls = SubprocVecEnv) 54 | else: 55 | training_env = CFD_Env(s0, idx_to_change, MAX_ITERATIONS, a_scaling, valid_states_file_path, use_delta_r) 56 | # Reset the environment 57 | observation, info = training_env.reset() 58 | 59 | if use_custom_policy: 60 | algorithm_name = algorithm_name + '_PolicyArch' + '_'.join(map(str, network_arch)) 61 | else: 62 | policy_kwargs = None 63 | 64 | 65 | 66 | # Get folders to save trained models and logs into 67 | models_dir = os.path.dirname(__file__) + '/Models/' + algorithm_name 68 | log_dir = os.path.dirname(__file__) + '/Logs' 69 | 70 | 71 | # Get the model 72 | model = PPO('MlpPolicy', training_env, verbose = 1, tensorboard_log = log_dir, policy_kwargs = policy_kwargs) 73 | 74 | # Train the model 75 | for i in range(1, EPOCHS): 76 | model.learn(total_timesteps = CHECKPOINT_TIMESTEPS, reset_num_timesteps = False, tb_log_name = algorithm_name, progress_bar = True) 77 | model.save(f'{models_dir}/{CHECKPOINT_TIMESTEPS * i}') --------------------------------------------------------------------------------