Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Genetic algo v1 #8

Merged
merged 15 commits into from
Feb 4, 2024
98 changes: 98 additions & 0 deletions algos/algo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
from utils.Solution import Solution
from utils.Shape import Shape
from utils.Container import Container
from enum import Enum
import random
random.seed(0)

# Consts
TYPE = "cgshop2024_solution"
NAME = "atris42"
META = {"approach": "generated solution"}
class AlgoClassification(Enum):
RANDOM = "random"
SORT_BY_AREA = "sort_by_area"
SORT_BY_VALUE = "sort_by_value"



class Algo:
def __init__(self, shapes: list[Shape], cont: Container, tries_on_random_creation: int = 100):
self.Shapes = shapes
self.Container = cont
self.ShuffledShapes = self.shuffle_list()
self.SortedbyAreaShapes = self.sort_area()
self.SortedbyValueShapes = self.sort_value()
self.TriesOnRandomCreation = tries_on_random_creation

def sort_value(self) -> list[Shape]:
return sorted(self.Shapes, key=lambda s: s.Value, reverse=True)

def sort_area(self) -> list[Shape]:
return sorted(self.Shapes, key=lambda s: s.get_area())

def shuffle_list(self) -> list[Shape]:
shuffled = self.Shapes[:]
random.shuffle(shuffled)
return shuffled

def find_ranges(self, s: Shape) -> tuple[int, int, int, int]:
min_shape_x = min(s.X_cor)
max_shape_x = max(s.X_cor)
min_shape_y = min(s.Y_cor)
max_shape_y = max(s.Y_cor)

min_container_x = min(self.Container.X_cor)
max_container_x = max(self.Container.X_cor)
min_container_y = min(self.Container.Y_cor)
max_container_y = max(self.Container.Y_cor)

min_x = min_container_x - min_shape_x
max_x = max_container_x - max_shape_x
min_y = min_container_y - min_shape_y
max_y = max_container_y - max_shape_y

return min_x, min_y, max_x, max_y


# 2 variatons of this function, controlled by 'classification' arg:
# 1. "random" - for a random order scan of the shapes list
# 2. "sort by area" - for an increasing area scan of the shapes list
# 3. "sort by value" - for an decreasing value scan of the shapes list
def create_random_offset_solution(self, classification: str) -> Solution:
s = Solution(TYPE, NAME, META, [], [], [], self.Container, self.Shapes)
solution_shapes_list = self.Shapes

if classification == AlgoClassification.RANDOM:
print("Random shapes list")
solution_shapes_list = self.ShuffledShapes
elif classification == AlgoClassification.SORT_BY_AREA:
print("Sorted by area")
solution_shapes_list = self.SortedbyAreaShapes
elif classification == AlgoClassification.SORT_BY_VALUE:
print("Sorted by value")
solution_shapes_list = self.SortedbyValueShapes

for shape in solution_shapes_list:
min_x, min_y, max_x, max_y = self.find_ranges(shape)
print(f"Shape {shape.Index}, Ranges: min_x={min_x}, min_y={min_y}, max_x={max_x}, max_y={max_y}")

for i in range(self.TriesOnRandomCreation):
x_sample = random.randint(min_x, max_x)
y_sample = random.randint(min_y, max_y)
print(f"Trying to place {shape.Index} with Ranges: min_x={min_x}, min_y={min_y}, max_x={max_x}, max_y={max_y} at ({x_sample}, {y_sample})")
s.X_Offset.append(x_sample)
s.Y_Offset.append(y_sample)
s.Items_ID.append(shape.Index)

ans = s.is_valid()
if ans:
print(f"Placed shape {shape.Index} successfully at ({x_sample}, {y_sample})")
break
else:
s.X_Offset.pop()
s.Y_Offset.pop()
s.Items_ID.pop()
# print(s)
return s

141 changes: 141 additions & 0 deletions algos/genetic_algo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import copy
import json
import time
from tqdm import tqdm
from .algo import Algo, AlgoClassification
from utils.Container import Container
from utils.Shape import Shape
from utils.Solution import Solution
from utils.utils import LoadJsonClassification, load_json_from_file
import random
random.seed(0)

class GeneticAlgo(Algo):
def __init__(self, shapes: list[Shape], cont: Container, pop_size: int, gens: int, tries_on_random_creation: int,instance_name: str):
super().__init__(shapes, cont, tries_on_random_creation)
if pop_size < 2:
raise Exception("Population size must be at least 2")
self.population_size = pop_size
self.max_generations = gens
self.curr_generation = []
self.next_generation = []
self.instance_name = instance_name

def run(self):
start_time = time.time()

self.curr_generation = self.generate_base_gen()
best_grade_so_far = max(self.curr_generation, key=lambda s: s.grade()).grade()
print(f"Best solution in base generation: {best_grade_so_far}")

with tqdm(total=self.max_generations, desc=f"Running genetic algorithm - Best Grade in baseGen: {best_grade_so_far}", unit="gen") as pbar:
for i in range(self.max_generations):
print(f"Starting generation {i + 1}")
self.next_generation = self.generate_next_gen()
self.curr_generation = self.next_generation
max_sol = max(self.curr_generation, key=lambda s: s.grade())
print(f"Best solution in generation {i+1}: {max_sol.grade()}")
best_grade_so_far = max_sol.grade()
pbar.set_description(f"Running genetic algorithm - Best Grade in gen {i+1}: {best_grade_so_far}")

pbar.update(1)

sol = max(self.curr_generation, key=lambda s: s.grade())
print(f"Best solution found: {sol}")
end_time = time.time()
duration = end_time - start_time
print(f"Total time taken: {duration:.3f} seconds")
sol.visualize_solution()

def generate_next_gen(self) -> list[Solution]:
# Elitism: Keep the best solution
new_gen = [max(self.curr_generation, key=lambda s: s.grade())]
# Generate new generation
for i in range(self.population_size-1):
# Select parents using tournament selection
p1 = self.tournament_selection()
p2 = self.tournament_selection()

child = self.crossover(p1, p2)

child = self.mutate(child)

# Ensure diversity by checking if the child is significantly different from existing solutions
if all(child.grade() != s.grade() for s in self.curr_generation):
new_gen.append(child)
else:
# If not diverse, generate a new solution
new_gen.append(self.create_random_offset_solution(AlgoClassification.SORT_BY_VALUE))

return sorted(new_gen, key=lambda s: s.grade(), reverse=True)

def generate_base_gen(self) -> list[Solution]:
base_gen = load_json_from_file(f'./data/baseGens/{self.instance_name}_baseGen.json', LoadJsonClassification.BASE_GEN)
if base_gen is None:
lst = [self.create_random_offset_solution(AlgoClassification.SORT_BY_AREA) for _ in range(self.population_size)]
base_gen = sorted(lst, key=lambda s: s.grade(), reverse=True)
# save base_gen to file, base gen is a list of solutions
solutions_as_dicts = [s.export_to_json() for s in base_gen]
with open(f'./data/baseGens/{self.instance_name}_baseGen.json', 'w') as f:
json.dump(solutions_as_dicts, f)
else:
# go over the list of solutions and assign the container and shapes list
for s in base_gen:
s.Container = self.Container
s.Shapes = self.Shapes
return base_gen

def crossover(self, s1: Solution, s2: Solution) -> Solution:
print("entered Crossover")
s1_child = copy.deepcopy(s1)
s2_child = copy.deepcopy(s2)

# Try to fit shapes that are in s2 and not in s1 into s1
for i in range(len(s2.Items_ID)):
if s2.Items_ID[i] not in s1_child.Items_ID:
temp_child = copy.deepcopy(s1_child)
temp_child.Items_ID.append(s2.Items_ID[i])
temp_child.X_Offset.append(s2.X_Offset[i])
temp_child.Y_Offset.append(s2.Y_Offset[i])

if temp_child.is_valid():
s1_child = copy.deepcopy(temp_child)
print(f"Merged shape into s1_child raising grade from {s1_child.grade()} to {temp_child.grade()}")

# Try to fit shapes that are in s1 and not in s2 into s2
for i in range(len(s1.Items_ID)):
if s1.Items_ID[i] not in s2_child.Items_ID:
temp_s2 = copy.deepcopy(s2_child)
temp_s2.Items_ID.append(s1.Items_ID[i])
temp_s2.X_Offset.append(s1.X_Offset[i])
temp_s2.Y_Offset.append(s1.Y_Offset[i])

if temp_s2.is_valid():
print(f"Merged shape into s2_child raising grade from {s2_child.grade()} to {temp_s2.grade()}")
s2_child = copy.deepcopy(temp_s2)

return max(s1_child, s2_child, key=lambda s: s.grade())

def tournament_selection(self) -> Solution:
# Tournament selection: Randomly select a subset of solutions and choose the best among them
tournament_size = min(3, len(self.curr_generation))
tournament_candidates = random.sample(self.curr_generation, tournament_size)
winner = max(tournament_candidates, key=lambda s: s.grade())
return winner

def mutate(self, solution: Solution) -> Solution:
# Mutation: Randomly perturb the solution
mutated_solution = copy.deepcopy(solution) # Ensure not to modify the original solution

for i in range(len(mutated_solution.Items_ID)):
# Apply a small random perturbation to the offset
mutated_solution.X_Offset[i] += random.uniform(-10, 10)
mutated_solution.Y_Offset[i] += random.uniform(-10, 10)

# Check if the mutated solution is valid
if mutated_solution.is_valid():
solution = copy.deepcopy(mutated_solution)
print("Mutated solution")

return solution

1 change: 1 addition & 0 deletions data/baseGens/atris42_baseGen.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[{"type": "cgshop2024_solution", "instance_name": "atris42", "num_included_items": 14, "meta": {"approach": "generated solution"}, "item_indices": [10, 39, 19, 31, 37, 11, 1, 12, 36, 8, 34, 25, 35, 17], "x_translations": [1696, 1124, 2594, 6504, 405, 4174, 6785, 5854, 6598, 3723, 2377, 8955, 312, 4103], "y_translations": [8285, 2000, 6614, 371, 3595, 4649, 1573, 7011, 4289, 592, 3273, 5365, 35, 7983]}, {"type": "cgshop2024_solution", "instance_name": "atris42", "num_included_items": 14, "meta": {"approach": "generated solution"}, "item_indices": [10, 39, 19, 31, 37, 11, 1, 12, 36, 8, 18, 25, 20, 17], "x_translations": [7521, 3550, 1768, 2685, 6046, 6728, 710, 2656, 3485, 7737, 7326, 389, 4977, 373], "y_translations": [679, 2535, 5702, 490, 5755, 3529, 2592, 7894, 3561, 5451, 1590, 5179, 8947, 482]}, {"type": "cgshop2024_solution", "instance_name": "atris42", "num_included_items": 14, "meta": {"approach": "generated solution"}, "item_indices": [10, 39, 19, 31, 37, 11, 1, 12, 36, 8, 18, 32, 30, 20], "x_translations": [1294, 6127, 2726, 2862, 5167, 2775, 7083, 451, 6950, 222, 2266, 154, 7823, 5313], "y_translations": [3798, 8242, 7483, 2235, 3954, 506, 1795, 6250, 6324, 1023, 4880, 1966, 3352, 72]}, {"type": "cgshop2024_solution", "instance_name": "atris42", "num_included_items": 13, "meta": {"approach": "generated solution"}, "item_indices": [10, 39, 19, 31, 37, 11, 1, 12, 36, 8, 34, 25, 4], "x_translations": [6369, 1028, 2172, 3068, 1207, 4606, 7420, 3939, 3871, 6905, 38, 70, 2280], "y_translations": [4907, 4019, 1432, 6176, 7294, 2305, 1826, 7959, 206, 6141, 257, 3034, 4211]}, {"type": "cgshop2024_solution", "instance_name": "atris42", "num_included_items": 13, "meta": {"approach": "generated solution"}, "item_indices": [10, 39, 19, 31, 37, 11, 1, 12, 36, 8, 18, 4, 20], "x_translations": [6647, 546, 144, 5, 7485, 2745, 3957, 3231, 6612, 601, 5537, 4142, 519], "y_translations": [6450, 4192, 1653, 5679, 7509, 1596, 3508, 7568, 2667, 6807, 54, 4900, 109]}, {"type": "cgshop2024_solution", "instance_name": "atris42", "num_included_items": 13, "meta": {"approach": "generated solution"}, "item_indices": [10, 39, 19, 31, 37, 11, 1, 12, 36, 8, 18, 14, 35], "x_translations": [764, 5818, 5012, 2729, 515, 5788, 6548, 1990, 1594, 3486, 6973, 7633, 90], "y_translations": [6534, 18, 8086, 3996, 1565, 2044, 4941, 132, 7872, 5083, 7084, 1219, 4331]}, {"type": "cgshop2024_solution", "instance_name": "atris42", "num_included_items": 13, "meta": {"approach": "generated solution"}, "item_indices": [10, 39, 19, 31, 37, 11, 1, 12, 36, 8, 18, 14, 25], "x_translations": [1899, 3929, 2389, 4661, 31, 4025, 5560, 7013, 818, 5882, 1958, 19, 8599], "y_translations": [6568, 7807, 3027, 597, 690, 4222, 2588, 5761, 7857, 6866, 108, 3858, 740]}, {"type": "cgshop2024_solution", "instance_name": "atris42", "num_included_items": 13, "meta": {"approach": "generated solution"}, "item_indices": [10, 39, 19, 31, 37, 11, 1, 12, 36, 8, 18, 25, 20], "x_translations": [5270, 398, 43, 1072, 7480, 6189, 3927, 489, 5436, 6532, 3690, 9076, 33], "y_translations": [3865, 8043, 4310, 6275, 91, 5888, 1478, 1547, 7652, 2881, 4879, 3165, 160]}, {"type": "cgshop2024_solution", "instance_name": "atris42", "num_included_items": 12, "meta": {"approach": "generated solution"}, "item_indices": [10, 39, 19, 31, 37, 11, 1, 12, 36, 8, 18, 34], "x_translations": [3665, 2721, 463, 2425, 6563, 6508, 1104, 4255, 6186, 3164, 33, 904], "y_translations": [5448, 1921, 156, 4123, 1438, 7280, 5728, 627, 4082, 6668, 2937, 7867]}, {"type": "cgshop2024_solution", "instance_name": "atris42", "num_included_items": 12, "meta": {"approach": "generated solution"}, "item_indices": [10, 39, 19, 31, 37, 11, 1, 12, 36, 18, 34, 14], "x_translations": [3045, 6110, 3889, 5716, 265, 6345, 66, 3026, 3265, 6539, 687, 7478], "y_translations": [3152, 5048, 7676, 6312, 1171, 1597, 5445, 4596, 448, 7390, 7696, 3413]}]
7 changes: 7 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from algos.genetic_algo import GeneticAlgo
from utils.utils import LoadJsonClassification, load_json_from_file

if __name__ == "__main__":
cont, shapes = load_json_from_file('./data/atris42.cgshop2024_instance.json', LoadJsonClassification.INSTANCE)
algo = GeneticAlgo(shapes=shapes, cont=cont, pop_size=10,gens=10,tries_on_random_creation=1000, instance_name=cont.Instance_Name)
algo.run()
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
shapely==2.0.2
matplotlib==3.8.2
ruff
ruff
tqdm
2 changes: 1 addition & 1 deletion utils/Container.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
class Container:
def __init__(self,x_cor,y_cor,in_name):
def __init__(self,x_cor:list[int]=[],y_cor:list[int]=[],in_name:str=""):
if len(x_cor) != len(y_cor):
raise Exception("Unmatched sizes!")
self.X_cor=x_cor
Expand Down
8 changes: 5 additions & 3 deletions utils/Shape.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@


class Shape:
def __init__(self, x_cor, y_cor, qnty, val, index):
def __init__(self, x_cor:list[int], y_cor:list[int], qnty:int, val:int, index:int):
if len(x_cor) != len(y_cor):
raise Exception("Unmatched sizes!")
self.X_cor = x_cor
Expand All @@ -12,7 +12,7 @@ def __init__(self, x_cor, y_cor, qnty, val, index):
self.Index = index
self.Polygon = Polygon(self.create_polygon_object())

def create_polygon_object(self):
def create_polygon_object(self)->list[(int,int)]:
vertices = []
for index in range(len(self.X_cor)):
vertices.append((self.X_cor[index], self.Y_cor[index]))
Expand All @@ -25,5 +25,7 @@ def __str__(self):
str += f"Original Index in instance file: {self.Index}"
return str

def get_area(self):
def get_area(self)->float:
return self.Polygon.area


Loading
Loading