major refactor

This commit is contained in:
2026-01-31 17:05:35 +01:00
parent 8ca904fc0a
commit e450625077

View File

@@ -22,86 +22,97 @@ Item :: struct {
profit, weight: int,
}
Chromosome :: ^bit_array.Bit_Array
Population :: [POPULATION_SIZE]Chromosome
items: [NUMBER_OF_ITEMS]Item
read_data :: proc(file: string) -> (res: [NUMBER_OF_ITEMS]Item, ok := true) {
data := string(os.read_entire_file(file) or_return)
data = strings.trim_space(data)
defer delete(data)
for line, idx in strings.split_lines(string(data))[1:] {
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
}
Chromosome :: bit_array.Bit_Array
// side-effect: reads global `items`
fitness :: proc(chrom: ^Chromosome) -> int {
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} // nb: eliminating branch may be faster
if !bit_array.get(chrom, idx) {continue}
tot_profit += items[idx].profit
tot_weight += items[idx].weight
}
return tot_profit - 3 * math.max(tot_weight - CAPACITY, 0)
return tot_profit - 500 * max(tot_weight - CAPACITY, 0)
}
Population :: [POPULATION_SIZE]Chromosome
generate_population :: proc() -> (res: Population) {
for &chrom in res {
chrom = bit_array.create(NUMBER_OF_ITEMS)^
for i in 0 ..< NUMBER_OF_ITEMS {
bit_array.set(&chrom, i, rand.int_max(2) == 1)
}
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
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 {
if chrom.free_pointer {continue}
bit_array.destroy(&chrom)
for chrom in pop {
bit_array.destroy(chrom)
}
}
evaluate_population :: proc(pop: ^Population) -> (res: [POPULATION_SIZE]int) {
for &chrom, i in pop {
res[i] = fitness(&chrom)
evaluate_population :: proc(pop: ^Population) -> [POPULATION_SIZE]int {
fitnesses: [POPULATION_SIZE]int
for chrom, i in pop {
fitnesses[i] = fitness(chrom)
}
return
return fitnesses
}
tournament_selection :: proc(
pop: ^Population,
fitnesses: []int,
k: int = TOURNAMENT_SIZE,
) -> ^Chromosome {
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 {
if fitnesses[idx] > best_fitness {
best_idx = idx
best_fitness = fitnesses[idx]
}
}
return &pop[best_idx]
return pop[best_idx]
}
roulette_selection :: proc(pop: ^Population, fitnesses: []int) -> ^Chromosome {
roulette_selection :: proc(pop: ^Population, fitnesses: []int) -> Chromosome {
total_fitness := 0
for f in fitnesses {
total_fitness += max(f, 0) // ignore negative fitness
total_fitness += max(f, 0)
}
if total_fitness == 0 {
return &pop[rand.int_max(POPULATION_SIZE)]
return pop[rand.int_max(POPULATION_SIZE)]
}
spin := rand.float32() * f32(total_fitness)
@@ -110,49 +121,49 @@ roulette_selection :: proc(pop: ^Population, fitnesses: []int) -> ^Chromosome {
for fitness, idx in fitnesses {
running_sum += max(fitness, 0)
if f32(running_sum) >= spin {
return &pop[idx]
return pop[idx]
}
}
return &pop[POPULATION_SIZE - 1]
return pop[POPULATION_SIZE - 1]
}
single_point_crossover :: proc(parent1, parent2: ^Chromosome) -> (child1, child2: Chromosome) {
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)^
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))
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))
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) {
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)^
child1 = bit_array.create(NUMBER_OF_ITEMS)
child2 = bit_array.create(NUMBER_OF_ITEMS)
for i in 0 ..< NUMBER_OF_ITEMS {
if i >= point1 && i < point2 {
bit_array.set(&child1, i, bit_array.get(parent2, i))
bit_array.set(&child2, i, bit_array.get(parent1, i))
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))
bit_array.set(child1, i, bit_array.get(parent1, i))
bit_array.set(child2, i, bit_array.get(parent2, i))
}
}
@@ -160,51 +171,47 @@ two_point_crossover :: proc(parent1, parent2: ^Chromosome) -> (child1, child2: C
}
uniform_crossover :: proc(
parent1, parent2: ^Chromosome,
prob: f32 = 0.5,
parent1, parent2: Chromosome,
prob := f32(0.5),
) -> (
child1, child2: Chromosome,
) {
child1 = bit_array.create(NUMBER_OF_ITEMS)^
child2 = bit_array.create(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 rand.float32() < prob {
bit_array.set(&child1, i, bit_array.get(parent1, i))
bit_array.set(&child2, i, bit_array.get(parent2, i))
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))
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: f32 = MUTATION_RATE) {
bit_flip_mutation :: proc(chrom: Chromosome, mutation_rate := MUTATION_RATE) {
for i in 0 ..< NUMBER_OF_ITEMS {
if rand.float32() < mutation_rate {
current := bit_array.get(chrom, i)
bit_array.set(chrom, i, !current)
if rand.float32() < f32(mutation_rate) {
bit_array.set(chrom, i, !bit_array.get(chrom, i))
}
}
}
swap_mutation :: proc(chrom: ^Chromosome) {
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) {
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
}
@@ -212,15 +219,42 @@ inversion_mutation :: proc(chrom: ^Chromosome) {
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,
@@ -233,8 +267,7 @@ elitism_survivor_selection :: proc(
is_parent: bool,
}
combined_size := POPULATION_SIZE * 2
combined := make([]Index_Fitness, combined_size)
combined := make([]Index_Fitness, POPULATION_SIZE * 2)
defer delete(combined)
for i in 0 ..< POPULATION_SIZE {
@@ -249,19 +282,13 @@ elitism_survivor_selection :: proc(
survivors: Population
for i in 0 ..< POPULATION_SIZE {
entry := combined[i]
source := entry.is_parent ? &parents[entry.idx] : &offspring[entry.idx]
// Create NEW bit array and copy bits
survivors[i] = bit_array.create(NUMBER_OF_ITEMS)^
for j in 0 ..< NUMBER_OF_ITEMS {
bit_array.set(&survivors[i], j, bit_array.get(source, j))
}
source := parents[entry.idx] if entry.is_parent else offspring[entry.idx]
survivors[i] = copy_chromosome(source)
}
return survivors
}
run_ga :: proc() {
population := generate_population()
defer destroy_population(&population)
@@ -272,13 +299,12 @@ run_ga :: proc() {
for gen in 0 ..< GENERATIONS {
fitnesses := evaluate_population(&population)
// log best fitnesses
for f, i in fitnesses {
if f <= best_fitness {continue}
best_fitness = f
best_generation = gen
chrom := &population[i]
chrom := population[i]
tot_profit, tot_weight := 0, 0
for idx in 0 ..< bit_array.len(chrom) {
if !bit_array.get(chrom, idx) {continue}
@@ -286,7 +312,7 @@ run_ga :: proc() {
tot_weight += items[idx].weight
}
fmt.printfln(
"gen %d: best fitness=%d, profit=%d, weight=%d (capacity=%d)",
"Gen %d: Fitness=%d, Profit=%d, Weight=%d/%d",
gen,
f,
tot_profit,
@@ -295,35 +321,9 @@ run_ga :: proc() {
)
}
// create offspring
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 = bit_array.create(NUMBER_OF_ITEMS)^
child2 = bit_array.create(NUMBER_OF_ITEMS)^
for j in 0 ..< NUMBER_OF_ITEMS {
bit_array.set(&child1, j, bit_array.get(parent1, j))
bit_array.set(&child2, j, bit_array.get(parent2, j))
}
}
bit_flip_mutation(&child1)
bit_flip_mutation(&child2)
offspring[i] = child1
if i + 1 < POPULATION_SIZE {
offspring[i + 1] = child2
}
}
// survivor selection
offspring := create_offspring(&population, fitnesses[:])
offspring_fitnesses := evaluate_population(&offspring)
new_population := elitism_survivor_selection(
&population,
&offspring,
@@ -331,34 +331,31 @@ run_ga :: proc() {
offspring_fitnesses[:],
)
// clean-up
destroy_population(&population)
destroy_population(&offspring)
population = new_population
}
fmt.println()
fmt.printfln("final best: fitness=%d (found at generation %d)", best_fitness, best_generation)
fmt.printfln("\nFinal Best: Fitness=%d (Generation %d)", best_fitness, best_generation)
}
main :: proc() {
data, ok := read_data(DATA_FILE)
if !ok {
fmt.eprintln("failed to read data from", DATA_FILE)
fmt.eprintln("Failed to read data from", DATA_FILE)
return
}
items = data
fmt.println("running genetic algorithm for binary knapsack problem.")
fmt.println("Running Genetic Algorithm for Binary Knapsack Problem")
fmt.printfln(
"items: %d, capacity: %d, population: %d, generations: %d",
"Items: %d, Capacity: %d, Population: %d, Generations: %d\n",
NUMBER_OF_ITEMS,
CAPACITY,
POPULATION_SIZE,
GENERATIONS,
)
fmt.println()
run_ga()
}