diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fff7b9a..7a62204 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: fail-fast: false matrix: version: ['1'] # Test against LTS and current minor release - os: [ubuntu-latest] # macOS-latest, windows-latest + os: [ubuntu-latest, macOS-latest] # macOS-latest, windows-latest arch: [x64] steps: - uses: actions/checkout@v2 diff --git a/Project.toml b/Project.toml index b32bcd3..a920969 100644 --- a/Project.toml +++ b/Project.toml @@ -5,6 +5,7 @@ version = "0.1.0" [deps] Libdl = "8f399da3-3557-5675-b5ff-fb832c97cbdb" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" [compat] julia = "1" diff --git a/README.md b/README.md index e1635f2..6d8b109 100644 --- a/README.md +++ b/README.md @@ -1 +1,5 @@ -# Concorde +# Concorde.jl + + +[![Build Status](https://github.com/chkwon/Concorde.jl/workflows/CI/badge.svg?branch=master)](https://github.com/chkwon/Concorde.jl/actions?query=workflow%3ACI) +[![codecov](https://codecov.io/gh/chkwon/Concorde.jl/branch/master/graph/badge.svg)](https://codecov.io/gh/chkwon/Concorde.jl) diff --git a/deps/build.jl b/deps/build.jl index 4667182..84c565d 100644 --- a/deps/build.jl +++ b/deps/build.jl @@ -14,29 +14,15 @@ const QSOPT_LOCATION = Dict( const CONCORDE_SRC = "http://www.math.uwaterloo.ca/tsp/concorde/downloads/codes/src/co031219.tgz" -function _prefix_suffix(str) - if Sys.islinux() && Sys.ARCH == :x86_64 - return "lib$(str).so" - elseif Sys.isapple() - return "lib$(str).dylib" - end - # elseif Sys.iswindows() - # return "$(str).dll" - # end - error( - "Unsupported operating system. Only 64-bit linux and macOS " * - "are supported." - ) -end - -function build_concorde_linux() +function _build_concorde() # Download qsopt qsopt_dir = joinpath(@__DIR__, "qsopt") if !isdir(qsopt_dir) mkdir(qsopt_dir) end - download(QSOPT_LOCATION["Linux"][1], joinpath(qsopt_dir, "qsopt.a")) - download(QSOPT_LOCATION["Linux"][2], joinpath(qsopt_dir, "qsopt.h")) + sys_type = Sys.isapple() ? "Darwin" : "Linux" + download(QSOPT_LOCATION[sys_type][1], joinpath(qsopt_dir, "qsopt.a")) + download(QSOPT_LOCATION[sys_type][2], joinpath(qsopt_dir, "qsopt.h")) # Download concorde concorde_tarball = download(CONCORDE_SRC) @@ -45,41 +31,52 @@ function build_concorde_linux() # Build concorde_src_dir = joinpath(@__DIR__, "concorde") cd(concorde_src_dir) - run(`bash -c "CFLAGS='-fPIC -O2 -g' ./configure --with-qsopt=$(qsopt_dir)"`) + + macflag = Sys.isapple() ? "--host=darwin" : "" + cflags = "-fPIC -O2 -g" + run(`bash -c "CFLAGS='$(cflags)' ./configure --with-qsopt=$(qsopt_dir) $(macflag)"`) run(`make clean`) run(`make`) lib_dir = joinpath(@__DIR__, "lib") if !isdir(lib_dir) mkdir(lib_dir) end + executable = joinpath(concorde_src_dir, "TSP", "concorde") + + return executable - # Build a shared library - for ext in ["a", "h"] - cp("concorde.$ext", joinpath(lib_dir, "concorde.$ext"), force=true) - end - cd(lib_dir) - run(`ar -x concorde.a`) - object_files = String[] - for f in readdir() - if f != "concorde.a" && f != "concorde.h" - push!(object_files, f) - end - end - of = join(object_files, " ") + # # Build a shared library from the static library + # for ext in ["a", "h"] + # cp("concorde.$ext", joinpath(lib_dir, "concorde.$ext"), force=true) + # end + # cd(lib_dir) + # run(`ar -x concorde.a`) - run(`bash -c "gcc -shared *.o -o libconcorde.so"`) - run(`bash -c "rm -rf *.o"`) - - cd(@__DIR__) - return joinpath(lib_dir, "libconcorde.so") + # shared_lib_filename = "libconcorde.so" + # if Sys.islinux() + # shared_lib_filename = "libconcorde.so" + # run(`bash -c "gcc -shared *.o -o $(shared_lib_filename)"`) + # elseif Sys.isapple() + # shared_lib_filename = "libconcorde.dylib" + # run(`bash -c "gcc -dynamiclib *.o -o $(shared_lib_filename)"`) + # else + # error( + # "Unsupported operating system. Only 64-bit linux and macOS " * + # "are supported." + # ) + # end + + # shared_lib = joinpath(lib_dir, shared_lib_filename) + # run(`bash -c "rm -rf *.o"`) + + # return shared_lib, executable end function build_concorde() - if Sys.islinux() && Sys.ARCH == :x86_64 - return build_concorde_linux() + if Sys.islinux() && Sys.ARCH == :x86_64 + return _build_concorde() elseif Sys.isapple() - return nothing - return build_concorde_linux() + return _build_concorde() end # elseif Sys.iswindows() # return "$(str).dll" @@ -88,24 +85,25 @@ function build_concorde() "Unsupported operating system. Only 64-bit linux and macOS " * "are supported." ) - return nothing + return nothing, nothing end function install_concorde() - concorde_lib_filename = get(ENV, "CONCORDE_JL_LOCATION", nothing) - if !haskey(ENV, "CONCORDE_JL_LOCATION") - concorde_lib_filename = build_concorde() - ENV["CONCORDE_JL_LOCATION"] = concorde_lib_filename + executable = get(ENV, "CONCORDE_EXECUTABLE", nothing) + if !haskey(ENV, "CONCORDE_EXECUTABLE") + executable = build_concorde() + ENV["CONCORDE_EXECUTABLE"] = executable end - if concorde_lib_filename === nothing - error("Environment variable `CONCORDE_JL_LOCATION` not found.") - elseif Libdl.dlopen(concorde_lib_filename) == C_NULL - error("Unable to open the concorde library $(concorde_lib_filename).") + if executable === nothing + error("Environment variable `CONCORDE_EXECUTABLE` not found.") + else + gr17tsp = joinpath(@__DIR__, "../test/gr17.tsp") + run(`$(executable) $(gr17tsp)`) end open(joinpath(@__DIR__, "deps.jl"), "w") do io - write(io, "const LIB_CONCORDE = \"$(escape_string(concorde_lib_filename))\"\n") + write(io, "const CONCORDE_EXECUTABLE = \"$(escape_string(executable))\"\n") end end diff --git a/src/Concorde.jl b/src/Concorde.jl index 19261e6..f2aca4b 100644 --- a/src/Concorde.jl +++ b/src/Concorde.jl @@ -1,6 +1,83 @@ module Concorde +using Random + include("../deps/deps.jl") # Write your package code here. +function read_solution(filepath) + sol = readlines(filepath) + n_nodes = sol[1] + tour = parse.(Int, split(join(sol[2:end]))) .+ 1 + return tour +end + +function tour_length(tour, M) + n_nodes = length(tour) + len = 0 + for i in 1:n_nodes + j = i + 1 + if i == n_nodes + j = 1 + end + + len += M[tour[i], tour[j]] + end + return len +end + +function solve_tsp(dist_mtx::Matrix{Int}) + n_nodes = size(dist_mtx, 1) + name = randstring(10) + filepath = joinpath(pwd(), name * ".tsp") + lower_diag_row = Int[] + for i in 1:n_nodes + for j in 1:i + push!(lower_diag_row, dist_mtx[i, j]) + end + end + buf = 10 + n_rows = length(lower_diag_row) / buf |> ceil |> Int + rows = String[] + for i in 1:n_rows + s = buf * (i-1) + 1 + t = min(buf * i, length(lower_diag_row)) + push!(rows, join(lower_diag_row[s:t], " ")) + end + + open(filepath, "w") do io + write(io, "NAME: $(name)\n") + write(io, "TYPE: TSP\n") + write(io, "COMMENT: $(name)\n") + write(io, "DIMENSION: $(n_nodes)\n") + write(io, "EDGE_WEIGHT_TYPE: EXPLICIT\n") + write(io, "EDGE_WEIGHT_FORMAT: LOWER_DIAG_ROW \n") + write(io, "EDGE_WEIGHT_SECTION\n") + for r in rows + write(io, "$r\n") + end + write(io, "EOF\n") + end + + run(`$(Concorde.CONCORDE_EXECUTABLE) $(filepath)`) + + sol_filepath = joinpath(pwd(), name * ".sol") + opt_tour = read_solution(sol_filepath) + opt_len = tour_length(opt_tour, dist_mtx) + + exts = ["mas", "pul", "sav", "sol", "tsp"] + for ext in exts + file = joinpath(pwd(), "$(name).$(ext)") + rm(file, force=true) + file = joinpath(pwd(), "O$(name).$(ext)") + rm(file, force=true) + end + return opt_tour, opt_len +end + + + + +export solve_tsp + end diff --git a/test/gr17.tsp b/test/gr17.tsp new file mode 100644 index 0000000..c0c8496 --- /dev/null +++ b/test/gr17.tsp @@ -0,0 +1,21 @@ +NAME: gr17 +TYPE: TSP +COMMENT: 17-city problem (Groetschel) +DIMENSION: 17 +EDGE_WEIGHT_TYPE: EXPLICIT +EDGE_WEIGHT_FORMAT: LOWER_DIAG_ROW +EDGE_WEIGHT_SECTION + 0 633 0 257 390 0 91 661 228 0 412 227 + 169 383 0 150 488 112 120 267 0 80 572 196 + 77 351 63 0 134 530 154 105 309 34 29 0 + 259 555 372 175 338 264 232 249 0 505 289 262 + 476 196 360 444 402 495 0 353 282 110 324 61 + 208 292 250 352 154 0 324 638 437 240 421 329 + 297 314 95 578 435 0 70 567 191 27 346 83 + 47 68 189 439 287 254 0 211 466 74 182 243 + 105 150 108 326 336 184 391 145 0 268 420 53 + 239 199 123 207 165 383 240 140 448 202 57 0 + 246 745 472 237 528 364 332 349 202 685 542 157 + 289 426 483 0 121 518 142 84 297 35 29 36 + 236 390 238 301 55 96 153 336 0 +EOF diff --git a/test/runtests.jl b/test/runtests.jl index 596ce95..800aad3 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,9 +1,24 @@ using Concorde using Test +# @testset "Concorde.jl" begin +# A = Cint.([4, 2, 5, 7, 2, 1, 3, 5]) +# ccall((:CCutil_int_array_quicksort, Concorde.LIB_CONCORDE), Cvoid, (Ref{Cint}, Cint), A, length(A)) +# @test A[1] == minimum(A) +# @test A[end] == maximum(A) +# end + + @testset "Concorde.jl" begin - A = Cint.([4, 2, 5, 7, 2, 1, 3, 5]) - ccall((:CCutil_int_array_quicksort, Concorde.LIB_CONCORDE), Cvoid, (Ref{Cint}, Cint), A, length(A)) - @test A[1] == minimum(A) - @test A[end] == maximum(A) + M = [ + 0 16 7 14 + 16 0 3 5 + 7 3 0 16 + 14 5 16 0 + ] + opt_tour, opt_len = solve_tsp(M) + @show opt_tour, opt_len + # display(M) + + @test opt_len == 29 end