diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bb19ab3..957dfee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,7 +50,6 @@ jobs: fail-fast: false matrix: version: - - "1.8" - "1" # automatically expands to the latest stable 1.x release of Julia - "pre" os: diff --git a/src/ConstraintDomains.jl b/src/ConstraintDomains.jl index efcefb9..f094b03 100644 --- a/src/ConstraintDomains.jl +++ b/src/ConstraintDomains.jl @@ -2,7 +2,6 @@ module ConstraintDomains # SECTION - Imports import ConstraintCommons: ConstraintCommons, δ_extrema -# import Dictionaries import PatternFolds: PatternFolds, Interval, Closed import StatsBase: sample import TestItems: @testitem @@ -11,7 +10,7 @@ import TestItems: @testitem export AbstractDomain export ContinuousDomain export DiscreteDomain -export ExploreSettings +export Explorer, ExploreSettings export RangeDomain export SetDomain @@ -19,7 +18,7 @@ export add! export delete! export domain export domain_size -export explore +export explore, explore! export generate_parameters export get_domain export intersect_domains diff --git a/src/common.jl b/src/common.jl index c577bb1..93e68a9 100644 --- a/src/common.jl +++ b/src/common.jl @@ -98,6 +98,9 @@ function Base.string(D::Vector{<:AbstractDomain}) end Base.string(d::AbstractDomain) = replace(string(d.domain), " " => "") +merge_domains(::EmptyDomain, d::D) where {D<:AbstractDomain} = d +merge_domains(d::D, ::EmptyDomain) where {D<:AbstractDomain} = d + ## SECTION - Test Items @testitem "EmptyDomain" tags = [:domains, :empty] begin ed = domain() diff --git a/src/continuous.jl b/src/continuous.jl index fa42182..b02ec11 100644 --- a/src/continuous.jl +++ b/src/continuous.jl @@ -114,7 +114,6 @@ function intersect_domains( return Intervals(new_itvls) end - """ Base.size(i::I) where {I <: Interval} diff --git a/src/explore.jl b/src/explore.jl index 5607d88..db47eab 100644 --- a/src/explore.jl +++ b/src/explore.jl @@ -19,48 +19,137 @@ Settings for the exploration of a search space composed by a collection of domai function ExploreSettings( domains; complete_search_limit = 10^6, - max_samplings = sum(domain_size, domains), + max_samplings = sum(domain_size, domains; init = 0), search = :flexible, solutions_limit = floor(Int, sqrt(max_samplings)), ) return ExploreSettings(complete_search_limit, max_samplings, search, solutions_limit) end +struct ExplorerState{T} + best::Vector{T} + solutions::Set{Vector{T}} + non_solutions::Set{Vector{T}} + + ExplorerState{T}() where {T} = new{T}([], Set{Vector{T}}(), Set{Vector{T}}()) +end + +ExplorerState(domains) = ExplorerState{Union{map(eltype, domains)...}}() + +mutable struct Explorer{F1<:Function,D<:AbstractDomain,F2<:Union{Function,Nothing},T} + concepts::Dict{Int,Tuple{F1,Vector{Int}}} + domains::Dict{Int,D} + objective::F2 + settings::ExploreSettings + state::ExplorerState{T} + + function Explorer( + concepts, + domains, + objective = nothing; + settings = ExploreSettings(domains), + ) + F1 = isempty(concepts) ? Function : Union{map(c -> typeof(c[1]), concepts)...} + D = isempty(domains) ? AbstractDomain : Union{map(typeof, domains)...} + F2 = typeof(objective) + T = isempty(domains) ? Real : Union{map(eltype, domains)...} + d_c = Dict(enumerate(concepts)) + d_d = Dict(enumerate(domains)) + return new{F1,D,F2,T}(d_c, d_d, objective, settings, ExplorerState{T}()) + end +end + +function Explorer() + concepts = Vector{Tuple{Function,Vector{Int}}}() + domains = Vector{AbstractDomain}() + objective = nothing + settings = ExploreSettings(domains) + return Explorer(concepts, domains, objective; settings) +end + +function Base.push!(explorer::Explorer, concept::Tuple{Function,Vector{Int}}) + max_key = maximum(keys(explorer.concepts); init = 0) + explorer.concepts[max_key+1] = concept + return max_key + 1 +end + +function delete_concept!(explorer::Explorer, key::Int) + delete!(explorer.concepts, key) + return nothing +end + +function Base.push!(explorer::Explorer, domain::AbstractDomain) + max_key = maximum(keys(explorer.domains); init = 0) + explorer.domains[max_key+1] = domain + return max_key + 1 +end + +function delete_domain!(explorer::Explorer, key::Int) + delete!(explorer.domains, key) + return nothing +end + +set!(explorer::Explorer, objective::Function) = explorer.objective = objective + +function update_exploration!(explorer, f, c, search = explorer.settings.search) + solutions = explorer.state.solutions + non_sltns = explorer.state.non_solutions + obj = explorer.objective + sl = search == :complete ? Inf : explorer.settings.solutions_limit + + cv = collect(c) + if f(cv) + if length(solutions) < sl + push!(solutions, cv) + obj !== nothing && (explorer.state.best = argmin(obj, solutions)) + end + else + if length(non_sltns) < sl + push!(non_sltns, cv) + end + end + return nothing +end + """ _explore(args...) Internals of the `explore` function. Behavior is automatically adjusted on the kind of exploration: `:flexible`, `:complete`, `:partial`. """ -function _explore(domains, f, s, ::Val{:partial}) - solutions = Set{Vector{Int}}() - non_sltns = Set{Vector{Int}}() +function _explore!(explorer, f, ::Val{:partial}) + sl = explorer.settings.solutions_limit + ms = explorer.settings.max_samplings - sl = s.solutions_limit + solutions = explorer.state.solutions + non_sltns = explorer.state.non_solutions + domains = explorer.domains |> values - for _ = 1:s.max_samplings + for _ = 1:ms length(solutions) ≥ sl && length(non_sltns) ≥ sl && break config = map(rand, domains) - c = f(config) ? solutions : non_sltns - length(c) < sl && push!(c, config) + update_exploration!(explorer, f, config) end - return solutions, non_sltns + return nothing end -function _explore(domains, f, ::ExploreSettings, ::Val{:complete}) - solutions = Set{Vector{Int}}() - non_sltns = Set{Vector{Int}}() - - configurations = Base.Iterators.product(map(d -> get_domain(d), domains)...) - foreach( - c -> (cv = collect(c); push!(f(cv) ? solutions : non_sltns, cv)), - configurations, - ) - return solutions, non_sltns +function _explore!(explorer, f, ::Val{:complete}) + C = Base.Iterators.product(map(d -> get_domain(d), explorer.domains |> values)...) + foreach(c -> update_exploration!(explorer, f, c, :complete), C) + return nothing end -function _explore(domains, f, s, ::Val{:flexible}) - search = s.max_samplings < s.complete_search_limit ? :complete : :partial - return _explore(domains, f, s, Val(search)) +function explore!(explorer::Explorer) + c = + x -> all([ + f(isempty(vars) ? x : @view x[vars]) for + (f, vars) in explorer.concepts |> values + ]) + s = explorer.settings + search = s.search + if search == :flexible + search = s.max_samplings < s.complete_search_limit ? :complete : :partial + end + return _explore!(explorer, c, Val(search)) end """ @@ -78,7 +167,9 @@ Beware that if the density of the solutions in the search space is low, `solutio """ function explore(domains, concept; settings = ExploreSettings(domains), parameters...) f = x -> concept(x; parameters...) - return _explore(domains, f, settings, Val(settings.search)) + explorer = Explorer([(f, Vector{Int}())], domains; settings) + explore!(explorer) + return explorer.state.solutions, explorer.state.non_solutions end ## SECTION - Test Items @@ -87,4 +178,6 @@ end X, X̅ = explore(domains, allunique) @test length(X) == factorial(4) @test length(X̅) == 4^4 - factorial(4) + + explorer = ConstraintDomains.Explorer() end diff --git a/src/general.jl b/src/general.jl index 2f30dc1..0bd7759 100644 --- a/src/general.jl +++ b/src/general.jl @@ -16,7 +16,7 @@ function Base.convert(::Type{Intervals}, d::RangeDomain{T}) where {T<:Real} return domain(Interval{T,Closed,Closed}(a, b)) end -function Base.convert(::Type{RangeDomain}, d::Intervals{T}) where {T<:Real} +function Base.convert(::Type{RangeDomain}, d::Intervals) i = get_domain(d)[1] a = Int(i.first) b = Int(i.last)