fix: nsga-II is complete the only remaining step is TOPSIS decision-making
This commit is contained in:
parent
57c23f290f
commit
c47071bc2d
@ -9,7 +9,7 @@ import time
|
||||
|
||||
|
||||
class MultiObjectiveGeneticAlgorithm:
|
||||
def __init__(self, population_size=100, generations=50, crossover_rate=0.9, mutation_rate=0.1,
|
||||
def __init__(self, population_size=50, generations=50, crossover_rate=0.9, mutation_rate=0.33,
|
||||
optimization_scenario=None, output_path=None):
|
||||
self.population_size = population_size
|
||||
self.population = []
|
||||
@ -23,13 +23,9 @@ class MultiObjectiveGeneticAlgorithm:
|
||||
self.crowding_distances = None
|
||||
self.output_path = output_path
|
||||
|
||||
# Initialize Population
|
||||
# Initialize Population
|
||||
def initialize_population(self, building, energy_system):
|
||||
design_period_start_time = time.time()
|
||||
design_period_energy_demands = self.design_period_identification(building)
|
||||
design_period_time = time.time() - design_period_start_time
|
||||
print(f"design period identification took {design_period_time:.2f} seconds")
|
||||
initializing_time_start = time.time()
|
||||
attempts = 0
|
||||
max_attempts = self.population_size * 20
|
||||
while len(self.population) < self.population_size and attempts < max_attempts:
|
||||
@ -45,8 +41,7 @@ class MultiObjectiveGeneticAlgorithm:
|
||||
if len(self.population) < self.population_size:
|
||||
raise RuntimeError(f"Could not generate a feasible population of size {self.population_size}. "
|
||||
f"Only {len(self.population)} feasible individuals were generated.")
|
||||
initializing_time = time.time() - initializing_time_start
|
||||
print(f"initializing took {initializing_time:.2f} seconds")
|
||||
|
||||
|
||||
@staticmethod
|
||||
def design_period_identification(building):
|
||||
@ -91,7 +86,7 @@ class MultiObjectiveGeneticAlgorithm:
|
||||
eta_c = 2 # Distribution index for SBX
|
||||
if random.random() < self.crossover_rate:
|
||||
child1, child2 = copy.deepcopy(parent1), copy.deepcopy(parent2)
|
||||
# Crossover for Generation Components
|
||||
# Crossover for Generation Components
|
||||
for i in range(len(parent1.individual['Generation Components'])):
|
||||
for cap_type in ['heating_capacity', 'cooling_capacity']:
|
||||
if parent1.individual['Generation Components'][i][cap_type] is not None:
|
||||
@ -108,7 +103,7 @@ class MultiObjectiveGeneticAlgorithm:
|
||||
0.5 * ((1 + beta) * x1 + (1 - beta) * x2))
|
||||
child2.individual['Generation Components'][i][cap_type] = max(0.1,
|
||||
0.5 * ((1 - beta) * x1 + (1 + beta) * x2))
|
||||
# Crossover for Energy Storage Components
|
||||
# Crossover for Energy Storage Components
|
||||
for i in range(len(parent1.individual['Energy Storage Components'])):
|
||||
for item in ['capacity', 'volume', 'heating_coil_capacity']:
|
||||
if parent1.individual['Energy Storage Components'][i][item] is not None:
|
||||
@ -133,24 +128,19 @@ class MultiObjectiveGeneticAlgorithm:
|
||||
def polynomial_mutation(self, individual, building, energy_system):
|
||||
"""
|
||||
Mutates the individual's generation and storage components using the Polynomial Mutation Operator.
|
||||
|
||||
- `individual`: The individual to mutate (contains generation and storage components).
|
||||
- `building`: Building data that contains constraints such as peak heating load and available space.
|
||||
|
||||
Returns the mutated individual.
|
||||
"""
|
||||
eta_m = 20 # Mutation distribution index (can be adjusted)
|
||||
design_period_energy_demands = self.design_period_identification(building)
|
||||
|
||||
def polynomial_mutation_operator(value, lower_bound, upper_bound):
|
||||
delta = (value - lower_bound) / (upper_bound - lower_bound)
|
||||
u = random.random()
|
||||
|
||||
if u < 0.5:
|
||||
delta_q = (2 * u) ** (1 / (eta_m + 1)) - 1
|
||||
else:
|
||||
delta_q = 1 - (2 * (1 - u)) ** (1 / (eta_m + 1))
|
||||
|
||||
mutated_value = value + delta_q * (upper_bound - lower_bound)
|
||||
return max(lower_bound, min(mutated_value, upper_bound)) # Ensure it's within bounds
|
||||
|
||||
@ -159,27 +149,26 @@ class MultiObjectiveGeneticAlgorithm:
|
||||
if random.random() < self.mutation_rate:
|
||||
if (generation_component['nominal_heating_efficiency'] is not None and
|
||||
(cte.HEATING or cte.DOMESTIC_HOT_WATER in energy_system.demand_types)):
|
||||
# Mutate heating capacity
|
||||
# Mutate heating capacity
|
||||
if cte.HEATING in energy_system.demand_types:
|
||||
max_demand = max(design_period_energy_demands[cte.HEATING]['demands']) / cte.WATTS_HOUR_TO_JULES
|
||||
else:
|
||||
max_demand = max(design_period_energy_demands[cte.DOMESTIC_HOT_WATER]['demands']) / cte.WATTS_HOUR_TO_JULES
|
||||
|
||||
generation_component['heating_capacity'] = polynomial_mutation_operator(
|
||||
generation_component['heating_capacity'], 0, max_demand)
|
||||
|
||||
generation_component['heating_capacity'], 0.1, max_demand)
|
||||
if generation_component['nominal_cooling_efficiency'] is not None and cte.COOLING in energy_system.demand_types:
|
||||
# Mutate cooling capacity
|
||||
# Mutate cooling capacity
|
||||
max_cooling_demand = max(design_period_energy_demands[cte.COOLING]['demands']) / cte.WATTS_HOUR_TO_JULES
|
||||
generation_component['cooling_capacity'] = polynomial_mutation_operator(
|
||||
generation_component['cooling_capacity'], 0, max_cooling_demand)
|
||||
generation_component['cooling_capacity'], 0.1, max_cooling_demand)
|
||||
|
||||
# Mutate Storage Components
|
||||
for storage_component in individual['Energy Storage Components']:
|
||||
if random.random() < self.mutation_rate:
|
||||
if storage_component['type'] == f'{cte.THERMAL}_storage':
|
||||
# Mutate the volume of thermal storage
|
||||
# Mutate the volume of thermal storage
|
||||
max_available_space = 0.01 * building.volume / building.storeys_above_ground
|
||||
lower_bound = random.uniform(0, 0.001) * max_available_space
|
||||
storage_component['volume'] = polynomial_mutation_operator(
|
||||
storage_component['volume'], 0, max_available_space)
|
||||
|
||||
@ -229,8 +218,8 @@ class MultiObjectiveGeneticAlgorithm:
|
||||
|
||||
def calculate_crowding_distance(self, front, crowding_distance):
|
||||
for j in range(len(self.population[0].fitness_score)):
|
||||
|
||||
sorted_front = sorted(front, key=lambda x: self.population[x].individual['fitness_score'][j])
|
||||
print(sorted_front)
|
||||
# Set distances to finite large numbers rather than `inf`
|
||||
crowding_distance[sorted_front[0]] = float(1e12)
|
||||
crowding_distance[sorted_front[-1]] = float(1e12)
|
||||
@ -242,6 +231,8 @@ class MultiObjectiveGeneticAlgorithm:
|
||||
(self.population[sorted_front[i + 1]].individual['fitness_score'][j] -
|
||||
self.population[sorted_front[i - 1]].individual['fitness_score'][j]) /
|
||||
(objective_max - objective_min))
|
||||
for i in front:
|
||||
self.population[i].crowding_distance = crowding_distance[i]
|
||||
return crowding_distance
|
||||
|
||||
@staticmethod
|
||||
@ -249,14 +240,11 @@ class MultiObjectiveGeneticAlgorithm:
|
||||
# Extract the list of objectives for both individuals
|
||||
objectives1 = individual1.individual['fitness_score']
|
||||
objectives2 = individual2.individual['fitness_score']
|
||||
|
||||
# Ensure both individuals have the same number of objectives
|
||||
assert len(objectives1) == len(objectives2), "Both individuals must have the same number of objectives"
|
||||
|
||||
# Flags to check if one dominates the other
|
||||
better_in_all = True # Whether individual1 is better in all objectives
|
||||
strictly_better_in_at_least_one = False # Whether individual1 is strictly better in at least one objective
|
||||
|
||||
for obj1, obj2 in zip(objectives1, objectives2):
|
||||
if obj1 > obj2:
|
||||
better_in_all = False # individual1 is worse in at least one objective
|
||||
@ -265,43 +253,80 @@ class MultiObjectiveGeneticAlgorithm:
|
||||
result = True if better_in_all and strictly_better_in_at_least_one else False
|
||||
return result
|
||||
|
||||
def solve_ga(self, building, energy_system):
|
||||
self.initialize_population(building, energy_system)
|
||||
progeny_population = []
|
||||
progeny_creation_time_start = time.time()
|
||||
while len(progeny_population) < self.population_size:
|
||||
parent1, parent2 = random.choice(self.population), random.choice(self.population)
|
||||
child1, child2 = self.sbx_crossover(parent1, parent2)
|
||||
self.polynomial_mutation(child1.individual, building, energy_system)
|
||||
self.polynomial_mutation(child2.individual, building, energy_system)
|
||||
child1.score_evaluation()
|
||||
child2.score_evaluation()
|
||||
progeny_population.extend([child1, child2])
|
||||
progeny_creation_duration = time.time() - progeny_creation_time_start
|
||||
print(f"progeny population creation took {progeny_creation_duration:.2f} seconds")
|
||||
self.population.extend(progeny_population)
|
||||
print([ind.fitness_score for ind in self.population])
|
||||
ns_start_time = time.time()
|
||||
fronts = self.fast_non_dominated_sorting()
|
||||
ns_duration = time.time() - ns_start_time
|
||||
print(f"non-dominated sorting took {ns_duration:.2f} seconds")
|
||||
self.crowding_distances = [0] * len(self.population)
|
||||
crowding_d_start_time = time.time()
|
||||
for front in fronts:
|
||||
print(front)
|
||||
self.crowding_distance = self.calculate_crowding_distance(front=front, crowding_distance=self.crowding_distances)
|
||||
print(self.crowding_distance)
|
||||
crowding_duration = time.time() - crowding_d_start_time
|
||||
print(f"crowding d calculation took {crowding_duration:.2f} seconds")
|
||||
self.plot_pareto_front(self.population)
|
||||
def nsga2_selection(self, fronts):
|
||||
new_population = []
|
||||
i = 0
|
||||
while len(new_population) + len(fronts[i]) <= self.population_size:
|
||||
for index in fronts[i]:
|
||||
# Skip individuals with infinite fitness values to avoid propagation
|
||||
if not math.isinf(self.population[index].individual['fitness_score'][0]) and \
|
||||
not math.isinf(self.population[index].individual['fitness_score'][1]):
|
||||
new_population.append(self.population[index])
|
||||
i += 1
|
||||
if i >= len(fronts):
|
||||
break
|
||||
if len(new_population) < self.population_size and i < len(fronts):
|
||||
sorted_front = sorted(fronts[i], key=lambda x: self.population[x].crowding_distance, reverse=True)
|
||||
for index in sorted_front:
|
||||
if len(new_population) < self.population_size:
|
||||
if not math.isinf(self.population[index].individual['fitness_score'][0]) and \
|
||||
not math.isinf(self.population[index].individual['fitness_score'][1]):
|
||||
new_population.append(self.population[index])
|
||||
else:
|
||||
break
|
||||
return new_population
|
||||
|
||||
@staticmethod
|
||||
def plot_pareto_front(pareto_population):
|
||||
# Extract LCC and LCE for plotting
|
||||
lcc_values = [individual.individual['lcc'] for individual in pareto_population]
|
||||
lce_values = [individual.individual['total_energy_consumption'] for individual in pareto_population]
|
||||
def solve_ga(self, building, energy_system):
|
||||
optimization_start = time.time()
|
||||
self.initialize_population(building, energy_system)
|
||||
for n in range(self.generations + 1):
|
||||
generation_n_start_time = time.time()
|
||||
print(f"Generation {n}")
|
||||
progeny_population = []
|
||||
while len(progeny_population) < self.population_size:
|
||||
parent1, parent2 = random.choice(self.population), random.choice(self.population)
|
||||
child1, child2 = self.sbx_crossover(parent1, parent2)
|
||||
self.polynomial_mutation(child1.individual, building, energy_system)
|
||||
self.polynomial_mutation(child2.individual, building, energy_system)
|
||||
child1.score_evaluation()
|
||||
child2.score_evaluation()
|
||||
progeny_population.extend([child1, child2])
|
||||
self.population.extend(progeny_population)
|
||||
fronts = self.fast_non_dominated_sorting()
|
||||
print([self.population[ind].fitness_score for ind in fronts[0]])
|
||||
print(fronts)
|
||||
crowding_distances = [0] * len(self.population)
|
||||
for front in fronts:
|
||||
self.calculate_crowding_distance(front=front, crowding_distance=crowding_distances)
|
||||
new_population = self.nsga2_selection(fronts=fronts)
|
||||
self.population = new_population
|
||||
generation_n_calc_duration = time.time() - generation_n_start_time
|
||||
print(f"Generation {n:.2f} took {generation_n_calc_duration:.2f} seconds")
|
||||
|
||||
if n == self.generations:
|
||||
fronts = self.fast_non_dominated_sorting()
|
||||
print(fronts)
|
||||
self.plot_pareto_front(fronts)
|
||||
optimization_duration = time.time() - optimization_start
|
||||
print(f"NSGA-II took {optimization_duration:.2f} seconds")
|
||||
|
||||
def plot_pareto_front(self, fronts):
|
||||
# Extract LCC and LCE for plotting all individuals
|
||||
lcc_values = [individual.individual['lcc'] for individual in self.population]
|
||||
lce_values = [individual.individual['total_energy_consumption'] for individual in self.population]
|
||||
# Plot all individuals as blue points
|
||||
plt.figure(figsize=(10, 6))
|
||||
plt.scatter(lcc_values, lce_values, color='blue', label='Pareto Front', alpha=0.6, edgecolors='w', s=80)
|
||||
plt.scatter(lcc_values, lce_values, color='blue', label='Population', alpha=0.6, edgecolors='w', s=80)
|
||||
# Extract and sort LCC and LCE for individuals in the first front (front_0) to ensure a connected line
|
||||
front_0 = [self.population[i] for i in fronts[0]]
|
||||
front_0_lcc = [individual.individual['lcc'] for individual in front_0]
|
||||
front_0_lce = [individual.individual['total_energy_consumption'] for individual in front_0]
|
||||
# Sort front_0 individuals by LCC or LCE to connect them in order (optional step for smooth line)
|
||||
sorted_front_0 = sorted(zip(front_0_lcc, front_0_lce), key=lambda x: x[0])
|
||||
sorted_lcc, sorted_lce = zip(*sorted_front_0)
|
||||
# Plot the Pareto front (front_0) as a red line
|
||||
plt.plot(sorted_lcc, sorted_lce, color='red', label='Pareto Front (Front 0)', linewidth=2)
|
||||
# Configure plot details
|
||||
plt.title('Pareto Front for Life Cycle Cost vs Life Cycle Energy')
|
||||
plt.xlabel('Life Cycle Cost (LCC)')
|
||||
plt.ylabel('Life Cycle Energy (LCE)')
|
||||
@ -309,6 +334,3 @@ class MultiObjectiveGeneticAlgorithm:
|
||||
plt.legend()
|
||||
plt.show()
|
||||
|
||||
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user