Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use coordinate descent for continuous variables #128

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
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
3 changes: 2 additions & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ julia = "1.6"

[extras]
Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595"
Intervals = "d8418881-c3e1-53bb-8760-2df7ec849ed5"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
TestItemRunner = "f8b46487-2199-4994-9208-9a1283c18c0a"

[targets]
test = ["Aqua", "Test", "TestItemRunner"]
test = ["Aqua", "Intervals", "Test", "TestItemRunner"]
1 change: 1 addition & 0 deletions src/LocalSearchSolvers.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ using Dictionaries
using Distributed
using JSON
using Lazy
using TestItems

# Exports internal
export constraint!, variable!, objective!, add!, add_var_to_cons!, add_value!
Expand Down
76 changes: 73 additions & 3 deletions src/solver.jl
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,72 @@ function _move!(s, x::Int, dim::Int = 0)
return best_values, best_swap, tabu
end

"""
armijo_line_search(f, x, d, fx; α0 = 1.0, β = 0.5, c = 1e-4)

Determines the optimal step size of a line search algorithm via the Armijo condition.
# Arguments:
- `f`: a function to minimize
- `x`: selected variable id
- `d`: descent direction (e.g., negative gradient)
- `fx`: value of f at `x`
- `α0`: initial step size
- `β`: step size reduction factor
- `c`: Armijo condition constant
"""
function armijo_line_search(f, x, d, fx, xmin, xmax; α0 = 1.0, β = 0.5, c = 1e-4)
α = α0
while true
new_x = x + α * d
# Ensure new_x is within [xmin, xmax]
if new_x < xmin
new_x = xmin
elseif new_x > xmax
new_x = xmax
end
if f(new_x) <= fx + c * α * d * fx || α < 1e-8
break
end
α *= β
end
return α
end

"""
_coordinate_descent_move!(s, x)

Runs an iteration of coordinate descent over axis "x".
The derivative is (temporarily?) computed via finite difference.
The step size is determined via the Armijo condition for line search.
"""
function _coordinate_descent_move!(s, x)
current_value = _value(s, x)
xmin = minimum(first, get_domain(s, x))
xmax = maximum(last, get_domain(s, x))

function f(val)
_value!(s, x, val)
_compute!(s)
return get_error(s)
end

current_error = f(current_value)
grad = (f(current_value + 1e-6) - f(current_value - 1e-6)) / (2e-6)

α = armijo_line_search(f, current_value, -grad, current_error, xmin, xmax)

new_value = clamp(current_value - α * grad,
minimum(first, get_domain(s, x)), maximum(last, get_domain(s, x)))

best_values = [new_value]

# revert to the original state
_value!(s, x, current_value)
_compute!(s)

return best_values, [x], false
end

"""
_step!(s)

Expand All @@ -346,9 +412,13 @@ function _step!(s)
x = _select_worse(s)
_verbose(s, "Selected x = $x")

# Local move (change the value of the selected variable)
best_values, best_swap, tabu = _move!(s, x)
# _compute!(s)
if typeof(get_variable(s, x).domain) <: ContinuousDomain
# We perform coordinate descent over the variable axis
best_values, best_swap, tabu = _coordinate_descent_move!(s, x)
else
# Local move (change the value of the selected variable)
best_values, best_swap, tabu = _move!(s, x)
end

# If local move is bad (tabu), then try permutation
if tabu
Expand Down
67 changes: 67 additions & 0 deletions test/raw_solver.jl
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,52 @@ function sudoku(n; start = nothing)
return m
end

function chemical_equilibrium(A, B, C)
m = model(; kind = :equilibrium)

N = length(C)
M = length(B)

d = domain(0 .. maximum(B))

# Add variables, number of moles per compound

foreach(_ -> variable!(m, d), 1:N)

# mass_conservation function
conserve = i -> (x -> begin
δ = abs(sum(A[:, i] .* x) - B[i])
return δ ≤ 1.e-6 ? 0.0 : δ
end
)

# Add constraints
for i in 1:M
constraint!(m, conserve(i), 1:N)
end

# computes the total energy freed by the reaction
free_energy = x -> sum(j -> x[j] * (C[j] + log(x[j] / sum(x))))

objective!(m, free_energy)

return m
end

function sum_squares(n)
m = model(; kind = :sum_squares)

d = domain(-10.0 .. 10.0)

foreach(_ -> variable!(m, d), 1:n)

ss = x -> sum(j -> j * x[j] * x[j], 1:n)

objective!(m, ss)

return m
end

@testset "Raw solver: internals" begin
models = [
sudoku(2)
Expand Down Expand Up @@ -203,3 +249,24 @@ end
@info "Sol (vals): $(!isnothing(best_value(s)) ? best_values(s) : nothing)"
@info time_info(s)
end

@testset "Raw solver: chemical equilibrium" begin
A = [2.0 1.0 3.0; 6.0 2.0 1.0; 1.0 2.0 4.0]
B = [20.0, 30.0, 25.0]
C = [-10.0, -8.0, -6.0]
m = chemical_equilibrium(A, B, C)
s = solver(m; options = Options(print_level = :minimal))
solve!(s)
display(solution(s))
display(s.time_stamps)
end

@testset "Raw solver: sum squares" begin
# Sanity check: simple quadratic function with a trivial minimum at 0
n = 10
m = sum_squares(n)
s = solver(m; options = Options(print_level = :minimal))
solve!(s)
display(solution(s))
display(s.time_stamps)
end
1 change: 1 addition & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import ConstraintDomains
import CompositionalNetworks
@everywhere using Constraints
using Dictionaries
using Intervals
@everywhere using LocalSearchSolvers
using Test
using TestItemRunner
Expand Down
Loading