From ecf80507f6de0d19fdca595c3f5fd34551f1b652 Mon Sep 17 00:00:00 2001 From: Dominik Krupke Date: Thu, 13 Feb 2025 08:08:14 +0100 Subject: [PATCH 01/20] feat: python bindings first version --- CMakeLists.txt | 22 + cmake/CPM.cmake | 1285 +++++++++++++++++++++++++++++++++ pyproject.toml | 92 +++ setup.py | 30 + src/algorithms/Refinement.hpp | 2 +- src/core/cft.hpp | 1 + src/core/parsing.hpp | 2 +- src/pyaccft/__init__.py | 111 +++ src/pyaccft/_bindings.cpp | 144 ++++ 9 files changed, 1687 insertions(+), 2 deletions(-) create mode 100644 cmake/CPM.cmake create mode 100644 pyproject.toml create mode 100644 setup.py create mode 100644 src/pyaccft/__init__.py create mode 100644 src/pyaccft/_bindings.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 66ae31f..0c63354 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,6 +28,28 @@ add_executable(accft ${SOURCE}) set(LIBRARIES fmt::fmt pthread dl m) target_link_libraries(accft PUBLIC ${LIBRARIES}) +# CPM Dependencies ~~~~~~~~~~~~~~~~ CPM allows us to include some simple +# dependencies without much fuss. Great default, for everything more +# complicated, uses conan. Why not always use conan? CPM works without any +# interaction (conan needs to be called externally). CPM also has no problems +# with install PyBind11 (which conan has at the time). If the dependency has +# complicated dependencies on its own or is slow to compile, better use conan. +# Check out https://github.com/cpm-cmake/CPM.cmake +include(./cmake/CPM.cmake) # Package manager for simple requirements. + +# PyBind11 +cpmaddpackage("gh:pybind/pybind11@2.10.0") # pybind11, essential +set(CMAKE_POSITION_INDEPENDENT_CODE ON) # The code needs to be compiled as PIC + # to build the shared lib for python. +set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) + +pybind11_add_module(_bindings ./src/pyaccft/_bindings.cpp) +target_link_libraries(_bindings PUBLIC fmt::fmt ${LIBRARIES}) +# enable compilation warnings +target_compile_options( + _bindings PRIVATE "$<$:-Wall>") +target_compile_definitions(_bindings PRIVATE PYBIND11_DETAILED_ERROR_MESSAGES) +install(TARGETS _bindings DESTINATION ./src/pyaccft/) ######################################## ############## Unit tests ############## diff --git a/cmake/CPM.cmake b/cmake/CPM.cmake new file mode 100644 index 0000000..80e6896 --- /dev/null +++ b/cmake/CPM.cmake @@ -0,0 +1,1285 @@ +# CPM.cmake - CMake's missing package manager +# =========================================== +# See https://github.com/cpm-cmake/CPM.cmake for usage and update instructions. +# +# MIT License +# ----------- +#[[ + Copyright (c) 2019-2023 Lars Melchior and contributors + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +]] + +cmake_minimum_required(VERSION 3.14 FATAL_ERROR) + +# Initialize logging prefix +if(NOT CPM_INDENT) + set(CPM_INDENT + "CPM:" + CACHE INTERNAL "" + ) +endif() + +if(NOT COMMAND cpm_message) + function(cpm_message) + message(${ARGV}) + endfunction() +endif() + +set(CURRENT_CPM_VERSION 1.0.0-development-version) + +get_filename_component(CPM_CURRENT_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}" REALPATH) +if(CPM_DIRECTORY) + if(NOT CPM_DIRECTORY STREQUAL CPM_CURRENT_DIRECTORY) + if(CPM_VERSION VERSION_LESS CURRENT_CPM_VERSION) + message( + AUTHOR_WARNING + "${CPM_INDENT} \ +A dependency is using a more recent CPM version (${CURRENT_CPM_VERSION}) than the current project (${CPM_VERSION}). \ +It is recommended to upgrade CPM to the most recent version. \ +See https://github.com/cpm-cmake/CPM.cmake for more information." + ) + endif() + if(${CMAKE_VERSION} VERSION_LESS "3.17.0") + include(FetchContent) + endif() + return() + endif() + + get_property( + CPM_INITIALIZED GLOBAL "" + PROPERTY CPM_INITIALIZED + SET + ) + if(CPM_INITIALIZED) + return() + endif() +endif() + +if(CURRENT_CPM_VERSION MATCHES "development-version") + message( + WARNING "${CPM_INDENT} Your project is using an unstable development version of CPM.cmake. \ +Please update to a recent release if possible. \ +See https://github.com/cpm-cmake/CPM.cmake for details." + ) +endif() + +set_property(GLOBAL PROPERTY CPM_INITIALIZED true) + +macro(cpm_set_policies) + # the policy allows us to change options without caching + cmake_policy(SET CMP0077 NEW) + set(CMAKE_POLICY_DEFAULT_CMP0077 NEW) + + # the policy allows us to change set(CACHE) without caching + if(POLICY CMP0126) + cmake_policy(SET CMP0126 NEW) + set(CMAKE_POLICY_DEFAULT_CMP0126 NEW) + endif() + + # The policy uses the download time for timestamp, instead of the timestamp in the archive. This + # allows for proper rebuilds when a projects url changes + if(POLICY CMP0135) + cmake_policy(SET CMP0135 NEW) + set(CMAKE_POLICY_DEFAULT_CMP0135 NEW) + endif() + + # treat relative git repository paths as being relative to the parent project's remote + if(POLICY CMP0150) + cmake_policy(SET CMP0150 NEW) + set(CMAKE_POLICY_DEFAULT_CMP0150 NEW) + endif() +endmacro() +cpm_set_policies() + +option(CPM_USE_LOCAL_PACKAGES "Always try to use `find_package` to get dependencies" + $ENV{CPM_USE_LOCAL_PACKAGES} +) +option(CPM_LOCAL_PACKAGES_ONLY "Only use `find_package` to get dependencies" + $ENV{CPM_LOCAL_PACKAGES_ONLY} +) +option(CPM_DOWNLOAD_ALL "Always download dependencies from source" $ENV{CPM_DOWNLOAD_ALL}) +option(CPM_DONT_UPDATE_MODULE_PATH "Don't update the module path to allow using find_package" + $ENV{CPM_DONT_UPDATE_MODULE_PATH} +) +option(CPM_DONT_CREATE_PACKAGE_LOCK "Don't create a package lock file in the binary path" + $ENV{CPM_DONT_CREATE_PACKAGE_LOCK} +) +option(CPM_INCLUDE_ALL_IN_PACKAGE_LOCK + "Add all packages added through CPM.cmake to the package lock" + $ENV{CPM_INCLUDE_ALL_IN_PACKAGE_LOCK} +) +option(CPM_USE_NAMED_CACHE_DIRECTORIES + "Use additional directory of package name in cache on the most nested level." + $ENV{CPM_USE_NAMED_CACHE_DIRECTORIES} +) + +set(CPM_VERSION + ${CURRENT_CPM_VERSION} + CACHE INTERNAL "" +) +set(CPM_DIRECTORY + ${CPM_CURRENT_DIRECTORY} + CACHE INTERNAL "" +) +set(CPM_FILE + ${CMAKE_CURRENT_LIST_FILE} + CACHE INTERNAL "" +) +set(CPM_PACKAGES + "" + CACHE INTERNAL "" +) +set(CPM_DRY_RUN + OFF + CACHE INTERNAL "Don't download or configure dependencies (for testing)" +) + +if(DEFINED ENV{CPM_SOURCE_CACHE}) + set(CPM_SOURCE_CACHE_DEFAULT $ENV{CPM_SOURCE_CACHE}) +else() + set(CPM_SOURCE_CACHE_DEFAULT OFF) +endif() + +set(CPM_SOURCE_CACHE + ${CPM_SOURCE_CACHE_DEFAULT} + CACHE PATH "Directory to download CPM dependencies" +) + +if(NOT CPM_DONT_UPDATE_MODULE_PATH AND NOT DEFINED CMAKE_FIND_PACKAGE_REDIRECTS_DIR) + set(CPM_MODULE_PATH + "${CMAKE_BINARY_DIR}/CPM_modules" + CACHE INTERNAL "" + ) + # remove old modules + file(REMOVE_RECURSE ${CPM_MODULE_PATH}) + file(MAKE_DIRECTORY ${CPM_MODULE_PATH}) + # locally added CPM modules should override global packages + set(CMAKE_MODULE_PATH "${CPM_MODULE_PATH};${CMAKE_MODULE_PATH}") +endif() + +if(NOT CPM_DONT_CREATE_PACKAGE_LOCK) + set(CPM_PACKAGE_LOCK_FILE + "${CMAKE_BINARY_DIR}/cpm-package-lock.cmake" + CACHE INTERNAL "" + ) + file(WRITE ${CPM_PACKAGE_LOCK_FILE} + "# CPM Package Lock\n# This file should be committed to version control\n\n" + ) +endif() + +include(FetchContent) + +# Try to infer package name from git repository uri (path or url) +function(cpm_package_name_from_git_uri URI RESULT) + if("${URI}" MATCHES "([^/:]+)/?.git/?$") + set(${RESULT} + ${CMAKE_MATCH_1} + PARENT_SCOPE + ) + else() + unset(${RESULT} PARENT_SCOPE) + endif() +endfunction() + +# Try to infer package name and version from a url +function(cpm_package_name_and_ver_from_url url outName outVer) + if(url MATCHES "[/\\?]([a-zA-Z0-9_\\.-]+)\\.(tar|tar\\.gz|tar\\.bz2|zip|ZIP)(\\?|/|$)") + # We matched an archive + set(filename "${CMAKE_MATCH_1}") + + if(filename MATCHES "([a-zA-Z0-9_\\.-]+)[_-]v?(([0-9]+\\.)*[0-9]+[a-zA-Z0-9]*)") + # We matched - (ie foo-1.2.3) + set(${outName} + "${CMAKE_MATCH_1}" + PARENT_SCOPE + ) + set(${outVer} + "${CMAKE_MATCH_2}" + PARENT_SCOPE + ) + elseif(filename MATCHES "(([0-9]+\\.)+[0-9]+[a-zA-Z0-9]*)") + # We couldn't find a name, but we found a version + # + # In many cases (which we don't handle here) the url would look something like + # `irrelevant/ACTUAL_PACKAGE_NAME/irrelevant/1.2.3.zip`. In such a case we can't possibly + # distinguish the package name from the irrelevant bits. Moreover if we try to match the + # package name from the filename, we'd get bogus at best. + unset(${outName} PARENT_SCOPE) + set(${outVer} + "${CMAKE_MATCH_1}" + PARENT_SCOPE + ) + else() + # Boldly assume that the file name is the package name. + # + # Yes, something like `irrelevant/ACTUAL_NAME/irrelevant/download.zip` will ruin our day, but + # such cases should be quite rare. No popular service does this... we think. + set(${outName} + "${filename}" + PARENT_SCOPE + ) + unset(${outVer} PARENT_SCOPE) + endif() + else() + # No ideas yet what to do with non-archives + unset(${outName} PARENT_SCOPE) + unset(${outVer} PARENT_SCOPE) + endif() +endfunction() + +function(cpm_find_package NAME VERSION) + string(REPLACE " " ";" EXTRA_ARGS "${ARGN}") + find_package(${NAME} ${VERSION} ${EXTRA_ARGS} QUIET) + if(${CPM_ARGS_NAME}_FOUND) + if(DEFINED ${CPM_ARGS_NAME}_VERSION) + set(VERSION ${${CPM_ARGS_NAME}_VERSION}) + endif() + cpm_message(STATUS "${CPM_INDENT} Using local package ${CPM_ARGS_NAME}@${VERSION}") + CPMRegisterPackage(${CPM_ARGS_NAME} "${VERSION}") + set(CPM_PACKAGE_FOUND + YES + PARENT_SCOPE + ) + else() + set(CPM_PACKAGE_FOUND + NO + PARENT_SCOPE + ) + endif() +endfunction() + +# Create a custom FindXXX.cmake module for a CPM package This prevents `find_package(NAME)` from +# finding the system library +function(cpm_create_module_file Name) + if(NOT CPM_DONT_UPDATE_MODULE_PATH) + if(DEFINED CMAKE_FIND_PACKAGE_REDIRECTS_DIR) + # Redirect find_package calls to the CPM package. This is what FetchContent does when you set + # OVERRIDE_FIND_PACKAGE. The CMAKE_FIND_PACKAGE_REDIRECTS_DIR works for find_package in CONFIG + # mode, unlike the Find${Name}.cmake fallback. CMAKE_FIND_PACKAGE_REDIRECTS_DIR is not defined + # in script mode, or in CMake < 3.24. + # https://cmake.org/cmake/help/latest/module/FetchContent.html#fetchcontent-find-package-integration-examples + string(TOLOWER ${Name} NameLower) + file(WRITE ${CMAKE_FIND_PACKAGE_REDIRECTS_DIR}/${NameLower}-config.cmake + "include(\"${CMAKE_CURRENT_LIST_DIR}/${NameLower}-extra.cmake\" OPTIONAL)\n" + "include(\"${CMAKE_CURRENT_LIST_DIR}/${Name}Extra.cmake\" OPTIONAL)\n" + ) + file(WRITE ${CMAKE_FIND_PACKAGE_REDIRECTS_DIR}/${NameLower}-version.cmake + "set(PACKAGE_VERSION_COMPATIBLE TRUE)\n" "set(PACKAGE_VERSION_EXACT TRUE)\n" + ) + else() + file(WRITE ${CPM_MODULE_PATH}/Find${Name}.cmake + "include(\"${CPM_FILE}\")\n${ARGN}\nset(${Name}_FOUND TRUE)" + ) + endif() + endif() +endfunction() + +# Find a package locally or fallback to CPMAddPackage +function(CPMFindPackage) + set(oneValueArgs NAME VERSION GIT_TAG FIND_PACKAGE_ARGUMENTS) + + cmake_parse_arguments(CPM_ARGS "" "${oneValueArgs}" "" ${ARGN}) + + if(NOT DEFINED CPM_ARGS_VERSION) + if(DEFINED CPM_ARGS_GIT_TAG) + cpm_get_version_from_git_tag("${CPM_ARGS_GIT_TAG}" CPM_ARGS_VERSION) + endif() + endif() + + set(downloadPackage ${CPM_DOWNLOAD_ALL}) + if(DEFINED CPM_DOWNLOAD_${CPM_ARGS_NAME}) + set(downloadPackage ${CPM_DOWNLOAD_${CPM_ARGS_NAME}}) + elseif(DEFINED ENV{CPM_DOWNLOAD_${CPM_ARGS_NAME}}) + set(downloadPackage $ENV{CPM_DOWNLOAD_${CPM_ARGS_NAME}}) + endif() + if(downloadPackage) + CPMAddPackage(${ARGN}) + cpm_export_variables(${CPM_ARGS_NAME}) + return() + endif() + + cpm_find_package(${CPM_ARGS_NAME} "${CPM_ARGS_VERSION}" ${CPM_ARGS_FIND_PACKAGE_ARGUMENTS}) + + if(NOT CPM_PACKAGE_FOUND) + CPMAddPackage(${ARGN}) + cpm_export_variables(${CPM_ARGS_NAME}) + endif() + +endfunction() + +# checks if a package has been added before +function(cpm_check_if_package_already_added CPM_ARGS_NAME CPM_ARGS_VERSION) + if("${CPM_ARGS_NAME}" IN_LIST CPM_PACKAGES) + CPMGetPackageVersion(${CPM_ARGS_NAME} CPM_PACKAGE_VERSION) + if("${CPM_PACKAGE_VERSION}" VERSION_LESS "${CPM_ARGS_VERSION}") + message( + WARNING + "${CPM_INDENT} Requires a newer version of ${CPM_ARGS_NAME} (${CPM_ARGS_VERSION}) than currently included (${CPM_PACKAGE_VERSION})." + ) + endif() + cpm_get_fetch_properties(${CPM_ARGS_NAME}) + set(${CPM_ARGS_NAME}_ADDED NO) + set(CPM_PACKAGE_ALREADY_ADDED + YES + PARENT_SCOPE + ) + cpm_export_variables(${CPM_ARGS_NAME}) + else() + set(CPM_PACKAGE_ALREADY_ADDED + NO + PARENT_SCOPE + ) + endif() +endfunction() + +# Parse the argument of CPMAddPackage in case a single one was provided and convert it to a list of +# arguments which can then be parsed idiomatically. For example gh:foo/bar@1.2.3 will be converted +# to: GITHUB_REPOSITORY;foo/bar;VERSION;1.2.3 +function(cpm_parse_add_package_single_arg arg outArgs) + # Look for a scheme + if("${arg}" MATCHES "^([a-zA-Z]+):(.+)$") + string(TOLOWER "${CMAKE_MATCH_1}" scheme) + set(uri "${CMAKE_MATCH_2}") + + # Check for CPM-specific schemes + if(scheme STREQUAL "gh") + set(out "GITHUB_REPOSITORY;${uri}") + set(packageType "git") + elseif(scheme STREQUAL "gl") + set(out "GITLAB_REPOSITORY;${uri}") + set(packageType "git") + elseif(scheme STREQUAL "bb") + set(out "BITBUCKET_REPOSITORY;${uri}") + set(packageType "git") + # A CPM-specific scheme was not found. Looks like this is a generic URL so try to determine + # type + elseif(arg MATCHES ".git/?(@|#|$)") + set(out "GIT_REPOSITORY;${arg}") + set(packageType "git") + else() + # Fall back to a URL + set(out "URL;${arg}") + set(packageType "archive") + + # We could also check for SVN since FetchContent supports it, but SVN is so rare these days. + # We just won't bother with the additional complexity it will induce in this function. SVN is + # done by multi-arg + endif() + else() + if(arg MATCHES ".git/?(@|#|$)") + set(out "GIT_REPOSITORY;${arg}") + set(packageType "git") + else() + # Give up + message(FATAL_ERROR "${CPM_INDENT} Can't determine package type of '${arg}'") + endif() + endif() + + # For all packages we interpret @... as version. Only replace the last occurrence. Thus URIs + # containing '@' can be used + string(REGEX REPLACE "@([^@]+)$" ";VERSION;\\1" out "${out}") + + # Parse the rest according to package type + if(packageType STREQUAL "git") + # For git repos we interpret #... as a tag or branch or commit hash + string(REGEX REPLACE "#([^#]+)$" ";GIT_TAG;\\1" out "${out}") + elseif(packageType STREQUAL "archive") + # For archives we interpret #... as a URL hash. + string(REGEX REPLACE "#([^#]+)$" ";URL_HASH;\\1" out "${out}") + # We don't try to parse the version if it's not provided explicitly. cpm_get_version_from_url + # should do this at a later point + else() + # We should never get here. This is an assertion and hitting it means there's a problem with the + # code above. A packageType was set, but not handled by this if-else. + message(FATAL_ERROR "${CPM_INDENT} Unsupported package type '${packageType}' of '${arg}'") + endif() + + set(${outArgs} + ${out} + PARENT_SCOPE + ) +endfunction() + +# Check that the working directory for a git repo is clean +function(cpm_check_git_working_dir_is_clean repoPath gitTag isClean) + + find_package(Git REQUIRED) + + if(NOT GIT_EXECUTABLE) + # No git executable, assume directory is clean + set(${isClean} + TRUE + PARENT_SCOPE + ) + return() + endif() + + # check for uncommitted changes + execute_process( + COMMAND ${GIT_EXECUTABLE} status --porcelain + RESULT_VARIABLE resultGitStatus + OUTPUT_VARIABLE repoStatus + OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET + WORKING_DIRECTORY ${repoPath} + ) + if(resultGitStatus) + # not supposed to happen, assume clean anyway + message(WARNING "${CPM_INDENT} Calling git status on folder ${repoPath} failed") + set(${isClean} + TRUE + PARENT_SCOPE + ) + return() + endif() + + if(NOT "${repoStatus}" STREQUAL "") + set(${isClean} + FALSE + PARENT_SCOPE + ) + return() + endif() + + # check for committed changes + execute_process( + COMMAND ${GIT_EXECUTABLE} diff -s --exit-code ${gitTag} + RESULT_VARIABLE resultGitDiff + OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_QUIET + WORKING_DIRECTORY ${repoPath} + ) + + if(${resultGitDiff} EQUAL 0) + set(${isClean} + TRUE + PARENT_SCOPE + ) + else() + set(${isClean} + FALSE + PARENT_SCOPE + ) + endif() + +endfunction() + +# Add PATCH_COMMAND to CPM_ARGS_UNPARSED_ARGUMENTS. This method consumes a list of files in ARGN +# then generates a `PATCH_COMMAND` appropriate for `ExternalProject_Add()`. This command is appended +# to the parent scope's `CPM_ARGS_UNPARSED_ARGUMENTS`. +function(cpm_add_patches) + # Return if no patch files are supplied. + if(NOT ARGN) + return() + endif() + + # Find the patch program. + find_program(PATCH_EXECUTABLE patch) + if(CMAKE_HOST_WIN32 AND NOT PATCH_EXECUTABLE) + # The Windows git executable is distributed with patch.exe. Find the path to the executable, if + # it exists, then search `../usr/bin` and `../../usr/bin` for patch.exe. + find_package(Git QUIET) + if(GIT_EXECUTABLE) + get_filename_component(extra_search_path ${GIT_EXECUTABLE} DIRECTORY) + get_filename_component(extra_search_path_1up ${extra_search_path} DIRECTORY) + get_filename_component(extra_search_path_2up ${extra_search_path_1up} DIRECTORY) + find_program( + PATCH_EXECUTABLE patch HINTS "${extra_search_path_1up}/usr/bin" + "${extra_search_path_2up}/usr/bin" + ) + endif() + endif() + if(NOT PATCH_EXECUTABLE) + message(FATAL_ERROR "Couldn't find `patch` executable to use with PATCHES keyword.") + endif() + + # Create a temporary + set(temp_list ${CPM_ARGS_UNPARSED_ARGUMENTS}) + + # Ensure each file exists (or error out) and add it to the list. + set(first_item True) + foreach(PATCH_FILE ${ARGN}) + # Make sure the patch file exists, if we can't find it, try again in the current directory. + if(NOT EXISTS "${PATCH_FILE}") + if(NOT EXISTS "${CMAKE_CURRENT_LIST_DIR}/${PATCH_FILE}") + message(FATAL_ERROR "Couldn't find patch file: '${PATCH_FILE}'") + endif() + set(PATCH_FILE "${CMAKE_CURRENT_LIST_DIR}/${PATCH_FILE}") + endif() + + # Convert to absolute path for use with patch file command. + get_filename_component(PATCH_FILE "${PATCH_FILE}" ABSOLUTE) + + # The first patch entry must be preceded by "PATCH_COMMAND" while the following items are + # preceded by "&&". + if(first_item) + set(first_item False) + list(APPEND temp_list "PATCH_COMMAND") + else() + list(APPEND temp_list "&&") + endif() + # Add the patch command to the list + list(APPEND temp_list "${PATCH_EXECUTABLE}" "-p1" "<" "${PATCH_FILE}") + endforeach() + + # Move temp out into parent scope. + set(CPM_ARGS_UNPARSED_ARGUMENTS + ${temp_list} + PARENT_SCOPE + ) + +endfunction() + +# method to overwrite internal FetchContent properties, to allow using CPM.cmake to overload +# FetchContent calls. As these are internal cmake properties, this method should be used carefully +# and may need modification in future CMake versions. Source: +# https://github.com/Kitware/CMake/blob/dc3d0b5a0a7d26d43d6cfeb511e224533b5d188f/Modules/FetchContent.cmake#L1152 +function(cpm_override_fetchcontent contentName) + cmake_parse_arguments(PARSE_ARGV 1 arg "" "SOURCE_DIR;BINARY_DIR" "") + if(NOT "${arg_UNPARSED_ARGUMENTS}" STREQUAL "") + message(FATAL_ERROR "${CPM_INDENT} Unsupported arguments: ${arg_UNPARSED_ARGUMENTS}") + endif() + + string(TOLOWER ${contentName} contentNameLower) + set(prefix "_FetchContent_${contentNameLower}") + + set(propertyName "${prefix}_sourceDir") + define_property( + GLOBAL + PROPERTY ${propertyName} + BRIEF_DOCS "Internal implementation detail of FetchContent_Populate()" + FULL_DOCS "Details used by FetchContent_Populate() for ${contentName}" + ) + set_property(GLOBAL PROPERTY ${propertyName} "${arg_SOURCE_DIR}") + + set(propertyName "${prefix}_binaryDir") + define_property( + GLOBAL + PROPERTY ${propertyName} + BRIEF_DOCS "Internal implementation detail of FetchContent_Populate()" + FULL_DOCS "Details used by FetchContent_Populate() for ${contentName}" + ) + set_property(GLOBAL PROPERTY ${propertyName} "${arg_BINARY_DIR}") + + set(propertyName "${prefix}_populated") + define_property( + GLOBAL + PROPERTY ${propertyName} + BRIEF_DOCS "Internal implementation detail of FetchContent_Populate()" + FULL_DOCS "Details used by FetchContent_Populate() for ${contentName}" + ) + set_property(GLOBAL PROPERTY ${propertyName} TRUE) +endfunction() + +# Download and add a package from source +function(CPMAddPackage) + cpm_set_policies() + + list(LENGTH ARGN argnLength) + if(argnLength EQUAL 1) + cpm_parse_add_package_single_arg("${ARGN}" ARGN) + + # The shorthand syntax implies EXCLUDE_FROM_ALL and SYSTEM + set(ARGN "${ARGN};EXCLUDE_FROM_ALL;YES;SYSTEM;YES;") + endif() + + set(oneValueArgs + NAME + FORCE + VERSION + GIT_TAG + DOWNLOAD_ONLY + GITHUB_REPOSITORY + GITLAB_REPOSITORY + BITBUCKET_REPOSITORY + GIT_REPOSITORY + SOURCE_DIR + FIND_PACKAGE_ARGUMENTS + NO_CACHE + SYSTEM + GIT_SHALLOW + EXCLUDE_FROM_ALL + SOURCE_SUBDIR + CUSTOM_CACHE_KEY + ) + + set(multiValueArgs URL OPTIONS DOWNLOAD_COMMAND PATCHES) + + cmake_parse_arguments(CPM_ARGS "" "${oneValueArgs}" "${multiValueArgs}" "${ARGN}") + + # Set default values for arguments + + if(NOT DEFINED CPM_ARGS_VERSION) + if(DEFINED CPM_ARGS_GIT_TAG) + cpm_get_version_from_git_tag("${CPM_ARGS_GIT_TAG}" CPM_ARGS_VERSION) + endif() + endif() + + if(CPM_ARGS_DOWNLOAD_ONLY) + set(DOWNLOAD_ONLY ${CPM_ARGS_DOWNLOAD_ONLY}) + else() + set(DOWNLOAD_ONLY NO) + endif() + + if(DEFINED CPM_ARGS_GITHUB_REPOSITORY) + set(CPM_ARGS_GIT_REPOSITORY "https://github.com/${CPM_ARGS_GITHUB_REPOSITORY}.git") + elseif(DEFINED CPM_ARGS_GITLAB_REPOSITORY) + set(CPM_ARGS_GIT_REPOSITORY "https://gitlab.com/${CPM_ARGS_GITLAB_REPOSITORY}.git") + elseif(DEFINED CPM_ARGS_BITBUCKET_REPOSITORY) + set(CPM_ARGS_GIT_REPOSITORY "https://bitbucket.org/${CPM_ARGS_BITBUCKET_REPOSITORY}.git") + endif() + + if(DEFINED CPM_ARGS_GIT_REPOSITORY) + list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS GIT_REPOSITORY ${CPM_ARGS_GIT_REPOSITORY}) + if(NOT DEFINED CPM_ARGS_GIT_TAG) + set(CPM_ARGS_GIT_TAG v${CPM_ARGS_VERSION}) + endif() + + # If a name wasn't provided, try to infer it from the git repo + if(NOT DEFINED CPM_ARGS_NAME) + cpm_package_name_from_git_uri(${CPM_ARGS_GIT_REPOSITORY} CPM_ARGS_NAME) + endif() + endif() + + set(CPM_SKIP_FETCH FALSE) + + if(DEFINED CPM_ARGS_GIT_TAG) + list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS GIT_TAG ${CPM_ARGS_GIT_TAG}) + # If GIT_SHALLOW is explicitly specified, honor the value. + if(DEFINED CPM_ARGS_GIT_SHALLOW) + list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS GIT_SHALLOW ${CPM_ARGS_GIT_SHALLOW}) + endif() + endif() + + if(DEFINED CPM_ARGS_URL) + # If a name or version aren't provided, try to infer them from the URL + list(GET CPM_ARGS_URL 0 firstUrl) + cpm_package_name_and_ver_from_url(${firstUrl} nameFromUrl verFromUrl) + # If we fail to obtain name and version from the first URL, we could try other URLs if any. + # However multiple URLs are expected to be quite rare, so for now we won't bother. + + # If the caller provided their own name and version, they trump the inferred ones. + if(NOT DEFINED CPM_ARGS_NAME) + set(CPM_ARGS_NAME ${nameFromUrl}) + endif() + if(NOT DEFINED CPM_ARGS_VERSION) + set(CPM_ARGS_VERSION ${verFromUrl}) + endif() + + list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS URL "${CPM_ARGS_URL}") + endif() + + # Check for required arguments + + if(NOT DEFINED CPM_ARGS_NAME) + message( + FATAL_ERROR + "${CPM_INDENT} 'NAME' was not provided and couldn't be automatically inferred for package added with arguments: '${ARGN}'" + ) + endif() + + # Check if package has been added before + cpm_check_if_package_already_added(${CPM_ARGS_NAME} "${CPM_ARGS_VERSION}") + if(CPM_PACKAGE_ALREADY_ADDED) + cpm_export_variables(${CPM_ARGS_NAME}) + return() + endif() + + # Check for manual overrides + if(NOT CPM_ARGS_FORCE AND NOT "${CPM_${CPM_ARGS_NAME}_SOURCE}" STREQUAL "") + set(PACKAGE_SOURCE ${CPM_${CPM_ARGS_NAME}_SOURCE}) + set(CPM_${CPM_ARGS_NAME}_SOURCE "") + CPMAddPackage( + NAME "${CPM_ARGS_NAME}" + SOURCE_DIR "${PACKAGE_SOURCE}" + EXCLUDE_FROM_ALL "${CPM_ARGS_EXCLUDE_FROM_ALL}" + SYSTEM "${CPM_ARGS_SYSTEM}" + PATCHES "${CPM_ARGS_PATCHES}" + OPTIONS "${CPM_ARGS_OPTIONS}" + SOURCE_SUBDIR "${CPM_ARGS_SOURCE_SUBDIR}" + DOWNLOAD_ONLY "${DOWNLOAD_ONLY}" + FORCE True + ) + cpm_export_variables(${CPM_ARGS_NAME}) + return() + endif() + + # Check for available declaration + if(NOT CPM_ARGS_FORCE AND NOT "${CPM_DECLARATION_${CPM_ARGS_NAME}}" STREQUAL "") + set(declaration ${CPM_DECLARATION_${CPM_ARGS_NAME}}) + set(CPM_DECLARATION_${CPM_ARGS_NAME} "") + CPMAddPackage(${declaration}) + cpm_export_variables(${CPM_ARGS_NAME}) + # checking again to ensure version and option compatibility + cpm_check_if_package_already_added(${CPM_ARGS_NAME} "${CPM_ARGS_VERSION}") + return() + endif() + + if(NOT CPM_ARGS_FORCE) + if(CPM_USE_LOCAL_PACKAGES OR CPM_LOCAL_PACKAGES_ONLY) + cpm_find_package(${CPM_ARGS_NAME} "${CPM_ARGS_VERSION}" ${CPM_ARGS_FIND_PACKAGE_ARGUMENTS}) + + if(CPM_PACKAGE_FOUND) + cpm_export_variables(${CPM_ARGS_NAME}) + return() + endif() + + if(CPM_LOCAL_PACKAGES_ONLY) + message( + SEND_ERROR + "${CPM_INDENT} ${CPM_ARGS_NAME} not found via find_package(${CPM_ARGS_NAME} ${CPM_ARGS_VERSION})" + ) + endif() + endif() + endif() + + CPMRegisterPackage("${CPM_ARGS_NAME}" "${CPM_ARGS_VERSION}") + + if(DEFINED CPM_ARGS_GIT_TAG) + set(PACKAGE_INFO "${CPM_ARGS_GIT_TAG}") + elseif(DEFINED CPM_ARGS_SOURCE_DIR) + set(PACKAGE_INFO "${CPM_ARGS_SOURCE_DIR}") + else() + set(PACKAGE_INFO "${CPM_ARGS_VERSION}") + endif() + + if(DEFINED FETCHCONTENT_BASE_DIR) + # respect user's FETCHCONTENT_BASE_DIR if set + set(CPM_FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR}) + else() + set(CPM_FETCHCONTENT_BASE_DIR ${CMAKE_BINARY_DIR}/_deps) + endif() + + cpm_add_patches(${CPM_ARGS_PATCHES}) + + if(DEFINED CPM_ARGS_DOWNLOAD_COMMAND) + list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS DOWNLOAD_COMMAND ${CPM_ARGS_DOWNLOAD_COMMAND}) + elseif(DEFINED CPM_ARGS_SOURCE_DIR) + list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS SOURCE_DIR ${CPM_ARGS_SOURCE_DIR}) + if(NOT IS_ABSOLUTE ${CPM_ARGS_SOURCE_DIR}) + # Expand `CPM_ARGS_SOURCE_DIR` relative path. This is important because EXISTS doesn't work + # for relative paths. + get_filename_component( + source_directory ${CPM_ARGS_SOURCE_DIR} REALPATH BASE_DIR ${CMAKE_CURRENT_BINARY_DIR} + ) + else() + set(source_directory ${CPM_ARGS_SOURCE_DIR}) + endif() + if(NOT EXISTS ${source_directory}) + string(TOLOWER ${CPM_ARGS_NAME} lower_case_name) + # remove timestamps so CMake will re-download the dependency + file(REMOVE_RECURSE "${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-subbuild") + endif() + elseif(CPM_SOURCE_CACHE AND NOT CPM_ARGS_NO_CACHE) + string(TOLOWER ${CPM_ARGS_NAME} lower_case_name) + set(origin_parameters ${CPM_ARGS_UNPARSED_ARGUMENTS}) + list(SORT origin_parameters) + if(CPM_ARGS_CUSTOM_CACHE_KEY) + # Application set a custom unique directory name + set(download_directory ${CPM_SOURCE_CACHE}/${lower_case_name}/${CPM_ARGS_CUSTOM_CACHE_KEY}) + elseif(CPM_USE_NAMED_CACHE_DIRECTORIES) + string(SHA1 origin_hash "${origin_parameters};NEW_CACHE_STRUCTURE_TAG") + set(download_directory ${CPM_SOURCE_CACHE}/${lower_case_name}/${origin_hash}/${CPM_ARGS_NAME}) + else() + string(SHA1 origin_hash "${origin_parameters}") + set(download_directory ${CPM_SOURCE_CACHE}/${lower_case_name}/${origin_hash}) + endif() + # Expand `download_directory` relative path. This is important because EXISTS doesn't work for + # relative paths. + get_filename_component(download_directory ${download_directory} ABSOLUTE) + list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS SOURCE_DIR ${download_directory}) + + if(CPM_SOURCE_CACHE) + file(LOCK ${download_directory}/../cmake.lock) + endif() + + if(EXISTS ${download_directory}) + if(CPM_SOURCE_CACHE) + file(LOCK ${download_directory}/../cmake.lock RELEASE) + endif() + + cpm_store_fetch_properties( + ${CPM_ARGS_NAME} "${download_directory}" + "${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-build" + ) + cpm_get_fetch_properties("${CPM_ARGS_NAME}") + + if(DEFINED CPM_ARGS_GIT_TAG AND NOT (PATCH_COMMAND IN_LIST CPM_ARGS_UNPARSED_ARGUMENTS)) + # warn if cache has been changed since checkout + cpm_check_git_working_dir_is_clean(${download_directory} ${CPM_ARGS_GIT_TAG} IS_CLEAN) + if(NOT ${IS_CLEAN}) + message( + WARNING "${CPM_INDENT} Cache for ${CPM_ARGS_NAME} (${download_directory}) is dirty" + ) + endif() + endif() + + cpm_add_subdirectory( + "${CPM_ARGS_NAME}" + "${DOWNLOAD_ONLY}" + "${${CPM_ARGS_NAME}_SOURCE_DIR}/${CPM_ARGS_SOURCE_SUBDIR}" + "${${CPM_ARGS_NAME}_BINARY_DIR}" + "${CPM_ARGS_EXCLUDE_FROM_ALL}" + "${CPM_ARGS_SYSTEM}" + "${CPM_ARGS_OPTIONS}" + ) + set(PACKAGE_INFO "${PACKAGE_INFO} at ${download_directory}") + + # As the source dir is already cached/populated, we override the call to FetchContent. + set(CPM_SKIP_FETCH TRUE) + cpm_override_fetchcontent( + "${lower_case_name}" SOURCE_DIR "${${CPM_ARGS_NAME}_SOURCE_DIR}/${CPM_ARGS_SOURCE_SUBDIR}" + BINARY_DIR "${${CPM_ARGS_NAME}_BINARY_DIR}" + ) + + else() + # Enable shallow clone when GIT_TAG is not a commit hash. Our guess may not be accurate, but + # it should guarantee no commit hash get mis-detected. + if(NOT DEFINED CPM_ARGS_GIT_SHALLOW) + cpm_is_git_tag_commit_hash("${CPM_ARGS_GIT_TAG}" IS_HASH) + if(NOT ${IS_HASH}) + list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS GIT_SHALLOW TRUE) + endif() + endif() + + # remove timestamps so CMake will re-download the dependency + file(REMOVE_RECURSE ${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-subbuild) + set(PACKAGE_INFO "${PACKAGE_INFO} to ${download_directory}") + endif() + endif() + + cpm_create_module_file(${CPM_ARGS_NAME} "CPMAddPackage(\"${ARGN}\")") + + if(CPM_PACKAGE_LOCK_ENABLED) + if((CPM_ARGS_VERSION AND NOT CPM_ARGS_SOURCE_DIR) OR CPM_INCLUDE_ALL_IN_PACKAGE_LOCK) + cpm_add_to_package_lock(${CPM_ARGS_NAME} "${ARGN}") + elseif(CPM_ARGS_SOURCE_DIR) + cpm_add_comment_to_package_lock(${CPM_ARGS_NAME} "local directory") + else() + cpm_add_comment_to_package_lock(${CPM_ARGS_NAME} "${ARGN}") + endif() + endif() + + cpm_message( + STATUS "${CPM_INDENT} Adding package ${CPM_ARGS_NAME}@${CPM_ARGS_VERSION} (${PACKAGE_INFO})" + ) + + if(NOT CPM_SKIP_FETCH) + # CMake 3.28 added EXCLUDE, SYSTEM (3.25), and SOURCE_SUBDIR (3.18) to FetchContent_Declare. + # Calling FetchContent_MakeAvailable will then internally forward these options to + # add_subdirectory. Up until these changes, we had to call FetchContent_Populate and + # add_subdirectory separately, which is no longer necessary and has been deprecated as of 3.30. + # A Bug in CMake prevents us to use the non-deprecated functions until 3.30.3. + set(fetchContentDeclareExtraArgs "") + if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.30.3") + if(${CPM_ARGS_EXCLUDE_FROM_ALL}) + list(APPEND fetchContentDeclareExtraArgs EXCLUDE_FROM_ALL) + endif() + if(${CPM_ARGS_SYSTEM}) + list(APPEND fetchContentDeclareExtraArgs SYSTEM) + endif() + if(DEFINED CPM_ARGS_SOURCE_SUBDIR) + list(APPEND fetchContentDeclareExtraArgs SOURCE_SUBDIR ${CPM_ARGS_SOURCE_SUBDIR}) + endif() + # For CMake version <3.28 OPTIONS are parsed in cpm_add_subdirectory + if(CPM_ARGS_OPTIONS AND NOT DOWNLOAD_ONLY) + foreach(OPTION ${CPM_ARGS_OPTIONS}) + cpm_parse_option("${OPTION}") + set(${OPTION_KEY} "${OPTION_VALUE}") + endforeach() + endif() + endif() + cpm_declare_fetch( + "${CPM_ARGS_NAME}" ${fetchContentDeclareExtraArgs} "${CPM_ARGS_UNPARSED_ARGUMENTS}" + ) + + cpm_fetch_package("${CPM_ARGS_NAME}" ${DOWNLOAD_ONLY} populated ${CPM_ARGS_UNPARSED_ARGUMENTS}) + if(CPM_SOURCE_CACHE AND download_directory) + file(LOCK ${download_directory}/../cmake.lock RELEASE) + endif() + if(${populated} AND ${CMAKE_VERSION} VERSION_LESS "3.30.3") + cpm_add_subdirectory( + "${CPM_ARGS_NAME}" + "${DOWNLOAD_ONLY}" + "${${CPM_ARGS_NAME}_SOURCE_DIR}/${CPM_ARGS_SOURCE_SUBDIR}" + "${${CPM_ARGS_NAME}_BINARY_DIR}" + "${CPM_ARGS_EXCLUDE_FROM_ALL}" + "${CPM_ARGS_SYSTEM}" + "${CPM_ARGS_OPTIONS}" + ) + endif() + cpm_get_fetch_properties("${CPM_ARGS_NAME}") + endif() + + set(${CPM_ARGS_NAME}_ADDED YES) + cpm_export_variables("${CPM_ARGS_NAME}") +endfunction() + +# Fetch a previously declared package +macro(CPMGetPackage Name) + if(DEFINED "CPM_DECLARATION_${Name}") + CPMAddPackage(NAME ${Name}) + else() + message(SEND_ERROR "${CPM_INDENT} Cannot retrieve package ${Name}: no declaration available") + endif() +endmacro() + +# export variables available to the caller to the parent scope expects ${CPM_ARGS_NAME} to be set +macro(cpm_export_variables name) + set(${name}_SOURCE_DIR + "${${name}_SOURCE_DIR}" + PARENT_SCOPE + ) + set(${name}_BINARY_DIR + "${${name}_BINARY_DIR}" + PARENT_SCOPE + ) + set(${name}_ADDED + "${${name}_ADDED}" + PARENT_SCOPE + ) + set(CPM_LAST_PACKAGE_NAME + "${name}" + PARENT_SCOPE + ) +endmacro() + +# declares a package, so that any call to CPMAddPackage for the package name will use these +# arguments instead. Previous declarations will not be overridden. +macro(CPMDeclarePackage Name) + if(NOT DEFINED "CPM_DECLARATION_${Name}") + set("CPM_DECLARATION_${Name}" "${ARGN}") + endif() +endmacro() + +function(cpm_add_to_package_lock Name) + if(NOT CPM_DONT_CREATE_PACKAGE_LOCK) + cpm_prettify_package_arguments(PRETTY_ARGN false ${ARGN}) + file(APPEND ${CPM_PACKAGE_LOCK_FILE} "# ${Name}\nCPMDeclarePackage(${Name}\n${PRETTY_ARGN})\n") + endif() +endfunction() + +function(cpm_add_comment_to_package_lock Name) + if(NOT CPM_DONT_CREATE_PACKAGE_LOCK) + cpm_prettify_package_arguments(PRETTY_ARGN true ${ARGN}) + file(APPEND ${CPM_PACKAGE_LOCK_FILE} + "# ${Name} (unversioned)\n# CPMDeclarePackage(${Name}\n${PRETTY_ARGN}#)\n" + ) + endif() +endfunction() + +# includes the package lock file if it exists and creates a target `cpm-update-package-lock` to +# update it +macro(CPMUsePackageLock file) + if(NOT CPM_DONT_CREATE_PACKAGE_LOCK) + get_filename_component(CPM_ABSOLUTE_PACKAGE_LOCK_PATH ${file} ABSOLUTE) + if(EXISTS ${CPM_ABSOLUTE_PACKAGE_LOCK_PATH}) + include(${CPM_ABSOLUTE_PACKAGE_LOCK_PATH}) + endif() + if(NOT TARGET cpm-update-package-lock) + add_custom_target( + cpm-update-package-lock COMMAND ${CMAKE_COMMAND} -E copy ${CPM_PACKAGE_LOCK_FILE} + ${CPM_ABSOLUTE_PACKAGE_LOCK_PATH} + ) + endif() + set(CPM_PACKAGE_LOCK_ENABLED true) + endif() +endmacro() + +# registers a package that has been added to CPM +function(CPMRegisterPackage PACKAGE VERSION) + list(APPEND CPM_PACKAGES ${PACKAGE}) + set(CPM_PACKAGES + ${CPM_PACKAGES} + CACHE INTERNAL "" + ) + set("CPM_PACKAGE_${PACKAGE}_VERSION" + ${VERSION} + CACHE INTERNAL "" + ) +endfunction() + +# retrieve the current version of the package to ${OUTPUT} +function(CPMGetPackageVersion PACKAGE OUTPUT) + set(${OUTPUT} + "${CPM_PACKAGE_${PACKAGE}_VERSION}" + PARENT_SCOPE + ) +endfunction() + +# declares a package in FetchContent_Declare +function(cpm_declare_fetch PACKAGE) + if(${CPM_DRY_RUN}) + cpm_message(STATUS "${CPM_INDENT} Package not declared (dry run)") + return() + endif() + + FetchContent_Declare(${PACKAGE} ${ARGN}) +endfunction() + +# returns properties for a package previously defined by cpm_declare_fetch +function(cpm_get_fetch_properties PACKAGE) + if(${CPM_DRY_RUN}) + return() + endif() + + set(${PACKAGE}_SOURCE_DIR + "${CPM_PACKAGE_${PACKAGE}_SOURCE_DIR}" + PARENT_SCOPE + ) + set(${PACKAGE}_BINARY_DIR + "${CPM_PACKAGE_${PACKAGE}_BINARY_DIR}" + PARENT_SCOPE + ) +endfunction() + +function(cpm_store_fetch_properties PACKAGE source_dir binary_dir) + if(${CPM_DRY_RUN}) + return() + endif() + + set(CPM_PACKAGE_${PACKAGE}_SOURCE_DIR + "${source_dir}" + CACHE INTERNAL "" + ) + set(CPM_PACKAGE_${PACKAGE}_BINARY_DIR + "${binary_dir}" + CACHE INTERNAL "" + ) +endfunction() + +# adds a package as a subdirectory if viable, according to provided options +function( + cpm_add_subdirectory + PACKAGE + DOWNLOAD_ONLY + SOURCE_DIR + BINARY_DIR + EXCLUDE + SYSTEM + OPTIONS +) + + if(NOT DOWNLOAD_ONLY AND EXISTS ${SOURCE_DIR}/CMakeLists.txt) + set(addSubdirectoryExtraArgs "") + if(EXCLUDE) + list(APPEND addSubdirectoryExtraArgs EXCLUDE_FROM_ALL) + endif() + if("${SYSTEM}" AND "${CMAKE_VERSION}" VERSION_GREATER_EQUAL "3.25") + # https://cmake.org/cmake/help/latest/prop_dir/SYSTEM.html#prop_dir:SYSTEM + list(APPEND addSubdirectoryExtraArgs SYSTEM) + endif() + if(OPTIONS) + foreach(OPTION ${OPTIONS}) + cpm_parse_option("${OPTION}") + set(${OPTION_KEY} "${OPTION_VALUE}") + endforeach() + endif() + set(CPM_OLD_INDENT "${CPM_INDENT}") + set(CPM_INDENT "${CPM_INDENT} ${PACKAGE}:") + add_subdirectory(${SOURCE_DIR} ${BINARY_DIR} ${addSubdirectoryExtraArgs}) + set(CPM_INDENT "${CPM_OLD_INDENT}") + endif() +endfunction() + +# downloads a previously declared package via FetchContent and exports the variables +# `${PACKAGE}_SOURCE_DIR` and `${PACKAGE}_BINARY_DIR` to the parent scope +function(cpm_fetch_package PACKAGE DOWNLOAD_ONLY populated) + set(${populated} + FALSE + PARENT_SCOPE + ) + if(${CPM_DRY_RUN}) + cpm_message(STATUS "${CPM_INDENT} Package ${PACKAGE} not fetched (dry run)") + return() + endif() + + FetchContent_GetProperties(${PACKAGE}) + + string(TOLOWER "${PACKAGE}" lower_case_name) + + if(NOT ${lower_case_name}_POPULATED) + if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.30.3") + if(DOWNLOAD_ONLY) + # MakeAvailable will call add_subdirectory internally which is not what we want when + # DOWNLOAD_ONLY is set. Populate will only download the dependency without adding it to the + # build + FetchContent_Populate( + ${PACKAGE} + SOURCE_DIR "${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-src" + BINARY_DIR "${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-build" + SUBBUILD_DIR "${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-subbuild" + ${ARGN} + ) + else() + FetchContent_MakeAvailable(${PACKAGE}) + endif() + else() + FetchContent_Populate(${PACKAGE}) + endif() + set(${populated} + TRUE + PARENT_SCOPE + ) + endif() + + cpm_store_fetch_properties( + ${CPM_ARGS_NAME} ${${lower_case_name}_SOURCE_DIR} ${${lower_case_name}_BINARY_DIR} + ) + + set(${PACKAGE}_SOURCE_DIR + ${${lower_case_name}_SOURCE_DIR} + PARENT_SCOPE + ) + set(${PACKAGE}_BINARY_DIR + ${${lower_case_name}_BINARY_DIR} + PARENT_SCOPE + ) +endfunction() + +# splits a package option +function(cpm_parse_option OPTION) + string(REGEX MATCH "^[^ ]+" OPTION_KEY "${OPTION}") + string(LENGTH "${OPTION}" OPTION_LENGTH) + string(LENGTH "${OPTION_KEY}" OPTION_KEY_LENGTH) + if(OPTION_KEY_LENGTH STREQUAL OPTION_LENGTH) + # no value for key provided, assume user wants to set option to "ON" + set(OPTION_VALUE "ON") + else() + math(EXPR OPTION_KEY_LENGTH "${OPTION_KEY_LENGTH}+1") + string(SUBSTRING "${OPTION}" "${OPTION_KEY_LENGTH}" "-1" OPTION_VALUE) + endif() + set(OPTION_KEY + "${OPTION_KEY}" + PARENT_SCOPE + ) + set(OPTION_VALUE + "${OPTION_VALUE}" + PARENT_SCOPE + ) +endfunction() + +# guesses the package version from a git tag +function(cpm_get_version_from_git_tag GIT_TAG RESULT) + string(LENGTH ${GIT_TAG} length) + if(length EQUAL 40) + # GIT_TAG is probably a git hash + set(${RESULT} + 0 + PARENT_SCOPE + ) + else() + string(REGEX MATCH "v?([0123456789.]*).*" _ ${GIT_TAG}) + set(${RESULT} + ${CMAKE_MATCH_1} + PARENT_SCOPE + ) + endif() +endfunction() + +# guesses if the git tag is a commit hash or an actual tag or a branch name. +function(cpm_is_git_tag_commit_hash GIT_TAG RESULT) + string(LENGTH "${GIT_TAG}" length) + # full hash has 40 characters, and short hash has at least 7 characters. + if(length LESS 7 OR length GREATER 40) + set(${RESULT} + 0 + PARENT_SCOPE + ) + else() + if(${GIT_TAG} MATCHES "^[a-fA-F0-9]+$") + set(${RESULT} + 1 + PARENT_SCOPE + ) + else() + set(${RESULT} + 0 + PARENT_SCOPE + ) + endif() + endif() +endfunction() + +function(cpm_prettify_package_arguments OUT_VAR IS_IN_COMMENT) + set(oneValueArgs + NAME + FORCE + VERSION + GIT_TAG + DOWNLOAD_ONLY + GITHUB_REPOSITORY + GITLAB_REPOSITORY + BITBUCKET_REPOSITORY + GIT_REPOSITORY + SOURCE_DIR + FIND_PACKAGE_ARGUMENTS + NO_CACHE + SYSTEM + GIT_SHALLOW + EXCLUDE_FROM_ALL + SOURCE_SUBDIR + ) + set(multiValueArgs URL OPTIONS DOWNLOAD_COMMAND) + cmake_parse_arguments(CPM_ARGS "" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + foreach(oneArgName ${oneValueArgs}) + if(DEFINED CPM_ARGS_${oneArgName}) + if(${IS_IN_COMMENT}) + string(APPEND PRETTY_OUT_VAR "#") + endif() + if(${oneArgName} STREQUAL "SOURCE_DIR") + string(REPLACE ${CMAKE_SOURCE_DIR} "\${CMAKE_SOURCE_DIR}" CPM_ARGS_${oneArgName} + ${CPM_ARGS_${oneArgName}} + ) + endif() + string(APPEND PRETTY_OUT_VAR " ${oneArgName} ${CPM_ARGS_${oneArgName}}\n") + endif() + endforeach() + foreach(multiArgName ${multiValueArgs}) + if(DEFINED CPM_ARGS_${multiArgName}) + if(${IS_IN_COMMENT}) + string(APPEND PRETTY_OUT_VAR "#") + endif() + string(APPEND PRETTY_OUT_VAR " ${multiArgName}\n") + foreach(singleOption ${CPM_ARGS_${multiArgName}}) + if(${IS_IN_COMMENT}) + string(APPEND PRETTY_OUT_VAR "#") + endif() + string(APPEND PRETTY_OUT_VAR " \"${singleOption}\"\n") + endforeach() + endif() + endforeach() + + if(NOT "${CPM_ARGS_UNPARSED_ARGUMENTS}" STREQUAL "") + if(${IS_IN_COMMENT}) + string(APPEND PRETTY_OUT_VAR "#") + endif() + string(APPEND PRETTY_OUT_VAR " ") + foreach(CPM_ARGS_UNPARSED_ARGUMENT ${CPM_ARGS_UNPARSED_ARGUMENTS}) + string(APPEND PRETTY_OUT_VAR " ${CPM_ARGS_UNPARSED_ARGUMENT}") + endforeach() + string(APPEND PRETTY_OUT_VAR "\n") + endif() + + set(${OUT_VAR} + ${PRETTY_OUT_VAR} + PARENT_SCOPE + ) + +endfunction() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c972cde --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,92 @@ +[build-system] +requires = [ + "conan>=2.0.0", + "setuptools", + "scikit-build>=0.17.3", + "skbuild-conan", + "cmake>=3.23", + "ninja", +] +build-backend = "setuptools.build_meta" + + +[tool.ruff] + +target-version = "py38" +src = ["src"] + +exclude = [] + +[tool.ruff.lint] +select = [ + "E", "F", "W", # flake8 + "B", "B904", # flake8-bugbear + "I", # isort + "ARG", # flake8-unused-arguments + "C4", # flake8-comprehensions + "EM", # flake8-errmsg + "ICN", # flake8-import-conventions + "ISC", # flake8-implicit-str-concat + "PGH", # pygrep-hooks + "PIE", # flake8-pie + "PL", # pylint + "PT", # flake8-pytest-style + "PTH", # flake8-use-pathlib + "RET", # flake8-return + "RUF", # Ruff-specific + "SIM", # flake8-simplify + "TID251", # flake8-tidy-imports.banned-api + "T20", # flake8-print + "UP", # pyupgrade + "YTT", # flake8-2020 +] +extend-ignore = [ + "PLR", # Design rules for pylint + "PLE1205", # Format check doesn't work with our custom logger + "E501", # Line too long + "PT004", # Incorrect, just usefixtures instead. + "RUF009", # Too easy to get a false positive +] +unfixable = ["T20", "F841"] + +[tool.mypy] +files = ["src", "tests"] +mypy_path = ["$MYPY_CONFIG_FILE_DIR/src"] +python_version = "3.10" +warn_unused_configs = true +show_error_codes = true +enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"] +strict = true +disallow_untyped_defs = false + +[[tool.mypy.overrides]] +module = ["algbench.*"] +disallow_untyped_defs = true + +[[tool.mypy.overrides]] +module = ["numpy", "pathspec", "setuptools_scm", "hatch_fancy_pypi_readme"] +ignore_missing_imports = true + + +[tool.pylint] +py-version = "3.7" +jobs = "0" +reports.output-format = "colorized" +similarities.ignore-imports = "yes" +good-names = ["f"] +messages_control.disable = [ + "design", + "fixme", + "import-outside-toplevel", + "invalid-name", + "line-too-long", + "missing-class-docstring", + "missing-function-docstring", + "missing-function-docstring", + "missing-module-docstring", + "wrong-import-position", + "unnecessary-ellipsis", # Conflicts with Protocols + "broad-except", + "unused-argument", # Handled by Ruff + "redefined-builtin", # ExceptionGroup is a builtin +] \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..847e8fb --- /dev/null +++ b/setup.py @@ -0,0 +1,30 @@ +from pathlib import Path + +from setuptools import find_packages +from skbuild import setup + + +def readme(): + """ + :return: Content of README.md + """ + + with Path("README.md").open() as file: + return file.read() + + +setup( # https://scikit-build.readthedocs.io/en/latest/usage.html#setup-options + name="pyaccft", + version="0.0.1", + author="TODO", + license="LICENSE", + description="Pybinding for accft", + long_description=readme(), + long_description_content_type="text/markdown", + packages=find_packages("src"), # Include all packages in `./src`. + package_dir={"": "src"}, # The root for our python package is in `./src`. + python_requires=">=3.10", # lowest python version supported. + install_requires=[ # Python Dependencies + ], + cmake_minimum_required_version="3.23", +) \ No newline at end of file diff --git a/src/algorithms/Refinement.hpp b/src/algorithms/Refinement.hpp index f3750ca..762fbc2 100644 --- a/src/algorithms/Refinement.hpp +++ b/src/algorithms/Refinement.hpp @@ -152,7 +152,7 @@ inline Solution run(Environment const& env, // in if (inst.rows.empty() || env.timer.elapsed() > env.time_limit) break; } - + best_sol.lower_bound = nofix_lb; return best_sol; } diff --git a/src/core/cft.hpp b/src/core/cft.hpp index a83a2c8..e3b3b77 100644 --- a/src/core/cft.hpp +++ b/src/core/cft.hpp @@ -111,6 +111,7 @@ struct CidxAndCost { struct Solution { std::vector idxs; real_t cost; + real_t lower_bound = 0.0; }; // Environment struct to hold all the parameters and working variables diff --git a/src/core/parsing.hpp b/src/core/parsing.hpp index de38c82..7c7e3f6 100644 --- a/src/core/parsing.hpp +++ b/src/core/parsing.hpp @@ -300,4 +300,4 @@ inline FileData parse_inst_and_initsol(Environment const& env) { } // namespace cft -#endif /* CFT_SRC_INSTANCE_PARSING_HPP */ +#endif /* CFT_SRC_INSTANCE_PARSING_HPP */"but their code is in a half-working prototype state and it's completely messy" diff --git a/src/pyaccft/__init__.py b/src/pyaccft/__init__.py new file mode 100644 index 0000000..2f7c9a8 --- /dev/null +++ b/src/pyaccft/__init__.py @@ -0,0 +1,111 @@ +from pathlib import Path +from ._bindings import ( + Environment, + parse_inst_and_initsol, + run, + Instance, + Solution, +) + +__all__ = ["Environment", "parse_inst_and_initsol", "run"] + + +class SetCoverSolver: + def __init__(self, num_elements: int = -1): + self.num_elements = num_elements + self._solution = None + self._instance = Instance() + self._initialized = False + + def add_set(self, elements: list[int], cost: float) -> int: + if cost < 0: + raise ValueError("Costs must be non-negative") + if not all(0 <= e < self.num_elements for e in elements): + raise ValueError("Elements must be in range [0, num_elements)") + self._instance.add(elements, cost) + self._initialized = False + return len(self._instance.costs) - 1 + + def from_file(self, filename: str | Path, parser: str = "RAIL") -> None: + env = Environment() + env.time_limit = 10 + env.verbose = 2 + env.inst_path = str(filename) + env.parser = parser + fdata = parse_inst_and_initsol(env) + self._instance = fdata.inst.copy() + self._solution = fdata.init_sol.copy() + print(self._solution) + self.num_elements = len(self._instance.costs) + self._initialized = True # a file is always considered initialized + + def solve( + self, + seed: int = 0, + time_limit: float = float('inf'), + verbose: int = 2, + epsilon: float = 0.999, + heur_iters: int = 250, + alpha: float = 1.1, + beta: float = 1.0, + abs_subgrad_exit: float = 1.0, + rel_subgrad_exit: float = 0.001, + ) -> None: + """ + Solves the set cover problem using the specified parameters. + + Parameters: + seed (int): Seed for the random number generator. Default is 0. + time_limit (int): Time limit in seconds. Default is 0 (no limit). + verbose (int): Verbosity level. Default is 2. + epsilon (float): Epsilon value for objective comparisons. Default is 0.999. + heur_iters (int): Number of iterations for the heuristic phase. Default is 250. + alpha (float): Relative fixing fraction increment. Default is 1.1. + beta (float): Relative cutoff value to terminate Refinement. Default is 1.0. + abs_subgrad_exit (float): Minimum LBs delta to trigger subgradient termination. Default is 1.0. + rel_subgrad_exit (float): Minimum LBs gap to trigger subgradient termination. Default is 0.001. + use_unit_costs (bool): Solve the given instance setting columns cost to one. Default is False. + + Returns: + None + """ + env = Environment() + env.seed = seed + env.time_limit = time_limit + env.verbose = verbose + env.epsilon = epsilon + env.heur_iters = heur_iters + env.alpha = alpha + env.beta = beta + env.abs_subgrad_exit = abs_subgrad_exit + env.rel_subgrad_exit = rel_subgrad_exit + if not self._initialized: + self._instance.rows.clear() + self._instance.fill_rows_from_cols(self.num_elements) + self._initialized = True + init_sol = Solution() if self._solution is None else self._solution + self._solution = run(env, self._instance, init_sol).copy() + + def get_solution(self) -> list[int] | None: + """ + Return the indices of the selected sets in the solution. + """ + if self._solution is None: + return None + return list(self._solution.idxs) + + def get_objective(self) -> float | None: + """ + Return the cost of the solution. + """ + if self._solution is None: + return None + return self._solution.objective + + def get_lower_bound(self) -> float: + """ + Return a lower bound on the optimal solution. + """ + if self._solution is None: + return 0 + return self._solution.lower_bound diff --git a/src/pyaccft/_bindings.cpp b/src/pyaccft/_bindings.cpp new file mode 100644 index 0000000..ee66124 --- /dev/null +++ b/src/pyaccft/_bindings.cpp @@ -0,0 +1,144 @@ +// pybind11 +#include // To define operator overloading +#include // Basic pybind11 functionality +#include // Automatic conversion of vectors + +// fmt +#include + +#include "../algorithms/Refinement.hpp" +#include "../core/CliArgs.hpp" +#include "../core/Instance.hpp" +#include "../core/cft.hpp" +#include "../core/parsing.hpp" +#include "../utils/print.hpp" + +int _main(int argc, char const** argv) { + + try { + auto env = cft::parse_cli_args(argc, argv); + + cft::print<1>(env, "CFT implementation by Luca Accorsi and Francesco Cavaliere.\n"); + cft::print<2>(env, "Compiled on " __DATE__ " at " __TIME__ ".\n\n"); + cft::print<3>(env, "Running with parameters set to:\n"); + cft::print_arg_values(env); + + auto fdata = cft::parse_inst_and_initsol(env); + auto sol = cft::run(env, fdata.inst, fdata.init_sol); + cft::write_solution(env.sol_path, sol); + cft::print<1>(env, + "CFT> Best solution {:.2f} time {:.2f}s\n", + sol.cost, + env.timer.elapsed()); + + } catch (std::exception const& e) { + fmt::print(stderr, "\nCFT> ERROR: {}\n", e.what()); + std::fflush(stdout); + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} + +// Pybind11 module definitions +PYBIND11_MODULE(_bindings, m) { + namespace py = pybind11; + using namespace cft; + + m.doc() = "Example of PyBind11 and CGAL."; // Optional module docstring + + py::class_(m, "Environment", "The Environment for the accft solver.") + .def(py::init<>()) + .def_readwrite("inst_path", &Environment::inst_path) + .def_readwrite("sol_path", &Environment::sol_path) + .def_readwrite("initsol_path", &Environment::initsol_path) + .def_readwrite("parser", &Environment::parser) + .def_readwrite("seed", &Environment::seed) + .def_readwrite("time_limit", &Environment::time_limit) + .def_readwrite("verbose", &Environment::verbose) + .def_readwrite("epsilon", &Environment::epsilon) + .def_readwrite("heur_iters", &Environment::heur_iters) + .def_readwrite("alpha", &Environment::alpha) + .def_readwrite("beta", &Environment::beta) + .def_readwrite("abs_subgrad_exit", &Environment::abs_subgrad_exit) + .def_readwrite("rel_subgrad_exit", &Environment::rel_subgrad_exit) + .def_readwrite("use_unit_costs", &Environment::use_unit_costs) + .def("__repr__", [](Environment const& a) { + return fmt::format("Environment(inst_path='{}', sol_path='{}', initsol_path='{}', " + "parser={}, " + "seed={}, time_limit={}, verbose={}, epsilon={}, heur_iters={}, " + "alpha={}, beta={}, abs_subgrad_exit={}, rel_subgrad_exit={}, " + "use_unit_costs={})", + a.inst_path, + a.sol_path, + a.initsol_path, + a.parser, + a.seed, + a.time_limit, + a.verbose, + a.epsilon, + a.heur_iters, + a.alpha, + a.beta, + a.abs_subgrad_exit, + a.rel_subgrad_exit, + a.use_unit_costs); + }); + py::class_>(m, "SparseBinMat") + .def(py::init<>()) + .def_readwrite("idxs", &SparseBinMat::idxs) + .def_readwrite("begs", &SparseBinMat::begs) + .def("__getitem__", [](SparseBinMat& self, std::size_t i) { return self[i]; }) + .def("__len__", &SparseBinMat::size) + .def("__repr__", + [](SparseBinMat const& a) { + return fmt::format("SparseBinMat(idxs={}, begs={})", a.idxs, a.begs); + }) + .def("clear", &SparseBinMat::clear) + .def("push_back", + static_cast::*)(std::vector const&)>( + &SparseBinMat::push_back)); + + + py::class_(m, "Instance") + .def(py::init<>()) + .def_readwrite("cols", &Instance::cols) + .def_readwrite("rows", &Instance::rows) + .def_readwrite("costs", &Instance::costs) + .def("add", + [](Instance& self, std::vector const& col, real_t cost) { + self.cols.push_back(col); + self.costs.push_back(cost); + return self.costs.size() - 1; + }) + .def("copy", [](Instance const& a) { return Instance(a); }) + .def("fill_rows_from_cols", [](Instance& self, size_t no_elements) { + self.rows.clear(); + fill_rows_from_cols(self.cols, no_elements, self.rows); + }); + + py::class_(m, "Solution") + .def(py::init<>()) + .def_readwrite("idxs", &Solution::idxs) + .def_readwrite("cost", &Solution::cost) + .def_readwrite("lower_bound", &Solution::lower_bound) + .def("copy", [](Solution const& a) { return Solution(a); }) + .def("__repr__", [](Solution const& a) { + return fmt::format("Solution(idxs={}, cost={}, lower_bound={})", + a.idxs, + a.cost, + a.lower_bound); + }); + + py::class_(m, "FileData") + .def_readwrite("inst", &FileData::inst) + .def_readwrite("init_sol", &FileData::init_sol); + + m.def("parse_inst_and_initsol", + &parse_inst_and_initsol, + "Parse the instance and initial solution."); + + m.def("fill_rows_from_cols", &fill_rows_from_cols, "Fill rows from columns."); + + + m.def("run", &run, "Run the accft solver."); +} \ No newline at end of file From 2d72128d86f3fb4e8de461994fc8df0e8db2460e Mon Sep 17 00:00:00 2001 From: Dominik Krupke Date: Sat, 15 Feb 2025 20:24:52 +0100 Subject: [PATCH 02/20] test: first test case, ci: added python testing ci, feat: renamed objective to cost --- .github/workflows/pytest.yml | 37 ++++++++ .gitignore | 173 ++++++++++++++++++++++++++++++++++- pyproject.toml | 86 +---------------- src/pyaccft/__init__.py | 6 +- test/test_basics.py | 17 ++++ 5 files changed, 230 insertions(+), 89 deletions(-) create mode 100644 .github/workflows/pytest.yml create mode 100644 test/test_basics.py diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml new file mode 100644 index 0000000..cead4fb --- /dev/null +++ b/.github/workflows/pytest.yml @@ -0,0 +1,37 @@ +name: Test on Linux + +on: + push: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10", "3.12"] + + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install flake8 pytest + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + - name: Lint with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: build and install + run: | + pip install --verbose . + - name: Test with pytest + run: | + pytest -s test diff --git a/.gitignore b/.gitignore index a62bd32..1e66c33 100644 --- a/.gitignore +++ b/.gitignore @@ -50,4 +50,175 @@ instances/mps/ex1010-pi.mps TODO.md perf.data* bench.sh -coverage \ No newline at end of file +coverage + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# skbuild_conan +_skbuild/ +.conan/ +**/CMakeUserPresets.json + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# VSCode +.vscode/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/pyproject.toml b/pyproject.toml index c972cde..0f2db66 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,92 +1,8 @@ [build-system] requires = [ - "conan>=2.0.0", "setuptools", "scikit-build>=0.17.3", - "skbuild-conan", "cmake>=3.23", "ninja", ] -build-backend = "setuptools.build_meta" - - -[tool.ruff] - -target-version = "py38" -src = ["src"] - -exclude = [] - -[tool.ruff.lint] -select = [ - "E", "F", "W", # flake8 - "B", "B904", # flake8-bugbear - "I", # isort - "ARG", # flake8-unused-arguments - "C4", # flake8-comprehensions - "EM", # flake8-errmsg - "ICN", # flake8-import-conventions - "ISC", # flake8-implicit-str-concat - "PGH", # pygrep-hooks - "PIE", # flake8-pie - "PL", # pylint - "PT", # flake8-pytest-style - "PTH", # flake8-use-pathlib - "RET", # flake8-return - "RUF", # Ruff-specific - "SIM", # flake8-simplify - "TID251", # flake8-tidy-imports.banned-api - "T20", # flake8-print - "UP", # pyupgrade - "YTT", # flake8-2020 -] -extend-ignore = [ - "PLR", # Design rules for pylint - "PLE1205", # Format check doesn't work with our custom logger - "E501", # Line too long - "PT004", # Incorrect, just usefixtures instead. - "RUF009", # Too easy to get a false positive -] -unfixable = ["T20", "F841"] - -[tool.mypy] -files = ["src", "tests"] -mypy_path = ["$MYPY_CONFIG_FILE_DIR/src"] -python_version = "3.10" -warn_unused_configs = true -show_error_codes = true -enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"] -strict = true -disallow_untyped_defs = false - -[[tool.mypy.overrides]] -module = ["algbench.*"] -disallow_untyped_defs = true - -[[tool.mypy.overrides]] -module = ["numpy", "pathspec", "setuptools_scm", "hatch_fancy_pypi_readme"] -ignore_missing_imports = true - - -[tool.pylint] -py-version = "3.7" -jobs = "0" -reports.output-format = "colorized" -similarities.ignore-imports = "yes" -good-names = ["f"] -messages_control.disable = [ - "design", - "fixme", - "import-outside-toplevel", - "invalid-name", - "line-too-long", - "missing-class-docstring", - "missing-function-docstring", - "missing-function-docstring", - "missing-module-docstring", - "wrong-import-position", - "unnecessary-ellipsis", # Conflicts with Protocols - "broad-except", - "unused-argument", # Handled by Ruff - "redefined-builtin", # ExceptionGroup is a builtin -] \ No newline at end of file +build-backend = "setuptools.build_meta" \ No newline at end of file diff --git a/src/pyaccft/__init__.py b/src/pyaccft/__init__.py index 2f7c9a8..29e6fcc 100644 --- a/src/pyaccft/__init__.py +++ b/src/pyaccft/__init__.py @@ -42,7 +42,7 @@ def from_file(self, filename: str | Path, parser: str = "RAIL") -> None: def solve( self, seed: int = 0, - time_limit: float = float('inf'), + time_limit: float = float("inf"), verbose: int = 2, epsilon: float = 0.999, heur_iters: int = 250, @@ -94,13 +94,13 @@ def get_solution(self) -> list[int] | None: return None return list(self._solution.idxs) - def get_objective(self) -> float | None: + def get_cost(self) -> float | None: """ Return the cost of the solution. """ if self._solution is None: return None - return self._solution.objective + return self._solution.cost def get_lower_bound(self) -> float: """ diff --git a/test/test_basics.py b/test/test_basics.py new file mode 100644 index 0000000..8ddbb6d --- /dev/null +++ b/test/test_basics.py @@ -0,0 +1,17 @@ +import pyaccft + + +def test_simple(): + solver = pyaccft.SetCoverSolver(10) + # solver.from_file("instances/rail/rail507", "RAIL") + solver.add_set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], cost=10) + solver.add_set([0, 1, 2, 3, 4, 5], cost=5) + solver.add_set([0, 1, 2, 3, 4], cost=4) + solver.add_set([6, 7, 8, 9], cost=4) + solver.solve() + solution = solver.get_solution() + assert solution is not None, "No solution found" + assert 1 in solution, "Element 1 not in solution" + assert 3 in solution, "Element 3 not in solution" + assert solver.get_cost() == 9, "Objective value is not 9" + assert solver.get_lower_bound() >= 8.9, "Lower bound is not 9.0" From ffbc274966405b0fc8fb7365aa0fd115cd34d9e1 Mon Sep 17 00:00:00 2001 From: Dominik Krupke Date: Sun, 16 Feb 2025 14:07:47 +0100 Subject: [PATCH 03/20] feat: safe-guarding against wrong usage. --- setup.py | 2 +- src/pyaccft/__init__.py | 29 ++++++++++++++--- src/pyaccft/_bindings.cpp | 67 +++++++++++++++++++++++---------------- test/test_basics.py | 66 +++++++++++++++++++++++++++++++++++++- 4 files changed, 130 insertions(+), 34 deletions(-) diff --git a/setup.py b/setup.py index 847e8fb..3922643 100644 --- a/setup.py +++ b/setup.py @@ -27,4 +27,4 @@ def readme(): install_requires=[ # Python Dependencies ], cmake_minimum_required_version="3.23", -) \ No newline at end of file +) diff --git a/src/pyaccft/__init__.py b/src/pyaccft/__init__.py index 29e6fcc..10cfea0 100644 --- a/src/pyaccft/__init__.py +++ b/src/pyaccft/__init__.py @@ -11,22 +11,39 @@ class SetCoverSolver: - def __init__(self, num_elements: int = -1): - self.num_elements = num_elements + """ + Defines a Set Cover Solver object that will solve the set cover problem using the CFT heuristic. + """ + + def __init__(self): self._solution = None self._instance = Instance() self._initialized = False + self._max_element = -1 def add_set(self, elements: list[int], cost: float) -> int: + """ + Add a set. Returns the index of the set which is also used in the solution. + """ if cost < 0: raise ValueError("Costs must be non-negative") - if not all(0 <= e < self.num_elements for e in elements): - raise ValueError("Elements must be in range [0, num_elements)") + if not all(0 <= e for e in elements): + raise ValueError("Elements must be zero-indexed.") + max_element = max(elements) + if max_element > self._max_element: + self._max_element = max_element + self._solution = ( + None # in case there is a new element, the solution is no longer + ) + # valid self._instance.add(elements, cost) self._initialized = False return len(self._instance.costs) - 1 def from_file(self, filename: str | Path, parser: str = "RAIL") -> None: + """ + Load an instance from a file. + """ env = Environment() env.time_limit = 10 env.verbose = 2 @@ -50,6 +67,7 @@ def solve( beta: float = 1.0, abs_subgrad_exit: float = 1.0, rel_subgrad_exit: float = 0.001, + min_fixing=0.3, ) -> None: """ Solves the set cover problem using the specified parameters. @@ -79,9 +97,10 @@ def solve( env.beta = beta env.abs_subgrad_exit = abs_subgrad_exit env.rel_subgrad_exit = rel_subgrad_exit + env.min_fixing = min_fixing if not self._initialized: self._instance.rows.clear() - self._instance.fill_rows_from_cols(self.num_elements) + self._instance.prepare() self._initialized = True init_sol = Solution() if self._solution is None else self._solution self._solution = run(env, self._instance, init_sol).copy() diff --git a/src/pyaccft/_bindings.cpp b/src/pyaccft/_bindings.cpp index ee66124..f08ab9b 100644 --- a/src/pyaccft/_bindings.cpp +++ b/src/pyaccft/_bindings.cpp @@ -6,6 +6,8 @@ // fmt #include +#include + #include "../algorithms/Refinement.hpp" #include "../core/CliArgs.hpp" #include "../core/Instance.hpp" @@ -13,30 +15,40 @@ #include "../core/parsing.hpp" #include "../utils/print.hpp" -int _main(int argc, char const** argv) { - - try { - auto env = cft::parse_cli_args(argc, argv); - - cft::print<1>(env, "CFT implementation by Luca Accorsi and Francesco Cavaliere.\n"); - cft::print<2>(env, "Compiled on " __DATE__ " at " __TIME__ ".\n\n"); - cft::print<3>(env, "Running with parameters set to:\n"); - cft::print_arg_values(env); - - auto fdata = cft::parse_inst_and_initsol(env); - auto sol = cft::run(env, fdata.inst, fdata.init_sol); - cft::write_solution(env.sol_path, sol); - cft::print<1>(env, - "CFT> Best solution {:.2f} time {:.2f}s\n", - sol.cost, - env.timer.elapsed()); - - } catch (std::exception const& e) { - fmt::print(stderr, "\nCFT> ERROR: {}\n", e.what()); - std::fflush(stdout); - return EXIT_FAILURE; +void check_and_fill_instance(cft::Instance& instance) { + auto& cols = instance.cols; + auto& rows = instance.rows; + using namespace cft; + size_t n = 0; + + // Determine the maximum index in cols + for (cidx_t j = 0_C; j < csize(cols); ++j) + for (ridx_t i : cols[j]) + n = std::max(n, static_cast(i + 1)); + + // Guard against the user entering huge indices because they + // might have misunderstood the 0-based indexing. + if (n > cols.idxs.size()) { + throw std::runtime_error(fmt::format("Item index out of bounds. Maximum index is {} but " + "size is {}. Please make sure that the n items are " + "indexed from 0 to n-1.", + n, + cols.idxs.size())); } - return EXIT_SUCCESS; + + // Check if every element (row) is in at least one column + std::vector in_col(n, false); + // Mark every element that is in a column (set) + for (cidx_t j = 0_C; j < csize(cols); ++j) + for (ridx_t i : cols[j]) + in_col[i] = true; + // Check if every element is in at least one column + for (size_t i = 0; i < n; ++i) + if (!in_col[i]) + throw std::runtime_error(fmt::format("Item {} not contained in any set.", i)); + + // Fill rows from columns + fill_rows_from_cols(cols, n, rows); } // Pybind11 module definitions @@ -62,6 +74,7 @@ PYBIND11_MODULE(_bindings, m) { .def_readwrite("abs_subgrad_exit", &Environment::abs_subgrad_exit) .def_readwrite("rel_subgrad_exit", &Environment::rel_subgrad_exit) .def_readwrite("use_unit_costs", &Environment::use_unit_costs) + .def_readwrite("min_fixing", &Environment::min_fixing) .def("__repr__", [](Environment const& a) { return fmt::format("Environment(inst_path='{}', sol_path='{}', initsol_path='{}', " "parser={}, " @@ -107,14 +120,14 @@ PYBIND11_MODULE(_bindings, m) { .def("add", [](Instance& self, std::vector const& col, real_t cost) { self.cols.push_back(col); + if (cost < 0.0) { + throw std::runtime_error("Costs must be non-negative."); + } self.costs.push_back(cost); return self.costs.size() - 1; }) .def("copy", [](Instance const& a) { return Instance(a); }) - .def("fill_rows_from_cols", [](Instance& self, size_t no_elements) { - self.rows.clear(); - fill_rows_from_cols(self.cols, no_elements, self.rows); - }); + .def("prepare", [](Instance& self) { check_and_fill_instance(self); }); py::class_(m, "Solution") .def(py::init<>()) diff --git a/test/test_basics.py b/test/test_basics.py index 8ddbb6d..c849a4e 100644 --- a/test/test_basics.py +++ b/test/test_basics.py @@ -1,8 +1,9 @@ import pyaccft +import pytest def test_simple(): - solver = pyaccft.SetCoverSolver(10) + solver = pyaccft.SetCoverSolver() # solver.from_file("instances/rail/rail507", "RAIL") solver.add_set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], cost=10) solver.add_set([0, 1, 2, 3, 4, 5], cost=5) @@ -15,3 +16,66 @@ def test_simple(): assert 3 in solution, "Element 3 not in solution" assert solver.get_cost() == 9, "Objective value is not 9" assert solver.get_lower_bound() >= 8.9, "Lower bound is not 9.0" + + +def test_simple_infeasible(): + solver = pyaccft.SetCoverSolver() + # solver.from_file("instances/rail/rail507", "RAIL") + solver.add_set([0, 1, 2, 3, 4, 5, 6, 8, 9], cost=10) + solver.add_set([0, 1, 2, 3, 4, 5], cost=5) + solver.add_set([0, 1, 2, 3, 4], cost=4) + solver.add_set([6, 8, 9], cost=4) + with pytest.raises(RuntimeError): + solver.solve() + + +def test_error_on_negative_cost(): + solver = pyaccft.SetCoverSolver() + with pytest.raises(ValueError): + solver.add_set([0, 1, 2, 3, 4, 5, 6, 8, 9], cost=-10) + + +def test_error_on_negative_element(): + solver = pyaccft.SetCoverSolver() + with pytest.raises(ValueError): + solver.add_set([-1, 1, 2, 3, 4, 5, 6, 8, 9], cost=10) + + +def test_simple_incremental(): + solver = pyaccft.SetCoverSolver() + # solver.from_file("instances/rail/rail507", "RAIL") + solver.add_set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], cost=10) + solver.add_set([0, 1, 2, 3, 4, 5], cost=5) + solver.add_set([0, 1, 2, 3, 4], cost=4) + solver.add_set([6, 7, 8, 9], cost=4) + solver.solve() + solution = solver.get_solution() + assert solution is not None, "No solution found" + assert 1 in solution, "Element 1 not in solution" + assert 3 in solution, "Element 3 not in solution" + assert solver.get_cost() == 9, "Objective value is not 9" + assert solver.get_lower_bound() >= 8.9, "Lower bound is not 9.0" + solver.add_set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], cost=5) + solver.solve() + solution = solver.get_solution() + assert solver.get_cost() == 5, "Objective value is not 5" + + +def test_simple_incremental_new_element(): + solver = pyaccft.SetCoverSolver() + # solver.from_file("instances/rail/rail507", "RAIL") + solver.add_set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], cost=10) + solver.add_set([0, 1, 2, 3, 4, 5], cost=5) + solver.add_set([0, 1, 2, 3, 4], cost=4) + solver.add_set([6, 7, 8, 9], cost=4) + solver.solve() + solution = solver.get_solution() + assert solution is not None, "No solution found" + assert 1 in solution, "Element 1 not in solution" + assert 3 in solution, "Element 3 not in solution" + assert solver.get_cost() == 9, "Objective value is not 9" + assert solver.get_lower_bound() >= 8.9, "Lower bound is not 9.0" + solver.add_set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], cost=15) + solver.solve() + solution = solver.get_solution() + assert solver.get_cost() == 15, "Objective value is not 15" From 842e761c7b668dad5e19335e32abf69a2fa9e7c0 Mon Sep 17 00:00:00 2001 From: Dominik Krupke Date: Sun, 16 Feb 2025 14:13:44 +0100 Subject: [PATCH 04/20] chores: refactored the package and improved documentation --- src/pyaccft/__init__.py | 131 +------------------------- src/pyaccft/set_cover_solver.py | 157 ++++++++++++++++++++++++++++++++ 2 files changed, 159 insertions(+), 129 deletions(-) create mode 100644 src/pyaccft/set_cover_solver.py diff --git a/src/pyaccft/__init__.py b/src/pyaccft/__init__.py index 10cfea0..b2636df 100644 --- a/src/pyaccft/__init__.py +++ b/src/pyaccft/__init__.py @@ -1,130 +1,3 @@ -from pathlib import Path -from ._bindings import ( - Environment, - parse_inst_and_initsol, - run, - Instance, - Solution, -) +from .set_cover_solver import SetCoverSolver -__all__ = ["Environment", "parse_inst_and_initsol", "run"] - - -class SetCoverSolver: - """ - Defines a Set Cover Solver object that will solve the set cover problem using the CFT heuristic. - """ - - def __init__(self): - self._solution = None - self._instance = Instance() - self._initialized = False - self._max_element = -1 - - def add_set(self, elements: list[int], cost: float) -> int: - """ - Add a set. Returns the index of the set which is also used in the solution. - """ - if cost < 0: - raise ValueError("Costs must be non-negative") - if not all(0 <= e for e in elements): - raise ValueError("Elements must be zero-indexed.") - max_element = max(elements) - if max_element > self._max_element: - self._max_element = max_element - self._solution = ( - None # in case there is a new element, the solution is no longer - ) - # valid - self._instance.add(elements, cost) - self._initialized = False - return len(self._instance.costs) - 1 - - def from_file(self, filename: str | Path, parser: str = "RAIL") -> None: - """ - Load an instance from a file. - """ - env = Environment() - env.time_limit = 10 - env.verbose = 2 - env.inst_path = str(filename) - env.parser = parser - fdata = parse_inst_and_initsol(env) - self._instance = fdata.inst.copy() - self._solution = fdata.init_sol.copy() - print(self._solution) - self.num_elements = len(self._instance.costs) - self._initialized = True # a file is always considered initialized - - def solve( - self, - seed: int = 0, - time_limit: float = float("inf"), - verbose: int = 2, - epsilon: float = 0.999, - heur_iters: int = 250, - alpha: float = 1.1, - beta: float = 1.0, - abs_subgrad_exit: float = 1.0, - rel_subgrad_exit: float = 0.001, - min_fixing=0.3, - ) -> None: - """ - Solves the set cover problem using the specified parameters. - - Parameters: - seed (int): Seed for the random number generator. Default is 0. - time_limit (int): Time limit in seconds. Default is 0 (no limit). - verbose (int): Verbosity level. Default is 2. - epsilon (float): Epsilon value for objective comparisons. Default is 0.999. - heur_iters (int): Number of iterations for the heuristic phase. Default is 250. - alpha (float): Relative fixing fraction increment. Default is 1.1. - beta (float): Relative cutoff value to terminate Refinement. Default is 1.0. - abs_subgrad_exit (float): Minimum LBs delta to trigger subgradient termination. Default is 1.0. - rel_subgrad_exit (float): Minimum LBs gap to trigger subgradient termination. Default is 0.001. - use_unit_costs (bool): Solve the given instance setting columns cost to one. Default is False. - - Returns: - None - """ - env = Environment() - env.seed = seed - env.time_limit = time_limit - env.verbose = verbose - env.epsilon = epsilon - env.heur_iters = heur_iters - env.alpha = alpha - env.beta = beta - env.abs_subgrad_exit = abs_subgrad_exit - env.rel_subgrad_exit = rel_subgrad_exit - env.min_fixing = min_fixing - if not self._initialized: - self._instance.rows.clear() - self._instance.prepare() - self._initialized = True - init_sol = Solution() if self._solution is None else self._solution - self._solution = run(env, self._instance, init_sol).copy() - - def get_solution(self) -> list[int] | None: - """ - Return the indices of the selected sets in the solution. - """ - if self._solution is None: - return None - return list(self._solution.idxs) - - def get_cost(self) -> float | None: - """ - Return the cost of the solution. - """ - if self._solution is None: - return None - return self._solution.cost - - def get_lower_bound(self) -> float: - """ - Return a lower bound on the optimal solution. - """ - if self._solution is None: - return 0 - return self._solution.lower_bound +__all__ = ["SetCoverSolver"] \ No newline at end of file diff --git a/src/pyaccft/set_cover_solver.py b/src/pyaccft/set_cover_solver.py new file mode 100644 index 0000000..e289154 --- /dev/null +++ b/src/pyaccft/set_cover_solver.py @@ -0,0 +1,157 @@ +from pathlib import Path +from ._bindings import ( + Environment, + parse_inst_and_initsol, + run, + Instance, + Solution, +) + + +class SetCoverSolver: + """ + Defines a Set Cover Solver object that will solve the set cover problem using the CFT heuristic. + The solver can be used incrementally by adding sets and then calling the solve method. + If no new elements are added, the solver will use the previous solution as a starting point. + + All elements must be zero-indexed. As we are working with indices, all elements between 0 and the maximum + element in the sets must appear in one of the sets for a feasible solution. If an element is not in any set, + it will be considered as not covered and the solution will be infeasible. + + The sets are also indexed starting from 0. The index of a set is returned when adding a set and can be used + to retrieve the solution. + + Example: + ```python + from pyaccft import SetCoverSolver + + solver = SetCoverSolver() + # Set 0 + solver.add_set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], cost=10) + # Set 1 + solver.add_set([0, 1, 2, 3, 4, 5], cost=5) + # Set 2 + solver.add_set([0, 1, 2, 3, 4], cost=4) + # Set 3 + solver.add_set([6, 7, 8, 9], cost=4) + solver.solve() + solution = solver.get_solution() + print("The following sets have been selected:", solution) + print("The cost of the solution is:", solver.get_cost()) + print("The lower bound of the solution is:", solver.get_lower_bound()) + ``` + """ + + def __init__(self): + self._solution = None + self._instance = Instance() + self._initialized = False + self._max_element = -1 + + def add_set(self, elements: list[int], cost: float) -> int: + """ + Add a set. Returns the index of the set which is also used in the solution. + """ + if cost < 0: + raise ValueError("Costs must be non-negative") + if not all(0 <= e for e in elements): + raise ValueError("Elements must be zero-indexed.") + max_element = max(elements) + if max_element > self._max_element: + self._max_element = max_element + self._solution = ( + None # in case there is a new element, the solution is no longer + ) + # valid + self._instance.add(elements, cost) + self._initialized = False + return len(self._instance.costs) - 1 + + def from_file(self, filename: str | Path, parser: str = "RAIL") -> None: + """ + Load an instance from a file. + """ + env = Environment() + env.time_limit = 10 + env.verbose = 2 + env.inst_path = str(filename) + env.parser = parser + fdata = parse_inst_and_initsol(env) + self._instance = fdata.inst.copy() + self._solution = fdata.init_sol.copy() + print(self._solution) + self.num_elements = len(self._instance.costs) + self._initialized = True # a file is always considered initialized + + def solve( + self, + seed: int = 0, + time_limit: float = float("inf"), + verbose: int = 2, + epsilon: float = 0.999, + heur_iters: int = 250, + alpha: float = 1.1, + beta: float = 1.0, + abs_subgrad_exit: float = 1.0, + rel_subgrad_exit: float = 0.001, + min_fixing=0.3, + ) -> None: + """ + Solves the set cover problem using the specified parameters. + + Parameters: + seed (int): Seed for the random number generator. Default is 0. + time_limit (int): Time limit in seconds. Default is 0 (no limit). + verbose (int): Verbosity level. Default is 2. + epsilon (float): Epsilon value for objective comparisons. Default is 0.999. + heur_iters (int): Number of iterations for the heuristic phase. Default is 250. + alpha (float): Relative fixing fraction increment. Default is 1.1. + beta (float): Relative cutoff value to terminate Refinement. Default is 1.0. + abs_subgrad_exit (float): Minimum LBs delta to trigger subgradient termination. Default is 1.0. + rel_subgrad_exit (float): Minimum LBs gap to trigger subgradient termination. Default is 0.001. + use_unit_costs (bool): Solve the given instance setting columns cost to one. Default is False. + + Returns: + None + """ + env = Environment() + env.seed = seed + env.time_limit = time_limit + env.verbose = verbose + env.epsilon = epsilon + env.heur_iters = heur_iters + env.alpha = alpha + env.beta = beta + env.abs_subgrad_exit = abs_subgrad_exit + env.rel_subgrad_exit = rel_subgrad_exit + env.min_fixing = min_fixing + if not self._initialized: + self._instance.rows.clear() + self._instance.prepare() + self._initialized = True + init_sol = Solution() if self._solution is None else self._solution + self._solution = run(env, self._instance, init_sol).copy() + + def get_solution(self) -> list[int] | None: + """ + Return the indices of the selected sets in the solution. + """ + if self._solution is None: + return None + return list(self._solution.idxs) + + def get_cost(self) -> float | None: + """ + Return the cost of the solution. + """ + if self._solution is None: + return None + return self._solution.cost + + def get_lower_bound(self) -> float: + """ + Return a lower bound on the optimal solution. + """ + if self._solution is None: + return 0 + return self._solution.lower_bound From f8a8527aba868de5e2c4314c32ed65fef0b60fad Mon Sep 17 00:00:00 2001 From: Dominik Krupke Date: Sun, 16 Feb 2025 14:17:05 +0100 Subject: [PATCH 05/20] chores: Sepearate readme for the python bindings. --- README.py.md | 33 +++++++++++++++++++++++++++++++++ setup.py | 2 +- 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 README.py.md diff --git a/README.py.md b/README.py.md new file mode 100644 index 0000000..7a0878f --- /dev/null +++ b/README.py.md @@ -0,0 +1,33 @@ +# Python Bindings for the AC-CFT Set Cover Heuristic + +## Install + +TODO + +## Usage + +To use the `SetCoverSolver`, first create an instance of the solver. You can then add sets with their respective costs and solve the set cover problem. The solver will find the optimal selection of sets that covers all elements at the minimum cost. + +Here is an example: + +```python +from pyaccft import SetCoverSolver + +solver = SetCoverSolver() +# Add sets with their respective costs +solver.add_set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], cost=10) +solver.add_set([0, 1, 2, 3, 4, 5], cost=5) +solver.add_set([0, 1, 2, 3, 4], cost=4) +solver.add_set([6, 7, 8, 9], cost=4) + +# Solve the set cover problem +solver.solve() + +# Retrieve and print the solution +solution = solver.get_solution() +print("The following sets have been selected:", solution) +print("The cost of the solution is:", solver.get_cost()) +print("The lower bound of the solution is:", solver.get_lower_bound()) +``` + +Ensure that all elements are zero-indexed and that every element between 0 and the maximum element appears in at least one set for a feasible solution. \ No newline at end of file diff --git a/setup.py b/setup.py index 3922643..c762fca 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ def readme(): :return: Content of README.md """ - with Path("README.md").open() as file: + with Path("README.py.md").open() as file: return file.read() From 352b0307b762d29f70c62715eee91a03ce67e3c1 Mon Sep 17 00:00:00 2001 From: Dominik Krupke Date: Sun, 16 Feb 2025 14:36:37 +0100 Subject: [PATCH 06/20] fix: only building python module when skbuild is set --- CMakeLists.txt | 47 +++++++++++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0c63354..b3c26be 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,28 +28,31 @@ add_executable(accft ${SOURCE}) set(LIBRARIES fmt::fmt pthread dl m) target_link_libraries(accft PUBLIC ${LIBRARIES}) -# CPM Dependencies ~~~~~~~~~~~~~~~~ CPM allows us to include some simple -# dependencies without much fuss. Great default, for everything more -# complicated, uses conan. Why not always use conan? CPM works without any -# interaction (conan needs to be called externally). CPM also has no problems -# with install PyBind11 (which conan has at the time). If the dependency has -# complicated dependencies on its own or is slow to compile, better use conan. -# Check out https://github.com/cpm-cmake/CPM.cmake -include(./cmake/CPM.cmake) # Package manager for simple requirements. - -# PyBind11 -cpmaddpackage("gh:pybind/pybind11@2.10.0") # pybind11, essential -set(CMAKE_POSITION_INDEPENDENT_CODE ON) # The code needs to be compiled as PIC - # to build the shared lib for python. -set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) - -pybind11_add_module(_bindings ./src/pyaccft/_bindings.cpp) -target_link_libraries(_bindings PUBLIC fmt::fmt ${LIBRARIES}) -# enable compilation warnings -target_compile_options( - _bindings PRIVATE "$<$:-Wall>") -target_compile_definitions(_bindings PRIVATE PYBIND11_DETAILED_ERROR_MESSAGES) -install(TARGETS _bindings DESTINATION ./src/pyaccft/) +if (SKBUILD) + # CPM Dependencies ~~~~~~~~~~~~~~~~ + # CPM allows us to include some simple + # dependencies without much fuss. Great default, for everything more + # complicated, uses conan. Why not always use conan? CPM works without any + # interaction (conan needs to be called externally). CPM also has no problems + # with install PyBind11 (which conan has at the time). If the dependency has + # complicated dependencies on its own or is slow to compile, better use conan. + # Check out https://github.com/cpm-cmake/CPM.cmake + include(./cmake/CPM.cmake) # Package manager for simple requirements. + + # PyBind11 + cpmaddpackage("gh:pybind/pybind11@2.10.0") # pybind11, essential + set(CMAKE_POSITION_INDEPENDENT_CODE ON) # The code needs to be compiled as PIC + # to build the shared lib for python. + set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) + + pybind11_add_module(_bindings ./src/pyaccft/_bindings.cpp) + target_link_libraries(_bindings PUBLIC fmt::fmt ${LIBRARIES}) + # enable compilation warnings + target_compile_options( + _bindings PRIVATE "$<$:-Wall>") + target_compile_definitions(_bindings PRIVATE PYBIND11_DETAILED_ERROR_MESSAGES) + install(TARGETS _bindings DESTINATION ./src/pyaccft/) +endif() ######################################## ############## Unit tests ############## From e9e1660eb394ce53460cfc25f5b9159337316826 Mon Sep 17 00:00:00 2001 From: Dominik Krupke Date: Sun, 16 Feb 2025 14:48:08 +0100 Subject: [PATCH 07/20] fix: Trying to fix the solution handling --- src/core/cft.hpp | 7 ++++++- test/cft_unittests.cpp | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/core/cft.hpp b/src/core/cft.hpp index e3b3b77..886f798 100644 --- a/src/core/cft.hpp +++ b/src/core/cft.hpp @@ -111,7 +111,12 @@ struct CidxAndCost { struct Solution { std::vector idxs; real_t cost; - real_t lower_bound = 0.0; + real_t lower_bound = 0.0_F; + + Solution() = default; + Solution(std::vector idxs, real_t cost, real_t lower_bound = 0.0_F) + : idxs(std::move(idxs)), cost(cost), lower_bound(lower_bound) {} + }; // Environment struct to hold all the parameters and working variables diff --git a/test/cft_unittests.cpp b/test/cft_unittests.cpp index 3cbe843..10c28f4 100644 --- a/test/cft_unittests.cpp +++ b/test/cft_unittests.cpp @@ -17,7 +17,7 @@ TEST_CASE("Test CidxAndCost struct") { } TEST_CASE("Test Solution struct") { - auto s = Solution{{}, 0.0_F}; + auto s = Solution{{}, 0.0_F, 0.0_F}; CHECK(s.idxs.empty()); CHECK(s.cost == 0.0_F); } From 22f44b86fb6b21cd5dcdfd73740555e96e9cb1bc Mon Sep 17 00:00:00 2001 From: Dominik Krupke Date: Sun, 16 Feb 2025 14:54:32 +0100 Subject: [PATCH 08/20] fix: SPDX stuff --- README.py.md | 5 +++++ pyproject.toml | 3 +++ setup.py | 3 +++ src/pyaccft/__init__.py | 3 +++ src/pyaccft/_bindings.cpp | 3 +++ src/pyaccft/set_cover_solver.py | 5 ++++- 6 files changed, 21 insertions(+), 1 deletion(-) diff --git a/README.py.md b/README.py.md index 7a0878f..13b6d5d 100644 --- a/README.py.md +++ b/README.py.md @@ -1,3 +1,8 @@ + + # Python Bindings for the AC-CFT Set Cover Heuristic ## Install diff --git a/pyproject.toml b/pyproject.toml index 0f2db66..c574afe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2024 Dominik Krupke +# SPDX-License-Identifier: MIT + [build-system] requires = [ "setuptools", diff --git a/setup.py b/setup.py index c762fca..661ba04 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2024 Dominik Krupke +# SPDX-License-Identifier: MIT + from pathlib import Path from setuptools import find_packages diff --git a/src/pyaccft/__init__.py b/src/pyaccft/__init__.py index b2636df..b2b212e 100644 --- a/src/pyaccft/__init__.py +++ b/src/pyaccft/__init__.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2024 Dominik Krupke +# SPDX-License-Identifier: MIT + from .set_cover_solver import SetCoverSolver __all__ = ["SetCoverSolver"] \ No newline at end of file diff --git a/src/pyaccft/_bindings.cpp b/src/pyaccft/_bindings.cpp index f08ab9b..7e9fbab 100644 --- a/src/pyaccft/_bindings.cpp +++ b/src/pyaccft/_bindings.cpp @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2024 Dominik Krupke +// SPDX-License-Identifier: MIT + // pybind11 #include // To define operator overloading #include // Basic pybind11 functionality diff --git a/src/pyaccft/set_cover_solver.py b/src/pyaccft/set_cover_solver.py index e289154..02cd3a4 100644 --- a/src/pyaccft/set_cover_solver.py +++ b/src/pyaccft/set_cover_solver.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2024 Dominik Krupke +# SPDX-License-Identifier: MIT + from pathlib import Path from ._bindings import ( Environment, @@ -24,7 +27,7 @@ class SetCoverSolver: Example: ```python from pyaccft import SetCoverSolver - + solver = SetCoverSolver() # Set 0 solver.add_set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], cost=10) From 1835d2daef8dc2bc2d42e35eb7e313340db71039 Mon Sep 17 00:00:00 2001 From: Dominik Krupke Date: Sun, 16 Feb 2025 14:57:04 +0100 Subject: [PATCH 09/20] fix: SPDX stuff --- README.py.md | 2 +- cmake/CPM.cmake | 2 ++ pyproject.toml | 2 +- setup.py | 2 +- src/pyaccft/__init__.py | 2 +- src/pyaccft/_bindings.cpp | 2 +- src/pyaccft/set_cover_solver.py | 2 +- 7 files changed, 8 insertions(+), 6 deletions(-) diff --git a/README.py.md b/README.py.md index 13b6d5d..f00b582 100644 --- a/README.py.md +++ b/README.py.md @@ -1,5 +1,5 @@ diff --git a/cmake/CPM.cmake b/cmake/CPM.cmake index 80e6896..35f9912 100644 --- a/cmake/CPM.cmake +++ b/cmake/CPM.cmake @@ -1,3 +1,5 @@ +# SPDX-FileCopyrightText: 2019-2023 Lars Melchior and contributors +# SPDX-License-Identifier: MIT # CPM.cmake - CMake's missing package manager # =========================================== # See https://github.com/cpm-cmake/CPM.cmake for usage and update instructions. diff --git a/pyproject.toml b/pyproject.toml index c574afe..35cee52 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2024 Dominik Krupke +# SPDX-FileCopyrightText: 2025 Dominik Krupke # SPDX-License-Identifier: MIT [build-system] diff --git a/setup.py b/setup.py index 661ba04..c300e55 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2024 Dominik Krupke +# SPDX-FileCopyrightText: 2025 Dominik Krupke # SPDX-License-Identifier: MIT from pathlib import Path diff --git a/src/pyaccft/__init__.py b/src/pyaccft/__init__.py index b2b212e..36bcc75 100644 --- a/src/pyaccft/__init__.py +++ b/src/pyaccft/__init__.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2024 Dominik Krupke +# SPDX-FileCopyrightText: 2025 Dominik Krupke # SPDX-License-Identifier: MIT from .set_cover_solver import SetCoverSolver diff --git a/src/pyaccft/_bindings.cpp b/src/pyaccft/_bindings.cpp index 7e9fbab..25af997 100644 --- a/src/pyaccft/_bindings.cpp +++ b/src/pyaccft/_bindings.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2024 Dominik Krupke +// SPDX-FileCopyrightText: 2025 Dominik Krupke // SPDX-License-Identifier: MIT // pybind11 diff --git a/src/pyaccft/set_cover_solver.py b/src/pyaccft/set_cover_solver.py index 02cd3a4..e75a43e 100644 --- a/src/pyaccft/set_cover_solver.py +++ b/src/pyaccft/set_cover_solver.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2024 Dominik Krupke +# SPDX-FileCopyrightText: 2025 Dominik Krupke # SPDX-License-Identifier: MIT from pathlib import Path From e0bbfae51de476389ee8332abacab37306ccc7ce Mon Sep 17 00:00:00 2001 From: Dominik Krupke Date: Sun, 16 Feb 2025 14:59:50 +0100 Subject: [PATCH 10/20] fix: SPDX stuff --- test/test_basics.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/test_basics.py b/test/test_basics.py index c849a4e..65ac226 100644 --- a/test/test_basics.py +++ b/test/test_basics.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2025 Dominik Krupke +# SPDX-License-Identifier: MIT + import pyaccft import pytest From 5c953b1e56a991932aab74749e8eb74cb0cb67b5 Mon Sep 17 00:00:00 2001 From: Dominik Krupke Date: Sun, 16 Feb 2025 15:05:59 +0100 Subject: [PATCH 11/20] fix: trying to fix compilation issue with fpic and fmt --- CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index b3c26be..49610b9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,6 +44,8 @@ if (SKBUILD) set(CMAKE_POSITION_INDEPENDENT_CODE ON) # The code needs to be compiled as PIC # to build the shared lib for python. set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) + # Ensure fmt is compiled with -fPIC + set_target_properties(fmt PROPERTIES POSITION_INDEPENDENT_CODE ON) pybind11_add_module(_bindings ./src/pyaccft/_bindings.cpp) target_link_libraries(_bindings PUBLIC fmt::fmt ${LIBRARIES}) From fbcf854e50ea31d2cb04799116621cd4f4a9325b Mon Sep 17 00:00:00 2001 From: Dominik Krupke Date: Sun, 16 Feb 2025 15:08:25 +0100 Subject: [PATCH 12/20] fix: getting rid of the stupid shadowing warnings --- src/core/cft.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/cft.hpp b/src/core/cft.hpp index 886f798..36d19d6 100644 --- a/src/core/cft.hpp +++ b/src/core/cft.hpp @@ -114,8 +114,8 @@ struct Solution { real_t lower_bound = 0.0_F; Solution() = default; - Solution(std::vector idxs, real_t cost, real_t lower_bound = 0.0_F) - : idxs(std::move(idxs)), cost(cost), lower_bound(lower_bound) {} + Solution(std::vector idxs_, real_t cost_, real_t lower_bound_ = 0.0_F) + : idxs(std::move(idxs_)), cost(cost_), lower_bound(lower_bound_) {} }; From 7e2d42a4e27d01ebc491348cbca37d8e9d6be6c3 Mon Sep 17 00:00:00 2001 From: Dominik Krupke Date: Sun, 16 Feb 2025 15:55:59 +0100 Subject: [PATCH 13/20] fix: removing badly copied comment --- src/core/parsing.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/parsing.hpp b/src/core/parsing.hpp index 7c7e3f6..de38c82 100644 --- a/src/core/parsing.hpp +++ b/src/core/parsing.hpp @@ -300,4 +300,4 @@ inline FileData parse_inst_and_initsol(Environment const& env) { } // namespace cft -#endif /* CFT_SRC_INSTANCE_PARSING_HPP */"but their code is in a half-working prototype state and it's completely messy" +#endif /* CFT_SRC_INSTANCE_PARSING_HPP */ From ca1e9d518c4dc0dd717c95a0aebaa0d395aa8387 Mon Sep 17 00:00:00 2001 From: Dominik Krupke Date: Sun, 16 Feb 2025 15:56:06 +0100 Subject: [PATCH 14/20] docs: added install docs --- README.py.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.py.md b/README.py.md index f00b582..4937e22 100644 --- a/README.py.md +++ b/README.py.md @@ -7,7 +7,11 @@ SPDX-License-Identifier: MIT ## Install -TODO +We will publish the package on PyPI soon. For now, you can install the package by cloning the repository and running the following command in the root directory: + +```bash +pip install --verbose . +``` ## Usage From 1c7f0f2b7ca1b751eeabf7005abe9e73b121f29b Mon Sep 17 00:00:00 2001 From: Dominik Krupke Date: Sun, 16 Feb 2025 16:47:19 +0100 Subject: [PATCH 15/20] chores: just a little improved docs before merge. --- .github/workflows/pytest.yml | 2 +- README.md | 7 +++++++ README.py.md | 20 +++++++++++++++++++- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index cead4fb..fdcedd9 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -1,4 +1,4 @@ -name: Test on Linux +name: Test Python-bindings on Linux on: push: diff --git a/README.md b/README.md index 5c8dca1..2835506 100644 --- a/README.md +++ b/README.md @@ -247,3 +247,10 @@ For instance, you can use this interface to: - _Introduce Specialized Types_: Utilize alternative number representations like fixed-point types for specific use cases. This approach tries to strike a good a balance between ease of use for common numeric types and the ability to tailor the library to your specific needs. + + +### Python Bindings + +The project also provides Python bindings with a simplified interface for quick usage. +While you can also access much of the C++-interface directly, the primary purpose is to provide a point and shoot interface for the algorithm. +You can find more details in a separate [README](README.py.md). \ No newline at end of file diff --git a/README.py.md b/README.py.md index 4937e22..e0ffc30 100644 --- a/README.py.md +++ b/README.py.md @@ -5,6 +5,8 @@ SPDX-License-Identifier: MIT # Python Bindings for the AC-CFT Set Cover Heuristic + + ## Install We will publish the package on PyPI soon. For now, you can install the package by cloning the repository and running the following command in the root directory: @@ -39,4 +41,20 @@ print("The cost of the solution is:", solver.get_cost()) print("The lower bound of the solution is:", solver.get_lower_bound()) ``` -Ensure that all elements are zero-indexed and that every element between 0 and the maximum element appears in at least one set for a feasible solution. \ No newline at end of file +Ensure that all elements are zero-indexed and that every element between 0 and the maximum element appears in at least one set for a feasible solution. + +## Configuration + +You can configure the solver by setting the following parameters in `solve`: + +- seed (int): Seed for the random number generator. Default is 0. +- time_limit (int): Time limit in seconds. Default is 0 (no limit). +- verbose (int): Verbosity level. Default is 2. +- epsilon (float): Epsilon value for objective comparisons. Default is 0.999. +- heur_iters (int): Number of iterations for the heuristic phase. Default is 250. +- alpha (float): Relative fixing fraction increment. Default is 1.1. +- beta (float): Relative cutoff value to terminate Refinement. Default is 1.0. +- abs_subgrad_exit (float): Minimum LBs delta to trigger subgradient termination. Default is 1.0. +- rel_subgrad_exit (float): Minimum LBs gap to trigger subgradient termination. Default is 0.001. +- use_unit_costs (bool): Solve the given instance setting columns cost to one. Default is False. + From c8fd47a33ed8f4b3603ae6a830e5006ecbb5e19d Mon Sep 17 00:00:00 2001 From: Francesco Cavaliere Date: Sat, 22 Feb 2025 19:03:45 +0100 Subject: [PATCH 16/20] Minor cosmetic changes Co-authored-by: d-krupke --- .gitignore | 31 ------------------------------- CMakeLists.txt | 6 ++++++ README.md | 14 +++++++------- 3 files changed, 13 insertions(+), 38 deletions(-) diff --git a/.gitignore b/.gitignore index 1e66c33..68cf528 100644 --- a/.gitignore +++ b/.gitignore @@ -57,9 +57,6 @@ __pycache__/ *.py[cod] *$py.class -# C extensions -*.so - # skbuild_conan _skbuild/ .conan/ @@ -141,25 +138,6 @@ target/ profile_default/ ipython_config.py -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - # pdm # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. #pdm.lock @@ -213,12 +191,3 @@ dmypy.json # Cython debug symbols cython_debug/ -# VSCode -.vscode/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 49610b9..dc8a96e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,6 +29,12 @@ set(LIBRARIES fmt::fmt pthread dl m) target_link_libraries(accft PUBLIC ${LIBRARIES}) if (SKBUILD) +set(PYTHON_BINDINGS ON) +endif() + +if (PYTHON_BINDINGS) +message(STATUS "PYTHON_BINDINGS: ${PYTHON_BINDINGS}") + # CPM Dependencies ~~~~~~~~~~~~~~~~ # CPM allows us to include some simple # dependencies without much fuss. Great default, for everything more diff --git a/README.md b/README.md index 2835506..03accfd 100644 --- a/README.md +++ b/README.md @@ -177,6 +177,13 @@ _Note: for rail2586, better results can be obtained emphasizing multipliers qual The results for the other datasets can be found in the [`benchmarks`](benchmarks/) directory. + +## Python Bindings + +The project also provides Python bindings with a simplified interface for quick usage. +While you can also access much of the C++-interface directly, the primary purpose is to provide a point and shoot interface for the algorithm. +You can find more details in the separate [README](README.py.md). + ## Coding Style Regarding the source code, here you can find the general set of rules that we try to enforce. Since this is a project we work on in our free time, we haven't been afraid to experiment with simple rules and convention that we adjusted along the way when we felt something wasn't working for us. @@ -247,10 +254,3 @@ For instance, you can use this interface to: - _Introduce Specialized Types_: Utilize alternative number representations like fixed-point types for specific use cases. This approach tries to strike a good a balance between ease of use for common numeric types and the ability to tailor the library to your specific needs. - - -### Python Bindings - -The project also provides Python bindings with a simplified interface for quick usage. -While you can also access much of the C++-interface directly, the primary purpose is to provide a point and shoot interface for the algorithm. -You can find more details in a separate [README](README.py.md). \ No newline at end of file From c98a0fab90985a5765ccf67dbbfb2964b10ae300 Mon Sep 17 00:00:00 2001 From: Francesco Cavaliere Date: Sat, 22 Feb 2025 19:05:22 +0100 Subject: [PATCH 17/20] + `DualState` (dua solution, called "state" to avoid confusion) + CftResult (probably a bettername exists) And made the proper changes --- src/algorithms/Refinement.hpp | 29 ++++---- src/algorithms/ThreePhase.hpp | 41 +++++------ src/core/cft.hpp | 19 +++-- src/main.cpp | 6 +- src/pyaccft/_bindings.cpp | 118 ++++++++++++++++++-------------- test/README_unittests.cpp | 6 +- test/Refinement_unittests.cpp | 14 ++-- test/cft_unittests.cpp | 16 ++++- test/custom_types_unittests.cpp | 12 ++-- test/large_types_unittests.cpp | 14 ++-- test/small_types_unittests.cpp | 14 ++-- 11 files changed, 157 insertions(+), 132 deletions(-) diff --git a/src/algorithms/Refinement.hpp b/src/algorithms/Refinement.hpp index 762fbc2..7997b70 100644 --- a/src/algorithms/Refinement.hpp +++ b/src/algorithms/Refinement.hpp @@ -88,25 +88,24 @@ namespace local { namespace { } // namespace local // Complete CFT algorithm (Refinement + call to 3-phase) -inline Solution run(Environment const& env, // in - Instance const& orig_inst, // in - Solution const& warmstart_sol = {} // in +inline CftResult run(Environment const& env, // in + Instance const& orig_inst, // in + Solution const& warmstart_sol = {} // in ) { cidx_t const ncols = csize(orig_inst.cols); ridx_t const nrows = rsize(orig_inst.rows); - auto inst = orig_inst; - auto best_sol = Solution(); - best_sol.cost = limits::max(); + auto inst = orig_inst; + auto nofix_dual = DualState(); + auto best_sol = Solution(); + best_sol.cost = limits::max(); if (!warmstart_sol.idxs.empty()) best_sol = warmstart_sol; auto three_phase = ThreePhase(); auto select_cols_to_fix = local::RefinementFixManager(); - auto nofix_lagr_mult = std::vector(); - auto nofix_lb = limits::max(); auto old2new = IdxsMaps(); auto fixing = FixingData(); auto max_cost = limits::max(); @@ -120,16 +119,15 @@ inline Solution run(Environment const& env, // in } if (iter_counter == 0) { - nofix_lagr_mult = std::move(result_3p.nofix_lagr_mult); - nofix_lb = result_3p.nofix_lb; - max_cost = env.beta * nofix_lb + env.epsilon; + nofix_dual = std::move(result_3p.dual); + max_cost = env.beta * nofix_dual.lb + env.epsilon; } if (best_sol.cost <= max_cost || env.timer.elapsed() > env.time_limit) break; inst = orig_inst; - auto cols_to_fix = select_cols_to_fix(env, inst, nofix_lagr_mult, best_sol); + auto cols_to_fix = select_cols_to_fix(env, inst, nofix_dual.mults, best_sol); if (!cols_to_fix.empty()) { make_identity_fixing_data(ncols, nrows, fixing); fix_columns_and_compute_maps(cols_to_fix, inst, fixing, old2new); @@ -140,8 +138,8 @@ inline Solution run(Environment const& env, // in "REFN> {:2}: Best solution {:.2f}, lb {:.2f}, gap {:.2f}%\n", iter_counter, best_sol.cost, - nofix_lb, - 100.0_F * (best_sol.cost - nofix_lb) / best_sol.cost); + nofix_dual.lb, + 100.0_F * (best_sol.cost - nofix_dual.lb) / best_sol.cost); print<2>(env, "REFN> {:2}: Fixed cost {:.2f}, free rows {:.0f}%, time {:.2f}s\n\n", iter_counter, @@ -152,8 +150,7 @@ inline Solution run(Environment const& env, // in if (inst.rows.empty() || env.timer.elapsed() > env.time_limit) break; } - best_sol.lower_bound = nofix_lb; - return best_sol; + return {std::move(best_sol), std::move(nofix_dual)}; } } // namespace cft diff --git a/src/algorithms/ThreePhase.hpp b/src/algorithms/ThreePhase.hpp index 6d4afdb..6f31983 100644 --- a/src/algorithms/ThreePhase.hpp +++ b/src/algorithms/ThreePhase.hpp @@ -15,37 +15,30 @@ namespace cft { -struct ThreePhaseResult { - Solution sol; // Best feasible solution found - std::vector nofix_lagr_mult; // Lagrangian multipliers before the first fixing - real_t nofix_lb; // Lower bound before the first fixing -}; - class ThreePhase { static constexpr real_t init_step_size = 0.1_F; // Caches - Subgradient subgrad; // Subgradient functor - Greedy greedy; // Greedy functor - ColFixing col_fixing; // Column fixing functor - Pricer pricer; // Pricing functor - FixingData fixing; // Column fixing data - Solution sol; // Current solution - Solution best_sol; // Best solution - InstAndMap core; // Core instance - std::vector lagr_mult; // Lagrangian multipliers - std::vector unfixed_lagr_mult; // Best multipliers before the first fixing + Subgradient subgrad; // Subgradient functor + Greedy greedy; // Greedy functor + ColFixing col_fixing; // Column fixing functor + Pricer pricer; // Pricing functor + FixingData fixing; // Column fixing data + Solution sol; // Current solution + Solution best_sol; // Best solution + InstAndMap core; // Core instance + std::vector lagr_mult; // Lagrangian multipliers + DualState nofix_dual; // Best multipliers before the first fixing public: // 3-phase algorithm consisting in subgradient, greedy and column fixing. // NOTE: inst gets progressively fixed inplace, loosing its original state. - ThreePhaseResult operator()(Environment const& env, // in - Instance& inst // in/cache + CftResult operator()(Environment const& env, // in + Instance& inst // in/cache ) { ridx_t const orig_nrows = rsize(inst.rows); // Original number of rows for ColFixing - auto tot_timer = Chrono<>(); - auto unfixed_lb = limits::min(); + auto tot_timer = Chrono<>(); _three_phase_setup(inst, greedy, sol, best_sol, core, lagr_mult, fixing); CFT_IF_DEBUG(auto inst_copy = inst); @@ -57,10 +50,8 @@ class ThreePhase { auto cutoff = best_sol.cost - fixing.fixed_cost; auto real_lb = subgrad(env, inst, cutoff, pricer, core, step_size, lagr_mult); - if (iter_counter == 0) { - unfixed_lagr_mult = lagr_mult; - unfixed_lb = real_lb; - } + if (iter_counter == 0) + nofix_dual = {lagr_mult, real_lb}; if (real_lb + fixing.fixed_cost >= best_sol.cost - env.epsilon || env.timer.elapsed() > env.time_limit) @@ -96,7 +87,7 @@ class ThreePhase { "3PHS> Best solution: {:.2f}, time: {:.2f}s\n\n", best_sol.cost, tot_timer.elapsed()); - return {best_sol, unfixed_lagr_mult, unfixed_lb}; + return {best_sol, nofix_dual}; } private: diff --git a/src/core/cft.hpp b/src/core/cft.hpp index 36d19d6..234aad5 100644 --- a/src/core/cft.hpp +++ b/src/core/cft.hpp @@ -107,16 +107,21 @@ struct CidxAndCost { real_t cost; }; -// Solution represented by a vector of column indexes and the total cost +// Solution for the Set Covering problem struct Solution { - std::vector idxs; - real_t cost; - real_t lower_bound = 0.0_F; + std::vector idxs; // List of selected column indexes ... + real_t cost; // ... their total cost +}; - Solution() = default; - Solution(std::vector idxs_, real_t cost_, real_t lower_bound_ = 0.0_F) - : idxs(std::move(idxs_)), cost(cost_), lower_bound(lower_bound_) {} +// Dual solution for the Set Covering problem +struct DualState { + std::vector mults; // Lagrangian multipliers + real_t lb; // Lower bound +}; +struct CftResult { + Solution sol; + DualState dual; }; // Environment struct to hold all the parameters and working variables diff --git a/src/main.cpp b/src/main.cpp index d40807c..6fdf157 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -18,11 +18,11 @@ int main(int argc, char const** argv) { cft::print_arg_values(env); auto fdata = cft::parse_inst_and_initsol(env); - auto sol = cft::run(env, fdata.inst, fdata.init_sol); - cft::write_solution(env.sol_path, sol); + auto res = cft::run(env, fdata.inst, fdata.init_sol); + cft::write_solution(env.sol_path, res.sol); cft::print<1>(env, "CFT> Best solution {:.2f} time {:.2f}s\n", - sol.cost, + res.sol.cost, env.timer.elapsed()); } catch (std::exception const& e) { diff --git a/src/pyaccft/_bindings.cpp b/src/pyaccft/_bindings.cpp index 25af997..eb92987 100644 --- a/src/pyaccft/_bindings.cpp +++ b/src/pyaccft/_bindings.cpp @@ -6,53 +6,52 @@ #include // Basic pybind11 functionality #include // Automatic conversion of vectors -// fmt -#include - #include #include "../algorithms/Refinement.hpp" -#include "../core/CliArgs.hpp" #include "../core/Instance.hpp" #include "../core/cft.hpp" #include "../core/parsing.hpp" -#include "../utils/print.hpp" -void check_and_fill_instance(cft::Instance& instance) { - auto& cols = instance.cols; - auto& rows = instance.rows; - using namespace cft; - size_t n = 0; - - // Determine the maximum index in cols - for (cidx_t j = 0_C; j < csize(cols); ++j) - for (ridx_t i : cols[j]) - n = std::max(n, static_cast(i + 1)); - - // Guard against the user entering huge indices because they - // might have misunderstood the 0-based indexing. - if (n > cols.idxs.size()) { - throw std::runtime_error(fmt::format("Item index out of bounds. Maximum index is {} but " - "size is {}. Please make sure that the n items are " - "indexed from 0 to n-1.", - n, - cols.idxs.size())); +namespace local { namespace { + void check_and_fill_instance(cft::Instance& instance) { + auto& cols = instance.cols; + auto& rows = instance.rows; + using namespace cft; + size_t n = 0; + + // Determine the maximum index in cols + for (cidx_t j = 0_C; j < csize(cols); ++j) + for (ridx_t i : cols[j]) + n = std::max(n, static_cast(i + 1)); + + // Guard against the user entering huge indices because they + // might have misunderstood the 0-based indexing. + if (n > cols.idxs.size()) { + throw std::runtime_error( + fmt::format("Item index out of bounds. Maximum index is {} but " + "size is {}. Please make sure that the n items are " + "indexed from 0 to n-1.", + n, + cols.idxs.size())); + } + + // Check if every element (row) is in at least one column + std::vector in_col(n, false); + // Mark every element that is in a column (set) + for (cidx_t j = 0_C; j < csize(cols); ++j) + for (ridx_t i : cols[j]) + in_col[i] = true; + // Check if every element is in at least one column + for (size_t i = 0; i < n; ++i) + if (!in_col[i]) + throw std::runtime_error(fmt::format("Item {} not contained in any set.", i)); + + // Fill rows from columns + fill_rows_from_cols(cols, n, rows); } - - // Check if every element (row) is in at least one column - std::vector in_col(n, false); - // Mark every element that is in a column (set) - for (cidx_t j = 0_C; j < csize(cols); ++j) - for (ridx_t i : cols[j]) - in_col[i] = true; - // Check if every element is in at least one column - for (size_t i = 0; i < n; ++i) - if (!in_col[i]) - throw std::runtime_error(fmt::format("Item {} not contained in any set.", i)); - - // Fill rows from columns - fill_rows_from_cols(cols, n, rows); -} +} // namespace +} // namespace local // Pybind11 module definitions PYBIND11_MODULE(_bindings, m) { @@ -123,28 +122,47 @@ PYBIND11_MODULE(_bindings, m) { .def("add", [](Instance& self, std::vector const& col, real_t cost) { self.cols.push_back(col); - if (cost < 0.0) { - throw std::runtime_error("Costs must be non-negative."); - } + if (cost < 0.0) + throw std::runtime_error("Costs must be non-negative."); self.costs.push_back(cost); return self.costs.size() - 1; }) - .def("copy", [](Instance const& a) { return Instance(a); }) - .def("prepare", [](Instance& self) { check_and_fill_instance(self); }); + .def("copy", [](Instance const& a) { return a; }) + .def("prepare", [](Instance& self) { ::local::check_and_fill_instance(self); }); + + + py::class_(m, "CftResult") + .def(py::init<>()) + .def_readwrite("sol", &CftResult::sol) + .def_readwrite("dual", &CftResult::dual) + .def("copy", [](CftResult const& a) { return a; }) + .def("__repr__", [](CftResult const& a) { + return fmt::format("CftResult(sol=({},{}), dual=({},{}))", + a.sol.cost, + a.sol.cost, + a.dual.mults, + a.dual.lb); + }); py::class_(m, "Solution") .def(py::init<>()) .def_readwrite("idxs", &Solution::idxs) .def_readwrite("cost", &Solution::cost) - .def_readwrite("lower_bound", &Solution::lower_bound) - .def("copy", [](Solution const& a) { return Solution(a); }) + .def("copy", [](Solution const& a) { return a; }) .def("__repr__", [](Solution const& a) { - return fmt::format("Solution(idxs={}, cost={}, lower_bound={})", - a.idxs, - a.cost, - a.lower_bound); + return fmt::format("Solution(idxs={}, cost={})", a.idxs, a.cost); }); + py::class_(m, "DualState") + .def(py::init<>()) + .def_readwrite("idxs", &DualState::mults) + .def_readwrite("cost", &DualState::lb) + .def("copy", [](DualState const& a) { return a; }) + .def("__repr__", [](DualState const& a) { + return fmt::format("DualState(mults={}, lb={})", a.mults, a.lb); + }); + + py::class_(m, "FileData") .def_readwrite("inst", &FileData::inst) .def_readwrite("init_sol", &FileData::init_sol); diff --git a/test/README_unittests.cpp b/test/README_unittests.cpp index 7c9a3e5..c94577c 100644 --- a/test/README_unittests.cpp +++ b/test/README_unittests.cpp @@ -48,8 +48,8 @@ TEST_CASE("Invoke whole algorithm") { env.time_limit = 10.0; // Time limit in seconds env.verbose = 2; // Log verbosity level (from 0 to 5) env.timer.restart(); // NOTE: the timer is used also for the time limit test - auto sol = cft::run(env, inst); - fmt::print("CFT solution cost: {}\n", sol.cost); + auto res = cft::run(env, inst); + fmt::print("CFT solution cost: {}\n", res.sol.cost); }()); } @@ -66,7 +66,7 @@ TEST_CASE("Invoke 3-phase algorithm") { REQUIRE_NOTHROW([&] { auto three_phase = cft::ThreePhase(); auto result = three_phase(env, inst); - fmt::print("3-phase solution cost: {}, LB: {}\n", result.sol.cost, result.nofix_lb); + fmt::print("3-phase solution cost: {}, LB: {}\n", result.sol.cost, result.dual.lb); }()); } diff --git a/test/Refinement_unittests.cpp b/test/Refinement_unittests.cpp index 2787271..7ef864a 100644 --- a/test/Refinement_unittests.cpp +++ b/test/Refinement_unittests.cpp @@ -23,14 +23,14 @@ TEST_CASE("Whole algorithm run test") { for (int n = 0; n < 100; ++n) { auto inst = Instance(); - auto sol = Solution(); + auto res = CftResult(); REQUIRE_NOTHROW(inst = make_easy_inst(n, 1000_C)); - REQUIRE_NOTHROW(sol = run(env, inst, init_sol)); - CHECK(sol.cost <= 1000.0_F); // Trivial bad solution has 1000 cost - CHECK(sol.cost >= as_real(sol.idxs.size())); // Min col cost is 1.0 - if (abs(sol.cost - 1000.0_F) < 1e-6_F) - CHECK(sol.idxs == init_sol.idxs); - CFT_IF_DEBUG(CHECK_NOTHROW(check_inst_solution(inst, sol))); + REQUIRE_NOTHROW(res = run(env, inst, init_sol)); + CHECK(res.sol.cost <= 1000.0_F); // Trivial bad solution has 1000 cost + CHECK(res.sol.cost >= as_real(res.sol.idxs.size())); // Min col cost is 1.0 + if (abs(res.sol.cost - 1000.0_F) < 1e-6_F) + CHECK(res.sol.idxs == init_sol.idxs); + CFT_IF_DEBUG(CHECK_NOTHROW(check_inst_solution(inst, res.sol))); } } diff --git a/test/cft_unittests.cpp b/test/cft_unittests.cpp index 10c28f4..e7bc77b 100644 --- a/test/cft_unittests.cpp +++ b/test/cft_unittests.cpp @@ -17,11 +17,25 @@ TEST_CASE("Test CidxAndCost struct") { } TEST_CASE("Test Solution struct") { - auto s = Solution{{}, 0.0_F, 0.0_F}; + auto s = Solution{{}, 0.0_F}; CHECK(s.idxs.empty()); CHECK(s.cost == 0.0_F); } +TEST_CASE("Test DualState struct") { + auto d = DualState{{}, 0.0_F}; + CHECK(d.mults.empty()); + CHECK(d.lb == 0.0_F); +} + +TEST_CASE("Test CftResult struct") { + auto r = CftResult{{{}, 1.2_F}, {{}, 2.3_F}}; + CHECK(r.sol.idxs.empty()); + CHECK(r.sol.cost == 1.2_F); + CHECK(r.dual.mults.empty()); + CHECK(r.dual.lb == 2.3_F); +} + TEST_CASE("Test as_cidx function") { int i = 10; cidx_t c = as_cidx(i); diff --git a/test/custom_types_unittests.cpp b/test/custom_types_unittests.cpp index 10d0a7f..0494ce8 100644 --- a/test/custom_types_unittests.cpp +++ b/test/custom_types_unittests.cpp @@ -68,12 +68,12 @@ TEST_CASE("Custom types, whole algorithm run test") { for (int n = 0; n < 20; ++n) { auto inst = make_easy_inst(n, 100_C); - auto sol = run(env, inst, init_sol); - CHECK((sol.cost <= 1000.0_F)); // Trivial bad solution has 1000 cost - CHECK((sol.cost >= as_real(sol.idxs.size()))); // Min col cost is 1.0 - if (abs(sol.cost - 1000.0_F) < 1e-6_F) - DOCTEST_CHECK_EQ(sol.idxs, init_sol.idxs); - CFT_IF_DEBUG(CHECK_NOTHROW(check_inst_solution(inst, sol))); + auto res = run(env, inst, init_sol); + CHECK((res.sol.cost <= 1000.0_F)); // Trivial bad solution has 1000 cost + CHECK((res.sol.cost >= as_real(res.sol.idxs.size()))); // Min col cost is 1.0 + if (abs(res.sol.cost - 1000.0_F) < 1e-6_F) + DOCTEST_CHECK_EQ(res.sol.idxs, init_sol.idxs); + CFT_IF_DEBUG(CHECK_NOTHROW(check_inst_solution(inst, res.sol))); } } diff --git a/test/large_types_unittests.cpp b/test/large_types_unittests.cpp index 4710540..e65a732 100644 --- a/test/large_types_unittests.cpp +++ b/test/large_types_unittests.cpp @@ -26,14 +26,14 @@ TEST_CASE("Custom types, whole algorithm run test") { for (int n = 0; n < 20; ++n) { auto inst = Instance(); - auto sol = Solution(); + auto res = CftResult(); REQUIRE_NOTHROW(inst = make_easy_inst(n, 1000_C)); - REQUIRE_NOTHROW(sol = run(env, inst, init_sol)); - CHECK((sol.cost <= 1000.0_F)); // Trivial bad solution has 1000 cost - CHECK((sol.cost >= as_real(sol.idxs.size()))); // Min col cost is 1.0 - if ((abs(sol.cost - 1000.0_F) < 1e-6_F)) - CHECK(sol.idxs == init_sol.idxs); - CFT_IF_DEBUG(CHECK_NOTHROW(check_inst_solution(inst, sol))); + REQUIRE_NOTHROW(res = run(env, inst, init_sol)); + CHECK((res.sol.cost <= 1000.0_F)); // Trivial bad solution has 1000 cost + CHECK((res.sol.cost >= as_real(res.sol.idxs.size()))); // Min col cost is 1.0 + if ((abs(res.sol.cost - 1000.0_F) < 1e-6_F)) + CHECK(res.sol.idxs == init_sol.idxs); + CFT_IF_DEBUG(CHECK_NOTHROW(check_inst_solution(inst, res.sol))); } } diff --git a/test/small_types_unittests.cpp b/test/small_types_unittests.cpp index 666a2a3..d868cd6 100644 --- a/test/small_types_unittests.cpp +++ b/test/small_types_unittests.cpp @@ -26,14 +26,14 @@ TEST_CASE("Custom types, whole algorithm run test") { for (int n = 0; n < 20; ++n) { auto inst = Instance(); - auto sol = Solution(); + auto res = CftResult(); REQUIRE_NOTHROW(inst = make_easy_inst(n, 1000_C)); - REQUIRE_NOTHROW(sol = run(env, inst, init_sol)); - CHECK((sol.cost <= 1000.0_F)); // Trivial bad solution has 1000 cost - CHECK((sol.cost >= as_real(sol.idxs.size()))); // Min col cost is 1.0 - if ((abs(sol.cost - 1000.0_F) < 1e-6_F)) - CHECK(sol.idxs == init_sol.idxs); - CFT_IF_DEBUG(CHECK_NOTHROW(check_inst_solution(inst, sol))); + REQUIRE_NOTHROW(res = run(env, inst, init_sol)); + CHECK((res.sol.cost <= 1000.0_F)); // Trivial bad solution has 1000 cost + CHECK((res.sol.cost >= as_real(res.sol.idxs.size()))); // Min col cost is 1.0 + if ((abs(res.sol.cost - 1000.0_F) < 1e-6_F)) + CHECK(res.sol.idxs == init_sol.idxs); + CFT_IF_DEBUG(CHECK_NOTHROW(check_inst_solution(inst, res.sol))); } } From 07a899b1698300fec855f5ca54ad26c20a453e63 Mon Sep 17 00:00:00 2001 From: Francesco Cavaliere Date: Sat, 22 Feb 2025 19:32:10 +0100 Subject: [PATCH 18/20] Trying to fix python pinding after last commit --- src/pyaccft/set_cover_solver.py | 34 +++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/src/pyaccft/set_cover_solver.py b/src/pyaccft/set_cover_solver.py index e75a43e..32e113c 100644 --- a/src/pyaccft/set_cover_solver.py +++ b/src/pyaccft/set_cover_solver.py @@ -8,6 +8,8 @@ run, Instance, Solution, + DualState, + CftResult, ) @@ -46,7 +48,7 @@ class SetCoverSolver: """ def __init__(self): - self._solution = None + self._result = None self._instance = Instance() self._initialized = False self._max_element = -1 @@ -62,7 +64,7 @@ def add_set(self, elements: list[int], cost: float) -> int: max_element = max(elements) if max_element > self._max_element: self._max_element = max_element - self._solution = ( + self._result = ( None # in case there is a new element, the solution is no longer ) # valid @@ -81,8 +83,8 @@ def from_file(self, filename: str | Path, parser: str = "RAIL") -> None: env.parser = parser fdata = parse_inst_and_initsol(env) self._instance = fdata.inst.copy() - self._solution = fdata.init_sol.copy() - print(self._solution) + self._result.solution = fdata.init_sol.copy() + print(self._result) self.num_elements = len(self._instance.costs) self._initialized = True # a file is always considered initialized @@ -132,29 +134,37 @@ def solve( self._instance.rows.clear() self._instance.prepare() self._initialized = True - init_sol = Solution() if self._solution is None else self._solution - self._solution = run(env, self._instance, init_sol).copy() + init_sol = Solution() if self._result is None else self._result.solution + self._result = run(env, self._instance, init_sol).copy() def get_solution(self) -> list[int] | None: """ Return the indices of the selected sets in the solution. """ - if self._solution is None: + if self._result is None: return None - return list(self._solution.idxs) + return list(self._result.solution.idxs) def get_cost(self) -> float | None: """ Return the cost of the solution. """ - if self._solution is None: + if self._result is None: return None - return self._solution.cost + return self._result.cost def get_lower_bound(self) -> float: """ Return a lower bound on the optimal solution. """ - if self._solution is None: + if self._result is None: return 0 - return self._solution.lower_bound + return self._result.lower_bound + + def get_dual_multipliers(self) -> list[float] | None: + """ + Return the dual multipliers of the solution. + """ + if self._result is None: + return None + return list(self._result.dual.mults) From 7f7848cade9d21b7b680ee2a070ecdb50fda13df Mon Sep 17 00:00:00 2001 From: Dominik Krupke Date: Sat, 22 Feb 2025 19:45:59 +0100 Subject: [PATCH 19/20] fix: fixing renamed bindings --- src/pyaccft/set_cover_solver.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pyaccft/set_cover_solver.py b/src/pyaccft/set_cover_solver.py index 32e113c..f4baa91 100644 --- a/src/pyaccft/set_cover_solver.py +++ b/src/pyaccft/set_cover_solver.py @@ -134,7 +134,7 @@ def solve( self._instance.rows.clear() self._instance.prepare() self._initialized = True - init_sol = Solution() if self._result is None else self._result.solution + init_sol = Solution() if self._result is None else self._result.sol self._result = run(env, self._instance, init_sol).copy() def get_solution(self) -> list[int] | None: @@ -143,7 +143,7 @@ def get_solution(self) -> list[int] | None: """ if self._result is None: return None - return list(self._result.solution.idxs) + return list(self._result.sol.idxs) def get_cost(self) -> float | None: """ @@ -151,7 +151,7 @@ def get_cost(self) -> float | None: """ if self._result is None: return None - return self._result.cost + return self._result.sol.cost def get_lower_bound(self) -> float: """ @@ -159,7 +159,7 @@ def get_lower_bound(self) -> float: """ if self._result is None: return 0 - return self._result.lower_bound + return self._result.dual.cost def get_dual_multipliers(self) -> list[float] | None: """ From 07f50602c03dcce2c88c9572fb54f8507a6b23c1 Mon Sep 17 00:00:00 2001 From: Francesco Cavaliere Date: Sat, 22 Feb 2025 19:54:13 +0100 Subject: [PATCH 20/20] Removed import and CPM --- CMakeLists.txt | 15 +- cmake/CPM.cmake | 1287 ------------------------------- src/pyaccft/set_cover_solver.py | 1 - 3 files changed, 3 insertions(+), 1300 deletions(-) delete mode 100644 cmake/CPM.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index dc8a96e..3b8cb93 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,20 +35,11 @@ endif() if (PYTHON_BINDINGS) message(STATUS "PYTHON_BINDINGS: ${PYTHON_BINDINGS}") - # CPM Dependencies ~~~~~~~~~~~~~~~~ - # CPM allows us to include some simple - # dependencies without much fuss. Great default, for everything more - # complicated, uses conan. Why not always use conan? CPM works without any - # interaction (conan needs to be called externally). CPM also has no problems - # with install PyBind11 (which conan has at the time). If the dependency has - # complicated dependencies on its own or is slow to compile, better use conan. - # Check out https://github.com/cpm-cmake/CPM.cmake - include(./cmake/CPM.cmake) # Package manager for simple requirements. - # PyBind11 - cpmaddpackage("gh:pybind/pybind11@2.10.0") # pybind11, essential + FetchContent_Declare(pybind11 GIT_REPOSITORY https://github.com/pybind/pybind11.git GIT_TAG v2.13.6) + FetchContent_MakeAvailable(pybind11) # pybind11, essential set(CMAKE_POSITION_INDEPENDENT_CODE ON) # The code needs to be compiled as PIC - # to build the shared lib for python. + # to build the shared lib for python. set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) # Ensure fmt is compiled with -fPIC set_target_properties(fmt PROPERTIES POSITION_INDEPENDENT_CODE ON) diff --git a/cmake/CPM.cmake b/cmake/CPM.cmake deleted file mode 100644 index 35f9912..0000000 --- a/cmake/CPM.cmake +++ /dev/null @@ -1,1287 +0,0 @@ -# SPDX-FileCopyrightText: 2019-2023 Lars Melchior and contributors -# SPDX-License-Identifier: MIT -# CPM.cmake - CMake's missing package manager -# =========================================== -# See https://github.com/cpm-cmake/CPM.cmake for usage and update instructions. -# -# MIT License -# ----------- -#[[ - Copyright (c) 2019-2023 Lars Melchior and contributors - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. -]] - -cmake_minimum_required(VERSION 3.14 FATAL_ERROR) - -# Initialize logging prefix -if(NOT CPM_INDENT) - set(CPM_INDENT - "CPM:" - CACHE INTERNAL "" - ) -endif() - -if(NOT COMMAND cpm_message) - function(cpm_message) - message(${ARGV}) - endfunction() -endif() - -set(CURRENT_CPM_VERSION 1.0.0-development-version) - -get_filename_component(CPM_CURRENT_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}" REALPATH) -if(CPM_DIRECTORY) - if(NOT CPM_DIRECTORY STREQUAL CPM_CURRENT_DIRECTORY) - if(CPM_VERSION VERSION_LESS CURRENT_CPM_VERSION) - message( - AUTHOR_WARNING - "${CPM_INDENT} \ -A dependency is using a more recent CPM version (${CURRENT_CPM_VERSION}) than the current project (${CPM_VERSION}). \ -It is recommended to upgrade CPM to the most recent version. \ -See https://github.com/cpm-cmake/CPM.cmake for more information." - ) - endif() - if(${CMAKE_VERSION} VERSION_LESS "3.17.0") - include(FetchContent) - endif() - return() - endif() - - get_property( - CPM_INITIALIZED GLOBAL "" - PROPERTY CPM_INITIALIZED - SET - ) - if(CPM_INITIALIZED) - return() - endif() -endif() - -if(CURRENT_CPM_VERSION MATCHES "development-version") - message( - WARNING "${CPM_INDENT} Your project is using an unstable development version of CPM.cmake. \ -Please update to a recent release if possible. \ -See https://github.com/cpm-cmake/CPM.cmake for details." - ) -endif() - -set_property(GLOBAL PROPERTY CPM_INITIALIZED true) - -macro(cpm_set_policies) - # the policy allows us to change options without caching - cmake_policy(SET CMP0077 NEW) - set(CMAKE_POLICY_DEFAULT_CMP0077 NEW) - - # the policy allows us to change set(CACHE) without caching - if(POLICY CMP0126) - cmake_policy(SET CMP0126 NEW) - set(CMAKE_POLICY_DEFAULT_CMP0126 NEW) - endif() - - # The policy uses the download time for timestamp, instead of the timestamp in the archive. This - # allows for proper rebuilds when a projects url changes - if(POLICY CMP0135) - cmake_policy(SET CMP0135 NEW) - set(CMAKE_POLICY_DEFAULT_CMP0135 NEW) - endif() - - # treat relative git repository paths as being relative to the parent project's remote - if(POLICY CMP0150) - cmake_policy(SET CMP0150 NEW) - set(CMAKE_POLICY_DEFAULT_CMP0150 NEW) - endif() -endmacro() -cpm_set_policies() - -option(CPM_USE_LOCAL_PACKAGES "Always try to use `find_package` to get dependencies" - $ENV{CPM_USE_LOCAL_PACKAGES} -) -option(CPM_LOCAL_PACKAGES_ONLY "Only use `find_package` to get dependencies" - $ENV{CPM_LOCAL_PACKAGES_ONLY} -) -option(CPM_DOWNLOAD_ALL "Always download dependencies from source" $ENV{CPM_DOWNLOAD_ALL}) -option(CPM_DONT_UPDATE_MODULE_PATH "Don't update the module path to allow using find_package" - $ENV{CPM_DONT_UPDATE_MODULE_PATH} -) -option(CPM_DONT_CREATE_PACKAGE_LOCK "Don't create a package lock file in the binary path" - $ENV{CPM_DONT_CREATE_PACKAGE_LOCK} -) -option(CPM_INCLUDE_ALL_IN_PACKAGE_LOCK - "Add all packages added through CPM.cmake to the package lock" - $ENV{CPM_INCLUDE_ALL_IN_PACKAGE_LOCK} -) -option(CPM_USE_NAMED_CACHE_DIRECTORIES - "Use additional directory of package name in cache on the most nested level." - $ENV{CPM_USE_NAMED_CACHE_DIRECTORIES} -) - -set(CPM_VERSION - ${CURRENT_CPM_VERSION} - CACHE INTERNAL "" -) -set(CPM_DIRECTORY - ${CPM_CURRENT_DIRECTORY} - CACHE INTERNAL "" -) -set(CPM_FILE - ${CMAKE_CURRENT_LIST_FILE} - CACHE INTERNAL "" -) -set(CPM_PACKAGES - "" - CACHE INTERNAL "" -) -set(CPM_DRY_RUN - OFF - CACHE INTERNAL "Don't download or configure dependencies (for testing)" -) - -if(DEFINED ENV{CPM_SOURCE_CACHE}) - set(CPM_SOURCE_CACHE_DEFAULT $ENV{CPM_SOURCE_CACHE}) -else() - set(CPM_SOURCE_CACHE_DEFAULT OFF) -endif() - -set(CPM_SOURCE_CACHE - ${CPM_SOURCE_CACHE_DEFAULT} - CACHE PATH "Directory to download CPM dependencies" -) - -if(NOT CPM_DONT_UPDATE_MODULE_PATH AND NOT DEFINED CMAKE_FIND_PACKAGE_REDIRECTS_DIR) - set(CPM_MODULE_PATH - "${CMAKE_BINARY_DIR}/CPM_modules" - CACHE INTERNAL "" - ) - # remove old modules - file(REMOVE_RECURSE ${CPM_MODULE_PATH}) - file(MAKE_DIRECTORY ${CPM_MODULE_PATH}) - # locally added CPM modules should override global packages - set(CMAKE_MODULE_PATH "${CPM_MODULE_PATH};${CMAKE_MODULE_PATH}") -endif() - -if(NOT CPM_DONT_CREATE_PACKAGE_LOCK) - set(CPM_PACKAGE_LOCK_FILE - "${CMAKE_BINARY_DIR}/cpm-package-lock.cmake" - CACHE INTERNAL "" - ) - file(WRITE ${CPM_PACKAGE_LOCK_FILE} - "# CPM Package Lock\n# This file should be committed to version control\n\n" - ) -endif() - -include(FetchContent) - -# Try to infer package name from git repository uri (path or url) -function(cpm_package_name_from_git_uri URI RESULT) - if("${URI}" MATCHES "([^/:]+)/?.git/?$") - set(${RESULT} - ${CMAKE_MATCH_1} - PARENT_SCOPE - ) - else() - unset(${RESULT} PARENT_SCOPE) - endif() -endfunction() - -# Try to infer package name and version from a url -function(cpm_package_name_and_ver_from_url url outName outVer) - if(url MATCHES "[/\\?]([a-zA-Z0-9_\\.-]+)\\.(tar|tar\\.gz|tar\\.bz2|zip|ZIP)(\\?|/|$)") - # We matched an archive - set(filename "${CMAKE_MATCH_1}") - - if(filename MATCHES "([a-zA-Z0-9_\\.-]+)[_-]v?(([0-9]+\\.)*[0-9]+[a-zA-Z0-9]*)") - # We matched - (ie foo-1.2.3) - set(${outName} - "${CMAKE_MATCH_1}" - PARENT_SCOPE - ) - set(${outVer} - "${CMAKE_MATCH_2}" - PARENT_SCOPE - ) - elseif(filename MATCHES "(([0-9]+\\.)+[0-9]+[a-zA-Z0-9]*)") - # We couldn't find a name, but we found a version - # - # In many cases (which we don't handle here) the url would look something like - # `irrelevant/ACTUAL_PACKAGE_NAME/irrelevant/1.2.3.zip`. In such a case we can't possibly - # distinguish the package name from the irrelevant bits. Moreover if we try to match the - # package name from the filename, we'd get bogus at best. - unset(${outName} PARENT_SCOPE) - set(${outVer} - "${CMAKE_MATCH_1}" - PARENT_SCOPE - ) - else() - # Boldly assume that the file name is the package name. - # - # Yes, something like `irrelevant/ACTUAL_NAME/irrelevant/download.zip` will ruin our day, but - # such cases should be quite rare. No popular service does this... we think. - set(${outName} - "${filename}" - PARENT_SCOPE - ) - unset(${outVer} PARENT_SCOPE) - endif() - else() - # No ideas yet what to do with non-archives - unset(${outName} PARENT_SCOPE) - unset(${outVer} PARENT_SCOPE) - endif() -endfunction() - -function(cpm_find_package NAME VERSION) - string(REPLACE " " ";" EXTRA_ARGS "${ARGN}") - find_package(${NAME} ${VERSION} ${EXTRA_ARGS} QUIET) - if(${CPM_ARGS_NAME}_FOUND) - if(DEFINED ${CPM_ARGS_NAME}_VERSION) - set(VERSION ${${CPM_ARGS_NAME}_VERSION}) - endif() - cpm_message(STATUS "${CPM_INDENT} Using local package ${CPM_ARGS_NAME}@${VERSION}") - CPMRegisterPackage(${CPM_ARGS_NAME} "${VERSION}") - set(CPM_PACKAGE_FOUND - YES - PARENT_SCOPE - ) - else() - set(CPM_PACKAGE_FOUND - NO - PARENT_SCOPE - ) - endif() -endfunction() - -# Create a custom FindXXX.cmake module for a CPM package This prevents `find_package(NAME)` from -# finding the system library -function(cpm_create_module_file Name) - if(NOT CPM_DONT_UPDATE_MODULE_PATH) - if(DEFINED CMAKE_FIND_PACKAGE_REDIRECTS_DIR) - # Redirect find_package calls to the CPM package. This is what FetchContent does when you set - # OVERRIDE_FIND_PACKAGE. The CMAKE_FIND_PACKAGE_REDIRECTS_DIR works for find_package in CONFIG - # mode, unlike the Find${Name}.cmake fallback. CMAKE_FIND_PACKAGE_REDIRECTS_DIR is not defined - # in script mode, or in CMake < 3.24. - # https://cmake.org/cmake/help/latest/module/FetchContent.html#fetchcontent-find-package-integration-examples - string(TOLOWER ${Name} NameLower) - file(WRITE ${CMAKE_FIND_PACKAGE_REDIRECTS_DIR}/${NameLower}-config.cmake - "include(\"${CMAKE_CURRENT_LIST_DIR}/${NameLower}-extra.cmake\" OPTIONAL)\n" - "include(\"${CMAKE_CURRENT_LIST_DIR}/${Name}Extra.cmake\" OPTIONAL)\n" - ) - file(WRITE ${CMAKE_FIND_PACKAGE_REDIRECTS_DIR}/${NameLower}-version.cmake - "set(PACKAGE_VERSION_COMPATIBLE TRUE)\n" "set(PACKAGE_VERSION_EXACT TRUE)\n" - ) - else() - file(WRITE ${CPM_MODULE_PATH}/Find${Name}.cmake - "include(\"${CPM_FILE}\")\n${ARGN}\nset(${Name}_FOUND TRUE)" - ) - endif() - endif() -endfunction() - -# Find a package locally or fallback to CPMAddPackage -function(CPMFindPackage) - set(oneValueArgs NAME VERSION GIT_TAG FIND_PACKAGE_ARGUMENTS) - - cmake_parse_arguments(CPM_ARGS "" "${oneValueArgs}" "" ${ARGN}) - - if(NOT DEFINED CPM_ARGS_VERSION) - if(DEFINED CPM_ARGS_GIT_TAG) - cpm_get_version_from_git_tag("${CPM_ARGS_GIT_TAG}" CPM_ARGS_VERSION) - endif() - endif() - - set(downloadPackage ${CPM_DOWNLOAD_ALL}) - if(DEFINED CPM_DOWNLOAD_${CPM_ARGS_NAME}) - set(downloadPackage ${CPM_DOWNLOAD_${CPM_ARGS_NAME}}) - elseif(DEFINED ENV{CPM_DOWNLOAD_${CPM_ARGS_NAME}}) - set(downloadPackage $ENV{CPM_DOWNLOAD_${CPM_ARGS_NAME}}) - endif() - if(downloadPackage) - CPMAddPackage(${ARGN}) - cpm_export_variables(${CPM_ARGS_NAME}) - return() - endif() - - cpm_find_package(${CPM_ARGS_NAME} "${CPM_ARGS_VERSION}" ${CPM_ARGS_FIND_PACKAGE_ARGUMENTS}) - - if(NOT CPM_PACKAGE_FOUND) - CPMAddPackage(${ARGN}) - cpm_export_variables(${CPM_ARGS_NAME}) - endif() - -endfunction() - -# checks if a package has been added before -function(cpm_check_if_package_already_added CPM_ARGS_NAME CPM_ARGS_VERSION) - if("${CPM_ARGS_NAME}" IN_LIST CPM_PACKAGES) - CPMGetPackageVersion(${CPM_ARGS_NAME} CPM_PACKAGE_VERSION) - if("${CPM_PACKAGE_VERSION}" VERSION_LESS "${CPM_ARGS_VERSION}") - message( - WARNING - "${CPM_INDENT} Requires a newer version of ${CPM_ARGS_NAME} (${CPM_ARGS_VERSION}) than currently included (${CPM_PACKAGE_VERSION})." - ) - endif() - cpm_get_fetch_properties(${CPM_ARGS_NAME}) - set(${CPM_ARGS_NAME}_ADDED NO) - set(CPM_PACKAGE_ALREADY_ADDED - YES - PARENT_SCOPE - ) - cpm_export_variables(${CPM_ARGS_NAME}) - else() - set(CPM_PACKAGE_ALREADY_ADDED - NO - PARENT_SCOPE - ) - endif() -endfunction() - -# Parse the argument of CPMAddPackage in case a single one was provided and convert it to a list of -# arguments which can then be parsed idiomatically. For example gh:foo/bar@1.2.3 will be converted -# to: GITHUB_REPOSITORY;foo/bar;VERSION;1.2.3 -function(cpm_parse_add_package_single_arg arg outArgs) - # Look for a scheme - if("${arg}" MATCHES "^([a-zA-Z]+):(.+)$") - string(TOLOWER "${CMAKE_MATCH_1}" scheme) - set(uri "${CMAKE_MATCH_2}") - - # Check for CPM-specific schemes - if(scheme STREQUAL "gh") - set(out "GITHUB_REPOSITORY;${uri}") - set(packageType "git") - elseif(scheme STREQUAL "gl") - set(out "GITLAB_REPOSITORY;${uri}") - set(packageType "git") - elseif(scheme STREQUAL "bb") - set(out "BITBUCKET_REPOSITORY;${uri}") - set(packageType "git") - # A CPM-specific scheme was not found. Looks like this is a generic URL so try to determine - # type - elseif(arg MATCHES ".git/?(@|#|$)") - set(out "GIT_REPOSITORY;${arg}") - set(packageType "git") - else() - # Fall back to a URL - set(out "URL;${arg}") - set(packageType "archive") - - # We could also check for SVN since FetchContent supports it, but SVN is so rare these days. - # We just won't bother with the additional complexity it will induce in this function. SVN is - # done by multi-arg - endif() - else() - if(arg MATCHES ".git/?(@|#|$)") - set(out "GIT_REPOSITORY;${arg}") - set(packageType "git") - else() - # Give up - message(FATAL_ERROR "${CPM_INDENT} Can't determine package type of '${arg}'") - endif() - endif() - - # For all packages we interpret @... as version. Only replace the last occurrence. Thus URIs - # containing '@' can be used - string(REGEX REPLACE "@([^@]+)$" ";VERSION;\\1" out "${out}") - - # Parse the rest according to package type - if(packageType STREQUAL "git") - # For git repos we interpret #... as a tag or branch or commit hash - string(REGEX REPLACE "#([^#]+)$" ";GIT_TAG;\\1" out "${out}") - elseif(packageType STREQUAL "archive") - # For archives we interpret #... as a URL hash. - string(REGEX REPLACE "#([^#]+)$" ";URL_HASH;\\1" out "${out}") - # We don't try to parse the version if it's not provided explicitly. cpm_get_version_from_url - # should do this at a later point - else() - # We should never get here. This is an assertion and hitting it means there's a problem with the - # code above. A packageType was set, but not handled by this if-else. - message(FATAL_ERROR "${CPM_INDENT} Unsupported package type '${packageType}' of '${arg}'") - endif() - - set(${outArgs} - ${out} - PARENT_SCOPE - ) -endfunction() - -# Check that the working directory for a git repo is clean -function(cpm_check_git_working_dir_is_clean repoPath gitTag isClean) - - find_package(Git REQUIRED) - - if(NOT GIT_EXECUTABLE) - # No git executable, assume directory is clean - set(${isClean} - TRUE - PARENT_SCOPE - ) - return() - endif() - - # check for uncommitted changes - execute_process( - COMMAND ${GIT_EXECUTABLE} status --porcelain - RESULT_VARIABLE resultGitStatus - OUTPUT_VARIABLE repoStatus - OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET - WORKING_DIRECTORY ${repoPath} - ) - if(resultGitStatus) - # not supposed to happen, assume clean anyway - message(WARNING "${CPM_INDENT} Calling git status on folder ${repoPath} failed") - set(${isClean} - TRUE - PARENT_SCOPE - ) - return() - endif() - - if(NOT "${repoStatus}" STREQUAL "") - set(${isClean} - FALSE - PARENT_SCOPE - ) - return() - endif() - - # check for committed changes - execute_process( - COMMAND ${GIT_EXECUTABLE} diff -s --exit-code ${gitTag} - RESULT_VARIABLE resultGitDiff - OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_QUIET - WORKING_DIRECTORY ${repoPath} - ) - - if(${resultGitDiff} EQUAL 0) - set(${isClean} - TRUE - PARENT_SCOPE - ) - else() - set(${isClean} - FALSE - PARENT_SCOPE - ) - endif() - -endfunction() - -# Add PATCH_COMMAND to CPM_ARGS_UNPARSED_ARGUMENTS. This method consumes a list of files in ARGN -# then generates a `PATCH_COMMAND` appropriate for `ExternalProject_Add()`. This command is appended -# to the parent scope's `CPM_ARGS_UNPARSED_ARGUMENTS`. -function(cpm_add_patches) - # Return if no patch files are supplied. - if(NOT ARGN) - return() - endif() - - # Find the patch program. - find_program(PATCH_EXECUTABLE patch) - if(CMAKE_HOST_WIN32 AND NOT PATCH_EXECUTABLE) - # The Windows git executable is distributed with patch.exe. Find the path to the executable, if - # it exists, then search `../usr/bin` and `../../usr/bin` for patch.exe. - find_package(Git QUIET) - if(GIT_EXECUTABLE) - get_filename_component(extra_search_path ${GIT_EXECUTABLE} DIRECTORY) - get_filename_component(extra_search_path_1up ${extra_search_path} DIRECTORY) - get_filename_component(extra_search_path_2up ${extra_search_path_1up} DIRECTORY) - find_program( - PATCH_EXECUTABLE patch HINTS "${extra_search_path_1up}/usr/bin" - "${extra_search_path_2up}/usr/bin" - ) - endif() - endif() - if(NOT PATCH_EXECUTABLE) - message(FATAL_ERROR "Couldn't find `patch` executable to use with PATCHES keyword.") - endif() - - # Create a temporary - set(temp_list ${CPM_ARGS_UNPARSED_ARGUMENTS}) - - # Ensure each file exists (or error out) and add it to the list. - set(first_item True) - foreach(PATCH_FILE ${ARGN}) - # Make sure the patch file exists, if we can't find it, try again in the current directory. - if(NOT EXISTS "${PATCH_FILE}") - if(NOT EXISTS "${CMAKE_CURRENT_LIST_DIR}/${PATCH_FILE}") - message(FATAL_ERROR "Couldn't find patch file: '${PATCH_FILE}'") - endif() - set(PATCH_FILE "${CMAKE_CURRENT_LIST_DIR}/${PATCH_FILE}") - endif() - - # Convert to absolute path for use with patch file command. - get_filename_component(PATCH_FILE "${PATCH_FILE}" ABSOLUTE) - - # The first patch entry must be preceded by "PATCH_COMMAND" while the following items are - # preceded by "&&". - if(first_item) - set(first_item False) - list(APPEND temp_list "PATCH_COMMAND") - else() - list(APPEND temp_list "&&") - endif() - # Add the patch command to the list - list(APPEND temp_list "${PATCH_EXECUTABLE}" "-p1" "<" "${PATCH_FILE}") - endforeach() - - # Move temp out into parent scope. - set(CPM_ARGS_UNPARSED_ARGUMENTS - ${temp_list} - PARENT_SCOPE - ) - -endfunction() - -# method to overwrite internal FetchContent properties, to allow using CPM.cmake to overload -# FetchContent calls. As these are internal cmake properties, this method should be used carefully -# and may need modification in future CMake versions. Source: -# https://github.com/Kitware/CMake/blob/dc3d0b5a0a7d26d43d6cfeb511e224533b5d188f/Modules/FetchContent.cmake#L1152 -function(cpm_override_fetchcontent contentName) - cmake_parse_arguments(PARSE_ARGV 1 arg "" "SOURCE_DIR;BINARY_DIR" "") - if(NOT "${arg_UNPARSED_ARGUMENTS}" STREQUAL "") - message(FATAL_ERROR "${CPM_INDENT} Unsupported arguments: ${arg_UNPARSED_ARGUMENTS}") - endif() - - string(TOLOWER ${contentName} contentNameLower) - set(prefix "_FetchContent_${contentNameLower}") - - set(propertyName "${prefix}_sourceDir") - define_property( - GLOBAL - PROPERTY ${propertyName} - BRIEF_DOCS "Internal implementation detail of FetchContent_Populate()" - FULL_DOCS "Details used by FetchContent_Populate() for ${contentName}" - ) - set_property(GLOBAL PROPERTY ${propertyName} "${arg_SOURCE_DIR}") - - set(propertyName "${prefix}_binaryDir") - define_property( - GLOBAL - PROPERTY ${propertyName} - BRIEF_DOCS "Internal implementation detail of FetchContent_Populate()" - FULL_DOCS "Details used by FetchContent_Populate() for ${contentName}" - ) - set_property(GLOBAL PROPERTY ${propertyName} "${arg_BINARY_DIR}") - - set(propertyName "${prefix}_populated") - define_property( - GLOBAL - PROPERTY ${propertyName} - BRIEF_DOCS "Internal implementation detail of FetchContent_Populate()" - FULL_DOCS "Details used by FetchContent_Populate() for ${contentName}" - ) - set_property(GLOBAL PROPERTY ${propertyName} TRUE) -endfunction() - -# Download and add a package from source -function(CPMAddPackage) - cpm_set_policies() - - list(LENGTH ARGN argnLength) - if(argnLength EQUAL 1) - cpm_parse_add_package_single_arg("${ARGN}" ARGN) - - # The shorthand syntax implies EXCLUDE_FROM_ALL and SYSTEM - set(ARGN "${ARGN};EXCLUDE_FROM_ALL;YES;SYSTEM;YES;") - endif() - - set(oneValueArgs - NAME - FORCE - VERSION - GIT_TAG - DOWNLOAD_ONLY - GITHUB_REPOSITORY - GITLAB_REPOSITORY - BITBUCKET_REPOSITORY - GIT_REPOSITORY - SOURCE_DIR - FIND_PACKAGE_ARGUMENTS - NO_CACHE - SYSTEM - GIT_SHALLOW - EXCLUDE_FROM_ALL - SOURCE_SUBDIR - CUSTOM_CACHE_KEY - ) - - set(multiValueArgs URL OPTIONS DOWNLOAD_COMMAND PATCHES) - - cmake_parse_arguments(CPM_ARGS "" "${oneValueArgs}" "${multiValueArgs}" "${ARGN}") - - # Set default values for arguments - - if(NOT DEFINED CPM_ARGS_VERSION) - if(DEFINED CPM_ARGS_GIT_TAG) - cpm_get_version_from_git_tag("${CPM_ARGS_GIT_TAG}" CPM_ARGS_VERSION) - endif() - endif() - - if(CPM_ARGS_DOWNLOAD_ONLY) - set(DOWNLOAD_ONLY ${CPM_ARGS_DOWNLOAD_ONLY}) - else() - set(DOWNLOAD_ONLY NO) - endif() - - if(DEFINED CPM_ARGS_GITHUB_REPOSITORY) - set(CPM_ARGS_GIT_REPOSITORY "https://github.com/${CPM_ARGS_GITHUB_REPOSITORY}.git") - elseif(DEFINED CPM_ARGS_GITLAB_REPOSITORY) - set(CPM_ARGS_GIT_REPOSITORY "https://gitlab.com/${CPM_ARGS_GITLAB_REPOSITORY}.git") - elseif(DEFINED CPM_ARGS_BITBUCKET_REPOSITORY) - set(CPM_ARGS_GIT_REPOSITORY "https://bitbucket.org/${CPM_ARGS_BITBUCKET_REPOSITORY}.git") - endif() - - if(DEFINED CPM_ARGS_GIT_REPOSITORY) - list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS GIT_REPOSITORY ${CPM_ARGS_GIT_REPOSITORY}) - if(NOT DEFINED CPM_ARGS_GIT_TAG) - set(CPM_ARGS_GIT_TAG v${CPM_ARGS_VERSION}) - endif() - - # If a name wasn't provided, try to infer it from the git repo - if(NOT DEFINED CPM_ARGS_NAME) - cpm_package_name_from_git_uri(${CPM_ARGS_GIT_REPOSITORY} CPM_ARGS_NAME) - endif() - endif() - - set(CPM_SKIP_FETCH FALSE) - - if(DEFINED CPM_ARGS_GIT_TAG) - list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS GIT_TAG ${CPM_ARGS_GIT_TAG}) - # If GIT_SHALLOW is explicitly specified, honor the value. - if(DEFINED CPM_ARGS_GIT_SHALLOW) - list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS GIT_SHALLOW ${CPM_ARGS_GIT_SHALLOW}) - endif() - endif() - - if(DEFINED CPM_ARGS_URL) - # If a name or version aren't provided, try to infer them from the URL - list(GET CPM_ARGS_URL 0 firstUrl) - cpm_package_name_and_ver_from_url(${firstUrl} nameFromUrl verFromUrl) - # If we fail to obtain name and version from the first URL, we could try other URLs if any. - # However multiple URLs are expected to be quite rare, so for now we won't bother. - - # If the caller provided their own name and version, they trump the inferred ones. - if(NOT DEFINED CPM_ARGS_NAME) - set(CPM_ARGS_NAME ${nameFromUrl}) - endif() - if(NOT DEFINED CPM_ARGS_VERSION) - set(CPM_ARGS_VERSION ${verFromUrl}) - endif() - - list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS URL "${CPM_ARGS_URL}") - endif() - - # Check for required arguments - - if(NOT DEFINED CPM_ARGS_NAME) - message( - FATAL_ERROR - "${CPM_INDENT} 'NAME' was not provided and couldn't be automatically inferred for package added with arguments: '${ARGN}'" - ) - endif() - - # Check if package has been added before - cpm_check_if_package_already_added(${CPM_ARGS_NAME} "${CPM_ARGS_VERSION}") - if(CPM_PACKAGE_ALREADY_ADDED) - cpm_export_variables(${CPM_ARGS_NAME}) - return() - endif() - - # Check for manual overrides - if(NOT CPM_ARGS_FORCE AND NOT "${CPM_${CPM_ARGS_NAME}_SOURCE}" STREQUAL "") - set(PACKAGE_SOURCE ${CPM_${CPM_ARGS_NAME}_SOURCE}) - set(CPM_${CPM_ARGS_NAME}_SOURCE "") - CPMAddPackage( - NAME "${CPM_ARGS_NAME}" - SOURCE_DIR "${PACKAGE_SOURCE}" - EXCLUDE_FROM_ALL "${CPM_ARGS_EXCLUDE_FROM_ALL}" - SYSTEM "${CPM_ARGS_SYSTEM}" - PATCHES "${CPM_ARGS_PATCHES}" - OPTIONS "${CPM_ARGS_OPTIONS}" - SOURCE_SUBDIR "${CPM_ARGS_SOURCE_SUBDIR}" - DOWNLOAD_ONLY "${DOWNLOAD_ONLY}" - FORCE True - ) - cpm_export_variables(${CPM_ARGS_NAME}) - return() - endif() - - # Check for available declaration - if(NOT CPM_ARGS_FORCE AND NOT "${CPM_DECLARATION_${CPM_ARGS_NAME}}" STREQUAL "") - set(declaration ${CPM_DECLARATION_${CPM_ARGS_NAME}}) - set(CPM_DECLARATION_${CPM_ARGS_NAME} "") - CPMAddPackage(${declaration}) - cpm_export_variables(${CPM_ARGS_NAME}) - # checking again to ensure version and option compatibility - cpm_check_if_package_already_added(${CPM_ARGS_NAME} "${CPM_ARGS_VERSION}") - return() - endif() - - if(NOT CPM_ARGS_FORCE) - if(CPM_USE_LOCAL_PACKAGES OR CPM_LOCAL_PACKAGES_ONLY) - cpm_find_package(${CPM_ARGS_NAME} "${CPM_ARGS_VERSION}" ${CPM_ARGS_FIND_PACKAGE_ARGUMENTS}) - - if(CPM_PACKAGE_FOUND) - cpm_export_variables(${CPM_ARGS_NAME}) - return() - endif() - - if(CPM_LOCAL_PACKAGES_ONLY) - message( - SEND_ERROR - "${CPM_INDENT} ${CPM_ARGS_NAME} not found via find_package(${CPM_ARGS_NAME} ${CPM_ARGS_VERSION})" - ) - endif() - endif() - endif() - - CPMRegisterPackage("${CPM_ARGS_NAME}" "${CPM_ARGS_VERSION}") - - if(DEFINED CPM_ARGS_GIT_TAG) - set(PACKAGE_INFO "${CPM_ARGS_GIT_TAG}") - elseif(DEFINED CPM_ARGS_SOURCE_DIR) - set(PACKAGE_INFO "${CPM_ARGS_SOURCE_DIR}") - else() - set(PACKAGE_INFO "${CPM_ARGS_VERSION}") - endif() - - if(DEFINED FETCHCONTENT_BASE_DIR) - # respect user's FETCHCONTENT_BASE_DIR if set - set(CPM_FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR}) - else() - set(CPM_FETCHCONTENT_BASE_DIR ${CMAKE_BINARY_DIR}/_deps) - endif() - - cpm_add_patches(${CPM_ARGS_PATCHES}) - - if(DEFINED CPM_ARGS_DOWNLOAD_COMMAND) - list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS DOWNLOAD_COMMAND ${CPM_ARGS_DOWNLOAD_COMMAND}) - elseif(DEFINED CPM_ARGS_SOURCE_DIR) - list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS SOURCE_DIR ${CPM_ARGS_SOURCE_DIR}) - if(NOT IS_ABSOLUTE ${CPM_ARGS_SOURCE_DIR}) - # Expand `CPM_ARGS_SOURCE_DIR` relative path. This is important because EXISTS doesn't work - # for relative paths. - get_filename_component( - source_directory ${CPM_ARGS_SOURCE_DIR} REALPATH BASE_DIR ${CMAKE_CURRENT_BINARY_DIR} - ) - else() - set(source_directory ${CPM_ARGS_SOURCE_DIR}) - endif() - if(NOT EXISTS ${source_directory}) - string(TOLOWER ${CPM_ARGS_NAME} lower_case_name) - # remove timestamps so CMake will re-download the dependency - file(REMOVE_RECURSE "${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-subbuild") - endif() - elseif(CPM_SOURCE_CACHE AND NOT CPM_ARGS_NO_CACHE) - string(TOLOWER ${CPM_ARGS_NAME} lower_case_name) - set(origin_parameters ${CPM_ARGS_UNPARSED_ARGUMENTS}) - list(SORT origin_parameters) - if(CPM_ARGS_CUSTOM_CACHE_KEY) - # Application set a custom unique directory name - set(download_directory ${CPM_SOURCE_CACHE}/${lower_case_name}/${CPM_ARGS_CUSTOM_CACHE_KEY}) - elseif(CPM_USE_NAMED_CACHE_DIRECTORIES) - string(SHA1 origin_hash "${origin_parameters};NEW_CACHE_STRUCTURE_TAG") - set(download_directory ${CPM_SOURCE_CACHE}/${lower_case_name}/${origin_hash}/${CPM_ARGS_NAME}) - else() - string(SHA1 origin_hash "${origin_parameters}") - set(download_directory ${CPM_SOURCE_CACHE}/${lower_case_name}/${origin_hash}) - endif() - # Expand `download_directory` relative path. This is important because EXISTS doesn't work for - # relative paths. - get_filename_component(download_directory ${download_directory} ABSOLUTE) - list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS SOURCE_DIR ${download_directory}) - - if(CPM_SOURCE_CACHE) - file(LOCK ${download_directory}/../cmake.lock) - endif() - - if(EXISTS ${download_directory}) - if(CPM_SOURCE_CACHE) - file(LOCK ${download_directory}/../cmake.lock RELEASE) - endif() - - cpm_store_fetch_properties( - ${CPM_ARGS_NAME} "${download_directory}" - "${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-build" - ) - cpm_get_fetch_properties("${CPM_ARGS_NAME}") - - if(DEFINED CPM_ARGS_GIT_TAG AND NOT (PATCH_COMMAND IN_LIST CPM_ARGS_UNPARSED_ARGUMENTS)) - # warn if cache has been changed since checkout - cpm_check_git_working_dir_is_clean(${download_directory} ${CPM_ARGS_GIT_TAG} IS_CLEAN) - if(NOT ${IS_CLEAN}) - message( - WARNING "${CPM_INDENT} Cache for ${CPM_ARGS_NAME} (${download_directory}) is dirty" - ) - endif() - endif() - - cpm_add_subdirectory( - "${CPM_ARGS_NAME}" - "${DOWNLOAD_ONLY}" - "${${CPM_ARGS_NAME}_SOURCE_DIR}/${CPM_ARGS_SOURCE_SUBDIR}" - "${${CPM_ARGS_NAME}_BINARY_DIR}" - "${CPM_ARGS_EXCLUDE_FROM_ALL}" - "${CPM_ARGS_SYSTEM}" - "${CPM_ARGS_OPTIONS}" - ) - set(PACKAGE_INFO "${PACKAGE_INFO} at ${download_directory}") - - # As the source dir is already cached/populated, we override the call to FetchContent. - set(CPM_SKIP_FETCH TRUE) - cpm_override_fetchcontent( - "${lower_case_name}" SOURCE_DIR "${${CPM_ARGS_NAME}_SOURCE_DIR}/${CPM_ARGS_SOURCE_SUBDIR}" - BINARY_DIR "${${CPM_ARGS_NAME}_BINARY_DIR}" - ) - - else() - # Enable shallow clone when GIT_TAG is not a commit hash. Our guess may not be accurate, but - # it should guarantee no commit hash get mis-detected. - if(NOT DEFINED CPM_ARGS_GIT_SHALLOW) - cpm_is_git_tag_commit_hash("${CPM_ARGS_GIT_TAG}" IS_HASH) - if(NOT ${IS_HASH}) - list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS GIT_SHALLOW TRUE) - endif() - endif() - - # remove timestamps so CMake will re-download the dependency - file(REMOVE_RECURSE ${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-subbuild) - set(PACKAGE_INFO "${PACKAGE_INFO} to ${download_directory}") - endif() - endif() - - cpm_create_module_file(${CPM_ARGS_NAME} "CPMAddPackage(\"${ARGN}\")") - - if(CPM_PACKAGE_LOCK_ENABLED) - if((CPM_ARGS_VERSION AND NOT CPM_ARGS_SOURCE_DIR) OR CPM_INCLUDE_ALL_IN_PACKAGE_LOCK) - cpm_add_to_package_lock(${CPM_ARGS_NAME} "${ARGN}") - elseif(CPM_ARGS_SOURCE_DIR) - cpm_add_comment_to_package_lock(${CPM_ARGS_NAME} "local directory") - else() - cpm_add_comment_to_package_lock(${CPM_ARGS_NAME} "${ARGN}") - endif() - endif() - - cpm_message( - STATUS "${CPM_INDENT} Adding package ${CPM_ARGS_NAME}@${CPM_ARGS_VERSION} (${PACKAGE_INFO})" - ) - - if(NOT CPM_SKIP_FETCH) - # CMake 3.28 added EXCLUDE, SYSTEM (3.25), and SOURCE_SUBDIR (3.18) to FetchContent_Declare. - # Calling FetchContent_MakeAvailable will then internally forward these options to - # add_subdirectory. Up until these changes, we had to call FetchContent_Populate and - # add_subdirectory separately, which is no longer necessary and has been deprecated as of 3.30. - # A Bug in CMake prevents us to use the non-deprecated functions until 3.30.3. - set(fetchContentDeclareExtraArgs "") - if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.30.3") - if(${CPM_ARGS_EXCLUDE_FROM_ALL}) - list(APPEND fetchContentDeclareExtraArgs EXCLUDE_FROM_ALL) - endif() - if(${CPM_ARGS_SYSTEM}) - list(APPEND fetchContentDeclareExtraArgs SYSTEM) - endif() - if(DEFINED CPM_ARGS_SOURCE_SUBDIR) - list(APPEND fetchContentDeclareExtraArgs SOURCE_SUBDIR ${CPM_ARGS_SOURCE_SUBDIR}) - endif() - # For CMake version <3.28 OPTIONS are parsed in cpm_add_subdirectory - if(CPM_ARGS_OPTIONS AND NOT DOWNLOAD_ONLY) - foreach(OPTION ${CPM_ARGS_OPTIONS}) - cpm_parse_option("${OPTION}") - set(${OPTION_KEY} "${OPTION_VALUE}") - endforeach() - endif() - endif() - cpm_declare_fetch( - "${CPM_ARGS_NAME}" ${fetchContentDeclareExtraArgs} "${CPM_ARGS_UNPARSED_ARGUMENTS}" - ) - - cpm_fetch_package("${CPM_ARGS_NAME}" ${DOWNLOAD_ONLY} populated ${CPM_ARGS_UNPARSED_ARGUMENTS}) - if(CPM_SOURCE_CACHE AND download_directory) - file(LOCK ${download_directory}/../cmake.lock RELEASE) - endif() - if(${populated} AND ${CMAKE_VERSION} VERSION_LESS "3.30.3") - cpm_add_subdirectory( - "${CPM_ARGS_NAME}" - "${DOWNLOAD_ONLY}" - "${${CPM_ARGS_NAME}_SOURCE_DIR}/${CPM_ARGS_SOURCE_SUBDIR}" - "${${CPM_ARGS_NAME}_BINARY_DIR}" - "${CPM_ARGS_EXCLUDE_FROM_ALL}" - "${CPM_ARGS_SYSTEM}" - "${CPM_ARGS_OPTIONS}" - ) - endif() - cpm_get_fetch_properties("${CPM_ARGS_NAME}") - endif() - - set(${CPM_ARGS_NAME}_ADDED YES) - cpm_export_variables("${CPM_ARGS_NAME}") -endfunction() - -# Fetch a previously declared package -macro(CPMGetPackage Name) - if(DEFINED "CPM_DECLARATION_${Name}") - CPMAddPackage(NAME ${Name}) - else() - message(SEND_ERROR "${CPM_INDENT} Cannot retrieve package ${Name}: no declaration available") - endif() -endmacro() - -# export variables available to the caller to the parent scope expects ${CPM_ARGS_NAME} to be set -macro(cpm_export_variables name) - set(${name}_SOURCE_DIR - "${${name}_SOURCE_DIR}" - PARENT_SCOPE - ) - set(${name}_BINARY_DIR - "${${name}_BINARY_DIR}" - PARENT_SCOPE - ) - set(${name}_ADDED - "${${name}_ADDED}" - PARENT_SCOPE - ) - set(CPM_LAST_PACKAGE_NAME - "${name}" - PARENT_SCOPE - ) -endmacro() - -# declares a package, so that any call to CPMAddPackage for the package name will use these -# arguments instead. Previous declarations will not be overridden. -macro(CPMDeclarePackage Name) - if(NOT DEFINED "CPM_DECLARATION_${Name}") - set("CPM_DECLARATION_${Name}" "${ARGN}") - endif() -endmacro() - -function(cpm_add_to_package_lock Name) - if(NOT CPM_DONT_CREATE_PACKAGE_LOCK) - cpm_prettify_package_arguments(PRETTY_ARGN false ${ARGN}) - file(APPEND ${CPM_PACKAGE_LOCK_FILE} "# ${Name}\nCPMDeclarePackage(${Name}\n${PRETTY_ARGN})\n") - endif() -endfunction() - -function(cpm_add_comment_to_package_lock Name) - if(NOT CPM_DONT_CREATE_PACKAGE_LOCK) - cpm_prettify_package_arguments(PRETTY_ARGN true ${ARGN}) - file(APPEND ${CPM_PACKAGE_LOCK_FILE} - "# ${Name} (unversioned)\n# CPMDeclarePackage(${Name}\n${PRETTY_ARGN}#)\n" - ) - endif() -endfunction() - -# includes the package lock file if it exists and creates a target `cpm-update-package-lock` to -# update it -macro(CPMUsePackageLock file) - if(NOT CPM_DONT_CREATE_PACKAGE_LOCK) - get_filename_component(CPM_ABSOLUTE_PACKAGE_LOCK_PATH ${file} ABSOLUTE) - if(EXISTS ${CPM_ABSOLUTE_PACKAGE_LOCK_PATH}) - include(${CPM_ABSOLUTE_PACKAGE_LOCK_PATH}) - endif() - if(NOT TARGET cpm-update-package-lock) - add_custom_target( - cpm-update-package-lock COMMAND ${CMAKE_COMMAND} -E copy ${CPM_PACKAGE_LOCK_FILE} - ${CPM_ABSOLUTE_PACKAGE_LOCK_PATH} - ) - endif() - set(CPM_PACKAGE_LOCK_ENABLED true) - endif() -endmacro() - -# registers a package that has been added to CPM -function(CPMRegisterPackage PACKAGE VERSION) - list(APPEND CPM_PACKAGES ${PACKAGE}) - set(CPM_PACKAGES - ${CPM_PACKAGES} - CACHE INTERNAL "" - ) - set("CPM_PACKAGE_${PACKAGE}_VERSION" - ${VERSION} - CACHE INTERNAL "" - ) -endfunction() - -# retrieve the current version of the package to ${OUTPUT} -function(CPMGetPackageVersion PACKAGE OUTPUT) - set(${OUTPUT} - "${CPM_PACKAGE_${PACKAGE}_VERSION}" - PARENT_SCOPE - ) -endfunction() - -# declares a package in FetchContent_Declare -function(cpm_declare_fetch PACKAGE) - if(${CPM_DRY_RUN}) - cpm_message(STATUS "${CPM_INDENT} Package not declared (dry run)") - return() - endif() - - FetchContent_Declare(${PACKAGE} ${ARGN}) -endfunction() - -# returns properties for a package previously defined by cpm_declare_fetch -function(cpm_get_fetch_properties PACKAGE) - if(${CPM_DRY_RUN}) - return() - endif() - - set(${PACKAGE}_SOURCE_DIR - "${CPM_PACKAGE_${PACKAGE}_SOURCE_DIR}" - PARENT_SCOPE - ) - set(${PACKAGE}_BINARY_DIR - "${CPM_PACKAGE_${PACKAGE}_BINARY_DIR}" - PARENT_SCOPE - ) -endfunction() - -function(cpm_store_fetch_properties PACKAGE source_dir binary_dir) - if(${CPM_DRY_RUN}) - return() - endif() - - set(CPM_PACKAGE_${PACKAGE}_SOURCE_DIR - "${source_dir}" - CACHE INTERNAL "" - ) - set(CPM_PACKAGE_${PACKAGE}_BINARY_DIR - "${binary_dir}" - CACHE INTERNAL "" - ) -endfunction() - -# adds a package as a subdirectory if viable, according to provided options -function( - cpm_add_subdirectory - PACKAGE - DOWNLOAD_ONLY - SOURCE_DIR - BINARY_DIR - EXCLUDE - SYSTEM - OPTIONS -) - - if(NOT DOWNLOAD_ONLY AND EXISTS ${SOURCE_DIR}/CMakeLists.txt) - set(addSubdirectoryExtraArgs "") - if(EXCLUDE) - list(APPEND addSubdirectoryExtraArgs EXCLUDE_FROM_ALL) - endif() - if("${SYSTEM}" AND "${CMAKE_VERSION}" VERSION_GREATER_EQUAL "3.25") - # https://cmake.org/cmake/help/latest/prop_dir/SYSTEM.html#prop_dir:SYSTEM - list(APPEND addSubdirectoryExtraArgs SYSTEM) - endif() - if(OPTIONS) - foreach(OPTION ${OPTIONS}) - cpm_parse_option("${OPTION}") - set(${OPTION_KEY} "${OPTION_VALUE}") - endforeach() - endif() - set(CPM_OLD_INDENT "${CPM_INDENT}") - set(CPM_INDENT "${CPM_INDENT} ${PACKAGE}:") - add_subdirectory(${SOURCE_DIR} ${BINARY_DIR} ${addSubdirectoryExtraArgs}) - set(CPM_INDENT "${CPM_OLD_INDENT}") - endif() -endfunction() - -# downloads a previously declared package via FetchContent and exports the variables -# `${PACKAGE}_SOURCE_DIR` and `${PACKAGE}_BINARY_DIR` to the parent scope -function(cpm_fetch_package PACKAGE DOWNLOAD_ONLY populated) - set(${populated} - FALSE - PARENT_SCOPE - ) - if(${CPM_DRY_RUN}) - cpm_message(STATUS "${CPM_INDENT} Package ${PACKAGE} not fetched (dry run)") - return() - endif() - - FetchContent_GetProperties(${PACKAGE}) - - string(TOLOWER "${PACKAGE}" lower_case_name) - - if(NOT ${lower_case_name}_POPULATED) - if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.30.3") - if(DOWNLOAD_ONLY) - # MakeAvailable will call add_subdirectory internally which is not what we want when - # DOWNLOAD_ONLY is set. Populate will only download the dependency without adding it to the - # build - FetchContent_Populate( - ${PACKAGE} - SOURCE_DIR "${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-src" - BINARY_DIR "${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-build" - SUBBUILD_DIR "${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-subbuild" - ${ARGN} - ) - else() - FetchContent_MakeAvailable(${PACKAGE}) - endif() - else() - FetchContent_Populate(${PACKAGE}) - endif() - set(${populated} - TRUE - PARENT_SCOPE - ) - endif() - - cpm_store_fetch_properties( - ${CPM_ARGS_NAME} ${${lower_case_name}_SOURCE_DIR} ${${lower_case_name}_BINARY_DIR} - ) - - set(${PACKAGE}_SOURCE_DIR - ${${lower_case_name}_SOURCE_DIR} - PARENT_SCOPE - ) - set(${PACKAGE}_BINARY_DIR - ${${lower_case_name}_BINARY_DIR} - PARENT_SCOPE - ) -endfunction() - -# splits a package option -function(cpm_parse_option OPTION) - string(REGEX MATCH "^[^ ]+" OPTION_KEY "${OPTION}") - string(LENGTH "${OPTION}" OPTION_LENGTH) - string(LENGTH "${OPTION_KEY}" OPTION_KEY_LENGTH) - if(OPTION_KEY_LENGTH STREQUAL OPTION_LENGTH) - # no value for key provided, assume user wants to set option to "ON" - set(OPTION_VALUE "ON") - else() - math(EXPR OPTION_KEY_LENGTH "${OPTION_KEY_LENGTH}+1") - string(SUBSTRING "${OPTION}" "${OPTION_KEY_LENGTH}" "-1" OPTION_VALUE) - endif() - set(OPTION_KEY - "${OPTION_KEY}" - PARENT_SCOPE - ) - set(OPTION_VALUE - "${OPTION_VALUE}" - PARENT_SCOPE - ) -endfunction() - -# guesses the package version from a git tag -function(cpm_get_version_from_git_tag GIT_TAG RESULT) - string(LENGTH ${GIT_TAG} length) - if(length EQUAL 40) - # GIT_TAG is probably a git hash - set(${RESULT} - 0 - PARENT_SCOPE - ) - else() - string(REGEX MATCH "v?([0123456789.]*).*" _ ${GIT_TAG}) - set(${RESULT} - ${CMAKE_MATCH_1} - PARENT_SCOPE - ) - endif() -endfunction() - -# guesses if the git tag is a commit hash or an actual tag or a branch name. -function(cpm_is_git_tag_commit_hash GIT_TAG RESULT) - string(LENGTH "${GIT_TAG}" length) - # full hash has 40 characters, and short hash has at least 7 characters. - if(length LESS 7 OR length GREATER 40) - set(${RESULT} - 0 - PARENT_SCOPE - ) - else() - if(${GIT_TAG} MATCHES "^[a-fA-F0-9]+$") - set(${RESULT} - 1 - PARENT_SCOPE - ) - else() - set(${RESULT} - 0 - PARENT_SCOPE - ) - endif() - endif() -endfunction() - -function(cpm_prettify_package_arguments OUT_VAR IS_IN_COMMENT) - set(oneValueArgs - NAME - FORCE - VERSION - GIT_TAG - DOWNLOAD_ONLY - GITHUB_REPOSITORY - GITLAB_REPOSITORY - BITBUCKET_REPOSITORY - GIT_REPOSITORY - SOURCE_DIR - FIND_PACKAGE_ARGUMENTS - NO_CACHE - SYSTEM - GIT_SHALLOW - EXCLUDE_FROM_ALL - SOURCE_SUBDIR - ) - set(multiValueArgs URL OPTIONS DOWNLOAD_COMMAND) - cmake_parse_arguments(CPM_ARGS "" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) - - foreach(oneArgName ${oneValueArgs}) - if(DEFINED CPM_ARGS_${oneArgName}) - if(${IS_IN_COMMENT}) - string(APPEND PRETTY_OUT_VAR "#") - endif() - if(${oneArgName} STREQUAL "SOURCE_DIR") - string(REPLACE ${CMAKE_SOURCE_DIR} "\${CMAKE_SOURCE_DIR}" CPM_ARGS_${oneArgName} - ${CPM_ARGS_${oneArgName}} - ) - endif() - string(APPEND PRETTY_OUT_VAR " ${oneArgName} ${CPM_ARGS_${oneArgName}}\n") - endif() - endforeach() - foreach(multiArgName ${multiValueArgs}) - if(DEFINED CPM_ARGS_${multiArgName}) - if(${IS_IN_COMMENT}) - string(APPEND PRETTY_OUT_VAR "#") - endif() - string(APPEND PRETTY_OUT_VAR " ${multiArgName}\n") - foreach(singleOption ${CPM_ARGS_${multiArgName}}) - if(${IS_IN_COMMENT}) - string(APPEND PRETTY_OUT_VAR "#") - endif() - string(APPEND PRETTY_OUT_VAR " \"${singleOption}\"\n") - endforeach() - endif() - endforeach() - - if(NOT "${CPM_ARGS_UNPARSED_ARGUMENTS}" STREQUAL "") - if(${IS_IN_COMMENT}) - string(APPEND PRETTY_OUT_VAR "#") - endif() - string(APPEND PRETTY_OUT_VAR " ") - foreach(CPM_ARGS_UNPARSED_ARGUMENT ${CPM_ARGS_UNPARSED_ARGUMENTS}) - string(APPEND PRETTY_OUT_VAR " ${CPM_ARGS_UNPARSED_ARGUMENT}") - endforeach() - string(APPEND PRETTY_OUT_VAR "\n") - endif() - - set(${OUT_VAR} - ${PRETTY_OUT_VAR} - PARENT_SCOPE - ) - -endfunction() diff --git a/src/pyaccft/set_cover_solver.py b/src/pyaccft/set_cover_solver.py index f4baa91..3d1f7b4 100644 --- a/src/pyaccft/set_cover_solver.py +++ b/src/pyaccft/set_cover_solver.py @@ -8,7 +8,6 @@ run, Instance, Solution, - DualState, CftResult, )