diff --git a/src/FileFormats/MOF/MOF.jl b/src/FileFormats/MOF/MOF.jl index 63028c1d7e..76537e1b43 100644 --- a/src/FileFormats/MOF/MOF.jl +++ b/src/FileFormats/MOF/MOF.jl @@ -7,7 +7,6 @@ import JSONSchema import MathOptInterface const MOI = MathOptInterface -const Object = OrderedCollections.OrderedDict{String, Any} const SCHEMA_PATH = joinpath(@__DIR__, "v0.4.0.json") const VERSION = let data = JSON.parsefile(SCHEMA_PATH, use_mmap=false) VersionNumber( @@ -16,7 +15,11 @@ const VERSION = let data = JSON.parsefile(SCHEMA_PATH, use_mmap=false) ) end -function _parse_mof_version(version::Object) +const OrderedObject = OrderedCollections.OrderedDict{String, Any} +const UnorderedObject = Dict{String, Any} +const Object = Union{OrderedObject, UnorderedObject} + +function _parse_mof_version(version) return VersionNumber(version["major"], version["minor"]) end @@ -54,7 +57,7 @@ struct Options end function get_options(m::Model) - return get(m.model.ext, :MOF_OPTIONS, Options(false, true, false)) + return get(m.model.ext, :MOF_OPTIONS, Options(false, false, false)) end """ @@ -67,13 +70,14 @@ Keyword arguments are: - `print_compact::Bool=false`: print the JSON file in a compact format without spaces or newlines. - - `validate::Bool=true`: validate each file prior to reading against the MOF - schema + - `validate::Bool=false`: validate each file prior to reading against the MOF + schema. Defaults to `false` because this can take a long time for large + models. - `warn::Bool=false`: print a warning when variables or constraints are renamed """ function Model(; - print_compact::Bool = false, validate::Bool = true, warn::Bool = false + print_compact::Bool = false, validate::Bool = false, warn::Bool = false ) model = MOI.Utilities.UniversalFallback(InnerModel{Float64}()) model.model.ext[:MOF_OPTIONS] = Options(print_compact, validate, warn) diff --git a/src/FileFormats/MOF/nonlinear.jl b/src/FileFormats/MOF/nonlinear.jl index ecad157725..0bf829d9e0 100644 --- a/src/FileFormats/MOF/nonlinear.jl +++ b/src/FileFormats/MOF/nonlinear.jl @@ -1,17 +1,23 @@ # Overload for writing. -function moi_to_object(foo::Nonlinear, model::Model, - name_map::Dict{MOI.VariableIndex, String}) - node_list = Object[] +function moi_to_object( + foo::Nonlinear, name_map::Dict{MOI.VariableIndex, String} +) + node_list = OrderedObject[] foo_object = convert_expr_to_mof(foo.expr, node_list, name_map) - return Object("head" => "ScalarNonlinearFunction", "root" => foo_object, - "node_list" => node_list) + return OrderedObject( + "head" => "ScalarNonlinearFunction", + "root" => foo_object, + "node_list" => node_list, + ) end # Overload for reading. function function_to_moi( - ::Val{:ScalarNonlinearFunction}, object::Object, model::Model, - name_map::Dict{String, MOI.VariableIndex}) - node_list = Object.(object["node_list"]) + ::Val{:ScalarNonlinearFunction}, + object::T, + name_map::Dict{String, MOI.VariableIndex} +) where {T <: Object} + node_list = T.(object["node_list"]) expr = convert_mof_to_expr(object["root"], node_list, name_map) return Nonlinear(expr) end @@ -53,8 +59,9 @@ function extract_function_and_set(expr::Expr) error("Oops. The constraint $(expr) wasn't recognised.") end -function write_nlpblock(object::Object, model::Model, - name_map::Dict{MOI.VariableIndex, String}) +function write_nlpblock( + object::T, model::Model, name_map::Dict{MOI.VariableIndex, String} +) where {T <: Object} # TODO(odow): is there a better way of checking if the NLPBlock is set? nlp_block = try MOI.get(model, MOI.NLPBlock()) @@ -68,18 +75,21 @@ function write_nlpblock(object::Object, model::Model, objective = MOI.objective_expr(nlp_block.evaluator) objective = lift_variable_indices(objective) sense = MOI.get(model, MOI.ObjectiveSense()) - object["objective"] = Object( + object["objective"] = T( "sense" => moi_to_object(sense), - "function" => moi_to_object(Nonlinear(objective), model, name_map) + "function" => moi_to_object(Nonlinear(objective), name_map) ) end for (row, bounds) in enumerate(nlp_block.constraint_bounds) constraint = MOI.constraint_expr(nlp_block.evaluator, row) (func, set) = extract_function_and_set(constraint) func = lift_variable_indices(func) - push!(object["constraints"], - Object("function" => moi_to_object(Nonlinear(func), model, name_map), - "set" => moi_to_object(set, model, name_map)) + push!( + object["constraints"], + T( + "function" => moi_to_object(Nonlinear(func), name_map), + "set" => moi_to_object(set, name_map) + ) ) end end @@ -255,15 +265,17 @@ for (mathoptformat_string, (julia_symbol, num_arguments)) in SUPPORTED_FUNCTIONS end """ - convert_mof_to_expr(node::Object, node_list::Vector{Object}, - name_map::Dict{String, MOI.VariableIndex}) + convert_mof_to_expr( + node::T, node_list::Vector{T}, name_map::Dict{String, MOI.VariableIndex} + ) Convert a MathOptFormat node `node` into a Julia expression given a list of MathOptFormat nodes in `node_list`. Variable names are mapped through `name_map` to their variable index. """ -function convert_mof_to_expr(node::Object, node_list::Vector{Object}, - name_map::Dict{String, MOI.VariableIndex}) +function convert_mof_to_expr( + node::T, node_list::Vector{T}, name_map::Dict{String, MOI.VariableIndex} +) where {T <: Object} head = node["head"] if head == "real" return node["value"] @@ -288,15 +300,21 @@ function convert_mof_to_expr(node::Object, node_list::Vector{Object}, end """ - convert_expr_to_mof(node::Object, node_list::Vector{Object}, - name_map::Dict{MOI.VariableIndex, String}) + convert_expr_to_mof( + node::T, + node_list::Vector{T}, + name_map::Dict{MOI.VariableIndex, String} + ) Convert a Julia expression into a MathOptFormat representation. Any intermediate nodes that are required are appended to `node_list`. Variable indices are mapped through `name_map` to their string name. """ -function convert_expr_to_mof(expr::Expr, node_list::Vector{Object}, - name_map::Dict{MOI.VariableIndex, String}) +function convert_expr_to_mof( + expr::Expr, + node_list::Vector{T}, + name_map::Dict{MOI.VariableIndex, String}, +) where {T <: Object} if expr.head != :call error("Expected an expression that was a function. Got $(expr).") end @@ -306,36 +324,40 @@ function convert_expr_to_mof(expr::Expr, node_list::Vector{Object}, end (mathoptformat_string, arity) = FUNCTION_TO_STRING[function_name] validate_arguments(function_name, arity, length(expr.args) - 1) - node = Object("head" => mathoptformat_string, "args" => Object[]) + node = T("head" => mathoptformat_string, "args" => T[]) for arg in @view(expr.args[2:end]) push!(node["args"], convert_expr_to_mof(arg, node_list, name_map)) end push!(node_list, node) - return Object("head" => "node", "index" => length(node_list)) + return T("head" => "node", "index" => length(node_list)) end # Recursion end for variables. -function convert_expr_to_mof(variable::MOI.VariableIndex, - node_list::Vector{Object}, - name_map::Dict{MOI.VariableIndex, String}) - return Object("head" => "variable", "name" => name_map[variable]) +function convert_expr_to_mof( + variable::MOI.VariableIndex, + ::Vector{T}, + name_map::Dict{MOI.VariableIndex, String}, +) where {T <: Object} + return T("head" => "variable", "name" => name_map[variable]) end # Recursion end for real constants. -function convert_expr_to_mof(value::Real, node_list::Vector{Object}, - name_map::Dict{MOI.VariableIndex, String}) - return Object("head" => "real", "value" => value) +function convert_expr_to_mof( + value::Real, ::Vector{T}, name_map::Dict{MOI.VariableIndex, String} +) where {T <: Object} + return T("head" => "real", "value" => value) end # Recursion end for complex numbers. -function convert_expr_to_mof(value::Complex, node_list::Vector{Object}, - name_map::Dict{MOI.VariableIndex, String}) - return Object("head" => "complex", "real" => real(value), - "imag" => imag(value)) +function convert_expr_to_mof( + value::Complex, ::Vector{T}, ::Dict{MOI.VariableIndex, String} +) where {T <: Object} + return T("head" => "complex", "real" => real(value), "imag" => imag(value)) end # Recursion fallback. -function convert_expr_to_mof(fallback, node_list::Vector{Object}, - name_map::Dict{MOI.VariableIndex, String}) +function convert_expr_to_mof( + fallback, ::Vector{<:Object}, ::Dict{MOI.VariableIndex, String} +) error("Unexpected $(typeof(fallback)) encountered: $(fallback).") end diff --git a/src/FileFormats/MOF/read.jl b/src/FileFormats/MOF/read.jl index c489ff999b..56765d1658 100644 --- a/src/FileFormats/MOF/read.jl +++ b/src/FileFormats/MOF/read.jl @@ -11,12 +11,14 @@ function Base.read!(io::IO, model::Model) if options.validate validate(io) end - object = JSON.parse(io; dicttype=Object) - file_version = _parse_mof_version(object["version"]) + object = JSON.parse(io; dicttype = UnorderedObject) + file_version = _parse_mof_version(object["version"]::UnorderedObject) if file_version > VERSION - error("Sorry, the file $(filename) can't be read because this library" * - " supports v$(VERSION) of MathOptFormat, but the file you are " * - "trying to read is v$(file_version).") + error( + "Sorry, the file can't be read because this library supports " * + "v$(VERSION) of MathOptFormat, but the file you are trying to " * + "read is v$(file_version)." + ) end name_map = read_variables(model, object) read_objective(model, object, name_map) @@ -25,46 +27,60 @@ function Base.read!(io::IO, model::Model) end function read_variables(model::Model, object::Object) - indices = MOI.add_variables(model, length(object["variables"])) name_map = Dict{String, MOI.VariableIndex}() - for (index, variable) in zip(indices, object["variables"]) - if !haskey(variable, "name") - error("Variable is missing a `name` field.") - end - name = variable["name"] - if name == "" - error("Variable name is `\"\"`. MathOptFormat variable names must" * - " be unique and non-empty.") + for variable::typeof(object) in object["variables"] + name = get(variable, "name", "")::String + if isempty(name) + error( + "Variable name is `\"\"`. MathOptFormat variable names must " * + "be unique and non-empty." + ) end + name_map[name] = index = MOI.add_variable(model) MOI.set(model, MOI.VariableName(), index, name) - name_map[name] = index end return name_map end function read_objective( - model::Model, object::Object, name_map::Dict{String, MOI.VariableIndex} + model::Model, + object::Object, + name_map::Dict{String, MOI.VariableIndex} ) - obj = object["objective"] - sense = read_objective_sense(obj["sense"]) + obj = object["objective"]::typeof(object) + sense = read_objective_sense(obj["sense"]::String) MOI.set(model, MOI.ObjectiveSense(), sense) if sense == MOI.FEASIBILITY_SENSE return end - func = function_to_moi(obj["function"], model, name_map) + func = function_to_moi(obj["function"]::typeof(object), name_map) MOI.set(model, MOI.ObjectiveFunction{typeof(func)}(), func) return end -function read_constraints(model::Model, object::Object, - name_map::Dict{String, MOI.VariableIndex}) - for constraint in object["constraints"] - foo = function_to_moi(constraint["function"], model, name_map) - set = set_to_moi(constraint["set"], model, name_map) - index = MOI.add_constraint(model, foo, set) - if haskey(constraint, "name") - MOI.set(model, MOI.ConstraintName(), index, constraint["name"]) - end +function _add_constraint( + model::Model, + object::Object, + name_map::Dict{String, MOI.VariableIndex} +) + index = MOI.add_constraint( + model, + function_to_moi(object["function"]::typeof(object), name_map), + set_to_moi(object["set"]::typeof(object)), + ) + if haskey(object, "name") + MOI.set(model, MOI.ConstraintName(), index, object["name"]::String) + end + return +end + +function read_constraints( + model::Model, + object::Object, + name_map::Dict{String, MOI.VariableIndex} +) + for constraint::typeof(object) in object["constraints"]::Vector + _add_constraint(model, constraint, name_map) end end @@ -79,28 +95,75 @@ function read_objective_sense(sense::String) end end +macro head_to_val(f, arg1, args...) + head = gensym() + body = Expr( + :if, + Expr(:call, :(==), head, string(arg1)), + Expr(:return, Val(arg1)) + ) + leaf = body + for arg in args + new_expr = Expr( + :elseif, + Expr(:call, :(==), head, string(arg)), + Expr(:return, Val(arg)) + ) + push!(leaf.args, new_expr) + leaf = new_expr + end + push!(leaf.args, Expr(:return, Val(head))) + quote + function $(esc(f))($head::String) + $body + end + end +end -""" - function_to_moi(x::OrderedDict, model::Model) +@head_to_val( + head_to_function, + SingleVariable, + VectorOfVariables, + ScalarAffineFunction, + ScalarQuadraticFunction, + VectorAffineFunction, + VectorQuadraticFunction, + ScalarNonlinearFunction, +) -Convert `x` from an OrderedDict representation into a MOI representation. """ -function function_to_moi(x::Object, args...) - val_type = Val{Symbol(x["head"])}() - return function_to_moi(val_type, x, args...) -end + function_to_moi( + x::Object, name_map::Dict{String, MOI.VariableIndex} + ) -function function_to_moi(::Val{FunctionSymbol}, object::Object, model::Model, - name_map::Dict{String, MOI.VariableIndex}) where FunctionSymbol - error("Version $(VERSION) of MathOptFormat does not support the function:" * - " $(FunctionSymbol).") +Convert `x` from an MOF representation into a MOI representation. +""" +function function_to_moi( + x::Object, name_map::Dict{String, MOI.VariableIndex} +) + val = head_to_function(x["head"]::String) + return function_to_moi(val, x, name_map) +end + +function function_to_moi( + ::Val{FunctionSymbol}, + ::Object, + ::Dict{String, MOI.VariableIndex}, +) where {FunctionSymbol} + error( + "Version $(VERSION) of MathOptFormat does not support the function: " * + "$(FunctionSymbol)." + ) end # ========== Non-typed scalar functions ========== -function function_to_moi(::Val{:SingleVariable}, object::Object, model::Model, - name_map::Dict{String, MOI.VariableIndex}) - return MOI.SingleVariable(name_map[object["variable"]]) +function function_to_moi( + ::Val{:SingleVariable}, + object::Object, + name_map::Dict{String, MOI.VariableIndex}, +) + return MOI.SingleVariable(name_map[object["variable"]::String]) end # ========== Typed scalar functions ========== @@ -110,227 +173,294 @@ end # (because it is unnecessary at the JSON level). function parse_scalar_affine_term( - object::Object, model::Model, name_map::Dict{String, MOI.VariableIndex}) + object::Object, name_map::Dict{String, MOI.VariableIndex} +) return MOI.ScalarAffineTerm( - object["coefficient"], - name_map[object["variable"]] + object["coefficient"]::Float64, name_map[object["variable"]::String] ) end -function parse_scalar_affine_terms( - terms, model::Model, name_map::Dict{String, MOI.VariableIndex}) - out_terms = MOI.ScalarAffineTerm{Float64}[] - for term in terms - push!(out_terms, parse_scalar_affine_term(term, model, name_map)) - end - return out_terms -end - -function function_to_moi(::Val{:ScalarAffineFunction}, object::Object, - model::Model, name_map::Dict{String, MOI.VariableIndex}) - return MOI.ScalarAffineFunction( - parse_scalar_affine_terms(object["terms"], model, name_map), - object["constant"] +function function_to_moi( + ::Val{:ScalarAffineFunction}, + object::Object, + name_map::Dict{String, MOI.VariableIndex}, +) + return MOI.ScalarAffineFunction{Float64}( + parse_scalar_affine_term.(object["terms"], Ref(name_map)), + object["constant"]::Float64, ) end function parse_scalar_quadratic_term( - object::Object, model::Model, name_map::Dict{String, MOI.VariableIndex}) + object::Object, name_map::Dict{String, MOI.VariableIndex} +) return MOI.ScalarQuadraticTerm( - object["coefficient"], - name_map[object["variable_1"]], - name_map[object["variable_2"]] + object["coefficient"]::Float64, + name_map[object["variable_1"]::String], + name_map[object["variable_2"]::String], ) end -function parse_scalar_quadratic_terms( - terms, model::Model, name_map::Dict{String, MOI.VariableIndex}) - out_terms = MOI.ScalarQuadraticTerm{Float64}[] - for term in terms - push!(out_terms, parse_scalar_quadratic_term(term, model, name_map)) - end - return out_terms -end - -function function_to_moi(::Val{:ScalarQuadraticFunction}, object::Object, - model::Model, name_map::Dict{String, MOI.VariableIndex}) - return MOI.ScalarQuadraticFunction( - parse_scalar_affine_terms(object["affine_terms"], model, name_map), - parse_scalar_quadratic_terms(object["quadratic_terms"], model, name_map), - object["constant"] +function function_to_moi( + ::Val{:ScalarQuadraticFunction}, + object::Object, + name_map::Dict{String, MOI.VariableIndex}, +) + return MOI.ScalarQuadraticFunction{Float64}( + parse_scalar_affine_term.(object["affine_terms"], Ref(name_map)), + parse_scalar_quadratic_term.(object["quadratic_terms"], Ref(name_map)), + object["constant"]::Float64, ) end # ========== Non-typed vector functions ========== -function function_to_moi(::Val{:VectorOfVariables}, object::Object, model::Model, - name_map::Dict{String, MOI.VariableIndex}) +function function_to_moi( + ::Val{:VectorOfVariables}, + object::Object, + name_map::Dict{String, MOI.VariableIndex}, +) return MOI.VectorOfVariables( - [name_map[variable] for variable in object["variables"]] + [name_map[variable] for variable::String in object["variables"]] ) end # ========== Typed vector functions ========== function parse_vector_affine_term( - object::Object, model::Model, name_map::Dict{String, MOI.VariableIndex}) + object::Object, name_map::Dict{String, MOI.VariableIndex} +) return MOI.VectorAffineTerm( - object["output_index"], - parse_scalar_affine_term(object["scalar_term"], model, name_map) + object["output_index"]::Int, + parse_scalar_affine_term(object["scalar_term"]::typeof(object), name_map), ) end -function parse_vector_affine_terms( - terms, model::Model, name_map::Dict{String, MOI.VariableIndex}) - out_terms = MOI.VectorAffineTerm{Float64}[] - for term in terms - push!(out_terms, parse_vector_affine_term(term, model, name_map)) - end - return out_terms -end - -function function_to_moi(::Val{:VectorAffineFunction}, object::Object, model::Model, - name_map::Dict{String, MOI.VariableIndex}) - return MOI.VectorAffineFunction( - parse_vector_affine_terms(object["terms"], model, name_map), - Float64.(object["constants"]) +function function_to_moi( + ::Val{:VectorAffineFunction}, + object::Object, + name_map::Dict{String, MOI.VariableIndex}, +) + return MOI.VectorAffineFunction{Float64}( + parse_vector_affine_term.(object["terms"], Ref(name_map)), + Float64.(object["constants"]), ) end function parse_vector_quadratic_term( - object::Object, model::Model, name_map::Dict{String, MOI.VariableIndex}) + object::Object, name_map::Dict{String, MOI.VariableIndex} +) return MOI.VectorQuadraticTerm( - object["output_index"], - parse_scalar_quadratic_term(object["scalar_term"], model, name_map) + object["output_index"]::Int, + parse_scalar_quadratic_term(object["scalar_term"], name_map), ) end -function parse_vector_quadratic_terms( - terms, model::Model, name_map::Dict{String, MOI.VariableIndex}) - out_terms = MOI.VectorQuadraticTerm{Float64}[] - for term in terms - push!(out_terms, parse_vector_quadratic_term(term, model, name_map)) - end - return out_terms -end - -function function_to_moi(::Val{:VectorQuadraticFunction}, object::Object, model::Model, - name_map::Dict{String, MOI.VariableIndex}) - return MOI.VectorQuadraticFunction( - parse_vector_affine_terms(object["affine_terms"], model, name_map), - parse_vector_quadratic_terms(object["quadratic_terms"], model, name_map), - Float64.(object["constants"]) +function function_to_moi( + ::Val{:VectorQuadraticFunction}, + object::Object, + name_map::Dict{String, MOI.VariableIndex}, +) + return MOI.VectorQuadraticFunction{Float64}( + parse_vector_affine_term.(object["affine_terms"], Ref(name_map)), + parse_vector_quadratic_term.(object["quadratic_terms"], Ref(name_map)), + Float64.(object["constants"]), ) end # ========== Default fallback ========== + +@head_to_val( + head_to_set, + ZeroOne, + Integer, + LessThan, + GreaterThan, + EqualTo, + Interval, + Semiinteger, + Semicontinuous, + Zeros, + Reals, + Nonnegatives, + Nonpositives, + SecondOrderCone, + RotatedSecondOrderCone, + GeometricMeanCone, + NormOneCone, + NormInfinityCone, + RelativeEntropyCone, + NormSpectralCone, + NormNuclearCone, + RootDetConeTriangle, + RootDetConeSquare, + LogDetConeTriangle, + LogDetConeSquare, + PositiveSemidefiniteConeTriangle, + PositiveSemidefiniteConeSquare, + ExponentialCone, + DualExponentialCone, + PowerCone, + DualPowerCone, + SOS1, + SOS2, + IndicatorSet, +) + """ - set_to_moi(x::OrderedDict, model::Model, name_map::Dict{String, MOI.VariableIndex}) + set_to_moi(x::Object) Convert `x` from an OrderedDict representation into a MOI representation. """ -function set_to_moi(x::Object, args...) - val_type = Val{Symbol(x["head"])}() - return set_to_moi(val_type, x, args...) +set_to_moi(x::Object) = set_to_moi(head_to_set(x["head"]::String), x) + +# ========== Non-typed scalar sets ========== + +function set_to_moi(::Val{:ZeroOne}, ::Object) + return MOI.ZeroOne() end -""" - set_info(::Type{Val{HeadName}}) where HeadName +function set_to_moi(::Val{:Integer}, ::Object) + return MOI.Integer() +end -Return a tuple of the corresponding MOI set and an ordered list of fieldnames. +# ========== Typed scalar sets ========== -`HeadName` is a symbol of the string returned by `head_name(set)`. +function set_to_moi(::Val{:LessThan}, object::Object) + return MOI.LessThan(object["upper"]) +end - HeadName = Symbol(head_name(set)) - typeof(set_info(Val{HeadName})[1]) == typeof(set) -""" -function set_info(::Type{Val{SetSymbol}}) where SetSymbol - error("Version $(VERSION) of MathOptFormat does not support the set: " * - "$(SetSymbol).") +function set_to_moi(::Val{:GreaterThan}, object::Object) + return MOI.GreaterThan(object["lower"]) end -function set_to_moi(::Val{SetSymbol}, object::Object, model::Model, - name_map::Dict{String, MOI.VariableIndex}) where SetSymbol - args = set_info(Val{SetSymbol}) - SetType = args[1] - if length(args) > 1 - return SetType([object[key] for key in args[2:end]]...) - else - return SetType() - end +function set_to_moi(::Val{:EqualTo}, object::Object) + return MOI.EqualTo(object["value"]) end -function set_to_moi(::Val{:SOS1}, object::Object, model::Model, - name_map::Dict{String, MOI.VariableIndex}) - return MOI.SOS1(Float64.(object["weights"])) +function set_to_moi(::Val{:Interval}, object::Object) + return MOI.Interval(object["lower"], object["upper"]) end -function set_to_moi(::Val{:SOS2}, object::Object, model::Model, - name_map::Dict{String, MOI.VariableIndex}) - return MOI.SOS2(Float64.(object["weights"])) + +function set_to_moi(::Val{:Semiinteger}, object::Object) + return MOI.Semiinteger(object["lower"], object["upper"]) end -function set_to_moi( - ::Val{:IndicatorSet}, object::Object, model::Model, - name_map::Dict{String, MOI.VariableIndex} -) - set = set_to_moi(object["set"], model, name_map) - indicator = if object["activate_on"] == "one" - MOI.ACTIVATE_ON_ONE - else - @assert object["activate_on"] == "zero" - MOI.ACTIVATE_ON_ZERO - end - return MOI.IndicatorSet{indicator}(set) +function set_to_moi(::Val{:Semicontinuous}, object::Object) + return MOI.Semicontinuous(object["lower"], object["upper"]) end -# ========== Non-typed scalar sets ========== -set_info(::Type{Val{:ZeroOne}}) = (MOI.ZeroOne,) -set_info(::Type{Val{:Integer}}) = (MOI.Integer,) +# ========== Non-typed vector sets ========== -# ========== Typed scalar sets ========== -set_info(::Type{Val{:LessThan}}) = (MOI.LessThan, "upper") -set_info(::Type{Val{:GreaterThan}}) = (MOI.GreaterThan, "lower") -set_info(::Type{Val{:EqualTo}}) = (MOI.EqualTo, "value") -set_info(::Type{Val{:Interval}}) = (MOI.Interval, "lower", "upper") -set_info(::Type{Val{:Semiinteger}}) = (MOI.Semiinteger, "lower", "upper") -set_info(::Type{Val{:Semicontinuous}}) = (MOI.Semicontinuous, "lower", "upper") +function set_to_moi(::Val{:Zeros}, object::Object) + return MOI.Zeros(object["dimension"]::Int) +end -# ========== Non-typed vector sets ========== -set_info(::Type{Val{:Zeros}}) = (MOI.Zeros, "dimension") -set_info(::Type{Val{:Reals}}) = (MOI.Reals, "dimension") -set_info(::Type{Val{:Nonnegatives}}) = (MOI.Nonnegatives, "dimension") -set_info(::Type{Val{:Nonpositives}}) = (MOI.Nonpositives, "dimension") -set_info(::Type{Val{:SecondOrderCone}}) = (MOI.SecondOrderCone, "dimension") -function set_info(::Type{Val{:RotatedSecondOrderCone}}) - return MOI.RotatedSecondOrderCone, "dimension" +function set_to_moi(::Val{:Reals}, object::Object) + return MOI.Reals(object["dimension"]::Int) +end + +function set_to_moi(::Val{:Nonnegatives}, object::Object) + return MOI.Nonnegatives(object["dimension"]::Int) +end + +function set_to_moi(::Val{:Nonpositives}, object::Object) + return MOI.Nonpositives(object["dimension"]::Int) +end + +function set_to_moi(::Val{:SecondOrderCone}, object::Object) + return MOI.SecondOrderCone(object["dimension"]::Int) +end + +function set_to_moi(::Val{:RotatedSecondOrderCone}, object::Object) + return MOI.RotatedSecondOrderCone(object["dimension"]::Int) +end + +function set_to_moi(::Val{:GeometricMeanCone}, object::Object) + return MOI.GeometricMeanCone(object["dimension"]::Int) +end + +function set_to_moi(::Val{:NormOneCone}, object::Object) + return MOI.NormOneCone(object["dimension"]::Int) +end + +function set_to_moi(::Val{:NormInfinityCone}, object::Object) + return MOI.NormInfinityCone(object["dimension"]::Int) +end + +function set_to_moi(::Val{:RelativeEntropyCone}, object::Object) + return MOI.RelativeEntropyCone(object["dimension"]::Int) +end + +function set_to_moi(::Val{:NormSpectralCone}, object::Object) + return MOI.NormSpectralCone( + object["row_dim"]::Int, object["column_dim"]::Int + ) +end + +function set_to_moi(::Val{:NormNuclearCone}, object::Object) + return MOI.NormNuclearCone( + object["row_dim"]::Int, object["column_dim"]::Int + ) +end + +function set_to_moi(::Val{:RootDetConeTriangle}, object::Object) + return MOI.RootDetConeTriangle(object["side_dimension"]::Int) +end + +function set_to_moi(::Val{:RootDetConeSquare}, object::Object) + return MOI.RootDetConeSquare(object["side_dimension"]::Int) end -set_info(::Type{Val{:GeometricMeanCone}}) = (MOI.GeometricMeanCone, "dimension") -set_info(::Type{Val{:NormOneCone}}) = (MOI.NormOneCone, "dimension") -set_info(::Type{Val{:NormInfinityCone}}) = (MOI.NormInfinityCone, "dimension") -set_info(::Type{Val{:RelativeEntropyCone}}) = (MOI.RelativeEntropyCone, "dimension") -set_info(::Type{Val{:NormSpectralCone}}) = (MOI.NormSpectralCone, "row_dim", "column_dim") -set_info(::Type{Val{:NormNuclearCone}}) = (MOI.NormNuclearCone, "row_dim", "column_dim") -function set_info(::Type{Val{:RootDetConeTriangle}}) - return MOI.RootDetConeTriangle, "side_dimension" + +function set_to_moi(::Val{:LogDetConeTriangle}, object::Object) + return MOI.LogDetConeTriangle(object["side_dimension"]::Int) end -function set_info(::Type{Val{:RootDetConeSquare}}) - return MOI.RootDetConeSquare, "side_dimension" + +function set_to_moi(::Val{:LogDetConeSquare}, object::Object) + return MOI.LogDetConeSquare(object["side_dimension"]::Int) end -function set_info(::Type{Val{:LogDetConeTriangle}}) - return MOI.LogDetConeTriangle, "side_dimension" + +function set_to_moi(::Val{:PositiveSemidefiniteConeTriangle}, object::Object) + return MOI.PositiveSemidefiniteConeTriangle(object["side_dimension"]::Int) end -function set_info(::Type{Val{:LogDetConeSquare}}) - return MOI.LogDetConeSquare, "side_dimension" + +function set_to_moi(::Val{:PositiveSemidefiniteConeSquare}, object::Object) + return MOI.PositiveSemidefiniteConeSquare(object["side_dimension"]::Int) end -function set_info(::Type{Val{:PositiveSemidefiniteConeTriangle}}) - return MOI.PositiveSemidefiniteConeTriangle, "side_dimension" + +function set_to_moi(::Val{:ExponentialCone}, ::Object) + return MOI.ExponentialCone() end -function set_info(::Type{Val{:PositiveSemidefiniteConeSquare}}) - return MOI.PositiveSemidefiniteConeSquare, "side_dimension" + +function set_to_moi(::Val{:DualExponentialCone}, ::Object) + return MOI.DualExponentialCone() end -set_info(::Type{Val{:ExponentialCone}}) = (MOI.ExponentialCone, ) -set_info(::Type{Val{:DualExponentialCone}}) = (MOI.DualExponentialCone, ) # ========== Typed vector sets ========== -set_info(::Type{Val{:PowerCone}}) = (MOI.PowerCone, "exponent") -set_info(::Type{Val{:DualPowerCone}}) = (MOI.DualPowerCone, "exponent") + +function set_to_moi(::Val{:PowerCone}, object::Object) + return MOI.PowerCone(object["exponent"]::Float64) +end + +function set_to_moi(::Val{:DualPowerCone}, object::Object) + return MOI.DualPowerCone(object["exponent"]::Float64) +end + +function set_to_moi(::Val{:SOS1}, object::Object) + return MOI.SOS1(Float64.(object["weights"])) +end + +function set_to_moi(::Val{:SOS2}, object::Object) + return MOI.SOS2(Float64.(object["weights"])) +end + +function set_to_moi(::Val{:IndicatorSet}, object::Object) + set = set_to_moi(object["set"]::typeof(object)) + indicator = if object["activate_on"]::String == "one" + MOI.ACTIVATE_ON_ONE + else + @assert object["activate_on"]::String == "zero" + MOI.ACTIVATE_ON_ZERO + end + return MOI.IndicatorSet{indicator}(set) +end diff --git a/src/FileFormats/MOF/write.jl b/src/FileFormats/MOF/write.jl index 97120c4840..d7ee76bbb7 100644 --- a/src/FileFormats/MOF/write.jl +++ b/src/FileFormats/MOF/write.jl @@ -5,14 +5,14 @@ Write `model` to `io` in the MathOptFormat file format. """ function Base.write(io::IO, model::Model) options = get_options(model) - object = Object( + object = OrderedObject( "name" => "MathOptFormat Model", - "version" => Object( + "version" => OrderedObject( "major" => Int(VERSION.major), "minor" => Int(VERSION.minor) ), "variables" => Object[], - "objective" => Object("sense" => "feasibility"), + "objective" => OrderedObject("sense" => "feasibility"), "constraints" => Object[] ) FileFormats.create_unique_names(model, warn=options.warn) @@ -25,7 +25,7 @@ function Base.write(io::IO, model::Model) return end -function write_variables(object::Object, model::Model) +function write_variables(object, model::Model) name_map = Dict{MOI.VariableIndex, String}() for index in MOI.get(model, MOI.ListOfVariableIndices()) variable = moi_to_object(index, model) @@ -36,24 +36,26 @@ function write_variables(object::Object, model::Model) end function write_objective( - object::Object, model::Model, name_map::Dict{MOI.VariableIndex, String} -) + object::T, model::Model, name_map::Dict{MOI.VariableIndex, String} +) where {T <: Object} if object["objective"]["sense"] != "feasibility" return # Objective must have been written from NLPBlock. end sense = MOI.get(model, MOI.ObjectiveSense()) - object["objective"] = Object("sense" => moi_to_object(sense)) + object["objective"] = T("sense" => moi_to_object(sense)) if sense != MOI.FEASIBILITY_SENSE - objective_type = MOI.get(model, MOI.ObjectiveFunctionType()) - objective_function = MOI.get(model, MOI.ObjectiveFunction{objective_type}()) - object["objective"]["function"] = - moi_to_object(objective_function, model, name_map) + F = MOI.get(model, MOI.ObjectiveFunctionType()) + objective_function = MOI.get(model, MOI.ObjectiveFunction{F}()) + object["objective"]["function"] = moi_to_object( + objective_function, name_map + ) end return end -function write_constraints(object::Object, model::Model, - name_map::Dict{MOI.VariableIndex, String}) +function write_constraints( + object, model::Model, name_map::Dict{MOI.VariableIndex, String} +) for (F, S) in MOI.get(model, MOI.ListOfConstraints()) for index in MOI.get(model, MOI.ListOfConstraintIndices{F,S}()) push!(object["constraints"], moi_to_object(index, model, name_map)) @@ -73,20 +75,23 @@ function moi_to_object(index::MOI.VariableIndex, model::Model) if name == "" error("Variable name for $(index) cannot be blank in an MOF file.") end - return Object("name" => name) + return OrderedObject("name" => name) end -function moi_to_object(index::MOI.ConstraintIndex{F,S}, model::Model, - name_map::Dict{MOI.VariableIndex, String}) where {F, S} +function moi_to_object( + index::MOI.ConstraintIndex{F,S}, + model::Model, + name_map::Dict{MOI.VariableIndex, String} +) where {F, S} func = MOI.get(model, MOI.ConstraintFunction(), index) set = MOI.get(model, MOI.ConstraintSet(), index) name = MOI.get(model, MOI.ConstraintName(), index) - object = Object() + object = OrderedObject() if name != "" object["name"] = name end - object["function"] = moi_to_object(func, model, name_map) - object["set"] = moi_to_object(set, model, name_map) + object["function"] = moi_to_object(func, name_map) + object["set"] = moi_to_object(set, name_map) return object end @@ -103,9 +108,10 @@ end # ========== Non-typed scalar functions ========== -function moi_to_object(foo::MOI.SingleVariable, model::Model, - name_map::Dict{MOI.VariableIndex, String}) - return Object( +function moi_to_object( + foo::MOI.SingleVariable, name_map::Dict{MOI.VariableIndex, String} +) + return OrderedObject( "head" => "SingleVariable", "variable" => name_map[foo.variable] ) @@ -113,86 +119,99 @@ end # ========== Typed scalar functions ========== -function moi_to_object(foo::MOI.ScalarAffineTerm{Float64}, model::Model, - name_map::Dict{MOI.VariableIndex, String}) - return Object( +function moi_to_object( + foo::MOI.ScalarAffineTerm{Float64}, + name_map::Dict{MOI.VariableIndex, String}, +) + return OrderedObject( "coefficient" => foo.coefficient, "variable" => name_map[foo.variable_index] ) end -function moi_to_object(foo::MOI.ScalarAffineFunction{Float64}, model::Model, - name_map::Dict{MOI.VariableIndex, String}) - return Object( +function moi_to_object( + foo::MOI.ScalarAffineFunction{Float64}, + name_map::Dict{MOI.VariableIndex, String}, +) + return OrderedObject( "head" => "ScalarAffineFunction", - "terms" => moi_to_object.(foo.terms, Ref(model), Ref(name_map)), - "constant" => foo.constant + "terms" => moi_to_object.(foo.terms, Ref(name_map)), + "constant" => foo.constant, ) end -function moi_to_object(foo::MOI.ScalarQuadraticTerm{Float64}, model::Model, - name_map::Dict{MOI.VariableIndex, String}) - return Object( +function moi_to_object( + foo::MOI.ScalarQuadraticTerm{Float64}, + name_map::Dict{MOI.VariableIndex, String}, +) + return OrderedObject( "coefficient" => foo.coefficient, "variable_1" => name_map[foo.variable_index_1], "variable_2" => name_map[foo.variable_index_2] ) end -function moi_to_object(foo::MOI.ScalarQuadraticFunction{Float64}, model::Model, - name_map::Dict{MOI.VariableIndex, String}) - return Object( +function moi_to_object( + foo::MOI.ScalarQuadraticFunction{Float64}, + name_map::Dict{MOI.VariableIndex, String}, +) + return OrderedObject( "head" => "ScalarQuadraticFunction", - "affine_terms" => moi_to_object.(foo.affine_terms, Ref(model), Ref(name_map)), - "quadratic_terms" => moi_to_object.(foo.quadratic_terms, Ref(model), Ref(name_map)), - "constant" => foo.constant + "affine_terms" => moi_to_object.(foo.affine_terms, Ref(name_map)), + "quadratic_terms" => moi_to_object.(foo.quadratic_terms, Ref(name_map)), + "constant" => foo.constant, ) end # ========== Non-typed vector functions ========== -function moi_to_object(foo::MOI.VectorOfVariables, model::Model, - name_map::Dict{MOI.VariableIndex, String}) - return Object( +function moi_to_object( + foo::MOI.VectorOfVariables, name_map::Dict{MOI.VariableIndex, String} +) + return OrderedObject( "head" => "VectorOfVariables", - "variables" => [name_map[variable] for variable in foo.variables] + "variables" => [name_map[variable] for variable in foo.variables], ) end # ========== Typed vector functions ========== -function moi_to_object(foo::MOI.VectorAffineTerm, model::Model, - name_map::Dict{MOI.VariableIndex, String}) - return Object( +function moi_to_object( + foo::MOI.VectorAffineTerm, name_map::Dict{MOI.VariableIndex, String} +) + return OrderedObject( "output_index" => foo.output_index, - "scalar_term" => moi_to_object(foo.scalar_term, model, name_map) + "scalar_term" => moi_to_object(foo.scalar_term, name_map), ) end -function moi_to_object(foo::MOI.VectorAffineFunction, model::Model, - name_map::Dict{MOI.VariableIndex, String}) - return Object( +function moi_to_object( + foo::MOI.VectorAffineFunction, name_map::Dict{MOI.VariableIndex, String} +) + return OrderedObject( "head" => "VectorAffineFunction", - "terms" => moi_to_object.(foo.terms, Ref(model), Ref(name_map)), - "constants" => foo.constants + "terms" => moi_to_object.(foo.terms, Ref(name_map)), + "constants" => foo.constants, ) end -function moi_to_object(foo::MOI.VectorQuadraticTerm, model::Model, - name_map::Dict{MOI.VariableIndex, String}) - return Object( +function moi_to_object( + foo::MOI.VectorQuadraticTerm, name_map::Dict{MOI.VariableIndex, String} +) + return OrderedObject( "output_index" => foo.output_index, - "scalar_term" => moi_to_object(foo.scalar_term, model, name_map) + "scalar_term" => moi_to_object(foo.scalar_term, name_map), ) end -function moi_to_object(foo::MOI.VectorQuadraticFunction, model::Model, - name_map::Dict{MOI.VariableIndex, String}) - return Object( +function moi_to_object( + foo::MOI.VectorQuadraticFunction, name_map::Dict{MOI.VariableIndex, String} +) + return OrderedObject( "head" => "VectorQuadraticFunction", - "affine_terms" => moi_to_object.(foo.affine_terms, Ref(model), Ref(name_map)), - "quadratic_terms" => moi_to_object.(foo.quadratic_terms, Ref(model), Ref(name_map)), - "constants" => foo.constants + "affine_terms" => moi_to_object.(foo.affine_terms, Ref(name_map)), + "quadratic_terms" => moi_to_object.(foo.quadratic_terms, Ref(name_map)), + "constants" => foo.constants, ) end @@ -208,9 +227,10 @@ function head_name end # this to be called for a set that is not defined in the MOIU Model constructor. # Add every field as the field is named in MathOptInterface. -function moi_to_object(set::SetType, model::Model, - name_map::Dict{MOI.VariableIndex, String}) where SetType - object = Object("head" => head_name(SetType)) +function moi_to_object( + set::SetType, ::Dict{MOI.VariableIndex, String} +) where {SetType} + object = OrderedObject("head" => head_name(SetType)) for key in fieldnames(SetType) object[string(key)] = getfield(set, key) end @@ -262,13 +282,12 @@ head_name(::Type{<:MOI.SOS1}) = "SOS1" head_name(::Type{<:MOI.SOS2}) = "SOS2" function moi_to_object( - set::MOI.IndicatorSet{I, S}, model::Model, - name_map::Dict{MOI.VariableIndex, String} + set::MOI.IndicatorSet{I, S}, name_map::Dict{MOI.VariableIndex, String} ) where {I, S} @assert I == MOI.ACTIVATE_ON_ONE || I == MOI.ACTIVATE_ON_ZERO - return Object( + return OrderedObject( "head" => "IndicatorSet", - "set" => moi_to_object(set.set, model, name_map), + "set" => moi_to_object(set.set, name_map), "activate_on" => (I == MOI.ACTIVATE_ON_ONE) ? "one" : "zero" ) end diff --git a/test/FileFormats/MOF/MOF.jl b/test/FileFormats/MOF/MOF.jl index 4039e289f8..f7b0637c15 100644 --- a/test/FileFormats/MOF/MOF.jl +++ b/test/FileFormats/MOF/MOF.jl @@ -15,7 +15,7 @@ struct UnsupportedSet <: MOI.AbstractSet end struct UnsupportedFunction <: MOI.AbstractFunction end function test_model_equality(model_string, variables, constraints; suffix="") - model = MOF.Model() + model = MOF.Model(validate = true) MOIU.loadfromstring!(model, model_string) MOI.write_to_file(model, TEST_MOF_FILE * suffix) model_2 = MOF.Model() @@ -58,7 +58,7 @@ end @test_throws Exception MOF.moi_to_object(variable_index, model) MOI.FileFormats.create_unique_names(model, warn=true) @test MOF.moi_to_object(variable_index, model) == - MOF.Object("name" => "x1") + MOF.OrderedObject("name" => "x1") end @testset "Duplicate variable name" begin model = MOF.Model() @@ -66,11 +66,11 @@ end MOI.set(model, MOI.VariableName(), x, "x") y = MOI.add_variable(model) MOI.set(model, MOI.VariableName(), y, "x") - @test MOF.moi_to_object(x, model) == MOF.Object("name" => "x") - @test MOF.moi_to_object(y, model) == MOF.Object("name" => "x") + @test MOF.moi_to_object(x, model) == MOF.OrderedObject("name" => "x") + @test MOF.moi_to_object(y, model) == MOF.OrderedObject("name" => "x") MOI.FileFormats.create_unique_names(model, warn=true) - @test MOF.moi_to_object(x, model) == MOF.Object("name" => "x") - @test MOF.moi_to_object(y, model) == MOF.Object("name" => "x_1") + @test MOF.moi_to_object(x, model) == MOF.OrderedObject("name" => "x") + @test MOF.moi_to_object(y, model) == MOF.OrderedObject("name" => "x_1") end @testset "Blank constraint name" begin model = MOF.Model() @@ -99,24 +99,24 @@ end end @testset "round trips" begin @testset "Empty model" begin - model = MOF.Model() + model = MOF.Model(validate = true) MOI.write_to_file(model, TEST_MOF_FILE) - model_2 = MOF.Model() + model_2 = MOF.Model(validate = true) MOI.read_from_file(model_2, TEST_MOF_FILE) MOIU.test_models_equal(model, model_2, String[], String[]) end @testset "FEASIBILITY_SENSE" begin - model = MOF.Model(validate=false) + model = MOF.Model(validate = true) x = MOI.add_variable(model) MOI.set(model, MOI.VariableName(), x, "x") MOI.set(model, MOI.ObjectiveSense(), MOI.FEASIBILITY_SENSE) MOI.write_to_file(model, TEST_MOF_FILE) - model_2 = MOF.Model(validate=false) + model_2 = MOF.Model(validate = true) MOI.read_from_file(model_2, TEST_MOF_FILE) MOIU.test_models_equal(model, model_2, ["x"], String[]) end @testset "Empty function term" begin - model = MOF.Model() + model = MOF.Model(validate = true) x = MOI.add_variable(model) MOI.set(model, MOI.VariableName(), x, "x") c = MOI.add_constraint(model, @@ -125,7 +125,7 @@ end ) MOI.set(model, MOI.ConstraintName(), c, "c") MOI.write_to_file(model, TEST_MOF_FILE) - model_2 = MOF.Model() + model_2 = MOF.Model(validate = true) MOI.read_from_file(model_2, TEST_MOF_FILE) MOIU.test_models_equal(model, model_2, ["x"], ["c"]) end diff --git a/test/FileFormats/MOF/nonlinear.jl b/test/FileFormats/MOF/nonlinear.jl index 82a97d6cd2..6159e9c7c3 100644 --- a/test/FileFormats/MOF/nonlinear.jl +++ b/test/FileFormats/MOF/nonlinear.jl @@ -1,7 +1,7 @@ function roundtrip_nonlinear_expression( expr, variable_to_string, string_to_variable ) - node_list = MOF.Object[] + node_list = MOF.OrderedObject[] object = MOF.convert_expr_to_mof(expr, node_list, variable_to_string) @test MOF.convert_mof_to_expr(object, node_list, string_to_variable) == expr end @@ -60,7 +60,7 @@ end :(not_supported_function(x)), node_list, variable_to_string) # Test unsupported function for MOF -> Expr. @test_throws Exception MOF.convert_mof_to_expr( - MOF.Object("head"=>"not_supported_function", "value"=>1), + MOF.OrderedObject("head"=>"not_supported_function", "value"=>1), node_list, string_to_variable) # Test n-ary function with no arguments. @test_throws Exception MOF.convert_expr_to_mof(