Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Gep.jl
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ The evolution process stops when either:
end

!isnothing(file_logger_callback) && file_logger_callback(population[1:population_size], epoch, selectedMembers)
!isnothing(save_state_callback) && save_state_callback(population, epoch)
!isnothing(save_state_callback) && save_state_callback(population, evalStrategy)

if epoch < epochs
parents = population[selectedMembers.indices]
Expand Down
143 changes: 78 additions & 65 deletions src/Selection.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,35 @@ using LinearAlgebra

export tournament_selection, nsga_selection, dominates_, fast_non_dominated_sort, calculate_fronts, determine_ranks, assign_crowding_distance


struct SelectedMembers
indices::Vector{Int}
fronts::Dict{Int,Vector{Int}}
end

#Note: selection is constructed to allways return a list of indices => {just care about the data not the objects}
@inline function tournament_selection(population::AbstractArray{Tuple}, number_of_winners::Int, tournament_size::Int)
@inline function tournament_selection(population::AbstractArray{Tuple}, number_of_winners::Int, tournament_size::Int)
selected_indices = Vector{Int}(undef, number_of_winners)
valid_indices_ = findall(x -> isfinite(x[1]), population)
valid_indices = []
doubles = Set()
for elem in valid_indices_
if !(population[elem] in doubles)
push!(doubles,population[elem])
push!(valid_indices, elem)
end
if !(population[elem] in doubles)
push!(doubles, population[elem])
push!(valid_indices, elem)
end
end

Threads.@threads for index in 1:number_of_winners-1
contenders = rand(valid_indices, tournament_size)
winner = reduce((best, contender) -> population[contender] < population[best] ? contender : best, contenders)
selected_indices[index] = winner

for index in 1:number_of_winners
if index == number_of_winners
selected_indices[index] = 1 # Keep the original behavior
else
contenders = rand(valid_indices, min(tournament_size, length(valid_indices)))
winner = reduce((best, contender) -> population[contender] < population[best] ? contender : best, contenders)
selected_indices[index] = winner
end
end
selected_indices[end] = 1
return SelectedMembers(selected_indices,Dict{Int,Vector{Int}}())
return SelectedMembers(selected_indices, Dict{Int,Vector{Int}}())
end


function count_infinites(t::Tuple)
return count(isinf, t) + count(isnan, t)
end
Expand All @@ -57,25 +57,22 @@ function dominates_(a::Tuple, b::Tuple)
return false
end


@inbounds for i in eachindex(a)
for i in eachindex(a)
ai, bi = a[i], b[i]
if ai ≪ bi
one_significant_smaller = true
elseif ai <= bi
all_smaller = true & all_smaller
elseif bi ≪ ai || ai > bi
all_smaller = all_smaller
else
all_smaller = false
end
end
return one_significant_smaller || all_smaller
end


@inline function determine_ranks(pop::Vector{T}) where T<:Tuple
@inline function determine_ranks(pop::Vector{T}) where {T<:Tuple}
n = length(pop)

dom_list = [ Int[] for i in 1:n ]
dom_list = [Int[] for _ in 1:n]
rank = zeros(Int, n)
dom_count = zeros(Int, n)

Expand All @@ -84,11 +81,10 @@ end
i_dominates_j = dominates_(pop[i], pop[j])
j_dominates_i = dominates_(pop[j], pop[i])


if i_dominates_j
if i_dominates_j
push!(dom_list[i], j)
dom_count[j] += 1
elseif j_dominates_i
elseif j_dominates_i
push!(dom_list[j], i)
dom_count[i] += 1
end
Expand All @@ -98,42 +94,42 @@ end
end
end

k = UInt16(2)
while any(==(k-one(UInt16)), (rank[p] for p in 1:n))
k = 2
while any(==(k - 1), rank)
for p in 1:n
if rank[p] == k-one(UInt16)
if rank[p] == k - 1
for q in dom_list[p]
dom_count[q] -= one(UInt16)
if dom_count[q] == zero(UInt16)
dom_count[q] -= 1
if dom_count[q] == 0
rank[q] = k
end
end
end
end
k += one(UInt16)
k += 1
end
return rank
end

@inline function fast_non_dominated_sort(population::Vector{T}) where {T<:Tuple}
ranks = determine_ranks(population)
pop_indices = [(index,rank) for (index, rank) in enumerate(ranks)]
sort!(pop_indices, by = x -> x[2])
pop_indices = [(index, rank) for (index, rank) in enumerate(ranks)]
sort!(pop_indices, by=x -> x[2])
return [elem[1] for elem in pop_indices]
end

@inline function calculate_fronts(population::Vector{T}) where {T<:Tuple}
ranks = determine_ranks(population)
min_rank = minimum(unique(ranks))
max_rank = maximum(unique(ranks))
if min_rank == 0
ranks = [rank == 0 ? max_rank+1 : rank for rank in ranks]
end
fronts = [Int[] for i in eachindex(unique(ranks))]
min_rank = minimum(ranks)
max_rank = maximum(ranks)

fronts = [Int[] for _ in min_rank:max_rank]

for (i, r) in enumerate(ranks)
push!(fronts[r], i)
push!(fronts[r-min_rank+1], i)
end

filter!(!isempty, fronts)
return fronts
end

Expand All @@ -145,10 +141,13 @@ end
for i in front
distances[i] = 0.0
end
# only looking to the direct neighbour!

for m in 1:objectives_count
sorted_front = sort(front, by=i -> population[i][m])
distances[sorted_front[1]] = distances[sorted_front[end]] = Inf

# Set extreme points to infinity
distances[sorted_front[1]] = Inf
distances[sorted_front[end]] = Inf

if n > 2
obj_range = population[sorted_front[end]][m] - population[sorted_front[1]][m]
Expand All @@ -165,34 +164,48 @@ end
end


@inline function nsga_selection(population::Vector{T}) where {T<:Tuple}
fronts = calculate_fronts(population)
n_fronts = length(fronts)

selected_indices = Int[]
pareto_fronts = Dict{Int,Vector{Int}}()

estimated_size = length(population)
selected_indices = Vector{Int}(undef, estimated_size)
current_idx = 1
function tournament_selection_nsga(pop_indices::Vector{Int}, ranks::Vector{Int}, crowding_distances::Dict{Int,Float64}, number_of_winners::Int, tournament_size::Int)
selected_indices = Vector{Int}(undef, number_of_winners)
for i in 1:number_of_winners
contenders = rand(pop_indices, tournament_size)
winner = reduce(contenders; init=contenders[1]) do best, contender
if ranks[contender] < ranks[best]
contender
elseif ranks[contender] > ranks[best]
best
else
crowding_distances[contender] > crowding_distances[best] ? contender : best
end
end
selected_indices[i] = winner
end
return selected_indices
end

function nsga_selection(population::Vector{T}; tournament_size::Int=2) where {T<:Tuple}
pop_size = length(population)

@inbounds for front_idx in 1:n_fronts
front = fronts[front_idx]
crowding_distances = assign_crowding_distance(front, population)
# Compute ranks for all individuals
ranks = determine_ranks(population)

sorted_front = sort(front, by=i -> crowding_distances[i], rev=true)
front_size = length(sorted_front)
copyto!(selected_indices, current_idx, sorted_front, 1, front_size)

pareto_fronts[front_idx] = sorted_front
current_idx +=front_size
# Compute fronts for tracking and crowding distances
fronts = calculate_fronts(population)
crowding_distances = Dict{Int,Float64}()
for front in fronts
front_distances = assign_crowding_distance(front, population)
for (i, d) in front_distances
crowding_distances[i] = d
end
end
resize!(selected_indices, current_idx - 1)

return SelectedMembers(selected_indices, pareto_fronts)
all_indices = collect(1:pop_size)
selected_indices = tournament_selection_nsga(all_indices, ranks, crowding_distances, pop_size, tournament_size)

@debug selected_indices
@debug population[selected_indices]

return SelectedMembers(selected_indices, Dict(enumerate(fronts)))
end


end
end
6 changes: 5 additions & 1 deletion src/Util.jl
Original file line number Diff line number Diff line change
Expand Up @@ -770,9 +770,13 @@ function minmax_scale(X::AbstractArray{T}; feature_range=(zero(T), one(T))) wher
end

function save_state(filename::String, state::Any)
open(filename, "w") do io
temp_filename = filename * ".tmp"
open(temp_filename, "w") do io
serialize(io, state)
flush(io)
end
mv(temp_filename, filename; force=true)
return true
end

function load_state(filename::String)
Expand Down
87 changes: 39 additions & 48 deletions test/non_dom_sort_test.jl
Original file line number Diff line number Diff line change
Expand Up @@ -6,41 +6,32 @@ end

# Test dominates_ function
@testset "dominates_ function" begin
@test dominates_((1, 2), (2, 3)) == true
@test dominates_((1, NaN), (1, 2)) == false
@test dominates_((1, 2), (NaN, 3)) == true
@test dominates_((1, 2, 3), (2, 3, 4)) == true
@test dominates_((1, 2, 3), (1, 2, 3)) == true
@test dominates_((1, 2, 3), (0, 3, 4)) == false
@test dominates_((1, 2), (2, 3)) == true # (1, 2) dominates (2, 3)
@test dominates_((1, NaN), (1, 2)) == false # NaN handling: (1, NaN) does not dominate
@test dominates_((1, 2), (NaN, 3)) == true # (1, 2) dominates (NaN, 3)
@test dominates_((1, 2, 3), (2, 3, 4)) == true # 3 objectives, all better
@test dominates_((1, 2, 3), (1, 2, 3)) == true # Equal objectives, should return true per definition
@test dominates_((1, 2, 3), (0, 3, 4)) == false # (0, 3, 4) is better in first objective
end

# Test fast_non_dominated_sort function
@testset "fast_non_dominated_sort function" begin

# Test calculate_fronts function
@testset "calculate_fronts function" begin
# 2 objectives
pop2 = create_population([
(1, 5),
(2, 4),
(3, 3),
(4, 2),
(5, 1),
(1, 4),
(2, 3),
(3, 2),
(4, 1),
(1, 3),
(2, 2),
(3, 1),
(1, 2),
(2, 1), #
(1, 1) #front 1
(1, 5), (2, 4), (3, 3), (4, 2), (5, 1), # Front 5
(1, 4), (2, 3), (3, 2), (4, 1), # Front 4
(1, 3), (2, 2), (3, 1), # Front 3
(1, 2), (2, 1), # Front 2
(1, 1) # Front 1
])
fronts2 = calculate_fronts(pop2)
@test length(fronts2) == 5
@test fronts2[1] == [15]
@test Set(fronts2[2]) == Set([13, 14])
@test Set(fronts2[3]) == Set([10, 11, 12])
@test Set(fronts2[4]) == Set([6, 7, 8, 9])
@test Set(fronts2[5]) == Set([1, 2, 3, 4, 5])
@test length(fronts2) == 5 # 5 non-dominated fronts
@test fronts2[1] == [15] # Front 1: (1, 1)
@test Set(fronts2[2]) == Set([13, 14]) # Front 2: (1, 2), (2, 1)
@test Set(fronts2[3]) == Set([10, 11, 12]) # Front 3: (1, 3), (2, 2), (3, 1)
@test Set(fronts2[4]) == Set([6, 7, 8, 9]) # Front 4: (1, 4), (2, 3), (3, 2), (4, 1)
@test Set(fronts2[5]) == Set([1, 2, 3, 4, 5]) # Front 5: (1, 5), (2, 4), (3, 3), (4, 2), (5, 1)

# 3 objectives
pop3 = create_population([
Expand All @@ -49,10 +40,9 @@ end
(1, 3, 2), (2, 1, 3), (3, 2, 1)
])
fronts3 = calculate_fronts(pop3)

@test length(fronts3) == 3
@test Set(fronts3[1]) == Set([1])
@test Set(fronts3[2]) == Set([2, 4, 5, 6, 7, 8, 9])
@test length(fronts3) == 3 # 3 non-dominated fronts
@test Set(fronts3[1]) == Set([1]) # Front 1: (1, 1, 1)
@test Set(fronts3[2]) == Set([2, 4, 5, 6, 7, 8, 9]) # Front 2: all others except (3, 3, 3)
end

# Test assign_crowding_distance function
Expand All @@ -61,21 +51,22 @@ end
pop2 = create_population([(1, 5), (2, 4), (3, 3), (4, 2), (5, 1)])
front2 = [1, 2, 3, 4, 5]
distances2 = assign_crowding_distance(front2, pop2)
@test distances2[1] == Inf
@test distances2[5] == Inf
@test distances2[3] ≈ 1
@test distances2[1] == Inf # Boundary point (1, 5)
@test distances2[5] == Inf # Boundary point (5, 1)
@test distances2[3] ≈ 1 # Middle point (3, 3) has finite distance

# 3 objectives
pop3 = create_population([(1, 1, 1), (2, 2, 2), (3, 3, 3), (1, 2, 3), (2, 3, 1), (3, 1, 2)])
front3 = [1, 4, 5, 6]
distances3 = assign_crowding_distance(front3, pop3)
@test distances3[1] == Inf
@test distances3[6] == Inf
@test sum(values(distances3)) > 0
@test distances3[1] == Inf # Boundary point (1, 1, 1)
@test distances3[6] == Inf # Boundary point (3, 1, 2)
@test sum(values(distances3)) > 0 # Sum of distances should be positive
end

# Test selection_NSGA function
@testset "selection_NSGA function" begin

# Test nsga_selection function
@testset "nsga_selection function" begin
# 2 objectives
pop2 = create_population([
(1, 5), (2, 4), (3, 3), (4, 2), (5, 1),
Expand All @@ -85,9 +76,9 @@ end
(1, 1)
])
selected2 = nsga_selection(pop2)
@test length(selected2.indices) == 15
@test 15 in selected2.indices # Preserve the best!! alllllways
@test length(selected2.fronts) == 5
@test length(selected2.indices) == 15 # Matches population size
@test all(i -> 1 <= i <= 15, selected2.indices) # All indices are valid
@test length(selected2.fronts) == 5 # Correct number of fronts

# 3 objectives
pop3 = create_population([
Expand All @@ -96,7 +87,7 @@ end
(1, 3, 2), (2, 1, 3), (3, 2, 1)
])
selected3 = nsga_selection(pop3)
@test length(selected3.indices) == 9
@test 1 in selected3.indices # The best individual should always be selected
@test length(selected3.fronts) == 3
end
@test length(selected3.indices) == 9 # Matches population size
@test all(i -> 1 <= i <= 9, selected3.indices) # All indices are valid
@test length(selected3.fronts) == 3 # Correct number of fronts
end