feat: single objective optimization finished

This commit is contained in:
Saeed Ranjbar 2024-09-27 16:59:09 -04:00
parent 07cdfc4bf7
commit f7815dc4c0
2 changed files with 159 additions and 36 deletions

View File

@ -10,55 +10,178 @@ from energy_system_modelling_package.energy_system_modelling_factories.system_si
class GeneticAlgorithm:
def __init__(self, population_size=100, generations=50, crossover_rate=0.8, mutation_rate=0.1,
def __init__(self, population_size=50, generations=50, crossover_rate=0.8, mutation_rate=0.1,
optimization_scenario='cost', output_path=None):
self.population_size = population_size
self.population = []
self.generations = generations
self.crossover_rate = crossover_rate
self.mutation_rate = mutation_rate
self.optimization_scenario = optimization_scenario
self.list_of_solutions = []
self.best_solution = None
self.best_solution_generation = None
self.output_path = output_path
def energy_system_archetype_optimal_sizing(self):
pass
# Initialize Population
def initialize_population(self, building, energy_system, heating_design_load=None, cooling_design_load=None,
available_space=None, dt=1800, fuel_price_index=0.05, electricity_tariff_type='fixed',
consumer_price_index=0.04, interest_rate=0.04, discount_rate=0.03, percentage_credit=0,
credit_years=15):
def initialize_population(self, building, energy_system):
"""
The population to optimize the sizes of generation and storage components of any energy system are optimized.
The initial population will be a list of individuals where each of them represent a possible solution to the
problem.
:param building:
:param energy_system:
:return: List
"""
population = []
for i in range(self.population_size):
population.append(Individual(building, energy_system, self.optimization_scenario,
heating_design_load=heating_design_load, cooling_design_load=cooling_design_load,
available_space=available_space, dt=dt, fuel_price_index=fuel_price_index,
electricity_tariff_type=electricity_tariff_type,
consumer_price_index=consumer_price_index, interest_rate=interest_rate,
discount_rate=discount_rate, percentage_credit=percentage_credit,
credit_years=credit_years))
return population
self.population.append(Individual(building, energy_system, self.optimization_scenario))
def order_population(self):
"""
ordering the population based on the fitness score in ascending order
:return:
"""
self.population = sorted(self.population, key=lambda x: x.individual['fitness_score'])
def tournament_selection(self):
selected = []
for _ in range(len(self.population)):
i, j = random.sample(range(self.population_size), 2)
if self.population[i].individual['fitness_score'] < self.population[j].individual['fitness_score']:
selected.append(copy.deepcopy(self.population[i]))
else:
selected.append(copy.deepcopy(self.population[j]))
return selected
def crossover(self, parent1, parent2):
"""
Crossover between two parents to produce two children.
Swaps generation components and storage components between the two parents with a 50% chance.
:param parent1: First parent individual.
:param parent2: Second parent individual.
:return: Two child individuals (child1 and child2).
"""
if random.random() < self.crossover_rate:
# Deep copy of the parents to create children
child1, child2 = copy.deepcopy(parent1), copy.deepcopy(parent2)
# Crossover for Generation Components
for i in range(len(parent1.individual['Generation Components'])):
if random.random() < 0.5:
# Swap the entire generation component
child1.individual['Generation Components'][i], child2.individual['Generation Components'][i] = (
child2.individual['Generation Components'][i],
child1.individual['Generation Components'][i]
)
# Crossover for Energy Storage Components
for i in range(len(parent1.individual['Energy Storage Components'])):
if random.random() < 0.5:
# Swap the entire storage component
child1.individual['Energy Storage Components'][i], child2.individual['Energy Storage Components'][i] = (
child2.individual['Energy Storage Components'][i],
child1.individual['Energy Storage Components'][i]
)
return child1, child2
else:
# If crossover does not happen, return copies of the original parents
return copy.deepcopy(parent1), copy.deepcopy(parent2)
def mutate(self, individual, building):
"""
Mutates the individual's generation and storage components.
- `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.
"""
# Mutate Generation Components
for generation_component in individual['Generation Components']:
if random.random() < self.mutation_rate:
if generation_component['nominal_heating_efficiency'] is not None:
# Mutate heating capacity
generation_component['heating_capacity'] = random.uniform(
0, building.heating_peak_load[cte.YEAR][0])
if generation_component['nominal_cooling_efficiency'] is not None:
# Mutate cooling capacity
generation_component['cooling_capacity'] = random.uniform(
0, building.cooling_peak_load[cte.YEAR][0])
# 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
max_available_space = 0.01 * building.volume / building.storeys_above_ground
storage_component['volume'] = random.uniform(0, max_available_space)
if storage_component['heating_coil_capacity'] is not None:
storage_component['volume'] = random.uniform(0, building.heating_peak_load[cte.YEAR][0])
return individual
def solve_ga(self, building, energy_system):
"""
Solving GA for a single energy system. Here are the steps:
1- Population is initialized using the "initialize_population" method in this class.
:param building:
:param energy_system:
:return:
1. Initialize population using the "initialize_population" method in this class.
2. Evaluate the initial population using the "score_evaluation" method in the Individual class.
3. Sort population based on fitness score.
4. Repeat selection, crossover, and mutation for a fixed number of generations.
5. Track the best solution found during the optimization process.
:param building: Building object for the energy system.
:param energy_system: Energy system to optimize.
:return: Best solution after running the GA.
"""
energy_system = energy_system
best_individuals = []
best_solution = None
population = self.initialize_population(building, energy_system)
for individual in population:
# Step 1: Initialize the population
self.initialize_population(building, energy_system)
# Step 2: Evaluate the initial population
for individual in self.population:
individual.score_evaluation()
print(individual.individual['feasible'])
print(individual.individual['fitness_score'])
# Step 3: Order population based on fitness scores
self.order_population()
# Track the best solution
self.best_solution = self.population[0]
self.list_of_solutions.append(copy.deepcopy(self.best_solution.individual))
# Step 4: Run GA for a fixed number of generations
for generation in range(self.generations):
print(f"Generation {generation}")
# Selection (using tournament selection)
selected_population = self.tournament_selection()
# Create the next generation through crossover and mutation
next_population = []
for i in range(0, self.population_size, 2):
parent1 = selected_population[i]
parent2 = selected_population[i + 1] if (i + 1) < len(selected_population) else selected_population[0]
# Step 5: Apply crossover
child1, child2 = self.crossover(parent1, parent2)
# Step 6: Apply mutation
self.mutate(child1.individual, building)
self.mutate(child2.individual, building)
# Step 7: Evaluate the children
child1.score_evaluation()
child2.score_evaluation()
next_population.extend([child1, child2])
# Replace old population with the new one
self.population = next_population
# Step 8: Sort the new population based on fitness
self.order_population()
# Track the best solution found in this generation
if self.population[0].individual['fitness_score'] < self.best_solution.individual['fitness_score']:
self.best_solution = self.population[0]
self.best_solution_generation = generation
# Store the best solution in the list of solutions
self.list_of_solutions.append(copy.deepcopy(self.population[0]))
print(f"Best solution found in generation {self.best_solution_generation}")
print(f"Best solution: {self.best_solution.individual}")
# Return the best solution after running GA
return self.best_solution

View File

@ -11,10 +11,9 @@ import numpy_financial as npf
class Individual:
def __init__(self, building, energy_system, optimization_scenario, heating_design_load=None,
cooling_design_load=None, available_space=None, dt=None, fuel_price_index=None,
electricity_tariff_type='fixed', consumer_price_index=None, interest_rate=None,
discount_rate=None, percentage_credit=None, credit_years=None):
def __init__(self, building, energy_system, optimization_scenario, heating_design_load=None, cooling_design_load=None,
dt=900, fuel_price_index=0.05, electricity_tariff_type='fixed', consumer_price_index=0.04,
interest_rate=0.04, discount_rate=0.03, percentage_credit=0, credit_years=15):
"""
:param building: building object
:param energy_system: energy system to be optimized
@ -22,8 +21,6 @@ class Individual:
energy consumption, and both together
:param heating_design_load: heating design load in W
:param cooling_design_load: cooling design load in W
:param available_space: available space for the system. currently it is only used to have a constraint on the
maximum size of the storage tank
:param dt the time step size used for simulations
:param fuel_price_index the price increase index of all fuels. A single value is used for all fuels.
:param electricity_tariff_type the electricity tariff type between 'fixed' and 'variable' for economic optimization
@ -35,7 +32,7 @@ class Individual:
self.optimization_scenario = optimization_scenario
self.heating_design_load = heating_design_load
self.cooling_design_load = cooling_design_load
self.available_space = available_space
self.available_space = building.volume / building.storeys_above_ground
self.dt = dt
self.fuel_price_index = fuel_price_index
self.electricity_tariff_type = electricity_tariff_type
@ -145,7 +142,10 @@ class Individual:
if storage_component['type'] == f'{cte.THERMAL}_storage':
storage_component['volume'] = random.uniform(0, 0.01 * self.available_space)
if storage_component['heating_coil_capacity'] is not None:
storage_component['heating_coil_capacity'] = random.uniform(0, self.heating_design_load)
if self.heating_design_load is not None:
storage_component['heating_coil_capacity'] = random.uniform(0, self.heating_design_load)
else:
storage_component['heating_coil_capacity'] = random.uniform(0, self.building.heating_peak_load[cte.YEAR][0])
def score_evaluation(self):
self.initialization()
@ -207,7 +207,7 @@ class Individual:
tes.volume = self.individual['Energy Storage Components'][0]['volume']
tes.height = self.building.average_storey_height - 1
tes.heating_coil_capacity = self.individual['Energy Storage Components'][0]['heating_coil_capacity'] \
if self.individual['Energy Storage Components'][0]['heating_coil_capacity'] is not None else 0
if self.individual['Energy Storage Components'][0]['heating_coil_capacity'] is not None else None
heating_demand_joules = self.building.heating_demand[cte.HOUR]
heating_peak_load_watts = self.heating_design_load if (self.heating_design_load is not
None) else self.building.heating_peak_load[cte.YEAR][0]
@ -230,7 +230,7 @@ class Individual:
tes.volume = self.individual['Energy Storage Components'][0]['volume']
tes.height = self.building.average_storey_height - 1
tes.heating_coil_capacity = self.individual['Energy Storage Components'][0]['heating_coil_capacity'] \
if self.individual['Energy Storage Components'][0]['heating_coil_capacity'] is not None else 0
if self.individual['Energy Storage Components'][0]['heating_coil_capacity'] is not None else None
dhw_demand_joules = self.building.domestic_hot_water_heat_demand[cte.HOUR]
upper_limit_tes = 65
outdoor_temperature = self.building.external_temperature[cte.HOUR]
@ -375,7 +375,7 @@ class Individual:
if generation_system.energy_storage_systems is not None:
for energy_storage_system in generation_system.energy_storage_systems:
if energy_storage_system.type_energy_stored == cte.THERMAL:
if energy_storage_system.heating_coil_capacity is None:
if energy_storage_system.heating_coil_capacity is not None:
investment_cost += float(capital_costs_chapter.item('D306010_storage_tank').initial_investment[0])
reposition_cost += float(capital_costs_chapter.item('D306010_storage_tank').reposition[0])
lifetime += float(capital_costs_chapter.item('D306010_storage_tank').lifetime)