diff --git a/.JuliaFormatter.toml b/.JuliaFormatter.toml index 545fb03..1515337 100644 --- a/.JuliaFormatter.toml +++ b/.JuliaFormatter.toml @@ -1,2 +1,2 @@ -align_assignment = true +align_assignment = true align_pair_arrow = true \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0f7af2f..aaf1904 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,33 +1,33 @@ -name: CI -on: - push: - branches: - - master - pull_request: - types: [opened, synchronize, reopened] -jobs: - test: - name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }} - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - include: - - version: '1' - os: ubuntu-latest - arch: x64 - - version: '1' - os: windows-latest - arch: x64 - steps: - - uses: actions/checkout@v2 - - uses: julia-actions/setup-julia@v1 - with: - version: ${{ matrix.version }} - arch: ${{ matrix.arch }} - - uses: julia-actions/julia-buildpkg@v1 - - uses: julia-actions/julia-runtest@v1 - # - uses: julia-actions/julia-processcoverage@v1 - # - uses: codecov/codecov-action@v2 - # with: +name: CI +on: + push: + branches: + - master + pull_request: + types: [opened, synchronize, reopened] +jobs: + test: + name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - version: '1' + os: ubuntu-latest + arch: x64 + - version: '1' + os: windows-latest + arch: x64 + steps: + - uses: actions/checkout@v2 + - uses: julia-actions/setup-julia@v1 + with: + version: ${{ matrix.version }} + arch: ${{ matrix.arch }} + - uses: julia-actions/julia-buildpkg@v1 + - uses: julia-actions/julia-runtest@v1 + # - uses: julia-actions/julia-processcoverage@v1 + # - uses: codecov/codecov-action@v2 + # with: # file: lcov.info \ No newline at end of file diff --git a/.github/workflows/tagbot.yml b/.github/workflows/tagbot.yml index 623860f..8f5ebd4 100644 --- a/.github/workflows/tagbot.yml +++ b/.github/workflows/tagbot.yml @@ -1,15 +1,15 @@ -name: TagBot -on: - issue_comment: - types: - - created - workflow_dispatch: -jobs: - TagBot: - if: github.event_name == 'workflow_dispatch' || github.actor == 'JuliaTagBot' - runs-on: ubuntu-latest - steps: - - uses: JuliaRegistries/TagBot@v1 - with: - token: ${{ secrets.GITHUB_TOKEN }} +name: TagBot +on: + issue_comment: + types: + - created + workflow_dispatch: +jobs: + TagBot: + if: github.event_name == 'workflow_dispatch' || github.actor == 'JuliaTagBot' + runs-on: ubuntu-latest + steps: + - uses: JuliaRegistries/TagBot@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} ssh: ${{ secrets.DOCUMENTER_KEY }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 29126e4..bc4047a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,24 +1,24 @@ -# Files generated by invoking Julia with --code-coverage -*.jl.cov -*.jl.*.cov - -# Files generated by invoking Julia with --track-allocation -*.jl.mem - -# System-specific files and directories generated by the BinaryProvider and BinDeps packages -# They contain absolute paths specific to the host computer, and so should not be committed -deps/deps.jl -deps/build.log -deps/downloads/ -deps/usr/ -deps/src/ - -# Build artifacts for creating documentation generated by the Documenter package -docs/build/ -docs/site/ - -# File generated by Pkg, the package manager, based on a corresponding Project.toml -# It records a fixed state of all packages used by the project. As such, it should not be -# committed for packages, but should be committed for applications that require a static -# environment. -Manifest.toml +# Files generated by invoking Julia with --code-coverage +*.jl.cov +*.jl.*.cov + +# Files generated by invoking Julia with --track-allocation +*.jl.mem + +# System-specific files and directories generated by the BinaryProvider and BinDeps packages +# They contain absolute paths specific to the host computer, and so should not be committed +deps/deps.jl +deps/build.log +deps/downloads/ +deps/usr/ +deps/src/ + +# Build artifacts for creating documentation generated by the Documenter package +docs/build/ +docs/site/ + +# File generated by Pkg, the package manager, based on a corresponding Project.toml +# It records a fixed state of all packages used by the project. As such, it should not be +# committed for packages, but should be committed for applications that require a static +# environment. +Manifest.toml diff --git a/LICENSE b/LICENSE index 851d806..f44c6b3 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,21 @@ -MIT License - -Copyright (c) 2022 PSR - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +MIT License + +Copyright (c) 2022 PSR + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Project.toml b/Project.toml index 9797c2d..9ee420a 100644 --- a/Project.toml +++ b/Project.toml @@ -5,13 +5,12 @@ version = "0.3.0" [deps] MQLib_jll = "4dedf8fe-8d9a-5fb8-8563-19379e8d5c54" +MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" -QUBODrivers = "a3f166f7-2cd3-47b6-9e1e-6fbfe0449eb0" -QUBOTools = "60eb5b62-0a39-4ddc-84c5-97d2adff9319" +QUBO = "ce8c2e91-a970-4681-856b-16178c24a30c" [compat] MQLib_jll = "0.1" Printf = "1" -QUBODrivers = "0.3" -QUBOTools = "0.9" +QUBO = "0.3.0" julia = "1.9" diff --git a/README.md b/README.md index 9b83ec6..35fd8cf 100644 --- a/README.md +++ b/README.md @@ -1,51 +1,51 @@ -# MQLib.jl -[![DOI](https://zenodo.org/badge/568213607.svg)](https://zenodo.org/badge/latestdoi/568213607) -[![QUBODRIVERS](https://img.shields.io/badge/Powered%20by-QUBODrivers.jl-%20%234063d8)](https://github.com/psrenergy/QUBODrivers.jl) - -[MQLib](https://github.com/MQLib/MQLib) wrapper for JuMP. - -## Installation -```julia -julia> import Pkg - -julia> Pkg.add("MQLib") -``` - -## Basic Usage -```julia -using JuMP, MQLib - -Q = [ - -1 2 2 - 2 -1 2 - 2 2 -1 -] - -model = Model(MQLib.Optimizer) - -@variable(model, x[1:3], Bin) -@objective(model, Max, x' * Q * x) - -optimize!(model) -``` - -## Selecting Heuristics - -This wrapper allows one to access all 39 QUBO and Max-Cut Heuristics provided by [MQLib](https://github.com/MQLib/MQLib). -Selecting the method to be used can be achieved via JuMP's attribute interface: - -```julia -JuMP.set_optimizer_attribute(model, "heuristic", "ALKHAMIS1998") -``` - -or by calling MQLib helper functions: - -```julia -MQLib.set_heuristic(model, "ALKHAMIS1998") -``` - -To list available heuristics and their descriptions, run: - -```julia -MQLib.show_heuristics() -``` +# MQLib.jl +[![DOI](https://zenodo.org/badge/568213607.svg)](https://zenodo.org/badge/latestdoi/568213607) +[![QUBODRIVERS](https://img.shields.io/badge/Powered%20by-QUBODrivers.jl-%20%234063d8)](https://github.com/psrenergy/QUBODrivers.jl) + +[MQLib](https://github.com/MQLib/MQLib) wrapper for JuMP. + +## Installation +```julia +julia> import Pkg + +julia> Pkg.add("MQLib") +``` + +## Basic Usage +```julia +using JuMP, MQLib + +Q = [ + -1 2 2 + 2 -1 2 + 2 2 -1 +] + +model = Model(MQLib.Optimizer) + +@variable(model, x[1:3], Bin) +@objective(model, Max, x' * Q * x) + +optimize!(model) +``` + +## Selecting Heuristics + +This wrapper allows one to access all 39 QUBO and Max-Cut Heuristics provided by [MQLib](https://github.com/MQLib/MQLib). +Selecting the method to be used can be achieved via JuMP's attribute interface: + +```julia +JuMP.set_optimizer_attribute(model, "heuristic", "ALKHAMIS1998") +``` + +or by calling MQLib helper functions: + +```julia +MQLib.set_heuristic(model, "ALKHAMIS1998") +``` + +To list available heuristics and their descriptions, run: + +```julia +MQLib.show_heuristics() +``` diff --git a/src/MQLib.jl b/src/MQLib.jl index eca84f8..8d7824f 100644 --- a/src/MQLib.jl +++ b/src/MQLib.jl @@ -1,228 +1,224 @@ -module MQLib - -using Printf - -import MQLib_jll -import QUBODrivers: - MOI, - QUBODrivers, - QUBOTools, - Sample, - SampleSet, - @setup, - sample - -const __VERSION__ = v"0.1.0" -const _HEURISTICS = Dict{String,String}() - -function __init__() - MQLib_jll.MQLib() do exe - ms = eachmatch(r"([a-zA-Z0-9]+)\r?\n\s+([^\r\n]+)\r?\n?", read(`$exe -l`, String)) - - for m in ms - push!(_HEURISTICS, m[1] => m[2]) - end - end - - return nothing -end - -@setup Optimizer begin - name = "MQLib" - version = __VERSION__ - attributes = begin - RandomSeed["seed"]::Union{Integer,Nothing} = nothing - NumberOfReads["num_reads"]::Integer = 1 - Heuristic["heuristic"]::Union{String,Nothing} = nothing - end -end - -function sample(sampler::Optimizer{T}) where {T} - n, L, Q, α, β = QUBOTools.qubo(sampler, :dict; sense = :max, domain = :bool) - - V = Set{Int}(1:n) - - model = QUBOTools.Model{Int,T,Int}( - V, L, Q; - scale = α, - offset = β, - sense = :max, - domain = :bool, - ) - - num_reads = MOI.get(sampler, MQLib.NumberOfReads()) - silent = MOI.get(sampler, MOI.Silent()) - heuristic = MOI.get(sampler, MQLib.Heuristic()) - random_seed = MOI.get(sampler, MQLib.RandomSeed()) - time_limit_sec = MOI.get(sampler, MOI.TimeLimitSec()) - - if num_reads <= 0 - error("Number of reads must be a positive integer") - end - - if !isnothing(heuristic) && !haskey(_HEURISTICS, heuristic) - error("Invalid QUBO Heuristic code '$heuristic'") - end - - if !isnothing(random_seed) - random_seed %= 65_536 - end - - run_time_limit = if isnothing(time_limit_sec) - 1.0 / num_reads - else - time_limit_sec / num_reads - end - - samples = Sample{T,Int}[] - metadata = Dict{String,Any}( - "time" => Dict{String,Any}(), - "origin" => Dict{String,Any}( - "name" => "MQLib", - "version" => __VERSION__, - "heuristic" => heuristic, - ), - ) - - mktempdir() do temp_path - file_path = joinpath(temp_path, "model.qubo") - - args = _mqlib_args(; - file_path, - heuristic, - random_seed, - run_time_limit, - ) - - QUBOTools.write_model(file_path, model, QUBOTools.QUBO(:mqlib)) - - MQLib_jll.MQLib() do exe - cmd = `$exe $args` - - _print_header(silent, heuristic) - - t = 0.0 - - for i = 1:num_reads - lines = readlines(cmd) - info = split(lines[begin], ',') - - λ = parse(T, info[4]) - ψ = parse.(Int, split(lines[end], ' ')) - s = Sample{T}(ψ, α * (λ + β)) - - push!(samples, s) - - m = collect(eachmatch(r"(([0-9]+):([0-9]+))+", info[6])) - λ̄ = parse.(Float64, getindex.(m, 2)) - t̄ = parse.(Float64, getindex.(m, 3)) - t += parse(Float64, info[5]) - - _print_iter(silent, i, λ̄, t .+ t̄) - end - - _print_footer(silent) - - metadata["time"]["effective"] = t - end - end - - return SampleSet{T}(samples, metadata; sense = :max, domain = :bool) -end - -function _print_header(silent::Bool, heuristic::Union{String,Nothing}) - if !silent - heuristic = something(heuristic, "Hyper-Heuristic") - - print( - """ - ▷ MQLib - ▷ Heuristic: $(heuristic) - ┌────────┬─────────────┬──────────┐ - │ iter │ value │ time │ - ├────────┼─────────────┼──────────┤ - """ - ) - end - - return nothing -end - -function _print_footer(silent::Bool) - if !silent - println( - """ - └────────┴─────────────┴──────────┘ - """ - ) - end - - return nothing -end - -function _print_iter(silent::Bool, i::Integer, λ::Vector{Float64}, t::Vector{Float64}) - if !silent - for (λ̄, t̄) in zip(λ, t) - if isnothing(i) - @printf("│ │ %11.3f │ %8.2f │\n", λ̄, t̄) - else - @printf("│ %6d │ %11.3f │ %8.2f │\n", i, λ̄, t̄) - - i = nothing - end - end - end - - return nothing -end - -function _mqlib_args(; - file_path::String, - heuristic::Union{String,Nothing}, - random_seed::Union{Integer,Nothing}, - run_time_limit::Float64, -) - args = `-fQ $file_path -r $run_time_limit -nv -ps` - - if !isnothing(random_seed) - args = `$args -s $random_seed` - end - - if isnothing(heuristic) - args = `$args -hh` - else - args = `$args -h $heuristic` - end - - return args -end - -function unset_heuristic(model) - set_heuristic(model, nothing) - - return nothing -end - -function set_heuristic(model, heuristic::Union{String,Nothing} = nothing) - MOI.set(model, MQLib.Heuristic(), heuristic) - - return nothing -end - -function get_heuristic(model) - return MOI.get(model, MQLib.Heuristic()) -end - -function heuristics() - return sort!(collect(keys(_HEURISTICS))) -end - -function show_heuristics() - for heuristic in heuristics() - println("$(heuristic):\n $(_HEURISTICS[heuristic])") - end - - return nothing -end - +module MQLib + +using Printf + +import MQLib_jll +import QUBO +import MathOptInterface as MOI +QUBODrivers = QUBO.QUBODrivers +QUBOTools = QUBO.QUBOTools + +const __VERSION__ = v"0.1.0" +const _HEURISTICS = Dict{String,String}() + +function __init__() + MQLib_jll.MQLib() do exe + ms = eachmatch(r"([a-zA-Z0-9]+)\r?\n\s+([^\r\n]+)\r?\n?", read(`$exe -l`, String)) + + for m in ms + push!(_HEURISTICS, m[1] => m[2]) + end + end + + return nothing +end + +QUBODrivers.@setup Optimizer begin + name = "MQLib" + version = __VERSION__ + attributes = begin + RandomSeed["seed"]::Union{Integer,Nothing} = nothing + NumberOfReads["num_reads"]::Integer = 1 + Heuristic["heuristic"]::Union{String,Nothing} = nothing + end +end + +function QUBODrivers.sample(sampler::Optimizer{T}) where {T} + n, L, Q, α, β = QUBOTools.qubo(sampler, :dict; sense = :max, domain = :bool) + + V = Set{Int}(1:n) + + model = QUBOTools.Model{Int,T,Int}( + V, L, Q; + scale = α, + offset = β, + sense = :max, + domain = :bool, + ) + + num_reads = MOI.get(sampler, MQLib.NumberOfReads()) + silent = MOI.get(sampler, MOI.Silent()) + heuristic = MOI.get(sampler, MQLib.Heuristic()) + random_seed = MOI.get(sampler, MQLib.RandomSeed()) + time_limit_sec = MOI.get(sampler, MOI.TimeLimitSec()) + + if num_reads <= 0 + error("Number of reads must be a positive integer") + end + + if !isnothing(heuristic) && !haskey(_HEURISTICS, heuristic) + error("Invalid QUBO Heuristic code '$heuristic'") + end + + if !isnothing(random_seed) + random_seed %= 65_536 + end + + run_time_limit = if isnothing(time_limit_sec) + 1.0 / num_reads + else + time_limit_sec / num_reads + end + + samples = QUBODrivers.Sample{T,Int}[] + metadata = Dict{String,Any}( + "time" => Dict{String,Any}(), + "origin" => Dict{String,Any}( + "name" => "MQLib", + "version" => __VERSION__, + "heuristic" => heuristic, + ), + ) + + mktempdir() do temp_path + file_path = joinpath(temp_path, "model.qubo") + + args = _mqlib_args(; + file_path, + heuristic, + random_seed, + run_time_limit, + ) + + QUBOTools.write_model(file_path, model, QUBOTools.QUBO(:mqlib)) + + MQLib_jll.MQLib() do exe + cmd = `$exe $args` + + _print_header(silent, heuristic) + + t = 0.0 + + for i = 1:num_reads + lines = readlines(cmd) + info = split(lines[begin], ',') + + λ = parse(T, info[4]) + ψ = parse.(Int, split(lines[end], ' ')) + s = QUBODrivers.Sample{T}(ψ, α * (λ + β)) + + push!(samples, s) + + m = collect(eachmatch(r"(([0-9]+):([0-9]+))+", info[6])) + λ̄ = parse.(Float64, getindex.(m, 2)) + t̄ = parse.(Float64, getindex.(m, 3)) + t += parse(Float64, info[5]) + + _print_iter(silent, i, λ̄, t .+ t̄) + end + + _print_footer(silent) + + metadata["time"]["effective"] = t + end + end + + return QUBOTools.SampleSet{T}(samples, metadata; sense = :max, domain = :bool) +end + +function _print_header(silent::Bool, heuristic::Union{String,Nothing}) + if !silent + heuristic = something(heuristic, "Hyper-Heuristic") + + print( + """ + ▷ MQLib + ▷ Heuristic: $(heuristic) + ┌────────┬─────────────┬──────────┐ + │ iter │ value │ time │ + ├────────┼─────────────┼──────────┤ + """ + ) + end + + return nothing +end + +function _print_footer(silent::Bool) + if !silent + println( + """ + └────────┴─────────────┴──────────┘ + """ + ) + end + + return nothing +end + +function _print_iter(silent::Bool, i::Integer, λ::Vector{Float64}, t::Vector{Float64}) + if !silent + for (λ̄, t̄) in zip(λ, t) + if isnothing(i) + @printf("│ │ %11.3f │ %8.2f │\n", λ̄, t̄) + else + @printf("│ %6d │ %11.3f │ %8.2f │\n", i, λ̄, t̄) + + i = nothing + end + end + end + + return nothing +end + +function _mqlib_args(; + file_path::String, + heuristic::Union{String,Nothing}, + random_seed::Union{Integer,Nothing}, + run_time_limit::Float64, +) + heur = if isnothing(heuristic) + `-hh` + else + `-h $heuristic` + end + + seed = if isnothing(random_seed) + `` + else + `-s $random_seed` + end + + return `$heur -fQ $file_path -r $run_time_limit -nv -ps $seed` +end + +function unset_heuristic(model) + set_heuristic(model, nothing) + + return nothing +end + +function set_heuristic(model, heuristic::Union{String,Nothing} = nothing) + MOI.set(model, MQLib.Heuristic(), heuristic) + + return nothing +end + +function get_heuristic(model) + return MOI.get(model, MQLib.Heuristic()) +end + +function heuristics() + return sort!(collect(keys(_HEURISTICS))) +end + +function show_heuristics() + for heuristic in heuristics() + println("$(heuristic):\n $(_HEURISTICS[heuristic])") + end + + return nothing +end + end # module \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index 73b8725..ab1c387 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,6 +1,6 @@ -import MQLib: MOI, MQLib, QUBODrivers - -QUBODrivers.test(MQLib.Optimizer) do model - MOI.set(model, MOI.Silent(), true) - MOI.set(model, MQLib.Heuristic(), first(MQLib.heuristics())) -end +import MQLib: MOI, MQLib, QUBO.QUBODrivers + +QUBODrivers.test(MQLib.Optimizer) do model + MOI.set(model, MOI.Silent(), true) + MOI.set(model, MQLib.Heuristic(), first(MQLib.heuristics())) +end