Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
version: 2
updates:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "julia" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"
36 changes: 36 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: CI
on:
push:
branches: [main]
pull_request:
types: [opened, synchronize, reopened]
jobs:
test:
name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- version: '1.10'
os: ubuntu-latest
arch: x64
- version: '1'
os: ubuntu-latest
arch: x64
steps:
- uses: actions/checkout@v6
- uses: julia-actions/setup-julia@v3
with:
version: ${{ matrix.version }}
arch: ${{ matrix.arch }}
- uses: julia-actions/cache@v3
- uses: julia-actions/julia-buildpkg@v1
- uses: julia-actions/julia-runtest@v1
with:
depwarn: error
- uses: julia-actions/julia-processcoverage@v1
- uses: codecov/codecov-action@v6
with:
files: lcov.info
token: ${{ secrets.CODECOV_TOKEN }}
162 changes: 162 additions & 0 deletions deps/build.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
# Copyright (c) 2019: Benoît Legat and contributors
#
# Use of this source code is governed by an MIT-style license that can be found
# in the LICENSE.md file or at https://opensource.org/licenses/MIT.

# Automatic installation of the PENSDP library from
# https://github.com/kocvara/pensdp
#
# PENSDP is the free SDP-only variant of the PENOPT family of solvers. The
# BMI-capable PENBMI is a commercial product; if the user has a working
# `libpenbmi` shared library, its path can be provided via the
# `PENOPT_LIBPENBMI` environment variable and it will be used in addition
# to the auto-installed PENSDP library.

using Libdl

const PENSDP_VERSION = "2.2"
const PENSDP_REPO = "https://github.com/kocvara/pensdp"
const DEPS_DIR = @__DIR__
const USR_DIR = joinpath(DEPS_DIR, "usr")
const LIB_DIR = joinpath(USR_DIR, "lib")
const DOWNLOAD_DIR = joinpath(DEPS_DIR, "downloads")

const ARCHIVES = Dict(
(:linux, :x86_64) => ("pensdp22_LNX64.tar.gz", :tar),
(:apple, :x86_64) => ("pensdp22_macos_intel64.zip", :zip),
(:windows, :x86_64) => ("pensdp22_Win64.zip", :zip),
)

function _platform()
if Sys.islinux()
return (:linux, Sys.ARCH)
elseif Sys.isapple()
return (:apple, Sys.ARCH)
elseif Sys.iswindows()
return (:windows, Sys.ARCH)
else
error("Unsupported platform: $(Sys.KERNEL)")
end
end

function _archive_url()
key = _platform()
if !haskey(ARCHIVES, key)
error(
"No pre-built PENSDP archive is published for $(key). " *
"See $(PENSDP_REPO).",
)
end
name, kind = ARCHIVES[key]
return "$(PENSDP_REPO)/raw/main/bin/$(name)", name, kind
end

function _download_and_extract()
mkpath(DOWNLOAD_DIR)
url, name, kind = _archive_url()
archive = joinpath(DOWNLOAD_DIR, name)
if !isfile(archive)
@info "Downloading $(url)"
download(url, archive)
end
extract_dir = joinpath(DOWNLOAD_DIR, "Pensdp$(PENSDP_VERSION)")
rm(extract_dir; force = true, recursive = true)
@info "Extracting $(archive)"
if kind == :tar
run(`tar -xzf $(archive) -C $(DOWNLOAD_DIR)`)
else
run(`unzip -q -o $(archive) -d $(DOWNLOAD_DIR)`)
end
return extract_dir
end

# Build a shared library that exports the `pensdp` symbol so that it can be
# loaded by Julia via `Libdl.dlopen` and called with `ccall`.
function _build_shared_library(extract_dir)
mkpath(LIB_DIR)
lib_src = joinpath(extract_dir, "lib")
libpensdp_a = joinpath(lib_src, "libpensdp.a")
isfile(libpensdp_a) || error("$(libpensdp_a) not found")
julia_libdir = joinpath(Sys.BINDIR, Base.LIBDIR, "julia")
os, _ = _platform()
if os == :linux
output = joinpath(LIB_DIR, "libpensdp.so")
libgoto = joinpath(lib_src, "libgoto2.a")
isfile(libgoto) || error("$(libgoto) not found")
cmd = `gcc -shared -fPIC -o $(output)
-L$(julia_libdir) -Wl,-rpath,$(julia_libdir)
-Wl,--whole-archive $(libpensdp_a) -Wl,--no-whole-archive
$(libgoto)
-lgfortran -lpthread -ldl -lm`
@info "Linking shared library" cmd
run(cmd)
return output
elseif os == :apple
output = joinpath(LIB_DIR, "libpensdp.dylib")
libgfortran_mac = joinpath(lib_src, "libgfortran_mac.a")
cmd = `gcc -dynamiclib -o $(output)
-Wl,-force_load,$(libpensdp_a)
$(libgfortran_mac)
-framework Accelerate
-lpthread`
@info "Linking shared library" cmd
run(cmd)
return output
else
error(
"Automatic build is currently implemented for Linux and macOS. " *
"On Windows please set `PENOPT_LIBPENBMI` (or build " *
"`libpensdp.dll` manually) and re-run `Pkg.build(\"Penopt\")`.",
)
end
end

function _write_deps(libpensdp_path)
libpenbmi_path = get(ENV, "PENOPT_LIBPENBMI", "")
open(joinpath(DEPS_DIR, "deps.jl"), "w") do io
println(io, "# Auto-generated by deps/build.jl - do not edit.")
println(io, "import Libdl")
println(io)
println(io, "const libpensdp = ", repr(libpensdp_path))
# When `libpenbmi` is not available, the constant is left as an empty
# string. `has_penbmi()` guards every ccall in `Penopt.jl`, so the
# `ccall((:penbmi, libpenbmi), ...)` site is never reached.
println(io, "const libpenbmi = ", repr(libpenbmi_path))
println(io)
println(io, """
function check_deps()
if !isfile(libpensdp)
error(
\"\$(libpensdp) does not exist, please re-run \" *
\"Pkg.build(\\\"Penopt\\\") and restart Julia.\",
)
end
if Libdl.dlopen_e(libpensdp) == C_NULL
error(
\"\$(libpensdp) cannot be opened, please re-run \" *
\"Pkg.build(\\\"Penopt\\\") and restart Julia.\",
)
end
if !isempty(libpenbmi)
if !isfile(libpenbmi)
error(\"\$(libpenbmi) does not exist.\")
end
if Libdl.dlopen_e(libpenbmi) == C_NULL
error(\"\$(libpenbmi) cannot be opened.\")
end
end
return
end""")
end
return
end

function main()
extract_dir = _download_and_extract()
libpensdp_path = _build_shared_library(extract_dir)
_write_deps(libpensdp_path)
@info "Penopt: PENSDP installed at $(libpensdp_path)"
return
end

main()
97 changes: 64 additions & 33 deletions src/MOI_wrapper.jl
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ mutable struct Optimizer <: MOI.AbstractOptimizer
end
end

MOI.get(::Optimizer, ::MOI.SolverName) = "Penbmi"
MOI.get(::Optimizer, ::MOI.SolverName) = has_penbmi() ? "Penbmi" : "Pensdp"

function MOI.supports(optimizer::Optimizer, param::MOI.RawOptimizerAttribute)
return param.name in IOPTIONS || param.name in FOPTIONS
Expand Down Expand Up @@ -482,6 +482,9 @@ function MOI.add_constraint(
return MOI.ConstraintIndex{typeof(func),typeof(set)}(ci.value)
end

_is_sdp(optimizer::Optimizer) =
isempty(optimizer.q_val) && isempty(optimizer.ki_val)

function MOI.optimize!(optimizer::Optimizer)
ioptions = optimizer.ioptions
if optimizer.silent
Expand All @@ -490,38 +493,66 @@ function MOI.optimize!(optimizer::Optimizer)
ioptions[11] = 0 # DIMACS : no
end
optimizer.x = copy(optimizer.x0)
return optimizer.fx,
_,
optimizer.uoutput,
optimizer.iresults,
optimizer.fresults,
optimizer.info = penbmi(
optimizer.msizes,
optimizer.x,
optimizer.fobj,
optimizer.q_col,
optimizer.q_row,
optimizer.q_val,
optimizer.ci,
optimizer.bi_dim,
optimizer.bi_idx,
optimizer.bi_val,
optimizer.ai_dim,
optimizer.ai_idx,
optimizer.ai_nzs,
optimizer.ai_val,
optimizer.ai_col,
optimizer.ai_row,
optimizer.ki_dim,
optimizer.ki_idx,
optimizer.kj_idx,
optimizer.ki_nzs,
optimizer.ki_val,
optimizer.ki_col,
optimizer.ki_row,
ioptions,
optimizer.foptions,
)
# PENSDP (auto-installed) handles SDP. Fall back to PENBMI when the
# objective has a quadratic part or when there are BMI (`K`) terms.
if _is_sdp(optimizer) && !has_penbmi()
optimizer.fx,
_,
optimizer.uoutput,
optimizer.iresults,
optimizer.fresults,
optimizer.info = pensdp(
optimizer.msizes,
optimizer.x,
optimizer.fobj,
optimizer.ci,
optimizer.bi_dim,
optimizer.bi_idx,
optimizer.bi_val,
optimizer.ai_dim,
optimizer.ai_idx,
optimizer.ai_nzs,
optimizer.ai_val,
optimizer.ai_col,
optimizer.ai_row,
ioptions,
optimizer.foptions,
)
else
optimizer.fx,
_,
optimizer.uoutput,
optimizer.iresults,
optimizer.fresults,
optimizer.info = penbmi(
optimizer.msizes,
optimizer.x,
optimizer.fobj,
optimizer.q_col,
optimizer.q_row,
optimizer.q_val,
optimizer.ci,
optimizer.bi_dim,
optimizer.bi_idx,
optimizer.bi_val,
optimizer.ai_dim,
optimizer.ai_idx,
optimizer.ai_nzs,
optimizer.ai_val,
optimizer.ai_col,
optimizer.ai_row,
optimizer.ki_dim,
optimizer.ki_idx,
optimizer.kj_idx,
optimizer.ki_nzs,
optimizer.ki_val,
optimizer.ki_col,
optimizer.ki_row,
ioptions,
optimizer.foptions,
)
end
return
end

function MOI.get(optimizer::Optimizer, ::MOI.SolveTimeSec)
Expand Down
Loading