diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 580217d..a54786e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,10 +13,10 @@ jobs: fail-fast: false matrix: include: - - version: '1' + - version: '1.9' os: ubuntu-latest arch: x64 - - version: '1' + - version: '1.9' os: windows-latest arch: x64 steps: diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index ed69406..b167809 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -14,7 +14,7 @@ jobs: - name: Setup Julia uses: julia-actions/setup-julia@latest with: - version: '1.6' + version: '1.9' - name: Develop QUBODrivers.jl run: julia --project=docs/ -e 'using Pkg; Pkg.develop(path=pwd())' - name: Build Package diff --git a/Project.toml b/Project.toml index 6a7bbd1..91460a6 100644 --- a/Project.toml +++ b/Project.toml @@ -1,21 +1,16 @@ -name = "QUBODrivers" -uuid = "a3f166f7-2cd3-47b6-9e1e-6fbfe0449eb0" -version = "0.2.0" -authors = [ - "pedromxavier ", - "pedroripper ", - "AndradeTiago ", - "joaquimg ", - "bernalde ", -] +name = "QUBODrivers" +uuid = "a3f166f7-2cd3-47b6-9e1e-6fbfe0449eb0" +authors = ["pedromxavier ", "pedroripper ", "AndradeTiago ", "joaquimg ", "bernalde "] +version = "0.3.0" [deps] MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" -QUBOTools = "60eb5b62-0a39-4ddc-84c5-97d2adff9319" -Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +QUBOTools = "60eb5b62-0a39-4ddc-84c5-97d2adff9319" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [compat] MathOptInterface = "1" -QUBOTools = "0.8" -julia = "1.6" +QUBOTools = "~0.9" +julia = "1.9" diff --git a/docs/Project.toml b/docs/Project.toml index e111c56..a275929 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,13 +1,14 @@ [deps] -Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" -JuMP = "4076af6c-e467-56ae-b986-b466b2749572" +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +JuMP = "4076af6c-e467-56ae-b986-b466b2749572" MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" -Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" -QUBOTools = "60eb5b62-0a39-4ddc-84c5-97d2adff9319" +Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" +QUBODrivers = "a3f166f7-2cd3-47b6-9e1e-6fbfe0449eb0" +QUBOTools = "60eb5b62-0a39-4ddc-84c5-97d2adff9319" [compat] -Documenter = "0.27" -JuMP = "1" +Documenter = "1" +JuMP = "1" MathOptInterface = "1" -Plots = "1.38" -QUBOTools = "0.8" +Plots = "1.38" +QUBOTools = "0.9" diff --git a/docs/make.jl b/docs/make.jl index 857492d..e241621 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,16 +1,15 @@ using Documenter using QUBODrivers -using QUBOTools # Set up to run docstrings with jldoctest DocMeta.setdocmeta!(QUBODrivers, :DocTestSetup, :(using QUBODrivers); recursive = true) -DocMeta.setdocmeta!(QUBOTools, :DocTestSetup, :(using QUBOTools); recursive = true) makedocs(; - modules = [QUBODrivers], - doctest = true, - clean = true, - format = Documenter.HTML( + modules = [QUBODrivers, QUBODrivers.QUBOTools], + doctest = true, + clean = true, + warnonly = [:missing_docs], + format = Documenter.HTML( assets = ["assets/extra_styles.css"], #, "assets/favicon.ico"], mathengine = Documenter.KaTeX(), sidebar_sitename = false, @@ -18,16 +17,26 @@ makedocs(; sitename = "QUBODrivers.jl", authors = "Pedro Xavier and Pedro Ripper and Tiago Andrade and Joaquim Garcia and David Bernal", pages = [ - "Home" => "index.md", - # "Manual" => "manual.md", - # "Examples" => "examples.md", - # "Samplers" => "samplers.md", + "Home" => "index.md", + "Manual" => [ + "Introduction" => "manual/1-intro.md", + "Solving QUBO" => "manual/2-solve.md", + "Samplers" => "manual/3-samplers.md", + "Sampler Setup" => "manual/4-setup.md", + "Test Suite" => "manual/5-tests.md", + "Benchmarking" => "manual/6-benchmarks.md", + ], + "Booklet" => [ + "Itroduction" => "booklet/1-intro.md", + "Sampler Interface" => "booklet/2-interface.md", + "Attribute System" => "booklet/3-attributes.md", + ], ], - workdir = @__DIR__, + workdir = @__DIR__ ) if "--skip-deploy" ∈ ARGS @warn "Skipping deployment" else - deploydocs(repo = raw"github.com/psrenergy/QUBODrivers.jl.git", push_preview = true) + deploydocs(; repo = raw"github.com/psrenergy/QUBODrivers.jl.git", push_preview = true) end \ No newline at end of file diff --git a/docs/src/booklet/1-intro.md b/docs/src/booklet/1-intro.md new file mode 100644 index 0000000..1968e86 --- /dev/null +++ b/docs/src/booklet/1-intro.md @@ -0,0 +1,12 @@ +# QUBODrivers.jl Booklet + +This booklet aims to provide an advanced overview of the QUBODrivers.jl package, delving into the details of the package's inner workings. +It is meant to discuss the package's design choices and provide a guide for developers who wish not only to implement new interfaces but also to extend the package's functionality. +Reading this booklet is not required to use the package, but it is strongly recommended for those who wish to contribute to the project. + +## Table of Contents + +```@contents +Pages = ["2-interface.md", "3-attributes.md"] +Depth = 2 +``` diff --git a/docs/src/booklet/2-interface.md b/docs/src/booklet/2-interface.md new file mode 100644 index 0000000..960065c --- /dev/null +++ b/docs/src/booklet/2-interface.md @@ -0,0 +1,9 @@ +# Sampler Interface + +```@docs +QUBODrivers.AbstractSampler +``` + +```@docs +QUBODrivers.set_model! +``` diff --git a/docs/src/booklet/3-attributes.md b/docs/src/booklet/3-attributes.md new file mode 100644 index 0000000..f5a7636 --- /dev/null +++ b/docs/src/booklet/3-attributes.md @@ -0,0 +1,61 @@ +# Attributes + +## API + +```@docs +QUBODrivers.SamplerAttribute +``` + +```@docs +QUBODrivers.RawSamplerAttribute +QUBODrivers.@raw_attr_str +``` + +```@docs +QUBODrivers.get_raw_attr +QUBODrivers.set_raw_attr! +QUBODrivers.default_raw_attr +``` + +## An advanced example + +```julia +module SuperSampler + +import QUBODrivers +import QUBODrivers: QUBOTools +import MathOptInterface as MOI + +QUBODrivers.@setup Optimizer begin + name = "Super Sampler" + version = v"1.0.2" + attributes = begin + NumberOfReads["num_reads"]::Integer = 100_000 + SuperAttribute["super_attr"]::String = "super" + end +end + +function MOI.set(sampler::Optimizer, attr::raw_attr"", value) + if !(value isa Integer) + error("'num_reads' must be an integer") + else + QUBODrivers.set_raw_attr!(sampler, attr, value) + end + + return nothing +end + +function MOI.set(sampler::Optimizer, attr::raw_attr"super_attr", value) + if !(value isa AbstractString) + error("'super_attr' must be a string") + elseif !(value ∈ ("super", "ultra", "mega")) + error("'super_attr' must be one of the following: 'super', 'ultra', 'mega'") + else + QUBODrivers.set_raw_attr!(sampler, attr, value) + end + + return nothing +end + +end # SuperSampler module +``` diff --git a/docs/src/examples.md b/docs/src/examples.md deleted file mode 100644 index c9675c1..0000000 --- a/docs/src/examples.md +++ /dev/null @@ -1,49 +0,0 @@ -# Examples - -## Solving Simple QUBO Model with QUBODrivers's [`RandomSampler`](@ref random-sampler) - -```@example simple-workflow -using JuMP -using QUBODrivers - -model = Model(RandomSampler.Optimizer) - -Q = [ - -1.0 2.0 2.0 - 2.0 -1.0 2.0 - 2.0 2.0 -1.0 -] - -@variable(model, x[1:3], Bin) -@objective(model, Min, x' * Q * x) - -optimize!(model) -``` - -### Recover Results - -```@example simple-workflow -for i = 1:result_count(model) - # State vector - xi = value.(x; result=i) - - # Energy - yi = objective_value(model; result=i) - - # Sampling Frequency - ri = reads(model; result=i) - - println("f($xi) = $(yi)\t×$(ri)") -end -``` - -### Plot: Sampling distribution - -```@example simple-workflow -using Plots - -# Extract SampleSet -ω = sampleset(model) - -plot(ω) -``` \ No newline at end of file diff --git a/docs/src/index.md b/docs/src/index.md index 2517fea..cc384ab 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -1,21 +1,24 @@ # QUBODrivers.jl Documentation ## Introduction + This package aims to provide a common [MOI](https://github.com/jump-dev/MathOptInterface.jl)-compliant API for [QUBO](https://en.wikipedia.org/wiki/Quadratic_unconstrained_binary_optimization) Sampling & Annealing machines. It also contains a few utility samplers and testing tools for performance comparison, sanity checks and basic analysis features. ## Quick Start ### Installation + [QUBODrivers.jl](https://github.com/psrenergy/QUBODrivers.jl) is registered in Julia's General Registry and is available for download using the standard package manager. ```julia-repl julia> import Pkg julia> Pkg.add("QUBODrivers") -``` +``` ### Example + ```@example using JuMP using QUBODrivers @@ -36,22 +39,21 @@ optimize!(model) for i = 1:result_count(model) xi = value.(x; result=i) yi = objective_value(model; result=i) - ri = reads(model; result=i) - println("f($xi) = $yi ($ri)") + println("f($xi) = $yi") end ``` \ No newline at end of file diff --git a/docs/src/interface.md b/docs/src/interface.md deleted file mode 100644 index 0aa52a2..0000000 --- a/docs/src/interface.md +++ /dev/null @@ -1,83 +0,0 @@ -# A new Sampler -This guide aims to provide a tutorial on how to implement new sampler interfaces using [QUBODrivers.jl](https://github.com/psrenergy/QUBODrivers.jl). - -## The `@setup` macro -Using the [`QUBODrivers.@setup`](@ref setup-macro) macro is the most straightforward way to get your sampler running right now. -Apart from the macro call it is needed to implement the [`QUBODrivers.sample`](@ref) method. - -### I. Imports -First of all, we are going to import both `QUBODrivers.jl` and also `MathOptInterface.jl`, commonly aliased as `MOI`. -```julia -import QUBODrivers -import MathOptInterface -const MOI = MathOptInterface -``` - -### II. `@setup` -This macro takes two arguments: the identifier of the sampler's `struct`, and a `begin...end` block containing configuration parameters as *key-value* pairs. -If ommited, the first defaults to `Optimizer`, following regular `MOI` conventions. -In order to work smoothly, this approach leverages the [`QUBOTools`](https://github.com/psrenergy/QUBOTools.jl) backend. - -We expect that most users will be happy with this approach and it is likely that it will be improved and receive support very often. - -## MathOptInterface API Coverage -This Document is intended to help keeping track of which MOI API Methods and Properties have been implemented for a new solver or model interface. - -### Reference: -[jump.dev/MathOptInterface.jl/stable/tutorials/implementing/](https://jump.dev/MathOptInterface.jl/stable/tutorials/implementing/) - -### Optimizer Interface -| Method | Status | -| :-------------------------------------------- | :----: | -| `MOI.empty!(::Optimizer)` | ✅ | -| `MOI.is_empty(::Optimizer)::Bool` | ✅ | -| `MOI.optimize!(::Optimizer, ::MOI.ModelLike)` | ✅ | -| `Base.show(::IO, ::Optimizer)` | ✔️ | - -### The `copy_to` interface -| Method | Status | -| :------------------------------------------ | :----: | -| `MOI.copy_to(::Optimizer, ::MOI.ModelLike)` | ✅ | - -### Constraint Support -| Method | Status | -| :------------------------------------------------------------------ | :----: | -| `MOI.supports_constraint(::Optimizer, ::F, ::S)::Bool where {F, S}` | ✔️ | - -## Attributes -| Property | Type | `get` | `set` | `supports` | -| :-------------------------- | :-------- | :---: | :---: | :--------: | -| `MOI.SolverName` | `String` | Ⓜ️ | - | - | -| `MOI.SolverVersion` | `String` | Ⓜ️ | - | - | -| `MOI.RawSolver` | `String` | ✔️ | - | - | -| `MOI.Name` | `String` | Ⓜ️ | Ⓜ️ | Ⓜ️ | -| `MOI.Silent` | `Bool` | Ⓜ️ | Ⓜ️ | Ⓜ️ | -| `MOI.TimeLimitSec` | `Float64` | Ⓜ️ | Ⓜ️ | Ⓜ️ | -| `MOI.RawOptimizerAttribute` | `Any` | Ⓜ️ | Ⓜ️ | Ⓜ️ | -| `MOI.NumberOfThreads` | `Int` | Ⓜ️ | Ⓜ️ | Ⓜ️ | - -## Solution -| Property | Type | `get` | `set` | `supports` | -| :---------------------- | :-------------------------- | :---: | :---: | :--------: | -| `MOI.PrimalStatus` | `MOI.ResultStatusCode` | Ⓜ️ | - | - | -| `MOI.DualStatus` | `MOI.ResultStatusCode` | Ⓜ️ | - | - | -| `MOI.RawStatusString` | `String` | Ⓜ️ | - | - | -| `MOI.ResultCount` | `Int` | Ⓜ️ | - | - | -| `MOI.TerminationStatus` | `MOI.TerminationStatusCode` | Ⓜ️ | - | - | -| `MOI.ObjectiveValue` | `T` | Ⓜ️ | - | - | -| `MOI.SolveTimeSec` | `Float64` | Ⓜ️ | - | - | -| `MOI.VariablePrimal` | `T` | Ⓜ️ | - | - | - -## Warm Start -| Property | Type | `get` | `set` | `supports` | -| :------------------------ | :---: | :---: | :---: | :--------: | -| `MOI.VariablePrimalStart` | `T` | Ⓜ️ | Ⓜ️ | Ⓜ️ | - -## Key -| Symbol | Meaning | -| :----: | :------------------------------------------------ | -| Ⓜ️ | Implemented via the [`@setup`]() macro | -| ✅ | Available for [`Sampler{T}`]() | -| ✔️ | Available for [`AbstracSampler{T}`]() | -| ⚠️ | Must be implemented | -| ❌ | Not implemented, but you can do it if you want to | \ No newline at end of file diff --git a/docs/src/manual.md b/docs/src/manual.md deleted file mode 100644 index 1c63e4d..0000000 --- a/docs/src/manual.md +++ /dev/null @@ -1,203 +0,0 @@ -# Manual - -## Introduction -The core idea behind this package is to provide a toolbox for developing and integrating [QUBO](https://en.wikipedia.org/wiki/Quadratic_unconstrained_binary_optimization) sampling tools with the [JuMP](https://jump.dev) mathematical programming environment. -Appart from the few couple exported utility engines, QUBODrivers.jl is inherently about extensions, which is achieved by implementing most of the [MOI](https://jump.dev/MathOptInterface.jl) requirements, leaving only the essential for the developer. - -### QUBO -An optimization problem is in its QUBO form if it is written as - -```math -\begin{array}{rl} - \min & \alpha \left[ \mathbf{x}'\mathbf{Q}\,\mathbf{x} + \mathbf{\ell}'\mathbf{x} + \beta \right] \\ - \text{s.t.} & \mathbf{x} \in S \cong \mathbb{B}^{n} -\end{array} -``` -with linear terms ``\mathbf{\ell} \in \mathbb{R}^{n}`` and quadratic ``\mathbf{Q} \in \mathbb{R}^{n \times n}``. ``\alpha, \beta \in \mathbb{R}`` are, respectively, the scaling and offset factors. - -The MOI-JuMP optimizers defined using the `QUBODrivers.AbstractSampler{T} <: MOI.AbstractOptimizer` interface only support models given in the QUBO form. -`QUBODrivers.jl` employs [QUBOTools](https://github.com/psrenergy/QUBOTools.jl) on many tasks involving data management and querying. -It is worth taking a look at [QUBOTool's docs](https://psrenergy.github.io/QUBOTools.jl). - -## Defining a new sampler interface - -### Showcase -Before explaining in detail how to use this package, it's good to list a few examples for the reader to grasp. -Below, there are links to the files where the actual interfaces are implemented, including thin wrappers, interfacing with Python and Julia implementations of common algorithms and heuristics. - -| Project | Source Code | -| :---------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------- | -| [DWaveNeal.jl](https://github.com/psrenergy/DWaveNeal.jl) | [DWaveNeal](https://github.com/psrenergy/DWaveNeal.jl/blob/main/src/DWaveNeal.jl) | -| [IsingSolvers.jl](https://github.com/psrenergy/IsingSolvers.jl) | [GreedyDescent](https://github.com/psrenergy/IsingSolvers.jl/blob/main/src/solvers/greedy_descent.jl) | -| | [ILP](https://github.com/psrenergy/IsingSolvers.jl/blob/main/src/solvers/ilp.jl) | -| | [MCMCRandom](https://github.com/psrenergy/IsingSolvers.jl/blob/main/src/solvers/mcmc_random.jl) | -| [QuantumAnnealingInterface.jl](https://github.com/psrenergy/QuantumAnnealingInterface.jl) | [QuantumAnnealingInterface](https://github.com/psrenergy/QuantumAnnealingInterface.jl/blob/main/src/QuantumAnnealingInterface.jl) | - -### The [`@setup`](@id setup) macro -`QUBODrivers.@setup` is available to speed up the interface setup process. -This mechanism was created to reach the majority of the target audience, that is, researchers interested in integrating their QUBO/Ising samplers in a common optimization ecossystem. - -```@docs -QUBODrivers.@setup -``` - -Inside a module scope for the new interface, one should call the [`QUBODrivers.@setup`](@ref setup) macro, specifying the solver's attributes as described in the macro's docs. -The second and last step is to define the `QUBODrivers.sample(::Optimizer)` method, that must return a [`QUBODrivers.SampleSet`](@ref sampleset). - -Using it might be somehow restrictive in comparison to the regular [JuMP/MOI Solver Interface workflow](https://jump.dev/MathOptInterface.jl/stable/tutorials/implementing/). -Yet, our guess is that most of this package's users are not considering going deeper into the MOI internals that soon. - -### [`@setup`](@ref setup-macro) example -The following example is intended to illustrate the usage of the macro, showing how simple it should be to implement a wrapper for a sampler implemented in another language such as C or C++. - -```julia -module SuperSampler - using QUBODrivers - - QUBODrivers.@setup Optimizer begin - name = "Super Sampler" - sense = :max - domain = :spin - version = v"1.0.2" - attributes = begin - SuperAttribute::String = "super" - NumberOfReads["num_reads"]::Integer = 1_000 - end - end - - model = Model(SuperSampler.Optimizer) - - @variable(model, x[1:n], Bin) - @objective(model, Min, x' * Q * x) - - function QUBODrivers.sample(sampler::Optimizer{T}) where {T} - # ~ Is your annealer running on the Ising Model? Have this: - h, J, u, v = QUBODrivers.ising( - sampler, - Vector, # Here we opt for a sparse, vector representation - ) - - n = MOI.get(sampler, MOI.NumberOfVariables()) - - # ~ Retrieve Attributes ~ # - num_reads = MOI.get(sampler, NumberOfReads()) - @assert num_reads > 0 - - super_attr = MOI.get(sampler, SuperAttribute()) - @assert super_attr ∈ ("super", "ultra", "mega") - - # ~*~ Timing Information ~*~ # - time_data = Dict{String,Any}() - - # ~*~ Run Algorithm ~*~ # - result = @timed Vector{Int}[ - super_sample(h, J, u, v; attr=super_attr) - for _ = 1:num_reads - ] - states = result.value - - # ~*~ Record Time ~*~ # - time_data["effective"] = result.time - - metadata = Dict{String,Any}( - "time" => time_data, - "origin" => "Super Sampling method" - ) - - # ~ Here some magic happens: - # By providing the sampler and a vector of states, - # QUBODrivers.jl computes the energy and organizes your - # solutions automatically, following the variable - # domain conventions specified previously. - return QUBODrivers.SampleSet{T}(sampler, states, metadata) - end - - function super_sample(h, J, u, v; super_attr, kws...) - return ccall( - :super_sample, - Vector{Int}, - ( - Ptr{Cdouble}, - Ptr{Cdouble}, - Ptr{Cdouble}, - Ptr{Cdouble}, - Cint, - Cstring, - ), - h, - J, - u, - v, - super_attr, - ) - end -end -``` - -### Walkthrough -Now, it's time to go through the example in greater detail. -First of all, the entire work must be done within a module. - -```julia -module SuperSampler - using QUBODrivers -``` - -By provding the `using QUBODrivers` statement, very little will be dumped into the namespace apart from the `MOI = MathOptInterface` constant. -MOI's methods will soon be very important to access our optimizer's attributes. - -```julia -QUBODrivers.@setup Optimizer begin - name = "Super Sampler" - sense = :max - domain = :spin - version = v"1.0.2" - attributes = begin - SuperAttribute::String = "super" - NumberOfReads["num_reads"]::Integer = 1_000 - end -end -``` - -The first parameter in the `@setup` call is the optimizer's identifier. -It defaults to `Optimizer` and, in this case, is responsible for defining the `Optimizer{T} <: MOI.AbstractOptimizer` struct. -A `begin...end` block comes next, with a few key-value pairs. - -!!! info - Our solver, when deployed to be used within JuMP, will probably have its users to follow the usual construct: - - ```julia - using JuMP - using SuperSampler - - model = Model(SuperSampler.Optimizer) - ``` - -The solver `name` must be a string, and will be used as the return value for [`MOI.get(::Optimizer, ::MOI.SolverName())`](https://jump.dev/MathOptInterface.jl/stable/reference/models/#MathOptInterface.SolverName). - -```julia -name = "Super Sampler" -``` - -The `sense` and `domain` values indicate how our new solvers expect its models to be presented and, even more importantly, how the resulting samples should be interpreted. -Their values must be either `:min` or `:max` and `:boll` or `:spin`, respectively. -Strings, symbols and literals are supported as input for these fields. - -```julia -sense = :max -domain = :spin -``` - -The other metadata entry is the `version` assignment, which is returned by [`MOI.get(::Optimizer, ::MOI.SolverVersion())`](https://jump.dev/MathOptInterface.jl/stable/reference/models/#MathOptInterface.SolverVersion). -In order to consistently support [semantic versioning](https://semver.org/) it is required that the version number comes as a _v-string_ e.g. `v"major.minor.patch"`. - -```julia -version = v"1.0.2" -``` - -# Model Mapping - -# Automatic Tests -```@docs -QUBODrivers.test -``` \ No newline at end of file diff --git a/docs/src/manual/1-intro.md b/docs/src/manual/1-intro.md new file mode 100644 index 0000000..eea99b5 --- /dev/null +++ b/docs/src/manual/1-intro.md @@ -0,0 +1,25 @@ +# Introduction + +## QUBO + +An optimization problem is in its QUBO form if it is written as + +```math +\begin{array}{rl} + \min & \alpha \left[ \mathbf{x}'\mathbf{Q}\,\mathbf{x} + \mathbf{\ell}'\mathbf{x} + \beta \right] \\ + \text{s.t.} & \mathbf{x} \in S \cong \mathbb{B}^{n} +\end{array} +``` + +with linear terms ``\mathbf{\ell} \in \mathbb{R}^{n}`` and quadratic ``\mathbf{Q} \in \mathbb{R}^{n \times n}``. ``\alpha, \beta \in \mathbb{R}`` are, respectively, the scaling and offset factors. + +The MOI-JuMP optimizers defined using the `QUBODrivers.AbstractSampler{T} <: MOI.AbstractOptimizer` interface only support models given in the QUBO form. +`QUBODrivers.jl` employs [QUBOTools](https://github.com/psrenergy/QUBOTools.jl) on many tasks involving data management and querying. +It is worth taking a look at [QUBOTool's docs](https://psrenergy.github.io/QUBOTools.jl). + +## Table of Contents + +```@contents +Pages = ["2-solve.md", "3-samplers.md", "4-setup.md", "5-tests.md", "6-benchmarks.md"] +Depth = 2 +``` diff --git a/docs/src/manual/2-solve.md b/docs/src/manual/2-solve.md new file mode 100644 index 0000000..c23f9ea --- /dev/null +++ b/docs/src/manual/2-solve.md @@ -0,0 +1,32 @@ +# Solving QUBO + +## Solving Simple QUBO Model with QUBODrivers' [`RandomSampler.Optimizer`](@ref) + +```@example simple-workflow +using JuMP +using QUBODrivers + +model = Model(RandomSampler.Optimizer) + +Q = [ + -1.0 2.0 2.0 + 2.0 -1.0 2.0 + 2.0 2.0 -1.0 +] + +@variable(model, x[1:3], Bin) +@objective(model, Min, x' * Q * x) + +optimize!(model) +``` + +### Recover Results + +```@example simple-workflow +for i = 1:result_count(model) + xi = value.(x; result=i) # Solution vector + yi = objective_value(model; result=i) # Energy + + println("f($xi) = $(yi)") +end +``` diff --git a/docs/src/manual/3-samplers.md b/docs/src/manual/3-samplers.md new file mode 100644 index 0000000..d9ad734 --- /dev/null +++ b/docs/src/manual/3-samplers.md @@ -0,0 +1,37 @@ +# Samplers + +## Utility Samplers + +### Exact Sampler + +```@docs +QUBODrivers.ExactSampler.Optimizer +``` + +### Random Sampler + +```@docs +QUBODrivers.RandomSampler.Optimizer +``` + +### Identity Sampler + +```@docs +QUBODrivers.IdentitySampler.Optimizer +``` + +## Showcase + +Before explaining in detail how to use this package, it's good to list a few examples for the reader to grasp. +Below, there are links to the files containing the actual interface implementations. +These are mostly thin wrappers interfacing with common algorithms and heuristics written in Python, Julia or C/C++. + +| Project | Source Code | +| :---------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------- | +| [DWave.jl](https://github.com/psrenergy/DWave.jl) | [DWave](https://github.com/psrenergy/DWave.jl/blob/main/src/DWave.jl) | +| [DWaveNeal.jl](https://github.com/psrenergy/DWaveNeal.jl) | [DWaveNeal](https://github.com/psrenergy/DWaveNeal.jl/blob/main/src/DWaveNeal.jl) | +| [IsingSolvers.jl](https://github.com/psrenergy/IsingSolvers.jl) | [GreedyDescent](https://github.com/psrenergy/IsingSolvers.jl/blob/main/src/solvers/greedy_descent.jl) | +| | [ILP](https://github.com/psrenergy/IsingSolvers.jl/blob/main/src/solvers/ilp.jl) | +| | [MCMCRandom](https://github.com/psrenergy/IsingSolvers.jl/blob/main/src/solvers/mcmc_random.jl) | +| [QuantumAnnealingInterface.jl](https://github.com/psrenergy/QuantumAnnealingInterface.jl) | [QuantumAnnealingInterface](https://github.com/psrenergy/QuantumAnnealingInterface.jl/blob/main/src/QuantumAnnealingInterface.jl) | +| [CIMOptimizer.jl](https://github.com/pedromxavier/CIMOptimizer.jl) | [CIMOptimizer](https://github.com/pedromxavier/CIMOptimizer.jl/blob/main/src/CIMOptimizer.jl) | diff --git a/docs/src/manual/4-setup.md b/docs/src/manual/4-setup.md new file mode 100644 index 0000000..36aacbc --- /dev/null +++ b/docs/src/manual/4-setup.md @@ -0,0 +1,148 @@ +# Sampler Setup + +This guide aims to provide a tutorial on how to implement new sampler interfaces using [QUBODrivers.jl](https://github.com/psrenergy/QUBODrivers.jl). +To get your QUBO sampler running right now, [QUBODrivers.jl](https://github.com/psrenergy/QUBODrivers.jl) will require only two main ingredients: a [`QUBODrivers.@setup`](@ref) macro call and a [`QUBODrivers.sample`](@ref) method implementation. + +## Imports + +First things first, we are going to import both [QUBODrivers.jl](https://github.com/psrenergy/QUBODrivers.jl) and also [MathOptInterface.jl](https://github.com/jump-dev/MathOptInterface.jl), commonly aliased as `MOI`. +Although not strictly necessary, we recommend that you also import [QUBOTools.jl](https://github.com/psrenergy/QUBOTools.jl)for convenience, as it provides many useful functions for QUBO manipulation. +It is readly available in the `QUBODrivers` module. + +```julia +import QUBODrivers +import QUBODrivers: QUBOTools +import MathOptInterface as MOI +``` + +## The [`QUBODrivers.@setup`](@ref) macro + +```@docs +QUBODrivers.@setup +``` + +This macro takes two arguments: the identifier of the sampler's `struct` (usually `Optimizer`), and a `begin...end` block containing configuration parameters as *key-value* pairs. + +The first parameter of the configuration block is the sampler's name, which will be used to identify it in the `MOI.SolverName` attribute. + +The next entry is the `version` assignment, which is accessed by the `MOI.SolverVersion` attribute. +In order to consistently support [semantic versioning](https://semver.org/) it is required that the version number comes as a *v-string* e.g. `v"major.minor.patch"`. + +!!! note + If missing, the `version` parameter matches the current version of `QUBODrivers.jl`. + +A simple yet valid `@setup` call would look like this: + +```julia +QUBODrivers.@setup Optimizer begin + name = "Super Sampler" + version = v"1.0.2" +end +``` + +We expect that most users will be happy with this approach and it is likely that it will fit most use cases. + +### Attributes + +The `attributes` parameter is also given by a `begin...end` block and contains the sampler's attributes. +These attributes are used to configure the sampler's behavior and are accessed by the `MOI.get` method. + +```julia +QUBODrivers.@setup Optimizer begin + name = "Super Sampler" + version = v"1.0.2" + attributes = begin + NumberOfReads["num_reads"]::Integer = 1_000 + SuperAttribute::String = "super" + end +end +``` + +## The [`QUBODrivers.sample`](@ref) method + +```@docs +QUBODrivers.sample +``` + +### The [`QUBODrivers.SampleSet`] collection + +## A complete example + +```julia +module SuperSampler + +import QUBODrivers +import QUBODrivers: QUBOTools +import MathOptInterface as MOI + +@doc raw""" + SuperSampler.Optimizer + +This sampler is super! +""" +QUBODrivers.@setup Optimizer begin + name = "Super Sampler" + version = v"1.0.2" + attributes = begin + NumberOfReads["num_reads"]::Integer = 1_000 + SuperAttribute::String = "super" + end +end + +function QUBODrivers.sample(sampler::Optimizer{T}) where {T} + # ~ Is your annealer running on the Ising Model? Have this: + n, h, J, α, β = QUBOTools.ising( + sampler, + :dense; # Here we opt for a dense matrix representation + sense = :max, + ) + + # ~ Retrieve Attributes using MathOptInterface ~ # + num_reads = MOI.get(sampler, NumberOfReads()) + super_attr = MOI.get(sampler, SuperAttribute()) + + # ~ Do some sampling ~ # + samples = QUBOTools.Sample{T,Int}[] + + clock = @timed for _ = 1:num_reads + ψ = super_sample(n, h, J, super_attr) + λ = QUBOTools.value(ψ, h, J, α, β) + + s = QUBOTools.Sample{T,Int}(ψ, λ) + + push!(samples, s) + end + + # ~ Store some metadata ~ # + metadata = Dict{String,Any}( + "num_reads" => num_reads, + "super_attr" => super_attr, + "time" => clock.time, + ) + + # ~ Return a SampleSet ~ # + return QUBOTools.SampleSet(samples, metadata; sense=:max, domain=:spin) +end + +function super_sample(n, h, J, super_attr) + # ~ Do some super sampling (using C/C++) ~ # + ψ = ccall( + :super_sample, + Vector{Int}, + ( + Cint, + Ptr{Float64}, + Ptr{Ptr{Float64}}, + Cstring + ), + n, + h, + J, + super_attr, + ) + + return ψ +end + +end # module +``` diff --git a/docs/src/manual/5-tests.md b/docs/src/manual/5-tests.md new file mode 100644 index 0000000..09157a4 --- /dev/null +++ b/docs/src/manual/5-tests.md @@ -0,0 +1,7 @@ +# Test Suite + +Besides establishing the connection between QUBO solvers and JuMP, this package also provides a test suite to ensure that the interface is implemented correctly. + +```@docs +QUBODrivers.test +``` diff --git a/docs/src/manual/6-benchmarks.md b/docs/src/manual/6-benchmarks.md new file mode 100644 index 0000000..af54b6e --- /dev/null +++ b/docs/src/manual/6-benchmarks.md @@ -0,0 +1 @@ +# Benchmarking diff --git a/docs/src/samplers.md b/docs/src/samplers.md deleted file mode 100644 index 3a70cf9..0000000 --- a/docs/src/samplers.md +++ /dev/null @@ -1,24 +0,0 @@ -# Samplers - -## [Abstract Sampler](@id abstract-sampler) -```@docs -QUBODrivers.AbstractSampler -``` - -## [Identity Sampler](@id identity-sampler) - -```@docs -QUBODrivers.IdentitySampler.Optimizer -``` - -## [Exact Sampler](@id exact-sampler) - -```@docs -QUBODrivers.ExactSampler.Optimizer -``` - -## [Random Sampler](@id random-sampler) - -```@docs -QUBODrivers.RandomSampler.Optimizer -``` \ No newline at end of file diff --git a/src/QUBODrivers.jl b/src/QUBODrivers.jl index 7b48dc5..baac227 100644 --- a/src/QUBODrivers.jl +++ b/src/QUBODrivers.jl @@ -10,25 +10,33 @@ const SAT{T} = MOI.ScalarAffineTerm{T} const SQF{T} = MOI.ScalarQuadraticFunction{T} const SQT{T} = MOI.ScalarQuadraticTerm{T} -import QUBOTools: QUBOTools, Sample, SampleSet, qubo, ising, ↑, ↓ +using QUBOTools +const Spin = QUBOTools.__moi_spin_set() -export MOI, Sample, SampleSet, Spin, qubo, ising, ↑, ↓ +using TOML +const __PROJECT__ = joinpath(@__DIR__, "..", "Project.toml") +const __VERSION__ = VersionNumber(TOML.parsefile(__PROJECT__)["version"]) -include("abstract/interface.jl") -include("abstract/wrapper.jl") +export MOI, Sample, SampleSet, Spin, ↓, ↑ -include("automatic/interface.jl") -include("automatic/attributes.jl") -include("automatic/setup.jl") -include("automatic/sample.jl") -include("automatic/wrapper.jl") +include("interface/sampler.jl") +include("interface/attributes.jl") -include("test/test.jl") +include("library/sampler/wrappers/moi.jl") +include("library/sampler/wrappers/qubotools.jl") + +include("library/test/test.jl") + +include("library/setup/error.jl") +include("library/setup/specs.jl") +include("library/setup/parse.jl") +include("library/setup/quote.jl") +include("library/setup/macro.jl") export ExactSampler, IdentitySampler, RandomSampler -include("drivers/ExactSampler.jl") -include("drivers/IdentitySampler.jl") -include("drivers/RandomSampler.jl") +include("library/drivers/ExactSampler.jl") +include("library/drivers/IdentitySampler.jl") +include("library/drivers/RandomSampler.jl") end # module QUBODrivers diff --git a/src/abstract/interface.jl b/src/abstract/interface.jl deleted file mode 100644 index 7fe8d9e..0000000 --- a/src/abstract/interface.jl +++ /dev/null @@ -1,13 +0,0 @@ -@doc raw""" - AbstractSampler{T} <: MOI.AbstractOptimizer -""" -abstract type AbstractSampler{T} <: MOI.AbstractOptimizer end - -@doc raw""" - sample(::AbstractSampler{T})::SampleSet{T} where {T} -""" -function sample end - -function sample(::AbstractSampler{T})::SampleSet{T} where {T} - return SampleSet{T}() -end \ No newline at end of file diff --git a/src/abstract/wrapper.jl b/src/abstract/wrapper.jl deleted file mode 100644 index a5f4e4d..0000000 --- a/src/abstract/wrapper.jl +++ /dev/null @@ -1,326 +0,0 @@ -@doc raw""" - Spin() - -The set ``\left\lbrace{}{-1, 1}\right\rbrace{}``. -""" struct Spin <: MOI.AbstractScalarSet end - -function MOIU._to_string(options::MOIU._PrintOptions, ::Spin) - return string(MOIU._to_string(options, ∈), " {-1, 1}") -end - -function MOIU._to_string(::MOIU._PrintOptions{MIME"text/latex"}, ::Spin) - return raw"\in \left\lbrace{}{-1, 1}\right\rbrace{}" -end - -# ~ Currently, all models in this context are unconstrained by definition. -MOI.supports_constraint( - ::AbstractSampler, - ::Type{<:MOI.AbstractFunction}, - ::Type{<:MOI.AbstractSet}, -) = false - -# ~ They are also binary -MOI.supports_constraint( - ::AbstractSampler, - ::Type{<:MOI.VariableIndex}, - ::Type{<:MOI.ZeroOne}, -) = true - -MOI.supports_constraint( - ::AbstractSampler, - ::Type{<:MOI.VariableIndex}, - ::Type{<:Spin}, -) = true - -# ~ Objective Function Support -MOI.supports( - ::AbstractSampler, - ::MOI.ObjectiveFunction{<:Any} -) = false - -MOI.supports( - ::AbstractSampler{T}, - ::MOI.ObjectiveFunction{<:Union{SQF{T}, SAF{T}, VI}} -) where {T} = true - -# By default, all samplers are their own raw solvers. -MOI.get(sampler::AbstractSampler, ::MOI.RawSolver) = sampler - -# Since problems are unconstrained, all available solutions are feasible. -function MOI.get(sampler::AbstractSampler, ps::MOI.PrimalStatus) - n = MOI.get(sampler, MOI.ResultCount()) - i = ps.result_index - - if 1 <= i <= n - return MOI.FEASIBLE_POINT - else - return MOI.NO_SOLUTION - end -end - -MOI.get(::AbstractSampler, ::MOI.DualStatus) = MOI.NO_SOLUTION - -function reads(model; result::Integer = 1) - return QUBOTools.reads(model, result) -end - -function QUBOTools.Sense(sense::MOI.OptimizationSense) - if sense === MOI.MIN_SENSE - return QUBOTools.Sense(:min) - elseif sense === MOI.MAX_SENSE - return QUBOTools.Sense(:max) - else - error("Invalid sense for QUBO: '$sense'") - end -end - -@doc raw""" - parse_model(model::MOI.ModelLike) - parse_model(T::Type, model::MOI.ModelLike) - -If the given model is ready to be interpreted as a QUBO model, then returns the corresponding `QUBOTools.StandardQUBOModel`. - -A few conditions must be met: - 1. All variables must be binary of a single kind (`VI ∈ MOI.ZeroOne` or `VI ∈ Spin`) - 2. No other constraints are allowed - 3. The objective function must be of type `MOI.ScalarQuadraticFunction`, `MOI.ScalarAffineFunction` or `MOI.VariableIndex` - 4. The objective sense must be either `MOI.MIN_SENSE` or `MOI.MAX_SENSE` -""" function parse_model end - -function parse_model(model::MOI.ModelLike) - return parse_model(Float64, model) -end - -function __is_quadratic(model::MOI.ModelLike) - return MOI.get(model, MOI.ObjectiveFunctionType()) <: Union{SQF,SAF,VI} -end - -function __is_unconstrained(model::MOI.ModelLike) - for (F, S) in MOI.get(model, MOI.ListOfConstraintTypesPresent()) - if !(F === VI && (S === MOI.ZeroOne || S === Spin)) - return false - end - end - - return true -end - -function __is_optimization(model::MOI.ModelLike) - S = MOI.get(model, MOI.ObjectiveSense()) - - return (S === MOI.MAX_SENSE || S === MOI.MIN_SENSE) -end - -function __extract_model( - ::Type{T}, - Ω::Set{VI}, - model::MOI.ModelLike, - ::QUBOTools.BoolDomain, -) where {T} - L = Dict{VI,T}(xi => zero(T) for xi ∈ Ω) - Q = Dict{Tuple{VI,VI},T}() - - offset = zero(T) - - F = MOI.get(model, MOI.ObjectiveFunctionType()) - f = MOI.get(model, MOI.ObjectiveFunction{F}()) - - if F <: VI - L[f] += one(T) - elseif F <: SAF - for a in f.terms - ci = a.coefficient - xi = a.variable - - L[xi] += ci - end - - offset += f.constant - elseif F <: SQF - for a in f.affine_terms - ci = a.coefficient - xi = a.variable - - L[xi] += ci - end - - for a in f.quadratic_terms - cij = a.coefficient - xi = a.variable_1 - xj = a.variable_2 - - if xi == xj - # ~ MOI assumes - # SQF := ½ x' Q x + a' x + β - # Thus, the main diagonal is doubled from our point of view - # ~ Also, in this case, x² = x - L[xi] += cij / 2 - else - Q[xi, xj] = get(Q, (xi, xj), zero(T)) + cij - end - end - - offset += f.constant - end - - return (L, Q, offset) -end - -function __extract_model( - ::Type{T}, - Ω::Set{VI}, - model::MOI.ModelLike, - ::QUBOTools.SpinDomain, -) where {T} - L = Dict{VI,T}(xi => zero(T) for xi ∈ Ω) - Q = Dict{Tuple{VI,VI},T}() - - offset = zero(T) - - F = MOI.get(model, MOI.ObjectiveFunctionType()) - f = MOI.get(model, MOI.ObjectiveFunction{F}()) - - if F <: VI - L[f] += one(T) - elseif F <: SAF - for a in f.terms - ci = a.coefficient - xi = a.variable - - L[xi] += ci - end - - offset += f.constant - elseif F <: SQF - for a in f.affine_terms - ci = a.coefficient - xi = a.variable - - L[xi] += ci - end - - for a in f.quadratic_terms - cij = a.coefficient - xi = a.variable_1 - xj = a.variable_2 - - if xi == xj - # ~ MOI assumes - # SQF := ½ s' J s + h' s + β - # Thus, the main diagonal is doubled from our point of view - # ~ Also, in this case, s² = 1 - offset += cij / 2 - else - Q[xi, xj] = get(Q, (xi, xj), zero(T)) + cij - end - end - - offset += f.constant - end - - return (L, Q, offset) -end - - -function parse_model(T::Type, model::MOI.ModelLike) - # ~*~ Check for emptiness ~*~ # - if MOI.is_empty(model) - return QUBOTools.Model{VI,T,Int}( - Dict{VI,T}(), - Dict{Tuple{VI,VI},T}(); - sense = QUBOTools.MinSense(), - domain = QUBOTools.BoolDomain(), - ) - end - - # ~*~ Validate Model ~*~ # - flag = false - - if !__is_quadratic(model) - @error "The given model's objective function is not a quadratic or linear polynomial" - flag = true - end - - if !__is_optimization(model) - @error "The given model lacks an optimization sense" - flag = true - end - - if !__is_unconstrained(model) - @error "The given model is not unconstrained" - flag = true - end - - Ω = Set{VI}(MOI.get(model, MOI.ListOfVariableIndices())) - 𝔹 = Set{VI}( - MOI.get(model, MOI.ConstraintFunction(), ci) for - ci in MOI.get(model, MOI.ListOfConstraintIndices{VI,MOI.ZeroOne}()) - ) - 𝕊 = if MOI.supports_constraint(model, VI, Spin) - Set{VI}( - MOI.get(model, MOI.ConstraintFunction(), ci) for - ci in MOI.get(model, MOI.ListOfConstraintIndices{VI,Spin}()) - ) - else # Models aren't obligated to support `Spin`! - Set{VI}() # empty set - end - - # ~*~ Retrieve Variable Domain ~*~ # - # Assuming: - # - 𝕊, 𝔹 ⊆ Ω - domain = if !isempty(𝕊) && !isempty(𝔹) - @error "The given model contains both boolean and spin variables" - flag = true - - nothing - elseif isempty(𝕊) # QUBO model? - if 𝔹 != Ω - @error "Not all variables in the given model are boolean" - flag = true - - nothing - else - QUBOTools.BoolDomain() - end - elseif isempty(𝔹) # Ising model? - if 𝕊 != Ω - @error "Not all variables in the given model are spin" - flag = true - - nothing - else - QUBOTools.SpinDomain() - end - end - - if flag - # Throw ToQUBO.jl advertisement on parsing error: - error( - """ - The current model could not be converted to QUBO in a straightforward fashion. - Consider using the ToQUBO.jl package, a sophisticated reformulation framework. - pkg> add ToQUBO # 😎 - """ - ) - end - - # ~*~ Retrieve Model ~*~ # - L, Q, offset = __extract_model(T, Ω, model, domain) - scale = one(T) - - # ~*~ Objective Sense ~*~ # - sense = QUBOTools.Sense(MOI.get(model, MOI.ObjectiveSense())) - - # ~*~ Return Model ~*~ # - return QUBOTools.Model{VI,T,Int}( - L, Q; - scale = scale, - offset = offset, - sense = sense, - domain = domain, - ) -end - -function QUBOTools.varlt(x::VI, y::VI) - return isless(x.value, y.value) -end diff --git a/src/automatic/attributes.jl b/src/automatic/attributes.jl deleted file mode 100644 index 1e462b8..0000000 --- a/src/automatic/attributes.jl +++ /dev/null @@ -1,226 +0,0 @@ -# ~*~ :: MathOptInterface Attributes :: ~*~ # -const MOI_ATTRIBUTE = Union{ - MOI.Name, - MOI.Silent, - MOI.TimeLimitSec, - MOI.NumberOfThreads, - MOI.VariablePrimalStart, -} - -mutable struct MOIAttributeData{T} - name::String - silent::Bool - time_limit_sec::Union{Float64,Nothing} - number_of_threads::Int - variable_primal_start::Dict{VI,T} - - function MOIAttributeData{T}(; - name::String = "", - silent::Bool = false, - time_limit_sec::Union{Float64,Nothing} = nothing, - number_of_threads::Integer = Threads.nthreads(), - variable_primal_start = Dict{VI,T}(), - ) where {T} - return new{T}( - name, - silent, - time_limit_sec, - number_of_threads, - variable_primal_start, - ) - end -end - -# MOI.Name -MOI.get(data::MOIAttributeData, ::MOI.Name) = data.name - -function MOI.set(data::MOIAttributeData, ::MOI.Name, name::String) - data.name = name - - return nothing -end - -# MOI.Silent -MOI.get(data::MOIAttributeData, ::MOI.Silent) = data.silent - -function MOI.set(data::MOIAttributeData, ::MOI.Silent, silent::Bool) - data.silent = silent - - return nothing -end - -# MOI.TimeLimitSec -MOI.get(data::MOIAttributeData, ::MOI.TimeLimitSec) = data.time_limit_sec - -function MOI.set( - data::MOIAttributeData, - ::MOI.TimeLimitSec, - time_limit_sec::Union{Float64,Nothing}, -) - @assert isnothing(time_limit_sec) || time_limit_sec >= 0.0 - - data.time_limit_sec = time_limit_sec - - return nothing -end - -# MOI.NumberOfThreads -MOI.get(data::MOIAttributeData, ::MOI.NumberOfThreads) = data.number_of_threads - -function MOI.set(data::MOIAttributeData, ::MOI.NumberOfThreads, number_of_threads::Integer) - @assert number_of_threads > 0 - - data.number_of_threads = number_of_threads - - return nothing -end - -# MOI.VariablePrimalStart -function MOI.get(data::MOIAttributeData, ::MOI.VariablePrimalStart, vi::VI) - return get(data.variable_primal_start, vi, nothing) -end - -function MOI.set( - data::MOIAttributeData, - ::MOI.VariablePrimalStart, - vi::VI, - ::Nothing, -) - delete!(data.variable_primal_start, vi) - - return nothing -end - -function MOI.set( - data::MOIAttributeData{T}, - ::MOI.VariablePrimalStart, - vi::VI, - value::T, -) where {T} - data.variable_primal_start[vi] = value - - return nothing -end - -# VariablePrimalStart -function MOI.get(sampler::AutomaticSampler{T}, attr::MOI.VariablePrimalStart, vi::VI) where {T} - return MOI.get(sampler.attr_data.moiattrs, attr, vi) -end - -function MOI.set( - sampler::AutomaticSampler{T}, - attr::MOI.VariablePrimalStart, - vi::VI, - value::Union{T,Nothing}, -) where {T} - MOI.set(sampler.attr_data.moiattrs, attr, vi, value) - - return nothing -end - -MOI.supports(::AutomaticSampler, ::MOI.VariablePrimalStart, ::Type{VI}) = true -MOI.supports(::AutomaticSampler, ::MOI.VariablePrimalStart, ::MOI.VariableIndex) = true - -# ~*~ :: Sampler Attributes :: ~*~ # -abstract type AbstractSamplerAttribute <: MOI.AbstractOptimizerAttribute end - -mutable struct AttributeWrapper{A<:Union{AbstractSamplerAttribute,Nothing},T} - value::T - rawattr::Union{String,Nothing} - optattr::A - - function AttributeWrapper{A,T}( - default::T; - rawattr::Union{String,Nothing} = nothing, - optattr::A = nothing, - ) where {A<:Union{AbstractSamplerAttribute,Nothing},T} - @assert !(isnothing(rawattr) && isnothing(optattr)) - - return new{A,T}(default, rawattr, optattr) - end -end - -struct AttributeData{T} - rawattrs::Dict{String,AttributeWrapper} - optattrs::Dict{AbstractSamplerAttribute,AttributeWrapper} - moiattrs::MOIAttributeData{T} - - function AttributeData{T}(attrs::Vector) where {T} - rawattrs = Dict{String,AttributeWrapper}() - optattrs = Dict{AbstractSamplerAttribute,AttributeWrapper}() - moiattrs = MOIAttributeData{T}() - - for attr::AttributeWrapper in attrs - if !isnothing(attr.rawattr) - rawattrs[attr.rawattr] = attr - end - - if !isnothing(attr.optattr) - optattrs[attr.optattr] = attr - end - end - - return new{T}(rawattrs, optattrs, moiattrs) - end -end - -# ~*~ :: Automatic Sampler Methods :: ~*~ # - -# MOI_ATTRIBUTE -function MOI.get(sampler::AutomaticSampler, attr::MOI_ATTRIBUTE) - return MOI.get(sampler.attr_data.moiattrs, attr) -end - -function MOI.set(sampler::AutomaticSampler, attr::MOI_ATTRIBUTE, value) - MOI.set(sampler.attr_data.moiattrs, attr, value) - - return nothing -end - -MOI.supports(sampler::AutomaticSampler, attr::MOI_ATTRIBUTE) = true - -# AbstractSamplerAttribute -function MOI.get(sampler::AutomaticSampler, attr::AbstractSamplerAttribute) - if haskey(sampler.attr_data.optattrs, attr) - return sampler.attr_data.optattrs[attr].value - else - error("Attribute '$attr' is not supported") - end -end - -function MOI.set(sampler::AutomaticSampler, attr::AbstractSamplerAttribute, value) - if haskey(sampler.attr_data.optattrs, attr) - sampler.attr_data.optattrs[attr].value = value - else - error("Attribute '$attr' is not supported") - end - - return nothing -end - -function MOI.supports(sampler::AutomaticSampler, attr::AbstractSamplerAttribute) - return haskey(sampler.attr_data.optattrs, attr) -end - -# RawOptimizerAttribute -function MOI.get(sampler::AutomaticSampler, raw_attr::MOI.RawOptimizerAttribute) - if haskey(sampler.attr_data.rawattrs, raw_attr.name) - return sampler.attr_data.rawattrs[raw_attr.name].value - else - error("Attribute '$raw_attr' is not supported") - end -end - -function MOI.set(sampler::AutomaticSampler, raw_attr::MOI.RawOptimizerAttribute, value) - if haskey(sampler.attr_data.rawattrs, raw_attr.name) - sampler.attr_data.rawattrs[raw_attr.name].value = value - else - error("Attribute '$raw_attr' is not supported") - end - - return nothing -end - -function MOI.supports(sampler::AutomaticSampler, raw_attr::MOI.RawOptimizerAttribute) - return haskey(sampler.attr_data.rawattrs, raw_attr.name) -end diff --git a/src/automatic/interface.jl b/src/automatic/interface.jl deleted file mode 100644 index 654753f..0000000 --- a/src/automatic/interface.jl +++ /dev/null @@ -1,4 +0,0 @@ -@doc raw""" - AutomaticSampler{T} -""" -abstract type AutomaticSampler{T} <: AbstractSampler{T} end \ No newline at end of file diff --git a/src/automatic/sample.jl b/src/automatic/sample.jl deleted file mode 100644 index 41a9972..0000000 --- a/src/automatic/sample.jl +++ /dev/null @@ -1,32 +0,0 @@ -function _sample!(sampler::AutomaticSampler{T}) where {T} - results = @timed sample(sampler)::SampleSet{T} - _sample!(sampler, results.value, results.time) - - return nothing -end - -function _sample!(sampler::AutomaticSampler{T}, sampleset::SampleSet{T}, total_time::Float64) where {T} - metadata = QUBOTools.metadata(sampleset)::Dict{String,Any} - - if !haskey(metadata, "time") - metadata["time"] = Dict{String,Any}("total" => total_time) - elseif !haskey(metadata["time"], "total") - metadata["time"]["total"] = total_time - end - - if !haskey(metadata, "status") - metadata["status"] = "" - end - - sampleset = QUBOTools.cast( - target_sense(sampler) => source_sense(sampler), - QUBOTools.cast( - target_domain(sampler) => source_domain(sampler), - sampleset, - ) - ) - - copy!(QUBOTools.sampleset(sampler.model), sampleset) - - return nothing -end diff --git a/src/automatic/setup.jl b/src/automatic/setup.jl deleted file mode 100644 index 696fc11..0000000 --- a/src/automatic/setup.jl +++ /dev/null @@ -1,361 +0,0 @@ -const __MODULES = Set{Module}() - -function __setup_error(msg::String) - error("Invalid usage of @setup: $msg") -end - -function __setup_parse_id(id::Symbol) - if Base.isidentifier(id) - return id - else - __setup_error("Invalid identifier for sampler: '$id'") - end -end - -function __setup_parse_id() - return :Optimizer -end - -function __setup_parse_param(::Val{X}, ::Any) where {X} - __setup_error( - "Invalid parameter '$X', valid options are: 'name', 'version', 'domain', 'attributes'", - ) -end - -function __setup_parse_param(::Val{:name}, value) - if value isa String - return value - else - __setup_error("Parameter 'name' must be a 'String'") - end -end - -function __setup_parse_param(::Val{:version}, value) - if value isa VersionNumber - return value - else - __setup_error("Parameter 'version' must be a 'VersionNumber'") - end -end - -function __setup_parse_param(::Val{:sense}, _value) - value = if _value isa QuoteNode - _value.value - elseif value isa String - Symbol(_value) - else - _value - end - - if (value === :min || value === :max) - return value - else - __setup_error("parameter 'sense' must be either ':min' or ':max', not '$_value'") - end -end - -function __setup_parse_param(::Val{:domain}, _value) - value = if _value isa QuoteNode - _value.value - elseif _value isa String - Symbol(_value) - else - _value - end - - if (value === :bool || value === :spin) - return value - else - __setup_error("parameter 'domain' must be either ':bool' or ':spin', not '$_value'") - end -end - -function __setup_parse_param(::Val{:attributes}, value) - if value isa Expr && value.head === :block - return Dict{Symbol,Any}[ - attr for attr in __setup_parse_attr.(value.args) if !isnothing(attr) - ] - else - __setup_error("Parameter 'attributes' must be a `begin ... end` block") - end -end - -function __setup_parse_attr(stmt) - if stmt isa LineNumberNode - return nothing - elseif !(stmt isa Expr && stmt.head === :(=)) - __setup_error( - "Each attribute definition must be an assignment to a default value ($stmt)", - ) - end - - attr, default = stmt.args - - type = nothing - optattr = nothing - rawattr = nothing - - if attr isa Symbol # ~ MOI attribute only - if !(Base.isidentifier(attr)) - __setup_error("attribute identifier '$attr' is not a valid one") - end - - optattr = attr - elseif attr isa String # ~ Raw attribute only - rawattr = attr - elseif attr isa Expr && attr.head === :(::) - attr, type = attr.args - - if attr isa Symbol - if !(Base.isidentifier(attr)) - __setup_error("attribute identifier '$attr' is not a valid one") - end - - optattr = attr - elseif attr isa String - rawattr = attr - elseif attr isa Expr && (attr.head === :ref || attr.head === :call) - optattr, rawattr = attr.args - - if optattr isa Symbol && rawattr isa String - if !(Base.isidentifier(optattr)) - __setup_error("attribute identifier '$optattr' is not a valid one") - end - else - __setup_error("invalid attribute identifier '$name($raw)'") - end - else - __setup_error("invalid attribute identifier '$attr'") - end - elseif attr isa Expr && (attr.head === :ref || attr.head === :call) - optattr, rawattr = attr.args - - if optattr isa Symbol && rawattr isa String - if !(Base.isidentifier(optattr)) - __setup_error("attribute identifier '$optattr' is not a valid one") - end - else - __setup_error("invalid attribute identifier '$name[$rawattr]'") - end - else - __setup_error("invalid attribute signature '$attr'") - end - - return Dict{Symbol,Any}( - :type => type, - :default => default, - :optattr => optattr, - :rawattr => rawattr, - ) -end - -function __setup_parse_params(block::Expr) - if !(block.head === :block) - __setup_error("Sampler configuration must be provided within a `begin ... end` block") - end - - params = Dict{Symbol,Any}( - :name => "", - :sense => :min, - :domain => :bool, - :version => v"1.0.0", - :attributes => Dict{Symbol,Any}[], - ) - - for item in block.args - if item isa LineNumberNode - continue - elseif item isa Expr && item.head === :(=) - param, value = item.args - - if param isa Symbol && Base.isidentifier(param) - params[param] = __setup_parse_param(Val(param), value) - else - __setup_error("sampler parameter key must be a valid identifier") - end - else - __setup_error("sampler parameters must be `key = value` pairs") - end - end - - # Post-processing - params[:sense] = QUBOTools.Sense(params[:sense]) - params[:domain] = QUBOTools.Domain(params[:domain]) - params[:attributes] = __setup_attr.(params[:attributes]) - - return params -end - -function __setup_parse_params() - __DEFAULT_PARAMETERS() -end - -function __setup_parse(args...) - __setup_error("macro takes exactly one or two arguments") -end - -function __setup_parse(expr) - if expr isa Symbol # Name - return (__setup_parse_id(expr), __setup_parse_params()) - elseif (expr isa Expr && expr.head === :block) - return (__setup_parse_id(), __setup_parse_params(expr)) - else - __setup_error( - "Single argument must be either an identifier or a `begin ... end` block", - ) - end -end - -function __setup_parse() - return (__setup_parse_id(), __setup_parse_params()) -end - -function __setup_parse(id, block) - params = Dict{Symbol,Any}() - - if !(id isa Symbol) - __setup_error("First argument must be an identifier") - end - - params[:id] = __setup_parse_id(id) - - if !(block isa Expr && block.head === :block) - __setup_error("Second argument must be a `begin ... end` block") - end - - merge!(params, __setup_parse_params(block)) - - return params -end - -function __setup_attr(attr) - type = attr[:type] - default = attr[:default] - optattr = attr[:optattr] - rawattr = attr[:rawattr] - - if !isnothing(optattr) && !isnothing(rawattr) - return quote - struct $(esc(optattr)) <: QUBODrivers.AbstractSamplerAttribute end - - push!( - __ATTRIBUTES, - QUBODrivers.AttributeWrapper{$(esc(optattr)),$(esc(type))}( - $(esc(default)); - rawattr = $(esc(rawattr)), - optattr = $(esc(optattr))(), - ), - ) - end - elseif !isnothing(optattr) - return quote - struct $(esc(optattr)) <: QUBODrivers.AbstractSamplerAttribute end - - push!( - __ATTRIBUTES, - QUBODrivers.AttributeWrapper{$(esc(optattr)),$(esc(type))}( - $(esc(default)); - optattr = $(esc(optattr))(), - ), - ) - end - elseif !isnothing(rawattr) - return quote - push!( - __ATTRIBUTES, - QUBODrivers.AttributeWrapper{Nothing,$(esc(type))}( - $(esc(default)); - rawattr = $(esc(rawattr)), - ), - ) - end - else - error("Looks like some assertions were skipped. Did you turn any optimizations on?") - end -end - -@doc raw""" - @setup(expr) - -The `@setup` macro receives a `begin ... end` block with an attribute definition on each of the block's statements. - -All attributes must be presented as an assignment to the default value of that attribute. To create a MathOptInterface optimizer attribute, an identifier must be present on the left hand side. If a solver-specific, raw attribute is desired, its name must be given as a string, e.g. between double quotes. In the special case where an attribute could be accessed in both ways, the identifier must be followed by the parenthesised raw attribute string. In any case, the attribute type can be specified typing the type assertion operator `::` followed by the type itself just before the equal sign. - -For example, a list of the valid syntax variations for the *number of reads* attribute follows: - - `"num_reads" = 1_000` - - `"num_reads"::Integer = 1_000` - - `NumberOfReads = 1_000` - - `NumberOfReads::Integer = 1_000` - - `NumberOfReads["num_reads"] = 1_000` - - `NumberOfReads["num_reads"]::Integer = 1_000` - -## Example - -``` -QUBODrivers.@setup Optimizer begin - name = "Super Sampler" - sense = :max - domain = :spin - version = v"1.0.2" - attributes = begin - NumberOfReads["num_reads"]::Integer = 1_000 - SuperAttribute["super_attr"] = nothing - end -end -``` -""" -macro setup(raw_args...) - # Check context - if __module__ === Main - __setup_error("macro must be called from within a module (not Main)") - elseif __module__ ∈ QUBODrivers.__MODULES - __setup_error("macro should be called only once within a module") - else - push!(QUBODrivers.__MODULES, __module__) - end - - # Parse parameters - args = map(a -> macroexpand(__module__, a), raw_args) - params = __setup_parse(args...) - - # Collect parameters - _id = params[:id] - _name = params[:name] - _sense = params[:sense] - _domain = params[:domain] - _version = params[:version] - _attributes = params[:attributes] - - # For this mechanism to work it is very important that the - # @setup macro is called at most once inside each module. - return quote - mutable struct $(esc(_id)){T} <: QUBODrivers.AutomaticSampler{T} - # Sense & Domain - sense::QUBOTools.Sense - domain::QUBOTools.Domain - # QUBOTools model - model::Union{QUBOTools.Model{VI,T,Int},Nothing} - # Attributes - attr_data::QUBODrivers.AttributeData{T} - end - - const __ATTRIBUTES = QUBODrivers.AttributeWrapper[] - - function $(esc(_id)){T}(args...; kws...) where {T} - return $(esc(_id)){T}( - $(esc(_sense)), # sense - $(esc(_domain)), # domain - nothing, # model - QUBODrivers.AttributeData{T}(__ATTRIBUTES), # attr_data - ) - end - - $(esc(_id))(args...; kws...) = $(esc(_id)){Float64}(args...; kws...) - - $(_attributes...) - - # MOI interface - MOI.get(::$(esc(_id)), ::MOI.SolverName) = $(esc(_name)) - MOI.get(::$(esc(_id)), ::MOI.SolverVersion) = $(esc(_version)) - end -end \ No newline at end of file diff --git a/src/automatic/wrapper.jl b/src/automatic/wrapper.jl deleted file mode 100644 index c6710c3..0000000 --- a/src/automatic/wrapper.jl +++ /dev/null @@ -1,259 +0,0 @@ -# ~*~ :: QUBOTools :: ~*~ # - -# Casting routes i.e. source => target pairs of senses and domains: -source_sense(sampler::AutomaticSampler) = QUBOTools.sense(sampler.model) -target_sense(sampler::AutomaticSampler) = sampler.sense - -source_domain(sampler::AutomaticSampler) = QUBOTools.domain(sampler.model) -target_domain(sampler::AutomaticSampler) = sampler.domain - -function QUBOTools.backend(sampler::AutomaticSampler) - return QUBOTools.cast( - source_sense(sampler) => target_sense(sampler), - QUBOTools.cast(source_domain(sampler) => target_domain(sampler), sampler.model), - ) -end - -# This is important to ensure aliasing: -function QUBOTools.metadata(sampler::AutomaticSampler) - return QUBOTools.metadata(sampler.model) -end - -function QUBOTools.warm_start(sampler::AutomaticSampler) - return QUBOTools.warm_start(sampler.model) -end - -# ~*~ :: MathOptInterface :: ~*~ # -function MOI.empty!(sampler::AutomaticSampler) - sampler.model = nothing - - return sampler -end - -function MOI.is_empty(sampler::AutomaticSampler) - return isnothing(sampler.model) -end - -function MOI.optimize!(sampler::AutomaticSampler) - return _sample!(sampler) -end - -function MOI.copy_to(sampler::AutomaticSampler{T}, model::MOI.ModelLike) where {T} - MOI.empty!(sampler) - - sampler.model = parse_model(T, model)::QUBOTools.Model{VI,T} - - ws = QUBOTools.warm_start(sampler)::Dict{VI,Int} - - # Collect warm-start values - for v in MOI.get(model, MOI.ListOfVariableIndices()) - x = MOI.get(model, MOI.VariablePrimalStart(), v) - - MOI.set(sampler, MOI.VariablePrimalStart(), v, x) - - if !isnothing(x) - ws[v] = QUBOTools.cast( - source_domain(sampler) => target_domain(sampler), - round(Int, x), - ) - end - end - - return MOIU.identity_index_map(model) -end - -function MOI.get( - sampler::AutomaticSampler, - st::Union{MOI.PrimalStatus,MOI.DualStatus}, - ::VI, -) - if !(1 <= st.result_index <= MOI.get(sampler, MOI.ResultCount())) - return MOI.NO_SOLUTION - else - # This status is also not very accurate, but all points are feasible - # in a general sense since these problems are unconstrained. - return MOI.FEASIBLE_POINT - end -end - -function MOI.get(sampler::AutomaticSampler, ::MOI.RawStatusString) - solution_metadata = QUBOTools.metadata(QUBOTools.sampleset(sampler.model)) - - if !haskey(solution_metadata, "status") - return "" - else - return solution_metadata["status"]::String - end -end - -MOI.supports(::AutomaticSampler, ::MOI.RawStatusString) = true - -function MOI.get(sampler::AutomaticSampler, ::MOI.ResultCount) - return length(QUBOTools.sampleset(sampler.model)) -end - -function MOI.get(sampler::AutomaticSampler, ::MOI.TerminationStatus) - ω = QUBOTools.sampleset(sampler.model) - - if isempty(ω) - if isempty(QUBOTools.metadata(ω)) - return MOI.OPTIMIZE_NOT_CALLED - else - return MOI.OTHER_ERROR - end - else - # This one is a little bit tricky... - # It is nice if samplers implement this method in order to give - # more accurate information. - return MOI.LOCALLY_SOLVED - end -end - -function MOI.get(sampler::AutomaticSampler{T}, ::MOI.ObjectiveSense) where {T} - sense = QUBOTools.sense(sampler.model) - - if sense === QUBOTools.Min - return MOI.MIN_SENSE - else - return MOI.MAX_SENSE - end -end - -function MOI.get(sampler::AutomaticSampler, ov::MOI.ObjectiveValue) - ω = QUBOTools.sampleset(sampler.model) - i = ov.result_index - n = length(ω) - - if isempty(ω) - error("Invalid result index '$i'; There are no solutions") - elseif !(1 <= i <= n) - error("Invalid result index '$i'; There are $(n) solutions") - end - - if MOI.get(sampler, MOI.ObjectiveSense()) === MOI.MAX_SENSE - i = n - i + 1 - end - - return QUBOTools.value(ω, i) -end - -function MOI.get(sampler::AutomaticSampler, ::MOI.SolveTimeSec) - return QUBOTools.effective_time(QUBOTools.sampleset(sampler.model)) -end - -function MOI.get(sampler::AutomaticSampler{T}, vp::MOI.VariablePrimal, vi::VI) where {T} - ω = QUBOTools.sampleset(sampler.model) - n = length(ω) - i = vp.result_index - - if isempty(ω) - error("Invalid result index '$i'; There are no solutions") - elseif !(1 <= i <= n) - error("Invalid result index '$i'; There are $(n) solutions") - end - - variable_map = QUBOTools.variable_map(sampler.model) - - if !haskey(variable_map, vi) - error("Variable index '$vi' not in model") - end - - if MOI.get(sampler, MOI.ObjectiveSense()) === MOI.MAX_SENSE - i = n - i + 1 - end - - j = variable_map[vi]::Integer - s = QUBOTools.state(ω, i, j) - - return convert(T, s) -end - -function MOI.get(sampler::AutomaticSampler, ::MOI.NumberOfVariables) - return QUBOTools.domain_size(sampler.model) -end - -function QUBOTools.qubo(sampler::AutomaticSampler, type::Type = Dict) - @assert !isnothing(sampler.model) - - n = QUBOTools.domain_size(sampler.model) - - L, Q, α, β = QUBOTools.cast( - source_sense(sampler) => target_sense(sampler), - # model terms and coefficients - QUBOTools.linear_terms(sampler.model), - QUBOTools.quadratic_terms(sampler.model), - QUBOTools.scale(sampler.model), - QUBOTools.offset(sampler.model), - ) - - L, Q, α, β = - QUBOTools.cast(source_domain(sampler) => target_domain(sampler), L, Q, α, β) - - return QUBOTools.qubo(type, n, L, Q, α, β) -end - -function QUBOTools.ising(sampler::AutomaticSampler, type::Type = Dict) - @assert !isnothing(sampler.model) - - n = QUBOTools.domain_size(sampler.model) - - L, Q, α, β = QUBOTools.cast( - source_sense(sampler) => target_sense(sampler), - # model terms and coefficients - QUBOTools.linear_terms(sampler.model), - QUBOTools.quadratic_terms(sampler.model), - QUBOTools.scale(sampler.model), - QUBOTools.offset(sampler.model), - ) - - L, Q, α, β = - QUBOTools.cast(source_domain(sampler) => target_domain(sampler), L, Q, α, β) - - return QUBOTools.ising(type, n, L, Q, α, β) -end - -# ~*~ File IO: Base API ~*~ # -# function Base.write( -# filename::AbstractString, -# sampler::AutomaticSampler, -# fmt::QUBOTools.AbstractFormat = QUBOTools.infer_format(filename), -# ) -# return QUBOTools.write_model(filename, sampler.model, fmt) -# end - -# function Base.read!( -# filename::AbstractString, -# sampler::AutomaticSampler, -# fmt::QUBOTools.AbstractFormat = QUBOTools.infer_format(filename), -# ) -# sampler.source = QUBOTools.read_model(filename, fmt) -# sampler.target = QUBOTools.format(sampler, sampler.source) - -# return sampler -# end - -function warm_start(sampler::AutomaticSampler, i::Integer) - v = QUBOTools.variable_inv(sampler, i) - x = MOI.get(sampler, MOI.VariablePrimalStart(), v) - - if isnothing(x) - return nothing - else - return QUBOTools.cast( - source_domain(sampler) => target_domain(sampler), - round(Int, x), - ) - end -end - -function warm_start(sampler::AutomaticSampler{T}) where {T} - n = MOI.get(sampler, MOI.NumberOfVariables()) - s = sizehint!(Dict{Int,Int}(), n) - - for i = 1:n - x = warm_start(sampler, i) - isnothing(x) || (s[i] = x) - end - - return s -end \ No newline at end of file diff --git a/src/benchmark/benchmark.jl b/src/benchmark/benchmark.jl deleted file mode 100644 index 2f4f9a0..0000000 --- a/src/benchmark/benchmark.jl +++ /dev/null @@ -1,27 +0,0 @@ -@doc raw""" - benchmark(sampler::AbstractSampler) - -""" function benchmark end - -function QUBODrivers.benchmark(sampler::AbstractSampler) - -end - -@doc raw""" - benchmark_suite(sampler::AbstractSampler) - -## Example - -``` -using QUBODrivers -using SuperSampler - -SUITE = QUBODrivers.benchmark_suite(SuperSampler.Optimizer) -``` -""" function benchmark_suite end - -function QUBODrivers.benchmark_suite(sampler::AbstractSampler) - suite = BenchmarkTools.BenchmarkGroup() - - return suite -end \ No newline at end of file diff --git a/src/drivers/IdentitySampler.jl b/src/drivers/IdentitySampler.jl deleted file mode 100644 index e5ebfec..0000000 --- a/src/drivers/IdentitySampler.jl +++ /dev/null @@ -1,47 +0,0 @@ -module IdentitySampler - -import QUBOTools -import QUBODrivers: MOI, Sample, SampleSet, @setup, sample, qubo, warm_start - -@setup Optimizer begin - name = "Identity Sampler" - sense = :min - domain = :bool -end - -@doc raw""" - IdentitySampler.Optimizer{T} - -This sampler selects precisely the state vector provided as warm-start. -""" Optimizer - -function sample_state(sampler::Optimizer{T}, n::Integer) where {T} - return round.(Int, warm_start.(sampler, 1:n)) -end - -function sample(sampler::Optimizer{T}) where {T} - # Retrieve Model - Q, α, β = qubo(sampler, Dict) - - # Retrieve Attributes - n = MOI.get(sampler, MOI.NumberOfVariables()) - - # Retrieve warm-start state - samples = Vector{Sample{T,Int}}(undef, 1) - results = @timed begin - ψ = sample_state(sampler, n) - λ = QUBOTools.value(Q, ψ, α, β) - - samples[] = Sample{T}(ψ, λ) - end - - # Write Solution Metadata - metadata = Dict{String,Any}( - "origin" => "Identity Sampler @ QUBODrivers.jl", - "time" => Dict{String,Any}("effective" => results.time), - ) - - return SampleSet{T}(samples, metadata) -end - -end # module \ No newline at end of file diff --git a/src/interface/attributes.jl b/src/interface/attributes.jl new file mode 100644 index 0000000..21b61d9 --- /dev/null +++ b/src/interface/attributes.jl @@ -0,0 +1,35 @@ +@doc raw""" + SamplerAttribute +""" +abstract type SamplerAttribute <: MOI.AbstractOptimizerAttribute end + +@doc raw""" + RawSamplerAttribute{key} +""" +struct RawSamplerAttribute{key} <: SamplerAttribute + RawSamplerAttribute{key}() where {key} = new{key}() +end + +RawSamplerAttribute(key::String) = RawSamplerAttribute{Symbol(key)}() + +@doc raw""" + @raw_attr_str +""" +macro raw_attr_str(key::String) + return :(RawSamplerAttribute{$(esc(QuoteNode(Symbol(key))))}) +end + +@doc raw""" + default_raw_attr +""" +function default_raw_attr end + +@doc raw""" + get_raw_attr +""" +function get_raw_attr end + +@doc raw""" + set_raw_attr! +""" +function set_raw_attr! end diff --git a/src/interface/sampler.jl b/src/interface/sampler.jl new file mode 100644 index 0000000..525986f --- /dev/null +++ b/src/interface/sampler.jl @@ -0,0 +1,44 @@ +@doc raw""" + AbstractSampler{T} <: MOI.AbstractOptimizer +""" +abstract type AbstractSampler{T} <: MOI.AbstractOptimizer end + +@doc raw""" + sample(::AbstractSampler{T})::SampleSet{T} where {T} +""" +function sample end + +function sample(::S) where {S<:AbstractSampler} + error("`QUBODrivers.sample` is not implemented for '$S'") +end + +@doc raw""" + set_model! +""" +function set_model! end + +function _sample!(sampler::AbstractSampler{T}) where {T} + results = @timed sample(sampler) + + _sample!(sampler, results.value, results.time) + + return nothing +end + +function _sample!(sampler::AbstractSampler{T}, sampleset::SampleSet{T}, total_time::Float64) where {T} + metadata = QUBOTools.metadata(sampleset)::Dict{String,Any} + + if !haskey(metadata, "time") + metadata["time"] = Dict{String,Any}("total" => total_time) + elseif !haskey(metadata["time"], "total") + metadata["time"]["total"] = total_time + end + + if !haskey(metadata, "status") + metadata["status"] = "" + end + + QUBOTools.attach!(sampler, sampleset) + + return nothing +end diff --git a/src/library/benchmark/benchmark.jl b/src/library/benchmark/benchmark.jl new file mode 100644 index 0000000..e125185 --- /dev/null +++ b/src/library/benchmark/benchmark.jl @@ -0,0 +1,38 @@ +@doc raw""" + benchmark_suite(::Type{S}) where {T,S<:AbstractSampler{T}} + +## Example + +``` +using QUBODrivers +using SuperSampler +using BenchmarkTools + +SUITE = QUBODrivers.benchmark_suite(SuperSampler.Optimizer) + +results = BenchmarkTools.run(SUITE) +``` +""" +function benchmark_suite end + +function QUBODrivers.benchmark_suite(::Type{S}) where {T,S<:AbstractSampler{T}} + suite = BenchmarkTools.BenchmarkGroup() + + error("This feature is not implemented yet.") + + return suite +end + +@doc raw""" + benchmark(::Type{S}) where {T,S<:AbstractSampler{T}} + +""" +function benchmark end + +function QUBODrivers.benchmark(::Type{S}) where {T,S<:AbstractSampler{T}} + suite = QUBODrivers.benchmark_suite(S) + + results = BenchmarkTools.run(suite) + + return results +end diff --git a/src/drivers/ExactSampler.jl b/src/library/drivers/ExactSampler.jl similarity index 65% rename from src/drivers/ExactSampler.jl rename to src/library/drivers/ExactSampler.jl index 0f4433a..37b9c59 100644 --- a/src/drivers/ExactSampler.jl +++ b/src/library/drivers/ExactSampler.jl @@ -1,13 +1,8 @@ module ExactSampler import QUBOTools -import QUBODrivers: MOI, Sample, SampleSet, @setup, sample, qubo - -@setup Optimizer begin - name = "Exact Sampler" - sense = :min - domain = :bool -end +import QUBODrivers +import QUBODrivers: MOI, Sample, SampleSet @doc raw""" ExactSampler.Optimizer{T} @@ -17,23 +12,26 @@ This sampler performs an exhaustive search over all ``2^{n}`` possible states. !!! warn Due to the exponetially large amount of visited states, it is not possible to use this sampler for problems any larger than ``20`` variables big. -""" Optimizer +""" +QUBODrivers.@setup Optimizer begin + name = "Exact Sampler" + version = QUBODrivers.__VERSION__ +end sample_state(i::Integer, n::Integer) = digits(Int, i - 1; base = 2, pad = n) -function sample(sampler::Optimizer{T}) where {T} +function QUBODrivers.sample(sampler::Optimizer{T}) where {T} # Retrieve Model - Q, α, β = qubo(sampler, Dict) + n, L, Q, α, β = QUBOTools.qubo(sampler, :sparse; sense = :min) # Retrieve Attributes - n = MOI.get(sampler, MOI.NumberOfVariables()) m = 2^n # Sample states & measure time samples = Vector{Sample{T,Int}}(undef, m) results = @timed for i = 1:m ψ = sample_state(i, n) - λ = QUBOTools.value(Q, ψ, α, β) + λ = QUBOTools.value(ψ, L, Q, α, β) samples[i] = Sample{T}(ψ, λ) end @@ -42,9 +40,10 @@ function sample(sampler::Optimizer{T}) where {T} metadata = Dict{String,Any}( "origin" => "Exact Sampler @ QUBODrivers.jl", "time" => Dict{String,Any}("effective" => results.time), + "status" => "optimal", ) - return SampleSet{T}(samples, metadata) + return SampleSet{T}(samples, metadata; sense = :min, domain = :bool) end end # module \ No newline at end of file diff --git a/src/library/drivers/IdentitySampler.jl b/src/library/drivers/IdentitySampler.jl new file mode 100644 index 0000000..6580475 --- /dev/null +++ b/src/library/drivers/IdentitySampler.jl @@ -0,0 +1,57 @@ +module IdentitySampler + +import QUBOTools +import QUBODrivers +import QUBODrivers: MOI, Sample, SampleSet + +@doc raw""" + IdentitySampler.Optimizer{T} + +This sampler selects precisely the state vector provided as warm-start. +""" +QUBODrivers.@setup Optimizer begin + name = "Identity Sampler" + version = QUBODrivers.__VERSION__ +end + +function sample_state(sampler::Optimizer{T}, n::Integer) where {T} + ψ = Vector{Int}(undef, n) + + for i = 1:n + s = QUBOTools.start(sampler, i; domain = :bool) + + if !isnothing(s) + ψ[i] = s + else + v = QUBOTools.variable(sampler, i) + + error("Warm-start value for '$v' is missing") + end + end + + return ψ +end + +function QUBODrivers.sample(sampler::Optimizer{T}) where {T} + # Retrieve Model + n, L, Q, α, β = QUBOTools.qubo(sampler, :dict; sense = :min) + + # Retrieve warm-start state + samples = Vector{Sample{T,Int}}(undef, 1) + results = @timed begin + ψ = sample_state(sampler, n) + λ = QUBOTools.value(ψ, L, Q, α, β) + + samples[] = Sample{T}(ψ, λ) + end + + # Write Solution Metadata + metadata = Dict{String,Any}( + "origin" => "Identity Sampler @ QUBODrivers.jl", + "time" => Dict{String,Any}("effective" => results.time), + ) + + return SampleSet{T}(samples, metadata; sense = :min, domain = :bool) +end + +end # module \ No newline at end of file diff --git a/src/drivers/README.md b/src/library/drivers/README.md similarity index 100% rename from src/drivers/README.md rename to src/library/drivers/README.md diff --git a/src/drivers/RandomSampler.jl b/src/library/drivers/RandomSampler.jl similarity index 67% rename from src/drivers/RandomSampler.jl rename to src/library/drivers/RandomSampler.jl index e41d57a..639eaa5 100644 --- a/src/drivers/RandomSampler.jl +++ b/src/library/drivers/RandomSampler.jl @@ -1,22 +1,11 @@ module RandomSampler import QUBOTools -import QUBODrivers: MOI, Sample, SampleSet, @setup, sample, qubo +import QUBODrivers +import QUBODrivers: MOI, Sample, SampleSet using Random -@setup Optimizer begin - name = "Random Sampler" - sense = :min - domain = :bool - version = v"0.6.0" - attributes = begin - RandomSeed["seed"]::Union{Integer,Nothing} = nothing - NumberOfReads["num_reads"]::Integer = 1_000 - RandomGenerator["rng"]::AbstractRNG = Random.GLOBAL_RNG - end -end - @doc raw""" RandomSampler.Optimizer{T} @@ -24,19 +13,27 @@ end - `RandomSeed`, `"seed"`: Random seed to initialize the random number generator. - `NumberOfReads`, `"num_reads"`: Number of random states sampled per run. - `RandomGenerator`, `"rng"`: Random Number Generator instance. -""" Optimizer +""" +QUBODrivers.@setup Optimizer begin + name = "Random Sampler" + version = QUBODrivers.__VERSION__ + attributes = begin + RandomSeed["seed"]::Union{Integer,Nothing} = nothing + NumberOfReads["num_reads"]::Integer = 1_000 + RandomGenerator["rng"]::AbstractRNG = Random.GLOBAL_RNG + end +end sample_state(rng::AbstractRNG, n::Integer) = rand(rng, (0, 1), n) -function sample(sampler::Optimizer{T}) where {T} +function QUBODrivers.sample(sampler::Optimizer{T}) where {T} # Retrieve Model - Q, α, β = qubo(sampler, Dict) + n, L, Q, α, β = QUBOTools.qubo(sampler, :dict; sense = :min) # Retrieve Attributes - n = MOI.get(sampler, MOI.NumberOfVariables()) - num_reads = MOI.get(sampler, RandomSampler.NumberOfReads()) - seed = MOI.get(sampler, RandomSampler.RandomSeed()) - rng = MOI.get(sampler, RandomSampler.RandomGenerator()) + num_reads = MOI.get(sampler, NumberOfReads()) + seed = MOI.get(sampler, RandomSeed()) + rng = MOI.get(sampler, RandomGenerator()) # Validate Input @assert num_reads >= 0 @@ -49,8 +46,8 @@ function sample(sampler::Optimizer{T}) where {T} # Sample Random States samples = Vector{Sample{T,Int}}(undef, num_reads) results = @timed for i = 1:num_reads - ψ = sample_state(rng, n) - λ = QUBOTools.value(Q, ψ, α, β) + ψ = sample_state(rng, n)::Vector{Int} + λ = QUBOTools.value(ψ, L, Q, α, β) samples[i] = Sample{T,Int}(ψ, λ) end @@ -61,7 +58,7 @@ function sample(sampler::Optimizer{T}) where {T} "time" => Dict{String,Any}("effective" => results.time), ) - return SampleSet{T}(samples, metadata) + return SampleSet{T}(samples, metadata; sense = :min, domain = :bool) end end # module \ No newline at end of file diff --git a/src/library/sampler/wrappers/moi.jl b/src/library/sampler/wrappers/moi.jl new file mode 100644 index 0000000..67cbd59 --- /dev/null +++ b/src/library/sampler/wrappers/moi.jl @@ -0,0 +1,148 @@ +# ~ Currently, all models in this context are unconstrained by definition. +MOI.supports_constraint( + ::AbstractSampler, + ::Type{<:MOI.AbstractFunction}, + ::Type{<:MOI.AbstractSet}, +) = false + +# ~ They are also binary +MOI.supports_constraint(::AbstractSampler, ::Type{VI}, ::Type{MOI.ZeroOne}) = true + +MOI.supports_constraint(::AbstractSampler, ::Type{VI}, ::Type{Spin}) = true + +# ~ Objective Function Support +MOI.supports(::AbstractSampler, ::MOI.ObjectiveFunction{<:Any}) = false + +MOI.supports( + ::AbstractSampler{T}, + ::MOI.ObjectiveFunction{<:Union{SQF{T},SAF{T},VI}}, +) where {T} = true + +# By default, all samplers are their own raw solvers. +MOI.get(sampler::AbstractSampler, ::MOI.RawSolver) = sampler + +# Since problems are unconstrained, all available solutions are feasible. +function MOI.get(sampler::AbstractSampler, ps::MOI.PrimalStatus) + m = MOI.get(sampler, MOI.ResultCount()) + i = ps.result_index + + if 1 <= i <= m + return MOI.FEASIBLE_POINT + else + return MOI.NO_SOLUTION + end +end + +# No constraints, no dual solutions +MOI.get(::AbstractSampler, ::MOI.DualStatus) = MOI.NO_SOLUTION + + +# ~*~ :: MathOptInterface :: ~*~ # +function MOI.empty!(sampler::AbstractSampler{T}) where {T} + QUBODrivers.set_model!(sampler, QUBOTools.Model{VI,T,Int}()) + + return sampler +end + +function MOI.is_empty(sampler::AbstractSampler) + return isempty(QUBOTools.backend(sampler)) +end + +function MOI.optimize!(sampler::AbstractSampler) + return _sample!(sampler) +end + +function MOI.copy_to(sampler::AbstractSampler{T}, src::MOI.ModelLike) where {T} + QUBODrivers.set_model!(sampler, QUBOTools.Model{T}(src)) + + # Collect warm-start values + for v in MOI.get(src, MOI.ListOfVariableIndices()) + x = MOI.get(src, MOI.VariablePrimalStart(), v) + + MOI.set(sampler, MOI.VariablePrimalStart(), v, x) + end + + return MOIU.identity_index_map(src) +end + +function MOI.get(sampler::AbstractSampler, ::MOI.RawStatusString) + solution_metadata = QUBOTools.metadata(QUBOTools.solution(sampler)) + + if !haskey(solution_metadata, "status") + return "" + else + return solution_metadata["status"]::String + end +end + +MOI.supports(::AbstractSampler, ::MOI.RawStatusString) = true + +function MOI.get(sampler::AbstractSampler, ::MOI.ResultCount) + return length(QUBOTools.solution(sampler)) +end + +function MOI.get(sampler::AbstractSampler, ::MOI.TerminationStatus) + ω = QUBOTools.solution(sampler) + + if isempty(ω) + if isempty(QUBOTools.metadata(ω)) + return MOI.OPTIMIZE_NOT_CALLED + else + return MOI.OTHER_ERROR + end + else + # This one is a little bit tricky... + # It is nice if samplers implement this method in order to give + # more accurate information. + return MOI.LOCALLY_SOLVED + end +end + +function MOI.get(sampler::AbstractSampler{T}, ::MOI.ObjectiveSense) where {T} + sense = QUBOTools.sense(sampler) + + if sense === QUBOTools.Min + return MOI.MIN_SENSE + else + return MOI.MAX_SENSE + end +end + +function MOI.get(sampler::AbstractSampler, ov::MOI.ObjectiveValue) + i = ov.result_index + ω = QUBOTools.solution(sampler) + m = length(ω) + + if isempty(ω) + error("Invalid result index '$i'; There are no solutions") + elseif !(1 <= i <= m) + error("Invalid result index '$i'; There are $(m) solutions") + end + + return QUBOTools.value(ω, i) +end + +function MOI.get(sampler::AbstractSampler, ::MOI.SolveTimeSec) + return QUBOTools.effective_time(QUBOTools.solution(sampler)) +end + +function MOI.get(sampler::AbstractSampler{T}, vp::MOI.VariablePrimal, vi::VI) where {T} + i = vp.result_index + ω = QUBOTools.solution(sampler) + m = length(ω) + + if isempty(ω) + error("Invalid result index '$i'; There are no solutions") + elseif !(1 <= i <= m) + error("Invalid result index '$i'; There are $(m) solutions") + end + + j = QUBOTools.index(sampler, vi) + s = QUBOTools.state(ω, i, j) + + return convert(T, s) +end + +function MOI.get(sampler::AbstractSampler, ::MOI.NumberOfVariables) + return QUBOTools.dimension(sampler) +end diff --git a/src/library/sampler/wrappers/qubotools.jl b/src/library/sampler/wrappers/qubotools.jl new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/library/sampler/wrappers/qubotools.jl @@ -0,0 +1 @@ + diff --git a/src/library/setup/error.jl b/src/library/setup/error.jl new file mode 100644 index 0000000..bb7e6f5 --- /dev/null +++ b/src/library/setup/error.jl @@ -0,0 +1,13 @@ +struct DriverSetupError <: Exception + msg::String +end + +function Base.showerror(io::IO, e::DriverSetupError) + print(io, "Invalid usage of @setup: $(e.msg)") + + return nothing +end + +function setup_error(msg::AbstractString) + throw(DriverSetupError(msg)) +end diff --git a/src/library/setup/macro.jl b/src/library/setup/macro.jl new file mode 100644 index 0000000..9076e19 --- /dev/null +++ b/src/library/setup/macro.jl @@ -0,0 +1,42 @@ +@doc raw""" + @setup(expr) + +The `@setup` macro receives a `begin ... end` block with an attribute definition on each of the block's statements. + +## Sampler Attributes + +All attributes must be presented as an assignment to the default value of that attribute. +To create a MathOptInterface optimizer attribute, an identifier must be present on the left hand side. +If a solver-specific, raw attribute is desired, its name must be given as a string, e.g. between double quotes. +In the special case where an attribute could be accessed in both ways, the identifier must be followed by the parenthesised raw attribute string. In any case, the attribute type can be specified typing the type assertion operator `::` followed by the type itself just before the equal sign. + +For example, a list of the valid syntax variations for the *number of reads* attribute follows: + - `"num_reads" = 1_000` + - `"num_reads"::Integer = 1_000` + - `NumberOfReads = 1_000` + - `NumberOfReads::Integer = 1_000` + - `NumberOfReads("num_reads") = 1_000` + - `NumberOfReads("num_reads")::Integer = 1_000` + +### Example + +``` +QUBODrivers.@setup Optimizer begin + name = "Super Sampler" + version = v"1.0.2" + attributes = begin + NumberOfReads["num_reads"]::Integer = 1_000 + SuperAttribute["super_attr"] = nothing + MegaAttribute::Union{String,Nothing} = "mega" + end +end +``` + +""" +macro setup(raw_args...) + # Parse parameters + args = map(a -> macroexpand(__module__, a), raw_args) + spec = __setup_parse(args...) + + return __setup_quote(spec) +end diff --git a/src/library/setup/parse.jl b/src/library/setup/parse.jl new file mode 100644 index 0000000..6d2ba4d --- /dev/null +++ b/src/library/setup/parse.jl @@ -0,0 +1,180 @@ +function __setup_parse(args...) + setup_error("Macro takes 1 or 2 arguments, not '$(length(args))'") +end + +function __setup_parse(expr) + if expr isa Symbol && Base.isidentifier(expr) + return _SamplerSpec(; id = __setup_parse_id(expr)) + elseif (expr isa Expr && expr.head === :block) + return __setup_parse_block(expr) + else + setup_error( + "Single argument must be either an identifier or a `begin ... end` block", + ) + end +end + +function __setup_parse(id, block) + if !(id isa Symbol) || !Base.isidentifier(id) + setup_error("First argument must be a valid identifier") + end + + if !(block isa Expr && block.head === :block) + setup_error("Second argument must be a `begin ... end` block") + end + + return __setup_parse_block(block; id) +end + +function __setup_parse_block(block; id = :Optimizer) + @assert (block isa Expr && block.head === :block) + + name = nothing + version = nothing + attributes = nothing + + for item in block.args + if item isa LineNumberNode # skip + continue + elseif item isa Expr && item.head === :(=) + key, value = item.args + + if key isa Symbol && Base.isidentifier(key) + if key === :name + if !isnothing(name) + setup_error("Duplicate entries for 'name'") + end + + name = value + elseif key === :version + if !isnothing(version) + setup_error("Duplicate entries for 'version'") + end + + version = value + elseif key === :attributes + if !isnothing(attributes) + setup_error("Duplicate entries for 'attributes' block") + end + + if !(value isa Expr && value.head === :block) + setup_error( + "Sampler attributes must be placed inside a `begin ... end` block", + ) + end + + attributes = _AttrSpec[] + + for stmt in value.args + attr_spec = __setup_parse_attr(stmt) + + if !isnothing(attr_spec) + push!(attributes, attr_spec) + end + end + else + setup_error( + "Sampler configuration keys must be either 'name', 'version' or 'attributes', not '$key'", + ) + end + else + setup_error("Sampler configuration keys must be valid identifiers, not '$key'") + end + else + setup_error("Sampler configuration must be provided by `key = value` pairs") + end + end + + if isnothing(name) + setup_error("'name' entry is missing") + end + + if isnothing(version) + version = QUBODrivers.__VERSION__ + end + + if isnothing(attributes) + attributes = _AttrSpec[] + end + + return _SamplerSpec(; id, name, version, attributes) +end + +function __setup_parse_attr(stmt) + opt_attr = nothing + raw_attr = nothing + val_type = :Any + default = nothing + + if stmt isa LineNumberNode + return nothing + elseif !(stmt isa Expr && stmt.head === :(=)) + setup_error( + "Each attribute definition must be an assignment to a default value ($stmt)", + ) + + return nothing + end + + attr, default = stmt.args + + if attr isa Symbol # ~ MOI attribute only + if !(Base.isidentifier(attr)) + setup_error("Attribute identifier '$attr' is not valid") + end + + opt_attr = attr + elseif attr isa String # ~ Raw attribute only + if isempty(attr) + setup_error("Raw attribute key can't be an empty string") + end + + raw_attr = attr + elseif attr isa Expr && attr.head === :(::) + attr, val_type = attr.args + + if attr isa Symbol + if !(Base.isidentifier(attr)) + setup_error("Attribute identifier '$attr' is not a valid one") + end + + opt_attr = attr + elseif attr isa String + raw_attr = attr + elseif attr isa Expr && (attr.head === :ref || attr.head === :call) + opt_attr, raw_attr = attr.args + + if opt_attr isa Symbol && raw_attr isa String + if !(Base.isidentifier(opt_attr)) + setup_error("Attribute identifier '$opt_attr' is not a valid one") + end + else + setup_error("Invalid attribute identifier '$name($raw)'") + end + else + setup_error("Invalid attribute identifier '$attr'") + end + elseif attr isa Expr && (attr.head === :ref || attr.head === :call) + opt_attr, raw_attr = attr.args + + if opt_attr isa Symbol && raw_attr isa String + if !(Base.isidentifier(opt_attr)) + setup_error("Attribute identifier '$opt_attr' is not a valid one") + end + else + setup_error("Invalid attribute identifier '$name[$raw_attr]'") + end + else + setup_error("Invalid attribute signature '$attr'") + end + + if !isnothing(raw_attr) + if startswith(raw_attr, "□/") + setup_error("Raw attributes starting with '□/' are reserved for internal use") + elseif startswith(raw_attr, "moi/") + setup_error("Raw attributes starting with 'moi/' are reserved for internal use") + end + end + + return _AttrSpec(; opt_attr, raw_attr, val_type, default) +end \ No newline at end of file diff --git a/src/library/setup/quote.jl b/src/library/setup/quote.jl new file mode 100644 index 0000000..bb4886e --- /dev/null +++ b/src/library/setup/quote.jl @@ -0,0 +1,328 @@ +function __setup_quote(spec::_SamplerSpec) + Optimizer = esc(spec.id) + + return quote + Base.@__doc__ mutable struct $(Optimizer){T} <: QUBODrivers.AbstractSampler{T} + model::QUBOTools.Model{VI,T,Int} + attributes::Dict{Symbol,Any} + + function $(Optimizer){T}() where {T} + return new{T}(QUBOTools.Model{VI,T,Int}(), Dict{Symbol,Any}()) + end + end + + # Default constructor + $(Optimizer)() = $(Optimizer){Float64}() + + # Interface definition + $(__setup_quote_interface(spec)) + end +end + +function __setup_quote_interface(spec::_SamplerSpec) + Optimizer = esc(spec.id) + OptimizerName = esc(spec.name) + OptimizerVersion = esc(spec.version) + + return quote + # QUBOTools interface + QUBOTools.backend(sampler::$(Optimizer)) = sampler.model + + function QUBODrivers.set_model!( + sampler::$(Optimizer){T}, + model::QUBOTools.Model{VI,T,Int}, + ) where {T} + sampler.model = model + + return model + end + + # MOI interface + function MOI.get(::$(Optimizer), ::MOI.SolverName) + return $(OptimizerName) + end + + function MOI.get(::$(Optimizer), ::MOI.SolverVersion) + return $(OptimizerVersion) + end + + let opt = $(Optimizer)() + opt_name = MOI.get(opt, MOI.SolverName()) + + if !(opt_name isa String) + error("'name' has to be a string, not '$(opt_name)::$(typeof(opt_name))'") + end + + opt_version = MOI.get(opt, MOI.SolverVersion()) + + if !(opt_version isa VersionNumber) + error("'version' has to be a VersionNumber, not '$(opt_version)::$(typeof(opt_version))'") + end + end + + # Attributes - get + function MOI.get(sampler::$(Optimizer), attr::MOI.RawOptimizerAttribute) + return MOI.get(sampler, QUBODrivers.RawSamplerAttribute(attr.name)) + end + + function MOI.get(sampler::$(Optimizer), attr::QUBODrivers.RawSamplerAttribute) + return QUBODrivers.get_raw_attr(sampler, attr) + end + + function QUBODrivers.get_raw_attr( + sampler::$(Optimizer), + attr::QUBODrivers.RawSamplerAttribute{key}, + ) where {key} + if haskey(sampler.attributes, key) + return sampler.attributes[key] + else + return QUBODrivers.default_raw_attr(sampler, attr) + end + end + + # Attributes - set + function MOI.set(sampler::$(Optimizer), attr::MOI.RawOptimizerAttribute, value::Any) + return MOI.set(sampler, QUBODrivers.RawSamplerAttribute(attr.name), value) + end + + function MOI.set( + sampler::$(Optimizer), + attr::QUBODrivers.RawSamplerAttribute, + value::Any, + ) + QUBODrivers.set_raw_attr!(sampler, attr, value) + + return nothing + end + + function QUBODrivers.set_raw_attr!( + sampler::$(Optimizer), + ::QUBODrivers.RawSamplerAttribute{key}, + value, + ) where {key} + sampler.attributes[key] = value + + return nothing + end + + # Attributes - support + function MOI.supports(sampler::$(Optimizer), attr::MOI.RawOptimizerAttribute)::Bool + return MOI.supports( + sampler::$(Optimizer), + QUBODrivers.RawSamplerAttribute(attr.name), + ) + end + + function MOI.supports(sampler::$(Optimizer), attr::QUBODrivers.RawSamplerAttribute) + return false + end + + # Attributes - MOI + $(__setup_quote_moi_attrs(spec)) + + # Attributes - specific dispatch + $((map(attr_spec -> __setup_quote_attribute(spec, attr_spec), spec.attributes))...) + end +end + +function __setup_quote_moi_attrs(spec::_SamplerSpec) + Optimizer = esc(spec.id) + + return quote + # MOI.Name - get + function MOI.get(sampler::$(Optimizer), ::MOI.Name) + return MOI.get(sampler, RawSamplerAttribute("moi/name")) + end + + QUBODrivers.default_raw_attr(::$(Optimizer), ::raw_attr"moi/name") = "" + + # MOI.Name - set + function MOI.set(sampler::$(Optimizer), ::MOI.Name, value) + return MOI.set(sampler, RawSamplerAttribute("moi/name"), value) + end + + function MOI.set(sampler::$(Optimizer), attr::raw_attr"moi/name", value) + value isa AbstractString || error("Value for 'MOI.Name' must be a string") + + QUBODrivers.set_raw_attr!(sampler, attr, convert(String, value)) + + return nothing + end + + # MOI.Name - Support + MOI.supports(::$(Optimizer), ::Union{MOI.Name, raw_attr"moi/name"}) = true + + # MOI.Silent - get + function MOI.get(sampler::$(Optimizer), ::MOI.Silent) + return MOI.get(sampler, RawSamplerAttribute("moi/silent")) + end + + QUBODrivers.default_raw_attr(::$(Optimizer), ::raw_attr"moi/silent") = false + + # MOI.Silent - set + function MOI.set(sampler::$(Optimizer), ::MOI.Silent, value) + return MOI.set(sampler, RawSamplerAttribute("moi/silent"), value) + end + + function MOI.set(sampler::$(Optimizer), attr::raw_attr"moi/silent", value) + value isa Bool || error("Value for 'MOI.Silent' must be a boolean") + + QUBODrivers.set_raw_attr!(sampler, attr, value) + + return nothing + end + + # MOI.Silent - Support + MOI.supports(::$(Optimizer), ::Union{MOI.Silent, raw_attr"moi/silent"}) = true + + # MOI.TimeLimitSec - get + function MOI.get(sampler::$(Optimizer), ::MOI.TimeLimitSec) + return MOI.get(sampler, RawSamplerAttribute("moi/timelimitsec")) + end + + QUBODrivers.default_raw_attr(::$(Optimizer), ::raw_attr"moi/timelimitsec") = nothing + + # MOI.TimeLimitSec - set + function MOI.set(sampler::$(Optimizer), ::MOI.TimeLimitSec, value) + return MOI.set(sampler, RawSamplerAttribute("moi/timelimitsec"), value) + end + + function MOI.set(sampler::$(Optimizer), attr::raw_attr"moi/timelimitsec", value) + if !(isnothing(value) || (value isa Real && value > zero(value))) + error("Value for 'MOI.TimeLimitSec' must be a positive number, or 'nothing'") + end + + QUBODrivers.set_raw_attr!(sampler, attr, convert(Union{Float64, Nothing}, value)) + + return nothing + end + + # MOI.NumberOfThreads - Support + MOI.supports(::$(Optimizer), ::Union{MOI.NumberOfThreads, raw_attr"moi/NumberOfThreads"}) = true + + # MOI.NumberOfThreads - get + function MOI.get(sampler::$(Optimizer), ::MOI.NumberOfThreads) + return MOI.get(sampler, RawSamplerAttribute("moi/numberofthreads")) + end + + QUBODrivers.default_raw_attr(::$(Optimizer), ::raw_attr"moi/numberofthreads") = 1 + + # MOI.NumberOfThreads - set + function MOI.set(sampler::$(Optimizer), ::MOI.NumberOfThreads, value) + return MOI.set(sampler, RawSamplerAttribute("moi/numberofthreads"), value) + end + + function MOI.set(sampler::$(Optimizer), attr::raw_attr"moi/numberofthreads", value) + if !(value isa Real && isinteger(value) && value > zero(value)) + error("Value for 'MOI.NumberOfThreads' must be a positive integer") + end + + QUBODrivers.set_raw_attr!(sampler, attr, convert(Union{Integer, Nothing}, value)) + + return nothing + end + + # MOI.NumberOfThreads - Support + MOI.supports(::$(Optimizer), ::Union{MOI.NumberOfThreads, raw_attr"moi/numberofthreads"}) = true + + # MOI.VariablePrimalStart - get + function MOI.get(sampler::$(Optimizer), ::MOI.VariablePrimalStart, vi::VI) + i = QUBOTools.index(sampler, vi) + + return QUBOTools.start(sampler, i) + end + + # MOI.VariablePrimalStart - set + function MOI.set(sampler::$(Optimizer){T}, ::MOI.VariablePrimalStart, vi::VI, value) where {T} + if !(isnothing(value) || value isa Real) + error("Value for 'MOI.VariablePrimalStart' must be an integer, or 'nothing'") + end + + if !isnothing(value) + if !(value isa Real && isinteger(value)) + error("Value for 'MOI.VariablePrimalStart' must be an integer, or 'nothing'") + end + + X = QUBOTools.domain(sampler) + + if X === QUBOTools.BoolDomain && !(value == zero(value) || value == one(value)) + error("Integer value for 'MOI.VariablePrimalStart' must be either '0' or '1'") + elseif X === QUBOTools.SpinDomain && !(value == -one(value) || value == one(value)) + error("Integer value for 'MOI.VariablePrimalStart' must be either '-1' or '1'") + end + end + + QUBOTools.attach!(sampler, vi => convert(Union{Integer, Nothing}, value)) + + return nothing + end + + function MOI.set(sampler::$(Optimizer), attr::raw_attr"moi/variableprimalstart", value) + if !(value isa Integer && value > zero(value)) + error("Value for 'MOI.VariablePrimalStart' must be a positive integer") + end + + QUBODrivers.set_raw_attr!(sampler, attr, convert(Union{Integer, Nothing}, value)) + + return nothing + end + + # MOI.VariablePrimalStart - Support + MOI.supports(::$(Optimizer), ::Union{MOI.VariablePrimalStart, raw_attr"moi/variableprimalstart"}) = true + end +end + +function __setup_quote_attribute(spec::_SamplerSpec, attr_spec::_AttrSpec) + Optimizer = esc(spec.id) + attr_name = Symbol(attr_spec.raw_attr) + attr_key = QuoteNode(attr_name) + attr_type = RawSamplerAttribute{attr_name} + attr = attr_type() + default = esc(attr_spec.default) + val_type = esc(attr_spec.val_type) + + attr_code = quote + # Attributes - set + function QUBODrivers.set_raw_attr!(sampler::$(Optimizer), ::$(attr_type), value) + sampler.attributes[$(attr_key)] = convert($(val_type), value) + + return nothing + end + + # Attributes - default + function QUBODrivers.default_raw_attr(sampler::$(Optimizer), ::$(attr_type)) + return $(default)::$(val_type) + end + + # Attributes - support + function MOI.supports(sampler::$(Optimizer), ::$(attr_type)) + return true + end + end + + if !isnothing(attr_spec.opt_attr) + Attribute = esc(attr_spec.opt_attr) + + return quote + $(attr_code) + + struct $(Attribute) <: QUBODrivers.SamplerAttribute end + + function MOI.get(sampler::$(Optimizer), ::$(Attribute)) + return MOI.get(sampler, $(attr)) + end + + function MOI.set(sampler::$(Optimizer), ::$(Attribute), value) + MOI.set(sampler, $(attr), value) + + return nothing + end + + function MOI.supports(sampler::$(Optimizer), ::$(Attribute)) + return true + end + end + else + return attr_code + end +end diff --git a/src/library/setup/specs.jl b/src/library/setup/specs.jl new file mode 100644 index 0000000..40bd04f --- /dev/null +++ b/src/library/setup/specs.jl @@ -0,0 +1,59 @@ +# Specifications to be extracted from macro call +struct _AttrSpec + opt_attr::Union{Symbol,Nothing} + raw_attr::String + val_type::Any + default::Any + + function _AttrSpec(; + opt_attr::Union{Symbol,Nothing} = nothing, + raw_attr::Union{String,Nothing} = nothing, + val_type::Union{Symbol,Expr} = :Any, + default::Any, + ) + @assert !isnothing(opt_attr) || !isnothing(raw_attr) + + if isnothing(raw_attr) + raw_attr = "□/$(opt_attr)" + else + @assert !isempty(raw_attr) + end + + Base.remove_linenums!(val_type) + Base.remove_linenums!(default) + + return new(opt_attr, raw_attr, val_type, default) + end +end + +function Base.:(==)(x::_AttrSpec, y::_AttrSpec) + return x.opt_attr === y.opt_attr && + x.raw_attr == y.raw_attr && + x.val_type == y.val_type && + x.default == y.default +end + +struct _SamplerSpec + id::Symbol + name::Any + version::Any + attributes::Vector{_AttrSpec} + + function _SamplerSpec(; + id::Symbol = :Optimizer, + name::Any = "", + version::Any = QUBODrivers.__VERSION__, + attributes::Vector{_AttrSpec} = _AttrSpec[], + ) + @assert Base.isidentifier(id) + + return new(id, name, version, attributes) + end +end + +function Base.:(==)(x::_SamplerSpec, y::_SamplerSpec) + return x.id === y.id && + x.name == y.name && + x.version == y.version && + x.attributes == y.attributes +end \ No newline at end of file diff --git a/src/test/examples/basic.jl b/src/library/test/examples/basic.jl similarity index 95% rename from src/test/examples/basic.jl rename to src/library/test/examples/basic.jl index bc3db67..708a24e 100644 --- a/src/test/examples/basic.jl +++ b/src/library/test/examples/basic.jl @@ -130,13 +130,13 @@ function _test_basic_spin_min( si = MOI.get.(model, MOI.VariablePrimal(i), s) Hi = MOI.get(model, MOI.ObjectiveValue(i)) - if si ≈ [↓, ↓, ↑] || si ≈ [↓, ↑, ↓] || si ≈ [↑, ↓, ↓] + if si ≈ [↑, ↑, ↓] || si ≈ [↑, ↓, ↑] || si ≈ [↓, ↑, ↑] Test.@test Hi ≈ -5.0 - elseif si ≈ [↑, ↑, ↓] || si ≈ [↑, ↓, ↑] || si ≈ [↓, ↑, ↑] + elseif si ≈ [↓, ↓, ↑] || si ≈ [↓, ↑, ↓] || si ≈ [↑, ↓, ↓] Test.@test Hi ≈ -3.0 - elseif si ≈ [↓, ↓, ↓] - Test.@test Hi ≈ 9.0 elseif si ≈ [↑, ↑, ↑] + Test.@test Hi ≈ 9.0 + elseif si ≈ [↓, ↓, ↓] Test.@test Hi ≈ 15.0 else Test.@test false @@ -154,7 +154,7 @@ function _test_basic_spin_max( h::Vector{T}, J::Matrix{T}, ) where {T,S<:AbstractSampler{T}} - Test.@testset "▷ Spin ⋄ Min" begin + Test.@testset "▷ Spin ⋄ Max" begin # Build Model model = MOI.instantiate(sampler; with_bridge_type = T) @@ -181,13 +181,13 @@ function _test_basic_spin_max( si = MOI.get.(model, MOI.VariablePrimal(i), s) Hi = MOI.get(model, MOI.ObjectiveValue(i)) - if si ≈ [↑, ↑, ↑] + if si ≈ [↓, ↓, ↓] Test.@test Hi ≈ 15.0 - elseif si ≈ [↓, ↓, ↓] + elseif si ≈ [↑, ↑, ↑] Test.@test Hi ≈ 9.0 - elseif si ≈ [↑, ↑, ↓] || si ≈ [↑, ↓, ↑] || si ≈ [↓, ↑, ↑] - Test.@test Hi ≈ -3.0 elseif si ≈ [↓, ↓, ↑] || si ≈ [↓, ↑, ↓] || si ≈ [↑, ↓, ↓] + Test.@test Hi ≈ -3.0 + elseif si ≈ [↑, ↑, ↓] || si ≈ [↑, ↓, ↑] || si ≈ [↓, ↑, ↑] Test.@test Hi ≈ -5.0 else Test.@test false diff --git a/src/test/examples/corner.jl b/src/library/test/examples/corner.jl similarity index 100% rename from src/test/examples/corner.jl rename to src/library/test/examples/corner.jl diff --git a/src/test/examples/examples.jl b/src/library/test/examples/examples.jl similarity index 100% rename from src/test/examples/examples.jl rename to src/library/test/examples/examples.jl diff --git a/src/test/interface/automatic.jl b/src/library/test/interface/automatic.jl similarity index 89% rename from src/test/interface/automatic.jl rename to src/library/test/interface/automatic.jl index ca4e1c1..87656e3 100644 --- a/src/test/interface/automatic.jl +++ b/src/library/test/interface/automatic.jl @@ -1,4 +1,4 @@ -function _test_automatic_interface(::Function, ::Type{S}) where {S<:AutomaticSampler} +function _test_automatic_interface(::Function, ::Type{S}) where {S<:AbstractSampler} Test.@testset "QUBODrivers (Automatic)" verbose = true begin Test.@test hasmethod(QUBODrivers.sample, (S,)) end diff --git a/src/test/interface/moi.jl b/src/library/test/interface/moi.jl similarity index 100% rename from src/test/interface/moi.jl rename to src/library/test/interface/moi.jl diff --git a/src/test/test.jl b/src/library/test/test.jl similarity index 56% rename from src/test/test.jl rename to src/library/test/test.jl index eba4c6f..e56c5aa 100644 --- a/src/test/test.jl +++ b/src/library/test/test.jl @@ -11,22 +11,33 @@ include("examples/examples.jl") """ function test end -function QUBODrivers.test(::Type{S}; examples::Bool=true) where {S<:AbstractSampler} +function QUBODrivers.test(::Type{S}; examples::Bool = true) where {S<:AbstractSampler} QUBODrivers.test(identity, S; examples) return nothing end -function QUBODrivers.test(config!::Function, ::Type{S}; examples::Bool=true) where {S<:AbstractSampler} +function QUBODrivers.test( + config!::Function, + ::Type{S}; + examples::Bool = true, +) where {S<:AbstractSampler} QUBODrivers.test(config!, S{Float64}; examples) return nothing end -function QUBODrivers.test(config!::Function, ::Type{S}; examples::Bool=true) where {T,S<:AbstractSampler{T}} - solver_name = MOI.get(S(), MOI.SolverName()) +function QUBODrivers.test( + config!::Function, + ::Type{S}; + examples::Bool = true, +) where {T,S<:AbstractSampler{T}} + solver = S() - Test.@testset "☢ QUBODrivers' Tests for $(solver_name) ☢" verbose = true begin + solver_name = MOI.get(solver, MOI.SolverName()) + solver_version = MOI.get(solver, MOI.SolverVersion()) + + Test.@testset "☢ QUBODrivers' Test Suite for «$(solver_name) v$(solver_version)» ☢" verbose = true begin Test.@testset "→ Interface" begin _test_moi_interface(config!, S) _test_automatic_interface(config!, S) diff --git a/test/assets/test_macro_throws.jl b/test/assets/test_macro_throws.jl new file mode 100644 index 0000000..0adb248 --- /dev/null +++ b/test/assets/test_macro_throws.jl @@ -0,0 +1,5 @@ +macro test_macro_throws(error, expr) + return quote + @test_throws $(esc(error)) eval(@macroexpand $(esc(expr))) + end +end diff --git a/test/drivers/exact_sampler.jl b/test/drivers/exact_sampler.jl index 4f0751b..770b1fe 100644 --- a/test/drivers/exact_sampler.jl +++ b/test/drivers/exact_sampler.jl @@ -2,4 +2,4 @@ function test_exact_sampler() QUBODrivers.test(ExactSampler.Optimizer) return nothing -end \ No newline at end of file +end diff --git a/test/drivers/identity_sampler.jl b/test/drivers/identity_sampler.jl index 1c24e91..1609c1c 100644 --- a/test/drivers/identity_sampler.jl +++ b/test/drivers/identity_sampler.jl @@ -1,22 +1,19 @@ +function _is_spin(model) + return (VI, Spin) ∈ MOI.get(model, MOI.ListOfConstraintTypesPresent()) +end + function test_identiy_sampler() QUBODrivers.test(IdentitySampler.Optimizer) do model - let is_spin = - (model) -> begin - return (MOI.VariableIndex, Spin) ∈ - MOI.get(model, MOI.ListOfConstraintTypesPresent()) - end - - if is_spin(model) - for (i, x) in enumerate(MOI.get(model, MOI.ListOfVariableIndices())) - MOI.set(model, MOI.VariablePrimalStart(), x, iseven(i) ? -1.0 : 1.0) - end - else # is spin - for (i, x) in enumerate(MOI.get(model, MOI.ListOfVariableIndices())) - MOI.set(model, MOI.VariablePrimalStart(), x, iseven(i) ? 0.0 : 1.0) - end + if _is_spin(model) + for (i, x) in enumerate(MOI.get(model, MOI.ListOfVariableIndices())) + MOI.set(model, MOI.VariablePrimalStart(), x, iseven(i) ? -1.0 : 1.0) + end + else # !is spin + for (i, x) in enumerate(MOI.get(model, MOI.ListOfVariableIndices())) + MOI.set(model, MOI.VariablePrimalStart(), x, iseven(i) ? 0.0 : 1.0) end end end return nothing -end \ No newline at end of file +end diff --git a/test/drivers/sampler_bundle.jl b/test/drivers/sampler_bundle.jl new file mode 100644 index 0000000..e3270b0 --- /dev/null +++ b/test/drivers/sampler_bundle.jl @@ -0,0 +1,13 @@ +include("exact_sampler.jl") +include("identity_sampler.jl") +include("random_sampler.jl") + +function test_sampler_bundle() + @testset "□ Utility Samplers Bundle" verbose = true begin + test_exact_sampler() + test_identiy_sampler() + test_random_sampler() + end + + return nothing +end diff --git a/test/runtests.jl b/test/runtests.jl index 039d160..a8e9056 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2,22 +2,20 @@ using Test using QUBODrivers using QUBODrivers: QUBOTools -include("drivers/exact_sampler.jl") -include("drivers/identity_sampler.jl") -include("drivers/random_sampler.jl") +const VI = MOI.VariableIndex -function test_drivers() - @testset "Driver Bundle" verbose = true begin - test_exact_sampler() - test_identiy_sampler() - test_random_sampler() - end +include("assets/test_macro_throws.jl") - return nothing -end +include("setup/setup.jl") +include("drivers/sampler_bundle.jl") function main() - test_drivers() + @testset "◈ ◈ ◈ QUBODrivers.jl Test Suite ◈ ◈ ◈" verbose = true begin + test_setup_macro() + test_sampler_bundle() + end + + return nothing end -main() # Here we go! \ No newline at end of file +main() # Here we go! diff --git a/test/setup/setup.jl b/test/setup/setup.jl new file mode 100644 index 0000000..2b4118a --- /dev/null +++ b/test/setup/setup.jl @@ -0,0 +1,136 @@ +function test_setup_macro() + @testset "□ @setup macro" verbose = true begin + test_setup_spec_parser() + end + + return nothing +end + +macro setup_spec(raw_args...) + args = map(a -> macroexpand(__module__, a), raw_args) + spec = QUBODrivers.__setup_parse(args...) + + return :($(esc(spec))) +end + +function test_setup_spec_parser() + @testset "▶ Parser" begin + @testset "→ Standard" begin + spec = @setup_spec Optimizer begin + name = "Super" * " " * "Sampler" + version = VersionNumber("1.2.3") + attributes = begin + SuperAttribute("super_attr")::Union{Integer,Nothing} = nothing + UltraAttribute["ultra_attr"]::Union{String,Nothing} = "" + MegaAttribute = (1, 2, 3) + "simple_attr"::Float64 = 1.2 + NormalAttribute("normal_attr") = [] + end + end + + @test spec == QUBODrivers._SamplerSpec(; + id = :Optimizer, + name = :("Super" * " " * "Sampler"), + version = :(VersionNumber("1.2.3")), + attributes = [ + QUBODrivers._AttrSpec(; # + opt_attr = :SuperAttribute, + raw_attr = "super_attr", + val_type = :(Union{Integer,Nothing}), + default = quote nothing end, + ), + QUBODrivers._AttrSpec(; # + opt_attr = :UltraAttribute, + raw_attr = "ultra_attr", + val_type = :(Union{String,Nothing}), + default = "", + ), + QUBODrivers._AttrSpec(; # + opt_attr = :MegaAttribute, + default = :((1, 2, 3)) + ), + QUBODrivers._AttrSpec(; # + raw_attr = "simple_attr", + val_type = :(Float64), + default = 1.2, + ), + QUBODrivers._AttrSpec(; # + opt_attr = :NormalAttribute, + raw_attr = "normal_attr", + val_type = :Any, + default = quote [] end, + ), + ], + ) + end + + @testset "→ Misuse" begin + # Empty macro call + @test_macro_throws QUBODrivers.DriverSetupError @setup_spec() + + # Too many arguments + @test_macro_throws QUBODrivers.DriverSetupError @setup_spec(Optimizer, 1, 2, 3) + + # Invalid single argument + @test_macro_throws QUBODrivers.DriverSetupError @setup_spec(0) + + # Invalid arguments + @test_macro_throws QUBODrivers.DriverSetupError @setup_spec(Optimizer, 0) + @test_macro_throws QUBODrivers.DriverSetupError @setup_spec(0, begin end) + + # Invalid keys + @test_macro_throws QUBODrivers.DriverSetupError @setup_spec(Optimizer, begin + key = "Optimizer" + end) + + @test_macro_throws QUBODrivers.DriverSetupError @setup_spec(Optimizer, begin + ! = "Optimizer" + end) + + @test_macro_throws QUBODrivers.DriverSetupError @setup_spec(Optimizer, begin + "Optimizer" + end) + + @test_macro_throws QUBODrivers.DriverSetupError @setup_spec(Optimizer, begin + 0 => "Optimizer" + end) + + # Duplicate entries + @test_macro_throws QUBODrivers.DriverSetupError @setup_spec(Optimizer, begin + name = "Optimizer" + version = VersionNumber("1.2.3") + name = "Optimizer" + end) + + @test_macro_throws QUBODrivers.DriverSetupError @setup_spec(Optimizer, begin + version = v"1.2.3" + name = "Optimizer" + version = VersionNumber("1.2.3") + end) + + @test_macro_throws QUBODrivers.DriverSetupError @setup_spec(Optimizer, begin + attributes = begin end + name = "Optimizer" + version = v"1.2.3" + attributes = begin end + end) + + # Invalid attribute block + @test_macro_throws QUBODrivers.DriverSetupError @setup_spec(Optimizer, begin + attributes = 0 + name = "Optimizer" + version = v"1.2.3" + end) + + @test_macro_throws QUBODrivers.DriverSetupError @setup_spec(Optimizer, begin + attributes = begin + ! = 3 + end + name = "Optimizer" + version = v"1.2.3" + end) + end + end + + return nothing +end