Files
IT3708/src/main.odin

419 lines
9.6 KiB
Odin

package main
import "core:container/bit_array"
import "core:encoding/csv"
import "core:fmt"
import "core:math"
import "core:math/rand"
import "core:os"
import "core:slice"
import "core:strconv"
import "core:strings"
OUTPUT_FILE :: "output/data.csv"
DATA_FILE :: "res/knapPI_12_500_1000_82.csv"
NUMBER_OF_ITEMS :: 500
CAPACITY :: 280785
POPULATION_SIZE :: 100
GENERATIONS :: 100
TOURNAMENT_SIZE :: 3
CROSSOVER_RATE :: 0.8
MUTATION_RATE :: 0.01
Item :: struct {
profit, weight: int,
}
Chromosome :: ^bit_array.Bit_Array
Population :: [POPULATION_SIZE]Chromosome
items: [NUMBER_OF_ITEMS]Item
Data :: struct {
best, worst: int,
mean: f32,
}
stats: [GENERATIONS]Data
read_data :: proc(file: string) -> (res: [NUMBER_OF_ITEMS]Item, ok := true) {
data := string(os.read_entire_file(file) or_return)
defer delete(data)
for line, idx in strings.split_lines(strings.trim_space(data))[1:] {
s := strings.split(line, ",")
res[idx] = {strconv.parse_int(s[1]) or_return, strconv.parse_int(s[2]) or_return}
}
return
}
write_results :: proc(filename: string, stats: []Data) -> bool {
handle, err := os.open(filename, os.O_CREATE | os.O_WRONLY | os.O_TRUNC, 0o644)
if err != os.ERROR_NONE {return false}
defer os.close(handle)
w: csv.Writer
csv.writer_init(&w, os.stream_from_handle(handle))
csv.write(&w, []string{"generation", "best", "worst", "mean"})
for stat, gen in stats {
csv.write(
&w,
[]string {
fmt.tprintf("%d", gen),
fmt.tprintf("%d", stat.best),
fmt.tprintf("%d", stat.worst),
fmt.tprintf("%f", stat.mean),
},
)
}
csv.writer_flush(&w)
return true
}
fitness :: proc(chrom: Chromosome) -> int {
tot_profit, tot_weight := 0, 0
for idx in 0 ..< bit_array.len(chrom) {
if !bit_array.get(chrom, idx) {continue}
tot_profit += items[idx].profit
tot_weight += items[idx].weight
}
return tot_profit - 500 * max(tot_weight - CAPACITY, 0)
}
create_random_chromosome :: proc() -> Chromosome {
chrom := bit_array.create(NUMBER_OF_ITEMS)
for i in 0 ..< NUMBER_OF_ITEMS {
bit_array.set(chrom, i, rand.int_max(2) == 1)
}
return chrom
}
copy_chromosome :: proc(src: Chromosome) -> Chromosome {
dest := bit_array.create(NUMBER_OF_ITEMS)
for i in 0 ..< NUMBER_OF_ITEMS {
bit_array.set(dest, i, bit_array.get(src, i))
}
return dest
}
generate_population :: proc() -> Population {
pop: Population
for i in 0 ..< POPULATION_SIZE {
pop[i] = create_random_chromosome()
}
return pop
}
destroy_population :: proc(pop: ^Population) {
for chrom in pop {
bit_array.destroy(chrom)
}
}
evaluate_population :: proc(pop: ^Population) -> [POPULATION_SIZE]int {
fitnesses: [POPULATION_SIZE]int
for chrom, i in pop {
fitnesses[i] = fitness(chrom)
}
return fitnesses
}
tournament_selection :: proc(
pop: ^Population,
fitnesses: []int,
k := TOURNAMENT_SIZE,
) -> Chromosome {
best_idx := rand.int_max(POPULATION_SIZE)
best_fitness := fitnesses[best_idx]
for _ in 1 ..< k {
idx := rand.int_max(POPULATION_SIZE)
if fitnesses[idx] > best_fitness {
best_idx = idx
best_fitness = fitnesses[idx]
}
}
return pop[best_idx]
}
roulette_selection :: proc(pop: ^Population, fitnesses: []int) -> Chromosome {
total_fitness := 0
for f in fitnesses {
total_fitness += max(f, 0)
}
if total_fitness == 0 {
return pop[rand.int_max(POPULATION_SIZE)]
}
spin := rand.float32() * f32(total_fitness)
running_sum := 0
for fitness, idx in fitnesses {
running_sum += max(fitness, 0)
if f32(running_sum) >= spin {
return pop[idx]
}
}
return pop[POPULATION_SIZE - 1]
}
single_point_crossover :: proc(parent1, parent2: Chromosome) -> (child1, child2: Chromosome) {
point := rand.int_max(NUMBER_OF_ITEMS)
child1 = bit_array.create(NUMBER_OF_ITEMS)
child2 = bit_array.create(NUMBER_OF_ITEMS)
for i in 0 ..< NUMBER_OF_ITEMS {
if i < point {
bit_array.set(child1, i, bit_array.get(parent1, i))
bit_array.set(child2, i, bit_array.get(parent2, i))
} else {
bit_array.set(child1, i, bit_array.get(parent2, i))
bit_array.set(child2, i, bit_array.get(parent1, i))
}
}
return
}
two_point_crossover :: proc(parent1, parent2: Chromosome) -> (child1, child2: Chromosome) {
point1 := rand.int_max(NUMBER_OF_ITEMS)
point2 := rand.int_max(NUMBER_OF_ITEMS)
if point1 > point2 {
point1, point2 = point2, point1
}
child1 = bit_array.create(NUMBER_OF_ITEMS)
child2 = bit_array.create(NUMBER_OF_ITEMS)
for i in 0 ..< NUMBER_OF_ITEMS {
in_swap := i >= point1 && i < point2
if in_swap {
bit_array.set(child1, i, bit_array.get(parent2, i))
bit_array.set(child2, i, bit_array.get(parent1, i))
} else {
bit_array.set(child1, i, bit_array.get(parent1, i))
bit_array.set(child2, i, bit_array.get(parent2, i))
}
}
return
}
uniform_crossover :: proc(
parent1, parent2: Chromosome,
prob := f32(0.5),
) -> (
child1, child2: Chromosome,
) {
child1 = bit_array.create(NUMBER_OF_ITEMS)
child2 = bit_array.create(NUMBER_OF_ITEMS)
for i in 0 ..< NUMBER_OF_ITEMS {
if rand.float32() < prob {
bit_array.set(child1, i, bit_array.get(parent1, i))
bit_array.set(child2, i, bit_array.get(parent2, i))
} else {
bit_array.set(child1, i, bit_array.get(parent2, i))
bit_array.set(child2, i, bit_array.get(parent1, i))
}
}
return
}
bit_flip_mutation :: proc(chrom: Chromosome, mutation_rate := MUTATION_RATE) {
for i in 0 ..< NUMBER_OF_ITEMS {
if rand.float32() < f32(mutation_rate) {
bit_array.set(chrom, i, !bit_array.get(chrom, i))
}
}
}
swap_mutation :: proc(chrom: Chromosome) {
idx1 := rand.int_max(NUMBER_OF_ITEMS)
idx2 := rand.int_max(NUMBER_OF_ITEMS)
bit1 := bit_array.get(chrom, idx1)
bit2 := bit_array.get(chrom, idx2)
bit_array.set(chrom, idx1, bit2)
bit_array.set(chrom, idx2, bit1)
}
inversion_mutation :: proc(chrom: Chromosome) {
point1 := rand.int_max(NUMBER_OF_ITEMS)
point2 := rand.int_max(NUMBER_OF_ITEMS)
if point1 > point2 {
point1, point2 = point2, point1
}
for i in 0 ..< (point2 - point1) / 2 {
idx1 := point1 + i
idx2 := point2 - i - 1
bit1 := bit_array.get(chrom, idx1)
bit2 := bit_array.get(chrom, idx2)
bit_array.set(chrom, idx1, bit2)
bit_array.set(chrom, idx2, bit1)
}
}
create_offspring :: proc(population: ^Population, fitnesses: []int) -> Population {
offspring: Population
for i := 0; i < POPULATION_SIZE; i += 2 {
parent1 := tournament_selection(population, fitnesses)
parent2 := tournament_selection(population, fitnesses)
child1, child2: Chromosome
if rand.float32() < CROSSOVER_RATE {
child1, child2 = single_point_crossover(parent1, parent2)
} else {
child1 = copy_chromosome(parent1)
child2 = copy_chromosome(parent2)
}
bit_flip_mutation(child1)
bit_flip_mutation(child2)
offspring[i] = child1
if i + 1 < POPULATION_SIZE {
offspring[i + 1] = child2
} else {
bit_array.destroy(child2)
}
}
return offspring
}
elitism_survivor_selection :: proc(
parents: ^Population,
offspring: ^Population,
parent_fitnesses: []int,
offspring_fitnesses: []int,
) -> Population {
Index_Fitness :: struct {
idx: int,
fitness: int,
is_parent: bool,
}
combined := make([]Index_Fitness, POPULATION_SIZE * 2)
defer delete(combined)
for i in 0 ..< POPULATION_SIZE {
combined[i] = {i, parent_fitnesses[i], true}
combined[POPULATION_SIZE + i] = {i, offspring_fitnesses[i], false}
}
slice.sort_by(combined[:], proc(a, b: Index_Fitness) -> bool {
return a.fitness > b.fitness
})
survivors: Population
for i in 0 ..< POPULATION_SIZE {
entry := combined[i]
source := parents[entry.idx] if entry.is_parent else offspring[entry.idx]
survivors[i] = copy_chromosome(source)
}
return survivors
}
compute_stats :: proc(fitnesses: []int) -> Data {
best, worst := math.min(int), math.max(int)
sum := 0
for f in fitnesses {
best = max(best, f)
worst = min(worst, f)
sum += f
}
return {best, worst, f32(sum) / f32(len(fitnesses))}
}
run_ga :: proc() {
population := generate_population()
defer destroy_population(&population)
best_fitness := math.min(int)
best_generation := 0
best_chromosome: Chromosome
for gen in 0 ..< GENERATIONS {
fitnesses := evaluate_population(&population)
stats[gen] = compute_stats(fitnesses[:])
for f, i in fitnesses {
if f <= best_fitness {continue}
best_fitness = f
best_generation = gen
bit_array.destroy(best_chromosome)
best_chromosome = copy_chromosome(population[i])
tot_profit, tot_weight := 0, 0
for idx in 0 ..< bit_array.len(best_chromosome) {
if !bit_array.get(best_chromosome, idx) {continue}
tot_profit += items[idx].profit
tot_weight += items[idx].weight
}
fmt.printfln(
"Gen %d: Fitness=%d, Profit=%d, Weight=%d/%d",
gen,
f,
tot_profit,
tot_weight,
CAPACITY,
)
}
offspring := create_offspring(&population, fitnesses[:])
offspring_fitnesses := evaluate_population(&offspring)
new_population := elitism_survivor_selection(
&population,
&offspring,
fitnesses[:],
offspring_fitnesses[:],
)
destroy_population(&population)
destroy_population(&offspring)
population = new_population
}
fmt.printfln("\nFinal Best: Fitness=%d (Generation %d)", best_fitness, best_generation)
fmt.println("this solution is the following bit-string:")
for i in 0 ..< best_chromosome.length {
b := bit_array.get(best_chromosome, i)
fmt.print(i32(b))
}
fmt.println()
write_results(OUTPUT_FILE, stats[:])
fmt.println("successfully wrote data to", OUTPUT_FILE)
}
main :: proc() {
data, ok := read_data(DATA_FILE)
if !ok {
fmt.eprintln("Failed to read data from", DATA_FILE)
return
}
items = data
fmt.println("Running Genetic Algorithm for Binary Knapsack Problem")
fmt.printfln(
"Items: %d, Capacity: %d, Population: %d, Generations: %d\n",
NUMBER_OF_ITEMS,
CAPACITY,
POPULATION_SIZE,
GENERATIONS,
)
run_ga()
}