diff --git a/Project.toml b/Project.toml index 52052ca..72db1d4 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "CBLS" uuid = "a3809bfe-37bb-4d48-a667-bac4c6be8d90" authors = ["Jean-Francois Baffier"] -version = "0.2.0" +version = "0.2.1" [deps] ConstraintCommons = "e37357d9-0691-492f-a822-e5ea6a920954" @@ -12,6 +12,7 @@ JuMP = "4076af6c-e467-56ae-b986-b466b2749572" Lazy = "50d2b5c4-7a5e-59d5-8109-a42b560f39c0" LocalSearchSolvers = "2b10edaa-728d-4283-ac71-07e312d6ccf3" MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" +Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" TestItems = "1c621080-faea-4a02-84b6-bbd5e436b8fe" [compat] diff --git a/src/CBLS.jl b/src/CBLS.jl index e636f28..2895f26 100644 --- a/src/CBLS.jl +++ b/src/CBLS.jl @@ -8,6 +8,7 @@ using JuMP using Lazy using LocalSearchSolvers using MathOptInterface +using Pkg using TestItems # Const @@ -30,7 +31,8 @@ const VAR_TYPES = Union{MOI.ZeroOne, MOI.Integer} export DiscreteSet # Export: Constraints -export Error, Predicate +export Error +export Intention, Predicate export AllDifferent export AllEqual diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index fca601b..e1070c6 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -1,12 +1,14 @@ """ JuMP.build_variable(::Function, info::JuMP.VariableInfo, set::T) where T <: MOI.AbstractScalarSet -DOCSTRING +Create a variable constrained by a scalar set. -# Arguments: -- ``: DESCRIPTION -- `info`: DESCRIPTION -- `set`: DESCRIPTION +# Arguments +- `info::JuMP.VariableInfo`: Information about the variable to be created. +- `set::T where T <: MOI.AbstractScalarSet`: The set defining the constraints on the variable. + +# Returns +- `JuMP.VariableConstrainedOnCreation`: A variable constrained by the specified set. """ function JuMP.build_variable( ::Function, @@ -19,12 +21,12 @@ end """ Optimizer <: MOI.AbstractOptimizer -DOCSTRING +Defines an optimizer for CBLS. -# Arguments: -- `solver::Solver`: DESCRIPTION -- `status::MOI.TerminationStatusCode`: DESCRIPTION -- `options::Options`: DESCRIPTION +# Fields +- `solver::LS.MainSolver`: The main solver used for local search. +- `int_vars::Set{Int}`: Set of integer variables. +- `compare_vars::Set{Int}`: Set of variables to compare. """ mutable struct Optimizer <: MOI.AbstractOptimizer solver::LS.MainSolver @@ -35,7 +37,14 @@ end """ Optimizer(model = Model(); options = Options()) -DOCSTRING +Create an instance of the Optimizer. + +# Arguments +- `model`: The model to be optimized. +- `options::Options`: Options for configuring the solver. + +# Returns +- `Optimizer`: An instance of the optimizer. """ function Optimizer(model = model(); options = Options()) return Optimizer( @@ -51,52 +60,104 @@ end @forward Optimizer.solver LS._best_bound, LS.best_value, LS.is_sat, LS.get_value @forward Optimizer.solver LS.domain_size, LS.best_values, LS._max_cons, LS.update_domain! @forward Optimizer.solver LS.get_variable, LS.has_solution, LS.sense, LS.sense! -@forward Optimizer.solver LS.time_info, LS.status +@forward Optimizer.solver LS.time_info, LS.status, LS.length_vars # forward functions from Solver (from Options) @forward Optimizer.solver LS._verbose, LS.set_option!, LS.get_option """ - MOI.get(::Optimizer, ::MOI.SolverName) = begin + MOI.get(::Optimizer, ::MOI.SolverName) + +Get the name of the solver. -DOCSTRING +# Arguments +- `::Optimizer`: The optimizer instance. + +# Returns +- `String`: The name of the solver. """ -MOI.get(::Optimizer, ::MOI.SolverName) = "LocalSearchSolvers" +MOI.get(::Optimizer, ::MOI.SolverName) = "CBLS" """ - MOI.set(::Optimizer, ::MOI.Silent, bool = true) = begin + MOI.set(::Optimizer, ::MOI.Silent, bool = true) + +Set the verbosity of the solver. -DOCSTRING +# Arguments +- `::Optimizer`: The optimizer instance. +- `::MOI.Silent`: The silent option for the solver. +- `bool::Bool`: Whether to set the solver to silent mode. -# Arguments: -- ``: DESCRIPTION -- ``: DESCRIPTION -- `bool`: DESCRIPTION +# Returns +- `Nothing` """ MOI.set(::Optimizer, ::MOI.Silent, bool = true) = @debug "TODO: Silent" """ - MOI.is_empty(model::Optimizer) = begin + MOI.is_empty(model::Optimizer) + +Check if the model is empty. -DOCSTRING +# Arguments +- `model::Optimizer`: The optimizer instance. + +# Returns +- `Bool`: True if the model is empty, false otherwise. """ MOI.is_empty(model::Optimizer) = LS._is_empty(model.solver) """ -Copy constructor for the optimizer + MOI.supports_incremental_interface(::Optimizer) + +Check if the optimizer supports incremental interface. + +# Arguments +- `::Optimizer`: The optimizer instance. + +# Returns +- `Bool`: True if the optimizer supports incremental interface, false otherwise. """ MOI.supports_incremental_interface(::Optimizer) = true + +""" + MOI.copy_to(model::Optimizer, src::MOI.ModelLike) + +Copy the source model to the optimizer. + +# Arguments +- `model::Optimizer`: The optimizer instance. +- `src::MOI.ModelLike`: The source model to be copied. + +# Returns +- `Nothing` +""" function MOI.copy_to(model::Optimizer, src::MOI.ModelLike) return MOIU.default_copy_to(model, src) end """ MOI.optimize!(model::Optimizer) + +Optimize the model using the optimizer. + +# Arguments +- `model::Optimizer`: The optimizer instance. + +# Returns +- `Nothing` """ MOI.optimize!(optimizer::Optimizer) = solve!(optimizer.solver) """ DiscreteSet(values) + +Create a discrete set of values. + +# Arguments +- `values::Vector{T}`: A vector of values to include in the set. + +# Returns +- `DiscreteSet{T}`: A discrete set containing the specified values. """ struct DiscreteSet{T <: Number} <: MOI.AbstractScalarSet values::Vector{T} @@ -105,22 +166,82 @@ DiscreteSet(values) = DiscreteSet(collect(values)) DiscreteSet(values::T...) where {T <: Number} = DiscreteSet(collect(values)) """ - Base.copy(set::DiscreteSet) = begin + Base.copy(set::DiscreteSet) + +Copy a discrete set. + +# Arguments +- `set::DiscreteSet`: The discrete set to be copied. -DOCSTRING +# Returns +- `DiscreteSet`: A copy of the discrete set. """ Base.copy(set::DiscreteSet) = DiscreteSet(copy(set.values)) """ - MOI.empty!(opt) = begin + MOI.empty!(opt) -DOCSTRING +Empty the optimizer. + +# Arguments +- `opt::Optimizer`: The optimizer instance. + +# Returns +- `Nothing` """ MOI.empty!(opt) = empty!(opt) +""" + MOI.is_valid(optimizer::Optimizer, index::CI{VI, MOI.Integer}) + +Check if an index is valid for the optimizer. + +# Arguments +- `optimizer::Optimizer`: The optimizer instance. +- `index::CI{VI, MOI.Integer}`: The index to be checked. + +# Returns +- `Bool`: True if the index is valid, false otherwise. +""" function MOI.is_valid(optimizer::Optimizer, index::CI{VI, MOI.Integer}) return index.value ∈ optimizer.int_vars end +""" + Base.copy(op::F) where {F <: Function} + +Copy a function. + +# Arguments +- `op::F`: The function to be copied. + +# Returns +- `F`: The copied function. +""" Base.copy(op::F) where {F <: Function} = op + +""" + Base.copy(::Nothing) + +Copy a `Nothing` value. + +# Arguments +- `::Nothing`: The `Nothing` value to be copied. + +# Returns +- `Nothing`: The copied `Nothing` value. +""" Base.copy(::Nothing) = nothing + +""" + Moi.get(::Optimizer, ::MOI.SolverVersion) + +Get the version of the solver, here `LocalSearchSolvers.jl`. +""" +function MOI.get(::Optimizer, ::MOI.SolverVersion) + deps = Pkg.dependencies() + local_search_solver_uuid = Base.UUID("2b10edaa-728d-4283-ac71-07e312d6ccf3") + return "v" * string(deps[local_search_solver_uuid].version) +end + +MOI.get(opt::Optimizer, ::MOI.NumberOfVariables) = LS.length_vars(opt) diff --git a/src/constraints.jl b/src/constraints.jl index 0e93d56..d49b244 100644 --- a/src/constraints.jl +++ b/src/constraints.jl @@ -73,50 +73,125 @@ end JuMP.moi_set(set::Error{F}, dim::Int) where {F <: Function} = MOIError(set.f, dim) """ - MOIPredicate{F <: Function} <: MOI.AbstractVectorSet + MOIIntention{F <: Function} <: MOI.AbstractVectorSet -DOCSTRING +Represents an intention set in the model. -# Arguments: -- `f::F`: DESCRIPTION -- `dimension::Int`: DESCRIPTION -- `MOIPredicate(f, dim = 0) = begin - #= none:5 =# - new{typeof(f)}(f, dim) - end`: DESCRIPTION +# Arguments +- `f::F`: A function representing the intention. +- `dimension::Int`: The dimension of the vector set. """ -struct MOIPredicate{F <: Function} <: MOI.AbstractVectorSet +struct MOIIntention{F <: Function} <: MOI.AbstractVectorSet f::F dimension::Int - MOIPredicate(f, dim = 0) = new{typeof(f)}(f, dim) + MOIIntention(f, dim = 0) = new{typeof(f)}(f, dim) end -function MOI.supports_constraint(::Optimizer, ::Type{VOV}, ::Type{MOIPredicate{F}} -) where {F <: Function} + +""" + MOI.supports_constraint(::Optimizer, ::Type{VOV}, ::Type{MOIIntention{F}}) where {F <: Function} + +Check if the optimizer supports a given intention constraint. + +# Arguments +- `::Optimizer`: The optimizer instance. +- `::Type{VOV}`: The type of the variable. +- `::Type{MOIIntention{F}}`: The type of the intention. + +# Returns +- `Bool`: True if the optimizer supports the constraint, false otherwise. +""" +function MOI.supports_constraint( + ::Optimizer, ::Type{VOV}, ::Type{MOIIntention{F}}) where {F <: Function} return true end + +""" + MOI.add_constraint(optimizer::Optimizer, vars::MOI.VectorOfVariables, set::MOIIntention{F}) where {F <: Function} + +Add an intention constraint to the optimizer. + +# Arguments +- `optimizer::Optimizer`: The optimizer instance. +- `vars::MOI.VectorOfVariables`: The variables for the constraint. +- `set::MOIIntention{F}`: The intention set defining the constraint. + +# Returns +- `CI{VOV, MOIIntention{F}}`: The constraint index. +""" function MOI.add_constraint(optimizer::Optimizer, vars::MOI.VectorOfVariables, - set::MOIPredicate{F}) where {F <: Function} + set::MOIIntention{F}) where {F <: Function} err = x -> convert(Float64, !set.f(x)) cidx = constraint!(optimizer, err, map(x -> x.value, vars.variables)) - return CI{VOV, MOIPredicate{F}}(cidx) + return CI{VOV, MOIIntention{F}}(cidx) end -Base.copy(set::MOIPredicate) = MOIPredicate(deepcopy(set.f), copy(set.dimension)) +""" + Base.copy(set::MOIIntention) + +Copy an intention set. + +# Arguments +- `set::MOIIntention`: The intention set to be copied. + +# Returns +- `MOIIntention`: A copy of the intention set. +""" +Base.copy(set::MOIIntention) = MOIIntention(deepcopy(set.f), copy(set.dimension)) """ Predicate{F <: Function} <: JuMP.AbstractVectorSet -Assuming `X` is a (collection of) variables, `concept` a boolean function over `X`, and that a `model` is defined. In `JuMP` syntax we can create a constraint based on `concept` as follows. +Deprecated: Use `Intention` instead. -```julia -@constraint(model, X in Predicate(concept)) -``` +Represents a predicate set in the model. + +# Arguments +- `f::F`: A function representing the predicate. """ struct Predicate{F <: Function} <: JuMP.AbstractVectorSet f::F end -JuMP.moi_set(set::Predicate, dim::Int) = MOIPredicate(set.f, dim) + +""" + Intention{F <: Function} <: JuMP.AbstractVectorSet + +Represents an intention set in the model. + +# Arguments +- `f::F`: A function representing the intention. +""" +struct Intention{F <: Function} <: JuMP.AbstractVectorSet + f::F +end + +""" + JuMP.moi_set(set::Predicate, dim::Int) -> MOIIntention + +Convert a `Predicate` set to a `MOIIntention` set. + +# Arguments +- `set::Predicate`: The predicate set to be converted. +- `dim::Int`: The dimension of the vector set. + +# Returns +- `MOIIntention`: The converted MOIIntention set. +""" +JuMP.moi_set(set::Predicate, dim::Int) = MOIIntention(set.f, dim) + +""" + JuMP.moi_set(set::Intention, dim::Int) -> MOIIntention + +Convert an `Intention` set to a `MOIIntention` set. + +# Arguments +- `set::Intention`: The intention set to be converted. +- `dim::Int`: The dimension of the vector set. + +# Returns +- `MOIIntention`: The converted MOIIntention set. +""" +JuMP.moi_set(set::Intention, dim::Int) = MOIIntention(set.f, dim) ## SECTION - Test Items @testitem "Error and Predicate" begin @@ -129,10 +204,10 @@ JuMP.moi_set(set::Predicate, dim::Int) = MOIPredicate(set.f, dim) @variable(model, 1≤Y[1:4]≤4, Int) @constraint(model, X in Error(x -> x[1] + x[2] + x[3] + x[4] == 10)) - @constraint(model, Y in Predicate(x -> x[1] + x[2] + x[3] + x[4] == 10)) + @constraint(model, Y in Intention(x -> x[1] + x[2] + x[3] + x[4] == 10)) optimize!(model) - @info "Error and Predicate" value.(X) value.(Y) + @info "Error and Intention" value.(X) value.(Y) termination_status(model) @info solution_summary(model) end diff --git a/src/constraints/cumulative.jl b/src/constraints/cumulative.jl index 62425c7..700efa2 100644 --- a/src/constraints/cumulative.jl +++ b/src/constraints/cumulative.jl @@ -61,10 +61,7 @@ struct Cumulative{F <: Function, T1 <: Number, T2 <: Number, V <: VecOrMat{T1}} end end -function Cumulative(; op::F = ≤, pair_vars::V = Vector{Number}(), - val::T2) where {F <: Function, T1 <: Number, T2 <: Number, V <: VecOrMat{T1}} - return Cumulative(op, pair_vars, val) -end +Cumulative(; op = ≤, pair_vars = Vector{Number}(), val) = Cumulative(op, pair_vars, val) function JuMP.moi_set(set::Cumulative, dim::Int) return MOICumulative(set.op, set.pair_vars, set.val, dim) diff --git a/src/constraints/extension.jl b/src/constraints/extension.jl index 60e03bd..3b597c0 100644 --- a/src/constraints/extension.jl +++ b/src/constraints/extension.jl @@ -51,11 +51,7 @@ struct Extension{T <: Number, V <: Union{Vector{Vector{T}}, Tuple{Vector{T}, Vec end end -function Extension(; - pair_vars::V) where { - T <: Number, V <: Union{Vector{Vector{T}}, Tuple{Vector{T}, Vector{T}}}} - return Extension(pair_vars) -end +Extension(; pair_vars) = Extension(pair_vars) function JuMP.moi_set(set::Extension, dim::Int) return MOIExtension(set.pair_vars, dim) diff --git a/src/constraints/n_values.jl b/src/constraints/n_values.jl index f4f542a..fd9b5de 100644 --- a/src/constraints/n_values.jl +++ b/src/constraints/n_values.jl @@ -54,12 +54,7 @@ struct NValues{F <: Function, T1 <: Number, T2 <: Number, V <: Vector{T2}} <: end end -function NValues(; op::F = ==, - val::T1, - vals::V = Vector{Number}()) where { - F <: Function, T1 <: Number, T2 <: Number, V <: Vector{T2}} - return NValues(op, val, vals) -end +NValues(; op = ==, val, vals = Vector{Number}()) = NValues(op, val, vals) function JuMP.moi_set(set::NValues, dim::Int) vals = isnothing(set.vals) ? Vector{Number}() : set.vals diff --git a/src/objectives.jl b/src/objectives.jl index 024aa9b..9d31c2b 100644 --- a/src/objectives.jl +++ b/src/objectives.jl @@ -72,3 +72,11 @@ end function MOIU.map_indices(::Function, sf::ScalarFunction{F, Nothing}) where {F <: Function} return ScalarFunction(sf.f, nothing) end + +function MOIU._to_string(::MOIU._PrintOptions, ::MOI.ModelLike, f::ScalarFunction) + return "Scalar Objective function: $(typeof(f))" +end + +function JuMP.jump_function_type(::GenericModel{T}, F::Type{<:ScalarFunction}) where {T} + return F +end diff --git a/test/JuMP.jl b/test/JuMP.jl index e549e08..8707bcd 100644 --- a/test/JuMP.jl +++ b/test/JuMP.jl @@ -9,15 +9,7 @@ using JuMP @variable(m, 1≤X[1:10]≤4, Int) @constraint(m, X in Error(err)) - @constraint(m, X in Predicate(concept)) - - @constraint(m, X in AllDifferent()) - @constraint(m, X in AllEqual()) - #@constraint(m, X in AllEqualParam(2)) - @constraint(m, X[1:4] in DistDifferent()) - #@constraint(m, X[1:4] in SequentialTasks()) - @constraint(m, X in Ordered()) - #@constraint(m, X in SumEqualParam(22)) + @constraint(m, X in Intention(concept)) optimize!(m) end @@ -33,8 +25,8 @@ end @variable(model, 0≤x≤20, Int) @variable(model, y in DiscreteSet(0:20)) - @constraint(model, [x, y] in Predicate(v -> 6v[1] + 8v[2] >= 100)) - @constraint(model, [x, y] in Predicate(v -> 7v[1] + 12v[2] >= 120)) + @constraint(model, [x, y] in Intention(v -> 6v[1] + 8v[2] >= 100)) + @constraint(model, [x, y] in Intention(v -> 7v[1] + 12v[2] >= 120)) objFunc = v -> 12v[1] + 20v[2] @objective(model, Min, ScalarFunction(objFunc)) @@ -43,4 +35,6 @@ end @info "JuMP: basic opt" value(x) value(y) (12 * value(x)+20 * value(y)) solve_time(model) termination_status(model) @info solution_summary(model) + + @info "testing objective printing" model end diff --git a/test/MOI_wrapper.jl b/test/MOI_wrapper.jl index 758b163..7f4a6d7 100644 --- a/test/MOI_wrapper.jl +++ b/test/MOI_wrapper.jl @@ -1,79 +1,83 @@ -using MathOptInterface -const MOI = MathOptInterface -const MOIT = MOI.Test -const MOIU = MOI.Utilities -const MOIB = MOI.Bridges +# ============================ /test/MOI_wrapper.jl ============================ +module TestCBLS -const VOV = MOI.VectorOfVariables -const VI = MOI.VariableIndex +import CBLS +using Test -const OPTIMIZER_CONSTRUCTOR = MOI.OptimizerWithAttributes( - CBLS.Optimizer, MOI.Silent() => true -) -const OPTIMIZER = MOI.instantiate(OPTIMIZER_CONSTRUCTOR) - -@testset "LocalSearchSolvers" begin - @test MOI.get(OPTIMIZER, MOI.SolverName()) == "LocalSearchSolvers" -end +import MathOptInterface as MOI -# @testset "supports_default_copy_to" begin -# @test MOIU.supports_default_copy_to(OPTIMIZER, false) -# # Use `@test !...` if names are not supported -# @test !MOIU.supports_default_copy_to(OPTIMIZER, true) -# end +const OPTIMIZER = MOI.instantiate( + MOI.OptimizerWithAttributes(CBLS.Optimizer, MOI.Silent() => true), +) const BRIDGED = MOI.instantiate( - OPTIMIZER_CONSTRUCTOR, with_bridge_type = Float64 + MOI.OptimizerWithAttributes(CBLS.Optimizer, MOI.Silent() => true), + with_bridge_type = Float64, with_cache_type = Float64 ) -const CONFIG = MOIT.Config(atol = 1e-6, rtol = 1e-6) - -# @testset "Unit" begin -# # Test all the functions included in dictionary `MOI.Test.unittests`, -# # except functions "number_threads" and "solve_qcp_edge_cases." -# MOIT.unittest( -# BRIDGED, -# CONFIG, -# ["number_threads", "solve_qcp_edge_cases"] -# ) -# end - -# @testset "Modification" begin -# MOIT.modificationtest(BRIDGED, CONFIG) -# end - -# @testset "Continuous Linear" begin -# MOIT.contlineartest(BRIDGED, CONFIG) -# end -# @testset "Continuous Conic" begin -# MOIT.contlineartest(BRIDGED, CONFIG) -# end - -# @testset "Integer Conic" begin -# MOIT.intconictest(BRIDGED, CONFIG) -# end -@testset "MOI: examples" begin - # m = LocalSearchSolvers.Optimizer() - # MOI.add_variables(m, 3) - # MOI.add_constraint(m, VI(1), LS.DiscreteSet([1,2,3])) - # MOI.add_constraint(m, VI(2), LS.DiscreteSet([1,2,3])) - # MOI.add_constraint(m, VI(3), LS.DiscreteSet([1,2,3])) +# See the docstring of MOI.Test.Config for other arguments. +const CONFIG = MOI.Test.Config( + # Modify tolerances as necessary. + atol = 1e-6, + rtol = 1e-6, + # Use MOI.LOCALLY_SOLVED for local solvers. + optimal_status = MOI.LOCALLY_SOLVED # Pass attributes or MOI functions to `exclude` to skip tests that # rely on this functionality. # exclude = Any[MOI.VariableName, MOI.delete] +) - # MOI.add_constraint(m, VOV([VI(1),VI(2)]), LS.MOIPredicate(allunique)) - # MOI.add_constraint(m, VOV([VI(2),VI(3)]), LS.MOIAllDifferent(2)) +""" + runtests() + +This function runs all functions in the this Module starting with `test_`. +""" +function runtests() + for name in names(@__MODULE__; all = true) + if startswith("$(name)", "test_") + @testset "$(name)" begin + getfield(@__MODULE__, name)() + end + end + end +end - # MOI.set(m, MOI.ObjectiveFunction{LS.ScalarFunction}(), LS.ScalarFunction(sum, VI(1))) +""" + test_runtests() + +This function runs all the tests in MathOptInterface.Test. + +Pass arguments to `exclude` to skip tests for functionality that is not +implemented or that your solver doesn't support. +""" +function test_runtests() + MOI.Test.runtests( + BRIDGED, + CONFIG, + exclude = [ + "test_attribute_SolveTimeSec", # Hang indefinitely + "test_model_copy_to_UnsupportedAttribute", # Not supported. What it is suppsoed to be? + "supports_constraint_VariableIndex_EqualTo" # Not supported. What it is suppsoed to be? + ], + # This argument is useful to prevent tests from failing on future + # releases of MOI that add new tests. Don't let this number get too far + # behind the current MOI release though. You should periodically check + # for new tests to fix bugs and implement new features. + exclude_tests_after = v"1.31.0", + verbose = true + ) + return +end - # MOI.optimize!(m) +""" + test_SolverName() - m1 = CBLS.Optimizer() - MOI.add_variable(m1) - MOI.add_constraint(m1, VI(1), CBLS.DiscreteSet([1, 2, 3])) +You can also write new tests for solver-specific functionality. Write each new +test as a function with a name beginning with `test_`. +""" +function test_SolverName() + @test MOI.get(CBLS.Optimizer(), MOI.SolverName()) == "CBLS" + return +end - m2 = CBLS.Optimizer() - MOI.add_constrained_variable(m2, CBLS.DiscreteSet([1, 2, 3])) +end # module TestCBLS - # opt = CBLS.sudoku(3, modeler = :MOI) - # MOI.optimize!(opt) - # @info solution(opt) -end +# This line at tne end of the file runs all the tests! +TestCBLS.runtests()