diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index ad8d929..d1d83c2 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -18,7 +18,7 @@ jobs: fail-fast: false matrix: version: - - '1.0' + - '1.6' - '1.9' - 'nightly' os: diff --git a/.gitignore b/.gitignore index 31d573e..4e890ab 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ /docs/Manifest.toml /docs/build/ Manifest.toml +/.vscode/* diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 9e26dfe..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/Project.toml b/Project.toml index 557be60..dc49acf 100644 --- a/Project.toml +++ b/Project.toml @@ -3,11 +3,17 @@ uuid = "e1d8bfa7-c465-446a-84b9-451470f6e76c" authors = ["andrewrosemberg and contributors"] version = "1.0.0-DEV" +[deps] +JuMP = "4076af6c-e467-56ae-b986-b466b2749572" +ParametricOptInterface = "0ce4ce61-57bf-432b-a095-efac525d185e" + [compat] julia = "1" [extras] Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +DelimitedFiles = "8bb1440f-4735-579b-a4ab-409b98df4dab" +HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b" [targets] -test = ["Test"] +test = ["Test", "DelimitedFiles", "HiGHS"] diff --git a/README.md b/README.md index 08dd682..44536b1 100644 --- a/README.md +++ b/README.md @@ -6,3 +6,51 @@ Learning to optimize (L2O) package that provides basic functionalities to help f [![Build Status](https://github.com/andrewrosemberg/L2O.jl/actions/workflows/CI.yml/badge.svg?branch=main)](https://github.com/andrewrosemberg/L2O.jl/actions/workflows/CI.yml?query=branch%3Amain) [![Build Status](https://travis-ci.com/andrewrosemberg/L2O.jl.svg?branch=main)](https://travis-ci.com/andrewrosemberg/L2O.jl) [![Coverage](https://codecov.io/gh/andrewrosemberg/L2O.jl/branch/main/graph/badge.svg)](https://codecov.io/gh/andrewrosemberg/L2O.jl) + +## Generate Dataset +This package provides a basic way of generating a dataset of the solutions of an optimization problem by varying the values of the parameters in the problem and recording it. + +The user needs to first define a problem iterator: + +```julia +# The problem to iterate over +model = Model(() -> POI.Optimizer(HiGHS.Optimizer())) +@variable(model, x) +p = @variable(model, _p in POI.Parameter(1.0)) # The parameter (defined using POI) +@constraint(model, cons, x + _p >= 3) +@objective(model, Min, 2x) + +# The ids +problem_ids = collect(1:10) + +# The parameter values +parameter_values = Dict(p => collect(1.0:10.0)) + +# The iterator +problem_iterator = ProblemIterator(problem_ids, parameter_values) +``` + +Then chose a type of recorder and what values to record: + +```julia +# CSV recorder to save the optimal primal and dual decision values +recorder = CSVRecorder("test.csv", primal_variables=[:x], dual_variables=[:cons]) + +# Finally solve all problems described by the iterator +solve_batch(model, problem_iterator, recorder) +``` + +Which creates the following CSV: + +| id | x | dual_cons | +|----|------|-----------| +| 1 | 2.0 | 2.0 | +| 2 | 1.0 | 2.0 | +| 3 | -0.0 | 2.0 | +| 4 | -1.0 | 2.0 | +| 5 | -2.0 | 2.0 | +| 6 | -3.0 | 2.0 | +| 7 | -4.0 | 2.0 | +| 8 | -5.0 | 2.0 | +| 9 | -6.0 | 2.0 | +| 10 | -7.0 | 2.0 | diff --git a/docs/src/index.md b/docs/src/index.md index d9dc513..c5fd7d3 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -6,6 +6,8 @@ CurrentModule = L2O Documentation for [L2O](https://github.com/andrewrosemberg/L2O.jl). +Learning to optimize (L2O) package that provides basic functionalities to help fit proxy models for optimization. + ```@index ``` diff --git a/src/L2O.jl b/src/L2O.jl index 0147403..13ae9e7 100644 --- a/src/L2O.jl +++ b/src/L2O.jl @@ -1,5 +1,10 @@ module L2O -# Write your package code here. +using JuMP +import ParametricOptInterface as POI + +export solve_batch, CSVRecorder, ProblemIterator + +include("datasetgen.jl") end diff --git a/src/datasetgen.jl b/src/datasetgen.jl new file mode 100644 index 0000000..c566e95 --- /dev/null +++ b/src/datasetgen.jl @@ -0,0 +1,125 @@ +""" + Recorder + +Abstract type for recorders of optimization problem solutions. +""" +abstract type Recorder end + +""" + CSVRecorder(filename; primal_variables=[], dual_variables=[], filterfn=(model)-> termination_status(model) == MOI.OPTIMAL) + +Recorder type of optimization problem solutions to a CSV file. +""" +mutable struct CSVRecorder <: Recorder + filename::String + primal_variables::AbstractArray{Symbol} + dual_variables::AbstractArray{Symbol} + filterfn::Function + + function CSVRecorder(filename::String; primal_variables=[], dual_variables=[], filterfn=(model)-> termination_status(model) == MOI.OPTIMAL) + return new(filename, primal_variables, dual_variables, filterfn) + end +end + +""" + ProblemIterator(ids::Vector{Integer}, pairs::Dict{VariableRef, Vector{Real}}) + +Iterator for optimization problem instances. +""" +struct ProblemIterator{T<:Real, Z<:Integer} + ids::Vector{Z} + pairs::Dict{VariableRef, Vector{T}} + function ProblemIterator(ids::Vector{Z}, pairs::Dict{VariableRef, Vector{T}}) where {T<:Real, Z<:Integer} + for (p, val) in pairs + @assert length(ids) == length(val) + end + return new{T, Z}(ids, pairs) + end +end + +""" + record(recorder::CSVRecorder, model::JuMP.Model, id::Int64) + +Record optimization problem solution to a CSV file. +""" +function record(recorder::CSVRecorder, model::JuMP.Model, id::Int64) + if !isfile(recorder.filename) + open(recorder.filename, "w") do f + write(f, "id") + for p in recorder.primal_variables + write(f, ",$p") + end + for p in recorder.dual_variables + write(f, ",dual_$p") + end + write(f, "\n") + end + end + open(recorder.filename, "a") do f + write(f, "$id") + for p in recorder.primal_variables + val = MOI.get(model, MOI.VariablePrimal(), model[p]) + write(f, ",$val") + end + for p in recorder.dual_variables + val = MOI.get(model, MOI.ConstraintDual(), model[p]) + write(f, ",$val") + end + write(f, "\n") + end +end + +""" + update_model!(model::JuMP.Model, p::VariableRef, val::Real) + +Update the value of a parameter in a JuMP model. +""" +function update_model!(model::JuMP.Model, p::VariableRef, val::T) where {T<:Real} + MOI.set(model, POI.ParameterValue(), p, val) +end + +""" + update_model!(model::JuMP.Model, p::VariableRef, val::AbstractArray{Real}) + +Update the value of a parameter in a JuMP model. +""" +function update_model!(model::JuMP.Model, p::VariableRef, val::AbstractArray{T}) where {T<:Real} + MOI.set(model, POI.ParameterValue(), p, val) +end + +""" + update_model!(model::JuMP.Model, pairs::Dict, idx::Integer) + +Update the values of parameters in a JuMP model. +""" +function update_model!(model::JuMP.Model, pairs::Dict, idx::Integer) + for (p, val) in pairs + update_model!(model, p, val[idx]) + end +end + +""" + solve_and_record(model::JuMP.Model, problem_iterator::ProblemIterator, recorder::Recorder, idx::Integer) + +Solve an optimization problem and record the solution. +""" +function solve_and_record(model::JuMP.Model, problem_iterator::ProblemIterator, recorder::Recorder, idx::Integer) + update_model!(model, problem_iterator.pairs, idx) + optimize!(model) + if recorder.filterfn(model) + record(recorder, model, problem_iterator.ids[idx]) + end + return nothing +end + +""" + solve_batch(model::JuMP.Model, problem_iterator::ProblemIterator, recorder::Recorder) + +Solve a batch of optimization problems and record the solutions. +""" +function solve_batch(model::JuMP.Model, problem_iterator::ProblemIterator, recorder::Recorder) + for idx in 1:length(problem_iterator.ids) + solve_and_record(model, problem_iterator, recorder, idx) + end + return nothing +end diff --git a/test/runtests.jl b/test/runtests.jl index 2ad48d7..b6071c1 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,6 +1,22 @@ using L2O using Test +using DelimitedFiles +using JuMP, HiGHS +import ParametricOptInterface as POI @testset "L2O.jl" begin - # Write your tests here. + model = Model(() -> POI.Optimizer(HiGHS.Optimizer())) + @variable(model, x) + p = @variable(model, _p in POI.Parameter(1.0)) + @constraint(model, cons, x + _p >= 3) + @objective(model, Min, 2x) + + num_p = 10 + problem_iterator = ProblemIterator(collect(1:num_p), Dict(p => collect(1.0:num_p))) + recorder = CSVRecorder("test.csv", primal_variables=[:x], dual_variables=[:cons]) + solve_batch(model, problem_iterator, recorder) + @test isfile("test.csv") + @test length(readdlm("test.csv", ',')[:, 1]) == num_p+1 + @test length(readdlm("test.csv", ',')[1, :]) == 3 + rm("test.csv") end