├── .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": "iVBORw0KGgoAAAANSUhEUgAAAiMAAAGzCAYAAAD9pBdvAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy81sbWrAAAACXBIWXMAAA9hAAAPYQGoP6dpAABbdElEQVR4nO3deVxU5f4H8M+ZgZkBhAFkVxTcUETBDcIltVAsI211CbfK2zUrzeqmLZJ1S72V2WKa/lIrb2qaWl6NUtMsoygWFXcRFZVVZIZFtpnz+wMZHVlkEDizfN6v17yKM8858z0MM+fjc57zHEEURRFEREREEpFJXQARERHZNoYRIiIikhTDCBEREUmKYYSIiIgkxTBCREREkmIYISIiIkkxjBAREZGkGEaIiIhIUgwjREREJCmGESIyEhAQgKlTp7ba6w0bNgzDhg1rtddrCWvXroUgCDh79qzUpRBZJIYRIokcOXIEsbGxaNeuHZRKJfz8/BAbG4ujR49KXVqzO3r0KN54440WP1gPGzYMgiAYHgqFAoGBgfjHP/6BzMzMFn1tImo6gfemIWp9W7ZswYQJE+Du7o4nnngCgYGBOHv2LD7//HMUFBRg48aNGDNmjCS1BQQEYNiwYVi7dm2zbXPz5s145JFHsHfv3lq9IBUVFQAAhUJx268zbNgwpKenY+HChYZtHz16FCtWrEDbtm1x7NgxODo63vbr3Eyn06GyshJKpRKCIDT79omsnZ3UBRDZmvT0dEyaNAmdOnXC/v374enpaXhu1qxZGDJkCGJjY3Ho0CEEBgZKWGn9SkpK4OTk1Czbao4QciO1Wo3Y2FijZYGBgXjmmWdw4MABjBgxollfDwDkcjnkcnmzba85f79EloCnaYha2bvvvovS0lKsXLnSKIgAgIeHBz777DMUFxfj3XffNSyfOnUqAgICam3rjTfeqPUv8TVr1uCuu+6Cl5cXlEolgoODsXz58lrriqKIf//732jfvj0cHR0xfPhwHDlypFa7mvEQv/zyC55++ml4eXmhffv2AIBz587h6aefRlBQEBwcHNC2bVs88sgjRqdj1q5di0ceeQQAMHz4cMMplH379gGoe8xIWVkZ3njjDXTr1g0qlQq+vr548MEHkZ6eXu/vtSE+Pj4AADs7439/Xbx4EY8//ji8vb2hVCrRs2dPrF692qhNQECA0amfGx81+1DfmJEffvgBQ4YMgZOTE5ydnTF69Ohav+OpU6eiTZs2SE9Px7333gtnZ2c89thjTdpPIkvFnhGiVrZ9+3YEBARgyJAhdT5/5513IiAgANu3b8enn35q8vaXL1+Onj174v7774ednR22b9+Op59+Gnq9HjNnzjS0mz9/Pv7973/j3nvvxb333ovk5GSMHDnScNrkZk8//TQ8PT0xf/58lJSUAAD++usv/P777xg/fjzat2+Ps2fPYvny5Rg2bBiOHj0KR0dH3HnnnXjuuefw0Ucf4ZVXXkGPHj0AwPDfm+l0Otx3333Ys2cPxo8fj1mzZqGoqAi7du1CWloaOnfu3OD+63Q65OfnAwAqKytx7NgxxMXFoUuXLhg0aJChXU5ODu644w4IgoBnnnkGnp6e+OGHH/DEE09Aq9Vi9uzZAIClS5eiuLjY6DU++OADpKamom3btvXW8dVXX2HKlCmIjo7G4sWLUVpaiuXLl2Pw4MFISUkxCpdVVVWIjo7G4MGD8d5777XIqSQisyYSUaspLCwUAYhjxoxpsN39998vAhC1Wq0oiqI4ZcoUsWPHjrXaxcXFiTd/jEtLS2u1i46OFjt16mT4OTc3V1QoFOLo0aNFvV5vWP7KK6+IAMQpU6YYlq1Zs0YEIA4ePFisqqq65WslJCSIAMQvv/zSsGzTpk0iAHHv3r212g8dOlQcOnSo4efVq1eLAMQlS5bUantjrXUZOnSoCKDWo0ePHuKZM2eM2j7xxBOir6+vmJ+fb7R8/PjxolqtrnPfRFEUv/nmGxGA+OabbxqW1fyOMjIyRFEUxaKiItHV1VWcPn260brZ2dmiWq02Wj5lyhQRgDh37twG943ImvE0DVErKioqAgA4Ozs32K7m+Zr2pnBwcDD8v0ajQX5+PoYOHYozZ85Ao9EAAHbv3o2Kigo8++yzRqd5anoD6jJ9+vRa4yJufK3KykpcvnwZXbp0gaurK5KTk02uHQC+/fZbeHh44Nlnn631XGMGhwYEBGDXrl3YtWsXfvjhByxduhQajQb33HMP8vLyAFSfovr2228RExMDURSRn59veERHR0Oj0dRZ/9GjR/H4449jzJgxeO211+qtYdeuXSgsLMSECROMti2XyxEREYG9e/fWWmfGjBm33Dcia8XTNEStqLEho6ioCIIgwMPDw+TXOHDgAOLi4pCQkIDS0lKj5zQaDdRqNc6dOwcA6Nq1q9Hznp6ecHNzq3O7dQ2mvXr1KhYuXIg1a9bg4sWLEG+4OK8m+JgqPT0dQUFBtcZ3NJaTkxOioqIMP48aNQqDBw9G//79sWjRIrz//vvIy8tDYWEhVq5ciZUrV9a5ndzcXKOftVotHnzwQbRr1w5ffvllg8Ho1KlTAIC77rqrzuddXFyMfrazszOMwyGyRQwjRK1IrVbDz88Phw4darDdoUOH0L59e8OVJvUd+HQ6ndHP6enpuPvuu9G9e3csWbIE/v7+UCgU2LlzJz744APo9fom135jL0iNZ599FmvWrMHs2bMRGRkJtVoNQRAwfvz423qt5tavXz+o1Wrs378fAAy1xcbGYsqUKXWu07t3b6Ofp06dikuXLiExMbFWmLhZzfa/+uorw+DZG90ctJRKJWQydlST7WIYIWplMTEx+Oyzz/Dbb79h8ODBtZ7/9ddfcfbsWcyZM8ewzM3NDYWFhbXa1vRw1Ni+fTvKy8vx/fffo0OHDoblN58W6NixI4Dqf8F36tTJsDwvLw9Xrlxp9L5s3rwZU6ZMwfvvv29YVlZWVqtWU+be6Ny5M/78809UVlbC3t6+0evdik6nMwxE9fT0hLOzM3Q6nVEvSn0WLVqEbdu2YcuWLejevfst29cMsvXy8mrU9olsHaM4USt78cUX4ejoiKeeegqXL182eq6goAD//Oc/4eLigmeeecawvHPnztBoNEY9KllZWdi6davR+jVjOm4+XbJmzRqjdlFRUbC3t8fHH39s1Hbp0qUm7YtcLjdaHwA+/vjjWj02NXNm1BWobvbQQw8hPz8fn3zySa3nbn6txtq7dy+Ki4sRGhpqqPuhhx7Ct99+i7S0tFrta8aWANXja1577TW8+uqrGDt2bKNeLzo6Gi4uLnjnnXdQWVnZ4PaJiD0jRK2uS5cu+PLLLzFhwgT06tWr1gysV65cwYYNG4zGaIwfPx4vv/wyHnjgATz33HOGy0S7detmNNBy5MiRUCgUiImJwVNPPYXi4mKsWrUKXl5eyMrKMrTz9PTEiy++iIULF+K+++7Dvffei5SUFPzwww8mjVO577778NVXX0GtViM4OBgJCQnYvXt3rUtew8LCIJfLsXjxYmg0GiiVSsNcKDebPHkyvvzyS8yZMweJiYkYMmQISkpKsHv3bjz99NO3nJlWo9Fg3bp1AKovmT1x4gSWL18OBwcHzJ0719Bu0aJF2Lt3LyIiIjB9+nQEBwejoKAAycnJ2L17NwoKCgAAEyZMgKenJ7p27WrYbo0RI0bA29u7Vg0uLi5Yvnw5Jk2ahL59+2L8+PHw9PTE+fPnsWPHDgwaNKjOsEVksyS8kofIph0+fFicOHGi6OPjI8pkMhGAqFKpxCNHjtTZ/qeffhJDQkJEhUIhBgUFievWravz0t7vv/9e7N27t6hSqcSAgABx8eLFhstlay49FUVR1Ol04oIFC0RfX1/RwcFBHDZsmJiWliZ27Nixzkt7//rrr1o1XblyRZw2bZro4eEhtmnTRoyOjhaPHz9eaxuiKIqrVq0SO3XqJMrlcqPLfG++tFcUqy8ZfvXVV8XAwEDR3t5e9PHxER9++GExPT29wd/pzZf2CoIguru7i/fff7+YlJRUq31OTo44c+ZM0d/f3/A6d999t7hy5UpDG9RxqXDNo2Yfbr60t8bevXvF6OhoUa1WiyqVSuzcubM4depU8e+//za0mTJliujk5NTgfhFZO96bhshMfPnll5g6dSpiY2Px5ZdfSl0OEVGr4WkaIjMxefJkZGVlYe7cuWjfvj3eeecdqUsiImoV7BkhIiIiSfFqGiIiIpIUwwgRERFJimGEiIiIJMUwQkRERJKyiKtp9Ho9Ll26BGdnZ5OmlSYiIiLpiKKIoqIi+Pn5NXj/JYsII5cuXYK/v7/UZRAREVETZGZmNnhnaosIIzW3Xc/MzLzl3TKJiIjIPGi1Wvj7+xuO4/WxiDBSc2rGxcWFYYSIiMjC3GqIBQewEhERkaQYRoiIiEhSDCNEREQkKYYRIiIikhTDCBEREUmKYYSIiIgkxTBCREREkmIYISIiIklZxKRnZBt0ehGJGQXILSqDl7MK4YHukMua715ELb19IiJqGpPDyP79+/Huu+8iKSkJWVlZ2Lp1K8aOHdvgOvv27cOcOXNw5MgR+Pv747XXXsPUqVObWDJZo/i0LCzYfhRZmjLDMl+1CnExwRgV4mv22ycioqYz+TRNSUkJQkNDsWzZska1z8jIwOjRozF8+HCkpqZi9uzZePLJJ/Hjjz+aXCxZp/i0LMxYl2wUFAAgW1OGGeuSEZ+WZdbbJyKi2yOIoig2eWVBuGXPyMsvv4wdO3YgLS3NsGz8+PEoLCxEfHx8o15Hq9VCrVZDo9Hw3jRWRqcXMXjxz7WCQg0BgI9ahd9evqtJp1RaevtERFS/xh6/W3zMSEJCAqKiooyWRUdHY/bs2fWuU15ejvLycsPPWq22pcojiSVmFNQbFABABJClKcPMr5Pgq3YweftZmquN2n5iRgEiO7c1eftERHT7WjyMZGdnw9vb22iZt7c3tFotrl69CgeH2geYhQsXYsGCBS1dGpmBHG39QeFG8Wk5LVrH6bwihhEiIomY5dU08+bNw5w5cww/a7Va+Pv7S1gRNSdRFHE0S4vvUy9hU1Jmo9YZG+aHdm6m94xcvHIV21Iv3bLd69uOYP2fmRgW5IlhQV7o28EVdnJe+U5E1BpaPIz4+PggJ8f4X7U5OTlwcXGps1cEAJRKJZRKZUuXRq3s3OUSfJ96Cd8dvITTucWG5QKqT5fUpWZMx/uPhjV5zMifGQXI1pTV+xr2cgGVuuqAdDRLi0/3pcNZZYchXT0wrJsXhgZ5wttFZfJrExFR47R4GImMjMTOnTuNlu3atQuRkZEt/dLUgho7Z0duURl2HMrCd6mXkJpZaFiusJMhqocX7g9th4oqHWZtSAVgHEpqthYXE9zkwaVymYC4mGDMWJdcK/TUbPHjCX3QP8Ad+0/mYd+JPOw/lYfC0krsPJyNnYezAQDdfZwxLMgLw4I80a+jG+wb6DXhfCZERKYx+Wqa4uJinD59GgDQp08fLFmyBMOHD4e7uzs6dOiAefPm4eLFi/jyyy8BVF/aGxISgpkzZ+Lxxx/Hzz//jOeeew47duxAdHR0o16TV9OYl1vN2aEtq8SPadn4/uAlHDidD/21vzCZAAzq4oExYe0Q3dMbzir7Rm+zpWu+kU4v4tCFQuw7kYd9J/Nw6EIhbvyUOCvtMKiLB4YFeWJokKfRwFrOZ0JEdF1jj98mh5F9+/Zh+PDhtZZPmTIFa9euxdSpU3H27Fns27fPaJ3nn38eR48eRfv27fH666+bNOkZw4j5qJmz4+Y/mppehz7+rjiSpUVFld7wXJ8OrhgT6ofRvf3g6Vz/6TdznYH1cnE5fj2Vj30ncrH/VD4KSiqMng/ydsawIE84KOT4cPepOn83ALA8ti8DCRHZlBYLI1JgGDEPt5qz40ZdvNpgbJgf7g9thw5tHVuhutah14s4fFFzrdckF6mZxr0m9eF8JkRki8xmnhGyHreaE6TGogd7YdwAfwiC9R10ZTIBof6uCPV3xayorrhSUoH9p/Kw+e8L+PV0fr3rcT4TIqL6MYxQo+UWNW5OEAeF3CqDSF3cnBQYE9YOABoMIzUa+zskIrIlnEiBGs2rgfEexu1s7zLYxu7z/w5mNXqiNyIiW8EwQo1ytUKHDYnnG2wjoPrKkfBA99YpyoyEB7rDV63CrfqDdh3LwZ3/2Yt3dh6rNRCWiMhWMYzQLWUWlOKh5b/ju4NZqBl7efNBtznmBLFkNfOZAHX/bgQAz0d1Q7+Obiiv0mPl/jO48z97sWTXSWjLKlu7XCIis8KraahBv53Kx7Prk3GltBJtnRRY9lhfFJZWcC6NetxqnhFRFLHvRB7e++kEjlyqvgGkq6M9nrqzM6YM7AhHBYdxEZH14KW9dFtEUcSqX89g0Q/HoReB3u3VWBHbD36u1RN8cZbR+jXmd6PXi4g/ko33fzqB9LwSAIBHGyWeGd4ZEyI6QGknl6J0IqJmxTBCTVZaUYV/bT6E/x3KAgA83K89/j02BCp7HiCbm04vYlvKRSzdcxKZBVcBAO1cHfDc3V3wUN/2vFkfEVk0hhFqkvOXS/GPr/7G8ewi2F0bBxF7R0ebuVRXKhVVenzzdyY+/vkUcrTlAIBADyfMjuqKmN5+kLHXiYgsEMMImeyXk3l4bn0KNFcr4dFGgU8f62eTV8ZIqaxSh68SzmH5L+mGq226+zjjhZFBiOrhxVBIRBaFYYQaTRRFLP8lHe/+eAKiCIT6u2JFbF+jG8BR6your8Lq3zKwav8ZFJVXAah+X14aGYRBXdoylBCRRWAYoVrqGlhZVqnDS5sPYufhbADA+AH+WDCmJwdQmonC0gp8tv8M1h44i6uVOgDAHZ3c8VJ0EPp1vN5rxQHFRGSOGEbISF2XnHq2UcJOLiBLUwZ7uYAF94dgYkQHCauk+uQWleHTven4+s/zqNBV3xF5eJAnXhgZhAtXSnmpNRGZJYYRMohPy8KMdcm1bm1fw0VlhzXTBhj9S5vM08XCq/h4zylsSroAnb7+j25Nn8jy2L4MJEQkmcYev3ndoJXT6UUs2H603iACVN/YLszfrdVqoqZr5+qARQ/1xu45Q3F/aP0ho+b9XrD9aIOhhYjIHDCMWLnEjAKj7vu65GjLkZhR0EoVUXMI9HDChPCODbYRAWRpyvjeEpHZYxixco29ZT1vbW95+N4SkbVgGLFyjb21fWPbkfnge0tE1oJhxMqFB7rDx6X+g5GA6isvOLmZ5QkPdIevWlXrLsE3++loNsqrdK1SExFRUzCMWDm5TMDgLm3rfK7mIBYXE8w5KSyQ/Np0/QBqBZIbf15z4CzGLvsdp3OLWq02IiJTMIxYubSLGnx38BKA6kt4b+SjVvHSTws3KsQXy2P7wkdt3Pvlo1ZhRWxf/N/k/nB3UuBYlhb3ffwb/vvnOVjA1fxEZGM4z4gVKymvwn0f/4aM/BJE9/TGsol98dfZK5yl0wo1NANrrrYML2w6iF9P5QMARgR7Y/FDveHupJCyZCKyAZz0jPDSpoPYlHQBvmoVfpg1BK6OPPjYKr1exOoDGVgcfxyVOhHeLkoseTQMg7p4SF0aEVkxTnpm474/eAmbki5AJgAfjAtjELFxMpmAJ4d0wtanB6GTpxNytOWI/fxPLPrhOCqq9FKXR0Q2jmHECmUWlOLVLYcBAM8M74I7OtU9gJVsT0g7Nf737GBMCO8AUQRW/JKOh5b/jjN5xVKXRkQ2jGHEylTq9HhuQwqKyqvQr6Mbnru7q9QlkZlxVNhh4YO9sCK2H1wd7XH4ogb3ffwbvvkrk4NbiUgSDCNW5sPdp5ByvhDOKjt8OD4MdnK+xVS3USE++GHWEER2aovSCh3+9e0hPPN1CjSllVKXRkQ2hkcqK/J7ej6W7TsNAFj0YG+0d3OUuCIyd75qB6x7MgL/GhUEO5mAHYezcM+H+/HnmctSl0ZENoRhxEoUlFTg+Y2pEEVg/AB/jO7NuUOoceQyAU8P64JvZwxEQFtHXNKUYcKqP/D+TydQqePgViJqeQwjVkAURfxr8yHkaMvR2dMJ86/NyklkilB/V/zvuSF4uF976EXg459P45EVCTh/uVTq0ojIyjGMWIGv/jiH3cdyoJDL8NGEPnBU2N16JaI6tFHa4b1HQvHxhD5wVtkhNbMQ9370K7amXJC6NCKyYgwjFu54thb/3nEMADDv3u7o6aeWuCKyBjGhfvhh1hAMCHBDcXkVnt94ELM2pEBbxsGtRNT8GEYs2NUKHZ79OgUVVXrc1d0LUwcGSF0SWZH2bo5YP/0OzBnRDXKZgO9SL+HeD39F0rkCqUsjIivD/nwLcvP9R74/eBGncovh6azEuw/3hiDwPjPUvOzkMjx3d1cM6uKBWRtScOHKVTz62R947q6umDm8M+zksgbvi0NE1Bi8N42FiE/LwoLtR5GlKav13LonIjC4K+8xQi1LW1aJ+dvSsC21+i7QAwLcMDasHT7Ze9ro79JXrUJcTDDvBk1EvDeNNYlPy8KMdcl1BhEAKC7neXxqeS4qeywd3wcfjAtFG6Ud/jp7Ba9uS6v1d5mtKcOMdcmIT8uSqFIisjQMI2ZOpxexYPtR1Nd9JQBYsP0odHqz7+AiK/FAn/bY/sxg2MvrPhVT85fIv0siaiyGETOXmFFQb48IUP3Fn6UpQ2IGBxVS68nWlqFSV3/Q4N8lEZmCYcTM5RbVH0Sa0o6oOfDvkoiaE8OImfNyVjVrO6LmwL9LImpODCNmLjzQHb5qFeq7UFJA9dUL4YHurVkW2bhb/V0CgIvKjn+XRNQoDCNmTi4TEBcTXOcA1poDQVxMMOd1oFZV83cJoN5Aoi2rwts7jkHPQaxEdAsMIxZgRLAPvJ2VtZb7qFVYHtuX8zmQJEaF+GJ5bF/4qI1PxfiqVXiwbzsAwOoDGXhmfTLKKnVSlEhEFoIzsFqAXUdzkFNUDheVHT4c3wfaskrOdElmYVSIL0YE+9Q5A+vQbp54cdNB7DycjfyiRKyc3A+ujgqpSyYiM8QwYuZEUcRn+9MBAJMjAzC8u5fEFREZk8sERHZuW2v5mLB28HRW4qmvkpB4tgAPr0jA2mkD0N7NUYIqicic8TSNmfv73BWknC+Ewk6GKbwRHlmYgZ09sOmfkfBxUeF0bjEe+PR3HLmkkbosIjIzDCNm7rNfqntFHu7XHp51jBshMnfdfVywdeZABHk7I6+oHI+uSMCvp/KkLouIzAjDiBk7lVOE3cdyIQjA9CGdpC6HqMl81Q745p+RuKOTO0oqdJi25i98m3RB6rKIyEwwjJixlfvPAACig30Q6OEkcTVEt0ftYI8vHg9HTKgfqvQiXth0EMv2noYF3DiciFoYw4iZytaUYVvqRQDAU0PZK0LWQWknx4fjwvDUndV/0+/+eAKvbUtDlU4vcWVEJCWGETO15vcMVOpEhAe6o08HN6nLIWo2MpmAeff2wBsxwRAE4L9/nsc/1yXhagXnIiGyVQwjZkhbVomv/zgPAIZ/QRJZm6mDAvHpxL5Q2Mmw+1guJqz6A5eLy6Uui4gkwDBihtb/eR5F5VXo6tUGw4M4rwhZr3t6+eLrJyPg6miP1MxCPLT8d5y7XCJ1WUTUyhhGzEx5lQ6rD2QAAP5xZyfIOMMqWbn+Ae7Y/M+BaO/mgLOXS/Hgp7/jYGah1GURUStiGDEz36VeQo62HN4uSowJayd1OUStootXG2x5eiB6+rngckkFxq/8Az8fz5G6LCJqJQwjZkSvF7Hq2uW8TwwOhMKObw/ZDi9nFTY+FYk7u3niaqUO079MwobE81KXRUStoElHu2XLliEgIAAqlQoRERFITExssP3SpUsRFBQEBwcH+Pv74/nnn0dZWVmTCrZme0/k4lRuMZyVdpgQ3kHqcohaXRulHT6f0h8P92sPnV7E3C2HsWTXSc5FQmTlTA4jGzduxJw5cxAXF4fk5GSEhoYiOjoaubm5dbb/+uuvMXfuXMTFxeHYsWP4/PPPsXHjRrzyyiu3Xby1+eyX6l6RiXd0gLPKXuJqiKRhL5fh3Yd747m7ugAAPtpzCv/afAiVnIuEyGqZHEaWLFmC6dOnY9q0aQgODsaKFSvg6OiI1atX19n+999/x6BBgzBx4kQEBARg5MiRmDBhwi17U2xN8vkrSDxbAHu5gMcHBUpdDpGkBEHAnJFBeOeBXpAJwKakC3jyi79RUl4ldWlE1AJMCiMVFRVISkpCVFTU9Q3IZIiKikJCQkKd6wwcOBBJSUmG8HHmzBns3LkT9957b72vU15eDq1Wa/Swdiuv9Yo80KcdvF1UEldDZB4mRnTAqsn94WAvxy8n8zBuZQJyi3iKl8jamBRG8vPzodPp4O3tbbTc29sb2dnZda4zceJEvPnmmxg8eDDs7e3RuXNnDBs2rMHTNAsXLoRarTY8/P39TSnT4pzJK8aPR6t/f//gJGdERu7u4Y31/7gDbZ0USLuoxYOf/o70vGKpyyKiZtTil2vs27cP77zzDj799FMkJydjy5Yt2LFjB956661615k3bx40Go3hkZmZ2dJlSmrVrxkQRSCqhxe6eDlLXQ6R2Qnzd8W3MwYioK0jLly5ioeW/46kcwVSl0VEzcSkMOLh4QG5XI6cHOPr/3NycuDj41PnOq+//jomTZqEJ598Er169cIDDzyAd955BwsXLoReX/eANKVSCRcXF6OHtcotKsO3ydW3Un9qaGeJqyEyXwEeTvh2xkCE+ruisLQSE1f9ifi0untkiciymBRGFAoF+vXrhz179hiW6fV67NmzB5GRkXWuU1paCpnM+GXkcjkA2Ozlejq9iIT0y/gu9SLe2XEMFVV69O3giv4deUM8ooa0baPE+ukRuLu7F8qr9Jjx3yR8mXAWgPHnKiH9MnR62/x+IbJEdqauMGfOHEyZMgX9+/dHeHg4li5dipKSEkybNg0AMHnyZLRr1w4LFy4EAMTExGDJkiXo06cPIiIicPr0abz++uuIiYkxhBJbEp+WhQXbjyJLYzwIb0CAOwSBU78T3Yqjwg6fTeqH+d8fwdd/nsf8747gt1P5OHRBg2zt9c+Vr1qFuJhgjArxlbBaImoMk8PIuHHjkJeXh/nz5yM7OxthYWGIj483DGo9f/68UU/Ia6+9BkEQ8Nprr+HixYvw9PRETEwM3n777ebbCwsRn5aFGeuSUde/11buP4M+HVz5xUnUCHZyGd4eG4J2rg5498cT+Olo7anjszVlmLEuGctj+/JzRWTmBNECzpVotVqo1WpoNBqLHT+i04sYvPjnWj0iNQQAPmoVfnv5Lsh5czyiRtHpRfR9axc0VyvrfJ6fKyJpNfb4zZuftJLEjIJ6gwgAiACyNGVIzOAVAkSNlZhRUG8QAfi5IrIUDCOtpLETNXFCJ6LG4+eKyDowjLQSL+fGzara2HZExM8VkbVgGGkl4YHu8FWrUN9ZawHVo//DA91bsywii3arzxUA+Ljwc0Vk7hhGWolcJiAuJrjO52q+SONigjnIjsgEN36u6vvkOKvsUFHFO/4SmTOGkVY0KsQXyyb2xc15w0et4uWHRE00KsQXy2P7wkdtfCrGo40CKjsZTuUW4x9f/Y2ySp1EFRLRrZg8zwjdHj83B+hFQGUvwztje8HX1QHhge7sESG6DaNCfDEi2AeJGQXILSqDl3P1qZnUzCuY9Hkifj2VjxnrkvDZpP5Q2PHfYETmhp/KVrbnWPXkTHd198KD/dojsnNbBhGiZiCXCYjs3BZjwtoZPlf9Orpj9dQBUNnLsPdEHp75OhmVOp6yITI3DCOtbPexXADA3d29Ja6EyDbc0akt/m/yACjsZPjpaA5mb0xFFQMJkVlhGGlFFwuv4liWFjIBGN7dS+pyiGzG4K4e+Cy2H+zlAnYcysK/Nh+CnjfSIzIbDCOt6Odrp2j6dnCDu5NC4mqIbMvw7l74ZGJfyGUCtqRcxCtbDzOQEJkJhpFWVHOKJiqYp2iIpBDd0wcfjg+DTAA2/JWJN7YfgQXcnovI6jGMtJLi8iokpF8GAET14CkaIqnc19sP7z8aCkEAvkw4h7d3HGMgIZIYw0gr+e1UHip0enRs64jOnm2kLofIpj3Qpz0WPtALAPB/v2Xg3R9PMJAQSYhhpJXceBWNIPBSXiKpjQ/vgDfH9AQAfLovHR/tOS1xRUS2i2GkFej0IvYevzZehKdoiMzG5MgAvDa6BwDgg90nsXxfusQVEdkmhpFWkJpZiMslFXBW2WEAb9hFZFaeHNIJL0UHAQAWxx/H6t8yJK6IyPYwjLSCmllXh3bzhL2cv3IiczNzeBc8d3dXAMCb/zuKdX+ck7giItvCI2Mr2FNzSW8PXtJLZK6ej+qKfw7tDAB4bVsavvkrU+KKiGwHw0gLyywoxYmcIshlAoYFeUpdDhHVQxAEvDwqCNMGBQAAXt5yCNtSLkpbFJGNYBhpYTWnaPp1dIOrI2ddJTJngiBg/n3BiL2jA0QRmPNNKnYcypK6LCKrxzDSwvbwKhoiiyIIAt68PwSP9GsPvQjM2pCCXUdzpC6LyKoxjLSgorJK/HGmetbVuzlehMhiyGQCFj3UG2PD/FClFzHzv8nYdyJX6rKIrBbDSAvafzIflToRgR5OnHWVyMLIZQLeeyQU9/byQYVOj6e+SsKB0/lSl0VklRhGWlDNeBGeoiGyTHZyGT4c3wdRPbxRXqXHk1/8jcSMAqnLIrI6DCMtRKcXsfdaty5P0RBZLnu5DMse64Oh3TxxtVKHaWsSkXz+itRlEVkVhpEWknz+Cq6UVkLtYI/+Hd2kLoeIboPSTo7PJvXDwM5tUVKhw5TViTh8QSN1WURWg2Gkhey+dopmWJAn7DjrKpHFU9nL8X9T+iM8wB1FZVWYtPpPHL2klbosIqvAo2QLqZl1ladoiKyHo8IOq6cNQJ8OrigsrcSkz//EqZwiqcsisngMIy3g3OUSnM4thp1MwNBunHWVyJq0Udph7bRwhLRzweWSCkz8vz9xJq9Y6rKILBrDSAvYfa1XZECAO9QO9hJXQ0TNTe1gj68ej0B3H2fkFZVj4qo/cf5yqdRlEVkshpEWUHNJ7928pJfIark5KbDuyQh08WqDbG0ZJv7fH7hYeFXqsogsEsNIM9OWVRrmIeBdeomsm0cbJb5+MgKBHk64cOUqJq76AznaMqnLIrI4DCPNRKcXkZB+Ge//eAJVehGdPBwR4OEkdVlE1MK8XFT4enoE/N0dcO5yKSau+gN5ReWG74TvUi8iIf0ydHpR6lKJzJYgiqLZf0K0Wi3UajU0Gg1cXFykLqeW+LQsLNh+FFma6/8iclLI8f6joRgV4ithZUTUWjILSjHuswRc0pTBT61ClV5EblG54XlftQpxMcH8TiCb0tjjN3tGblN8WhZmrEs2CiIAUFKhw4x1yYhP4+3HiWyBv7sjvp5+B1xUdrikKTMKIgCQrSnjdwJRPRhGboNOL2LB9qNoqGtpwfaj7J4lshH+7o5Q2NX9tVrzLcDvBKLaGEZuQ2JGQa0ekRuJALI0ZbyxFpGNSMwoQH5xRb3P8zuBqG4MI7cht6hxo+Yb246ILBu/E4iahmHkNng5q5q1HRFZNn4nEDUNw8htCA90h69aBaGe5wVUj6APD3RvzbKISCK3+k4A+J1AVBeGkdsglwmIiwmu87maL6O4mGDIZQ19NRGRtbjxO6G+T/2YMD9+JxDdhGHkNo0K8cXy2L5wVMiNlvuoVVge25dzChDZmJrvBB+18amYmu+IL34/h0MXCiWojMh8cdKzZnL/x7/i0EUtpg0MwMiePggPdOe/fohsmE4vIjGjALlFZfByVqFvB1dM/yoJ+0/mwaONEttmDkR7N0epyyRqUZz0rBVV6vQ4nlN9C/GpgwIQ2bktgwiRjZPLBER2bosxYe0Q2bktlPZyLJvYB919nJFfXI7H1/4FbVml1GUSmQWGkWZwOrcYFVV6OKvs0MGd/9Ihoro5q+yxeuoAeLsocTKnGDPWJaGiSi91WUSSYxhpBmkXNQCAnn4uEAT2iBBR/fxcHfD5lAFwVMhx4PRlvLr1MCzgbDlRi2IYaQZHLmkBACF+aokrISJLENJOjWUT+0ImAJuSLmDZ3tNSl0QkKYaRZlDTMxLSjmGEiBpneHcvLLi/JwDgvZ9O4rvUixJXRCQdhpHbpNOLOJp1rWeknXle6UNE5mlSZACmDwkEALy06RDvWUM2i2HkNmXkl6C0QgcHezkCPdpIXQ4RWZh59/TAqJ4+qNDp8Y+v/saZvGKpSyJqdQwjt+nIpepTNMF+Lrycl4hMJpMJ+GBcGEL9XVFYWolpa//C5eJyqcsialUMI7fJMF7Ej6doiKhpHBRy/N/k/mjv5oBzl0vxj6+SUFapk7osolbDMHKb0i5WjxfpycGrRHQbPJ2VWDttAFxUdkg6dwUvbDoIvZ6X/JJtYBi5DaIoIu1STc8IwwgR3Z4uXs74bFJ/2MsF7DiUhf/8eELqkohaBcPIbcgsuIqisioo5DJ09ebgVSK6fZGd22LRg70BACt+Scf6xPMSV0TU8poURpYtW4aAgACoVCpEREQgMTGxwfaFhYWYOXMmfH19oVQq0a1bN+zcubNJBZuTml6RIB9n2MuZ64ioeTzUrz1m3d0VAPDatjT8cjJP4oqIWpbJR9CNGzdizpw5iIuLQ3JyMkJDQxEdHY3c3Nw621dUVGDEiBE4e/YsNm/ejBMnTmDVqlVo167dbRcvteuTnXHwKhE1r9lRXfFgn3bQ6UXM/G8yjl2bz4jIGpkcRpYsWYLp06dj2rRpCA4OxooVK+Do6IjVq1fX2X716tUoKCjAtm3bMGjQIAQEBGDo0KEIDQ297eKllnZtGvieHC9CRM1MEAQseqg37ujkjuLyKjy+9i/kaMukLouoRZgURioqKpCUlISoqKjrG5DJEBUVhYSEhDrX+f777xEZGYmZM2fC29sbISEheOedd6DT1X/ZWnl5ObRardHD3IiiiCOcBp6IWpDCTobPYvujs6cTsjRleHztXygpr5K6LKJmZ1IYyc/Ph06ng7e3t9Fyb29vZGdn17nOmTNnsHnzZuh0OuzcuROvv/463n//ffz73/+u93UWLlwItVptePj7+5tSZqvI1pbhckkF5DIB3X2cpS6HiKyU2tEea6aGo62TAkcuafHs+hRU6fRSl0XUrFp81KVer4eXlxdWrlyJfv36Ydy4cXj11VexYsWKeteZN28eNBqN4ZGZmdnSZZqsZn6Rrl5toLKXS1wNEVmzDm0d8X9T+kNpJ8PPx3OxYPtRiCLnICHrYVIY8fDwgFwuR05OjtHynJwc+Pj41LmOr68vunXrBrn8+gG7R48eyM7ORkVFRZ3rKJVKuLi4GD3MTc3gVY4XIaLW0KeDGz4cHwZBAL764xw+/y1D6pKImo1JYUShUKBfv37Ys2ePYZler8eePXsQGRlZ5zqDBg3C6dOnoddf71Y8efIkfH19oVAomli29GruScMraYiotYwK8cUr9/QAALy98xji0+o+PU5kaUw+TTNnzhysWrUKX3zxBY4dO4YZM2agpKQE06ZNAwBMnjwZ8+bNM7SfMWMGCgoKMGvWLJw8eRI7duzAO++8g5kzZzbfXkjgyLUraTh4lYha05NDAjHpjo4QRWD2xhSkZhZKXRLRbbMzdYVx48YhLy8P8+fPR3Z2NsLCwhAfH28Y1Hr+/HnIZNczjr+/P3788Uc8//zz6N27N9q1a4dZs2bh5Zdfbr69aGX5xeXI0pRBEIAevuwZIaLWIwgC4mKCceFKKfaeyMOTX/yFrU8Pgr+7o9SlETWZIFrAKCitVgu1Wg2NRmMW40d+OZmHKasT0cnTCT+/MEzqcojIBpWUV+GRFQk4mqVFZ08nbJkxCGpHe6nLIjLS2OM35zBvAsPMqxy8SkQScVLaYfXUAfBxUSE9rwT/XJeEiipe8kuWiWGkCTh4lYjMgY9ahTXTBqCN0g4JZy5j7pZDvOSXLBLDSBPUzDHCnhEikloPXxcse6wv5DIBW5Iv4sM9p6QuichkDCMm0pRW4nxBKQDOMUJE5mFoN0+8NSYEALB09ylsSb4gcUVEpmEYMdGRrOpTNP7uDhwsRkRmY2JEBzw1tBMA4OVvDyEh/bLEFRE1HsOIiY7wFA0RmamXo7tjdC9fVOpEPPXV3zidWyR1SUSNwjBiorRLvFMvEZknmUzA+4+Gom8HV2jLqjBt7V/ILy6XuiyiW2IYMdH1e9LwShoiMj8qezlWTe6PDu6OyCy4iie/+BtllTqpyyJqEMOICUrKq3AmvwQAB68Skflq20aJNdMGQO1gj9TMQjy/MRV6PS/5JfPFMGKCY1laiCLg46KCp7NS6nKIiOrV2bMNVk7qB4Vchh/SsrEo/rjUJRHVi2HEBIaZVznZGRFZgIhObfHuI70BACv3n8FXf5yTuCKiujGMNJJOL+LnE7kAALWDPXTs8iQiCzAmrB1eGNENABD3XRr2Hs+FTi8iIf0yvku9iIT0y/w+I8nxRnmNEJ+WhQXbjyJLU2ZY5qtWIS4mGKNCfFu9HiIiU4iiiJc2H8LmpAtQ2sngrLJDfnGF4Xl+n1FL4Y3ymkl8WhZmrEs2CiIAkK0pw4x1yYhPy5KoMiKixhEEAe880AvdfdqgvEpvFEQAfp+R9BhGGqDTi1iw/Sjq6jqqWbZg+1F2cRKR2ZPLBFwprazzOX6fkdQYRhqQmFFQq0fkRiKALE0ZEjMKWq8oIqImSMwoQI62/gnQ+H1GUmIYaUBuUf1BpCntiIikwu8zMmcMIw3wclY1azsiIqnw+4zMGcNIA8ID3eGrVkGo53kB1aPQwwPdW7MsIiKT3er7DOD3GUmHYaQBcpmAuJjgOp+r+UDHxQRDLmvo401EJL0bv8/q+8b6V3QQv89IEgwjtzAqxBcfjAurtdxHrcLy2L68Lp+ILMaoEF8sj+0LH7XxqZia/BF/JBsWMPUUWSE7qQuwBDV36HW0l2HhQ73h5Vzdlcl/QRCRpRkV4osRwT5IzChAblEZvJxVsJcLmLjqT/x4JAfLf0nH08O6SF0m2RiGkUa4UHgVANDRow3GhLWTuBoiotsjlwmI7NzWaNmCMT0xb8thvPfjCYT4qXFnN0+JqiNbxNM0jXDxSnUYaefKUeZEZJ0mhHfAuP7+0IvAcxtSkFlQKnVJZEMYRhrhUmFNGHGQuBIiopazYExP9G6vRmFpJf65LglllTqpSyIbwTDSCBevhRE/hhEismIqezmWx/aDu5MCRy5p8erWNA5opVbBMNIIhp4RN4YRIrJu7Vwd8MmEPpAJwLfJF7Duz/NSl0Q2gGGkEa6PGWEYISLrN7CLB+be0x0A8Ob2I0g6x/vVUMtiGLmFSp0e2drqezUwjBCRrZg+pBNG9/JFpU7EjHXJvGcNtSiGkVvI0ZZBLwIKuQwebZRSl0NE1CoEQcB/Hu6Nrl5tkFtUjpn/TUalTi91WWSlGEZuoeYUja+rCjJOckZENsRJaYfPJvWDs9IOf529grd3HJO6JLJSDCO3cEnD8SJEZLs6ebbBkmu3xFj7+1lsTbkgbUFklRhGboGDV4nI1o0I9sazd1VPET9vy2EcuaSRuCKyNgwjt8A5RoiIgNlR3TC0myfKKvX457okFJZWSF0SWRGGkVu4WHjtShrOMUJENkwuE/Dh+DD4uzsgs+AqZm1IhU7PCdGoeTCM3MLFK9X3Z+BpGiKyda6OCnwW2x8qexl+OZmHD3eflLokshIMIw0QRRGXCjnHCBFRjWA/Fyx8sBcA4KOfT2PX0RyJKyJrwDDSgCullbh67UZRPmresZeICAAe6NMeUwcGAADmbEzFmbxiaQsii8cw0oCaK2k8nZVQ2cslroaIyHy8OroHwgPcUVRehae+SkJJeZXUJZEFYxhpQM2VNDxFQ0RkzF4uwyeP9YGXsxKncovxr82HeIdfajKGkQYwjBAR1c/LWYXlsX1hLxew43AWVv16RuqSyEIxjDTgUk0Y4WW9RER16tfRHfPvCwYALPrhOH4/nS9xRWSJGEYaUDNmxI+DV4mI6hV7R0c81Lc99CLwzPoUQ68yUWMxjDTAcJrGzVHiSoiIzJcgCHj7gRD09HNBQUkFZqxLQtm1KxGJGoNhpAGXOGaEiKhRVPZyrIjtB1dHexy6oMEb3x+RuiSyIAwj9bhaocPlkup7LzCMEBHdmr+7Iz6e0AcyAdjwVybWJ56XuiSyEAwj9bikqe4VaaO0g4uDncTVEBFZhiFdPfFidBAAIO67I0g5f0XiisgSMIzUwzB41VUFQRAkroaIyHLMGNoZ0T29UaHTY8a6ZOQVlUtdEpk5hpF6cLwIEVHTCIKA9x4JRWdPJ2Rry/DM18mo0umlLovMGMNIPS5yjhEioiZzVtnjs0n94KSQ48+MAiz64bjUJZEZYxipx/XTNAwjRERN0cXLGe8/GgoA+L/fMvD9wUsSV0TmimGkHpwKnojo9o0K8cWMYZ0BAC9vPoTj2VqJKyJzxDBSD4YRIqLm8eLIIAzu4oGrlTo89VUSNFcrpS6JzAzDSB10ehHZmjIAHDNCRHS75DIBH03og3auDjh3uRRzNqZCr+cdfuk6hpE65BaVoUovwk4mwMuZ96UhIrpd7k4KfDapH5R2Muw5nouPfz4tdUlkRhhG6lAzeNVHrYJcxjlGiIiaQ0g7Nd5+oBcAYOmek/j5eI7EFZG5YBipA8eLEBG1jIf7tUfsHR0gisDsDak4m18idUlkBpoURpYtW4aAgACoVCpEREQgMTGxUett2LABgiBg7NixTXnZVsMwQkTUcubf1xN9OrhCW1aFf65LQmlFldQlkcRMDiMbN27EnDlzEBcXh+TkZISGhiI6Ohq5ubkNrnf27Fm8+OKLGDJkSJOLbS2XOOEZEVGLUdjJsPyxfvBoo8Tx7CLM/fYwRJEDWm2ZyWFkyZIlmD59OqZNm4bg4GCsWLECjo6OWL16db3r6HQ6PPbYY1iwYAE6dep0WwW3hpoxI+wZISJqGT5qFT59rC/sZAK+P3gJqw+clbokkpBJYaSiogJJSUmIioq6vgGZDFFRUUhISKh3vTfffBNeXl544oknGvU65eXl0Gq1Ro/WVHOahrOvEhG1nPBAd7w6ugcA4J2dx/DHmcsSV0RSMSmM5OfnQ6fTwdvb22i5t7c3srOz61znt99+w+eff45Vq1Y1+nUWLlwItVptePj7+5tS5m0RRfF6zwhP0xARtaipAwMwNswPOr2IZ75ORpbmqtQlkQRa9GqaoqIiTJo0CatWrYKHh0ej15s3bx40Go3hkZmZ2YJVGtNerUJJhQ4A4KdmGCEiakmCIGDhg73Rw9cF+cUVmLEuGeVVOqnLolZmZ0pjDw8PyOVy5OQYXxuek5MDHx+fWu3T09Nx9uxZxMTEGJbp9dW3kbazs8OJEyfQuXPnWusplUoolUpTSms2Nado2jop4KCQS1IDEZEtcVDI8VlsP9z38a9IzSzEm9uPGuYjIdtgUs+IQqFAv379sGfPHsMyvV6PPXv2IDIyslb77t274/Dhw0hNTTU87r//fgwfPhypqamtevqlMXR60TAJj9rBHjpOV0xE1Co6tHXEhxP6QBCA//55HhsSzyMh/TK+S72IhPTL/D62cib1jADAnDlzMGXKFPTv3x/h4eFYunQpSkpKMG3aNADA5MmT0a5dOyxcuBAqlQohISFG67u6ugJAreVSi0/LwoLtR5F17Z40Z/JLMHjxz4iLCcaoEF+JqyMisn7Dg7zwfFQ3LNl1EnO3HDZ6zlet4vexFTN5zMi4cePw3nvvYf78+QgLC0Nqairi4+MNg1rPnz+PrKysZi+0JcWnZWHGumRDEKmRrSnDjHXJiE+zrP0hIrJUXTzb1Lmc38fWTRAtYKYZrVYLtVoNjUYDFxeXZt22Ti9i8OKfawWRGgKqr4f/7eW7eJ8aIqIWxO9j69PY47fN35smMaOg3j98ABABZGnKkJhR0HpFERHZIH4f2y6bDyO5RfX/4TelHRERNQ2/j22XzYcRL2dVs7YjIqKm4fex7bL5MBIe6A5ftQr1nX0UUD2KOzzQvTXLIiKyObf6Pgb4fWytbD6MyGUC4mKCAaDWB6Dm57iYYA6WIiJqYQ19H9cY0tWD38dWyObDCACMCvHF8ti+8HQ2nvXVR63C8ti+vK6diKiV1Hwf+6iNT8W4qKqnxdqWcglpFzVSlEYtyOYv7b3RiewiRC/dDwd7OVZPHYDwQHcmcCIiCej0IhIzCpBbVAYvZxUGBLjhn+uSsftYDgI9nPC/ZwfDSWnyvJ3UynhpbxOUVFQBADycFYjs3JZBhIhIInKZgMjObTEmrB0iO7eFnVyGdx/uDV+1Chn5JZj/3RGpS6RmxDByA+3VSgCAi8pe4kqIiOhmbk4KLB0XBpkAfJt8AVtTLkhdEjUThpEbaMuqe0YYRoiIzFNEp7Z47u6uAIDXtqYhI79E4oqoOTCM3MDQM+LA85BERObq2bu6IjzQHSUVOjy7PhnlVTqpS6LbxDByA20ZT9MQEZk7uUzAh+PD4Opoj7SLWvwn/oTUJdFtYhi5gfbqtdM0DgwjRETmzFftgHcfDgUAfP5bBvYez5W4IrodDCM3YM8IEZHlGBHsjakDAwAAL2w6iBwt71ljqRhGbsAxI0RElmXuPd0R7OuCgpIKzN6QCp3e7KfOojowjNyAV9MQEVkWlb0cH0/sA0eFHAlnLmP5vtNSl0RNwDByg+s9IwwjRESWorNnG7w5JgQA8MHuU/j7bIHEFZGpGEZucH3MCE/TEBFZkof6tsPYMD/o9CJmbUiFprRS6pLIBAwjN+DVNERElkkQBPz7gV7o2NYRFwuv4uVvD8ECbr1G1zCM3MDQM8IwQkRkcdoo7fDxhD6wlwuIP5KN//55XuqSqJEYRq4pq9ShokoPgKdpiIgsVe/2rnh5VHcAwJv/O4rj2VqJK6LGYBi5pmbwqkwAnBQMI0RElurxQYEYFuSJiio9nvk6BaXX7shO5oth5JqaUzTOKnvIZILE1RARUVPJZALeeyQUXs5KnM4txpvbj0pdEt0Cw8g1GsPgVfaKEBFZOo82SiwdFwZBADb8lYntBy9JXRI1gGHkGk4FT0RkXQZ28cDMYV0AAK9sOYzMglKJK6L6MIxcUzNmRM0raYiIrMbsqK7o19ENReVVeHZ9Cip1eqlLojowjFzDqeCJiKyPnVyGD8eHwUVlh9TMQrz/00mpS6I6MIxcw5vkERFZp/Zujlj8UG8AwIpf0rH/ZJ7EFdHNGEau4ZgRIiLrdU8vXzwW0QEAMOebg8grKpe4IroRw8g1nAqeiMi6vX5fMIK8nZFfXI4536RCr+d08eaCYeQa3iSPiMi6qezl+HhiH6jsZfj1VD5W/XpG6pLoGoaRa66PGWHPCBGRterm7Yy4mJ4AgHd/PIGU81ckrogAhhEDXk1DRGQbxg/wx+jevqjSi3huQ4qhZ5ykwzByTRF7RoiIbIIgCFj4YC+0d3NAZsFVvLLlMESR40ekxDByjWHMCC/tJSKyei4qe3w0oQ/kMgH/O5SFb/7OlLokm8YwAkAUxetX0/A0DRGRTejbwQ0vjgwCAMR9fwSnc4skrsh2MYwAKK/So+LaFME8TUNEZDueurMThnT1QFmlHs98nYKySp3UJdkkhhFcv5JGJgBOCrnE1RARUWuRyQS8/2goPNoocDy7CG/vOCZ1STaJYQSA5obBq4IgSFwNERG1Ji9nFZY8GgYA+OqPc4hPy5a2IBvEMAJOBU9EZOvu7OaJp+7sBAD41+aDuFh4VeKKbAvDCG6cCp5X0hAR2aoXRgYh1N8V2rIqzFqfgqprYwmp5TGMgD0jREQEKOxk+Hh8Hzgr7fD3uSv4aM8pqUuyGQwjuGEqeIYRIiKb1qGtI95+sBcA4OO9p/F7er7EFdkGhhHcMBU8T9MQEdm8+0P9MK6/P0QReH5jKgpKKqQuyeoxjOB6z4iac4wQERGAuPuD0dnTCTnacry46SCni29hDCPgmBEiIjLmqLDDJxP7QmEnw8/Hc7H6wFmpS7JqDCO48WoahhEiIqrWw9cFr4/uAQBY9MMxHL6gkbgi68UwAt4kj4iI6hZ7R0dE9/RGpU7Es+uTUVxeJXVJVolhBLyahoiI6iYIAhY/1Bt+ahXOXi7F/G1pUpdklRhGcOPVNAwjRERkzNVRgQ8n9IFMALakXMS3SRekLsnqMIyAPSNERNSwAQHumB3VDQDw+ndpOJNXLHFF1sXmw4goihwzQkREtzRzeBfc0ckdpRU6PLs+BeVVOqlLsho2H0bKKvWo1FVfP86eESIiqo9cJmDpuD5wc7THkUtaLP7hhNQlWQ2bDyM1vSJymQBHhVziaoiIyJz5qFV4/9FQAMDqAxnYcyxH4oqsA8OIYbyIHQRBkLgaIiIyd3d198bjgwIBAC9uOohsTZnEFVk+mw8jNfcckMsEJKRfhk7PKX+JiKhhL98ThJ5+LrhSWonZG1NQUaVHQvplfJd6kceSJmhSGFm2bBkCAgKgUqkQERGBxMTEetuuWrUKQ4YMgZubG9zc3BAVFdVg+9YUn5aFf65LAgDkF1dgwqo/MHjxz4hPy5K4MiIiMmdKOzk+mdgXTgo5/jhTgL5v7cKEVX9g1oZUHkuawOQwsnHjRsyZMwdxcXFITk5GaGgooqOjkZubW2f7ffv2YcKECdi7dy8SEhLg7++PkSNH4uLFi7dd/O2IT8vCjHXJuFJaabQ8W1OGGeuS+UdEREQNCvRwwiP92wNArZlZeSwxjSCaeCvCiIgIDBgwAJ988gkAQK/Xw9/fH88++yzmzp17y/V1Oh3c3NzwySefYPLkyY16Ta1WC7VaDY1GAxcXF1PKrbsGvYjBi39GVj3n+QRUD1L67eW7IJdxHAkREdXGY8mtNfb4bVLPSEVFBZKSkhAVFXV9AzIZoqKikJCQ0KhtlJaWorKyEu7u7vW2KS8vh1arNXo0p8SMgnr/eABABJClKUNiRkGzvi4REVkPHkuaj0lhJD8/HzqdDt7e3kbLvb29kZ2d3ahtvPzyy/Dz8zMKNDdbuHAh1Gq14eHv729KmbeUW9S4kc+NbUdERLaHx5Lm06pX0yxatAgbNmzA1q1boVKp6m03b948aDQawyMzM7NZ6/Byrv+1m9KOiIhsD48lzcek+c89PDwgl8uRk2M8yUtOTg58fHwaXPe9997DokWLsHv3bvTu3bvBtkqlEkql0pTSTBIe6A5ftQrZmjLUNWCm5jxfeGD9p5KIiMi28VjSfEzqGVEoFOjXrx/27NljWKbX67Fnzx5ERkbWu95//vMfvPXWW4iPj0f//v2bXm0zkcsExMUE1/lczRCjuJhgmx1wREREt3bjsaSuo4UIHksay+TTNHPmzMGqVavwxRdf4NixY5gxYwZKSkowbdo0AMDkyZMxb948Q/vFixfj9ddfx+rVqxEQEIDs7GxkZ2ejuFjaOx6OCvHF8ti+cLA3ngLeR63C8ti+GBXiK1FlRERkKWqOJT7q2qdi7GQCOnu2kaAqy2PybWrHjRuHvLw8zJ8/H9nZ2QgLC0N8fLxhUOv58+chk13POMuXL0dFRQUefvhho+3ExcXhjTfeuL3qb9OoEF/8dCQHW1Iu4v5QP0wI74DwQHemWCIiarRRIb4YEeyDxIwC5BaVwctZiRW/pOOXk/l4/ptUbH16EOzlNj/heYNMnmdECs09z8iNZm1IwXepl/Da6B54ckinZt02ERHZplxtGUYu3Y/C0ko8d3dXzBnRTeqSJNEi84xYo6pr9w+wY28IERE1Ey8XFf49NgQAsGzvaaRmFkpbkJljGNHpAQB27EIjIqJmdF9vP9wf6gedXsScb1JxtUIndUlmy+aPwFW66p4Rezl7RoiIqHm9OaYnvF2UOJNXgsXxx6Uux2zZfBipNJymsflfBRERNTNXRwX+83AoAGDt72dx4HS+xBWZJ5s/Al8/TcOeESIian5Du3ki9o4OAIAXNx2E5mrlLdawPQwj7BkhIqIW9sq9PdCxrSOyNGVYsP2I1OWYHZs/ArNnhIiIWpqjwg5LHg2DTAC2JF9EfFqW1CWZFYYRPQewEhFRy+vX0Q0zhnUGALyyNQ15ReUSV2Q+bD6MVF67mkbO0zRERNTCZt3dDT18XVBQUoF5Ww7BAuYdbRU2fwTW6atP09hz0jMiImphCjsZPhgXCoVcht3HcrEp6YLUJZkFmw8jNfOMcNIzIiJqDd19XPDCyOrp4d/cfhSZBaUSVyQ9mz8CV+o5gJWIiFrXk0M6YUCAG4rLq/DCpoPQ6237dI3NhxFDzwhP0xARUSuRywS8/0gYHBVyJGYUYPWBDKlLkhTDCOcZISIiCXRo64jX7wsGAPznxxM4mVMkcUXSsfkjcM08I7y0l4iIWtv4Af4YHuSJiio9nt+YiooqvdQlSYJhhANYiYhIIoIgYPFDveHqaI8jl7T45OdTUpckCZs/AhsGsHLMCBERScDLRYW3x/YCACzbl46U81ckrqj12XwY0dWMGeFpGiIiksjo3r4YE+YHnV7EC98cxNUKndQltSqbDiOiKBpmYOUAViIiktKb94fA20WJM/klWBx/XOpyWpVNH4F1N1zXzQGsREQkJbWjPd59OBQAsPb3s/jtVL7EFbUemw4jVTeEEQ5gJSIiqd3ZzROT7ugIAHhp80ForlZKXFHrsOkjsFEY4QBWIiIyA/Pu7Y6Ato7I0pRhwfdHpC6nVdh2GNFdv56bYYSIiMyBo8IO7z8aBpkAbEm5iB8OZ0ldUouz6TBSM3gVqJ6al4iIyBz06+iGGcM6AwBe2XoYuUVlElfUsmw6jFTpr8++KggMI0REZD5m3d0Nwb4uuFJaiVe2HIYoWu/N9Gw7jFzrGWGvCBERmRuFnQwfjAuDQi7D7mO52PT3BalLajG2HUauDWC15xwjRERkhoJ8nPFidDcAwILtR5BZUCpxRS3Dpo/CNQNYOfsqERGZqycGd0J4gDtKKnR4YdNB6PXWd7rGpsNIJW+SR0REZk4uE/DeI6FwUsiRmFGA1QcypC6p2dn0UbiKN8kjIiIL0KGtI16/LxgA8J8fT+BkTpHEFTUvGw8jvEkeERFZhnED/HFXdy9UVOnx/MZUVFTpb72ShbDtMKLjAFYiIrIMgiBg0UO94OZojyOXtPj451NSl9RsbPoozAGsRERkSbycVXj7gV4AgGV7TyP5/BWJK2oeNh1GKmtO07BnhIiILMS9vXwxNswPehF44ZuDuFqhk7qk22bTR2Gdnj0jRERkeRbcHwIfFxUy8kuw6IdjUpdz22w6jBgu7eXVNEREZEHUjvZ495HeAIAvEs7h11N5Eld0e2w6jFRxnhEiIrJQQ7p6YnJkRwDAS5sOQVNaKXFFTWfTR+Ebb5RHRERkaebe0x2BHk7I1pbhje1HpC6nyWw7jBhulGfTvwYiIrJQjgo7vP9oKGQCsDXlInYezpK6pCax6aOwoWeEY0aIiMhC9e3ghqeHdQEAvLr1MHKLyiSuyHQ2HUau35uGYYSIiCzXc3d3RbCvC66UVmLet4chipZ1Mz2bDiPXJz2z6V8DERFZOIWdDB+MC4NCLsOe47n45u9MqUsyiU0fhQ33puFpGiIisnBBPs54MbobAODN7UeRWVAqcUWNxzACzsBKRETW4YnBnRAe6I6SCh1e+OYgdHrLOF1j00fhmtM0vLSXiIisgVwm4P1HQuGkkCPxbAFW/5YhdUmNYtNhhANYiYjI2vi7O2J+TDAA4N0fT+BEdpHEFd2aTYcRHU/TEBGRFXq0vz/u7u6FCp0ec75JRUWVXuqSGmTTR+HKmhvlcQArERFZEUEQsPChXnBztMeRS1p8tOeU1CU1yKbDCO9NQ0RE1srLWYV3HugFAPh032kkn78icUX1s+mjMAewEhGRNbunly8e6NMOehF44ZuDKK2okrqkOtl0GKnkmBEiIrJyb9zfEz4uKmTkl2DRD8elLqdONn0U1vFqGiIisnJqB3u8+0hvAMCXCeew/2SexBXVZtNhhANYiYjIFgzp6okpkR0BAP/afAia0kqJKzJm02GEA1iJiMhWzL2nBzp5OCFbW4a479OkLseITR+Fq/QcwEpERLbBQSHH+4+GQiYA21IvYcehLKlLMrDZMKLTi8jVlgMAzl0usZj5+4mIiJqqTwc3zBzeBQDw2rbDyCq8ioT0y/gu9SIS0i9LdiwURFE0+6OwVquFWq2GRqOBi4vLbW8vPi0LC7YfRZamzLDMV61CXEwwRoX43vb2iYiIzFVFlR4PfHoARy5pobSTofyG2Vmb+1jY2ON3k3pGli1bhoCAAKhUKkRERCAxMbHB9ps2bUL37t2hUqnQq1cv7Ny5sykv2yzi07IwY12yURABgGxNGWasS0Z8mvl0WxERETU3hZ0MD/VtDwBGQQSQ7lhochjZuHEj5syZg7i4OCQnJyM0NBTR0dHIzc2ts/3vv/+OCRMm4IknnkBKSgrGjh2LsWPHIi2t9QfP6PQiFmw/irq6gmqWLdh+lKdsiIjIaun0Ilb9eqbO56Q6FpocRpYsWYLp06dj2rRpCA4OxooVK+Do6IjVq1fX2f7DDz/EqFGj8NJLL6FHjx5466230LdvX3zyySf1vkZ5eTm0Wq3RozkkZhTU6hG5kQggS1OGxIyCZnk9IiIic2OOx0KTwkhFRQWSkpIQFRV1fQMyGaKiopCQkFDnOgkJCUbtASA6Orre9gCwcOFCqNVqw8Pf39+UMuuVW1T/L78p7YiIiCyNOR4LTQoj+fn50Ol08Pb2Nlru7e2N7OzsOtfJzs42qT0AzJs3DxqNxvDIzMw0pcx6eTmrmrUdERGRpTHHY6Fdq72SCZRKJZRKZbNvNzzQHb5qFbI1ZXWOGxEA+KhVCA90b/bXJiIiMgfmeCw0qWfEw8MDcrkcOTk5RstzcnLg4+NT5zo+Pj4mtW9JcpmAuJhgANW/7BvV/BwXEww5p4cnIiIrZY7HQpPCiEKhQL9+/bBnzx7DMr1ejz179iAyMrLOdSIjI43aA8CuXbvqbd/SRoX4YnlsX/iojbuffNQqLI/ty3lGiIjI6pnbsdDk0zRz5szBlClT0L9/f4SHh2Pp0qUoKSnBtGnTAACTJ09Gu3btsHDhQgDArFmzMHToULz//vsYPXo0NmzYgL///hsrV65s3j0xwagQX4wI9kFiRgFyi8rg5VzdHcUeESIishXmdCw0OYyMGzcOeXl5mD9/PrKzsxEWFob4+HjDINXz589DJrve4TJw4EB8/fXXeO211/DKK6+ga9eu2LZtG0JCQppvL5pALhMQ2bmtpDUQERFJyVyOhTY5HTwRERG1vBadDp6IiIiouTCMEBERkaQYRoiIiEhSDCNEREQkKYYRIiIikhTDCBEREUmKYYSIiIgkxTBCREREkjLLu/berGZeNq1WK3ElRERE1Fg1x+1bza9qEWGkqKgIAODv7y9xJURERGSqoqIiqNXqep+3iOng9Xo9Ll26BGdnZwhC893AR6vVwt/fH5mZmVY7zby17yP3z/JZ+z5y/yyfte9jS+6fKIooKiqCn5+f0X3rbmYRPSMymQzt27dvse27uLhY5R/Yjax9H7l/ls/a95H7Z/msfR9bav8a6hGpwQGsREREJCmGESIiIpKUTYcRpVKJuLg4KJVKqUtpMda+j9w/y2ft+8j9s3zWvo/msH8WMYCViIiIrJdN94wQERGR9BhGiIiISFIMI0RERCQphhEiIiKSFMMIERERScrqwsiyZcsQEBAAlUqFiIgIJCYmNth+06ZN6N69O1QqFXr16oWdO3caPS+KIubPnw9fX184ODggKioKp06dasldaJAp+7dq1SoMGTIEbm5ucHNzQ1RUVK32U6dOhSAIRo9Ro0a19G40yJR9XLt2ba36VSqVURtLfg+HDRtWa/8EQcDo0aMNbczpPdy/fz9iYmLg5+cHQRCwbdu2W66zb98+9O3bF0qlEl26dMHatWtrtTH1c91STN2/LVu2YMSIEfD09ISLiwsiIyPx448/GrV54403ar1/3bt3b8G9aJip+7hv3746/0azs7ON2lnqe1jX50sQBPTs2dPQxpzew4ULF2LAgAFwdnaGl5cXxo4dixMnTtxyPamPhVYVRjZu3Ig5c+YgLi4OycnJCA0NRXR0NHJzc+ts//vvv2PChAl44oknkJKSgrFjx2Ls2LFIS0sztPnPf/6Djz76CCtWrMCff/4JJycnREdHo6ysrLV2y8DU/du3bx8mTJiAvXv3IiEhAf7+/hg5ciQuXrxo1G7UqFHIysoyPNavX98au1MnU/cRqJ7C+Mb6z507Z/S8Jb+HW7ZsMdq3tLQ0yOVyPPLII0btzOU9LCkpQWhoKJYtW9ao9hkZGRg9ejSGDx+O1NRUzJ49G08++aTRAbspfxMtxdT9279/P0aMGIGdO3ciKSkJw4cPR0xMDFJSUoza9ezZ0+j9++2331qi/EYxdR9rnDhxwmgfvLy8DM9Z8nv44YcfGu1XZmYm3N3da30GzeU9/OWXXzBz5kz88ccf2LVrFyorKzFy5EiUlJTUu45ZHAtFKxIeHi7OnDnT8LNOpxP9/PzEhQsX1tn+0UcfFUePHm20LCIiQnzqqadEURRFvV4v+vj4iO+++67h+cLCQlGpVIrr169vgT1omKn7d7OqqirR2dlZ/OKLLwzLpkyZIo4ZM6a5S20yU/dxzZo1olqtrnd71vYefvDBB6Kzs7NYXFxsWGZu72ENAOLWrVsbbPOvf/1L7Nmzp9GycePGidHR0Yafb/d31lIas391CQ4OFhcsWGD4OS4uTgwNDW2+wppRY/Zx7969IgDxypUr9baxpvdw69atoiAI4tmzZw3LzPk9zM3NFQGIv/zyS71tzOFYaDU9IxUVFUhKSkJUVJRhmUwmQ1RUFBISEupcJyEhwag9AERHRxvaZ2RkIDs726iNWq1GREREvdtsKU3Zv5uVlpaisrIS7u7uRsv37dsHLy8vBAUFYcaMGbh8+XKz1t5YTd3H4uJidOzYEf7+/hgzZgyOHDlieM7a3sPPP/8c48ePh5OTk9Fyc3kPTXWrz2Bz/M7MiV6vR1FRUa3P4KlTp+Dn54dOnTrhsccew/nz5yWqsOnCwsLg6+uLESNG4MCBA4bl1vYefv7554iKikLHjh2Nlpvre6jRaACg1t/cjczhWGg1YSQ/Px86nQ7e3t5Gy729vWudu6yRnZ3dYPua/5qyzZbSlP272csvvww/Pz+jP6hRo0bhyy+/xJ49e7B48WL88ssvuOeee6DT6Zq1/sZoyj4GBQVh9erV+O6777Bu3Tro9XoMHDgQFy5cAGBd72FiYiLS0tLw5JNPGi03p/fQVPV9BrVaLa5evdosf/fm5L333kNxcTEeffRRw7KIiAisXbsW8fHxWL58OTIyMjBkyBAUFRVJWGnj+fr6YsWKFfj222/x7bffwt/fH8OGDUNycjKA5vnuMheXLl3CDz/8UOszaK7voV6vx+zZszFo0CCEhITU284cjoV2zbIVMnuLFi3Chg0bsG/fPqMBnuPHjzf8f69evdC7d2907twZ+/btw9133y1FqSaJjIxEZGSk4eeBAweiR48e+Oyzz/DWW29JWFnz+/zzz9GrVy+Eh4cbLbf099BWfP3111iwYAG+++47o/EU99xzj+H/e/fujYiICHTs2BHffPMNnnjiCSlKNUlQUBCCgoIMPw8cOBDp6en44IMP8NVXX0lYWfP74osv4OrqirFjxxotN9f3cObMmUhLS5N0DFJjWU3PiIeHB+RyOXJycoyW5+TkwMfHp851fHx8Gmxf819TttlSmrJ/Nd577z0sWrQIP/30E3r37t1g206dOsHDwwOnT5++7ZpNdTv7WMPe3h59+vQx1G8t72FJSQk2bNjQqC82Kd9DU9X3GXRxcYGDg0Oz/E2Ygw0bNuDJJ5/EN998U6s7/Gaurq7o1q2bRbx/9QkPDzfUby3voSiKWL16NSZNmgSFQtFgW3N4D5955hn873//w969e9G+ffsG25rDsdBqwohCoUC/fv2wZ88ewzK9Xo89e/YY/cv5RpGRkUbtAWDXrl2G9oGBgfDx8TFqo9Vq8eeff9a7zZbSlP0DqkdAv/XWW4iPj0f//v1v+ToXLlzA5cuX4evr2yx1m6Kp+3gjnU6Hw4cPG+q3hvcQqL7srry8HLGxsbd8HSnfQ1Pd6jPYHH8TUlu/fj2mTZuG9evXG12SXZ/i4mKkp6dbxPtXn9TUVEP91vAeAtVXqZw+fbpR/yCQ8j0URRHPPPMMtm7dip9//hmBgYG3XMcsjoXNMgzWTGzYsEFUKpXi2rVrxaNHj4r/+Mc/RFdXVzE7O1sURVGcNGmSOHfuXEP7AwcOiHZ2duJ7770nHjt2TIyLixPt7e3Fw4cPG9osWrRIdHV1Fb/77jvx0KFD4pgxY8TAwEDx6tWrZr9/ixYtEhUKhbh582YxKyvL8CgqKhJFURSLiorEF198UUxISBAzMjLE3bt3i3379hW7du0qlpWVtfr+NWUfFyxYIP74449ienq6mJSUJI4fP15UqVTikSNHDG0s+T2sMXjwYHHcuHG1lpvbe1hUVCSmpKSIKSkpIgBxyZIlYkpKinju3DlRFEVx7ty54qRJkwztz5w5Izo6OoovvfSSeOzYMXHZsmWiXC4X4+PjDW1u9Tsz5/3773//K9rZ2YnLli0z+gwWFhYa2rzwwgvivn37xIyMDPHAgQNiVFSU6OHhIebm5rb6/omi6fv4wQcfiNu2bRNPnTolHj58WJw1a5Yok8nE3bt3G9pY8ntYIzY2VoyIiKhzm+b0Hs6YMUNUq9Xivn37jP7mSktLDW3M8VhoVWFEFEXx448/Fjt06CAqFAoxPDxc/OOPPwzPDR06VJwyZYpR+2+++Ubs1q2bqFAoxJ49e4o7duwwel6v14uvv/666O3tLSqVSvHuu+8WT5w40Rq7UidT9q9jx44igFqPuLg4URRFsbS0VBw5cqTo6ekp2tvbix07dhSnT58uyRfEjUzZx9mzZxvaent7i/fee6+YnJxstD1Lfg9FURSPHz8uAhB/+umnWtsyt/ew5jLPmx81+zRlyhRx6NChtdYJCwsTFQqF2KlTJ3HNmjW1ttvQ76w1mbp/Q4cObbC9KFZfyuzr6ysqFAqxXbt24rhx48TTp0+37o7dwNR9XLx4sdi5c2dRpVKJ7u7u4rBhw8Sff/651nYt9T0UxerLWB0cHMSVK1fWuU1zeg/r2jcARp8rczwWCteKJyIiIpKE1YwZISIiIsvEMEJERESSYhghIiIiSTGMEBERkaQYRoiIiEhSDCNEREQkKYYRIiIikhTDCBEREUmKYYSIiIgkxTBCREREkmIYISIiIkn9PwoDKi3dR9BSAAAAAElFTkSuQmCC",
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+1aXRUWbz7G0EJERGRjDCytMBgFVu0+BdHEvoZtq3afgsHYVAsiIiLqCAwsrcjMu9LozMrNBIACXRUy867YrygiIqIuhoGlFUVlzYeVtrQjIiIi6zGwtMLSGxvyBohERES206bAsnbtWgQHB0OlUiEqKgqZmZnNtn3nnXcwduxY9OjRAz169EBMTEyj9vPnz4dMJjN7xMXFtaW0DhcZ4g1/tQqyZvbLAPirVYgM8bZnWURERF2K1YHlo48+QkJCApKSknDs2DFEREQgNjYWRUVFTbZPS0vD7NmzceDAAWRkZCAwMBCTJ0/GxYsXzdrFxcWhoKDA9NiyZUvbetTBFHIZkqaFNbmvIcQkTQuDQt5cpCEiIqL2kgkhrJreEhUVhbvuugtvvvkmAMBoNCIwMBBLly7F8uXLW329wWBAjx498Oabb2Lu3LkA6s+wlJaWYufOnRbVUF1djerqatNzvV6PwMBA6HQ6eHl5WdMdi6VkFWDpluOoNdz44/JXq5A0LQxx4f42eU8iIiJnptfroVarLfr+tuoMS01NDY4ePYqYmJgbB5DLERMTg4yMDIuOUVlZidraWnh7m19CSUtLg6+vLwYOHIjFixejpKSk2WMkJydDrVabHoGBgdZ0o03iwv3Rp7s7AGDpvf2x5eG7cejpexlWiIiI7MCqwFJcXAyDwQA/Pz+z7X5+ftBqtRYd4+mnn0ZAQIBZ6ImLi8OmTZuQmpqK1atX4+DBg5gyZQoMBkOTx0hMTIROpzM98vPzrelGm5VeqwUATIsIQHS/nrwMREREZCcu9nyzF154AVu3bkVaWhpUqhuzambNmmX6/ZAhQzB06FD069cPaWlpmDhxYqPjKJVKKJVKu9TcwGAUpsDSvZurXd+biIioq7PqDIuPjw8UCgUKCwvNthcWFkKj0bT42pdffhkvvPACvvzySwwdOrTFtn379oWPjw9ycnKsKc+m9Ndq0TDap7u7m2OLISIi6mKsCixubm4YMWIEUlNTTduMRiNSU1MRHR3d7OtefPFFPPfcc0hJScHIkSNbfZ8LFy6gpKQE/v7SGR9ytbIGAOCpdIGbC5evISIisierv3kTEhLwzjvv4P3338fp06exePFiVFRUYMGCBQCAuXPnIjEx0dR+9erVWLFiBd59910EBwdDq9VCq9WivLwcAFBeXo4nn3wShw8fxtmzZ5Gamorp06ejf//+iI2N7aButl9DYOnuwctBRERE9mb1GJaZM2fi8uXLWLlyJbRaLYYNG4aUlBTTQNzz589DLr+Rg9atW4eamhrcf//9ZsdJSkrCs88+C4VCge+//x7vv/8+SktLERAQgMmTJ+O5556z+ziVllytqB+/4t2Nl4OIiIjszep1WKTImnncbbXt23w89cn3GDegF95/KNIm70FERNSV2Gwdlq6s9PolIW8PnmEhIiKyNwYWC12p4JRmIiIiR2FgsVDDGZYeHMNCRERkdwwsFmqYJdSDl4SIiIjsjoHFQg2zhHrwkhAREZHdMbBYqOEMC6c1ExER2R8Di4VMC8cxsBAREdkdA4sFhBC4Wnl94TiOYSEiIrI7BhYL6KvqYDDWr6/Hac1ERET2x8BigYYpze6uCqhcFQ6uhoiIqOthYLEALwcRERE5FgOLBa5WNAy45eUgIiIiR2BgscBV3keIiIjIoRhYLHClglOaiYiIHImBxQKlDWNYeEmIiIjIIRhYLHCFi8YRERE5FAOLBW7cqZlnWIiIiByBgcUCphsfctAtERGRQzCwWOCq6QwLAwsREZEjMLBYgNOaiYiIHIuBpRVCCNMlIS4cR0RE5BgMLK2orDGgxmAEwEtCREREjsLA0oqGy0FuLnJ0c+OND4mIiByBgaUVphlC3Vwhk8kcXA0REVHXxMDSCs4QIiIicjwGllYwsBARETkeA0srrlZwSjMREZGjMbC04kolpzQTERE5GgNLK0p5SYiIiMjhGFhaYDAKZBeVAwB012phMAoHV0RERNQ1MbA0IyWrAGNW70dGbgkA4IPD5zBm9X6kZBU4uDIiIqKup02BZe3atQgODoZKpUJUVBQyMzNbbP/xxx9j0KBBUKlUGDJkCPbu3Wu2XwiBlStXwt/fH+7u7oiJiUF2dnZbSusQKVkFWLz5GAp0VWbbtboqLN58jKGFiIjIzqwOLB999BESEhKQlJSEY8eOISIiArGxsSgqKmqy/TfffIPZs2dj4cKFOH78OOLj4xEfH4+srCxTmxdffBGvv/461q9fjyNHjsDDwwOxsbGoqqpq8pi2ZDAKrNp9Ck1d/GnYtmr3KV4eIiIisiOZEMKqb96oqCjcddddePPNNwEARqMRgYGBWLp0KZYvX96o/cyZM1FRUYHPPvvMtO3uu+/GsGHDsH79egghEBAQgMcffxxPPPEEAECn08HPzw8bN27ErFmzGh2zuroa1dXVpud6vR6BgYHQ6XTw8vKypjuNZOSWYPY7h1ttt+XhuxHdr2e73ouIiKgr0+v1UKvVFn1/W3WGpaamBkePHkVMTMyNA8jliImJQUZGRpOvycjIMGsPALGxsab2eXl50Gq1Zm3UajWioqKaPWZycjLUarXpERgYaE03WlRUZtlZHUvbERERUftZFViKi4thMBjg5+dntt3Pzw9arbbJ12i12hbbN/xqzTETExOh0+lMj/z8fGu60SJfT1WHtiMiIqL2c3F0AW2hVCqhVCptcuzIEG/4q1XQ6qqaHMciA6BRqxAZ4m2T9yciIqLGrDrD4uPjA4VCgcLCQrPthYWF0Gg0Tb5Go9G02L7hV2uOaUsKuQxJ08IA1IeTmzU8T5oWBoWcd24mIiKyF6sCi5ubG0aMGIHU1FTTNqPRiNTUVERHRzf5mujoaLP2ALBv3z5T+5CQEGg0GrM2er0eR44cafaYthYX7o91c4ZDoza/7KNRq7BuznDEhfs7pC4iIqKuyupLQgkJCZg3bx5GjhyJyMhIrFmzBhUVFViwYAEAYO7cuejduzeSk5MBAI899hjGjRuHf/zjH5g6dSq2bt2Kb7/9Fm+//TYAQCaTYdmyZXj++ecRGhqKkJAQrFixAgEBAYiPj++4nlopLtwfk8I0yMy7gqKyKvh61l8G4pkVIiIi+7M6sMycOROXL1/GypUrodVqMWzYMKSkpJgGzZ4/fx5y+Y0TN6NGjcKHH36IZ555Bn/5y18QGhqKnTt3Ijw83NTmqaeeQkVFBR555BGUlpZizJgxSElJgUrl2IGtCrmMU5eJiIgkwOp1WKTImnncREREJA02W4eFiIiIyBEYWIiIiEjyGFiIiIhI8hhYiIiISPIYWIiIiEjyGFiIiIhI8hhYiIiISPIYWIiIiEjyOuXdmm/VsPadXq93cCVERERkqYbvbUvWsHWKwFJWVgYACAwMdHAlREREZK2ysjKo1eoW2zjF0vxGoxGXLl2Cp6cnZLKOvTmhXq9HYGAg8vPznXLZf2fvH+D8fWT/Oj9n76Oz9w9w/j7aqn9CCJSVlSEgIMDsPoRNcYozLHK5HH369LHpe3h5eTnlD2EDZ+8f4Px9ZP86P2fvo7P3D3D+Ptqif62dWWnAQbdEREQkeQwsREREJHkMLK1QKpVISkqCUql0dCk24ez9A5y/j+xf5+fsfXT2/gHO30cp9M8pBt0SERGRc+MZFiIiIpI8BhYiIiKSPAYWIiIikjwGFiIiIpI8BhYiIiKSvC4ZWNauXYvg4GCoVCpERUUhMzOzxfYff/wxBg0aBJVKhSFDhmDv3r1m+4UQWLlyJfz9/eHu7o6YmBhkZ2fbsgstsqZ/77zzDsaOHYsePXqgR48eiImJadR+/vz5kMlkZo+4uDhbd6NZ1vRv48aNjWpXqVRmbaT2+QHW9XH8+PGN+iiTyTB16lRTGyl9hunp6Zg2bRoCAgIgk8mwc+fOVl+TlpaG4cOHQ6lUon///ti4cWOjNtb+vbYVa/u3Y8cOTJo0Cb169YKXlxeio6PxxRdfmLV59tlnG31+gwYNsmEvmmdt/9LS0pr8+dRqtWbtpPL5Adb3sam/XzKZDIMHDza1kdJnmJycjLvuuguenp7w9fVFfHw8zpw50+rrHP1d2OUCy0cffYSEhAQkJSXh2LFjiIiIQGxsLIqKipps/80332D27NlYuHAhjh8/jvj4eMTHxyMrK8vU5sUXX8Trr7+O9evX48iRI/Dw8EBsbCyqqqrs1S0Ta/uXlpaG2bNn48CBA8jIyEBgYCAmT56MixcvmrWLi4tDQUGB6bFlyxZ7dKcRa/sH1C8lfXPt586dM9svpc8PsL6PO3bsMOtfVlYWFAoFHnjgAbN2UvkMKyoqEBERgbVr11rUPi8vD1OnTsWECRNw4sQJLFu2DIsWLTL7Um/Lz4WtWNu/9PR0TJo0CXv37sXRo0cxYcIETJs2DcePHzdrN3jwYLPP79ChQ7Yov1XW9q/BmTNnzOr39fU17ZPS5wdY38fXXnvNrG/5+fnw9vZu9HdQKp/hwYMHsWTJEhw+fBj79u1DbW0tJk+ejIqKimZfI4nvQtHFREZGiiVLlpieGwwGERAQIJKTk5tsP2PGDDF16lSzbVFRUeL3v/+9EEIIo9EoNBqNeOmll0z7S0tLhVKpFFu2bLFBD1pmbf9uVVdXJzw9PcX7779v2jZv3jwxffr0ji61Tazt33vvvSfUanWzx5Pa5ydE+z/DV199VXh6eory8nLTNil9hjcDID799NMW2zz11FNi8ODBZttmzpwpYmNjTc/b+2dmK5b0rylhYWFi1apVpudJSUkiIiKi4wrrIJb078CBAwKAuHr1arNtpPr5CdG2z/DTTz8VMplMnD171rRNqp+hEEIUFRUJAOLgwYPNtpHCd2GXOsNSU1ODo0ePIiYmxrRNLpcjJiYGGRkZTb4mIyPDrD0AxMbGmtrn5eVBq9WatVGr1YiKimr2mLbSlv7dqrKyErW1tfD29jbbnpaWBl9fXwwcOBCLFy9GSUlJh9Zuibb2r7y8HEFBQQgMDMT06dNx8uRJ0z4pfX5Ax3yGGzZswKxZs+Dh4WG2XQqfYVu09newI/7MpMRoNKKsrKzR38Hs7GwEBASgb9+++N3vfofz5887qMK2GTZsGPz9/TFp0iR8/fXXpu3O9vkB9X8HY2JiEBQUZLZdqp+hTqcDgEY/czeTwndhlwosxcXFMBgM8PPzM9vu5+fX6HpqA61W22L7hl+tOaattKV/t3r66acREBBg9kMXFxeHTZs2ITU1FatXr8bBgwcxZcoUGAyGDq2/NW3p38CBA/Huu+9i165d2Lx5M4xGI0aNGoULFy4AkNbnB7T/M8zMzERWVhYWLVpktl0qn2FbNPd3UK/X49q1ax3ycy8lL7/8MsrLyzFjxgzTtqioKGzcuBEpKSlYt24d8vLyMHbsWJSVlTmwUsv4+/tj/fr12L59O7Zv347AwECMHz8ex44dA9Ax/25JyaVLl/D55583+jso1c/QaDRi2bJlGD16NMLDw5ttJ4XvQpcOOQo5hRdeeAFbt25FWlqa2cDUWbNmmX4/ZMgQDB06FP369UNaWhomTpzoiFItFh0djejoaNPzUaNG4Y477sBbb72F5557zoGV2caGDRswZMgQREZGmm3vzJ9hV/Lhhx9i1apV2LVrl9kYjylTpph+P3ToUERFRSEoKAjbtm3DwoULHVGqxQYOHIiBAweano8aNQq5ubl49dVX8cEHHziwMtt4//330b17d8THx5ttl+pnuGTJEmRlZTlsPI01utQZFh8fHygUChQWFpptLywshEajafI1Go2mxfYNv1pzTFtpS/8avPzyy3jhhRfw5ZdfYujQoS227du3L3x8fJCTk9Pumq3Rnv41cHV1xZ133mmqXUqfH9C+PlZUVGDr1q0W/ePnqM+wLZr7O+jl5QV3d/cO+bmQgq1bt2LRokXYtm1bo1Pvt+revTsGDBjQKT6/pkRGRppqd5bPD6ifJfPuu+/iwQcfhJubW4ttpfAZPvroo/jss89w4MAB9OnTp8W2Uvgu7FKBxc3NDSNGjEBqaqppm9FoRGpqqtn/wm8WHR1t1h4A9u3bZ2ofEhICjUZj1kav1+PIkSPNHtNW2tI/oH5k93PPPYeUlBSMHDmy1fe5cOECSkpK4O/v3yF1W6qt/buZwWDADz/8YKpdSp8f0L4+fvzxx6iursacOXNafR9HfYZt0drfwY74uXC0LVu2YMGCBdiyZYvZdPTmlJeXIzc3t1N8fk05ceKEqXZn+PwaHDx4EDk5ORb9p8GRn6EQAo8++ig+/fRT7N+/HyEhIa2+RhLfhR0ydLcT2bp1q1AqlWLjxo3i1KlT4pFHHhHdu3cXWq1WCCHEgw8+KJYvX25q//XXXwsXFxfx8ssvi9OnT4ukpCTh6uoqfvjhB1ObF154QXTv3l3s2rVLfP/992L69OkiJCREXLt2TfL9e+GFF4Sbm5v45JNPREFBgelRVlYmhBCirKxMPPHEEyIjI0Pk5eWJr776SgwfPlyEhoaKqqoqyfdv1apV4osvvhC5ubni6NGjYtasWUKlUomTJ0+a2kjp8xPC+j42GDNmjJg5c2aj7VL7DMvKysTx48fF8ePHBQDxyiuviOPHj4tz584JIYRYvny5ePDBB03tf/75Z9GtWzfx5JNPitOnT4u1a9cKhUIhUlJSTG1a+zOTcv/+/e9/CxcXF7F27Vqzv4OlpaWmNo8//rhIS0sTeXl54uuvvxYxMTHCx8dHFBUVSb5/r776qti5c6fIzs4WP/zwg3jssceEXC4XX331lamNlD4/IazvY4M5c+aIqKioJo8ppc9w8eLFQq1Wi7S0NLOfucrKSlMbKX4XdrnAIoQQb7zxhrj99tuFm5ubiIyMFIcPHzbtGzdunJg3b55Z+23btokBAwYINzc3MXjwYLFnzx6z/UajUaxYsUL4+fkJpVIpJk6cKM6cOWOPrjTJmv4FBQUJAI0eSUlJQgghKisrxeTJk0WvXr2Eq6urCAoKEg8//LDD/iERwrr+LVu2zNTWz89P/OIXvxDHjh0zO57UPj8hrP8Z/fHHHwUA8eWXXzY6ltQ+w4Zprrc+Gvo0b948MW7cuEavGTZsmHBzcxN9+/YV7733XqPjtvRnZk/W9m/cuHEttheifhq3v7+/cHNzE7179xYzZ84UOTk59u3Yddb2b/Xq1aJfv35CpVIJb29vMX78eLF///5Gx5XK5ydE235GS0tLhbu7u3j77bebPKaUPsOm+gbA7O+VFL8LZdeLJyIiIpKsLjWGhYiIiDonBhYiIiKSPAYWIiIikjwGFiIiIpI8BhYiIiKSPAYWIiIikjwGFiIiIpI8BhYiIiKSPAYWIiIikjwGFiIiIpI8BhYiIiKSvP8Hqni7+4Gj9K0AAAAASUVORK5CYII=",
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}')
--------------------------------------------------------------------------------