diff --git a/src/Gep.jl b/src/Gep.jl index 25428d0..7e85c34 100644 --- a/src/Gep.jl +++ b/src/Gep.jl @@ -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] diff --git a/src/Selection.jl b/src/Selection.jl index 2d36850..1e46d24 100644 --- a/src/Selection.jl +++ b/src/Selection.jl @@ -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 @@ -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) @@ -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 @@ -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 @@ -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] @@ -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 \ No newline at end of file diff --git a/src/Util.jl b/src/Util.jl index a95217c..11f620b 100644 --- a/src/Util.jl +++ b/src/Util.jl @@ -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) diff --git a/test/non_dom_sort_test.jl b/test/non_dom_sort_test.jl index 64556b3..4fc2999 100644 --- a/test/non_dom_sort_test.jl +++ b/test/non_dom_sort_test.jl @@ -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([ @@ -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 @@ -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), @@ -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([ @@ -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 \ No newline at end of file