diff --git a/Project.toml b/Project.toml index 66e5547b..0cb3f686 100644 --- a/Project.toml +++ b/Project.toml @@ -8,6 +8,7 @@ AMD = "14f7f29c-3bd6-536c-9a0b-7339e30b5a3e" DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" GenericLinearAlgebra = "14197337-ba66-59df-a3e3-ca00e7dcff7a" HSL = "34c5aeac-e683-54a6-a0e9-6e0fdc586c50" +JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" Pardiso = "46dd5b70-b6fb-5a00-ae2d-e8fea33afaf2" diff --git a/src/Clarabel.jl b/src/Clarabel.jl index d82db53a..075fcc5c 100644 --- a/src/Clarabel.jl +++ b/src/Clarabel.jl @@ -90,6 +90,9 @@ module Clarabel end end + # JSON I/O + include("./json.jl") + #MathOptInterface for JuMP/Convex.jl module MOI #extensions providing non-standard MOI constraint types include("./MOI_wrapper/MOI_extensions.jl") diff --git a/src/json.jl b/src/json.jl new file mode 100644 index 00000000..4b74f765 --- /dev/null +++ b/src/json.jl @@ -0,0 +1,188 @@ +using JSON, SparseArrays, DataStructures + +# A struct very similar to the problem data, but containing only +# the data types provided by the user (i.e. no internal types). + +mutable struct JsonProblemData{T} + P::SparseMatrixCSC{T} + q::Vector{T} + A::SparseMatrixCSC{T} + b::Vector{T} + cones::Vector{SupportedCone} + settings::Settings{T} +end + + + +function write_to_file(solver::Solver{T}, file::String) where {T} + + json_data = JsonProblemData( + deepcopy(solver.data.P), + deepcopy(solver.data.q), + deepcopy(solver.data.A), + deepcopy(solver.data.b), + deepcopy(solver.data.cones), + deepcopy(solver.settings), + ) + + dinv = solver.data.equilibration.dinv + einv = solver.data.equilibration.einv + c = solver.data.equilibration.c[] + + lrscale!(dinv,json_data.P,dinv) + json_data.q .*= dinv + json_data.P .*= inv(c) + json_data.q .*= inv(c) + + lrscale!(einv,json_data.A,dinv) + json_data.b .*= einv + + # sanitize settings to remove values that + # can't be serialized, i.e. infs + sanitize_settings!(json_data.settings) + + open(file,"w") do io + JSON.print(io,JSON.lower(json_data)) + end + +end + + +function read_from_file(file::String) + + buffer = open(file, "r") do file + read(file, String) + end + json_data = JSON.parse(buffer) + + P = parse(json_data["P"], SparseMatrixCSC{Float64}) + q = parse(json_data["q"], Vector{Float64}) + A = parse(json_data["A"], SparseMatrixCSC{Float64}) + b = parse(json_data["b"], Vector{Float64}) + cones = parse.(json_data["cones"], SupportedCone) + settings = parse(json_data["settings"], Settings{Float64}) + + # desanitize settings to restore inf bounds + desanitize_settings!(settings) + + return Clarabel.Solver(P, q, A, b, cones, settings) + +end + + + +function sanitize_settings!(settings::Settings{T}) where T + + for field in fieldnames(typeof(settings)) + value = getfield(settings, field) + if isa(value,T) && isinf(value) + setfield!(settings, field, sign(value) * floatmax(T)) + end + end +end + + +function desanitize_settings!(settings::Settings{T}) where T + + for field in fieldnames(typeof(settings)) + value = getfield(settings, field) + + if isa(value,T) && floatmax(T) == abs(value) + setfield!(settings, field, sign(value) * T(Inf)) + end + end +end + + + + +# Julia lowers SparseMatricCSC into a dense-like format, so force +# it back to a sparse format with the CSC fields +function JSON.lower(data::JsonProblemData{T}) where T + return OrderedDict( + "P" => lower(data.P), + "q" => data.q, + "A" => lower(data.A), + "b" => data.b, + "cones" => lower.(data.cones), + "settings" => data.settings, + ) +end + + +# Julia lowers SparseMatricCSC into a dense-like format, so force +# it back to a sparse format with the CSC fields +function lower(A::SparseMatrixCSC{T}) where T + return OrderedDict( + "m" => A.m, + "n" => A.n, + "colptr" => A.colptr .- 1, + "rowval" => A.rowval .- 1, + "nzval" => A.nzval, + ) +end + +function lower(cone::SupportedCone) + + #PJG: This won't work on cones with fields other than dim + typesym = nameof(typeof(cone)) + + if isa(cone, PowerConeT) + return OrderedDict(typesym => cone.α) + elseif isa(cone,GenPowerConeT) + return OrderedDict(typesym => [cone.α, cone.dim2]) + else + return OrderedDict(typesym => cone.dim) + end +end + + +function parse(dict::AbstractDict, ::Type{SparseMatrixCSC{T}}) where{T} + + SparseMatrixCSC( + Int(dict["m"]), + Int(dict["n"]), + convert(Vector{Int},dict["colptr"]) .+ 1, + convert(Vector{Int},dict["rowval"]) .+ 1, + convert(Vector{T},dict["nzval"]), + ) +end + + +function parse(data::Vector{Any}, ::Type{Vector{T}}) where T + T.(data) +end + +function parse(dict::AbstractDict, ::Type{Settings{T}}) where T + + settings = Settings{T}() + for (key, value) in dict + + symbkey = Symbol(key) + typeval = typeof(getfield(settings, symbkey)) + + setfield!(settings, symbkey, typeval(value)) + end + return settings + + end + + function parse(dict::AbstractDict, ::Type{SupportedCone}) + + # there should only be 1 key in the dict + key = collect(keys(dict))[1] + coneT = eval(Meta.parse(key)) + + if key == "GenPowerConeT" + vals = dict[key] + α = convert(Vector{Float64}, vals[1]) + dim2 = Int(vals[2]) + return coneT(α,dim2) + + else + # all other cones have a single scalar field + scalar = dict[key] + return coneT(scalar) + end + + end \ No newline at end of file