diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..08aa856 --- /dev/null +++ b/.clang-format @@ -0,0 +1,19 @@ +--- +BasedOnStyle: Microsoft +UseTab: Never +IndentWidth: 4 +ColumnLimit: 100 +--- +Language: Cpp +PointerAlignment: Left +DerivePointerAlignment: false +AllowShortFunctionsOnASingleLine: All +AlignAfterOpenBracket: BlockIndent +AlignEscapedNewlines: Left +BreakBeforeBraces: Attach +BreakAfterAttributes: Always +AlwaysBreakTemplateDeclarations: Yes +AlwaysBreakBeforeMultilineStrings: true +SortIncludes: CaseInsensitive +InsertNewlineAtEOF: true +--- diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..4be0ec3 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,39 @@ +--- +Checks: |- + clang-diagnostic-* + clang-analyzer-*, + cppcoreguidelines-*,-cppcoreguidelines-avoid-magic-numbers + modernize-*, +WarningsAsErrors: '' +FormatStyle: file +HeaderFilterRegex: '' +CheckOptions: + - key: cert-dcl16-c.NewSuffixes + value: L;LL;LU;LLU + - key: cert-oop54-cpp.WarnOnlyIfThisHasSuspiciousField + value: 0 + - key: cppcoreguidelines-explicit-virtual-functions.IgnoreDestructors + value: 1 + - key: cppcoreguidelines-non-private-member-variables-in-classes.IgnoreClassesWithAllMemberVariablesBeingPublic + value: 1 + - key: google-readability-braces-around-statements.ShortStatementLines + value: 1 + - key: google-readability-function-size.StatementThreshold + value: 800 + - key: google-readability-namespace-comments.ShortNamespaceLines + value: 10 + - key: google-readability-namespace-comments.SpacesBeforeComments + value: 2 + - key: modernize-loop-convert.MaxCopySize + value: 16 + - key: modernize-loop-convert.MinConfidence + value: reasonable + - key: modernize-loop-convert.NamingStyle + value: CamelCase + - key: modernize-pass-by-value.IncludeStyle + value: llvm + - key: modernize-replace-auto-ptr.IncludeStyle + value: llvm + - key: modernize-use-nullptr.NullMacros + value: NULL +... diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..3e98e5b --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,22 @@ +## *Who* is the bug affecting? + + +## *What* is affected by this bug? + + +## *When* does this occur? + + +## *Where* on the platform does it happen? + + + +## *How* do we replicate the issue? + + + +## Expected behavior (i.e. solution) + + + +## Other Comments diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..fb8a9e5 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,28 @@ + + +## Description + + +## Motivation and Context + + + +## How has this been tested? + + + + +## Screenshots (if appropriate): + +## Types of changes + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) + +## Checklist: + + +- [ ] My code follows the code style of this project. +- [ ] My change requires a change to the documentation. +- [ ] I have updated the documentation accordingly. diff --git a/.github/workflows/clang-format-check.yml b/.github/workflows/clang-format-check.yml new file mode 100644 index 0000000..b23918f --- /dev/null +++ b/.github/workflows/clang-format-check.yml @@ -0,0 +1,16 @@ +name: clang-format Check + +on: [push, pull_request] + +jobs: + formatting-check: + name: Formatting Check + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Run clang-format style check for C/C++ programs. + uses: jidicula/clang-format-action@v4.11.0 + with: + clang-format-version: '16' diff --git a/.github/workflows/gcc.yml b/.github/workflows/gcc.yml new file mode 100644 index 0000000..ea648a6 --- /dev/null +++ b/.github/workflows/gcc.yml @@ -0,0 +1,48 @@ +name: gcc + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +env: + PLATFORM_NAME: gcc-linux_x64 + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Get Date + id: get-date + run: | + echo "date=$(/bin/date -u "+%Y%m%d")" >> $GITHUB_OUTPUT + shell: bash + + - name: Install latest CMake and Ninja + id: install-cmake + uses: lukka/get-cmake@latest + + - name: Cache Outputs + id: cache-outputs + uses: actions/cache@v4 + with: + path: | + ${{github.workspace}}/out + ${{github.workspace}}/third_party + key: ${{runner.os}}-${{steps.get-date.outputs.date}} + restore-keys: | + ${{runner.os}}- + + - name: Run CMake consuming CMakePresets.json + uses: lukka/run-cmake@v10 + with: + configurePreset: ${{env.PLATFORM_NAME}} + configurePresetAdditionalArgs: "['-DCMAKE_C_COMPILER=gcc-13', '-DCMAKE_CXX_COMPILER=g++-13']" + buildPreset: ${{env.PLATFORM_NAME}} + buildPresetAdditionalArgs: "[]" + testPreset: ${{env.PLATFORM_NAME}} + testPresetAdditionalArgs: "[]" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ca92d09 --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# CMakePresets.json File +CMakeUserPresets.json + +# Build Directory +out/ +build/ + +# third-party dependencies +third_party/ + +# Vscode Configuration +.vscode/ + +# Clangd Cache +.cache/ + +# Youcompleteme Configuration +.ycm_extra_conf.py + +# Force Include Conf +!conf/** diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..e95d9e3 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,58 @@ +cmake_minimum_required(VERSION 3.26.4) + +project(modern-osqp-cpp + LANGUAGES + C + CXX +) + +set(CMAKE_C_STANDARD 17) +set(CMAKE_C_STANDARD_REQUIRED True) +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED True) + +set(CMAKE_C_FLAGS_DEBUG "-Og -g") +set(CMAKE_CXX_FLAGS_DEBUG "-Og -g") + +set(CMAKE_C_FLAGS_RELEASE "-O2 -DNDEBUG") +set(CMAKE_CXX_FLAGS_RELEASE "-O2 -DNDEBUG") + +set(CMAKE_C_FLAGS_RELWITHDEBINFO "-O2 -g -DNDEBUG") +set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "-O2 -g -DNDEBUG") + +set(CMAKE_C_FLAGS_MINSIZEREL "-Os -DNDEBUG") +set(CMAKE_CXX_FLAGS_MINSIZEREL "-Os -DNDEBUG") + +set(CMAKE_COMPILE_WARNING_AS_ERROR True) + +add_compile_options( + -pipe + -fno-plt + -fexceptions + -fstack-clash-protection + -fcf-protection + -Wall + -Wextra + -Wpedantic + -Wno-unused-parameter +) + +set(CPM_SOURCE_CACHE third_party) +set(CPM_USE_LOCAL_PACKAGES True) + +add_compile_definitions( + SPDLOG_ACTIVE_LEVEL=SPDLOG_LEVEL_TRACE + DOCTEST_CONFIG_SUPER_FAST_ASSERTS +) + +include(CTest) +enable_testing() + +include(cmake/CPM.cmake) +include(cmake/third_party.cmake) +include(cmake/utils.cmake) + +include_directories(${PROJECT_SOURCE_DIR}) + +add_subdirectory(src) +add_subdirectory(tests) diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 0000000..07799af --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,104 @@ +{ + "version": 8, + "configurePresets": [ + { + "name": "base_configure", + "displayName": "Base Configure", + "description": "Base configuration for all presets", + "generator": "Ninja", + "binaryDir": "${sourceDir}/out/build/${presetName}", + "installDir": "${sourceDir}/out/install/${presetName}", + "cacheVariables": { + "CMAKE_EXPORT_COMPILE_COMMANDS": true + } + }, + { + "name": "gcc-linux_x64", + "displayName": "GCC 13.2.1 x86_64-pc-linux-gnu", + "description": "Using compilers: C = /usr/bin/gcc, CXX = /usr/bin/g++", + "inherits": [ + "base_configure" + ], + "cacheVariables": { + "CMAKE_C_COMPILER": "/usr/bin/gcc", + "CMAKE_CXX_COMPILER": "/usr/bin/g++", + "CMAKE_C_FLAGS": "-march=native -Wp,-D_FORTIFY_SOURCE=2", + "CMAKE_CXX_FLAGS": "-march=native -Wp,-D_FORTIFY_SOURCE=2", + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "clang-linux_x64", + "displayName": "Clang 16.0.6 x86_64-pc-linux-gnu", + "description": "Using compilers: C = /usr/bin/clang, CXX = /usr/bin/clang++", + "inherits": [ + "base_configure" + ], + "cacheVariables": { + "CMAKE_C_COMPILER": "/usr/bin/clang", + "CMAKE_CXX_COMPILER": "/usr/bin/clang++", + "CMAKE_C_FLAGS": "-march=native -Wp,-D_FORTIFY_SOURCE=2", + "CMAKE_CXX_FLAGS": "-march=native -Wp,-D_FORTIFY_SOURCE=2", + "CMAKE_BUILD_TYPE": "Release" + } + } + ], + "buildPresets": [ + { + "name": "base_build", + "displayName": "Base Build", + "configurePreset": "base_configure", + "targets": "all", + "jobs": 0 + }, + { + "name": "gcc-linux_x64", + "displayName": "GCC 13.2.1 x86_64-pc-linux-gnu", + "configurePreset": "gcc-linux_x64", + "inheritConfigureEnvironment": true, + "inherits": [ + "base_build" + ] + }, + { + "name": "clang-linux_x64", + "displayName": "Clang 16.0.6 x86_64-pc-linux-gnu", + "configurePreset": "clang-linux_x64", + "inheritConfigureEnvironment": true, + "inherits": [ + "base_build" + ] + } + ], + "testPresets": [ + { + "name": "base_test", + "displayName": "Base Test", + "configurePreset": "base_configure", + "output": { + "outputOnFailure": true + }, + "execution": { + "jobs": 0 + } + }, + { + "name": "gcc-linux_x64", + "displayName": "GCC 13.2.1 x86_64-pc-linux-gnu", + "configurePreset": "gcc-linux_x64", + "inheritConfigureEnvironment": true, + "inherits": [ + "base_test" + ] + }, + { + "name": "clang-linux_x64", + "displayName": "Clang 16.0.6 x86_64-pc-linux-gnu", + "configurePreset": "clang-linux_x64", + "inheritConfigureEnvironment": true, + "inherits": [ + "base_test" + ] + } + ] +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..89d495f --- /dev/null +++ b/LICENSE @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (c) 2024, Houchen Li + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/cmake/CPM.cmake b/cmake/CPM.cmake new file mode 100644 index 0000000..70aebf1 --- /dev/null +++ b/cmake/CPM.cmake @@ -0,0 +1,1154 @@ +# 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-2022 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 0.38.1) + +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() +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) + 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) + # erase any previous modules + file(WRITE ${CPM_MODULE_PATH}/Find${Name}.cmake + "include(\"${CPM_FILE}\")\n${ARGN}\nset(${Name}_FOUND TRUE)" + ) + 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_check_if_package_already_added(${CPM_ARGS_NAME} "${CPM_ARGS_VERSION}") + if(CPM_PACKAGE_ALREADY_ADDED) + 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 bug in 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() + +# 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 + DOWNLOAD_COMMAND + FIND_PACKAGE_ARGUMENTS + NO_CACHE + SYSTEM + GIT_SHALLOW + EXCLUDE_FROM_ALL + SOURCE_SUBDIR + ) + + set(multiValueArgs URL OPTIONS) + + 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}" + 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() + + 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_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) + cpm_declare_fetch( + "${CPM_ARGS_NAME}" "${CPM_ARGS_VERSION}" "${PACKAGE_INFO}" "${CPM_ARGS_UNPARSED_ARGUMENTS}" + ) + cpm_fetch_package("${CPM_ARGS_NAME}" populated) + if(CPM_CACHE_SOURCE AND download_directory) + file(LOCK ${download_directory}/../cmake.lock RELEASE) + endif() + if(${populated}) + 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 VERSION INFO) + 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 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) + FetchContent_Populate(${PACKAGE}) + 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 + GIT_REPOSITORY + SOURCE_DIR + DOWNLOAD_COMMAND + FIND_PACKAGE_ARGUMENTS + NO_CACHE + SYSTEM + GIT_SHALLOW + ) + set(multiValueArgs OPTIONS) + 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/cmake/third_party.cmake b/cmake/third_party.cmake new file mode 100644 index 0000000..0a2566e --- /dev/null +++ b/cmake/third_party.cmake @@ -0,0 +1,36 @@ +CPMAddPackage( + NAME spdlog + GITHUB_REPOSITORY "gabime/spdlog" + GIT_TAG "v1.12.0" + OPTIONS + "SPDLOG_BUILD_SHARED OFF" + "SPDLOG_ENABLE_PCH ON" + "SPDLOG_BUILD_PIC ON" + "SPDLOG_TIDY OFF" + "SPDLOG_USE_STD_FORMAT ON" +) + +CPMAddPackage( + NAME doctest + GITHUB_REPOSITORY "doctest/doctest" + GIT_TAG "v2.4.9" + OPTIONS + "DOCTEST_WITH_MAIN_IN_STATIC_LIB ON" + "DOCTEST_NO_INSTALL ON" + "DOCTEST_USE_STD_HEADERS ON" +) + +CPMAddPackage( + NAME osqp + GITHUB_REPOSITORY "osqp/osqp" + GIT_TAG "v1.0.0.beta1" + FORCE True + OPTIONS + "QDLDL_LONG OFF" + "QDLDL_FLOAT OFF" + "OSQP_USE_LONG OFF" + "OSQP_USE_FLOAT OFF" + "OSQP_BUILD_SHARED_LIB OFF" + "OSQP_BUILD_DEMO_EXE OFF" + "CMAKE_COMPILE_WARNING_AS_ERROR OFF" +) diff --git a/cmake/utils.cmake b/cmake/utils.cmake new file mode 100644 index 0000000..38dd30a --- /dev/null +++ b/cmake/utils.cmake @@ -0,0 +1,23 @@ +function(add_header_only_library name header_name) + add_library(${name} INTERFACE) + target_compile_definitions(${name} INTERFACE LIBRARY_HEADER_ONLY) + target_sources(${name} + INTERFACE + ${header_name} + ) +endfunction() + +function(add_doctest_test name source_name) + set(test_name ${name}_test) + add_executable(${test_name} ${source_name}) + target_link_libraries(${test_name} + PRIVATE + doctest::doctest + spdlog::spdlog + ${name} + ) + add_test( + NAME ${test_name} + COMMAND ${test_name} + ) +endfunction() diff --git a/conf/personalized_files/.vscode/c_cpp_properties.json b/conf/personalized_files/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..bb43f52 --- /dev/null +++ b/conf/personalized_files/.vscode/c_cpp_properties.json @@ -0,0 +1,35 @@ +{ + "version": 4, + "env": { + "platformName": "gcc-linux_x64", + "myDefaultIncludePath": [ + "${workspaceFolder}", + "${workspaceFolder}/out/install/${platformName}/include" + ], + "myGccCompilerPath": "/usr/bin/gcc", + "myClangCompilerPath": "/usr/bin/clang" + }, + "configurations": [ + { + "name": "Linux", + "intelliSenseMode": "gcc-x64", + "includePath": [ + "${myDefaultIncludePath}" + ], + "defines": [], + "forcedInclude": [], + "compilerPath": "${myGccCompilerPath}", + "cStandard": "gnu17", + "cppStandard": "gnu++20", + "configurationProvider": "ms-vscode.cmake-tools", + "compileCommands": "${workspaceFolder}/out/build/${platformName}/compile_commands.json", + "browse": { + "path": [ + "${workspaceFolder}" + ], + "limitSymbolsToIncludedHeaders": true, + "databaseFilename": "" + } + } + ] +} diff --git a/conf/personalized_files/.vscode/launch.json b/conf/personalized_files/.vscode/launch.json new file mode 100644 index 0000000..953211e --- /dev/null +++ b/conf/personalized_files/.vscode/launch.json @@ -0,0 +1,55 @@ +{ + // 使用 IntelliSense 了解相关属性。 + // 悬停以查看现有属性的描述。 + // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "(gdb) Launch", + "type": "cppdbg", + "request": "launch", + // Resolved by CMake Tools: + "program": "${command:cmake.launchTargetPath}", + "args": [], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "environment": [ + { + // add the directory where our target was built to the PATHs + // it gets resolved by CMake Tools: + "name": "PATH", + "value": "${env:PATH}:${command:cmake.getLaunchTargetDirectory}" + } + ], + "console": "integratedTerminal", + "MIMode": "gdb", + "setupCommands": [ + { + "description": "Enable pretty-printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + } + ] + }, + { + "name": "(lldb) Launch", + "type": "cppdbg", + "request": "launch", + // Resolved by CMake Tools: + "program": "${command:cmake.launchTargetPath}", + "args": [], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "environment": [ + { + // add the directory where our target was built to the PATHs + // it gets resolved by CMake Tools: + "name": "PATH", + "value": "${env:PATH}:${command:cmake.getLaunchTargetDirectory}" + } + ], + "console": "integratedTerminal", + "MIMode": "lldb" + } + ] +} diff --git a/conf/personalized_files/.vscode/settings.json b/conf/personalized_files/.vscode/settings.json new file mode 100644 index 0000000..870eba8 --- /dev/null +++ b/conf/personalized_files/.vscode/settings.json @@ -0,0 +1,91 @@ +{ + // Basic Settings + "update.mode": "none", + // "window.titleBarStyle": "custom", + // "workbench.colorTheme": "Material Theme Darker", + // "workbench.iconTheme": "eq-material-theme-icons-darker", + // "editor.fontFamily": "'SauceCodePro Nerd Font Mono'", + // "editor.fontWeight": "600", + // "editor.fontSize": 12, + "editor.rulers": [ + 100 + ], + "editor.wordWrap": "on", + "editor.cursorStyle": "line", + "editor.lineNumbers": "on", + "editor.detectIndentation": false, + "editor.tabSize": 4, + "editor.insertSpaces": true, + "editor.formatOnSave": true, + "editor.formatOnPaste": true, + "editor.formatOnSaveMode": "modifications", + "editor.wordWrapColumn": 100, + "editor.wordSeparators": "/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-", + "editor.minimap.enabled": false, + "editor.inlayHints.enabled": "off", + "files.insertFinalNewline": true, + // "terminal.integrated.defaultProfile.linux": "zsh", + // "terminal.integrated.gpuAcceleration": "on", + // "terminal.integrated.fontWeight": "600", + "workbench.editorAssociations": { + "*.ipynb": "jupyter-notebook" + }, + // Extension Settings + "C_Cpp.intelliSenseEngine": "disabled", + "C_Cpp.codeAnalysis.clangTidy.enabled": true, + "C_Cpp.default.cStandard": "gnu17", + "C_Cpp.default.cppStandard": "gnu++20", + "C_Cpp.doxygen.generateOnType": false, + "cmake.showOptionsMovedNotification": false, + "cmake.parallelJobs": 16, + "cmake.configureOnOpen": true, + "cmake.skipConfigureIfCachePresent": true, + "clangd.arguments": [ + "--log=verbose", + "--pretty", + "--all-scopes-completion", + "--completion-style=bundled", + "--header-insertion=never", + "--header-insertion-decorators", + "--function-arg-placeholders", + "--background-index", + "--clang-tidy", + "--fallback-style=Microsoft", + "-j=16", + "--pch-storage=memory", + "--compile-commands-dir=out/build/gcc-linux_x64" + ], + "Codegeex.Privacy": true, + "Codegeex.OnlyKeyControl": true, + "Codegeex.UseSimilarFileForPrompt": true, + // Language Settings + "[cpp]": { + "editor.tabSize": 4, + "editor.defaultFormatter": "llvm-vs-code-extensions.vscode-clangd", + "editor.wordBasedSuggestions": "off", + "editor.suggest.insertMode": "replace", + "editor.autoClosingComments": "never", + "editor.semanticHighlighting.enabled": true + }, + "[python]": { + "editor.tabSize": 4, + "editor.formatOnType": true, + "editor.defaultFormatter": "ms-python.pylint", + "diffEditor.ignoreTrimWhitespace": false, + "gitlens.codeLens.symbolScopes": [ + "!Module" + ], + "editor.wordBasedSuggestions": "off" + }, + "[cmake]": { + "editor.tabSize": 2, + "editor.defaultFormatter": "ms-vscode.cmake-tools" + }, + "[json]": { + "editor.defaultFormatter": "vscode.json-language-features", + "editor.quickSuggestions": { + "strings": true + }, + "editor.suggest.insertMode": "replace" + } +} diff --git a/conf/personalized_files/CMakeUserPresets.json b/conf/personalized_files/CMakeUserPresets.json new file mode 100644 index 0000000..83769e9 --- /dev/null +++ b/conf/personalized_files/CMakeUserPresets.json @@ -0,0 +1,67 @@ +{ + "version": 8, + "configurePresets": [ + { + "name": "gcc-linux_x64-debug", + "displayName": "GCC 13.2.1 x86_64-pc-linux-gnu Debug", + "description": "Using compilers: C = /usr/bin/gcc, CXX = /usr/bin/g++", + "inherits": [ + "gcc-linux_x64" + ], + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + }, + { + "name": "clang-linux_x64-debug", + "displayName": "Clang 16.0.6 x86_64-pc-linux-gnu Debug", + "description": "Using compilers: C = /usr/bin/clang, CXX = /usr/bin/clang++", + "inherits": [ + "clang-linux_x64" + ], + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + } + ], + "buildPresets": [ + { + "name": "gcc-linux_x64-debug", + "displayName": "GCC 13.2.1 x86_64-pc-linux-gnu Debug", + "configurePreset": "gcc-linux_x64-debug", + "inheritConfigureEnvironment": true, + "inherits": [ + "gcc-linux_x64" + ] + }, + { + "name": "clang-linux_x64-debug", + "displayName": "Clang 16.0.6 x86_64-pc-linux-gnu Debug", + "configurePreset": "clang-linux_x64-debug", + "inheritConfigureEnvironment": true, + "inherits": [ + "clang-linux_x64" + ] + } + ], + "testPresets": [ + { + "name": "gcc-linux_x64-debug", + "displayName": "GCC 13.2.1 x86_64-pc-linux-gnu Debug", + "configurePreset": "gcc-linux_x64-debug", + "inheritConfigureEnvironment": true, + "inherits": [ + "gcc-linux_x64" + ] + }, + { + "name": "clang-linux_x64-debug", + "displayName": "Clang 16.0.6 x86_64-pc-linux-gnu Debug", + "configurePreset": "clang-linux_x64-debug", + "inheritConfigureEnvironment": true, + "inherits": [ + "clang-linux_x64" + ] + } + ] +} diff --git a/conf/personalized_files/git_conf/config b/conf/personalized_files/git_conf/config new file mode 100644 index 0000000..98bb27e --- /dev/null +++ b/conf/personalized_files/git_conf/config @@ -0,0 +1,2 @@ +[core] + whitespace = tab-in-indent diff --git a/conf/personalized_files/git_conf/hooks/pre-commit b/conf/personalized_files/git_conf/hooks/pre-commit new file mode 100755 index 0000000..7267971 --- /dev/null +++ b/conf/personalized_files/git_conf/hooks/pre-commit @@ -0,0 +1,64 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed. +# Called by "git commit" with no arguments. The hook should +# exit with non-zero status after issuing an appropriate message if +# it wants to stop the commit. +# +# To enable this hook, rename this file to "pre-commit". + +if git rev-parse --verify HEAD >/dev/null 2>&1 +then + against=HEAD +else + # Initial commit: diff against an empty tree object + against=$(git hash-object -t tree /dev/null) +fi + +# We should pass only added or modified C/C++ source files to +# clang-format and cppcheck. +changed_files=$(git diff-index --cached $against | \ + grep -E '[MA] .*\.(c|cpp|cc|cxx)$' | cut -f 2) + +if [ -n "${changed_files}" ]; then + clang-format \ + --dry-run \ + --Werror \ + ${changed_files} + exit $? +fi + +# If you want to allow non-ASCII filenames set this variable to true. +allownonascii=$(git config --type=bool hooks.allownonascii) + +# Redirect output to stderr. +exec 1>&2 + +# Cross platform projects tend to avoid non-ASCII filenames; prevent +# them from being added to the repository. We exploit the fact that the +# printable range starts at the space character and ends with tilde. +if [ "$allownonascii" != "true" ] && + # Note that the use of brackets around a tr range is ok here, (it's + # even required, for portability to Solaris 10's /usr/bin/tr), since + # the square bracket bytes happen to fall in the designated range. + test $(git diff --cached --name-only --diff-filter=A -z $against | + LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0 +then + cat <<\EOF +Error: Attempt to add a non-ASCII file name. + +This can cause problems if you want to work with people on other platforms. + +To be portable it is advisable to rename the file. + +If you know what you are doing you can disable this check using: + + git config hooks.allownonascii true +EOF + exit 1 +fi + +# If there are whitespace errors, print the offending file names and fail. +exec git diff-index --check --cached $against -- + +exit 0 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..c1bf5bf --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,16 @@ +add_header_only_library(qp_problem qp_problem.hpp) +target_link_libraries(qp_problem + INTERFACE + coo_matrix + lil_matrix +) + +add_header_only_library(osqp_solver osqp_solver.hpp) +target_link_libraries(osqp_solver + INTERFACE + osqpstatic + csc_matrix + qp_problem +) + +add_subdirectory(sparse_matrix) diff --git a/src/osqp_solver.hpp b/src/osqp_solver.hpp new file mode 100644 index 0000000..84e905e --- /dev/null +++ b/src/osqp_solver.hpp @@ -0,0 +1,178 @@ +/** + * @file osqp_solver.h + * @author Houchen Li (houchen_li@hotmail.com) + * @brief + * @version 0.1 + * @date 2023-10-29 + * + * @copyright Copyright (c) 2023 Houchen Li + * All rights reserved. + * + */ + +#pragma once + +extern "C" { +#include "osqp.h" +} + +#include +#include +#include +#include +#include + +#include "src/qp_problem.hpp" +#include "src/sparse_matrix/csc_matrix.hpp" + +namespace osqp { + +namespace detail { + +class [[nodiscard]] ExecOnExit final { + public: + using Callback = std::function; + ExecOnExit(Callback callback) noexcept : callback_(std::move(callback)) {} + + ExecOnExit() noexcept = delete; + ExecOnExit(const ExecOnExit& other) noexcept = delete; + ExecOnExit(ExecOnExit&& other) noexcept = delete; + auto operator=(const ExecOnExit& other) noexcept -> ExecOnExit& = delete; + auto operator=(ExecOnExit&& other) noexcept -> ExecOnExit& = delete; + ~ExecOnExit() { callback_(); } + + private: + Callback callback_; +}; + +} // namespace detail + +using OsqpSettings = OSQPSettings; +using OsqpInfo = OSQPInfo; + +struct [[nodiscard]] OsqpResult final { + std::vector prim_vars; + std::vector prim_inf_cert; + std::vector dual_vars; + std::vector dual_inf_cert; + OsqpInfo info; +}; + +class [[nodiscard]] OsqpSolver final { + public: + [[using gnu: always_inline]] OsqpSolver() noexcept { osqp_set_default_settings(&settings); } + [[using gnu: always_inline]] explicit OsqpSolver(const OsqpSettings& c_settings) noexcept + : settings{c_settings} {} + + OsqpSolver(const OsqpSolver& other) noexcept = delete; + OsqpSolver(OsqpSolver&& other) noexcept = delete; + auto operator=(const OsqpSolver& other) noexcept -> OsqpSolver& = delete; + auto operator=(OsqpSolver&& other) noexcept -> OsqpSolver& = delete; + ~OsqpSolver() noexcept = default; + + template + [[using gnu: pure, flatten, leaf]] [[nodiscard]] + auto solve( + const QpProblem& qp_problem, const std::vector& prim_vars_0 = {}, + const std::vector& dual_vars_0 = {} + ) const -> OsqpResult { +#ifndef NDEBUG + if (!prim_vars_0.empty() && prim_vars_0.size() != qp_problem.num_variables()) { + std::string error_msg = std::format( + "Osqp runtime error detected! Size of prim_vars_0_ and num_vars must be identical: " + "prim_vars_0_.size() = {0:d} while num_vars = {1:d}", + prim_vars_0.size(), qp_problem.num_variables() + ); + throw std::invalid_argument(std::move(error_msg)); + } + if (!dual_vars_0.empty() && dual_vars_0.size() != qp_problem.num_constraints()) { + std::string error_msg = std::format( + "Osqp runtime error detected! Size of dual_vars_0_ and num_cons must be identical: " + "dual_vars_0_.size() = {0:d} while num_cons = {1:d}", + dual_vars_0.size(), qp_problem.num_constraints() + ); + throw std::invalid_argument(std::move(error_msg)); + } +#endif + + OSQPInt exit_flag{0}; + OSQPSolver* solver = static_cast(malloc(sizeof(OSQPSolver))); + OSQPCscMatrix* P = static_cast(malloc(sizeof(OSQPCscMatrix))); + OSQPCscMatrix* A = static_cast(malloc(sizeof(OSQPCscMatrix))); + + detail::ExecOnExit exec_on_exit{[&solver, &P, &A]() -> void { + osqp_cleanup(solver); + free(P); + free(A); + return; + }}; + + const OSQPInt num_vars = static_cast(qp_problem.num_variables()); + const OSQPInt num_cons = static_cast(qp_problem.num_constraints()); + CscMatrix objective_matrix{qp_problem.objective_matrix_}; + std::vector objective_vector{qp_problem.objective_vector_}; + CscMatrix constrain_matrix{qp_problem.constrain_matrix_}; + std::vector lower_bounds{qp_problem.lower_bounds_}; + std::vector upper_bounds{qp_problem.upper_bounds_}; + + csc_set_data( + P, num_vars, num_vars, static_cast(objective_matrix.nnzs()), + const_cast(objective_matrix.values().data()), + const_cast(objective_matrix.innerIndices().data()), + const_cast(objective_matrix.outerIndices().data()) + ); + csc_set_data( + A, num_cons, num_vars, static_cast(constrain_matrix.nnzs()), + const_cast(constrain_matrix.values().data()), + const_cast(constrain_matrix.innerIndices().data()), + const_cast(constrain_matrix.outerIndices().data()) + ); + + exit_flag = osqp_setup( + &solver, P, objective_vector.data(), A, lower_bounds.data(), upper_bounds.data(), + num_cons, num_vars, &settings + ); + if (exit_flag) { + std::string error_msg = std::format( + "Osqp runtime error detected! Not able to setup a OSQPSolver: exit_flag = {0:d}.", + exit_flag + ); + throw std::runtime_error(std::move(error_msg)); + } + + // Set warm start + if (!prim_vars_0.empty() && !dual_vars_0.empty()) { + exit_flag = osqp_warm_start(solver, prim_vars_0.data(), dual_vars_0.data()); + if (exit_flag) { + std::string error_msg = std::format( + "Osqp runtime error detected! Not able to set warm start for a OSQPSolver: " + "exit_flag = {0:d}.", + exit_flag + ); + throw std::runtime_error(std::move(error_msg)); + } + } + + exit_flag = osqp_solve(solver); + if (exit_flag) { + std::string error_msg = std::format( + "Osqp runtime error detected! Not able to execute a OSQPSolver: exit_flag = {0:d}.", + exit_flag + ); + throw std::runtime_error(std::move(error_msg)); + } + + return OsqpResult{ + .prim_vars{solver->solution->x, solver->solution->x + num_vars}, + .prim_inf_cert{ + solver->solution->prim_inf_cert, solver->solution->prim_inf_cert + num_vars}, + .dual_vars{solver->solution->y, solver->solution->y + num_cons}, + .dual_inf_cert{ + solver->solution->dual_inf_cert, solver->solution->dual_inf_cert + num_cons}, + .info{*(solver->info)}}; + } + + OsqpSettings settings{}; +}; + +} // namespace osqp diff --git a/src/qp_problem.hpp b/src/qp_problem.hpp new file mode 100644 index 0000000..b9c54cf --- /dev/null +++ b/src/qp_problem.hpp @@ -0,0 +1,290 @@ +/** + * @file qp_problem.h + * @author Houchen Li (houchen_li@hotmail.com) + * @brief + * @version 0.1 + * @date 2023-10-28 + * + * @copyright Copyright (c) 2023 Houchen Li + * All rights reserved. + * + */ + +#pragma once + +#include +#include +#include +#include + +#include "src/sparse_matrix/coo_matrix.hpp" +#include "src/sparse_matrix/lil_matrix.hpp" + +namespace osqp { + +template +class [[nodiscard]] QpProblem final { + friend class OsqpSolver; + + public: + [[using gnu: always_inline]] QpProblem(std::size_t num_vars, std::size_t num_cons) noexcept + : num_vars_{static_cast(num_vars)}, num_cons_{static_cast(num_cons)}, + objective_matrix_{num_vars, num_vars}, objective_vector_(num_vars, 0.0), + constrain_matrix_{num_cons, num_vars}, + lower_bounds_(num_cons, std::numeric_limits::lowest()), + upper_bounds_(num_cons, std::numeric_limits::max()) {} + + QpProblem() noexcept = default; + QpProblem(const QpProblem& other) noexcept = default; + QpProblem(QpProblem&& other) noexcept = default; + auto operator=(const QpProblem& other) noexcept -> QpProblem& = default; + auto operator=(QpProblem&& other) noexcept -> QpProblem& = default; + ~QpProblem() noexcept = default; + + [[using gnu: always_inline]] + auto resize(std::size_t num_vars, std::size_t num_cons) noexcept -> void { + num_vars_ = static_cast(num_vars); + num_cons_ = static_cast(num_cons); + objective_matrix_.resize(num_vars, num_vars); + objective_vector_.resize(num_vars, 0.0); + constrain_matrix_.resize(num_cons, num_vars); + lower_bounds_.resize(num_cons, std::numeric_limits::lowest()); + upper_bounds_.resize(num_cons, std::numeric_limits::max()); + return; + } + + [[using gnu: always_inline]] + auto clear() noexcept -> void { + num_vars_ = 0; + num_cons_ = 0; + objective_matrix_.clear(); + std::fill(objective_vector_.begin(), objective_vector_.end(), 0.0); + constrain_matrix_.clear(); + std::fill( + lower_bounds_.begin(), lower_bounds_.end(), std::numeric_limits::lowest() + ); + std::fill(upper_bounds_.begin(), upper_bounds_.end(), std::numeric_limits::max()); + return; + } + + [[using gnu: pure, always_inline]] [[nodiscard]] + auto num_variables() const noexcept -> std::size_t { + return objective_matrix_.nrows(); + } + + [[using gnu: pure, always_inline]] [[nodiscard]] + auto num_constraints() const noexcept -> std::size_t { + return constrain_matrix_.nrows(); + } + + [[using gnu: pure]] + auto cost(std::contiguous_iterator auto first, std::sentinel_for auto last) + const noexcept -> Scalar + requires std::floating_point::value_type> + { + if (last - first != num_vars_) { + spdlog::warn( + "Mismatch size detected! The size of state_vec and num_variables has to be " + "identical: state_vec.size() = {0:d} while num_variables() = {1:d}.", + last - first, num_vars_ + ); + return 0.0; + } + Scalar cost{0.0}; + const std::vector& values{objective_matrix_.values()}; + const std::vector& row_indices{objective_matrix_.rowIndices()}; + const std::vector& col_indices{objective_matrix_.colIndices()}; + const Index values_size = values.size(); + for (Index i{0}; i < values_size; ++i) { + const Index row = row_indices[i]; + const Index col = col_indices[i]; + if (row != col) { + cost += values[i] * first[row] * first[col]; + } else { + cost += values[i] * first[row] * first[col] * 0.5; + } + } + for (Index i{0}; i < num_vars_; ++i) { + cost += objective_vector_[i] * first[i]; + } + return cost; + } + + [[using gnu: pure]] + auto validate(std::contiguous_iterator auto first, std::sentinel_for auto last) + const noexcept -> bool + requires std::floating_point::value_type> + { + if (last - first != num_vars_) { + spdlog::warn( + "Mismatch size detected! The size of state_vec and num_variables has to be " + "identical: state_vec.size() = {0:d} while num_variables() = {1:d}.", + last - first, num_vars_ + ); + return false; + } + for (Index i{0}; i < num_cons_; ++i) { + Scalar inner_prod{0.0}; + const std::vector& row_values_ = constrain_matrix_.rowValues(i); + const std::vector& row_indices = constrain_matrix_.rowIndices(i); + for (Index j{0}; j < num_vars_; ++j) { + inner_prod += row_values_[j] * first[row_indices[j]]; + } + if (inner_prod < lower_bounds_[i] || inner_prod > upper_bounds_[i]) { + return false; + } + } + return true; + } + + [[using gnu: always_inline, hot]] + auto addQuadCostTerm(Index row, Index col, Scalar coeff) noexcept -> void { + if (row >= num_vars_ || col >= num_vars_) { + spdlog::warn( + "Out-of-range error detected! The index ({0:d},{1:d}) is not in the allowed range: " + "the allowed range is ({2:d},{3:d}).", + row, col, objective_matrix_.nrows(), objective_matrix_.ncols() + ); + return; + } + if (coeff == 0.0) { + return; + } + if (row < col) { + coeff += objective_matrix_.coeff(row, col); + objective_matrix_.updateCoeff(row, col, coeff); + } else if (row == col) { + coeff *= 2.0; + coeff += objective_matrix_.coeff(row, col); + objective_matrix_.updateCoeff(row, col, coeff); + } else { + spdlog::warn( + "The objective matrix has to be a upper triangular sparse matrix which requires " + "row_index < col_index. row_index: {0:d}, col_index: {1:d}. This term is updated " + "to ({1:d}, {0:d}) instead.", + row, col + ); + coeff += objective_matrix_.coeff(col, row); + objective_matrix_.updateCoeff(col, row, coeff); + } + return; + } + + [[using gnu: always_inline, hot]] + auto updateQuadCostTerm(Index row, Index col, Scalar coeff) noexcept -> void { + if (row >= num_vars_ || col >= num_vars_) { + spdlog::warn( + "Out-of-range error detected! The index ({0:d},{1:d}) is not in the allowed range: " + "the allowed range is ({2:d},{3:d}).", + row, col, objective_matrix_.nrows(), objective_matrix_.ncols() + ); + return; + } + if (row < col) { + objective_matrix_.updateCoeff(row, col, coeff); + } else if (row == col) { + objective_matrix_.updateCoeff(row, col, coeff * 2.0); + } else { + spdlog::warn( + "The objective matrix has to be a upper triangular sparse matrix which requires " + "row_index < col_index. row_index: {0:d}, col_index: {1:d}. This term is updated " + "to ({1:d}, {0:d}) instead.", + row, col + ); + objective_matrix_.updateCoeff(col, row, coeff); + } + return; + } + + [[using gnu: always_inline, hot]] + auto addLinCostTerm(Index row, Scalar coeff) noexcept -> void { + if (row >= num_vars_) { + spdlog::warn( + "Out-of-range error detected! The input row has to be in the allowed range: row = " + "{0:d} while max_row = {1:d}).", + row, objective_vector_.size() + ); + return; + } + objective_vector_[row] += coeff; + return; + } + + [[using gnu: always_inline, hot]] + auto updateLinCostTerm(Index row, Scalar coeff) noexcept -> void { + if (row >= num_vars_) { + spdlog::warn( + "Out-of-range error detected! The input row has to be in the allowed range: row = " + "{0:d} while max_row = {1:d}).", + row, objective_vector_.size() + ); + return; + } + objective_vector_[row] = coeff; + return; + } + + [[using gnu: always_inline, hot]] + auto addClampCostTerm( + std::unordered_map constrain_vec, Scalar offset, Scalar linear_coeff, + Scalar quadratic_coeff = 0.0 + ) noexcept -> void { + objective_matrix_.resize(num_vars_ + 1, num_vars_ + 1); + objective_matrix_.updateCoeff(num_vars_, num_vars_, quadratic_coeff * 2.0); + objective_vector_.push_back(linear_coeff); + constrain_matrix_.resize(num_cons_ + 2, num_vars_ + 1); + constrain_matrix_.updateRow(num_cons_, {{num_vars_, 1.0}}); + lower_bounds_.push_back(0.0); + upper_bounds_.push_back(std::numeric_limits::max()); + constrain_vec.emplace(num_vars_, -1.0); + constrain_matrix_.updateRow(num_cons_ + 1, constrain_vec); + lower_bounds_.push_back(std::numeric_limits::lowest()); + upper_bounds_.push_back(offset); + num_vars_ += 1; + num_cons_ += 2; + return; + } + + [[using gnu: always_inline, hot]] + auto addConstrainTerm( + const std::unordered_map& constrain_vec, Scalar lower_bound, + Scalar upper_bound + ) noexcept -> void { + constrain_matrix_.resize(num_cons_ + 1, constrain_matrix_.ncols()); + constrain_matrix_.updateRow(num_cons_, constrain_vec); + lower_bounds_.push_back(lower_bound); + upper_bounds_.push_back(upper_bound); + num_cons_ += 1; + return; + } + + [[using gnu: always_inline, hot]] + auto updateConstrainTerm( + Index row, const std::unordered_map& constrain_vec, Scalar lower_bound, + Scalar upper_bound + ) noexcept -> void { + if (row >= num_cons_) { + spdlog::warn( + "Out-of-range error detected! The input row has to be in the allowed range: row = " + "{0:d} while max_row = {1:d}).", + row, constrain_matrix_.nrows() + ); + return; + } + constrain_matrix_.updateRow(row, constrain_vec); + lower_bounds_[row] = lower_bound; + upper_bounds_[row] = upper_bound; + return; + } + + private: + Index num_vars_{0}; + Index num_cons_{0}; + CooMatrix objective_matrix_{}; + std::vector objective_vector_{}; + LilMatrix constrain_matrix_{}; + std::vector lower_bounds_{}; + std::vector upper_bounds_{}; +}; + +} // namespace osqp diff --git a/src/sparse_matrix/CMakeLists.txt b/src/sparse_matrix/CMakeLists.txt new file mode 100644 index 0000000..0bd7185 --- /dev/null +++ b/src/sparse_matrix/CMakeLists.txt @@ -0,0 +1,25 @@ +add_header_only_library(sparse_matrix sparse_matrix.hpp) + +add_header_only_library(coo_matrix coo_matrix.hpp) +target_link_libraries(coo_matrix + INTERFACE + sparse_matrix +) + +add_header_only_library(lil_matrix lil_matrix.hpp) +target_link_libraries(lil_matrix + INTERFACE + sparse_matrix +) + +add_header_only_library(csc_matrix csc_matrix.hpp) +target_link_libraries(csc_matrix + INTERFACE + sparse_matrix +) + +add_header_only_library(csr_matrix csr_matrix.hpp) +target_link_libraries(csr_matrix + INTERFACE + sparse_matrix +) diff --git a/src/sparse_matrix/coo_matrix.hpp b/src/sparse_matrix/coo_matrix.hpp new file mode 100644 index 0000000..c153c0a --- /dev/null +++ b/src/sparse_matrix/coo_matrix.hpp @@ -0,0 +1,250 @@ +/** + * @file coo_matrix.hpp + * @author Houchen Li (houchen_li@hotmail.com) + * @brief + * @version 0.1 + * @date 2023-10-20 + * + * @copyright Copyright (c) 2023 Houchen Li + * All rights reserved. + * + */ + +#pragma once + +#include +#include +#include + +#include "spdlog/spdlog.h" + +#include "src/sparse_matrix/sparse_matrix.hpp" + +namespace osqp { + +template +class LilMatrix; + +template +class CscMatrix; + +template +class CsrMatrix; + +template +class [[nodiscard]] CooMatrix final : public SparseMatrix, Scalar, Index> { + friend class LilMatrix; + friend class CscMatrix; + friend class CsrMatrix; + + public: + [[using gnu: always_inline]] explicit CooMatrix(std::size_t nrows, std::size_t ncols) noexcept + : nrows_{nrows}, ncols_{ncols} {} + + CooMatrix() noexcept = default; + CooMatrix(const CooMatrix& other) noexcept = default; + CooMatrix(CooMatrix&& other) noexcept = default; + auto operator=(const CooMatrix& other) noexcept -> CooMatrix& = default; + auto operator=(CooMatrix&& other) noexcept -> CooMatrix& = default; + ~CooMatrix() noexcept override = default; + + [[using gnu: flatten, leaf]] CooMatrix(const LilMatrix& lil_matrix) noexcept + : nrows_{lil_matrix.nrows_}, ncols_{lil_matrix.ncols_} { + const std::size_t values_size = lil_matrix.nnzs(); + offsets_map_.reserve(values_size); + values_.reserve(values_size); + row_indices_.reserve(values_size); + col_indices_.reserve(values_size); + for (const auto& [row, offsets_map] : lil_matrix.row_offsets_map_) { + for (const auto& [col, offset] : offsets_map) { + offsets_map_.emplace(IndexPair{row, col}, values_.size()); + values_.push_back(lil_matrix.row_values_map_.at(row)[offset]); + row_indices_.push_back(row); + col_indices_.push_back(col); + } + } + } + + [[using gnu: pure, always_inline]] [[nodiscard]] + auto nrows() const noexcept -> std::size_t { + return nrows_; + } + + [[using gnu: pure, always_inline]] [[nodiscard]] + auto ncols() const noexcept -> std::size_t { + return ncols_; + } + + [[using gnu: pure, always_inline]] [[nodiscard]] + auto nnzs() const noexcept -> std::size_t { + return values_.size(); + } + + [[using gnu: flatten, leaf]] + auto resize(std::size_t nrows, std::size_t ncols) noexcept -> void { + if (nrows < nrows_ || ncols < ncols_) { + const std::size_t values_size = values_.size(); + std::size_t j{0}; + for (std::size_t i{0}; i < values_size; ++i) { + if (row_indices_[i] >= static_cast(nrows) || + col_indices_[i] >= static_cast(ncols)) { + offsets_map_.erase(IndexPair{row_indices_[i], col_indices_[i]}); + continue; + } + if (j != i) { + offsets_map_[IndexPair{row_indices_[i], col_indices_[i]}] = j; + values_[j] = values_[i]; + row_indices_[j] = row_indices_[i]; + col_indices_[j] = col_indices_[i]; + } + ++j; + } + values_.resize(j); + row_indices_.resize(j); + col_indices_.resize(j); + values_.shrink_to_fit(); + row_indices_.shrink_to_fit(); + col_indices_.shrink_to_fit(); + } + nrows_ = nrows; + ncols_ = ncols; + return; + } + + [[using gnu: pure, always_inline, hot]] + auto coeff(Index row, Index col) const noexcept -> Scalar { + if (row >= static_cast(nrows_) || col >= static_cast(ncols_)) { + spdlog::warn( + "Out of range issue detected! \"row\" and \"col\" should be less than \"nrows\" " + "and \"ncols\" respectively: (row, col) = ({0:d}, {1:d}) while (nrows, ncols) = " + "({2:d}, {3:d}).", + row, col, nrows_, ncols_ + ); + return 0.0; + } + IndexPair index_pair{row, col}; + if (const auto search = offsets_map_.find(index_pair); search != offsets_map_.end()) { + return values_[search->second]; + } + return 0.0; + } + + [[using gnu: always_inline, hot]] + auto updateCoeff(Index row, Index col, Scalar coeff) noexcept -> void { + if (row >= static_cast(nrows_) || col >= static_cast(ncols_)) { + spdlog::warn( + "Out of range issue detected! \"row\" and \"col\" should be less than \"nrows\" " + "and \"ncols\" respectively: (row, col) = ({0:d}, {1:d}) while (nrows, ncols) = " + "({2:d}, {3:d}).", + row, col, nrows_, ncols_ + ); + return; + } + IndexPair index_pair{row, col}; + if (auto search = offsets_map_.find(index_pair); search != offsets_map_.end()) { + const std::size_t offset = search->second; + values_[offset] = coeff; + } else { + if (coeff != 0.0) { + offsets_map_[index_pair] = values_.size(); + values_.push_back(coeff); + row_indices_.push_back(row); + col_indices_.push_back(col); + } + } + return; + } + + [[using gnu: pure, always_inline]] [[nodiscard]] + auto values() const noexcept -> const std::vector& { + return values_; + } + + [[using gnu: pure, always_inline]] [[nodiscard]] + auto rowIndices() const noexcept -> const std::vector& { + return row_indices_; + } + + [[using gnu: pure, always_inline]] [[nodiscard]] + auto colIndices() const noexcept -> const std::vector& { + return col_indices_; + } + + [[using gnu: always_inline]] + auto clear() noexcept -> void { + offsets_map_.clear(); + values_.clear(); + row_indices_.clear(); + col_indices_.clear(); + return; + } + + private: + struct [[nodiscard]] IndexPair final { + Index row; + Index col; + + [[using gnu: pure, always_inline]] + auto + operator==(IndexPair index_pair) const noexcept -> bool { + return row == index_pair.row && col == index_pair.col; + } + }; + + struct [[nodiscard]] IndexPairHash final { + [[using gnu: pure, always_inline]] + auto + operator()(typename CooMatrix::IndexPair index_pair) const noexcept + -> std::size_t { + return std::hash()(index_pair.row) ^ std::hash()(index_pair.col); + } + }; + + struct [[nodiscard]] IndexPairRowMajorCompare final { + [[using gnu: pure, always_inline, leaf]] + auto + operator()(const IndexPair& lhs, const IndexPair& rhs) const noexcept -> bool { + if (lhs.row < rhs.row) { + return true; + } else if (lhs.row > rhs.row) { + return false; + } + if (lhs.col < rhs.col) { + return true; + } else if (lhs.col > rhs.col) { + return false; + } + return false; + } + }; + + struct [[nodiscard]] IndexPairColumnMajorCompare final { + [[using gnu: pure, always_inline, leaf]] + auto + operator()(const IndexPair& lhs, const IndexPair& rhs) const noexcept -> bool { + if (lhs.col < rhs.col) { + return true; + } else if (lhs.col > rhs.col) { + return false; + } + if (lhs.row < rhs.row) { + return true; + } else if (lhs.row > rhs.row) { + return false; + } + return false; + } + }; + + std::size_t nrows_{0}; + std::size_t ncols_{0}; + std::unordered_map< + typename CooMatrix::IndexPair, std::size_t, + typename CooMatrix::IndexPairHash> + offsets_map_{}; + std::vector values_{}; + std::vector row_indices_{}; + std::vector col_indices_{}; +}; + +} // namespace osqp diff --git a/src/sparse_matrix/csc_matrix.hpp b/src/sparse_matrix/csc_matrix.hpp new file mode 100644 index 0000000..dc456b6 --- /dev/null +++ b/src/sparse_matrix/csc_matrix.hpp @@ -0,0 +1,231 @@ +/** + * @file csc_matrix.hpp + * @author Houchen Li (houchen_li@hotmail.com) + * @brief + * @version 0.1 + * @date 2023-10-20 + * + * @copyright Copyright (c) 2023 Houchen Li + * All rights reserved. + * + */ + +#pragma once + +#include +#include +#include +#include +#include + +#include "spdlog/spdlog.h" + +#include "src/sparse_matrix/sparse_matrix.hpp" + +namespace osqp { + +template +class CooMatrix; + +template +class LilMatrix; + +template +class [[nodiscard]] CscMatrix final : public SparseMatrix, Scalar, Index> { + public: + [[using gnu: always_inline]] explicit CscMatrix(std::size_t nrows, std::size_t ncols) noexcept + : nrows_{nrows}, outer_indices_(ncols + 1, 0) {} + + CscMatrix() noexcept = default; + CscMatrix(const CscMatrix& other) noexcept = default; + CscMatrix(CscMatrix&& other) noexcept = default; + auto operator=(const CscMatrix& other) noexcept -> CscMatrix& = default; + auto operator=(CscMatrix&& other) noexcept -> CscMatrix& = default; + ~CscMatrix() noexcept override = default; + + [[using gnu: flatten, leaf]] CscMatrix(const CooMatrix& coo_matrix) noexcept + : nrows_{coo_matrix.nrows_} { + std::map< + typename CooMatrix::IndexPair, std::size_t, + typename CooMatrix::IndexPairColumnMajorCompare> + offsets_map{coo_matrix.offsets_map_.cbegin(), coo_matrix.offsets_map_.cend()}; + const std::size_t nnzs = coo_matrix.nnzs(); + values_.reserve(nnzs); + inner_indices_.reserve(nnzs); + outer_indices_.reserve(coo_matrix.ncols_ + 1); + outer_indices_.push_back(0); + for (const auto& [index_pair, offset] : offsets_map) { + values_.push_back(coo_matrix.values_[offset]); + inner_indices_.push_back(index_pair.row); + const std::size_t diff_col = index_pair.col - outer_indices_.size() + 1; + for (std::size_t i{0}; i < diff_col; ++i) { + outer_indices_.push_back(values_.size() - 1); + } + } + for (std::size_t i = outer_indices_.size(); i < coo_matrix.ncols_ + 1; ++i) { + outer_indices_.push_back(nnzs); + } + } + + [[using gnu: always_inline]] CscMatrix(const LilMatrix& lil_matrix) noexcept { + CooMatrix coo_matrix{lil_matrix}; + *this = std::move(coo_matrix); + } + + [[using gnu: pure, always_inline]] [[nodiscard]] + auto nrows() const noexcept -> std::size_t { + return nrows_; + } + + [[using gnu: pure, always_inline]] [[nodiscard]] + auto ncols() const noexcept -> std::size_t { + return outer_indices_.size() - 1; + } + + [[using gnu: pure, always_inline]] [[nodiscard]] + auto nnzs() const noexcept -> std::size_t { + return values_.size(); + } + + [[using gnu: flatten, leaf]] + auto resize(std::size_t nrows, std::size_t ncols) noexcept -> void { + if (ncols >= outer_indices_.size() - 1) { + for (std::size_t i = outer_indices_.size() - 1; i < ncols; ++i) { + outer_indices_.push_back(outer_indices_.back()); + } + } else { + outer_indices_.resize(ncols + 1); + values_.resize(outer_indices_.back()); + inner_indices_.resize(outer_indices_.back()); + } + const std::size_t values_size{values_.size()}; + if (nrows < nrows_) { + std::size_t j{0}, k{0}; + nrows_ = nrows; + for (std::size_t i{0}; i < values_size; ++i) { + if (inner_indices_[i] >= static_cast(nrows_)) { + continue; + } + if (j != i) { + values_[j] = values_[i]; + inner_indices_[j] = inner_indices_[i]; + if (i == static_cast(outer_indices_[k])) { + outer_indices_[k] = j; + } + } + ++j; + if (i == static_cast(outer_indices_[k])) { + ++k; + } + } + outer_indices_[ncols] = j; + values_.resize(j); + inner_indices_.resize(j); + outer_indices_.shrink_to_fit(); + values_.shrink_to_fit(); + inner_indices_.shrink_to_fit(); + } + return; + } + + [[using gnu: pure, always_inline, hot]] + auto coeff(Index row, Index col) const noexcept -> Scalar { + if (row >= static_cast(nrows_) || + col >= static_cast(outer_indices_.size() - 1)) { + spdlog::warn( + "Out of range issue detected! \"row\" and \"col\" should be less than \"nrows\" " + "and \"ncols\" respectively: (row, col) = ({0:d}, {1:d}) while (nrows, ncols) = " + "({2:d}, {3:d}).", + row, col, nrows_, outer_indices_.size() - 1 + ); + return 0.0; + } + if (const auto search = std::find( + inner_indices_.cbegin() + outer_indices_[col], + inner_indices_.cbegin() + outer_indices_[col + 1], row + ); + search != inner_indices_.cbegin() + outer_indices_[col + 1]) { + return values_[search - inner_indices_.cbegin()]; + } + return 0.0; + } + + [[using gnu: hot]] + auto updateCoeff(Index row, Index col, Scalar value) noexcept -> void { + if (row >= static_cast(nrows_) || + col >= static_cast(outer_indices_.size() - 1)) { + spdlog::warn( + "Out of range issue detected! \"row\" and \"col\" should be less than \"nrows\" " + "and \"ncols\" respectively: (row, col) = ({0:d}, {1:d}) while (nrows, ncols) = " + "({2:d}, {3:d}).", + row, col, nrows_, outer_indices_.size() - 1 + ); + return; + } + if (const auto search = std::find( + inner_indices_.cbegin() + outer_indices_[col], + inner_indices_.cbegin() + outer_indices_[col + 1], row + ); + search != inner_indices_.cbegin() + outer_indices_[col + 1]) { + const std::size_t inner_offset = search - inner_indices_.cbegin(); + if (value != 0.0) { + values_[inner_offset] = value; + } else { + values_.erase(values_.begin() + inner_offset); + inner_indices_.erase(inner_indices_.begin() + inner_offset); + for (auto outer_it = outer_indices_.begin() + col + 1; + outer_it != outer_indices_.end(); ++outer_it) { + --(*outer_it); + } + } + return; + } else { + if (value != 0.0) { + const std::size_t inner_offset = + std::upper_bound( + inner_indices_.cbegin() + outer_indices_[col], + inner_indices_.cbegin() + outer_indices_[col + 1], row + ) - + inner_indices_.cbegin(); + values_.insert(values_.begin() + inner_offset, value); + inner_indices_.insert(inner_indices_.begin() + inner_offset, row); + for (auto outer_it = outer_indices_.begin() + col + 1; + outer_it != outer_indices_.end(); ++outer_it) { + ++(*outer_it); + } + } + } + return; + } + + [[using gnu: pure, always_inline]] [[nodiscard]] + auto values() const noexcept -> const std::vector& { + return values_; + } + + [[using gnu: pure, always_inline]] [[nodiscard]] + auto innerIndices() const noexcept -> const std::vector& { + return inner_indices_; + } + + [[using gnu: pure, always_inline]] [[nodiscard]] + auto outerIndices() const noexcept -> const std::vector& { + return outer_indices_; + } + + [[using gnu: always_inline]] + auto clear() noexcept -> void { + values_.clear(); + inner_indices_.clear(); + std::fill(outer_indices_.begin(), outer_indices_.end(), 0); + return; + } + + private: + std::size_t nrows_{0}; + std::vector values_{}; + std::vector inner_indices_{}; + std::vector outer_indices_{}; +}; + +} // namespace osqp diff --git a/src/sparse_matrix/csr_matrix.hpp b/src/sparse_matrix/csr_matrix.hpp new file mode 100644 index 0000000..f90b5a0 --- /dev/null +++ b/src/sparse_matrix/csr_matrix.hpp @@ -0,0 +1,231 @@ +/** + * @file csr_matrix.hpp + * @author Houchen Li (houchen_li@hotmail.com) + * @brief + * @version 0.1 + * @date 2023-10-20 + * + * @copyright Copyright (c) 2023 Houchen Li + * All rights reserved. + * + */ + +#pragma once + +#include +#include +#include +#include +#include + +#include "spdlog/spdlog.h" + +#include "src/sparse_matrix/sparse_matrix.hpp" + +namespace osqp { + +template +class CooMatrix; + +template +class LilMatrix; + +template +class [[nodiscard]] CsrMatrix final : public SparseMatrix, Scalar, Index> { + public: + [[using gnu: always_inline]] explicit CsrMatrix(std::size_t nrows, std::size_t ncols) noexcept + : ncols_{ncols}, outer_indices_(nrows + 1, 0) {} + + CsrMatrix() noexcept = default; + CsrMatrix(const CsrMatrix& other) noexcept = default; + CsrMatrix(CsrMatrix&& other) noexcept = default; + auto operator=(const CsrMatrix& other) noexcept -> CsrMatrix& = default; + auto operator=(CsrMatrix&& other) noexcept -> CsrMatrix& = default; + ~CsrMatrix() noexcept override = default; + + [[using gnu: flatten, leaf]] CsrMatrix(const CooMatrix& coo_matrix) noexcept + : ncols_{coo_matrix.ncols_} { + std::map< + typename CooMatrix::IndexPair, std::size_t, + typename CooMatrix::IndexPairRowMajorCompare> + offsets_map{coo_matrix.offsets_map_.cbegin(), coo_matrix.offsets_map_.cend()}; + const std::size_t nnzs = coo_matrix.nnzs(); + values_.reserve(nnzs); + inner_indices_.reserve(nnzs); + outer_indices_.reserve(coo_matrix.nrows_ + 1); + outer_indices_.push_back(0); + for (const auto& [index_pair, offset] : offsets_map) { + values_.push_back(coo_matrix.values_[offset]); + inner_indices_.push_back(index_pair.col); + const std::size_t diff_row = index_pair.row - outer_indices_.size() + 1; + for (std::size_t i{0}; i < diff_row; ++i) { + outer_indices_.push_back(values_.size() - 1); + } + } + for (std::size_t i = outer_indices_.size(); i < coo_matrix.nrows_ + 1; ++i) { + outer_indices_.push_back(nnzs); + } + } + + [[using gnu: always_inline]] CsrMatrix(const LilMatrix& lil_matrix) noexcept { + CooMatrix coo_matrix{lil_matrix}; + *this = std::move(coo_matrix); + } + + [[using gnu: pure, always_inline]] [[nodiscard]] + auto nrows() const noexcept -> std::size_t { + return outer_indices_.size() - 1; + } + + [[using gnu: pure, always_inline]] [[nodiscard]] + auto ncols() const noexcept -> std::size_t { + return ncols_; + } + + [[using gnu: pure, always_inline]] [[nodiscard]] + auto nnzs() const noexcept -> std::size_t { + return values_.size(); + } + + [[using gnu: flatten, leaf]] + auto resize(std::size_t nrows, std::size_t ncols) noexcept -> void { + if (nrows >= outer_indices_.size() - 1) { + for (std::size_t i = outer_indices_.size() - 1; i < nrows; ++i) { + outer_indices_.push_back(outer_indices_.back()); + } + } else { + outer_indices_.resize(nrows + 1); + values_.resize(outer_indices_.back()); + inner_indices_.resize(outer_indices_.back()); + } + const std::size_t values_size{values_.size()}; + if (ncols < ncols_) { + std::size_t j{0}, k{0}; + ncols_ = ncols; + for (std::size_t i{0}; i < values_size; ++i) { + if (inner_indices_[i] >= static_cast(ncols_)) { + continue; + } + if (j != i) { + values_[j] = values_[i]; + inner_indices_[j] = inner_indices_[i]; + if (i == static_cast(outer_indices_[k])) { + outer_indices_[k] = j; + } + } + ++j; + if (i == static_cast(outer_indices_[k])) { + ++k; + } + } + outer_indices_[ncols] = j; + values_.resize(j); + inner_indices_.resize(j); + outer_indices_.shrink_to_fit(); + values_.shrink_to_fit(); + inner_indices_.shrink_to_fit(); + } + return; + } + + [[using gnu: pure, always_inline, hot]] + auto coeff(Index row, Index col) const noexcept -> Scalar { + if (row >= static_cast(outer_indices_.size() - 1) || + col >= static_cast(ncols_)) { + spdlog::warn( + "Out of range issue detected! \"row\" and \"col\" should be less than \"nrows\" " + "and \"ncols\" respectively: (row, col) = ({0:d}, {1:d}) while (nrows, ncols) = " + "({2:d}, {3:d}).", + row, col, outer_indices_.size() - 1, ncols_ + ); + return 0.0; + } + if (const auto search = std::find( + inner_indices_.cbegin() + outer_indices_[row], + inner_indices_.cbegin() + outer_indices_[row + 1], col + ); + search != inner_indices_.cbegin() + outer_indices_[row + 1]) { + return values_[search - inner_indices_.cbegin()]; + } + return 0.0; + } + + [[using gnu: hot]] + auto updateCoeff(Index row, Index col, Scalar value) noexcept -> void { + if (row >= static_cast(outer_indices_.size() - 1) || + col >= static_cast(ncols_)) { + spdlog::warn( + "Out of range issue detected! \"row\" and \"col\" should be less than \"nrows\" " + "and \"ncols\" respectively: (row, col) = ({0:d}, {1:d}) while (nrows, ncols) = " + "({2:d}, {3:d}).", + row, col, outer_indices_.size() - 1, ncols_ + ); + return; + } + if (const auto search = std::find( + inner_indices_.cbegin() + outer_indices_[row], + inner_indices_.cbegin() + outer_indices_[row + 1], col + ); + search != inner_indices_.cbegin() + outer_indices_[row + 1]) { + const std::size_t inner_offset = search - inner_indices_.cbegin(); + if (value != 0.0) { + values_[inner_offset] = value; + } else { + values_.erase(values_.begin() + inner_offset); + inner_indices_.erase(inner_indices_.begin() + inner_offset); + for (auto outer_it = outer_indices_.begin() + row + 1; + outer_it != outer_indices_.end(); ++outer_it) { + --(*outer_it); + } + } + return; + } else { + if (value != 0.0) { + const std::size_t inner_offset = + std::upper_bound( + inner_indices_.cbegin() + outer_indices_[row], + inner_indices_.cbegin() + outer_indices_[row + 1], col + ) - + inner_indices_.cbegin(); + values_.insert(values_.begin() + inner_offset, value); + inner_indices_.insert(inner_indices_.begin() + inner_offset, col); + for (auto outer_it = outer_indices_.begin() + row + 1; + outer_it != outer_indices_.end(); ++outer_it) { + ++(*outer_it); + } + } + } + return; + } + + [[using gnu: pure, always_inline]] [[nodiscard]] + auto values() const noexcept -> const std::vector& { + return values_; + } + + [[using gnu: pure, always_inline]] [[nodiscard]] + auto innerIndices() const noexcept -> const std::vector& { + return inner_indices_; + } + + [[using gnu: pure, always_inline]] [[nodiscard]] + auto outerIndices() const noexcept -> const std::vector& { + return outer_indices_; + } + + [[using gnu: always_inline]] + auto clear() noexcept -> void { + values_.clear(); + inner_indices_.clear(); + std::fill(outer_indices_.begin(), outer_indices_.end(), 0); + return; + } + + private: + std::size_t ncols_{0}; + std::vector values_{}; + std::vector inner_indices_{}; + std::vector outer_indices_{}; +}; + +} // namespace osqp diff --git a/src/sparse_matrix/lil_matrix.hpp b/src/sparse_matrix/lil_matrix.hpp new file mode 100644 index 0000000..e2c32e4 --- /dev/null +++ b/src/sparse_matrix/lil_matrix.hpp @@ -0,0 +1,327 @@ +/** + * @file lil_matrix.hpp + * @author Houchen Li (houchen_li@hotmail.com) + * @brief + * @version 0.1 + * @date 2023-10-20 + * + * @copyright Copyright (c) 2023 Houchen Li + * All rights reserved. + * + */ + +#pragma once + +#include +#include +#include +#include +#include + +#include "spdlog/spdlog.h" + +#include "src/sparse_matrix/sparse_matrix.hpp" + +namespace osqp { + +template +class CooMatrix; + +template +class CscMatrix; + +template +class [[nodiscard]] LilMatrix final : public SparseMatrix, Scalar, Index> { + friend class CooMatrix; + + public: + [[using gnu: always_inline]] explicit LilMatrix(std::size_t nrows, std::size_t ncols) noexcept + : nrows_{nrows}, ncols_{ncols} {} + + LilMatrix() noexcept = default; + LilMatrix(const LilMatrix& other) noexcept = default; + LilMatrix(LilMatrix&& other) noexcept = default; + auto operator=(const LilMatrix& other) noexcept -> LilMatrix& = default; + auto operator=(LilMatrix&& other) noexcept -> LilMatrix& = default; + ~LilMatrix() noexcept override = default; + + [[using gnu: flatten, leaf]] LilMatrix(const CooMatrix& coo_matrix) noexcept + : nrows_{coo_matrix.nrows_}, ncols_{coo_matrix.ncols_}, nnzs_{coo_matrix.values_.size()} { + for (const auto& [index_pair, offset] : coo_matrix.offsets_map_) { + row_offsets_map_[index_pair.row].emplace( + index_pair.col, row_values_map_[index_pair.row].size() + ); + row_values_map_[index_pair.row].emplace_back(coo_matrix.values_[offset]); + row_indices_map_[index_pair.row].emplace_back(index_pair.col); + } + for (auto& [row, values] : row_values_map_) { + values.shrink_to_fit(); + } + for (auto& [row, indices] : row_indices_map_) { + indices.shrink_to_fit(); + } + } + + [[using gnu: pure, always_inline]] [[nodiscard]] + auto nrows() const noexcept -> std::size_t { + return nrows_; + } + + [[using gnu: pure, always_inline]] [[nodiscard]] + auto ncols() const noexcept -> std::size_t { + return ncols_; + } + + [[using gnu: pure, always_inline]] [[nodiscard]] + auto nnzs() const noexcept -> std::size_t { + return nnzs_; + } + + [[using gnu: flatten, leaf]] + auto resize(std::size_t nrows, std::size_t ncols) noexcept -> void { + if (nrows < nrows_ || ncols < ncols_) { + std::vector remove_rows; + for (const auto& [row, row_offsets_map] : row_offsets_map_) { + if (row >= static_cast(nrows)) { + remove_rows.push_back(row); + } + } + for (const Index row : remove_rows) { + row_offsets_map_.erase(row); + nnzs_ -= row_values_map_[row].size(); + row_values_map_.erase(row); + row_indices_map_.erase(row); + } + remove_rows.clear(); + for (auto& [row, indices] : row_indices_map_) { + nnzs_ -= indices.size(); + std::size_t j{0}; + for (std::size_t i{0}; i < indices.size(); ++i) { + if (indices[i] >= static_cast(ncols)) { + row_offsets_map_[row].erase(indices[i]); + continue; + } + if (j != i) { + row_offsets_map_[row][indices[i]] = j; + row_values_map_[row][j] = row_values_map_[row][i]; + indices[j] = indices[i]; + } + ++j; + } + if (j != 0) { + row_values_map_[row].resize(j); + row_indices_map_[row].resize(j); + indices.shrink_to_fit(); + indices.shrink_to_fit(); + nnzs_ += j; + } else { + remove_rows.push_back(row); + } + } + for (const Index row : remove_rows) { + row_offsets_map_.erase(row); + row_values_map_.erase(row); + row_indices_map_.erase(row); + } + remove_rows.clear(); + } + nrows_ = nrows; + ncols_ = ncols; + return; + } + + [[using gnu: pure, always_inline, hot]] + auto coeff(Index row, Index col) const noexcept -> Scalar { + if (row >= static_cast(nrows_) || col >= static_cast(ncols_)) { + spdlog::warn( + "Out of range issue detected! \"row\" and \"col\" should be less than \"nrows\" " + "and \"ncols\" respectively: (row, col) = ({0:d}, {1:d}) while (nrows, ncols) = " + "({2:d}, {3:d}).", + row, col, nrows_, ncols_ + ); + return 0.0; + } + if (const auto search_row = row_offsets_map_.find(row); + search_row != row_offsets_map_.end()) { + if (const auto search_col = search_row->second.find(col); + search_col != search_row->second.end()) { + const std::size_t offset = search_col->second; + return row_values_map_.at(row)[offset]; + } + } + return 0.0; + } + + [[using gnu: hot]] + auto updateRow(Index row, std::vector values, std::vector indices) noexcept + -> void { + if (row >= static_cast(nrows_)) { + spdlog::warn( + "Out of range issue detected! \"row\" should be less than \"nrows\": row = {0:d} " + "while nrows = {1:d}.", + row, nrows_ + ); + return; + } + if (values.size() != indices.size()) { + const std::size_t values_size{std::min(values.size(), indices.size())}; + values.resize(values_size); + indices.resize(values_size); + } + if (auto search = row_offsets_map_.find(row); search != row_offsets_map_.end()) { + row_offsets_map_.erase(search); + } + if (auto search = row_values_map_.find(row); search != row_values_map_.end()) { + nnzs_ -= search->second.size(); + row_values_map_.erase(search); + } + if (auto search = row_indices_map_.find(row); search != row_indices_map_.end()) { + row_indices_map_.erase(search); + } + if (values.size() == 0) { + return; + } + const std::size_t values_size = values.size(); + std::unordered_map indices_map; + indices_map.reserve(values_size); + std::size_t j = 0; + for (std::size_t i = 0; i < values_size; ++i) { + if (indices[i] >= static_cast(ncols_)) { + spdlog::warn( + "Out of range issue detected! \"col\" should be less than \"ncols\": col = " + "{0:d} while ncols = {1:d}.", + indices[i], ncols_ + ); + continue; + } + if (values[i] == 0.0) { + continue; + } + indices_map.emplace(indices[i], j); + if (j != i) { + values[j] = values[i]; + indices[j] = indices[i]; + } + ++j; + } + if (j != 0) { + values.resize(j); + indices.resize(j); + values.shrink_to_fit(); + indices.shrink_to_fit(); + row_offsets_map_.emplace(row, std::move(indices_map)); + nnzs_ += values.size(); + row_values_map_.emplace(row, std::move(values)); + row_indices_map_.emplace(row, std::move(indices)); + } + return; + } + + [[using gnu: hot]] + auto updateRow(Index row, const std::unordered_map& values_map) noexcept + -> void { + if (row >= static_cast(nrows_)) { + spdlog::warn( + "Out of range issue detected! \"row\" should be less than \"nrows\": row = {0:d} " + "while nrows = {1:d}.", + row, nrows_ + ); + return; + } + if (auto search = row_offsets_map_.find(row); search != row_offsets_map_.end()) { + row_offsets_map_.erase(search); + } + if (auto search = row_values_map_.find(row); search != row_values_map_.end()) { + nnzs_ -= search->second.size(); + row_values_map_.erase(search); + } + if (auto search = row_indices_map_.find(row); search != row_indices_map_.end()) { + row_indices_map_.erase(search); + } + if (values_map.size() == 0) { + return; + } + std::unordered_map indices_map; + indices_map.reserve(values_map.size()); + std::vector values; + values.reserve(values_map.size()); + std::vector indices; + indices.reserve(values_map.size()); + for (const auto& [col, value] : values_map) { + if (col >= static_cast(ncols_)) { + spdlog::warn( + "Out of range issue detected! \"col\" should be less than \"ncols\": col = " + "{0:d} while ncols = {1:d}.", + col, ncols_ + ); + continue; + } + if (value == 0.0) { + continue; + } + indices_map.emplace(col, values.size()); + values.push_back(value); + indices.push_back(col); + } + if (!values.empty()) { + values.shrink_to_fit(); + indices.shrink_to_fit(); + row_offsets_map_.emplace(row, std::move(indices_map)); + nnzs_ += values.size(); + row_values_map_.emplace(row, std::move(values)); + row_indices_map_.emplace(row, std::move(indices)); + } + return; + } + + [[using gnu: pure, always_inline]] + auto rowValues(Index row) const noexcept -> std::vector { + if (row >= static_cast(nrows_)) { + spdlog::warn( + "Out of range issue detected! \"row\" should be less than \"nrows\": row = {0:d} " + "while nrows = {1:d}.", + row, nrows_ + ); + return {}; + } + if (const auto search = row_offsets_map_.find(row); search != row_offsets_map_.end()) { + return row_values_map_.at(row); + } + return {}; + } + + [[using gnu: pure, always_inline]] + auto rowIndices(Index row) const noexcept -> std::vector { + if (row >= static_cast(nrows_)) { + spdlog::warn( + "Out of range issue detected! \"row\" should be less than \"nrows\": row = {0:d} " + "while nrows = {1:d}.", + row, nrows_ + ); + return {}; + } + if (const auto search = row_offsets_map_.find(row); search != row_offsets_map_.end()) { + return row_indices_map_.at(row); + } + return {}; + } + + [[using gnu: always_inline]] + auto clear() noexcept -> void { + nnzs_ = 0; + row_offsets_map_.clear(); + row_values_map_.clear(); + row_indices_map_.clear(); + return; + } + + private: + std::size_t nrows_{0}; + std::size_t ncols_{0}; + std::size_t nnzs_{0}; + std::unordered_map> row_offsets_map_{}; + std::unordered_map> row_values_map_{}; + std::unordered_map> row_indices_map_{}; +}; + +} // namespace osqp diff --git a/src/sparse_matrix/sparse_matrix.hpp b/src/sparse_matrix/sparse_matrix.hpp new file mode 100644 index 0000000..6b34644 --- /dev/null +++ b/src/sparse_matrix/sparse_matrix.hpp @@ -0,0 +1,78 @@ +/** + * @file sparse_matrix.hpp + * @author Houchen Li (houchen_li@hotmail.com) + * @brief + * @version 0.1 + * @date 2023-10-19 + * + * @copyright Copyright (c) 2023 Houchen Li + * All rights reserved. + * + */ + +#pragma once + +#include +#include +#include + +namespace osqp { + +template +class [[nodiscard]] SparseMatrix { + public: + SparseMatrix() noexcept = default; + SparseMatrix(const SparseMatrix& other) noexcept = default; + SparseMatrix(SparseMatrix&& other) noexcept = default; + auto operator=(const SparseMatrix& other) noexcept -> SparseMatrix& = default; + auto operator=(SparseMatrix&& other) noexcept -> SparseMatrix& = default; + virtual ~SparseMatrix() noexcept = 0; + + [[using gnu: pure, always_inline]] [[nodiscard]] + auto nrows() const noexcept -> std::size_t { + return underlying()->nrows(); + } + + [[using gnu: pure, always_inline]] [[nodiscard]] + auto ncols() const noexcept -> std::size_t { + return underlying()->ncols(); + } + + [[using gnu: pure, always_inline]] [[nodiscard]] + auto nnzs() const noexcept -> std::size_t { + return underlying()->nnzs(); + } + + [[using gnu: always_inline]] + auto resize(std::size_t nrows, std::size_t ncols) noexcept -> void { + underlying()->resize(nrows, ncols); + return; + } + + [[using gnu: pure, always_inline]] + auto coeff(Index row, Index col) const noexcept -> Scalar { + return underlying()->coeff(row, col); + } + + [[using gnu: pure, always_inline]] + auto + operator()(Index row, Index col) const noexcept -> Scalar { + return coeff(row, col); + } + + private: + [[using gnu: pure, always_inline]] + auto underlying() noexcept -> DerivedMatrix* { + return static_cast(this); + } + + [[using gnu: pure, always_inline]] + auto underlying() const noexcept -> const DerivedMatrix* { + return static_cast(this); + } +}; + +template +inline SparseMatrix::~SparseMatrix() noexcept = default; + +} // namespace osqp diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..4777a2c --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,5 @@ +add_doctest_test(qp_problem qp_problem_test.cpp) + +add_doctest_test(osqp_solver osqp_solver_test.cpp) + +add_subdirectory(sparse_matrix) diff --git a/tests/osqp_solver_test.cpp b/tests/osqp_solver_test.cpp new file mode 100644 index 0000000..f54eb02 --- /dev/null +++ b/tests/osqp_solver_test.cpp @@ -0,0 +1,77 @@ +/** + * @file osqp_solver_test.cpp + * @author Houchen Li (houchen_li@hotmail.com) + * @brief + * @version 0.1 + * @date 2023-07-21 + * + * @copyright Copyright (c) 2023 Houchen Li + * All rights reserved. + * + */ + +#include "src/osqp_solver.hpp" + +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include "doctest/doctest.h" + +namespace osqp { + +constexpr double kEpsilon{1E-8}; + +/** + * @brief Here we construct a simple Quadratic programming model to test. + * cost: + * f(x0, x1) = 2 * x0^2 + x1^2 + x0*x1 + x0 + x1 + * constraints: + * 1 <= x0 + x1 <= 1 + * 0 <= x0 <= 0.7 + * 0 <= x1 <= 0.7 + * + */ +class OsqpSolverTestFixture { + public: + OsqpSolverTestFixture() : qp_problem(2, 3), solver() { + solver.settings.polishing = 1; + + qp_problem.updateQuadCostTerm(0, 0, 2.0); + qp_problem.updateQuadCostTerm(1, 1, 1.0); + qp_problem.updateQuadCostTerm(0, 1, 1.0); + qp_problem.updateLinCostTerm(0, 1.0); + qp_problem.updateLinCostTerm(1, 1.0); + qp_problem.updateConstrainTerm(0, {{0, 1.0}, {1, 1.0}}, 1.0, 1.0); + qp_problem.updateConstrainTerm(1, {{0, 1.0}}, 0.0, 0.7); + qp_problem.updateConstrainTerm(2, {{1, 1.0}}, 0.0, 0.7); + + // Run cold start + result = solver.solve(qp_problem); + + CHECK_EQ(result.prim_vars[0], doctest::Approx(0.3).epsilon(1e-7)); + CHECK_EQ(result.prim_vars[1], doctest::Approx(0.7).epsilon(1e-7)); + CHECK_EQ(result.dual_vars[0], doctest::Approx(-2.9).epsilon(1e-7)); + CHECK_EQ(result.dual_vars[1], 0.0); + CHECK_EQ(result.dual_vars[2], doctest::Approx(0.2).epsilon(1e-6)); + CHECK_EQ(result.info.iter, 25); + } + + protected: + QpProblem qp_problem{}; + OsqpResult result{}; + OsqpSolver solver{}; +}; + +TEST_CASE_FIXTURE(OsqpSolverTestFixture, "WarmStart") { + const std::vector prim_vars_0{0.3, 0.7}; + const std::vector dual_vars_0{-2.9, 0.0, 0.2}; + + result = solver.solve(qp_problem, prim_vars_0, dual_vars_0); + + CHECK_EQ(result.prim_vars[0], doctest::Approx(0.3).epsilon(1e-7)); + CHECK_EQ(result.prim_vars[1], doctest::Approx(0.7).epsilon(1e-7)); + CHECK_EQ(result.dual_vars[0], doctest::Approx(-2.9).epsilon(1e-7)); + CHECK_EQ(result.dual_vars[1], 0.0); + CHECK_EQ(result.dual_vars[2], doctest::Approx(0.2).epsilon(1e-6)); + CHECK_EQ(result.info.iter, 25); +} + +} // namespace osqp diff --git a/tests/qp_problem_test.cpp b/tests/qp_problem_test.cpp new file mode 100644 index 0000000..198f4c8 --- /dev/null +++ b/tests/qp_problem_test.cpp @@ -0,0 +1,87 @@ +/** + * @file qp_problem_test.cpp + * @author Houchen Li (houchen_li@hotmail.com) + * @brief + * @version 0.1 + * @date 2023-07-20 + * + * @copyright Copyright (c) 2023 Houchen Li + * All rights reserved. + * + */ + +#include "src/qp_problem.hpp" + +#include + +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include "doctest/doctest.h" + +namespace osqp { + +constexpr double kEpsilon{1E-8}; + +/** + * @brief Here we construct a simple Quadratic programming model to test. + * cost: + * f(x0, x1) = 2 * x0^2 + x1^2 + x0*x1 + x0 + x1 + * constraints: + * 1 <= x0 + x1 <= 1 + * 0 <= x0 <= 0.7 + * 0 <= x1 <= 0.7 + * + */ +class QpProblemTestFixture { + public: + QpProblemTestFixture() : qp_problem(2, 3) { + qp_problem.addQuadCostTerm(0, 0, 2.0); + qp_problem.addQuadCostTerm(1, 1, 1.0); + qp_problem.addQuadCostTerm(0, 1, 1.0); + qp_problem.addLinCostTerm(0, 1.0); + qp_problem.addLinCostTerm(1, 1.0); + qp_problem.updateConstrainTerm(0, {{0, 1.0}, {1, 1.0}}, 1.0, 1.0); + qp_problem.updateConstrainTerm(1, {{0, 1.0}}, 0.0, 0.7); + qp_problem.updateConstrainTerm(2, {{1, 1.0}}, 0.0, 0.7); + } + + protected: + QpProblem qp_problem{}; +}; + +TEST_CASE_FIXTURE(QpProblemTestFixture, "Basic") { + std::vector state_vec; + double exact_cost{0.0}; + SUBCASE("state_0") { + state_vec = {0.0, 0.0}; + exact_cost = 0.0; + CHECK_FALSE(qp_problem.validate(state_vec.cbegin(), state_vec.cend())); + } + + SUBCASE("state_1") { + state_vec = {0.462, 0.538}; + exact_cost = state_vec[0] * state_vec[0] * 2.0 + state_vec[1] * state_vec[1] + + state_vec[0] * state_vec[1] + state_vec[0] + state_vec[1]; + CHECK(qp_problem.validate(state_vec.cbegin(), state_vec.cend())); + } + + SUBCASE("state_2") { + state_vec = {0.462, 0.538 + kEpsilon}; + exact_cost = state_vec[0] * state_vec[0] * 2.0 + state_vec[1] * state_vec[1] + + state_vec[0] * state_vec[1] + state_vec[0] + state_vec[1]; + CHECK_FALSE(qp_problem.validate(state_vec.cbegin(), state_vec.cend())); + } + + SUBCASE("state_2") { + state_vec = {0.462 - kEpsilon, 0.538}; + exact_cost = state_vec[0] * state_vec[0] * 2.0 + state_vec[1] * state_vec[1] + + state_vec[0] * state_vec[1] + state_vec[0] + state_vec[1]; + CHECK_FALSE(qp_problem.validate(state_vec.cbegin(), state_vec.cend())); + } + + CHECK_EQ( + qp_problem.cost(state_vec.cbegin(), state_vec.cend()), + doctest::Approx(exact_cost).epsilon(kEpsilon) + ); +} + +} // namespace osqp diff --git a/tests/sparse_matrix/CMakeLists.txt b/tests/sparse_matrix/CMakeLists.txt new file mode 100644 index 0000000..d37cf81 --- /dev/null +++ b/tests/sparse_matrix/CMakeLists.txt @@ -0,0 +1,7 @@ +add_doctest_test(coo_matrix coo_matrix_test.cpp) + +add_doctest_test(lil_matrix lil_matrix_test.cpp) + +add_doctest_test(csc_matrix csc_matrix_test.cpp) + +add_doctest_test(csr_matrix csr_matrix_test.cpp) diff --git a/tests/sparse_matrix/coo_matrix_test.cpp b/tests/sparse_matrix/coo_matrix_test.cpp new file mode 100644 index 0000000..364f30f --- /dev/null +++ b/tests/sparse_matrix/coo_matrix_test.cpp @@ -0,0 +1,128 @@ +/** + * @file coo_matrix_test.cpp + * @author Houchen Li (houchen_li@hotmail.com) + * @brief + * @version 0.1 + * @date 2023-10-27 + * + * @copyright Copyright (c) 2023 Houchen Li + * All rights reserved. + * + */ + +#include "src/sparse_matrix/coo_matrix.hpp" + +#include "src/sparse_matrix/lil_matrix.hpp" + +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include "doctest/doctest.h" + +namespace osqp { + +TEST_CASE("Basic") { + CooMatrix coo_matrix(8, 8); + + CHECK_EQ(coo_matrix.nrows(), 8); + CHECK_EQ(coo_matrix.ncols(), 8); + + coo_matrix.updateCoeff(2, 5, 5.0); + coo_matrix.updateCoeff(7, 3, 2874.0843); + coo_matrix.updateCoeff(0, 6, -408.876); + coo_matrix.updateCoeff(4, 6, 0.0); + coo_matrix.updateCoeff(8, 0, 1.5); + + CHECK_EQ(coo_matrix.coeff(2, 5), 5.0); + CHECK_EQ(coo_matrix.coeff(7, 3), 2874.0843); + CHECK_EQ(coo_matrix.coeff(0, 6), -408.876); + CHECK_EQ(coo_matrix.coeff(4, 6), 0.0); + CHECK_EQ(coo_matrix.coeff(8, 0), 0.0); + + CHECK_EQ(coo_matrix.coeff(1, 3), 0.0); + CHECK_EQ(coo_matrix.coeff(2, 4), 0.0); + + std::size_t nnzs = coo_matrix.nnzs(); + const std::vector& values = coo_matrix.values(); + const std::vector& row_indices = coo_matrix.rowIndices(); + const std::vector& col_indices = coo_matrix.colIndices(); + + CHECK_EQ(nnzs, 3); + + for (std::size_t i = 0; i < nnzs; ++i) { + CHECK_EQ(values[i], coo_matrix.coeff(row_indices[i], col_indices[i])); + } + + coo_matrix.updateCoeff(7, 3, 28234.0843); + coo_matrix.updateCoeff(2, 5, 7.0); + coo_matrix.updateCoeff(0, 6, 408.876); + coo_matrix.updateCoeff(4, 6, 1.0); + + nnzs = coo_matrix.nnzs(); + CHECK_EQ(nnzs, 4); + + CHECK_EQ(coo_matrix.coeff(2, 5), 7.0); + CHECK_EQ(coo_matrix.coeff(7, 3), 28234.0843); + CHECK_EQ(coo_matrix.coeff(0, 6), 408.876); + CHECK_EQ(coo_matrix.coeff(4, 6), 1.0); + + coo_matrix.clear(); + + nnzs = coo_matrix.nnzs(); + CHECK_EQ(nnzs, 0); +} + +TEST_CASE("Conversion") { + LilMatrix lil_matrix(4, 8); + std::vector row_values_0{1.5, 345.2, 567.4, 0.0}; + std::vector row_indices_0{3, 1, 0, 5}; + + std::unordered_map row_values_map_3{{2, 1.57}, {4, -35.2}, {1, 0.0}, {3, 3775.0}}; + + lil_matrix.updateRow(0, row_values_0, row_indices_0); + lil_matrix.updateRow(3, row_values_map_3); + + CooMatrix coo_matrix{lil_matrix}; + + CHECK_EQ(coo_matrix.nrows(), 4); + CHECK_EQ(coo_matrix.ncols(), 8); + CHECK_EQ(coo_matrix.nnzs(), 6); + + CHECK_EQ(coo_matrix.coeff(0, 3), 1.5); + CHECK_EQ(coo_matrix.coeff(0, 1), 345.2); + CHECK_EQ(coo_matrix.coeff(0, 0), 567.4); + CHECK_EQ(coo_matrix.coeff(0, 5), 0.0); + CHECK_EQ(coo_matrix.coeff(0, 2), 0.0); + CHECK_EQ(coo_matrix.coeff(3, 2), 1.57); + CHECK_EQ(coo_matrix.coeff(3, 4), -35.2); + CHECK_EQ(coo_matrix.coeff(3, 1), 0.0); + CHECK_EQ(coo_matrix.coeff(3, 3), 3775.0); +} + +TEST_CASE("Resize") { + CooMatrix coo_matrix(8, 8); + + CHECK_EQ(coo_matrix.nrows(), 8); + CHECK_EQ(coo_matrix.ncols(), 8); + + coo_matrix.updateCoeff(2, 5, 5.0); + coo_matrix.updateCoeff(7, 3, 2874.0843); + coo_matrix.updateCoeff(0, 6, -408.876); + coo_matrix.updateCoeff(4, 6, 0.0); + coo_matrix.updateCoeff(8, 0, 1.5); + + CHECK_EQ(coo_matrix.nnzs(), 3); + + coo_matrix.resize(6, 6); + + CHECK_EQ(coo_matrix.nrows(), 6); + CHECK_EQ(coo_matrix.ncols(), 6); + + coo_matrix.updateCoeff(2, 5, 5.0); + coo_matrix.updateCoeff(7, 3, 0.0); + coo_matrix.updateCoeff(0, 6, 0.0); + coo_matrix.updateCoeff(4, 6, 0.0); + coo_matrix.updateCoeff(8, 0, 0.0); + + CHECK_EQ(coo_matrix.nnzs(), 1); +} + +} // namespace osqp diff --git a/tests/sparse_matrix/csc_matrix_test.cpp b/tests/sparse_matrix/csc_matrix_test.cpp new file mode 100644 index 0000000..f95b018 --- /dev/null +++ b/tests/sparse_matrix/csc_matrix_test.cpp @@ -0,0 +1,196 @@ +/** + * @file csc_matrix_test.cpp + * @author Houchen Li (houchen_li@hotmail.com) + * @brief + * @version 0.1 + * @date 2023-11-04 + * + * @copyright Copyright (c) 2023 Houchen Li + * All rights reserved. + * + */ + +#include "src/sparse_matrix/csc_matrix.hpp" + +#include "src/sparse_matrix/coo_matrix.hpp" +#include "src/sparse_matrix/lil_matrix.hpp" + +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include "doctest/doctest.h" + +namespace osqp { + +TEST_CASE("Basic") { + CscMatrix csc_matrix(5, 5); + + CHECK_EQ(csc_matrix.nrows(), 5); + CHECK_EQ(csc_matrix.ncols(), 5); + CHECK_EQ(csc_matrix.nnzs(), 0); + CHECK_EQ(csc_matrix.outerIndices().size(), 6); + + csc_matrix.updateCoeff(4, 4, 8.0); + csc_matrix.updateCoeff(1, 4, 17.0); + csc_matrix.updateCoeff(2, 3, 1.0); + csc_matrix.updateCoeff(4, 2, 14.0); + csc_matrix.updateCoeff(2, 1, 5.0); + csc_matrix.updateCoeff(0, 1, 3.0); + csc_matrix.updateCoeff(2, 0, 7.0); + csc_matrix.updateCoeff(1, 0, 22.0); + csc_matrix.updateCoeff(5, 5, 1.0); + + CHECK_EQ(csc_matrix.coeff(1, 0), 22.0); + CHECK_EQ(csc_matrix.coeff(2, 0), 7.0); + CHECK_EQ(csc_matrix.coeff(0, 1), 3.0); + CHECK_EQ(csc_matrix.coeff(2, 1), 5.0); + CHECK_EQ(csc_matrix.coeff(4, 2), 14.0); + CHECK_EQ(csc_matrix.coeff(2, 3), 1.0); + CHECK_EQ(csc_matrix.coeff(1, 4), 17.0); + CHECK_EQ(csc_matrix.coeff(4, 4), 8.0); + CHECK_EQ(csc_matrix.coeff(5, 5), 0.0); + CHECK_EQ(csc_matrix.coeff(3, 3), 0.0); + + const std::size_t values_size{csc_matrix.values().size()}; + const std::size_t inner_indices_size{csc_matrix.innerIndices().size()}; + const std::size_t outer_indices_size{csc_matrix.outerIndices().size()}; + const std::vector& values{csc_matrix.values()}; + const std::vector& inner_indices{csc_matrix.innerIndices()}; + const std::vector& outer_indices{csc_matrix.outerIndices()}; + + const std::vector exact_values{22.0, 7.0, 3.0, 5.0, 14.0, 1.0, 17.0, 8.0}; + const std::vector exact_inner_indices{1, 2, 0, 2, 4, 2, 1, 4}; + const std::vector exact_outer_indices{0, 2, 4, 5, 6, 8}; + + CHECK_EQ(values_size, exact_values.size()); + CHECK_EQ(inner_indices_size, exact_inner_indices.size()); + CHECK_EQ(outer_indices_size, exact_outer_indices.size()); + + for (std::size_t i{0}; i < values_size; ++i) { + CHECK_EQ(values[i], exact_values[i]); + } + for (std::size_t i{0}; i < inner_indices_size; ++i) { + CHECK_EQ(inner_indices[i], exact_inner_indices[i]); + } + for (std::size_t i{0}; i < outer_indices_size; ++i) { + CHECK_EQ(outer_indices[i], exact_outer_indices[i]); + } +} + +TEST_CASE("Conversion") { + CooMatrix coo_matrix(5, 5); + + coo_matrix.updateCoeff(4, 4, 8.0); + coo_matrix.updateCoeff(1, 4, 17.0); + coo_matrix.updateCoeff(2, 3, 1.0); + coo_matrix.updateCoeff(4, 2, 14.0); + coo_matrix.updateCoeff(2, 1, 5.0); + coo_matrix.updateCoeff(0, 1, 3.0); + coo_matrix.updateCoeff(2, 0, 7.0); + coo_matrix.updateCoeff(1, 0, 22.0); + coo_matrix.updateCoeff(5, 5, 1.0); + + CscMatrix csc_matrix{coo_matrix}; + + CHECK_EQ(csc_matrix.nrows(), 5); + CHECK_EQ(csc_matrix.ncols(), 5); + CHECK_EQ(csc_matrix.nnzs(), 8); + CHECK_EQ(csc_matrix.outerIndices().size(), 6); + + const std::size_t values_size{csc_matrix.values().size()}; + const std::size_t inner_indices_size{csc_matrix.innerIndices().size()}; + const std::size_t outer_indices_size{csc_matrix.outerIndices().size()}; + const std::vector& values{csc_matrix.values()}; + const std::vector& inner_indices{csc_matrix.innerIndices()}; + const std::vector& outer_indices{csc_matrix.outerIndices()}; + + const std::vector exact_values{22.0, 7.0, 3.0, 5.0, 14.0, 1.0, 17.0, 8.0}; + const std::vector exact_inner_indices{1, 2, 0, 2, 4, 2, 1, 4}; + const std::vector exact_outer_indices{0, 2, 4, 5, 6, 8}; + + CHECK_EQ(values_size, exact_values.size()); + CHECK_EQ(inner_indices_size, exact_inner_indices.size()); + CHECK_EQ(outer_indices_size, exact_outer_indices.size()); + + for (std::size_t i{0}; i < values_size; ++i) { + CHECK_EQ(values[i], exact_values[i]); + } + for (std::size_t i{0}; i < inner_indices_size; ++i) { + CHECK_EQ(inner_indices[i], exact_inner_indices[i]); + } + for (std::size_t i{0}; i < outer_indices_size; ++i) { + CHECK_EQ(outer_indices[i], exact_outer_indices[i]); + } +} + +TEST_CASE("Resize") { + CscMatrix csc_matrix(5, 5); + + csc_matrix.updateCoeff(4, 4, 8.0); + csc_matrix.updateCoeff(1, 4, 17.0); + csc_matrix.updateCoeff(2, 3, 1.0); + csc_matrix.updateCoeff(4, 2, 14.0); + csc_matrix.updateCoeff(2, 1, 5.0); + csc_matrix.updateCoeff(0, 1, 3.0); + csc_matrix.updateCoeff(2, 0, 7.0); + csc_matrix.updateCoeff(1, 0, 22.0); + csc_matrix.updateCoeff(5, 5, 1.0); + + SUBCASE("EnlargeSize") { + csc_matrix.resize(10, 10); + + const std::size_t values_size{csc_matrix.values().size()}; + const std::size_t inner_indices_size{csc_matrix.innerIndices().size()}; + const std::size_t outer_indices_size{csc_matrix.outerIndices().size()}; + const std::vector& values{csc_matrix.values()}; + const std::vector& inner_indices{csc_matrix.innerIndices()}; + const std::vector& outer_indices{csc_matrix.outerIndices()}; + + const std::vector exact_values{22.0, 7.0, 3.0, 5.0, 14.0, 1.0, 17.0, 8.0}; + const std::vector exact_inner_indices{1, 2, 0, 2, 4, 2, 1, 4}; + const std::vector exact_outer_indices{0, 2, 4, 5, 6, 8, 8, 8, 8, 8, 8}; + + CHECK_EQ(values_size, exact_values.size()); + CHECK_EQ(inner_indices_size, exact_inner_indices.size()); + CHECK_EQ(outer_indices_size, exact_outer_indices.size()); + + for (std::size_t i{0}; i < values_size; ++i) { + CHECK_EQ(values[i], exact_values[i]); + } + for (std::size_t i{0}; i < inner_indices_size; ++i) { + CHECK_EQ(inner_indices[i], exact_inner_indices[i]); + } + for (std::size_t i{0}; i < outer_indices_size; ++i) { + CHECK_EQ(outer_indices[i], exact_outer_indices[i]); + } + } + + SUBCASE("DescendSize") { + csc_matrix.resize(3, 3); + + const std::size_t values_size{csc_matrix.values().size()}; + const std::size_t inner_indices_size{csc_matrix.innerIndices().size()}; + const std::size_t outer_indices_size{csc_matrix.outerIndices().size()}; + const std::vector& values{csc_matrix.values()}; + const std::vector& inner_indices{csc_matrix.innerIndices()}; + const std::vector& outer_indices{csc_matrix.outerIndices()}; + + const std::vector exact_values{22.0, 7.0, 3.0, 5.0}; + const std::vector exact_inner_indices{1, 2, 0, 2}; + const std::vector exact_outer_indices{0, 2, 4, 4}; + + CHECK_EQ(values_size, exact_values.size()); + CHECK_EQ(inner_indices_size, exact_inner_indices.size()); + CHECK_EQ(outer_indices_size, exact_outer_indices.size()); + + for (std::size_t i{0}; i < values_size; ++i) { + CHECK_EQ(values[i], exact_values[i]); + } + for (std::size_t i{0}; i < inner_indices_size; ++i) { + CHECK_EQ(inner_indices[i], exact_inner_indices[i]); + } + for (std::size_t i{0}; i < outer_indices_size; ++i) { + CHECK_EQ(outer_indices[i], exact_outer_indices[i]); + } + } +} + +} // namespace osqp diff --git a/tests/sparse_matrix/csr_matrix_test.cpp b/tests/sparse_matrix/csr_matrix_test.cpp new file mode 100644 index 0000000..c37dbda --- /dev/null +++ b/tests/sparse_matrix/csr_matrix_test.cpp @@ -0,0 +1,196 @@ +/** + * @file csr_matrix_test.cpp + * @author Houchen Li (houchen_li@hotmail.com) + * @brief + * @version 0.1 + * @date 2023-11-04 + * + * @copyright Copyright (c) 2023 Houchen Li + * All rights reserved. + * + */ + +#include "src/sparse_matrix/csr_matrix.hpp" + +#include "src/sparse_matrix/coo_matrix.hpp" +#include "src/sparse_matrix/lil_matrix.hpp" + +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include "doctest/doctest.h" + +namespace osqp { + +TEST_CASE("Basic") { + CsrMatrix csr_matrix(5, 5); + + CHECK_EQ(csr_matrix.nrows(), 5); + CHECK_EQ(csr_matrix.ncols(), 5); + CHECK_EQ(csr_matrix.nnzs(), 0); + CHECK_EQ(csr_matrix.outerIndices().size(), 6); + + csr_matrix.updateCoeff(4, 4, 8.0); + csr_matrix.updateCoeff(1, 4, 17.0); + csr_matrix.updateCoeff(2, 3, 1.0); + csr_matrix.updateCoeff(4, 2, 14.0); + csr_matrix.updateCoeff(2, 1, 5.0); + csr_matrix.updateCoeff(0, 1, 3.0); + csr_matrix.updateCoeff(2, 0, 7.0); + csr_matrix.updateCoeff(1, 0, 22.0); + csr_matrix.updateCoeff(5, 5, 1.0); + + CHECK_EQ(csr_matrix.coeff(1, 0), 22.0); + CHECK_EQ(csr_matrix.coeff(2, 0), 7.0); + CHECK_EQ(csr_matrix.coeff(0, 1), 3.0); + CHECK_EQ(csr_matrix.coeff(2, 1), 5.0); + CHECK_EQ(csr_matrix.coeff(4, 2), 14.0); + CHECK_EQ(csr_matrix.coeff(2, 3), 1.0); + CHECK_EQ(csr_matrix.coeff(1, 4), 17.0); + CHECK_EQ(csr_matrix.coeff(4, 4), 8.0); + CHECK_EQ(csr_matrix.coeff(5, 5), 0.0); + CHECK_EQ(csr_matrix.coeff(3, 3), 0.0); + + const std::size_t values_size{csr_matrix.values().size()}; + const std::size_t inner_indices_size{csr_matrix.innerIndices().size()}; + const std::size_t outer_indices_size{csr_matrix.outerIndices().size()}; + const std::vector& values{csr_matrix.values()}; + const std::vector& inner_indices{csr_matrix.innerIndices()}; + const std::vector& outer_indices{csr_matrix.outerIndices()}; + + const std::vector exact_values{3.0, 22.0, 17.0, 7.0, 5.0, 1.0, 14.0, 8.0}; + const std::vector exact_inner_indices{1, 0, 4, 0, 1, 3, 2, 4}; + const std::vector exact_outer_indices{0, 1, 3, 6, 6, 8}; + + CHECK_EQ(values_size, exact_values.size()); + CHECK_EQ(inner_indices_size, exact_inner_indices.size()); + CHECK_EQ(outer_indices_size, exact_outer_indices.size()); + + for (std::size_t i{0}; i < values_size; ++i) { + CHECK_EQ(values[i], exact_values[i]); + } + for (std::size_t i{0}; i < inner_indices_size; ++i) { + CHECK_EQ(inner_indices[i], exact_inner_indices[i]); + } + for (std::size_t i{0}; i < outer_indices_size; ++i) { + CHECK_EQ(outer_indices[i], exact_outer_indices[i]); + } +} + +TEST_CASE("Conversion") { + CooMatrix coo_matrix(5, 5); + + coo_matrix.updateCoeff(4, 4, 8.0); + coo_matrix.updateCoeff(1, 4, 17.0); + coo_matrix.updateCoeff(2, 3, 1.0); + coo_matrix.updateCoeff(4, 2, 14.0); + coo_matrix.updateCoeff(2, 1, 5.0); + coo_matrix.updateCoeff(0, 1, 3.0); + coo_matrix.updateCoeff(2, 0, 7.0); + coo_matrix.updateCoeff(1, 0, 22.0); + coo_matrix.updateCoeff(5, 5, 1.0); + + CsrMatrix csr_matrix{coo_matrix}; + + CHECK_EQ(csr_matrix.nrows(), 5); + CHECK_EQ(csr_matrix.ncols(), 5); + CHECK_EQ(csr_matrix.nnzs(), 8); + CHECK_EQ(csr_matrix.outerIndices().size(), 6); + + const std::size_t values_size{csr_matrix.values().size()}; + const std::size_t inner_indices_size{csr_matrix.innerIndices().size()}; + const std::size_t outer_indices_size{csr_matrix.outerIndices().size()}; + const std::vector& values{csr_matrix.values()}; + const std::vector& inner_indices{csr_matrix.innerIndices()}; + const std::vector& outer_indices{csr_matrix.outerIndices()}; + + const std::vector exact_values{3.0, 22.0, 17.0, 7.0, 5.0, 1.0, 14.0, 8.0}; + const std::vector exact_inner_indices{1, 0, 4, 0, 1, 3, 2, 4}; + const std::vector exact_outer_indices{0, 1, 3, 6, 6, 8}; + + CHECK_EQ(values_size, exact_values.size()); + CHECK_EQ(inner_indices_size, exact_inner_indices.size()); + CHECK_EQ(outer_indices_size, exact_outer_indices.size()); + + for (std::size_t i{0}; i < values_size; ++i) { + CHECK_EQ(values[i], exact_values[i]); + } + for (std::size_t i{0}; i < inner_indices_size; ++i) { + CHECK_EQ(inner_indices[i], exact_inner_indices[i]); + } + for (std::size_t i{0}; i < outer_indices_size; ++i) { + CHECK_EQ(outer_indices[i], exact_outer_indices[i]); + } +} + +TEST_CASE("Resize") { + CsrMatrix csr_matrix(5, 5); + + csr_matrix.updateCoeff(4, 4, 8.0); + csr_matrix.updateCoeff(1, 4, 17.0); + csr_matrix.updateCoeff(2, 3, 1.0); + csr_matrix.updateCoeff(4, 2, 14.0); + csr_matrix.updateCoeff(2, 1, 5.0); + csr_matrix.updateCoeff(0, 1, 3.0); + csr_matrix.updateCoeff(2, 0, 7.0); + csr_matrix.updateCoeff(1, 0, 22.0); + csr_matrix.updateCoeff(5, 5, 1.0); + + SUBCASE("EnlargeSize") { + csr_matrix.resize(10, 10); + + const std::size_t values_size{csr_matrix.values().size()}; + const std::size_t inner_indices_size{csr_matrix.innerIndices().size()}; + const std::size_t outer_indices_size{csr_matrix.outerIndices().size()}; + const std::vector& values{csr_matrix.values()}; + const std::vector& inner_indices{csr_matrix.innerIndices()}; + const std::vector& outer_indices{csr_matrix.outerIndices()}; + + const std::vector exact_values{3.0, 22.0, 17.0, 7.0, 5.0, 1.0, 14.0, 8.0}; + const std::vector exact_inner_indices{1, 0, 4, 0, 1, 3, 2, 4}; + const std::vector exact_outer_indices{0, 1, 3, 6, 6, 8, 8, 8, 8, 8, 8}; + + CHECK_EQ(values_size, exact_values.size()); + CHECK_EQ(inner_indices_size, exact_inner_indices.size()); + CHECK_EQ(outer_indices_size, exact_outer_indices.size()); + + for (std::size_t i{0}; i < values_size; ++i) { + CHECK_EQ(values[i], exact_values[i]); + } + for (std::size_t i{0}; i < inner_indices_size; ++i) { + CHECK_EQ(inner_indices[i], exact_inner_indices[i]); + } + for (std::size_t i{0}; i < outer_indices_size; ++i) { + CHECK_EQ(outer_indices[i], exact_outer_indices[i]); + } + } + + SUBCASE("DescendSize") { + csr_matrix.resize(3, 3); + + const std::size_t values_size{csr_matrix.values().size()}; + const std::size_t inner_indices_size{csr_matrix.innerIndices().size()}; + const std::size_t outer_indices_size{csr_matrix.outerIndices().size()}; + const std::vector& values{csr_matrix.values()}; + const std::vector& inner_indices{csr_matrix.innerIndices()}; + const std::vector& outer_indices{csr_matrix.outerIndices()}; + + const std::vector exact_values{3.0, 22.0, 7.0, 5.0}; + const std::vector exact_inner_indices{1, 0, 0, 1}; + const std::vector exact_outer_indices{0, 1, 2, 4}; + + CHECK_EQ(values_size, exact_values.size()); + CHECK_EQ(inner_indices_size, exact_inner_indices.size()); + CHECK_EQ(outer_indices_size, exact_outer_indices.size()); + + for (std::size_t i{0}; i < values_size; ++i) { + CHECK_EQ(values[i], exact_values[i]); + } + for (std::size_t i{0}; i < inner_indices_size; ++i) { + CHECK_EQ(inner_indices[i], exact_inner_indices[i]); + } + for (std::size_t i{0}; i < outer_indices_size; ++i) { + CHECK_EQ(outer_indices[i], exact_outer_indices[i]); + } + } +} + +} // namespace osqp diff --git a/tests/sparse_matrix/lil_matrix_test.cpp b/tests/sparse_matrix/lil_matrix_test.cpp new file mode 100644 index 0000000..c09a665 --- /dev/null +++ b/tests/sparse_matrix/lil_matrix_test.cpp @@ -0,0 +1,99 @@ +/** + * @file lil_matrix_test.cpp + * @author Houchen Li (houchen_li@hotmail.com) + * @brief + * @version 0.1 + * @date 2023-10-28 + * + * @copyright Copyright (c) 2023 Houchen Li + * All rights reserved. + * + */ + +#include "src/sparse_matrix/lil_matrix.hpp" + +#include +#include + +#include "src/sparse_matrix/coo_matrix.hpp" + +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include "doctest/doctest.h" + +namespace osqp { + +TEST_CASE("Basic") { + LilMatrix lil_matrix(4, 8); + std::vector row_values_0{1.5, 345.2, 567.4, 0.0}; + std::vector row_indices_0{3, 1, 0, 5}; + + std::unordered_map row_values_map_3{{2, 1.57}, {4, -35.2}, {1, 0.0}, {3, 3775.0}}; + + lil_matrix.updateRow(0, row_values_0, row_indices_0); + lil_matrix.updateRow(3, row_values_map_3); + + CHECK_EQ(lil_matrix.nrows(), 4); + CHECK_EQ(lil_matrix.ncols(), 8); + CHECK_EQ(lil_matrix.nnzs(), 6); + + CHECK_EQ(lil_matrix.coeff(0, 3), 1.5); + CHECK_EQ(lil_matrix.coeff(0, 1), 345.2); + CHECK_EQ(lil_matrix.coeff(0, 0), 567.4); + CHECK_EQ(lil_matrix.coeff(0, 5), 0.0); + CHECK_EQ(lil_matrix.coeff(0, 2), 0.0); + CHECK_EQ(lil_matrix.coeff(3, 2), 1.57); + CHECK_EQ(lil_matrix.coeff(3, 4), -35.2); + CHECK_EQ(lil_matrix.coeff(3, 1), 0.0); + CHECK_EQ(lil_matrix.coeff(3, 3), 3775.0); +} + +TEST_CASE("Conversion") { + CooMatrix coo_matrix(8, 8); + + coo_matrix.updateCoeff(2, 5, 5.0); + coo_matrix.updateCoeff(7, 3, 2874.0843); + coo_matrix.updateCoeff(0, 6, -408.876); + coo_matrix.updateCoeff(4, 6, 0.0); + coo_matrix.updateCoeff(8, 0, 1.5); + + LilMatrix lil_matrix{coo_matrix}; + + CHECK_EQ(lil_matrix.nrows(), 8); + CHECK_EQ(lil_matrix.ncols(), 8); + CHECK_EQ(lil_matrix.nnzs(), 3); + + CHECK_EQ(lil_matrix.coeff(2, 5), 5.0); + CHECK_EQ(lil_matrix.coeff(7, 3), 2874.0843); + CHECK_EQ(lil_matrix.coeff(0, 6), -408.876); + CHECK_EQ(lil_matrix.coeff(4, 6), 0.0); + CHECK_EQ(lil_matrix.coeff(8, 0), 0.0); +} + +TEST_CASE("Resize") { + LilMatrix lil_matrix(4, 8); + std::vector row_values_0{1.5, 345.2, 567.4, 0.0}; + std::vector row_indices_0{3, 1, 0, 5}; + + std::unordered_map row_values_map_3{{2, 1.57}, {4, -35.2}, {1, 0.0}, {3, 3775.0}}; + + lil_matrix.updateRow(0, row_values_0, row_indices_0); + lil_matrix.updateRow(3, row_values_map_3); + + lil_matrix.resize(2, 4); + + CHECK_EQ(lil_matrix.nrows(), 2); + CHECK_EQ(lil_matrix.ncols(), 4); + CHECK_EQ(lil_matrix.nnzs(), 3); + + CHECK_EQ(lil_matrix.coeff(0, 3), 1.5); + CHECK_EQ(lil_matrix.coeff(0, 1), 345.2); + CHECK_EQ(lil_matrix.coeff(0, 0), 567.4); + CHECK_EQ(lil_matrix.coeff(0, 5), 0.0); + CHECK_EQ(lil_matrix.coeff(0, 2), 0.0); + CHECK_EQ(lil_matrix.coeff(3, 2), 0.0); + CHECK_EQ(lil_matrix.coeff(3, 4), 0.0); + CHECK_EQ(lil_matrix.coeff(3, 1), 0.0); + CHECK_EQ(lil_matrix.coeff(3, 3), 0.0); +} + +} // namespace osqp