diff --git a/Makefile b/Makefile index 78d2538..408bcc4 100644 --- a/Makefile +++ b/Makefile @@ -6,14 +6,14 @@ setup: @julia --project=scripts/build -e 'import Pkg; Pkg.develop(path=@__DIR__)' build: - @julia --project=scripts/build ./scripts/build/build.jl + @julia --project=scripts/build scripts/build/script.jl + +test-build: + @julia --project=scripts/build --load scripts/build/runtests.jl --eval 'test_main()' clear: @rm -rf ./dist -run: setup - @julia --project=scripts/run/mqlib ./scripts/run/mqlib/run.jl - setup-docs: @julia --project=docs -e 'import Pkg; Pkg.develop(path=@__DIR__); Pkg.instantiate()' diff --git a/Project.toml b/Project.toml index 89c100c..102eb14 100644 --- a/Project.toml +++ b/Project.toml @@ -28,4 +28,4 @@ QUBOTools = "0.10" SQLite = "1" Pkg = "1" TOML = "1" -julia = "1.9" +julia = "1.10" diff --git a/docs/src/booklet/1-design.md b/docs/src/booklet/1-design.md index d7ed966..6ecb30e 100644 --- a/docs/src/booklet/1-design.md +++ b/docs/src/booklet/1-design.md @@ -8,3 +8,7 @@ The HDF5 file format is used to store the data. The data is stored in a hierarch - `instances`: Contains the instances of the data. - `solutions`: Contains the solutions to the instances. + +## Collections + +One of the core ideas behind [`QUBOLib`](https://github.com/JuliaQUBO/QUBOLib.jl) is to deploy other QUBO instance libraries in a bundle. diff --git a/docs/src/booklet/2-build.md b/docs/src/booklet/2-build.md new file mode 100644 index 0000000..3663c9a --- /dev/null +++ b/docs/src/booklet/2-build.md @@ -0,0 +1,22 @@ +# Build Process + +Building QUBOLib from scratch + +## Distribution Folder structure + +``` +dist +├── build +│ ├── Artifact.toml +│ ├── git-tree.hash +│ ├── tar-ball.hash +│ ├── last.tag +│ └── next.tag +└── cache + └── collection... + └── data +qubolib +├── archive.h5 +└── index.db +``` + diff --git a/scripts/build/build.jl b/scripts/build/build.jl index b79a878..378ea7c 100644 --- a/scripts/build/build.jl +++ b/scripts/build/build.jl @@ -1,40 +1,38 @@ using JuliaFormatter using LaTeXStrings +import Downloads import QUBOLib import QUBOTools -import Downloads +include("library/clear.jl") include("library/deploy.jl") -include("sources/arXiv_1903_10928_3r3x.jl") -include("sources/arXiv_1903_10928_5r5x.jl") +include("sources/hen.jl") include("sources/qplib.jl") function build_standard_qubolib( - path::AbstractString = root_path(); - clear_build::Bool = false, + path::AbstractString; clear_cache::Bool = false, + clear_build::Bool = false, ) - @info "Building QUBOLib v$(QUBOLib.__version__())" - - if clear_build - QUBOLib.clear_build(path) - end + @info "Building QUBOLib v$(QUBOLib.__version__()) @ $(path)" if clear_cache - QUBOLib.clear_cache(path) + @info "[Clearing Cache]" + + clear_cache!(path) end - close(QUBOLib.access(; path, create = true)) + if clear_build + @info "[Clearing Build]" - QUBOLib.access(; path) do index - build_arXiv_1903_10928_3r3x!(index) + QUBOLib.access(; path, clear = true) |> close end - # QUBOLib.access(; path) do index - # build_arXiv_1903_10928_5r5x!(index) - # end + QUBOLib.access(; path) do index + build_hen!(index) + end QUBOLib.access(; path) do index build_qplib!(index) @@ -43,15 +41,14 @@ function build_standard_qubolib( return nothing end +function main(args::Vector{String} = ARGS) + QUBOLib.print_logo(stdout) -function main() build_standard_qubolib( QUBOLib.root_path(); - clear_build = ("--clear-build" ∈ ARGS), - clear_cache = ("--clear-cache" ∈ ARGS), + clear_cache = ("--clear-cache" ∈ args), + clear_build = ("--clear-build" ∈ args), ) return nothing end - -main() # Here we go! diff --git a/scripts/build/library/clear.jl b/scripts/build/library/clear.jl index 1a002c3..2e0b2dc 100644 --- a/scripts/build/library/clear.jl +++ b/scripts/build/library/clear.jl @@ -1,11 +1,5 @@ -function clear_build(path::AbstractString = QUBOLib.root_path()) - rm(build_path(path; ifmissing = identity); force = true, recursive = true) - - return nothing -end - -function clear_cache(path::AbstractString = QUBOLib.root_path()) - rm(cache_path(path; ifmissing = identity); force = true, recursive = true) +function clear_cache!(path::AbstractString = QUBOLib.root_path()) + rm(QUBOLib.cache_path(path); force = true, recursive = true) return nothing end diff --git a/scripts/build/library/deploy.jl b/scripts/build/library/deploy.jl index 69b0c6f..34e9b8a 100644 --- a/scripts/build/library/deploy.jl +++ b/scripts/build/library/deploy.jl @@ -1,77 +1,94 @@ -function deploy(path::AbstractString) +function deploy!(path::AbstractString) @assert Sys.islinux() # Calculate tree hash - tree_hash = bytes2hex(Pkg.GitTools.tree_hash(build_path(path))) + git_tree_hash = bytes2hex(Pkg.GitTools.tree_hash(QUBOLib.library_path(path))) # Build tarball - temp_path = abspath(Tar.create(build_path(path))) + temp_path = abspath(Tar.create(QUBOLib.library_path(path))) # Compress tarball run(`gzip -9 $temp_path`) # Move tarball - file_path = mkpath(abspath(dist_path(path), "qubolib.tar.gz")) + tar_ball_path = mkpath(abspath(QUBOLib.dist_path(path), "qubolib.tar.gz")) - rm(file_path; force = true) + rm(tar_ball_path; force = true) - cp("$temp_path.gz", file_path; force = true) + cp("$temp_path.gz", tar_ball_path; force = true) # Remove temporary files rm(temp_path; force = true) rm("$temp_path.gz"; force = true) # Write hash to file - write(joinpath(dist_path(path), "tree.hash"), tree_hash) + write(joinpath(QUBOLib.build_path(path), "git-tree.hash"), git_tree_hash) # Also, compute sha256 sum - ball_hash = read(pipeline(`sha256sum -z $file_path`, `cut -d ' ' -f 1`), String) + tar_ball_hash = read(pipeline(`sha256sum -z $tar_ball_path`, `cut -d ' ' -f 1`), String) - write(joinpath(dist_path(path), "ball.hash"), ball_hash) - - return nothing -end + write(joinpath(QUBOLib.build_path(path), "tar-ball.hash"), tar_ball_hash) -function next_tag(path::AbstractString) - last_tag = if haskey(ENV, "LAST_QUBOLIB_TAG") - x = tryparse(VersionNumber, ENV["LAST_QUBOLIB_TAG"]) + # Retrieve last QUBOLib tag + # last_tag = read(joinpath(QUBOLib.build_path(path), "last.tag"), String) + # next_tag = next_data_tag(last_tag) - if isnothing(x) - @warn("Pushing tag forward") - - v"0.1.0" - else - x - end - else - last_tag_path = abspath(path, "last.tag") + # write(joinpath(QUBOLib.build_path(path), "next.tag"), next_tag) - if isfile(last_tag_path) - text = read(last_tag_path, String) + # # Write Artifacts.toml entry + # artifact_entry = """ + # [qubolib] + # git-tree-sha1 = "$(git_tree_hash)" + # lazy = true - m = match(r"tag:\s*v(.*)", text) + # [[qubolib.download]] + # url = "https://github.com/JuliaQUBO/QUBOLib.jl/releases/download/$(qubolib_tag)/qubolib.tar.gz" + # sha256 = "$(tar_ball_hash)" + # """ - if isnothing(m) - @error("Tag not found in '$last_tag_path'") + # write(joinpath(QUBOLib.build_path(path), "Artifacts.toml"), artifact_entry) + + return nothing +end - exit(1) - end +function next_data_tag(last_tag::AbstractString)::String + return next_data_tag(parse(VersionNumber, last_tag)) +end - parse(VersionNumber, m[1]) +function next_data_tag(last_tag::VersionNumber)::String + next_tag = if isempty(last_tag.prerelease) + @assert isempty(last_tag.build) + + VersionNumber( + last_tag.major, + last_tag.minor, + last_tag.patch, + ("data",), + (1,), + ) + else # !isempty(last_tag.prerelease) + @assert only(last_tag.prerelease) == "data" + + if isempty(last_tag.build) + VersionNumber( + last_tag.major, + last_tag.minor, + last_tag.patch, + last_tag.prerelease, + (1,), + ) else - @error("File '$last_tag_path' not found") - - exit(1) + @assert only(last_tag.build) isa Integer "last_tag.build = $(last_tag.build)" + + VersionNumber( + last_tag.major, + last_tag.minor, + last_tag.patch, + last_tag.prerelease, + (only(last_tag.build) + 1,) + ) end end - next_tag = VersionNumber( - last_tag.major, - last_tag.minor, - last_tag.patch + 1, - last_tag.prerelease, - last_tag.build, - ) - - return "v$next_tag" + return "v$(next_tag)" end diff --git a/scripts/build/runtests.jl b/scripts/build/runtests.jl new file mode 100644 index 0000000..dac57aa --- /dev/null +++ b/scripts/build/runtests.jl @@ -0,0 +1,20 @@ +using Test + +include("build.jl") + +function test_tags() + @testset "▶ Tags" begin + @test next_data_tag("v0.1.0") == "v0.1.0-data+1" + @test next_data_tag("v1.2.3-data+2") == "v1.2.3-data+3" + end + + return nothing +end + +function test_main() + @testset "♦ QUBOLib.jl/scripts/build test suite ♦" verbose = true begin + test_tags() + end + + return nothing +end diff --git a/scripts/build/script.jl b/scripts/build/script.jl new file mode 100644 index 0000000..cf91164 --- /dev/null +++ b/scripts/build/script.jl @@ -0,0 +1,3 @@ +include("build.jl") + +main() # Here we go! diff --git a/scripts/build/sources/arXiv_1903_10928_3r3x.jl b/scripts/build/sources/arXiv_1903_10928_3r3x.jl deleted file mode 100644 index 0a349a0..0000000 --- a/scripts/build/sources/arXiv_1903_10928_3r3x.jl +++ /dev/null @@ -1,77 +0,0 @@ -const ARXIV_1903_10928_3R3X_URL = "https://sites.usc.edu/itayhen/files/2019/09/3r3x.zip" - -function load_arXiv_1903_10928_3r3x!(index::QUBOLib.LibraryIndex) - @info "[arXiv_1903_10928_3r3x] Downloading instances" - - _cache_path = mkpath(abspath(QUBOLib.cache_path(index; create = true), "arXiv-1903-10928-3r3x")) - _data_path = mkpath(abspath(_cache_path, "data")) - _zip_path = abspath(_cache_path, "arXiv_1903_10928_3r3x.zip") - - # Download arXiv_1903_10928 3r3x archive - if isfile(_zip_path) - @info "[arXiv_1903_10928_3r3x] Archive already downloaded" - else - @info "[arXiv_1903_10928_3r3x] Downloading archive" - - Downloads.download(ARXIV_1903_10928_5R5X_URL, _zip_path) - end - - # Extract arXiv_1903_10928 3r3x archive - @assert run(`which unzip`, devnull, devnull).exitcode == 0 "'unzip' is required to extract QPLIB archive" - - @info "[arXiv_1903_10928_3r3x] Extracting archive" - - run(``` - unzip -qq -o -j - $_zip_path - 'instance*.txt' - -d $_data_path - ```) - - return nothing -end - -function build_arXiv_1903_10928_3r3x!(index::QUBOLib.LibraryIndex; cache::Bool = true) - if QUBOLib.has_collection(index, "arXiv-1903-10928-3r3x") - @info "[arXiv_1903_10928_3r3x] Collection already exists" - - if cache - return nothing - else - QUBOLib.remove_collection!(index, "arXiv-1903-10928-3r3x") - end - end - - QUBOLib.add_collection!( - index, - "arXiv-1903-10928-3r3x", - Dict{String,Any}( - "name" => "3-Regular 3-XORSAT (arXiv:1903.10928)", - "author" => ["Itay Hen"], - "description" => "3R3X instances for \"Equation Planting: A Tool for Benchmarking Ising Machines\"", - "year" => 2019, - "url" => ARXIV_1903_10928_3R3X_URL, - ), - ) - - load_arXiv_1903_10928_3r3x!(index) - - _data_path = abspath( - QUBOLib.cache_path(index), - "arXiv-1903-10928-3r3x", - "data", - ) - - @info "[arXiv_1903_10928_3r3x] Building index" - - for path in readdir(_data_path; join = true) - model = QUBOTools.read_model(path, QUBOTools.Qubist()) - mod_i = QUBOLib.add_instance!(index, model, "arXiv-1903-10928-3r3x") - - if isnothing(mod_i) - @warn "[arXiv_1903_10928_3r3x] Failed to read instance '$path'" - end - end - - return nothing -end diff --git a/scripts/build/sources/arXiv_1903_10928_5r5x.jl b/scripts/build/sources/arXiv_1903_10928_5r5x.jl deleted file mode 100644 index b1179ec..0000000 --- a/scripts/build/sources/arXiv_1903_10928_5r5x.jl +++ /dev/null @@ -1,77 +0,0 @@ -const ARXIV_1903_10928_5R5X_URL = "https://sites.usc.edu/itayhen/files/2019/09/5r5x.zip" - -function load_arXiv_1903_10928_5r5x!(index::QUBOLib.LibraryIndex) - @info "[arXiv_1903_10928_5r5x] Downloading instances" - - _cache_path = mkpath(abspath(QUBOLib.cache_path(index; create = true), "arXiv-1903-10928-5r5x")) - _data_path = mkpath(abspath(_cache_path, "data")) - _zip_path = abspath(_cache_path, "arXiv_1903_10928_5r5x.zip") - - # Download arXiv_1903_10928 5r5x archive - if isfile(_zip_path) - @info "[arXiv_1903_10928_5r5x] Archive already downloaded" - else - @info "[arXiv_1903_10928_5r5x] Downloading archive" - - Downloads.download(ARXIV_1903_10928_5R5X_URL, _zip_path) - end - - # Extract arXiv_1903_10928 5r5x archive - @assert run(`which unzip`, devnull, devnull).exitcode == 0 "'unzip' is required to extract QPLIB archive" - - @info "[arXiv_1903_10928_5r5x] Extracting archive" - - run(``` - unzip -qq -o -j - $_zip_path - 'instance*.txt' - -d $_data_path - ```) - - return nothing -end - -function build_arXiv_1903_10928_5r5x!(index::QUBOLib.LibraryIndex; cache::Bool = true) - if QUBOLib.has_collection(index, "arXiv-1903-10928-5r5x") - @info "[arXiv_1903_10928_5r5x] Collection already exists" - - if cache - return nothing - else - QUBOLib.remove_collection!(index, "arXiv-1903-10928-5r5x") - end - end - - QUBOLib.add_collection!( - index, - "arXiv-1903-10928-5r5x", - Dict{String,Any}( - "name" => "5-Regular 5-XORSAT (arXiv:1903.10928)", - "author" => ["Itay Hen"], - "description" => "5R5X instances for \"Equation Planting: A Tool for Benchmarking Ising Machines\"", - "year" => 2019, - "url" => ARXIV_1903_10928_5R5X_URL, - ), - ) - - load_arXiv_1903_10928_5r5x!(index) - - _data_path = abspath( - QUBOLib.cache_path(index), - "arXiv-1903-10928-5r5x", - "data", - ) - - @info "[arXiv_1903_10928_5r5x] Building index" - - for path in readdir(_data_path; join = true) - model = QUBOTools.read_model(path, QUBOTools.Qubist()) - mod_i = QUBOLib.add_instance!(index, model, "arXiv-1903-10928-5r5x") - - if isnothing(mod_i) - @warn "[arXiv_1903_10928_5r5x] Failed to read instance '$path'" - end - end - - return nothing -end diff --git a/scripts/build/sources/arXiv_2103_08464_3r3x.jl b/scripts/build/sources/arXiv_2103_08464_3r3x.jl deleted file mode 100644 index 6019047..0000000 --- a/scripts/build/sources/arXiv_2103_08464_3r3x.jl +++ /dev/null @@ -1,5 +0,0 @@ -function build_arXiv_2103_08464_3r3x!(index::LibraryIndex) - @info "[arXiv_2103_08464_3r3x] Building index" - - return nothing -end diff --git a/scripts/build/sources/hen.jl b/scripts/build/sources/hen.jl new file mode 100644 index 0000000..847b4c3 --- /dev/null +++ b/scripts/build/sources/hen.jl @@ -0,0 +1,110 @@ +const HEN_DATA = Dict( + "arXiv-1903-10928-3r3x" => Dict( + :url => "https://sites.usc.edu/itayhen/files/2019/09/3r3x.zip", + :data => Dict{String,Any}( + "name" => "3-Regular 3-XORSAT (arXiv:1903.10928)", + "author" => ["Itay Hen"], + "description" => "3R3X instances for 'Equation Planting: A Tool for Benchmarking Ising Machines'", + "year" => 2019, + "url" => "https://arxiv.org/abs/1903.10928", + ), + ), + "arXiv-1903-10928-5r5x" => Dict( + :url => "https://sites.usc.edu/itayhen/files/2019/09/5r5x.zip", + :data => Dict{String,Any}( + "name" => "5-Regular 5-XORSAT (arXiv:1903.10928)", + "author" => ["Itay Hen"], + "description" => "5R5X instances for 'Equation Planting: A Tool for Benchmarking Ising Machines'", + "year" => 2019, + "url" => "https://arxiv.org/abs/1903.10928", + ), + ), + "arXiv-2103-08464-3r3x" => Dict( + :url => "https://unmm-my.sharepoint.com/personal/talbash_unm_edu/_layouts/15/download.aspx?SourceUrl=%2Fpersonal%2Ftalbash%5Funm%5Fedu%2FDocuments%2FWebsiteData%2FarXiv210308464%2F3r3x%5F2body%2Ezip", + :data => Dict{String,Any}( + "name" => "3-Regular 3-XORSAT (arXiv:2103.08464)", + "author" => ["Matthew Kowalsky", "Tameem Albash", "Itay Hen", "Daniel A. Lidar"], + "description" => "3R3X instances for '3-Regular 3-XORSAT Planted Solutions Benchmark of Classical and Quantum Heuristic Optimizers'", + "year" => 2021, + "url" => "https://arxiv.org/abs/2103.08464", + ), + ), +) + +function load_hen!(index::QUBOLib.LibraryIndex, code::AbstractString) + @info "[$code] Downloading instances" + + data_path = mkpath(QUBOLib.cache_data_path(index, code)) + file_path = QUBOLib.cache_path(index, code, "$code.zip") + + # Download arXiv_2103_08464 3r3x archive + if isfile(file_path) + @info "[$code] Archive already downloaded" + else + @info "[$code] Downloading archive" + + Downloads.download(HEN_DATA[code][:url], file_path) + end + + # Extract arXiv_2103_08464 3r3x archive + @assert success(`which unzip`) "'unzip' is required to extract QPLIB archive" + + @info "[$code] Extracting archive" + + run(`unzip -qq -o -j $file_path 'instance*.txt' -d $data_path`) + + return nothing +end + +function build_hen!(index::QUBOLib.LibraryIndex; cache::Bool = true) + for code in keys(HEN_DATA) + build_hen!(index, code; cache) + end + + return nothing +end + +function build_hen!(index::QUBOLib.LibraryIndex, code::AbstractString; cache::Bool = true) + @info "[$code] Building index" + + if QUBOLib.has_collection(index, code) + @info "[$code] Collection already exists" + + if cache + return nothing + else + QUBOLib.remove_collection!(index, code) + end + end + + load_hen!(index, code) + + QUBOLib.add_collection!(index, code, HEN_DATA[code][:data]) + + data_path = QUBOLib.cache_data_path(index, code) + + for path in readdir(data_path; join = true) + model = try + QUBOTools.read_model(path, QUBOTools.Qubist()) + catch e + if e isa QUBOTools.SyntaxError + @warn """ + [$code] Failed to read instance '$path': + $(sprint(showerror, e)) + """ + + continue + else + rethrow(e) + end + end + + mod_i = QUBOLib.add_instance!(index, model, code; name = basename(path)) + + if isnothing(mod_i) + @warn "[$code] Failed to read instance '$path'" + end + end + + return nothing +end diff --git a/scripts/build/sources/qplib.jl b/scripts/build/sources/qplib.jl index 65ba73c..6891460 100644 --- a/scripts/build/sources/qplib.jl +++ b/scripts/build/sources/qplib.jl @@ -278,15 +278,15 @@ function build_qplib!(index::QUBOLib.LibraryIndex; cache::Bool = true) @info "[qplib] Building index" - _data_path = abspath(QUBOLib.cache_path(index; create = true), "qplib", "data") + data_path = QUBOLib.cache_data_path(index, "qplib") for code in code_list - mod_path = joinpath(_data_path, "$(code).qplib") - var_path = joinpath(_data_path, "$(code).lp") - sol_path = joinpath(_data_path, "$(code).sol") + mod_path = joinpath(data_path, "$(code).qplib") + var_path = joinpath(data_path, "$(code).lp") + sol_path = joinpath(data_path, "$(code).sol") model = QUBOTools.read_model(mod_path, Format(:qplib)) - mod_i = QUBOLib.add_instance!(index, model, "qplib") + mod_i = QUBOLib.add_instance!(index, model, "qplib"; name = basename(mod_path)) if isfile(sol_path) var_map = _get_qplib_var_map(var_path) @@ -309,17 +309,16 @@ end function load_qplib!(index::QUBOLib.LibraryIndex) @assert Sys.isunix() "Processing QPLIB is only possible on Unix systems" - _cache_path = mkpath(abspath(QUBOLib.cache_path(index; create = true), "qplib")) - _data_path = mkpath(abspath(_cache_path, "data")) - _zip_path = abspath(_cache_path, "qplib.zip") + data_path = mkpath(QUBOLib.cache_data_path(index, "qplib")) + file_path = abspath(QUBOLib.cache_path(index, "qplib"), "qplib.zip") # Download QPLIB archive - if isfile(_zip_path) + if isfile(file_path) @info "[qplib] Archive already downloaded" else @info "[qplib] Downloading archive" - Downloads.download(QPLIB_URL, _zip_path) + Downloads.download(QPLIB_URL, file_path) end # Extract QPLIB archive @@ -329,11 +328,11 @@ function load_qplib!(index::QUBOLib.LibraryIndex) run(``` unzip -qq -o -j - $_zip_path + $file_path 'qplib/html/qplib/*' 'qplib/html/sol/*' 'qplib/html/lp/*' - -d $_data_path + -d $data_path ```) # Remove non-QUBO instances @@ -341,13 +340,13 @@ function load_qplib!(index::QUBOLib.LibraryIndex) code_list = String[] - for file_path in filter(endswith(".qplib"), readdir(_data_path; join = true)) - code = readline(file_path) + for model_path in filter(endswith(".qplib"), readdir(data_path; join = true)) + code = readline(model_path) - if !_is_qplib_qubo(file_path) - rm(joinpath(_data_path, "$(code).qplib"); force = true) - rm(joinpath(_data_path, "$(code).lp"); force = true) - rm(joinpath(_data_path, "$(code).sol"); force = true) + if !_is_qplib_qubo(model_path) + rm(joinpath(data_path, "$(code).qplib"); force = true) + rm(joinpath(data_path, "$(code).lp"); force = true) + rm(joinpath(data_path, "$(code).sol"); force = true) else push!(code_list, code) end diff --git a/src/QUBOLib.jl b/src/QUBOLib.jl index d6cab03..02b460d 100644 --- a/src/QUBOLib.jl +++ b/src/QUBOLib.jl @@ -22,7 +22,7 @@ const QUBOLIB_SQL_PATH = joinpath(@__DIR__, "assets", "qubolib.sql") const COLLECTION_SCHEMA_PATH = joinpath(@__DIR__, "assets", "collection.schema.json") const COLLECTION_SCHEMA = JSONSchema.Schema(JSON.parsefile(COLLECTION_SCHEMA_PATH)) -const QUBOLIB_LOGO = """ +const QUBOLIB_LOGO = raw""" ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ ▄██████▄ ██ ██ █████▄ ▄██████▄ ┃ ┃ ██ ██ ██ ██ ██ ██ ██ ██ ┃ @@ -30,11 +30,11 @@ const QUBOLIB_LOGO = """ ┃ ██ ▀▀▄███ ██ ██ ██ ██ ██ ██ ┃ ┃ ▀██████▀▄▄ ▀██████▀ █████▀ ▀██████▀ ┃ ┃ ┃ -┃ ██ ██ ██ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ┃ -┃ ██ ██ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ┃ -┃ ██ ██ █████▄ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ┃ -┃ ██ ██ ██ ██ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ┃ -┃ ███████ ██ █████▀ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ┃ +┃ ██ ██ ██ ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ ┃ +┃ ██ ██ ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ ┃ +┃ ██ ██ █████▄ ┃ +┃ ██ ██ ██ ██ ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ ┃ +┃ ███████ ██ █████▀ ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ """ diff --git a/src/assets/qubolib.sql b/src/assets/qubolib.sql index 897a03c..4e6a02d 100644 --- a/src/assets/qubolib.sql +++ b/src/assets/qubolib.sql @@ -38,6 +38,7 @@ CREATE TABLE Instances ( instance INTEGER PRIMARY KEY, collection TEXT NOT NULL , + name TEXT NULL , dimension INTEGER NOT NULL , min REAL NOT NULL , max REAL NOT NULL , diff --git a/src/library/access.jl b/src/library/access.jl index 40eb19c..320ad75 100644 --- a/src/library/access.jl +++ b/src/library/access.jl @@ -1,5 +1,5 @@ -function access(callback::Any; path::AbstractString = pwd()) - index = access(; path) +function access(callback::Any; path::AbstractString = pwd(), clear::Bool = false) + index = access(; path, clear) @assert isopen(index) @@ -15,58 +15,78 @@ function access(callback::Any; path::AbstractString = pwd()) end end -function access(; path::AbstractString = pwd()) - if !is_installed(library_path(path)) - install(library_path(path)) +function access(; path::AbstractString = pwd(), clear::Bool = false) + if !is_installed(path) || clear + install(path; clear) end - return load_index(library_path(path)) + return load_index(path) end function is_installed(path::AbstractString)::Bool - return isdir(path) && isfile(database_path(path)) && isfile(archive_path(path)) + lib_path = library_path(path) + + return isdir(lib_path) && isfile(database_path(lib_path)) && isfile(archive_path(lib_path)) end -function install(path::AbstractString) - mkdir(path) - - for src_name in readdir(library_path()) - src_path = abspath(library_path(), src_name) - dst_path = abspath(path, src_name) - - cp( - src_path, - dst_path; - force = true, - follow_symlinks = true, - ) - - chmod(dst_path, 0o644) +function install(path::AbstractString; clear::Bool = false) + lib_path = library_path(path) + + if clear + rm(lib_path; force = true, recursive = true) + + mkpath(lib_path) + else + mkpath(lib_path) + + for src_name in readdir(library_path()) + src_path = abspath(library_path(), src_name) + dst_path = abspath(lib_path, src_name) + + cp( + src_path, + dst_path; + force = true, + follow_symlinks = true, + ) + + chmod(dst_path, 0o644) + end end return nothing end function load_index(path::AbstractString) - @assert isdir(path) + lib_path = library_path(path) + + @assert isdir(lib_path) - db = load_database(database_path(path)) - h5 = load_archive(archive_path(path)) + db = load_database(database_path(lib_path)) + h5 = load_archive(archive_path(lib_path)) - if isnothing(db) || isnothing(h5) + if isnothing(db) && isnothing(h5) # In this case, we have to create both return create_index(path) + elseif isnothing(db) || isnothing(h5) + error("QUBOLib Installation is compromised: Try running `access` with the `clear` argument set to `true`.") + + return nothing else - return LibraryIndex(db, h5, path) + return LibraryIndex(db, h5, lib_path) end end function create_index(path::AbstractString) - # When building, $path is assumed to be pointing to dist/build - db = create_database(database_path(path)) - h5 = create_archive(archive_path(path)) + # When building, $path is assumed to be pointing to dist/ or any other root path + lib_path = library_path(path) + + @assert isdir(lib_path) + + db = create_database(database_path(lib_path)) + h5 = create_archive(archive_path(lib_path)) - return LibraryIndex(db, h5, path) + return LibraryIndex(db, h5, lib_path) end function load_database(path::AbstractString)::Union{SQLite.DB,Nothing} @@ -77,13 +97,19 @@ function load_database(path::AbstractString)::Union{SQLite.DB,Nothing} end end +function each_stmt(src::AbstractString) + return Iterators.filter(!isempty, Iterators.map(strip, eachsplit(src, ';'))) +end + function create_database(path::AbstractString) rm(path; force = true) # Remove file if it exists db = SQLite.DB(path) open(QUBOLIB_SQL_PATH) do file - DBInterface.executemultiple(db, read(file, String)) + for stmt in each_stmt(read(file, String)) + DBInterface.execute(db, stmt) + end end return db diff --git a/src/library/instances.jl b/src/library/instances.jl index 640e374..43ab002 100644 --- a/src/library/instances.jl +++ b/src/library/instances.jl @@ -1,7 +1,8 @@ function add_instance!( index::LibraryIndex, model::QUBOTools.Model{Int,Float64,Int}, - collection::AbstractString = "standalone", + collection::AbstractString = "standalone"; + name::Union{<:AbstractString,Nothing} = nothing, )::Integer @assert isopen(index) @@ -15,39 +16,29 @@ function add_instance!( query = DBInterface.execute( db, """ - INSERT INTO Instances ( - collection , - dimension , - min , - max , - abs_min , - abs_max , - linear_min , - linear_max , - quadratic_min , - quadratic_max , - density , - linear_density , - quadratic_density - ) - VALUES ( - ?, - ?, - ?, - ?, - ?, - ?, - ?, - ?, - ?, - ?, - ?, - ?, - ? - ); + INSERT INTO + Instances ( + collection , + name , + dimension , + min , + max , + abs_min , + abs_max , + linear_min , + linear_max , + quadratic_min , + quadratic_max , + density , + linear_density , + quadratic_density + ) + VALUES + (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); """, ( String(collection), + isnothing(name) ? missing : String(name), QUBOTools.dimension(model), min(minimum(L), minimum(Q)), max(maximum(L), maximum(Q)), diff --git a/src/library/path.jl b/src/library/path.jl index 4ae8688..705b85e 100644 --- a/src/library/path.jl +++ b/src/library/path.jl @@ -9,13 +9,15 @@ function library_path()::String end function library_path(path::AbstractString)::String - return abspath(path, "qubolib") + return joinpath(path, "qubolib") end +library_path(index::LibraryIndex) = index.path + @doc raw""" database_path(path::AbstractString=library_path())::AbstractString -Returns the absolute path to the database file, given a reference `path`. +Returns the absolute path to the database file, given a reference library path `path`. """ function database_path(path::AbstractString = library_path())::String return joinpath(path, "index.db") @@ -26,7 +28,7 @@ database_path(index::LibraryIndex) = database_path(index.path) @doc raw""" archive_path(path::AbstractString=library_path())::AbstractString -Returns the absolute path to the archive file, given a reference `path`. +Returns the absolute path to the archive file, given a reference library path `path`. """ function archive_path(path::AbstractString = library_path())::String return joinpath(path, "archive.h5") @@ -37,23 +39,6 @@ archive_path(index::LibraryIndex) = archive_path(index.path) # Functions below will be more often used when building the library, # therefore they will point to the the project's root path by default. -raw""" - _get_path(path::AbstractString; create::Bool = false) -""" -function _get_path( - path::AbstractString; - create::Bool = false, - ifmissing::Any = path -> error("Path '$path' does not exist"), -)::String - if ispath(path) - return abspath(path) - elseif create - return abspath(mkdir(path)) - else - return ifmissing(path) - end -end - @doc raw""" root_path()::AbstractString @@ -68,50 +53,48 @@ function root_path()::AbstractString return __project__() end +root_path(index::LibraryIndex) = abspath(dirname(index.path)) + @doc raw""" dist_path(path::AbstractString=root_path())::AbstractString -Returns the absolute path to the distribution folder, given a reference `path`. -The path is created if it does not exist. +Returns the absolute path to the distribution folder, given a reference _root path_ `path`. """ -function dist_path( - path::AbstractString = root_path(); - create::Bool = false, - ifmissing::Any = path -> error("No distribution path at '$path'"), -)::String - return _get_path(abspath(path, "dist"); create, ifmissing) +function dist_path(path::AbstractString = root_path())::String + return joinpath(path, "dist") end -dist_path(index::LibraryIndex; kws...) = dist_path(index.path; kws...) +dist_path(index::LibraryIndex) = dist_path(root_path(index)) @doc raw""" build_path(path::AbstractString=root_path())::AbstractString -Returns the absolute path to the build folder, given a reference `path`. -The path is created if it does not exist. +Returns the absolute path to the build folder, given a reference _root path_ `path`. """ -function build_path( - path::AbstractString = root_path(); - create::Bool = false, - ifmissing::Any = path -> error("No build path at '$path'"), -)::String - return _get_path(abspath(dist_path(path; create, ifmissing), "build"); create, ifmissing) +function build_path(path::AbstractString = root_path())::String + return joinpath(dist_path(path), "build") end -build_path(index::LibraryIndex; kws...) = build_path(index.path; kws...) +build_path(index::LibraryIndex) = build_path(root_path(index)) @doc raw""" cache_path(path::AbstractString=root_path())::AbstractString -Returns the absolute path to the cache folder, given a reference `path`. -The path is created if it does not exist. +Returns the absolute path to the cache folder, given a reference _root path_ `path`. +""" +function cache_path(path::AbstractString = root_path(), paths...)::String + return joinpath(dist_path(path), "cache", paths...) +end + +cache_path(index::LibraryIndex, paths...) = cache_path(root_path(index), paths...) + +@doc raw""" + cache_data_path(path::AbstractString, paths...) + +Returns the absolute path to the data cache folder, given a reference _root path_ `path`. """ -function cache_path( - path::AbstractString = root_path(); - create::Bool = false, - ifmissing::Any = path -> error("No cache path at '$path'"), -)::String - return _get_path(abspath(dist_path(path; create, ifmissing), "cache"); create, ifmissing) +function cache_data_path(path::AbstractString = root_path(), paths...)::String + return joinpath(cache_path(path, paths...), "data") end -cache_path(index::LibraryIndex; kws...) = cache_path(index.path; kws...) +cache_data_path(index::LibraryIndex, paths...) = cache_data_path(root_path(index), paths...) diff --git a/test/library/path.jl b/test/library/path.jl new file mode 100644 index 0000000..c004f6a --- /dev/null +++ b/test/library/path.jl @@ -0,0 +1,25 @@ +raw""" + test_path() + +Test path-related functions: +- `QUBOLib.library_path` +- `QUBOLib.root_path` +- `QUBOLib.dist_path` +- `QUBOLib.build_path` +- `QUBOLib.cache_path` +""" +function test_path() + @testset "→ Paths" begin + mktempdir() do path + @test abspath(QUBOLib.library_path(path)) == abspath(path, "qubolib") # path/qubolib + + @test abspath(QUBOLib.dist_path(path)) == abspath(path, "dist") # path/dist + @test abspath(QUBOLib.build_path(path)) == abspath(path, "dist", "build") # path/dist/build + @test abspath(QUBOLib.cache_path(path)) == abspath(path, "dist", "cache") # path/dist/cache + @test abspath(QUBOLib.cache_path(path, "collection")) == abspath(path, "dist", "cache", "collection") # path/dist/cache/collection + @test abspath(QUBOLib.cache_data_path(path, "collection")) == abspath(path, "dist", "cache", "collection", "data") # path/dist/cache/collection + end + end + + return nothing +end diff --git a/test/runtests.jl b/test/runtests.jl index 799d59c..8255fb1 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -5,9 +5,11 @@ import QUBOTools import QUBOLib include("synthesis.jl") +include("library/path.jl") function main() @testset "♣ QUBOLib.jl «$(QUBOLib.__version__())» Test Suite ♣" verbose = true begin + test_path() test_synthesis() end