diff --git a/cpp/movement_recognition/CMake/ConfigureCrossCompile.cmake b/cpp/movement_recognition/CMake/ConfigureCrossCompile.cmake new file mode 100644 index 00000000..d105734f --- /dev/null +++ b/cpp/movement_recognition/CMake/ConfigureCrossCompile.cmake @@ -0,0 +1,141 @@ +# This file adds the necessary configurations to cross compile +# mlpack for embedded systems. You need to set the following variables +# from the command line: CMAKE_SYSROOT and TOOLCHAIN_PREFIX. +# This file will compile OpenBLAS if it is downloaded and it is not +# available on your system in order to find the BLAS library. If OpenBLAS will +# be compiled, the OPENBLAS_TARGET variable must be set. This can be done +# by, e.g., setting ARCH_NAME (which will set OPENBLAS_TARGET in +# `flags-config.cmake`). + +# Apply a list of patch files to an OpenBLAS source tree before it is built. +# `srcDir` is the unpacked OpenBLAS directory; `patches` is a ;-list of patch +# files (each `patch -p1`-compatible). Applying is idempotent: a patch that is +# already applied (detected via a reverse dry-run) is skipped, so re-running +# CMake against an existing build tree is safe. A patch that neither applies +# cleanly nor is already applied is a hard error -- unlike an in-place `sed`, it +# never silently does nothing when the upstream source has changed. +function(apply_openblas_patches srcDir patches) + find_program(PATCH_EXECUTABLE patch) + if(NOT PATCH_EXECUTABLE) + message(FATAL_ERROR "The 'patch' tool is required to apply OPENBLAS_PATCHES " + "but was not found on PATH.") + endif() + + foreach(patchFile IN LISTS patches) + if(NOT IS_ABSOLUTE "${patchFile}") + set(patchFile "${CMAKE_CURRENT_LIST_DIR}/${patchFile}") + endif() + if(NOT EXISTS "${patchFile}") + message(FATAL_ERROR "OpenBLAS patch not found: ${patchFile}") + endif() + + get_filename_component(patchName "${patchFile}" NAME) + + # Already applied? `patch -R --dry-run` succeeds only if the reverse patch + # would apply, i.e. the forward patch is already in place. + execute_process( + COMMAND ${PATCH_EXECUTABLE} -p1 -R --dry-run --force + --input=${patchFile} + WORKING_DIRECTORY ${srcDir} + RESULT_VARIABLE alreadyApplied + OUTPUT_QUIET ERROR_QUIET) + if(alreadyApplied EQUAL 0) + message(STATUS "OpenBLAS patch already applied, skipping: ${patchName}") + continue() + endif() + + execute_process( + COMMAND ${PATCH_EXECUTABLE} -p1 --forward --input=${patchFile} + WORKING_DIRECTORY ${srcDir} + RESULT_VARIABLE patchResult + OUTPUT_VARIABLE patchOutput ERROR_VARIABLE patchOutput) + if(NOT patchResult EQUAL 0) + message(FATAL_ERROR + "Failed to apply OpenBLAS patch ${patchName}:\n${patchOutput}") + endif() + message(STATUS "Applied OpenBLAS patch: ${patchName}") + endforeach() +endfunction() + +if (CMAKE_CROSSCOMPILING) + include(CMake/crosscompile-arch-config.cmake) + if (NOT CMAKE_SYSROOT AND (NOT TOOLCHAIN_PREFIX)) + message(FATAL_ERROR "Neither CMAKE_SYSROOT nor TOOLCHAIN_PREFIX are set; please set both of them and try again.") + elseif(NOT CMAKE_SYSROOT) + message(FATAL_ERROR "Cannot configure: CMAKE_SYSROOT must be set when performing cross-compiling!") + elseif(NOT TOOLCHAIN_PREFIX) + message(FATAL_ERROR "Cannot configure: TOOLCHAIN_PREFIX must be set when performing cross-compiling!") + endif() + + # Now make sure that we can still compile a simple test program. + # (This ensures we didn't add any bad CXXFLAGS.) + # Note that OUTPUT_VARIABLE is only available in newer versions of CMake! + # CMake 3.23 (silently) introduced the variable. + include(CheckCXXSourceCompiles) + if (CMAKE_VERSION VERSION_LESS "3.22.0") + check_cxx_source_compiles("int main() { return 0; }" COMPILE_SUCCESS) + if (NOT COMPILE_SUCCESS) + message(FATAL_ERROR "The C++ cross-compiler at ${CMAKE_CXX_COMPILER} is " + "not able to compile a trivial test program. Check the CXXFLAGS!") + endif () + else () + check_cxx_source_compiles("int main() { return 0; }" COMPILE_SUCCESS + OUTPUT_VARIABLE COMPILE_OUTPUT) + if (NOT COMPILE_SUCCESS) + message(FATAL_ERROR "The C++ cross-compiler at ${CMAKE_CXX_COMPILER} is " + "not able to compile a trivial test program. Compiler output:\n\n" + "${COMPILE_OUTPUT}") + endif () + endif () +endif() + +macro(search_openblas version) + set(BLA_STATIC ON) + find_package(BLAS) + if (NOT BLAS_FOUND OR (NOT BLAS_LIBRARIES)) + if(NOT OPENBLAS_TARGET) + message(FATAL_ERROR "Cannot compile OpenBLAS: OPENBLAS_TARGET is not set. Either set that variable, or set BOARD_NAME correctly!") + endif() + get_deps(https://github.com/xianyi/OpenBLAS/releases/download/v${version}/OpenBLAS-${version}.tar.gz OpenBLAS OpenBLAS-${version}.tar.gz) + if (NOT MSVC) + if (NOT EXISTS "${CMAKE_BINARY_DIR}/deps/OpenBLAS-${version}/libopenblas.a") + set(ENV{COMMON_OPT} "${CMAKE_OPENBLAS_FLAGS}") # Pass our flags to OpenBLAS + + # NN-on-device memory fit (riscv64). OpenBLAS lazily allocates a per-GEMM + # scratch buffer (BUFFER_SIZE -- 32 MB on riscv64) sized for its default + # N-block (SGEMM_DEFAULT_R = 12288). That single 32 MB allocation does + # not fit on a ~28 MB device, so the first f32 matrix-multiply -- e.g. the + # neural network's dense layers -- is OOM-killed at startup. (Random + # forest and KNN avoid that big GEMM path, which is why only the NN + # failed.) The fix is a patch that shrinks the N-block to 2048 and the + # buffer to 8 MB; see CMake/patches/openblas-riscv64-low-memory.patch and + # README.md / BINARY_SIZE.md for the full investigation. + # + # For the generic riscv64 target we apply that patch by default; a caller + # can override or extend the list with -DOPENBLAS_PATCHES="a.patch;b.patch" + # (absolute paths, or relative to this CMake/ directory). + if(NOT DEFINED OPENBLAS_PATCHES AND OPENBLAS_TARGET STREQUAL "RISCV64_GENERIC") + set(OPENBLAS_PATCHES + "${CMAKE_CURRENT_LIST_DIR}/patches/openblas-riscv64-low-memory.patch") + endif() + apply_openblas_patches("${CMAKE_BINARY_DIR}/deps/OpenBLAS-${version}" + "${OPENBLAS_PATCHES}") + # USE_THREAD=0 / NUM_THREADS=1 / USE_OPENMP=0: build a single-threaded + # OpenBLAS. On a single-core, 64 MB target (Milk-V Duo) the threaded + # build spawns worker threads that busy-wait (spin) at startup, which + # starves the main thread on one core and hangs the program before it + # even runs. Single-threaded also removes the per-thread GEMM buffers. + execute_process(COMMAND make TARGET=${OPENBLAS_TARGET} BINARY=${OPENBLAS_BINARY} HOSTCC=gcc CC=${CMAKE_C_COMPILER} FC=${CMAKE_FORTRAN_COMPILER} NO_SHARED=1 USE_THREAD=0 NUM_THREADS=1 USE_OPENMP=0 + WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/deps/OpenBLAS-${version}) + endif() + file(GLOB OPENBLAS_LIBRARIES "${CMAKE_BINARY_DIR}/deps/OpenBLAS-${version}/libopenblas.a") + set(BLAS_openblas_LIBRARY ${OPENBLAS_LIBRARIES}) + set(LAPACK_openblas_LIBRARY ${OPENBLAS_LIBRARIES}) + set(BLA_VENDOR OpenBLAS) + set(BLAS_FOUND ON) + endif() + endif() + find_library(GFORTRAN NAMES libgfortran.a) + find_library(PTHREAD NAMES libpthread.a) + set(CROSS_COMPILE_SUPPORT_LIBRARIES ${GFORTRAN} ${PTHREAD}) +endmacro() diff --git a/cpp/movement_recognition/CMake/crosscompile-arch-config.cmake b/cpp/movement_recognition/CMake/crosscompile-arch-config.cmake new file mode 100644 index 00000000..9421eff7 --- /dev/null +++ b/cpp/movement_recognition/CMake/crosscompile-arch-config.cmake @@ -0,0 +1,97 @@ +# This function provides a set of specific flags for each supported board +# depending on the processor type. The objective is to optimize for size. +# Thus, all of the following flags are chosen carefully to reduce binary +# footprints. + +# Set generic minimization flags for all platforms. +# These flags are the same for all cross-compilation cases and they are +# mainly to reduce the binary footprint. +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Os -s -fdata-sections -ffunction-sections") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fomit-frame-pointer -fno-unwind-tables") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-asynchronous-unwind-tables -fvisibility=hidden") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fshort-enums -finline-small-functions") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -findirect-inlining -fno-common") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fmerge-all-constants -fno-ident") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-unroll-loops -fno-math-errno") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-stack-protector") +set(CMAKE_OPENBLAS_FLAGS "${CMAKE_CXX_FLAGS}") # OpenBLAS does not supoport flto +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -flto") +set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--hash-style=gnu -Wl,--build-id=none") +set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-z,norelro") +set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--gc-sections") +## Keep the following flag in comment, they might be relevant in the case of MCU's +#set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wl,-nmagic,-Bsymbolic -nostartfiles") + +# BOARD_NAME is deprecated and will be removed in mlpack 5. +# please use ARCH_NAME instead. +set(BOARD_NAME "" CACHE STRING "Specify Board name to optimize for.") +set(ARCH_NAME "" CACHE STRING "Name of embedded architecture to optimize for.") + +if (BOARD_NAME) + set(ARCH_NAME "${BOARD_NAME}") +endif() + +string(TOUPPER ${ARCH_NAME} ARCH) + +# Set specific platforms CMAKE CXX flags. +if(ARCH STREQUAL "RPI2" OR ARCH STREQUAL "CORTEXA7") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mtune=cortex-a7") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mfloat-abi=hard -mfpu=neon-vfpv4") + set(OPENBLAS_TARGET "ARMV7") + set(OPENBLAS_BINARY "32") +elseif(ARCH STREQUAL "CORTEXA8") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mtune=cortex-a8") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mfloat-abi=hard -mfpu=neon") + set(OPENBLAS_TARGET "ARMV7") + set(OPENBLAS_BINARY "32") +elseif(ARCH STREQUAL "CORTEXA9") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mtune=cortex-a9") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mfloat-abi=hard -mfpu=neon") + set(OPENBLAS_TARGET "CORTEXA9") + set(OPENBLAS_BINARY "32") +elseif(ARCH STREQUAL "CORTEXA15") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mtune=cortex-a15") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mfloat-abi=hard -mfpu=neon") + set(OPENBLAS_TARGET "CORTEXA15") + set(OPENBLAS_BINARY "32") +elseif(ARCH STREQUAL "RPI3" OR ARCH STREQUAL "CORTEXA53") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mtune=cortex-a53 -ftree-vectorize") + set(OPENBLAS_TARGET "CORTEXA53") + set(OPENBLAS_BINARY "64") +elseif(ARCH STREQUAL "RPI4" OR ARCH STREQUAL "CORTEXA72") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -march=armv8.2-a+crypto+fp16+rcpc+dotprod -fasynchronous-unwind-tables") + set(OPENBLAS_TARGET "CORTEXA72") + set(OPENBLAS_BINARY "64") +elseif(ARCH STREQUAL "JETSONAGX" OR ARCH STREQUAL "CORTEXA76") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mtune=cortex-a76 -ftree-vectorize") + set(OPENBLAS_TARGET "CORTEXA76") + set(OPENBLAS_BINARY "64") +elseif(ARCH STREQUAL "BV") + set(OPENBLAS_TARGET "RISCV64_GENERIC") + set(OPENBLAS_BINARY "64") +elseif(ARCH STREQUAL "RV64GCV") + # RV64GCV is the ISA (e.g. the T-Head C906 core); -mtune=thead-c906 is the + # matching GCC tuning flag (there is no -mtune=rv64gcv). + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mtune=thead-c906") + set(OPENBLAS_TARGET "RISCV64_GENERIC") + set(OPENBLAS_BINARY "64") +elseif(ARCH STREQUAL "x280") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mtune=sifive-x280") + set(OPENBLAS_TARGET "x280") + set(OPENBLAS_BINARY "64") +elseif(ARCH STREQUAL "KATAMI") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -march=pentium3") + set(OPENBLAS_TARGET "KATAMI") + set(OPENBLAS_BINARY "32") +elseif(ARCH STREQUAL "COPPERMINE") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -march=pentium3") + set(OPENBLAS_TARGET "COPPERMINE") + set(OPENBLAS_BINARY "32") +elseif(ARCH STREQUAL "NORTHWOOD") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -march=pentium4") + set(OPENBLAS_TARGET "NORTHWOOD") + set(OPENBLAS_BINARY "32") +elseif(ARCH) + ## TODO: update documentation with a list of the supported boards. + message(FATAL_ERROR "Given ARCH_NAME is not known; please choose a supported board from the list") +endif() diff --git a/cpp/movement_recognition/CMake/crosscompile-toolchain.cmake b/cpp/movement_recognition/CMake/crosscompile-toolchain.cmake new file mode 100644 index 00000000..20e72c3e --- /dev/null +++ b/cpp/movement_recognition/CMake/crosscompile-toolchain.cmake @@ -0,0 +1,40 @@ +## This file handles cross-compilation configurations for any architecture. +## The objective of this file is to find and assign cross-compiler and the +## entire toolchain. +## +## This configuration works best with the buildroot toolchain. When using this +## file, be sure to set the TOOLCHAIN_PREFIX and CMAKE_SYSROOT variables, +## preferably via the CMake configuration command (e.g. `-DCMAKE_SYSROOT=<...>`). +## +## You can use any toochain to produce the cross compiled binaries. However, +## we recommend using buildroot toolchain for cross-compilation. Here is the +## link to download the toolchains: https://toolchains.bootlin.com/ + +set(CMAKE_SYSTEM_NAME Linux) +set(CMAKE_SYSROOT) +set(TOOLCHAIN_PREFIX "" CACHE STRING "Path for toolchain for cross compiler and other compilation tools.") + +# Ensure that CMake tries to build static libraries when testing the compiler. +set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY) + +set(CMAKE_AR "${TOOLCHAIN_PREFIX}gcc-ar" CACHE FILEPATH "" FORCE) +set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}gcc) +set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}g++) +set(CMAKE_LINKER ${TOOLCHAIN_PREFIX}ld) +set(CMAKE_C_ARCHIVE_CREATE " qcs ") +set(CMAKE_C_ARCHIVE_FINISH true) +set(CMAKE_FORTRAN_COMPILER ${TOOLCHAIN_PREFIX}gfortran) +set(CMAKE_ASM_COMPILER ${CMAKE_C_COMPILER}) +set(CMAKE_OBJCOPY ${TOOLCHAIN_PREFIX}objcopy CACHE INTERNAL "objcopy tool") +set(CMAKE_SIZE_UTIL ${TOOLCHAIN_PREFIX}size CACHE INTERNAL "size tool") + +## Here are the standard ROOT_PATH if you are using the standard toolchain +## if you are using a different toolchain you have to specify that too. +set(CMAKE_FIND_ROOT_PATH "${CMAKE_SYSROOT}") + +set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --sysroot=${CMAKE_SYSROOT}" CACHE INTERNAL "" FORCE) + +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) diff --git a/cpp/movement_recognition/CMake/mlpack.cmake b/cpp/movement_recognition/CMake/mlpack.cmake new file mode 100644 index 00000000..e263ae1b --- /dev/null +++ b/cpp/movement_recognition/CMake/mlpack.cmake @@ -0,0 +1,745 @@ +# - Find mlpack +# Find the mlpack C++ library +# +# author Omar Shrit + +##=================================================== +## FUNCTION DOCUMENTATION +##=================================================== +# +# find_mlpack() +#---------------------- +# +# Call this macro to find mlpack and its dependencies (Armadillo, ensmallen, +# cereal, and any Armadillo dependencies). +# +# This function will not automatically download any missing dependencies, and +# will instead throw errors if dependencies are not found. For a version that +# automatically downloads dependencies, see `fetch_mlpack()`. +# +# Configuration options: +# +# MLPACK_DISABLE_OPENMP: if set, parallelism via OpenMP will be disabled. +# MLPACK_USE_SYSTEM_STB: if set, STB will be searched for on the system, +# instead of using the version bundled with mlpack. +# +# If mlpack is successfully found, the `MLPACK_FOUND` variable will be set to +# `TRUE`; otherwise, it will be set to `FALSE`. +# +# This macro will set the following variables: +# +# MLPACK_INCLUDE_DIRS: list of all include directories for mlpack and its +# dependencies (Armadillo, cereal, ensmallen) +# MLPACK_LIBRARIES: list of all dependency libraries to link against (typically +# just OpenBLAS) +# +# +# fetch_mlpack(COMPILE_OPENBLAS) +#----------------------- +# +# This macro downloads the mlpack library and its dependencies. Call this +# function to find mlpack and its dependencies (Armadillo, ensmallen, cereal) on +# a system where mlpack or those dependencies may not be available. +# +# fetch_mlpack() accepts one parameter, `COMPILE_OPENBLAS`. When this is set to +# `TRUE`, then OpenBLAS will be downloaded and compiled as a dependency of +# Armadillo. If `COMPILE_OPENBLAS` is set to `FALSE`, then it is expected that +# OpenBLAS or a BLAS/LAPACK library is already available on the system. When +# CMAKE_CROSSCOMPILING is set, then OpenBLAS is always compiled for the target +# architecture. +# +# Other dependencies of mlpack do not need compilation, as they are all +# header-only. +# +# If mlpack is not found on the system, `fetch_mlpack()` will download the +# latest stable version of mlpack. +# +# Configuration options: +# +# MLPACK_DISABLE_OPENMP: if set, parallelism via OpenMP will be disabled. +# MLPACK_DISABLE_STB: if set, mlpack image (STB) support is compiled out. +# MLPACK_DISABLE_DR_LIBS: if set, mlpack audio (dr_libs) support is compiled out. +# MLPACK_DISABLE_HTTPLIB: if set, mlpack httplib support is compiled out. +# MLPACK_USE_SYSTEM_STB: if set, STB will be searched for on the system, +# instead of using the version bundled with mlpack. +# +# After all libraries are downloaded and set up, the macro will set the +# following variables: +# +# MLPACK_INCLUDE_DIRS: list of all include directories for mlpack and its +# dependencies (Armadillo, cereal, ensmallen) +# MLPACK_LIBRARIES: list of all dependency libraries to link against (typically +# just OpenBLAS) +# +##=================================================== +## INTERNAL FUNCTION DOCUMENTATION +##=================================================== +# +# get_deps(LINK DEPS_NAME PACKAGE) +#------------------- +# +# This macro allows to download dependenices from the link that is provided to +# them. You need to pass the LINK to download from, the name of +# the dependency, and the filename to store the downloaded package to such +# as armadillo.tar.gz and they are downloaded into +# ${CMAKE_BINARY_DIR}/deps/${PACKAGE} +# At each download, this module sets a GENERIC_INCLUDE_DIR path, +# which means that you need to set the main path for the include +# directories for each package. +# Note that, the package should be compressed only as .tar.gz +# +# +# find_armadillo() +#------------------ +# +# This macro finds armadillo library, and sets the necessary paths to each +# one of the parameters. If the library is not found this macro will set +# ARMADILLO_FOUND to false. +# +# This macro sets the following variables: +# ARMADILLO_FOUND - set to true if the library is found +# ARMADILLO_INCLUDE_DIRS - list of required include directories +# ARMADILLO_LIBRARIES - list of libraries to be linked +# ARMADILLO_VERSION_MAJOR - major version number +# ARMADILLO_VERSION_MINOR - minor version number +# ARMADILLO_VERSION_PATCH - patch version number +# ARMADILLO_VERSION_STRING - version number as a string (ex: "1.0.4") +# ARMADILLO_VERSION_NAME - name of the version (ex: "Antipodean Antileech") +# +# +# find_ensmallen +#------------------ +# +# This macro finds ensmallen library and sets the necessary paths to each +# one of the parameters. If the library is not found this function will set +# ENSMALLEN_FOUND to false. +# +# This module sets the following variables: +# ENSMALLEN_FOUND - set to true if the library is found +# ENSMALLEN_INCLUDE_DIR - list of required include directories +# ENSMALLEN_VERSION_MAJOR - major version number +# ENSMALLEN_VERSION_MINOR - minor version number +# ENSMALLEN_VERSION_PATCH - patch version number +# ENSMALLEN_VERSION_STRING - version number as a string (ex: "1.0.4") +# ENSMALLEN_VERSION_NAME - name of the version (ex: "Antipodean Antileech") +# +# +# find_cereal() +#------------------ +# +# This macro finds cereal library and sets the necessary paths to each +# one of the parameters. If the library is not found this macro will set +# CEREAL_FOUND to false. + +# This module sets the following variables: +# CEREAL_FOUND - set to true if the library is found +# CEREAL_INCLUDE_DIR - list of required include directories +# CEREAL_VERSION_MAJOR - major version number +# CEREAL_VERSION_MINOR - minor version number +# CEREAL_VERSION_PATCH - patch version number +# CEREAL_VERSION_STRING - version number as a string (ex: "1.0.4") +# +# +# find stb() +#------------------ +# +# This macro finds STB library and sets the necessary paths to each +# one of the parameters. If the library is not found this macro will set +# STB_FOUND to false. +# +# - Find STB_IMAGE +# +# This module sets the following variables: +# STB_IMAGE_FOUND - set to true if the library is found +# STB_IMAGE_INCLUDE_DIR - list of required include directories +# STB_INCLUDE_NEEDS_STB_SUFFIX - whether or not the include files are under an +# stb/ directory; if "YES", then includes must be done as, e.g., +# stb/stb_image.h. +# +# +# find_openmp() +#----------------------- +# +# This macro finds if OpenMP library is installed and supported by the +# compiler. if the library found then it sets the following parameters: +# +# OpenMP_FOUND - set to true if the library is found +# +# +# find_mlpack_internal() +#----------------------- +# +# This macro finds the mlpack library and sets the necessary paths to each one +# of the parameters. If the library is not found this macro will set +# MLPACK_FOUND to false. +# +# This module sets the following variables: +# MLPACK_FOUND - set to true if the library is found +# MLPACK_VERSION_MAJOR - major version number +# MLPACK_VERSION_MINOR - minor version number +# MLPACK_VERSION_PATCH - patch version number +# MLPACK_VERSION_STRING - version number as a string (ex: "1.0.4") +# MLPACK_INCLUDE_DIR - list of mlpack include directories +# +# +##=================================================== +## MLPACK DEPENDENCIES SETTINGS. +##=================================================== + +# Set minimum library versions required by mlpack. +# +# For Armadillo, try to keep the minimum required version less than or equal to +# what's available on the current Ubuntu LTS or most recent stable RHEL release. +# See https://github.com/mlpack/mlpack/issues/3033 for some more discussion. +set(ARMADILLO_VERSION "10.8.2") +set(ENSMALLEN_VERSION "2.10.0") +set(CEREAL_VERSION "1.1.2") +set(OPENBLAS_VERSION "0.3.29") + +# Set library version to be used when fetching them from the source. +set(ARMADILLO_FETCH_VERSION "12.6.5") +set(ENSMALLEN_FETCH_VERSION "latest") +set(CEREAL_FETCH_VERSION "1.3.2") +set(MLPACK_FETCH_VERSION "latest") + +# Set required standard to C++17. +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +set(MLPACK_DISABLE_OPENMP OFF) + +option(MLPACK_DISABLE_STB "Disable mlpack image (STB) support" OFF) +option(MLPACK_DISABLE_DR_LIBS "Disable mlpack audio (dr_libs) support" OFF) +option(MLPACK_DISABLE_HTTPLIB "Disable mlpack httplib support" OFF) + +macro(apply_mlpack_compile_options) + foreach(_mlpack_opt MLPACK_DISABLE_STB MLPACK_DISABLE_DR_LIBS + MLPACK_DISABLE_HTTPLIB) + if (${_mlpack_opt}) + add_compile_definitions(${_mlpack_opt}) + endif () + endforeach() +endmacro() + +##=================================================== +## MLPACK AUTODOWNLOADER DEPENDENCIES FUNCTIONS +##=================================================== + +# This function auto-downloads mlpack dependencies. +macro(get_deps LINK DEPS_NAME PACKAGE) + if (NOT EXISTS "${CMAKE_BINARY_DIR}/deps/${PACKAGE}") + file(DOWNLOAD ${LINK} + "${CMAKE_BINARY_DIR}/deps/${PACKAGE}" + STATUS DOWNLOAD_STATUS_LIST LOG DOWNLOAD_LOG + SHOW_PROGRESS) + list(GET DOWNLOAD_STATUS_LIST 0 DOWNLOAD_STATUS) + if (DOWNLOAD_STATUS EQUAL 0) + execute_process(COMMAND ${CMAKE_COMMAND} -E + tar xf "${CMAKE_BINARY_DIR}/deps/${PACKAGE}" + WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/deps/") + else () + list(GET DOWNLOAD_STATUS_LIST 1 DOWNLOAD_ERROR) + message(FATAL_ERROR + "Could not download ${DEPS_NAME}! Error code ${DOWNLOAD_STATUS}: ${DOWNLOAD_ERROR}! Error log: ${DOWNLOAD_LOG}") + endif() + endif() + # Get the name of the directory. + file (GLOB DIRECTORIES RELATIVE "${CMAKE_BINARY_DIR}/deps/" + "${CMAKE_BINARY_DIR}/deps/${DEPS_NAME}*.*") + if(${DEPS_NAME} MATCHES "stb") + file (GLOB DIRECTORIES RELATIVE "${CMAKE_BINARY_DIR}/deps/" + "${CMAKE_BINARY_DIR}/deps/${DEPS_NAME}") + endif() + # list(FILTER) is not available on 3.5 or older, but try to keep + # configuring without filtering the list anyway + # (it works only if the file is present as .tar.gz). + list(FILTER DIRECTORIES EXCLUDE REGEX ".*\.tar\.gz") + list(LENGTH DIRECTORIES DIRECTORIES_LEN) + if (DIRECTORIES_LEN GREATER 0) + list(GET DIRECTORIES 0 DEPENDENCY_DIR) + set(GENERIC_INCLUDE_DIR "${CMAKE_BINARY_DIR}/deps/${DEPENDENCY_DIR}/include") + install(DIRECTORY "${GENERIC_INCLUDE_DIR}/" DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}") + else () + message(FATAL_ERROR + "Problem unpacking ${DEPS_NAME}! Expected only one directory " + "${DEPS_NAME};. Try to remove the directory ${CMAKE_BINARY_DIR}/deps and reconfigure.") + endif () +endmacro() + +macro(find_armadillo) + cmake_policy(PUSH) + cmake_policy(SET CMP0159 NEW) # file(STRINGS) with REGEX updates CMAKE_MATCH_ + + set(CURRENT_PATH ${ARGN}) + if (CURRENT_PATH) + find_path(ARMADILLO_INCLUDE_DIR + NAMES armadillo + PATHS "${CURRENT_PATH}/deps/Armadillo/include" + NO_DEFAULT_PATH) + else() + find_path(ARMADILLO_INCLUDE_DIR + NAMES armadillo + PATHS "$ENV{ProgramFiles}/Armadillo/include") + endif() + if (ARMADILLO_INCLUDE_DIR) + # ------------------------------------------------------------------------ + # Extract version information from + # ------------------------------------------------------------------------ + + # WARNING: Early releases of Armadillo didn't have the arma_version.hpp file. + # (e.g. v.0.9.8-1 in ubuntu maverick packages (2001-03-15)) + # If the file is missing, set all values to 0 + set(ARMADILLO_VERSION_MAJOR 0) + set(ARMADILLO_VERSION_MINOR 0) + set(ARMADILLO_VERSION_PATCH 0) + set(ARMADILLO_VERSION_NAME "EARLY RELEASE") + + if (EXISTS "${ARMADILLO_INCLUDE_DIR}/armadillo_bits/arma_version.hpp") + + # Read and parse armdillo version header file for version number + file(STRINGS "${ARMADILLO_INCLUDE_DIR}/armadillo_bits/arma_version.hpp" _ARMA_HEADER_CONTENTS REGEX "#define ARMA_VERSION_[A-Z]+ ") + string(REGEX REPLACE ".*#define ARMA_VERSION_MAJOR ([0-9]+).*" "\\1" ARMADILLO_VERSION_MAJOR "${_ARMA_HEADER_CONTENTS}") + string(REGEX REPLACE ".*#define ARMA_VERSION_MINOR ([0-9]+).*" "\\1" ARMADILLO_VERSION_MINOR "${_ARMA_HEADER_CONTENTS}") + string(REGEX REPLACE ".*#define ARMA_VERSION_PATCH ([0-9]+).*" "\\1" ARMADILLO_VERSION_PATCH "${_ARMA_HEADER_CONTENTS}") + + # WARNING: The number of spaces before the version name is not one. + string(REGEX REPLACE ".*#define ARMA_VERSION_NAME\ +\"([0-9a-zA-Z\ _-]+)\".*" "\\1" ARMADILLO_VERSION_NAME "${_ARMA_HEADER_CONTENTS}") + set(ARMADILLO_FOUND YES) + endif() + + set(ARMADILLO_VERSION_STRING "${ARMADILLO_VERSION_MAJOR}.${ARMADILLO_VERSION_MINOR}.${ARMADILLO_VERSION_PATCH}") + endif() + + if (EXISTS "${ARMADILLO_INCLUDE_DIR}/armadillo_bits/config.hpp") + file(STRINGS "${ARMADILLO_INCLUDE_DIR}/armadillo_bits/config.hpp" _ARMA_CONFIG_CONTENTS REGEX "^#define ARMA_USE_[A-Z]+") + string(REGEX MATCH "ARMA_USE_WRAPPER" _ARMA_USE_WRAPPER "${_ARMA_CONFIG_CONTENTS}") + string(REGEX MATCH "ARMA_USE_LAPACK" _ARMA_USE_LAPACK "${_ARMA_CONFIG_CONTENTS}") + string(REGEX MATCH "ARMA_USE_BLAS" _ARMA_USE_BLAS "${_ARMA_CONFIG_CONTENTS}") + string(REGEX MATCH "ARMA_USE_ARPACK" _ARMA_USE_ARPACK "${_ARMA_CONFIG_CONTENTS}") + string(REGEX MATCH "ARMA_USE_HDF5" _ARMA_USE_HDF5 "${_ARMA_CONFIG_CONTENTS}") + endif() + + # If _ARMA_USE_WRAPPER is set, then we just link to armadillo, but if it's not then we need support libraries instead + set(_ARMA_SUPPORT_LIBRARIES) + + if(_ARMA_USE_WRAPPER) + # Link to the armadillo wrapper library. + find_library(ARMADILLO_LIBRARY + NAMES armadillo + NAMES_PER_DIR + PATHS + "$ENV{ProgramFiles}/Armadillo/lib" + "$ENV{ProgramFiles}/Armadillo/lib64" + "$ENV{ProgramFiles}/Armadillo" + ) + set(_ARMA_REQUIRED_VARS ARMADILLO_LIBRARY) + else() + set(ARMADILLO_LIBRARY "") + endif() + + # Transitive linking with the wrapper does not work with MSVC, + # so we must *also* link against Armadillo's dependencies. + if(NOT _ARMA_USE_WRAPPER OR MSVC) + # Link directly to individual components. + foreach(pkg + LAPACK + BLAS + ARPACK + HDF5 + ) + if(_ARMA_USE_${pkg}) + find_package(${pkg} QUIET) + list(APPEND _ARMA_REQUIRED_VARS "${pkg}_FOUND") + if(${pkg}_FOUND) + list(APPEND _ARMA_SUPPORT_LIBRARIES ${${pkg}_LIBRARIES}) + endif() + endif() + endforeach() + endif() + if (ARMADILLO_FOUND) + set(ARMADILLO_INCLUDE_DIRS ${ARMADILLO_INCLUDE_DIR}) + set(ARMADILLO_LIBRARIES ${ARMADILLO_LIBRARY} ${_ARMA_SUPPORT_LIBRARIES}) + endif() + # Clean up internal variables + unset(_ARMA_REQUIRED_VARS) + unset(_ARMA_SUPPORT_LIBRARIES) + unset(_ARMA_USE_WRAPPER) + unset(_ARMA_USE_LAPACK) + unset(_ARMA_USE_BLAS) + unset(_ARMA_USE_ARPACK) + unset(_ARMA_USE_HDF5) + unset(_ARMA_CONFIG_CONTENTS) + unset(_ARMA_HEADER_CONTENTS) + + cmake_policy(POP) +endmacro() + +# Findcereal.cmake +macro(find_cereal) + + set(CURRENT_PATH ${ARGN}) + if (CURRENT_PATH) + find_path(CEREAL_INCLUDE_DIR + NAMES cereal + PATHS "${CURRENT_PATH}/deps/cereal/include" + NO_DEFAULT_PATH) + else() + find_path(CEREAL_INCLUDE_DIR + NAMES cereal + PATHS "$ENV{ProgramFiles}/cereal/include") + endif() + + if (CEREAL_INCLUDE_DIR) + # ------------------------------------------------------------------------ + # Extract version information from + # ------------------------------------------------------------------------ + set(CEREAL_FOUND YES) + set(CEREAL_VERSION_MAJOR 0) + set(CEREAL_VERSION_MINOR 0) + set(CEREAL_VERSION_PATCH 0) + + if (EXISTS "${CEREAL_INCLUDE_DIR}/cereal/version.hpp") + + # Read and parse cereal version header file for version number + file(READ "${CEREAL_INCLUDE_DIR}/cereal/version.hpp" + _CEREAL_HEADER_CONTENTS) + string(REGEX REPLACE ".*#define CEREAL_VERSION_MAJOR ([0-9]+).*" "\\1" + CEREAL_VERSION_MAJOR "${_CEREAL_HEADER_CONTENTS}") + string(REGEX REPLACE ".*#define CEREAL_VERSION_MINOR ([0-9]+).*" "\\1" + CEREAL_VERSION_MINOR "${_CEREAL_HEADER_CONTENTS}") + string(REGEX REPLACE ".*#define CEREAL_VERSION_PATCH ([0-9]+).*" "\\1" + CEREAL_VERSION_PATCH "${_CEREAL_HEADER_CONTENTS}") + + elseif (EXISTS "${CEREAL_INCLUDE_DIR}/cereal/details/polymorphic_impl_fwd.hpp") + + set(CEREAL_VERSION_MAJOR 1) + set(CEREAL_VERSION_MINOR 2) + set(CEREAL_VERSION_PATCH 0) + elseif (EXISTS "${CEREAL_INCLUDE_DIR}/cereal/types/valarray.hpp") + + set(CEREAL_VERSION_MAJOR 1) + set(CEREAL_VERSION_MINOR 1) + set(CEREAL_VERSION_PATCH 2) + elseif (EXISTS "${CEREAL_INCLUDE_DIR}/cereal/cereal.hpp") + + set(CEREAL_VERSION_MAJOR 1) + set(CEREAL_VERSION_MINOR 1) + set(CEREAL_VERSION_PATCH 1) + else() + + set(CEREAL_FOUND NO) + endif() + set(CEREAL_VERSION_STRING "${CEREAL_VERSION_MAJOR}.${CEREAL_VERSION_MINOR}.${CEREAL_VERSION_PATCH}") + endif () + +endmacro() + +macro(find_ensmallen) + + set(CURRENT_PATH ${ARGN}) + if (CURRENT_PATH) + find_path(ENSMALLEN_INCLUDE_DIR + NAMES ensmallen.hpp + PATHS "${CURRENT_PATH}/deps/ensmallen/include" + NO_DEFAULT_PATH) + else() + file(GLOB ENSMALLEN_SEARCH_PATHS + ${CMAKE_BINARY_DIR}/deps/ensmallen-[0-9]*.[0-9]*.[0-9]*) + find_path(ENSMALLEN_INCLUDE_DIR + NAMES ensmallen.hpp + PATHS ${ENSMALLEN_SEARCH_PATHS}/include) + endif() + + if (ENSMALLEN_INCLUDE_DIR) + # ------------------------------------------------------------------------ + # Extract version information from + # ------------------------------------------------------------------------ + + set(ENSMALLEN_VERSION_MAJOR 0) + set(ENSMALLEN_VERSION_MINOR 0) + set(ENSMALLEN_VERSION_PATCH 0) + set(ENSMALLEN_VERSION_NAME "unknown") + + if(EXISTS "${ENSMALLEN_INCLUDE_DIR}/ensmallen_bits/ens_version.hpp") + + set(ENSMALLEN_FOUND YES) + + # Read and parse Ensmallen version header file for version number + file(READ "${ENSMALLEN_INCLUDE_DIR}/ensmallen_bits/ens_version.hpp" + _ensmallen_HEADER_CONTENTS) + string(REGEX REPLACE ".*#define ENS_VERSION_MAJOR ([0-9]+).*" "\\1" + ENSMALLEN_VERSION_MAJOR "${_ensmallen_HEADER_CONTENTS}") + string(REGEX REPLACE ".*#define ENS_VERSION_MINOR ([0-9]+).*" "\\1" + ENSMALLEN_VERSION_MINOR "${_ensmallen_HEADER_CONTENTS}") + string(REGEX REPLACE ".*#define ENS_VERSION_PATCH ([0-9]+).*" "\\1" + ENSMALLEN_VERSION_PATCH "${_ensmallen_HEADER_CONTENTS}") + + # WARNING: The number of spaces before the version name is not one. + string(REGEX REPLACE + ".*#define ENS_VERSION_NAME\ +\"([0-9a-zA-Z\ _-]+)\".*" "\\1" + ENSMALLEN_VERSION_NAME "${_ensmallen_HEADER_CONTENTS}") + + endif() + + set(ENSMALLEN_VERSION_STRING "${ENSMALLEN_VERSION_MAJOR}.${ENSMALLEN_VERSION_MINOR}.${ENSMALLEN_VERSION_PATCH}") + endif () +endmacro() + +macro(find_stb) + file(GLOB STB_IMAGE_SEARCH_PATHS + ${CMAKE_BINARY_DIR}/deps/ + ${CMAKE_BINARY_DIR}/deps/stb) + find_path(STB_IMAGE_INCLUDE_DIR_1 + NAMES stb_image.h stb_image_write.h stb_image_resize2.h + PATHS ${STB_IMAGE_SEARCH_PATHS} ${STB_IMAGE_INCLUDE_DIR}) + + if(STB_IMAGE_INCLUDE_DIR_1) + set(STB_IMAGE_INCLUDE_DIR "${STB_IMAGE_INCLUDE_DIR_1}" CACHE PATH + "stb_image include directory") + + # Either we found /usr/include/stb_image.h (or similar), or the user passed + # a directory in STB_IMAGE_SEARCH_PATHS that directly contains stb_image.h. + # In either of those cases, we want to include , not + # . + set(STB_INCLUDE_NEEDS_STB_SUFFIX "NO") + else () + find_path(STB_IMAGE_INCLUDE_DIR_2 + NAMES stb_image.h stb_image_write.h stb_image_resize2.h + PATHS ${STB_IMAGE_SEARCH_PATHS} ${STB_IMAGE_INCLUDE_DIR} + PATH_SUFFIXES stb/) + + if (STB_IMAGE_INCLUDE_DIR_2) + set(STB_IMAGE_INCLUDE_DIR "${STB_IMAGE_INCLUDE_DIR_2}" CACHE PATH + "stb_image include directory") + + # Since we searched the same paths but allowed an stb/ suffix this time, + # then there is definitely a suffix. + set(STB_INCLUDE_NEEDS_STB_SUFFIX "YES") + # Strip the suffix. + string(REGEX REPLACE ".*stb[/]?$" "" STB_IMAGE_INCLUDE_DIR + "${STB_IMAGE_INCLUDE_DIR}") + endif () + endif () + +endmacro() + +macro(find_openmp) + find_package(OpenMP) + + if (OpenMP_FOUND AND OpenMP_CXX_VERSION VERSION_GREATER_EQUAL 3.0.0) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${OpenMP_C_FLAGS}") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${OpenMP_CXX_FLAGS}") + set(MLPACK_LIBRARIES ${MLPACK_LIBRARIES} ${OpenMP_CXX_LIBRARIES}) + else () + # Disable warnings for all the unknown OpenMP pragmas. + if (NOT MSVC) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-unknown-pragmas") + else () + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4068") + endif () + set(OpenMP_CXX_FLAGS "") + endif () +endmacro() + +macro(find_mlpack_internal) + + set(CURRENT_PATH ${ARGN}) + + if (CURRENT_PATH) + file(GLOB MLPACK_SEARCH_PATHS + ${CURRENT_PATH}/deps/mlpack-[0-9]*.[0-9]*.[0-9]*/src/) + + list(POP_BACK MLPACK_SEARCH_PATHS MLPACK_SEARCH_PATH) + if (EXISTS ${MLPACK_SEARCH_PATH}/mlpack.hpp) + set(MLPACK_INCLUDE_DIR ${MLPACK_SEARCH_PATH}) + endif() + + else() + file(GLOB MLPACK_SEARCH_PATHS + ${CMAKE_BINARY_DIR}/deps/mlpack-[0-9]*.[0-9]*.[0-9]*) + + # This will be executed if mlpack is installed already. + find_path(MLPACK_INCLUDE_DIR + NAMES mlpack.hpp + PATHS ${MLPACK_SEARCH_PATHS}/include) + + # This will be executed when compiling mlpack bindings and tests. + if (NOT MLPACK_INCLUDE_DIR) + find_path(MLPACK_INCLUDE_DIR + NAMES mlpack.hpp + PATHS "${CMAKE_CURRENT_SOURCE_DIR}/src/") + endif() + endif() + + if (MLPACK_INCLUDE_DIR) + # ------------------------------------------------------------------------ + # Extract version information from + # ------------------------------------------------------------------------ + + set(MLPACK_VERSION_MAJOR 0) + set(MLPACK_VERSION_MINOR 0) + set(MLPACK_VERSION_PATCH 0) + + if (EXISTS "${MLPACK_INCLUDE_DIR}/mlpack/core/util/version.hpp") + + set(MLPACK_FOUND YES) + + # Read and parse mlpack version header file for version number + file(READ "${MLPACK_INCLUDE_DIR}/mlpack/core/util/version.hpp" + _mlpack_HEADER_CONTENTS) + string(REGEX REPLACE ".*#define MLPACK_VERSION_MAJOR ([0-9]+).*" "\\1" + MLPACK_VERSION_MAJOR "${_mlpack_HEADER_CONTENTS}") + string(REGEX REPLACE ".*#define MLPACK_VERSION_MINOR ([0-9]+).*" "\\1" + MLPACK_VERSION_MINOR "${_mlpack_HEADER_CONTENTS}") + string(REGEX REPLACE ".*#define MLPACK_VERSION_PATCH ([0-9]+).*" "\\1" + MLPACK_VERSION_PATCH "${_mlpack_HEADER_CONTENTS}") + + endif() + + set(MLPACK_VERSION_STRING "${MLPACK_VERSION_MAJOR}.${MLPACK_VERSION_MINOR}.${MLPACK_VERSION_PATCH}") + endif() + + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(mlpack + REQUIRED_VARS MLPACK_INCLUDE_DIR + VERSION_VAR MLPACK_VERSION_STRING) + +endmacro() + +macro(fetch_mlpack COMPILE_OPENBLAS) + + if (CMAKE_CROSSCOMPILING) + search_openblas(${OPENBLAS_VERSION}) + # Set to cross compile openblas if the user forgot to do so. + set(COMPILE_OPENBLAS ON) + endif() + + find_package(BLAS PATHS ${CMAKE_BINARY_DIR}) + if (NOT BLAS_FOUND OR (NOT BLAS_LIBRARIES)) + get_deps(https://github.com/xianyi/OpenBLAS/releases/download/v${OPENBLAS_VERSION}/OpenBLAS-${OPENBLAS_VERSION}.tar.gz + OpenBLAS OpenBLAS-${OPENBLAS_VERSION}.tar.gz) + if (NOT COMPILE_OPENBLAS) + message(WARNING "OpenBLAS is downloaded but not compiled. Please compile + OpenBLAS before compiling mlpack") + else() + execute_process(COMMAND make NO_SHARED=1 WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/deps/OpenBLAS-${version}) + file(GLOB OPENBLAS_LIBRARIES "${CMAKE_BINARY_DIR}/deps/OpenBLAS-${version}/libopenblas.a") + set(BLAS_openblas_LIBRARY ${OPENBLAS_LIBRARIES}) + set(LAPACK_openblas_LIBRARY ${OPENBLAS_LIBRARIES}) + set(BLAS_FOUND ON) + endif() + endif() + + find_armadillo(${CMAKE_BINARY_DIR}) + if (NOT ARMADILLO_FOUND) + if (NOT CMAKE_CROSSCOMPILING) + find_package(BLAS QUIET) + find_package(LAPACK QUIET) + endif() + get_deps(https://files.mlpack.org/armadillo-${ARMADILLO_FETCH_VERSION}.tar.gz armadillo armadillo-${ARMADILLO_FETCH_VERSION}.tar.gz) + set(ARMADILLO_INCLUDE_DIR ${GENERIC_INCLUDE_DIR}) + find_armadillo(${CMAKE_BINARY_DIR}) + endif() + if (ARMADILLO_FOUND) + # Include directories for the previous dependencies. + set(MLPACK_INCLUDE_DIRS ${MLPACK_INCLUDE_DIRS} ${ARMADILLO_INCLUDE_DIRS}) + set(MLPACK_LIBRARIES ${MLPACK_LIBRARIES} ${ARMADILLO_LIBRARIES}) + endif() + + find_ensmallen(${CMAKE_BINARY_DIR}) + if (NOT ENSMALLEN_FOUND) + get_deps(https://www.ensmallen.org/files/ensmallen-${ENSMALLEN_FETCH_VERSION}.tar.gz ensmallen ensmallen-${ENSMALLEN_FETCH_VERSION}.tar.gz) + set(ENSMALLEN_INCLUDE_DIR ${GENERIC_INCLUDE_DIR}) + find_ensmallen(${CMAKE_BINARY_DIR}) + endif() + if (ENSMALLEN_FOUND) + set(MLPACK_INCLUDE_DIRS ${MLPACK_INCLUDE_DIRS} ${ENSMALLEN_INCLUDE_DIR}) + endif() + + find_cereal(${CMAKE_BINARY_DIR}) + if (NOT CEREAL_FOUND) + get_deps(https://github.com/USCiLab/cereal/archive/refs/tags/v${CEREAL_FETCH_VERSION}.tar.gz cereal cereal-${CEREAL_FETCH_VERSION}.tar.gz) + set(CEREAL_INCLUDE_DIR ${GENERIC_INCLUDE_DIR}) + find_cereal(${CMAKE_BINARY_DIR}) + endif() + if (CEREAL_FOUND) + set(MLPACK_INCLUDE_DIRS ${MLPACK_INCLUDE_DIRS} ${CEREAL_INCLUDE_DIR}) + endif() + + find_mlpack_internal(${CMAKE_BINARY_DIR}) + if (NOT MLPACK_FOUND) + get_deps(https://www.mlpack.org/files/mlpack-${MLPACK_FETCH_VERSION}.tar.gz mlpack mlpack-${MLPACK_FETCH_VERSION}.tar.gz) + set(MLPACK_INCLUDE_DIR ${GENERIC_INCLUDE_DIR}) + find_mlpack_internal(${CMAKE_BINARY_DIR}) + endif() + if (MLPACK_FOUND) + set(MLPACK_INCLUDE_DIRS ${MLPACK_INCLUDE_DIRS} ${MLPACK_INCLUDE_DIR}) + endif() + + find_openmp() + + apply_mlpack_compile_options() + +endmacro() + +##=================================================== +## MLPACK MAIN FUNCTIONS CALL. +##=================================================== + +macro(find_mlpack) + # If we're using gcc, then we need to link against pthreads to use std::thread, + # which we do in the tests. + if (CMAKE_COMPILER_IS_GNUCC) + find_package(Threads) + set(MLPACK_LIBRARIES ${MLPACK_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT}) + endif() + + if (NOT MLPACK_DISABLE_OPENMP) + find_openmp() + endif () + + find_armadillo() + if (ARMADILLO_FOUND) + set(MLPACK_INCLUDE_DIRS ${ARMADILLO_INCLUDE_DIRS}) + set(MLPACK_LIBRARIES ${MLPACK_LIBRARIES} ${ARMADILLO_LIBRARIES}) + else() + message(FATAL_ERROR "Armadillo not found, (required dependency of mlpack).") + endif () + + find_ensmallen() + if (ENSMALLEN_FOUND) + set(MLPACK_INCLUDE_DIRS ${MLPACK_INCLUDE_DIRS} ${ENSMALLEN_INCLUDE_DIR}) + else() + message(FATAL_ERROR "Ensmallen not found, (required dependency of mlpack).") + endif() + + find_cereal() + if (CEREAL_FOUND) + set(MLPACK_INCLUDE_DIRS ${MLPACK_INCLUDE_DIRS} ${CEREAL_INCLUDE_DIR}) + else() + message(FATAL_ERROR "Cereal not found, (required dependency of mlpack).") + endif() + + if (MLPACK_USE_SYSTEM_STB) + find_stb() + endif() + if (StbImage_FOUND) + set(STB_AVAILABLE "1") + add_definitions(-DMLPACK_USE_SYSTEM_STB) + set(MLPACK_INCLUDE_DIRS ${MLPACK_INCLUDE_DIRS} ${STB_IMAGE_INCLUDE_DIR}) + endif() + + find_mlpack_internal() + if (MLPACK_FOUND) + set(MLPACK_INCLUDE_DIRS ${MLPACK_INCLUDE_DIRS} ${MLPACK_INCLUDE_DIR}) + else() + message(FATAL_ERROR "mlpack not found!") + endif() + + apply_mlpack_compile_options() + + mark_as_advanced(MLPACK_INCLUDE_DIR) + mark_as_advanced(MLPACK_INCLUDE_DIRS) + mark_as_advanced(MLPACK_LIBRARIES) + +endmacro() diff --git a/cpp/movement_recognition/CMakeLists.txt b/cpp/movement_recognition/CMakeLists.txt new file mode 100644 index 00000000..2c12803e --- /dev/null +++ b/cpp/movement_recognition/CMakeLists.txt @@ -0,0 +1,110 @@ +# Build for the movement-recognition example: one CMake project, four programs. +# +# imu_test -- sensor check + magnetometer calibration (no mlpack) +# collect -- record sensor data to CSV (no mlpack) +# train -- train a neural network from the CSVs (mlpack) +# infer -- live inference from the IMU (mlpack) +# +# `imu_test` and `collect` only need the Linux I2C headers; they are compiled +# exception-free and size-optimized into small static binaries. `train` and +# `infer` link mlpack, so this project fetches mlpack + its dependencies and +# cross-compiles OpenBLAS via the CMake/ machinery (see ConfigureCrossCompile). +# +# Host build (your computer): +# cmake .. && make +# +# Cross-compile for the Milk-V Duo (SOPHGO CV1800B, RISC-V C906, 64 MB) with a +# Bootlin riscv64-lp64d musl toolchain: +# cmake -DCMAKE_CROSSCOMPILING=ON -DARCH_NAME=RV64GCV \ +# -DCMAKE_TOOLCHAIN_FILE=../CMake/crosscompile-toolchain.cmake \ +# -DTOOLCHAIN_PREFIX=/path/to/.../bin/riscv64-buildroot-linux-musl- \ +# -DCMAKE_SYSROOT=/path/to/.../riscv64-buildroot-linux-musl/sysroot .. +# make +# +# Build a single program with, e.g., `make collect` or `make train`. + +cmake_minimum_required(VERSION 3.11) +project(movement_recognition CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +include(CMake/ConfigureCrossCompile.cmake) +include(CMake/mlpack.cmake) + +# The Duo is effectively single-core for this workload; OpenMP buys nothing and +# its runtime spawns spinning worker threads that starve the program at startup. +# Disable it AFTER include(mlpack.cmake) (which resets the flag) and BEFORE +# fetch_mlpack() (which runs find_package(OpenMP)). +set(MLPACK_DISABLE_OPENMP ON) + +# This example only loads CSVs, never images, so compile out mlpack's image +# (STB) support to shrink the binary (~50-80 KB). MLPACK_DISABLE_STB is one of +# the footprint options mlpack.cmake exposes; setting it here (or on the command +# line, -DMLPACK_DISABLE_STB=ON) makes fetch_mlpack() apply the compile +# definition for us. Harmless no-op on an mlpack that predates the switch. +set(MLPACK_DISABLE_STB ON) + +# Download + configure mlpack and its deps (armadillo, ensmallen, cereal, +# OpenBLAS, built single-threaded -- see ConfigureCrossCompile.cmake). +fetch_mlpack(ON) + +# Link statically for the device (so nothing needs to be installed on the Duo); +# a host build links normally, which needs no static system libraries. +if(CMAKE_CROSSCOMPILING) + set(LINK_STATIC -static) +endif() + +# The GY-89 driver is self-contained: the I2C wrapper and the three chip drivers. +# The magnetometer calibration and the SensorBoard that composes everything into +# one object are small enough to be header-only (see their .hpp files), so they +# need no entry here. Every program that talks to the sensor compiles these in. +set(DRIVER_SOURCES + driver/imu.cpp + driver/l3gd20h.cpp + driver/lsm303d.cpp + driver/bmp180.cpp + driver/i2c_bus.cpp +) + +# --- imu_test and collect: tiny, mlpack-free, exception-free ---------------- +# Built for size (no exceptions/RTTI, dead-section stripped) so they stay in the +# 150-170 KB range on the device. They do not depend on mlpack/Armadillo. +add_executable(imu_test driver/imu_test.cpp ${DRIVER_SOURCES}) +add_executable(collect collect/collect_main.cpp ${DRIVER_SOURCES}) + +foreach(tool imu_test collect) + target_compile_options(${tool} PRIVATE + -Os -fno-exceptions -fno-rtti -ffunction-sections -fdata-sections) + target_link_libraries(${tool} PRIVATE ${LINK_STATIC} + -Wl,--gc-sections + pthread) # std::this_thread::sleep_* pulls in pthread +endforeach() + +# --- train and infer: link mlpack ------------------------------------------- +# When cross-compiling, link the cross-built OpenBLAS explicitly, plus its +# gfortran/m runtime: Armadillo is fetched in "wrapper" mode, so MLPACK_LIBRARIES +# can come out without BLAS, which the FFN / matrix paths need (dgemm_, ddot_, +# ...). On a host build MLPACK_LIBRARIES already carries the system BLAS. +if(CMAKE_CROSSCOMPILING) + file(GLOB OPENBLAS_LIB "${CMAKE_BINARY_DIR}/deps/OpenBLAS-*/libopenblas.a") + set(MLPACK_EXTRA_LIBS ${OPENBLAS_LIB} gfortran m) +endif() + +add_executable(train train/train.cpp) +target_include_directories(train SYSTEM PRIVATE ${MLPACK_INCLUDE_DIRS}) +target_link_libraries(train PRIVATE ${LINK_STATIC} + ${MLPACK_LIBRARIES} + ${MLPACK_EXTRA_LIBS} + pthread +) + +# The inference tool talks to the IMU, so it links the driver alongside mlpack; +# it prints predictions to stdout. +add_executable(infer infer/infer.cpp ${DRIVER_SOURCES}) +target_include_directories(infer SYSTEM PRIVATE ${MLPACK_INCLUDE_DIRS}) +target_link_libraries(infer PRIVATE ${LINK_STATIC} + ${MLPACK_LIBRARIES} + ${MLPACK_EXTRA_LIBS} + pthread +) diff --git a/cpp/movement_recognition/collect/collect_main.cpp b/cpp/movement_recognition/collect/collect_main.cpp new file mode 100644 index 00000000..68c9d1b6 --- /dev/null +++ b/cpp/movement_recognition/collect/collect_main.cpp @@ -0,0 +1,220 @@ +/** + * @file collect_main.cpp + * @author Omar Shrit + * + * Small data-collection app that records GY-89 sensor data to a CSV file. It + * depends only on the drivers in ../driver; argument parsing is hand-rolled and + * all I/O is C stdio, so the binary stays tiny and exception-free. + * + * - choose which sensors to record (`--sensors accel,gyro,mag,baro` or `all`); + * - timestamp every row with the Unix clock in microseconds; + * - name the output file `