diff --git a/.JuliaFormatter.toml b/.JuliaFormatter.toml new file mode 100644 index 0000000..ffd961f --- /dev/null +++ b/.JuliaFormatter.toml @@ -0,0 +1,10 @@ +# Configuration file for JuliaFormatter.jl +# For more information, see: https://domluna.github.io/JuliaFormatter.jl/stable/config/ + +verbose = true +format_markdown = true +remove_extra_newlines = true +always_for_in = true +conditional_to_if = true +always_use_return = true +format_docstrings = true diff --git a/.github/workflows/FormatCheck.yml b/.github/workflows/FormatCheck.yml new file mode 100644 index 0000000..8f47bb2 --- /dev/null +++ b/.github/workflows/FormatCheck.yml @@ -0,0 +1,31 @@ +name: FormatCheck +on: + push: + branches: + - main + pull_request: + types: [opened, synchronize, reopened] +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: julia-actions/setup-julia@latest + with: + version: '1' + - uses: actions/checkout@v1 + - name: Format check + shell: julia --color=yes {0} + run: | + using Pkg + # If you update the version, also update the style guide docs. + Pkg.add(PackageSpec(name="JuliaFormatter", version="1")) + using JuliaFormatter + format("src", verbose=true) + format("test", verbose=true) + out = String(read(Cmd(`git diff`))) + if isempty(out) + exit(0) + end + @error "Some files have not been formatted!" + write(stdout, out) + exit(1) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37e48fb..8f8a2cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,4 +6,5 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [0.1.0] -- Initial release + + - Initial release diff --git a/README.md b/README.md index 516b066..d1c880e 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ Currently, JSON is used for serialization, and only optimization solvers and constraint programming solvers are supported. ## API + The main API is `solve` which takes a JSON request or Julia dictionary and a solver and returns a response. @@ -17,12 +18,12 @@ using SolverAPI import HiGHS tiny_min = Dict( - "version" => "0.1", - "sense" => "min", - "variables" => ["x"], - "constraints" => [["==", "x", 1], ["Int", "x"]], - "objectives" => ["x"], - ) + "version" => "0.1", + "sense" => "min", + "variables" => ["x"], + "constraints" => [["==", "x", 1], ["Int", "x"]], + "objectives" => ["x"], +) solve(tiny_min, HiGHS.Optimizer()) ``` @@ -30,7 +31,9 @@ solve(tiny_min, HiGHS.Optimizer()) ## Format ### Request + Request format example: + ```json { "version": "0.1", @@ -58,29 +61,35 @@ Request format example: } } ``` + Required fields: -- `version`: [String] The version of the API that is used. -- `options`: [Array] Options, such as the time limit, if the - model should be printed, or general solver attributes. For a - complete list, please refer to the documentation of the solver. -- `sense`: [String] One of `feas`, `min`, or `max` -- `variables`: [Array] A list of variables that are used in the model. -- `constraints`: [Array] A list of constraints. Each constraint - contains an operation and a set of arguments, such as `["==", "x", 1]`. -- `objectives`: [Array] The objective. + + - `version`: [String] The version of the API that is used. + - `options`: [Array] Options, such as the time limit, if the + model should be printed, or general solver attributes. For a + complete list, please refer to the documentation of the solver. + - `sense`: [String] One of `feas`, `min`, or `max` + - `variables`: [Array] A list of variables that are used in the model. + - `constraints`: [Array] A list of constraints. Each constraint + contains an operation and a set of arguments, such as `["==", "x", 1]`. + - `objectives`: [Array] The objective. #### Options + We always support the following options: -- `silent`: [Bool] Controls if the solver prints any logs. -- `time_limit_sec`: [Float64] Limits the total time expended. The optimization - returns a `TIME_LIMIT` status. -- `print_only`: [Bool] If set to true the model will only be printed - and not solved. -- `print_format`: [String] If and how the model should be - printed. Currently supported formats: default, LaTex, MOF, LP, MPS, NL. + + - `silent`: [Bool] Controls if the solver prints any logs. + - `time_limit_sec`: [Float64] Limits the total time expended. The optimization + returns a `TIME_LIMIT` status. + - `print_only`: [Bool] If set to true the model will only be printed + and not solved. + - `print_format`: [String] If and how the model should be + printed. Currently supported formats: MOI, LaTeX, MOF, LP, MPS, NL. ### Response + Response format examples: + ```json { "version": "0.1", @@ -88,10 +97,10 @@ Response format examples: { "objective_value": 0.0, "primal_status": "FEASIBLE_POINT", - "sol_names": [ + "names": [ "\"x\"" ], - "sol_values": [ + "values": [ 1 ] } @@ -99,7 +108,9 @@ Response format examples: "termination_status": "OPTIMAL" } ``` + Example with a model error: + ```json { "version": "0.1", @@ -114,6 +125,7 @@ Example with a model error: ``` Example with printing and no solving: + ```json { "model_string": "{\"name\":\"MathOptFormat Model\",\"version\":{\"major\":1,\"minor\":4},\"variables\":[{\"name\":\"x\"}],\"objective\":{\"sense\":\"min\",\"function\":{\"type\":\"Variable\",\"name\":\"x\"}},\"constraints\":[{\"name\":\"c1\",\"function\":{\"type\":\"ScalarAffineFunction\",\"terms\":[{\"coefficient\":1.0,\"variable\":\"x\"}],\"constant\":0.0},\"set\":{\"type\":\"EqualTo\",\"value\":1.0}},{\"function\":{\"type\":\"Variable\",\"name\":\"x\"},\"set\":{\"type\":\"Integer\"}}]}", @@ -123,16 +135,16 @@ Example with printing and no solving: ``` Required fields: -- `version`: [String] The version of the API that is used. -- `termination_status`: [String] The MOI termination status. + + - `version`: [String] The version of the API that is used. + - `termination_status`: [String] The MOI termination status. Optional fields: -- `results`: [Array] The results array. Depending on - the optimization none, one, or multiple results will be - present. Each result will contain multiple fields describing the - solution, such as `objective_value`, `primal_status`, - `sol_names`, and `sol_values`. -- `errors`: [Array] None, one, or multiple errors that were present. -- `model_string`: [String] If requested via `print_format` the model -as a string. + - `results`: [Array] The results array. Zero, one, or multiple + results will be present. Each result will contain multiple fields + describing the solution, such as `objective_value`, `primal_status`, + `names`, and `values`. + - `errors`: [Array] None, one, or multiple errors that were present. + - `model_string`: [String] If requested via `print_format` the model + as a string. diff --git a/src/SolverAPI.jl b/src/SolverAPI.jl index 6b026ef..164fabf 100644 --- a/src/SolverAPI.jl +++ b/src/SolverAPI.jl @@ -3,11 +3,9 @@ This module provides an interface to solve optimization problems. -Exports `solve`, `print_model`, `response`, `serialize`, and -`deserialize`. +Exports `solve`, `print_model`, `response`, `serialize`, and `deserialize`. -Our format is based on JSON. For examples, please see `tests/inputs` -and `tests/outputs`. +Our format is based on JSON. For examples, see `tests/inputs` and `tests/outputs`. """ module SolverAPI @@ -29,23 +27,18 @@ An `@enum` of possible errors types. ## Values -- `InvalidFormat`: The request is syntactically incorrect. Expected - fields might be missing or of the wrong type. - -- `InvalidModel`: The model is semantically incorrect. For example, an - unknown `sense` was provided. - -- `Unsupported`: Some unsupported feature was used. That includes - using unsupported attributes for a solver or specifying an - unsupported solver. - -- `NotAllowed`: An operation is supported but cannot be applied in the - current state of the model - -- `Domain`: A domain-specific, internal error occurred. For example, a - function cannot be converted to either linear or quadratic form. - -- `Other`: An error that does not fit in any of the above categories. + - `InvalidFormat`: The request is syntactically incorrect. Expected + fields might be missing or of the wrong type. + - `InvalidModel`: The model is semantically incorrect. For example, an + unknown `sense` was provided. + - `Unsupported`: Some unsupported feature was used. That includes + using unsupported attributes for a solver or specifying an + unsupported solver. + - `NotAllowed`: An operation is supported but cannot be applied in the + current state of the model + - `Domain`: A domain-specific, internal error occurred. For example, a + function cannot be converted to either linear or quadratic form. + - `Other`: An error that does not fit in any of the above categories. """ @enum ErrorType InvalidFormat InvalidModel Unsupported NotAllowed Domain Other @@ -60,18 +53,20 @@ end Return a response ## Args -- `request::SolverAPI.Request`: [optionally] The request that was received. -- `model::MOI.ModelLike`: [optionally] The model that was solved. + + - `request::SolverAPI.Request`: [optionally] The request that was received. + - `model::MOI.ModelLike`: [optionally] The model that was solved. ## Keyword Args -- `version::String`: The version of the API. -- `termination_status::MOI.TerminationStatus`: The termination status of the solver. -- `errors::SolverAPI.Error`: A vector of `Error`s. + + - `version::String`: The version of the API. + - `termination_status::MOI.TerminationStatus`: The termination status of the solver. + - `errors::SolverAPI.Error`: A vector of `Error`s. """ function response(; - version="0.1", - termination_status=MOI.OTHER_ERROR, - errors::Vector{Error}=Error[], + version = "0.1", + termination_status = MOI.OTHER_ERROR, + errors::Vector{Error} = Error[], ) pairs = Pair{String,Any}["version"=>version, "termination_status"=>termination_status] if !isempty(errors) @@ -79,8 +74,8 @@ function response(; end return Dict(pairs) end -response(error::Error; kw...) = response(; errors=[error], kw...) -function response(json::Request, model::MOI.ModelLike; version="0.1", kw...) +response(error::Error; kw...) = response(; errors = [error], kw...) +function response(json::Request, model::MOI.ModelLike; version = "0.1", kw...) res = Dict{String,Any}() res["version"] = version @@ -101,9 +96,9 @@ function response(json::Request, model::MOI.ModelLike; version="0.1", kw...) result_count = MOI.get(model, MOI.ResultCount()) - results = [Dict{String,Any}() for _ = 1:result_count] + results = [Dict{String,Any}() for _ in 1:result_count] - for idx = 1:result_count + for idx in 1:result_count results[idx]["primal_status"] = MOI.get(model, MOI.PrimalStatus(idx)) if status in (MOI.OPTIMAL, MOI.LOCALLY_SOLVED) @@ -128,16 +123,15 @@ end """ serialize([io], response::Response) -Serialize the `response`. +Serialize the `response`. ## Args -- `io::IO`: An optional IO stream to write to. If not provided, a - `Vector{UInt8}` is returned. -- `response`: A `SolverAPI.Response` to serialize. + + - `io::IO`: An optional IO stream to write to. If not provided, a + `Vector{UInt8}` is returned. + - `response`: A `SolverAPI.Response` to serialize. """ -function serialize(response::Response) - return Vector{UInt8}(JSON3.write(response)) -end +serialize(response::Response) = Vector{UInt8}(JSON3.write(response)) serialize(io::IO, response::Response) = JSON3.write(io, response) """ @@ -147,9 +141,7 @@ serialize(io::IO, response::Response) = JSON3.write(io, response) Deserialize `body` to an `Request`. Returns an instances that can be used with `solve` or `print_model`. """ -function deserialize(body::Vector{UInt8}) - return JSON3.read(body) -end +deserialize(body::Vector{UInt8}) = JSON3.read(body) deserialize(body::String) = deserialize(Vector{UInt8}(body)) """ @@ -160,11 +152,11 @@ gracefully and included in `Response`. ## Args -- `fn`: [Optionally] A function to call before solving the problem. This is useful - for setting up custom solver options. The function is called with - the `MOI.ModelLike` model and should return `Nothing`. -- `request`: A `SolverAPI.Request` specifying the optimization problem. -- `solver`: A `MOI.AbstractOptimizer` to use to solve the problem. + - `fn`: [Optionally] A function to call before solving the problem. This is useful + for setting up custom solver options. The function is called with + the `MOI.ModelLike` model and should return `Nothing`. + - `request`: A `SolverAPI.Request` specifying the optimization problem. + - `solver`: A `MOI.AbstractOptimizer` to use to solve the problem. """ function solve(fn, json::Request, solver::MOI.AbstractOptimizer) errors = validate(json) @@ -212,51 +204,56 @@ solve(request::Dict, solver::MOI.AbstractOptimizer) = print_model(model::MOI.ModelLike, format::String)::String Print the `model`. `format` options (case-insensitive) are: -- `default` -- `LaTeX` -- `MOF` -- `LP` -- `MPS` -- `NL` + + - `MOI` (MathOptInterface default printout) + - `LaTeX` (MathOptInterface default LaTeX printout) + - `MOF` (MathOptFormat file) + - `LP` (LP file) + - `MPS` (MPS file) + - `NL` (NL file) """ function print_model(model::MOI.ModelLike, format::String) - format = lowercase(format) - if format == "default" || format == "latex" - mime = MIME(format == "latex" ? "text/latex" : "text/plain") - options = - MOI.Utilities._PrintOptions(mime; simplify_coefficients=true, print_types=false) + format_lower = lowercase(format) + if format_lower == "moi" || format_lower == "latex" + mime = MIME(format_lower == "latex" ? "text/latex" : "text/plain") + options = MOI.Utilities._PrintOptions( + mime; + simplify_coefficients = true, + print_types = false, + ) return sprint() do io - MOI.Utilities._print_model(io, options, model) + return MOI.Utilities._print_model(io, options, model) end - elseif format == "latex" + elseif format_lower == "latex" # NOTE: there are options for latex_formulation, e.g. don't print MOI function/set types # https://jump.dev/MathOptInterface.jl/dev/submodules/Utilities/reference/#MathOptInterface.Utilities.latex_formulation return sprint(print, MOI.Utilities.latex_formulation(model)) - elseif format == "mof" - options = (; format=MOI.FileFormats.FORMAT_MOF, print_compact=true) - elseif format == "lp" - options = (; format=MOI.FileFormats.FORMAT_LP) - elseif format == "mps" - options = (; format=MOI.FileFormats.FORMAT_MPS) - elseif format == "nl" - options = (; format=MOI.FileFormats.FORMAT_NL) + elseif format_lower == "mof" + options = (; format = MOI.FileFormats.FORMAT_MOF, print_compact = true) + elseif format_lower == "lp" + options = (; format = MOI.FileFormats.FORMAT_LP) + elseif format_lower == "mps" + options = (; format = MOI.FileFormats.FORMAT_MPS) + elseif format_lower == "nl" + options = (; format = MOI.FileFormats.FORMAT_NL) else - throw(Error(Unsupported, "File type $format not supported.")) + throw(Error(Unsupported, "File type \"$format\" not supported.")) end dest = MOI.FileFormats.Model(; options...) MOI.copy_to(dest, model) return sprint(write, dest) end -function print_model(request::Request; T=Float64) +function print_model(request::Request; T = Float64) errors = validate(request) - if length(errors) > 0 throw(CompositeException(errors)) end options = get(() -> Dict{String,Any}(), request, :options) - format = get(options, :print_format, "default") + # Default to MOI format. + format = get(options, :print_format, "MOI") + # TODO cleanup/refactor solver_info logic. use_indicator = T == Float64 solver_info = Dict{Symbol,Any}(:use_indicator => use_indicator) model = MOI.Utilities.Model{T}() @@ -266,76 +263,57 @@ function print_model(request::Request; T=Float64) end print_model(request::Dict) = print_model(JSON3.read(JSON3.write(request))) - # Internal # ======================================================================================== function validate(json::Request)#::Vector{Error} out = Error[] + # Helper for adding errors + _err(msg::String) = push!(out, Error(InvalidFormat, msg)) valid_shape = true # Syntax. for k in [:version, :sense, :variables, :constraints, :objectives] if !haskey(json, k) valid_shape = false - push!(out, Error(InvalidFormat, "Missing required field `$(k)`.")) + _err("Missing required field `$(k)`.") end end # If the shape is not valid we can't continue. - if !valid_shape - return out - end + valid_shape || return out if !isa(json.version, String) - push!(out, Error(InvalidFormat, "Invalid `version` field. Must be a string.")) + _err("Invalid `version` field. Must be a string.") end if json.version != "0.1" - push!( - out, - Error( - InvalidFormat, - """Invalid version `$(repr(json.version))`. Only `"0.1"` is supported.""", - ), - ) + _err("Invalid version `$(repr(json.version))`. Only `\"0.1\"` is supported.") end if haskey(json, :options) && !isa(json.options, JSON3.Object) - push!(out, Error(InvalidFormat, "Invalid `options` field. Must be an object.")) + _err("Invalid `options` field. Must be an object.") end for k in [:variables, :constraints, :objectives] if !isa(json[k], JSON3.Array) - push!(out, Error(InvalidFormat, "Invalid `$(k)` field. Must be an array.")) + _err("Invalid `$(k)` field. Must be an array.") end end if any(si -> !isa(si, String), json.variables) - push!(out, Error(InvalidFormat, "Variables must be of type `String`.")) + _err("Variables must be of type `String`.") end # Semantics. if !in(json.sense, ["feas", "min", "max"]) - push!( - out, - Error( - InvalidModel, - "Invalid `sense` field. Must be one of `feas`, `min`, or `max`.", - ), - ) + _err("Invalid `sense` field. Must be one of `feas`, `min`, or `max`.") end if haskey(json, :options) for (T, k) in [(String, :print_format), (Number, :time_limit_sec)] if haskey(json.options, k) && !isa(json.options[k], T) - push!( - out, - Error( - InvalidFormat, - "Invalid `options.$(k)` field. Must be of type `$(T)`.", - ), - ) + _err("Invalid `options.$(k)` field. Must be of type `$(T)`.") end end @@ -344,13 +322,7 @@ function validate(json::Request)#::Vector{Error} val = json.options[k] # We allow `0` and `1` for convenience. if !isa(val, Bool) && val isa Number && val != 0 && val != 1 - push!( - out, - Error( - InvalidFormat, - "Invalid `options.$(k)` field. Must be a boolean.", - ), - ) + _err("Invalid `options.$(k)` field. Must be a boolean.") end end end @@ -358,29 +330,20 @@ function validate(json::Request)#::Vector{Error} if json.sense == "feas" if !isempty(json.objectives) - push!( - out, - Error(InvalidModel, "Objectives must be empty when `sense` is `feas`."), - ) + _err("Objectives must be empty when `sense` is `feas`.") end else if length(json.objectives) != 1 - push!(out, Error(InvalidModel, "Only a single objective is supported.")) + _err("Only a single objective is supported.") end end for con in json.constraints if first(con) == "range" if length(con) != 5 - push!( - out, - Error(InvalidModel, "The `range` constraint expects 4 arguments."), - ) + _err("The `range` constraint expects 4 arguments.") elseif con[4] != 1 - push!( - out, - Error(InvalidModel, "The `range` constraint expects a step size of 1."), - ) + _err("The `range` constraint expects a step size of 1.") end end end @@ -388,7 +351,6 @@ function validate(json::Request)#::Vector{Error} return out end - function initialize(json::Request, solver::MOI.AbstractOptimizer)#::Tuple{Type, Dict{Symbol, Any}, MOI.ModelLike} solver_info = Dict{Symbol,Any}() @@ -403,7 +365,7 @@ function initialize(json::Request, solver::MOI.AbstractOptimizer)#::Tuple{Type, solver_info[:use_indicator] = true end - model = MOI.instantiate(() -> solver; with_cache_type=T, with_bridge_type=T) + model = MOI.instantiate(() -> solver; with_cache_type = T, with_bridge_type = T) for (key, val) in options if key in [:solver, :print_format, :print_only] @@ -524,17 +486,6 @@ function canonicalize_SNF(::Type{T}, f) where {T<:Real} return f end -# TODO(cdc) experiment more: -# function canonicalize_SNF(::Type{T}, f) where {T <: Real} -# try -# f = convert(MOI.ScalarQuadraticFunction{T}, f) -# try -# f = convert(MOI.ScalarAffineFunction{T}, f) -# catch end -# catch end -# return f -# end - function add_obj!( ::Type{T}, model::MOI.ModelLike, @@ -570,12 +521,8 @@ function add_cons!( function _check_v_type(v) if !(v isa MOI.VariableIndex) - throw( - Error( - InvalidModel, - "Variable $v must be of type MOI.VariableIndex, not $(typeof(v)).", - ), - ) + msg = "Variable $v must be of type MOI.VariableIndex, not $(typeof(v))." + throw(Error(InvalidModel, msg)) end end @@ -583,7 +530,9 @@ function add_cons!( for i in eachindex(a) i == 1 && continue if a[i] isa Bool - a[i] || push!(out, Error(InvalidModel, "Model is infeasible.")) + if !a[i] + throw(Error(InvalidModel, "Model is infeasible.")) + end else add_cons!(T, model, a[i], vars_map, solver_info) end @@ -611,8 +560,9 @@ function add_cons!( elseif head == "range" v = json_to_snf(a[5], vars_map) - (a[2] isa Int && a[3] isa Int) || + if !(a[2] isa Int && a[3] isa Int) throw(Error(InvalidModel, "The `range` constraint expects integer bounds.")) + end _check_v_type(v) MOI.add_constraint(model, v, MOI.Integer()) @@ -620,34 +570,29 @@ function add_cons!( elseif head == "implies" && solver_info[:use_indicator] # TODO maybe just check if model supports indicator constraints # use an MOI indicator constraint - length(a) == 3 || + if length(a) != 3 throw(Error(InvalidModel, "The `implies` constraint expects 2 arguments.")) + end f = json_to_snf(a[2], vars_map) g = json_to_snf(a[3], vars_map) - (f.head == :(==) && length(f.args) == 2) || throw( - Error( - InvalidModel, - "The first argument of the `implies` constraint expects to be converted to an equality SNF with 2 arguments.", - ), - ) + if !(f.head == :(==) && length(f.args) == 2) + msg = "The first argument of the `implies` constraint expects to be converted to an equality SNF with 2 arguments." + throw(Error(InvalidModel, msg)) + end v, b = f.args _check_v_type(v) - (b == 1 || b == 0) || throw( - Error( - InvalidModel, - "The second argument of the derived equality SNF from the `implies` constraint expects a binary variable.", - ), - ) + if b != 1 && b != 0 + msg = "The second argument of the derived equality SNF from the `implies` constraint expects a binary variable." + throw(Error(InvalidModel, msg)) + end A = (b == 1) ? MOI.ACTIVATE_ON_ONE : MOI.ACTIVATE_ON_ZERO S1 = get(ineq_to_moi, g.head, nothing) - (!isnothing(S1) && length(g.args) == 2) || throw( - Error( - InvalidModel, - "The second argument of the `implies` constraint expects to be converted to an (in)equality SNF with 2 arguments.", - ), - ) + if isnothing(S1) || length(g.args) != 2 + msg = "The second argument of the `implies` constraint expects to be converted to an (in)equality SNF with 2 arguments." + throw(Error(InvalidModel, msg)) + end h = shift_terms(T, g.args) vaf = MOI.Utilities.operate(vcat, T, v, h) @@ -663,12 +608,8 @@ function add_cons!( g = shift_terms(T, f.args) s = S(zero(T)) if !MOI.supports_constraint(model, typeof(g), typeof(s)) - throw( - Error( - Unsupported, - "Constraint $g in $s isn't supported by this solver.", - ), - ) + msg = "Constraint $g in $s isn't supported by this solver." + throw(Error(Unsupported, msg)) end ci = MOI.Utilities.normalize_and_add_constraint(model, g, s) end @@ -679,7 +620,7 @@ end ineq_to_moi = Dict(:<= => MOI.LessThan, :>= => MOI.GreaterThan, :(==) => MOI.EqualTo) function shift_terms(::Type{T}, args::Vector) where {T<:Real} - @assert length(args) == 2 # This should never happen. + @assert length(args) == 2 # This should never happen. g1 = canonicalize_SNF(T, args[1]) g2 = canonicalize_SNF(T, args[2]) return MOI.Utilities.operate(-, T, g1, g2) diff --git a/test/inputs/cons_is_not_arr.json b/test/inputs/cons_is_not_arr.json index da0db3a..456fce1 100644 --- a/test/inputs/cons_is_not_arr.json +++ b/test/inputs/cons_is_not_arr.json @@ -1,2 +1 @@ {"version":"0.1","sense":"feas","variables":["x"],"constraints":"==","objectives":[],"options":{"solver":"MiniZinc"}} - diff --git a/test/inputs/feas_range.json b/test/inputs/feas_range.json index 4ffb36a..fb5b943 100644 --- a/test/inputs/feas_range.json +++ b/test/inputs/feas_range.json @@ -1,2 +1 @@ {"version":"0.1","sense":"feas","variables":["x"],"constraints":[["range",9, 9, 1, "x"],["Float","x"]],"objectives":[],"options":{"solver":"MiniZinc"}} - diff --git a/test/inputs/feas_with_obj.json b/test/inputs/feas_with_obj.json index 9e35e03..70759f9 100644 --- a/test/inputs/feas_with_obj.json +++ b/test/inputs/feas_with_obj.json @@ -1,2 +1 @@ {"version":"0.1","sense":"feas","variables":["x"],"constraints":[["==","x",1],["Int","x"]],"objectives":["x"],"options":{"solver":"MiniZinc"}} - diff --git a/test/inputs/incorrect_range_num_params.json b/test/inputs/incorrect_range_num_params.json index 258f048..0d7bf99 100644 --- a/test/inputs/incorrect_range_num_params.json +++ b/test/inputs/incorrect_range_num_params.json @@ -1,2 +1 @@ {"version":"0.1","sense":"feas","variables":["x"],"constraints":[["range", 9, 9, "x"],["Int","x"]],"objectives":[],"options":{"solver":"MiniZinc"}} - diff --git a/test/inputs/incorrect_range_step_not_1.json b/test/inputs/incorrect_range_step_not_1.json index e8c79f4..88dbf36 100644 --- a/test/inputs/incorrect_range_step_not_1.json +++ b/test/inputs/incorrect_range_step_not_1.json @@ -1,2 +1 @@ {"version":"0.1","sense":"feas","variables":["x"],"constraints":[["range",9, 9, 2, "x"],["Int","x"]],"objectives":[],"options":{"solver":"MiniZinc"}} - diff --git a/test/inputs/incorrect_solver.json b/test/inputs/incorrect_solver.json index 94b6d64..730b3f9 100644 --- a/test/inputs/incorrect_solver.json +++ b/test/inputs/incorrect_solver.json @@ -1,2 +1 @@ {"version":"0.1","sense":"feas","variables":["x"],"constraints":[["==","x",1],["Int","x"]],"objectives":[],"options":{"solver":"Cplex"}} - diff --git a/test/inputs/min_no_obj.json b/test/inputs/min_no_obj.json index d8f1c37..7f9c38c 100644 --- a/test/inputs/min_no_obj.json +++ b/test/inputs/min_no_obj.json @@ -1,2 +1 @@ {"version":"0.1","sense":"min","variables":["x"],"constraints":[["==","x",1],["Int","x"]],"objectives":[],"options":{"time_limit_sec":60,"solver":"HiGHS","silent":0,"print_format":"MOF"}} - diff --git a/test/inputs/min_range.json b/test/inputs/min_range.json index 743a512..487c0dc 100644 --- a/test/inputs/min_range.json +++ b/test/inputs/min_range.json @@ -1,2 +1 @@ -{"version":"0.1","sense":"min","variables":["x"],"constraints":[["range",0,9,1,"x"],["Int","x"]],"objectives":["x"],"options":{"time_limit_sec":60,"solver":"HiGHS","silent":0,"print_format":"MOF"}} - +{"version":"0.1","sense":"min","variables":["x"],"constraints":[["range",0,9,1,"x"],["Int","x"]],"objectives":["x"],"options":{"time_limit_sec":60,"solver":"HiGHS","silent":0,"print_format":"MOI"}} diff --git a/test/inputs/missing_cons.json b/test/inputs/missing_cons.json index ba46190..89b7338 100644 --- a/test/inputs/missing_cons.json +++ b/test/inputs/missing_cons.json @@ -1,2 +1 @@ {"version":"0.1","sense":"feas","variables":["x"],"objectives":[],"options":{"solver":"MiniZinc"}} - diff --git a/test/inputs/missing_objs.json b/test/inputs/missing_objs.json index 1ef0617..8ccf681 100644 --- a/test/inputs/missing_objs.json +++ b/test/inputs/missing_objs.json @@ -1,2 +1 @@ {"version":"0.1","sense":"feas","variables":["x"],"constraints":[["==","x",1],["Int","x"]],"options":{"solver":"MiniZinc"}} - diff --git a/test/inputs/missing_sense.json b/test/inputs/missing_sense.json index 1bf3ee2..fcf11ba 100644 --- a/test/inputs/missing_sense.json +++ b/test/inputs/missing_sense.json @@ -1,2 +1 @@ {"version":"0.1","variables":["x"],"constraints":[["==","x",1],["Int","x"]],"objectives":[],"options":{"solver":"MiniZinc"}} - diff --git a/test/inputs/missing_vars.json b/test/inputs/missing_vars.json index cbac8fc..b182b50 100644 --- a/test/inputs/missing_vars.json +++ b/test/inputs/missing_vars.json @@ -1,2 +1 @@ {"version":"0.1","sense":"feas","constraints":[["==","x",1],["Int","x"]],"objectives":[],"options":{"solver":"MiniZinc"}} - diff --git a/test/inputs/missing_version.json b/test/inputs/missing_version.json index cd0c8d3..f9241e7 100644 --- a/test/inputs/missing_version.json +++ b/test/inputs/missing_version.json @@ -1,2 +1 @@ {"sense":"feas","variables":["x"],"constraints":[["==","x",1],["Int","x"]],"objectives":[],"options":{"solver":"MiniZinc"}} - diff --git a/test/inputs/n_queens.json b/test/inputs/n_queens.json new file mode 100644 index 0000000..e77a8fc --- /dev/null +++ b/test/inputs/n_queens.json @@ -0,0 +1 @@ +{"version":"0.1","sense":"feas","variables":["q_1","q_2","q_3","q_4"],"constraints":[["range",1,4,1,"q_1"],["range",1,4,1,"q_2"],["range",1,4,1,"q_3"],["range",1,4,1,"q_4"],["and",["and",["!=","q_1","q_2"],["!=",["abs",["-","q_1","q_2"]],1]],["and",["!=","q_1","q_3"],["!=",["abs",["-","q_1","q_3"]],2]],["and",["!=","q_1","q_4"],["!=",["abs",["-","q_1","q_4"]],3]],["and",["!=","q_2","q_3"],["!=",["abs",["-","q_2","q_3"]],1]],["and",["!=","q_2","q_4"],["!=",["abs",["-","q_2","q_4"]],2]],["and",["!=","q_3","q_4"],["!=",["abs",["-","q_3","q_4"]],1]]]],"objectives":[],"options":{"solver":"MiniZinc"}} diff --git a/test/inputs/obj_len_greater_than_1.json b/test/inputs/obj_len_greater_than_1.json index 89c9a52..1aae734 100644 --- a/test/inputs/obj_len_greater_than_1.json +++ b/test/inputs/obj_len_greater_than_1.json @@ -1,2 +1 @@ {"version":"0.1","sense":"min","variables":["x"],"constraints":[["==","x",1],["Int","x"]],"objectives":["x","x+1"],"options":{"time_limit_sec":60,"solver":"HiGHS","silent":0,"print_format":"MOF"}} - diff --git a/test/inputs/objs_is_not_arr.json b/test/inputs/objs_is_not_arr.json index f075111..bcb8a11 100644 --- a/test/inputs/objs_is_not_arr.json +++ b/test/inputs/objs_is_not_arr.json @@ -1,2 +1 @@ {"version":"0.1","sense":"feas","variables":["x"],"constraints":[["==","x",1],["Int","x"]],"objectives":"x","options":{"solver":"MiniZinc"}} - diff --git a/test/inputs/simple_lp.json b/test/inputs/simple_lp.json new file mode 100644 index 0000000..7ffd886 --- /dev/null +++ b/test/inputs/simple_lp.json @@ -0,0 +1 @@ +{"version":"0.1","sense":"min","variables":["x","y"],"constraints":[["and",["==",["+","x",["*",3,"y"]],1],[">=",["+","x","y"],1]],["and",["Int","x"],["Nonneg","x"]],["Int","y"]],"objectives":[["+",["*",2,"x"],"y"]],"options":{"solver":"HiGHS"}} diff --git a/test/inputs/tiny_feas.json b/test/inputs/tiny_feas.json index 893d910..d6d4877 100644 --- a/test/inputs/tiny_feas.json +++ b/test/inputs/tiny_feas.json @@ -1,2 +1 @@ {"version":"0.1","sense":"feas","variables":["x"],"constraints":[["==","x",1],["Int","x"]],"objectives":[],"options":{"solver":"MiniZinc"}} - diff --git a/test/inputs/tiny_infeas.json b/test/inputs/tiny_infeas.json index b65c21f..92188e7 100644 --- a/test/inputs/tiny_infeas.json +++ b/test/inputs/tiny_infeas.json @@ -1,2 +1 @@ {"version":"0.1","sense":"feas","variables":["x"],"constraints":[["range",10, 9, 1, "x"],["Float","x"]],"objectives":[],"options":{"solver":"MiniZinc"}} - diff --git a/test/inputs/tiny_min.json b/test/inputs/tiny_min.json index 2af457f..e21e7fe 100644 --- a/test/inputs/tiny_min.json +++ b/test/inputs/tiny_min.json @@ -1,2 +1 @@ -{"version":"0.1","sense":"min","variables":["x"],"constraints":[["==","x",1],["Int","x"]],"objectives":["x"],"options":{"time_limit_sec":60,"solver":"HiGHS","silent":0,"print_format":"MOF"}} - +{"version":"0.1","sense":"min","variables":["x"],"constraints":[["==","x",1],["Int","x"]],"objectives":["x"],"options":{"time_limit_sec":60,"solver":"HiGHS","silent":0,"print_format":"LaTeX"}} diff --git a/test/inputs/unsupported_print_format.json b/test/inputs/unsupported_print_format.json new file mode 100644 index 0000000..75ab80d --- /dev/null +++ b/test/inputs/unsupported_print_format.json @@ -0,0 +1 @@ +{"version":"0.1","sense":"min","variables":["x"],"constraints":[["==","x",1],["Int","x"]],"objectives":["x"],"options":{"time_limit_sec":60,"solver":"HiGHS","silent":0,"print_format":"What"}} diff --git a/test/inputs/unsupported_sense.json b/test/inputs/unsupported_sense.json index b4e932c..b69ed57 100644 --- a/test/inputs/unsupported_sense.json +++ b/test/inputs/unsupported_sense.json @@ -1,2 +1 @@ {"version":"0.1","sense":"feasibility","variables":["x"],"constraints":[["==","x",1],["Int","x"]],"objectives":["x"],"options":{"solver":"MiniZinc"}} - diff --git a/test/inputs/vars_is_not_arr.json b/test/inputs/vars_is_not_arr.json index 2794d4c..b14018c 100644 --- a/test/inputs/vars_is_not_arr.json +++ b/test/inputs/vars_is_not_arr.json @@ -1,2 +1 @@ {"version":"0.1","sense":"feas","variables":"x","constraints":[["==","x",1],["Int","x"]],"objectives":[],"options":{"solver":"MiniZinc"}} - diff --git a/test/inputs/vars_is_not_str.json b/test/inputs/vars_is_not_str.json index bdb7f2f..7c5e765 100644 --- a/test/inputs/vars_is_not_str.json +++ b/test/inputs/vars_is_not_str.json @@ -1,2 +1 @@ {"version":"0.1","sense":"feas","variables":[0],"constraints":[["==","x",1],["Int","x"]],"objectives":[],"options":{"solver":"MiniZinc"}} - diff --git a/test/outputs/min_range.json b/test/outputs/min_range.json index d0f898c..d6144f8 100644 --- a/test/outputs/min_range.json +++ b/test/outputs/min_range.json @@ -1,2 +1 @@ -{"version":"0.1", "model_string":"{\"name\":\"MathOptFormat Model\",\"version\":{\"major\":1,\"minor\":4},\"variables\":[{\"name\":\"x\"}],\"objective\":{\"sense\":\"min\",\"function\":{\"type\":\"Variable\",\"name\":\"x\"}},\"constraints\":[{\"function\":{\"type\":\"Variable\",\"name\":\"x\"},\"set\":{\"type\":\"Interval\",\"lower\":0.0,\"upper\":9.0}},{\"function\":{\"type\":\"Variable\",\"name\":\"x\"},\"set\":{\"type\":\"Integer\"}}]}","termination_status":"OPTIMAL","results":[{"primal_status":"FEASIBLE_POINT", "names":["\"x\""],"values":[0.0],"objective_value":0.0}]} - +{"model_string":"Minimize: x\n\nSubject to:\n x ∈ [0, 9]\n x ∈ ℤ\n","termination_status":"OPTIMAL","results":[{"names":["\"x\""],"values":[0.0],"primal_status":"FEASIBLE_POINT","objective_value":0.0}],"version":"0.1"} diff --git a/test/outputs/n_queens.json b/test/outputs/n_queens.json new file mode 100644 index 0000000..03a3498 --- /dev/null +++ b/test/outputs/n_queens.json @@ -0,0 +1 @@ +{"termination_status":"OPTIMAL","results":[{"primal_status":"FEASIBLE_POINT","names":["\"q_1\"","\"q_2\"","\"q_3\"","\"q_4\""],"values":[2,4,1,3]}],"version":"0.1"} diff --git a/test/outputs/simple_lp.json b/test/outputs/simple_lp.json new file mode 100644 index 0000000..207c412 --- /dev/null +++ b/test/outputs/simple_lp.json @@ -0,0 +1 @@ +{"termination_status":"OPTIMAL","results":[{"primal_status":"FEASIBLE_POINT","names":["\"x\"","\"y\""],"values":[1.0,0.0],"objective_value":2.0}],"version":"0.1"} diff --git a/test/outputs/tiny_feas.json b/test/outputs/tiny_feas.json index c3371b1..bb3ac4f 100644 --- a/test/outputs/tiny_feas.json +++ b/test/outputs/tiny_feas.json @@ -1,3 +1 @@ {"version":"0.1", "termination_status":"OPTIMAL","results":[{"primal_status": "FEASIBLE_POINT", "names":["\"x\""],"values":[1]}]} - - diff --git a/test/outputs/tiny_infeas.json b/test/outputs/tiny_infeas.json index 4f6ad5c..f68de06 100644 --- a/test/outputs/tiny_infeas.json +++ b/test/outputs/tiny_infeas.json @@ -1,2 +1 @@ {"version":"0.1", "termination_status":"INFEASIBLE"} - diff --git a/test/outputs/tiny_min.json b/test/outputs/tiny_min.json index 50361a1..1cbe96e 100644 --- a/test/outputs/tiny_min.json +++ b/test/outputs/tiny_min.json @@ -1,3 +1 @@ -{"version": "0.1", "model_string":"{\"name\":\"MathOptFormat Model\",\"version\":{\"major\":1,\"minor\":4},\"variables\":[{\"name\":\"x\"}],\"objective\":{\"sense\":\"min\",\"function\":{\"type\":\"Variable\",\"name\":\"x\"}},\"constraints\":[{\"name\":\"c1\",\"function\":{\"type\":\"ScalarAffineFunction\",\"terms\":[{\"coefficient\":1.0,\"variable\":\"x\"}],\"constant\":0.0},\"set\":{\"type\":\"EqualTo\",\"value\":1.0}},{\"function\":{\"type\":\"Variable\",\"name\":\"x\"},\"set\":{\"type\":\"Integer\"}}]}","termination_status":"OPTIMAL","results":[{"primal_status": "FEASIBLE_POINT", "names":["\"x\""],"values":[1.0],"objective_value":1.0}]} - - +{"model_string":"$$ \\begin{aligned}\n\\min\\quad & x \\\\\n\\text{Subject to}\\\\\n & x = 1 \\\\\n & x \\in \\mathbb{Z} \\\\\n\\end{aligned} $$","termination_status":"OPTIMAL","results":[{"names":["\"x\""],"values":[1.0],"primal_status":"FEASIBLE_POINT","objective_value":1.0}],"version":"0.1"} diff --git a/test/solve_tests.jl b/test/solve_tests.jl index 88b11ec..860b892 100644 --- a/test/solve_tests.jl +++ b/test/solve_tests.jl @@ -10,11 +10,12 @@ export run_solve, read_json function _get_solver(solver_name::String) if solver_name == "MiniZinc" - solver = MiniZinc.Optimizer{Int}("chuffed") + return MiniZinc.Optimizer{Int}("chuffed") + elseif solver_name == "HiGHS" + return HiGHS.Optimizer() else - solver = HiGHS.Optimizer() + error("Solver $solver_name not supported.") end - return solver end function run_solve(input::String) @@ -34,7 +35,15 @@ end # end of setup module. import JSON3 # names of JSON files in inputs/ and outputs/ folders - json_names = ["feas_range", "min_range", "tiny_min", "tiny_feas", "tiny_infeas"] + json_names = [ + "feas_range", + "min_range", + "tiny_min", + "tiny_feas", + "tiny_infeas", + "simple_lp", + "n_queens", + ] @testset "$j" for j in json_names input = read_json("inputs", j) @@ -54,40 +63,42 @@ end :objectives => ["x"], ) - for format in ["default", "latex", "mof", "lp", "mps", "nl"] + # test each format + for format in ["moi", "latex", "mof", "lp", "mps", "nl"] options = Dict(:print_format => format) @test print_model(Dict(tiny_min..., :options => options)) isa String end - end @testitem "validate" setup = [SolverSetup] begin using SolverAPI: deserialize, validate import JSON3 - # scenarios with incorrect format + # scenarios with incorrect format format_err_json_names = [ + # TODO fix: error not thrown for "unsupported_print_format" + # "unsupported_print_format", # print format not supported "feas_with_obj", # objective provided for a feasibility problem "min_no_obj", # no objective function specified for a minimization problem - "unsupported_sense", # unsupported sense such as 'feasiblity' + "unsupported_sense", # unsupported sense such as 'feasiblity' "obj_len_greater_than_1", # length of objective greater than 1 - "incorrect_range_num_params", # number of parameters not equal to 4 + "incorrect_range_num_params", # number of parameters not equal to 4 "incorrect_range_step_not_1", # step not one in range definition - "vars_is_not_str", # field variables is not a string - "vars_is_not_arr", # field variables is not an array - "objs_is_not_arr", # field objectives is not an array - "cons_is_not_arr", # field constraints is not an array - "missing_vars", # missing field variables - "missing_cons", # missing field constraints - "missing_objs", # missing field objectives + "vars_is_not_str", # field variables is not a string + "vars_is_not_arr", # field variables is not an array + "objs_is_not_arr", # field objectives is not an array + "cons_is_not_arr", # field constraints is not an array + "missing_vars", # missing field variables + "missing_cons", # missing field constraints + "missing_objs", # missing field objectives "missing_sense", # missing field sense - "missing_version", # missing field version + "missing_version", # missing field version ] @testset "$j" for j in format_err_json_names input = deserialize(read_json("inputs", j)) errors = validate(input) + @test errors isa Vector{SolverAPI.Error} @test length(errors) >= 1 - @test all(errors[i] isa SolverAPI.Error for i in eachindex(errors)) end end