77c = 0 # Marginal cost
88l = 10 # Length of binary string (chromosomes)
99T = 1000 # Max iterations
10-
11- # Student ID logic for probabilities
1210Y = 1 # Second to last digit of ID
1311Z = 3 # Last digit of ID
1412
15- p_cross = 0.200 + (0.01 * Y ) + (0.001 * Z )
16- p_mut = 0.00200 + (0.0001 * Y ) + (0.00001 * Z )
13+ p_cross_bench = 0.200 + (0.01 * Y ) + (0.001 * Z )
14+ p_mut_bench = 0.00200 + (0.0001 * Y ) + (0.00001 * Z )
1715
1816# Theoretical Values
19- Q_cournot_total = (n / (n + 1 )) * (alpha - c )
20- q_cournot_indiv = Q_cournot_total / n
17+ Q_nash_total = (n / (n + 1 )) * (alpha - c )
18+ q_nash_indiv = Q_nash_total / n
2119
2220
2321def BinaryToDecimal (binary_array ):
@@ -29,14 +27,14 @@ def BinaryToDecimal(binary_array):
2927
3028def BinaryToOutput (binary_array ):
3129 x_it = BinaryToDecimal (binary_array )
32- q_i = (alpha / n ) * (x_it / (2 ** l )) ### I am not sure about this scaling
30+ q_i = alpha * (
31+ x_it / (2 ** l )
32+ ) # Scale the integer [0, 1023] to output (Note: 2^10 = 1024. Max x_it is 1023.)
3333 return q_i
3434
3535
3636def CalculatePrice (total_Q ):
3737 price = alpha - total_Q
38- if price < 0 :
39- price = 0
4038 return price
4139
4240
@@ -52,9 +50,14 @@ def Variance(values):
5250 return variance
5351
5452
55- def run_simulation (p_cross , p_mut ):
56- Population = []
53+ def run_simulation (p_cross , p_mut , scenario_name ):
54+ print (f"\n --- Running Scenario: { scenario_name } ---" )
55+ print (f"p_cross: { p_cross :.5f} , p_mut: { p_mut :.5f} " )
5756
57+ random .seed (42 )
58+ np .random .seed (42 )
59+
60+ Population = []
5861 for i in range (n ):
5962 # Generate random binary string of length l
6063 genes = [random .randint (0 , 1 ) for _ in range (l )]
@@ -65,57 +68,51 @@ def run_simulation(p_cross, p_mut):
6568 variance_history = []
6669
6770 for t in range (1 , T + 1 ):
68-
6971 outputs_list = []
7072 total_Q = 0
7173
74+ # Calculate outputs for current population
7275 for firm in Population :
7376 q_i = BinaryToOutput (firm )
7477 outputs_list .append (q_i )
7578 total_Q += q_i
7679
7780 current_price = CalculatePrice (total_Q )
7881
79- # Check for zero-profit trap: if price is 0, restart population
80- if current_price <= 0 :
81- Population = []
82- for i in range (n ):
83- genes = [random .randint (0 , 1 ) for _ in range (l )]
84- Population .append (genes )
85- continue
86-
8782 # Store stats
8883 total_Q_history .append (total_Q )
8984 variance_history .append (Variance (outputs_list ))
9085
91- # A. RECOMBINATION
92- # Calculate fitness scores (profits) for all firms
86+ # A. RECOMBINATION (Roulette Wheel Selection)
9387 fitness_scores = []
9488 for firm in Population :
9589 q_i = BinaryToOutput (firm )
9690 profit = CalculateProfit (q_i , current_price )
9791 fitness_scores .append (profit )
9892
99- # Check if we can use fitness-based selection
93+ # Handle negative fitness for probability calculation
10094 min_fitness = min (fitness_scores )
101- total_fitness = sum (fitness_scores )
95+ if min_fitness < 0 :
96+ # Shift all fitness values to be positive
97+ offset = - min_fitness
98+ # Add a small epsilon to ensure non-zero probabilities if all are equal
99+ fitness_scores = [f + offset + 1e-6 for f in fitness_scores ]
100+ # But this solution can left useless in a situation where all firms have zero fitness score
102101
103- # If negative fitness or zero total fitness, restart population
104- if min_fitness < 0 or total_fitness <= 0 :
105- Population = []
106- for i in range (n ):
107- genes = [random .randint (0 , 1 ) for _ in range (l )]
108- Population .append (genes )
109- continue
102+ total_fitness = sum (fitness_scores )
110103
111- # Normalize to get probabilities
112- probabilities = [f / total_fitness for f in fitness_scores ]
104+ if total_fitness == 0 :
105+ probabilities = [
106+ 1.0 / n
107+ ] * n # If all firms have zero fitness score, each firm has equal probability of being selected
108+ else :
109+ probabilities = [f / total_fitness for f in fitness_scores ]
113110
114- # Select firms based on their fitness probabilities
111+ # Create mating pool
115112 mating_pool = []
116- for _ in range ( n ):
117- selected_firm_idx = np . random . choice ( len ( Population ), p = probabilities )
118- mating_pool .append (Population [selected_firm_idx ].copy ())
113+ indices = np . random . choice ( len ( Population ), size = n , p = probabilities )
114+ for idx in indices :
115+ mating_pool .append (Population [idx ].copy ())
119116
120117 # B. CROSSOVER
121118 offspring_pool = []
@@ -150,14 +147,14 @@ def run_simulation(p_cross, p_mut):
150147 q_parent = BinaryToOutput (Parent )
151148 q_child = BinaryToOutput (Child )
152149
153- # Calculate Q_others
150+ # Calculate Q_others (aggregate of all others in current generation)
154151 Q_others = total_Q - q_parent
155152
156153 # Parent Profit (Actual)
157- # Uses the actual market price that occurred
158154 profit_parent = CalculateProfit (q_parent , current_price )
159155
160- # Recalculate price if Child were in the market
156+ # Child Profit (Hypothetical)
157+ # Evaluate Child against the *current* behavior of others
161158 new_total_Q = Q_others + q_child
162159 new_price = CalculatePrice (new_total_Q )
163160 profit_child = CalculateProfit (q_child , new_price )
@@ -169,43 +166,46 @@ def run_simulation(p_cross, p_mut):
169166
170167 Population = Next_Generation
171168
172- # ---------------------------------------------------------
173- # RESULTS
174- # ---------------------------------------------------------
175- print (f"\n Simulation completed!" )
176- print (f"Final Total Q: { total_Q_history [- 1 ]} " )
177- print (f"Target Cournot Total Q: { Q_cournot_total :.2f} " )
178- print (f"Final Variance: { variance_history [- 1 ]} " )
169+ # RESULTS & PLOTTING
170+ print (f"Simulation completed!" )
171+ print (f"Final Total Q: { total_Q_history [- 1 ]:.2f} " )
172+ print (f"Target Nash Total Q: { Q_nash_total :.2f} " )
173+ print (f"Final Variance: { variance_history [- 1 ]:.2f} " )
179174
175+ # Plotting
180176 plt .figure (figsize = (12 , 5 ))
181177
178+ # Plot 1: Total Quantity
182179 plt .subplot (1 , 2 , 1 )
183- plt .plot (total_Q_history , label = "Actual Total Q" )
184- plt .axhline (y = Q_cournot_total , color = "r" , linestyle = "--" , label = "Cournot Eq " )
180+ plt .plot (total_Q_history , label = "Actual Total Q" , color = "blue" )
181+ plt .axhline (y = Q_nash_total , color = "r" , linestyle = "--" , label = "Nash Equilibrium " )
185182 plt .xlabel ("Iteration" )
186183 plt .ylabel ("Total Quantity (Q)" )
187- plt .title ("Total Quantity Convergence" )
184+ plt .title (f"Industry Output Convergence\n ( { scenario_name } ) " )
188185 plt .legend ()
189- plt .grid (True )
186+ plt .grid (True , alpha = 0.3 )
190187
188+ # Plot 2: Variance
191189 plt .subplot (1 , 2 , 2 )
192- plt .plot (variance_history )
190+ plt .plot (variance_history , color = "green" )
193191 plt .xlabel ("Iteration" )
194- plt .ylabel ("Variance of Outputs " )
195- plt .title ("Variance Over Time " )
196- plt .grid (True )
192+ plt .ylabel ("Variance" )
193+ plt .title (f"Population Variance \n ( { scenario_name } ) " )
194+ plt .grid (True , alpha = 0.3 )
197195
198196 plt .tight_layout ()
199- plt .savefig ("genetic_algorithm_results.png" )
200- plt .show ()
197+ filename = f"results_{ scenario_name .replace (' ' , '_' )} .png"
198+ plt .savefig (f"results/{ filename } " )
199+ print (f"Saved plot to results/{ filename } " )
200+ plt .close ()
201201
202202
203203if __name__ == "__main__" :
204- # Scenario 1: Benchmark
205- run_simulation (p_cross , p_mut )
204+ # (ii) Benchmark Scenario
205+ run_simulation (p_cross_bench , p_mut_bench , "Benchmark" )
206206
207- # Scenario 2: High Crossover (+0.250)
208- run_simulation (p_cross + 0.250 , p_mut )
207+ # (iii) High Crossover (+0.250)
208+ run_simulation (p_cross_bench + 0.250 , p_mut_bench , "High_Crossover" )
209209
210- # Scenario 3: High Mutation (+0.00250)
211- run_simulation (p_cross , p_mut + 0.00250 )
210+ # (iv) High Mutation (+0.00250)
211+ run_simulation (p_cross_bench , p_mut_bench + 0.00250 , "High_Mutation" )
0 commit comments