diff --git a/.gitignore b/.gitignore index 9d84258..54717f5 100644 --- a/.gitignore +++ b/.gitignore @@ -33,4 +33,5 @@ *.app build/ *.dSYM/** - +cmake-build*/** +.idea diff --git a/CMakeLists.txt b/CMakeLists.txt index 4d7b7f7..137e21e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,14 +1,59 @@ cmake_minimum_required(VERSION 3.20) -project(BayesNet) +project(BayesNet + VERSION 0.1.0 + DESCRIPTION "Bayesian Network and basic classifiers Library." + HOMEPAGE_URL "https://github.com/rmontanana/bayesnet" + LANGUAGES CXX +) + find_package(Torch REQUIRED) if (POLICY CMP0135) cmake_policy(SET CMP0135 NEW) endif () +# Global CMake variables +# ---------------------- set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${TORCH_CXX_FLAGS}") +# Options +# ------- +option(ENABLE_CLANG_TIDY "Enable to add clang tidy." OFF) +option(ENABLE_TESTING "Unit testing build" ON) +option(CODE_COVERAGE "Collect coverage from test library" ON) + +set(CMAKE_BUILD_TYPE "Debug") + +# CMakes modules +# -------------- +set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules ${CMAKE_MODULE_PATH}) + + +# Subdirectories +# -------------- +add_subdirectory(config) add_subdirectory(src) -add_subdirectory(sample) \ No newline at end of file +add_subdirectory(sample) + +# Testing +# ------- +if (ENABLE_TESTING) + MESSAGE("Testing enabled") + enable_testing() + if (CODE_COVERAGE) + include(CodeCoverage) + MESSAGE("Code coverage enabled") + set(CMAKE_C_FLAGS " ${CMAKE_C_FLAGS} -fprofile-arcs -ftest-coverage") + set(CMAKE_CXX_FLAGS " ${CMAKE_CXX_FLAGS} -fprofile-arcs -ftest-coverage") + SET(GCC_COVERAGE_LINK_FLAGS " ${GCC_COVERAGE_LINK_FLAGS} -lgcov --coverage") + endif (CODE_COVERAGE) + find_package(Catch2 3 REQUIRED) + include(CTest) + include(Catch) + add_subdirectory(tests) +endif (ENABLE_TESTING) \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e02fe9a --- /dev/null +++ b/Makefile @@ -0,0 +1,59 @@ +SHELL := /bin/bash +.DEFAULT_GOAL := help +.PHONY: coverage setup help build test + +setup: ## Install dependencies for tests and coverage + @if [ "$(shell uname)" = "Darwin" ]; then \ + brew install gcovr; \ + brew install lcov; \ + fi + @if [ "$(shell uname)" = "Linux" ]; then \ + pip install gcovr; \ + fi + +dependency: ## Create a dependency graph diagram of the project (build/dependency.png) + cd build && cmake .. --graphviz=dependency.dot && dot -Tpng dependency.dot -o dependency.png + +build: ## Build the project + @echo ">>> Building BayesNet ..."; + @if [ -d ./build ]; then rm -rf ./build; fi + @mkdir build; + cmake -S . -B build; \ + cd build; \ + make; \ + + @echo ">>> Done"; + +test: ## Run tests + @echo "* Running tests..."; + find . -name "*.gcda" -print0 | xargs -0 rm + @cd build; \ + cmake --build . --target unit_tests ; + @cd build/tests; \ + ./unit_tests; + +coverage: ## Run tests and generate coverage report (build/index.html) + @echo "*Building tests..."; + find . -name "*.gcda" -print0 | xargs -0 rm + @cd build; \ + cmake --build . --target unit_tests ; + @cd build/tests; \ + ./unit_tests; + gcovr ; + +help: ## Show help message + @IFS=$$'\n' ; \ + help_lines=(`fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/##/:/'`); \ + printf "%s\n\n" "Usage: make [task]"; \ + printf "%-20s %s\n" "task" "help" ; \ + printf "%-20s %s\n" "------" "----" ; \ + for help_line in $${help_lines[@]}; do \ + IFS=$$':' ; \ + help_split=($$help_line) ; \ + help_command=`echo $${help_split[0]} | sed -e 's/^ *//' -e 's/ *$$//'` ; \ + help_info=`echo $${help_split[2]} | sed -e 's/^ *//' -e 's/ *$$//'` ; \ + printf '\033[36m'; \ + printf "%-20s %s" $$help_command ; \ + printf '\033[0m'; \ + printf "%s\n" $$help_info; \ + done diff --git a/cmake/modules/CodeCoverage.cmake b/cmake/modules/CodeCoverage.cmake new file mode 100644 index 0000000..d4a039f --- /dev/null +++ b/cmake/modules/CodeCoverage.cmake @@ -0,0 +1,742 @@ +# Copyright (c) 2012 - 2017, Lars Bilke +# All rights reserved. +# +# 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. +# +# CHANGES: +# +# 2012-01-31, Lars Bilke +# - Enable Code Coverage +# +# 2013-09-17, Joakim Söderberg +# - Added support for Clang. +# - Some additional usage instructions. +# +# 2016-02-03, Lars Bilke +# - Refactored functions to use named parameters +# +# 2017-06-02, Lars Bilke +# - Merged with modified version from github.com/ufz/ogs +# +# 2019-05-06, Anatolii Kurotych +# - Remove unnecessary --coverage flag +# +# 2019-12-13, FeRD (Frank Dana) +# - Deprecate COVERAGE_LCOVR_EXCLUDES and COVERAGE_GCOVR_EXCLUDES lists in favor +# of tool-agnostic COVERAGE_EXCLUDES variable, or EXCLUDE setup arguments. +# - CMake 3.4+: All excludes can be specified relative to BASE_DIRECTORY +# - All setup functions: accept BASE_DIRECTORY, EXCLUDE list +# - Set lcov basedir with -b argument +# - Add automatic --demangle-cpp in lcovr, if 'c++filt' is available (can be +# overridden with NO_DEMANGLE option in setup_target_for_coverage_lcovr().) +# - Delete output dir, .info file on 'make clean' +# - Remove Python detection, since version mismatches will break gcovr +# - Minor cleanup (lowercase function names, update examples...) +# +# 2019-12-19, FeRD (Frank Dana) +# - Rename Lcov outputs, make filtered file canonical, fix cleanup for targets +# +# 2020-01-19, Bob Apthorpe +# - Added gfortran support +# +# 2020-02-17, FeRD (Frank Dana) +# - Make all add_custom_target()s VERBATIM to auto-escape wildcard characters +# in EXCLUDEs, and remove manual escaping from gcovr targets +# +# 2021-01-19, Robin Mueller +# - Add CODE_COVERAGE_VERBOSE option which will allow to print out commands which are run +# - Added the option for users to set the GCOVR_ADDITIONAL_ARGS variable to supply additional +# flags to the gcovr command +# +# 2020-05-04, Mihchael Davis +# - Add -fprofile-abs-path to make gcno files contain absolute paths +# - Fix BASE_DIRECTORY not working when defined +# - Change BYPRODUCT from folder to index.html to stop ninja from complaining about double defines +# +# 2021-05-10, Martin Stump +# - Check if the generator is multi-config before warning about non-Debug builds +# +# 2022-02-22, Marko Wehle +# - Change gcovr output from -o for --xml and --html output respectively. +# This will allow for Multiple Output Formats at the same time by making use of GCOVR_ADDITIONAL_ARGS, e.g. GCOVR_ADDITIONAL_ARGS "--txt". +# +# 2022-09-28, Sebastian Mueller +# - fix append_coverage_compiler_flags_to_target to correctly add flags +# - replace "-fprofile-arcs -ftest-coverage" with "--coverage" (equivalent) +# +# USAGE: +# +# 1. Copy this file into your cmake modules path. +# +# 2. Add the following line to your CMakeLists.txt (best inside an if-condition +# using a CMake option() to enable it just optionally): +# include(CodeCoverage) +# +# 3. Append necessary compiler flags for all supported source files: +# append_coverage_compiler_flags() +# Or for specific target: +# append_coverage_compiler_flags_to_target(YOUR_TARGET_NAME) +# +# 3.a (OPTIONAL) Set appropriate optimization flags, e.g. -O0, -O1 or -Og +# +# 4. If you need to exclude additional directories from the report, specify them +# using full paths in the COVERAGE_EXCLUDES variable before calling +# setup_target_for_coverage_*(). +# Example: +# set(COVERAGE_EXCLUDES +# '${PROJECT_SOURCE_DIR}/src/dir1/*' +# '/path/to/my/src/dir2/*') +# Or, use the EXCLUDE argument to setup_target_for_coverage_*(). +# Example: +# setup_target_for_coverage_lcov( +# NAME coverage +# EXECUTABLE testrunner +# EXCLUDE "${PROJECT_SOURCE_DIR}/src/dir1/*" "/path/to/my/src/dir2/*") +# +# 4.a NOTE: With CMake 3.4+, COVERAGE_EXCLUDES or EXCLUDE can also be set +# relative to the BASE_DIRECTORY (default: PROJECT_SOURCE_DIR) +# Example: +# set(COVERAGE_EXCLUDES "dir1/*") +# setup_target_for_coverage_gcovr_html( +# NAME coverage +# EXECUTABLE testrunner +# BASE_DIRECTORY "${PROJECT_SOURCE_DIR}/src" +# EXCLUDE "dir2/*") +# +# 5. Use the functions described below to create a custom make target which +# runs your test executable and produces a code coverage report. +# +# 6. Build a Debug build: +# cmake -DCMAKE_BUILD_TYPE=Debug .. +# make +# make my_coverage_target +# + +include(CMakeParseArguments) + +option(CODE_COVERAGE_VERBOSE "Verbose information" FALSE) + +# Check prereqs +find_program( GCOV_PATH gcov ) +find_program( LCOV_PATH NAMES lcov lcov.bat lcov.exe lcov.perl) +find_program( FASTCOV_PATH NAMES fastcov fastcov.py ) +find_program( GENHTML_PATH NAMES genhtml genhtml.perl genhtml.bat ) +find_program( GCOVR_PATH gcovr PATHS ${CMAKE_SOURCE_DIR}/scripts/test) +find_program( CPPFILT_PATH NAMES c++filt ) + +if(NOT GCOV_PATH) + message(FATAL_ERROR "gcov not found! Aborting...") +endif() # NOT GCOV_PATH + +# Check supported compiler (Clang, GNU and Flang) +get_property(LANGUAGES GLOBAL PROPERTY ENABLED_LANGUAGES) +foreach(LANG ${LANGUAGES}) + if("${CMAKE_${LANG}_COMPILER_ID}" MATCHES "(Apple)?[Cc]lang") + if("${CMAKE_${LANG}_COMPILER_VERSION}" VERSION_LESS 3) + message(FATAL_ERROR "Clang version must be 3.0.0 or greater! Aborting...") + endif() + elseif(NOT "${CMAKE_${LANG}_COMPILER_ID}" MATCHES "GNU" + AND NOT "${CMAKE_${LANG}_COMPILER_ID}" MATCHES "(LLVM)?[Ff]lang") + message(FATAL_ERROR "Compiler is not GNU or Flang! Aborting...") + endif() +endforeach() + +set(COVERAGE_COMPILER_FLAGS "-g --coverage" + CACHE INTERNAL "") +if(CMAKE_CXX_COMPILER_ID MATCHES "(GNU|Clang)") + include(CheckCXXCompilerFlag) + check_cxx_compiler_flag(-fprofile-abs-path HAVE_fprofile_abs_path) + if(HAVE_fprofile_abs_path) + set(COVERAGE_COMPILER_FLAGS "${COVERAGE_COMPILER_FLAGS} -fprofile-abs-path") + endif() +endif() + +set(CMAKE_Fortran_FLAGS_COVERAGE + ${COVERAGE_COMPILER_FLAGS} + CACHE STRING "Flags used by the Fortran compiler during coverage builds." + FORCE ) +set(CMAKE_CXX_FLAGS_COVERAGE + ${COVERAGE_COMPILER_FLAGS} + CACHE STRING "Flags used by the C++ compiler during coverage builds." + FORCE ) +set(CMAKE_C_FLAGS_COVERAGE + ${COVERAGE_COMPILER_FLAGS} + CACHE STRING "Flags used by the C compiler during coverage builds." + FORCE ) +set(CMAKE_EXE_LINKER_FLAGS_COVERAGE + "" + CACHE STRING "Flags used for linking binaries during coverage builds." + FORCE ) +set(CMAKE_SHARED_LINKER_FLAGS_COVERAGE + "" + CACHE STRING "Flags used by the shared libraries linker during coverage builds." + FORCE ) +mark_as_advanced( + CMAKE_Fortran_FLAGS_COVERAGE + CMAKE_CXX_FLAGS_COVERAGE + CMAKE_C_FLAGS_COVERAGE + CMAKE_EXE_LINKER_FLAGS_COVERAGE + CMAKE_SHARED_LINKER_FLAGS_COVERAGE ) + +get_property(GENERATOR_IS_MULTI_CONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(NOT (CMAKE_BUILD_TYPE STREQUAL "Debug" OR GENERATOR_IS_MULTI_CONFIG)) + message(WARNING "Code coverage results with an optimised (non-Debug) build may be misleading") +endif() # NOT (CMAKE_BUILD_TYPE STREQUAL "Debug" OR GENERATOR_IS_MULTI_CONFIG) + +if(CMAKE_C_COMPILER_ID STREQUAL "GNU" OR CMAKE_Fortran_COMPILER_ID STREQUAL "GNU") + link_libraries(gcov) +endif() + +# Defines a target for running and collection code coverage information +# Builds dependencies, runs the given executable and outputs reports. +# NOTE! The executable should always have a ZERO as exit code otherwise +# the coverage generation will not complete. +# +# setup_target_for_coverage_lcov( +# NAME testrunner_coverage # New target name +# EXECUTABLE testrunner -j ${PROCESSOR_COUNT} # Executable in PROJECT_BINARY_DIR +# DEPENDENCIES testrunner # Dependencies to build first +# BASE_DIRECTORY "../" # Base directory for report +# # (defaults to PROJECT_SOURCE_DIR) +# EXCLUDE "src/dir1/*" "src/dir2/*" # Patterns to exclude (can be relative +# # to BASE_DIRECTORY, with CMake 3.4+) +# NO_DEMANGLE # Don't demangle C++ symbols +# # even if c++filt is found +# ) +function(setup_target_for_coverage_lcov) + + set(options NO_DEMANGLE SONARQUBE) + set(oneValueArgs BASE_DIRECTORY NAME) + set(multiValueArgs EXCLUDE EXECUTABLE EXECUTABLE_ARGS DEPENDENCIES LCOV_ARGS GENHTML_ARGS) + cmake_parse_arguments(Coverage "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + if(NOT LCOV_PATH) + message(FATAL_ERROR "lcov not found! Aborting...") + endif() # NOT LCOV_PATH + + if(NOT GENHTML_PATH) + message(FATAL_ERROR "genhtml not found! Aborting...") + endif() # NOT GENHTML_PATH + + # Set base directory (as absolute path), or default to PROJECT_SOURCE_DIR + if(DEFINED Coverage_BASE_DIRECTORY) + get_filename_component(BASEDIR ${Coverage_BASE_DIRECTORY} ABSOLUTE) + else() + set(BASEDIR ${PROJECT_SOURCE_DIR}) + endif() + + # Collect excludes (CMake 3.4+: Also compute absolute paths) + set(LCOV_EXCLUDES "") + foreach(EXCLUDE ${Coverage_EXCLUDE} ${COVERAGE_EXCLUDES} ${COVERAGE_LCOV_EXCLUDES}) + if(CMAKE_VERSION VERSION_GREATER 3.4) + get_filename_component(EXCLUDE ${EXCLUDE} ABSOLUTE BASE_DIR ${BASEDIR}) + endif() + list(APPEND LCOV_EXCLUDES "${EXCLUDE}") + endforeach() + list(REMOVE_DUPLICATES LCOV_EXCLUDES) + + # Conditional arguments + if(CPPFILT_PATH AND NOT ${Coverage_NO_DEMANGLE}) + set(GENHTML_EXTRA_ARGS "--demangle-cpp") + endif() + + # Setting up commands which will be run to generate coverage data. + # Cleanup lcov + set(LCOV_CLEAN_CMD + ${LCOV_PATH} ${Coverage_LCOV_ARGS} --gcov-tool ${GCOV_PATH} -directory . + -b ${BASEDIR} --zerocounters + ) + # Create baseline to make sure untouched files show up in the report + set(LCOV_BASELINE_CMD + ${LCOV_PATH} ${Coverage_LCOV_ARGS} --gcov-tool ${GCOV_PATH} -c -i -d . -b + ${BASEDIR} -o ${Coverage_NAME}.base + ) + # Run tests + set(LCOV_EXEC_TESTS_CMD + ${Coverage_EXECUTABLE} ${Coverage_EXECUTABLE_ARGS} + ) + # Capturing lcov counters and generating report + set(LCOV_CAPTURE_CMD + ${LCOV_PATH} ${Coverage_LCOV_ARGS} --gcov-tool ${GCOV_PATH} --directory . -b + ${BASEDIR} --capture --output-file ${Coverage_NAME}.capture + ) + # add baseline counters + set(LCOV_BASELINE_COUNT_CMD + ${LCOV_PATH} ${Coverage_LCOV_ARGS} --gcov-tool ${GCOV_PATH} -a ${Coverage_NAME}.base + -a ${Coverage_NAME}.capture --output-file ${Coverage_NAME}.total + ) + # filter collected data to final coverage report + set(LCOV_FILTER_CMD + ${LCOV_PATH} ${Coverage_LCOV_ARGS} --gcov-tool ${GCOV_PATH} --remove + ${Coverage_NAME}.total ${LCOV_EXCLUDES} --output-file ${Coverage_NAME}.info + ) + # Generate HTML output + set(LCOV_GEN_HTML_CMD + ${GENHTML_PATH} ${GENHTML_EXTRA_ARGS} ${Coverage_GENHTML_ARGS} -o + ${Coverage_NAME} ${Coverage_NAME}.info + ) + if(${Coverage_SONARQUBE}) + # Generate SonarQube output + set(GCOVR_XML_CMD + ${GCOVR_PATH} --sonarqube ${Coverage_NAME}_sonarqube.xml -r ${BASEDIR} ${GCOVR_ADDITIONAL_ARGS} + ${GCOVR_EXCLUDE_ARGS} --object-directory=${PROJECT_BINARY_DIR} + ) + set(GCOVR_XML_CMD_COMMAND + COMMAND ${GCOVR_XML_CMD} + ) + set(GCOVR_XML_CMD_BYPRODUCTS ${Coverage_NAME}_sonarqube.xml) + set(GCOVR_XML_CMD_COMMENT COMMENT "SonarQube code coverage info report saved in ${Coverage_NAME}_sonarqube.xml.") + endif() + + + if(CODE_COVERAGE_VERBOSE) + message(STATUS "Executed command report") + message(STATUS "Command to clean up lcov: ") + string(REPLACE ";" " " LCOV_CLEAN_CMD_SPACED "${LCOV_CLEAN_CMD}") + message(STATUS "${LCOV_CLEAN_CMD_SPACED}") + + message(STATUS "Command to create baseline: ") + string(REPLACE ";" " " LCOV_BASELINE_CMD_SPACED "${LCOV_BASELINE_CMD}") + message(STATUS "${LCOV_BASELINE_CMD_SPACED}") + + message(STATUS "Command to run the tests: ") + string(REPLACE ";" " " LCOV_EXEC_TESTS_CMD_SPACED "${LCOV_EXEC_TESTS_CMD}") + message(STATUS "${LCOV_EXEC_TESTS_CMD_SPACED}") + + message(STATUS "Command to capture counters and generate report: ") + string(REPLACE ";" " " LCOV_CAPTURE_CMD_SPACED "${LCOV_CAPTURE_CMD}") + message(STATUS "${LCOV_CAPTURE_CMD_SPACED}") + + message(STATUS "Command to add baseline counters: ") + string(REPLACE ";" " " LCOV_BASELINE_COUNT_CMD_SPACED "${LCOV_BASELINE_COUNT_CMD}") + message(STATUS "${LCOV_BASELINE_COUNT_CMD_SPACED}") + + message(STATUS "Command to filter collected data: ") + string(REPLACE ";" " " LCOV_FILTER_CMD_SPACED "${LCOV_FILTER_CMD}") + message(STATUS "${LCOV_FILTER_CMD_SPACED}") + + message(STATUS "Command to generate lcov HTML output: ") + string(REPLACE ";" " " LCOV_GEN_HTML_CMD_SPACED "${LCOV_GEN_HTML_CMD}") + message(STATUS "${LCOV_GEN_HTML_CMD_SPACED}") + + if(${Coverage_SONARQUBE}) + message(STATUS "Command to generate SonarQube XML output: ") + string(REPLACE ";" " " GCOVR_XML_CMD_SPACED "${GCOVR_XML_CMD}") + message(STATUS "${GCOVR_XML_CMD_SPACED}") + endif() + endif() + + # Setup target + add_custom_target(${Coverage_NAME} + COMMAND ${LCOV_CLEAN_CMD} + COMMAND ${LCOV_BASELINE_CMD} + COMMAND ${LCOV_EXEC_TESTS_CMD} + COMMAND ${LCOV_CAPTURE_CMD} + COMMAND ${LCOV_BASELINE_COUNT_CMD} + COMMAND ${LCOV_FILTER_CMD} + COMMAND ${LCOV_GEN_HTML_CMD} + ${GCOVR_XML_CMD_COMMAND} + + # Set output files as GENERATED (will be removed on 'make clean') + BYPRODUCTS + ${Coverage_NAME}.base + ${Coverage_NAME}.capture + ${Coverage_NAME}.total + ${Coverage_NAME}.info + ${GCOVR_XML_CMD_BYPRODUCTS} + ${Coverage_NAME}/index.html + WORKING_DIRECTORY ${PROJECT_BINARY_DIR} + DEPENDS ${Coverage_DEPENDENCIES} + VERBATIM # Protect arguments to commands + COMMENT "Resetting code coverage counters to zero.\nProcessing code coverage counters and generating report." + ) + + # Show where to find the lcov info report + add_custom_command(TARGET ${Coverage_NAME} POST_BUILD + COMMAND ; + COMMENT "Lcov code coverage info report saved in ${Coverage_NAME}.info." + ${GCOVR_XML_CMD_COMMENT} + ) + + # Show info where to find the report + add_custom_command(TARGET ${Coverage_NAME} POST_BUILD + COMMAND ; + COMMENT "Open ./${Coverage_NAME}/index.html in your browser to view the coverage report." + ) + +endfunction() # setup_target_for_coverage_lcov + +# Defines a target for running and collection code coverage information +# Builds dependencies, runs the given executable and outputs reports. +# NOTE! The executable should always have a ZERO as exit code otherwise +# the coverage generation will not complete. +# +# setup_target_for_coverage_gcovr_xml( +# NAME ctest_coverage # New target name +# EXECUTABLE ctest -j ${PROCESSOR_COUNT} # Executable in PROJECT_BINARY_DIR +# DEPENDENCIES executable_target # Dependencies to build first +# BASE_DIRECTORY "../" # Base directory for report +# # (defaults to PROJECT_SOURCE_DIR) +# EXCLUDE "src/dir1/*" "src/dir2/*" # Patterns to exclude (can be relative +# # to BASE_DIRECTORY, with CMake 3.4+) +# ) +# The user can set the variable GCOVR_ADDITIONAL_ARGS to supply additional flags to the +# GCVOR command. +function(setup_target_for_coverage_gcovr_xml) + + set(options NONE) + set(oneValueArgs BASE_DIRECTORY NAME) + set(multiValueArgs EXCLUDE EXECUTABLE EXECUTABLE_ARGS DEPENDENCIES) + cmake_parse_arguments(Coverage "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + if(NOT GCOVR_PATH) + message(FATAL_ERROR "gcovr not found! Aborting...") + endif() # NOT GCOVR_PATH + + # Set base directory (as absolute path), or default to PROJECT_SOURCE_DIR + if(DEFINED Coverage_BASE_DIRECTORY) + get_filename_component(BASEDIR ${Coverage_BASE_DIRECTORY} ABSOLUTE) + else() + set(BASEDIR ${PROJECT_SOURCE_DIR}) + endif() + + # Collect excludes (CMake 3.4+: Also compute absolute paths) + set(GCOVR_EXCLUDES "") + foreach(EXCLUDE ${Coverage_EXCLUDE} ${COVERAGE_EXCLUDES} ${COVERAGE_GCOVR_EXCLUDES}) + if(CMAKE_VERSION VERSION_GREATER 3.4) + get_filename_component(EXCLUDE ${EXCLUDE} ABSOLUTE BASE_DIR ${BASEDIR}) + endif() + list(APPEND GCOVR_EXCLUDES "${EXCLUDE}") + endforeach() + list(REMOVE_DUPLICATES GCOVR_EXCLUDES) + + # Combine excludes to several -e arguments + set(GCOVR_EXCLUDE_ARGS "") + foreach(EXCLUDE ${GCOVR_EXCLUDES}) + list(APPEND GCOVR_EXCLUDE_ARGS "-e") + list(APPEND GCOVR_EXCLUDE_ARGS "${EXCLUDE}") + endforeach() + + # Set up commands which will be run to generate coverage data + # Run tests + set(GCOVR_XML_EXEC_TESTS_CMD + ${Coverage_EXECUTABLE} ${Coverage_EXECUTABLE_ARGS} + ) + # Running gcovr + set(GCOVR_XML_CMD + ${GCOVR_PATH} --xml ${Coverage_NAME}.xml -r ${BASEDIR} ${GCOVR_ADDITIONAL_ARGS} + ${GCOVR_EXCLUDE_ARGS} --object-directory=${PROJECT_BINARY_DIR} + ) + + if(CODE_COVERAGE_VERBOSE) + message(STATUS "Executed command report") + + message(STATUS "Command to run tests: ") + string(REPLACE ";" " " GCOVR_XML_EXEC_TESTS_CMD_SPACED "${GCOVR_XML_EXEC_TESTS_CMD}") + message(STATUS "${GCOVR_XML_EXEC_TESTS_CMD_SPACED}") + + message(STATUS "Command to generate gcovr XML coverage data: ") + string(REPLACE ";" " " GCOVR_XML_CMD_SPACED "${GCOVR_XML_CMD}") + message(STATUS "${GCOVR_XML_CMD_SPACED}") + endif() + + add_custom_target(${Coverage_NAME} + COMMAND ${GCOVR_XML_EXEC_TESTS_CMD} + COMMAND ${GCOVR_XML_CMD} + + BYPRODUCTS ${Coverage_NAME}.xml + WORKING_DIRECTORY ${PROJECT_BINARY_DIR} + DEPENDS ${Coverage_DEPENDENCIES} + VERBATIM # Protect arguments to commands + COMMENT "Running gcovr to produce Cobertura code coverage report." + ) + + # Show info where to find the report + add_custom_command(TARGET ${Coverage_NAME} POST_BUILD + COMMAND ; + COMMENT "Cobertura code coverage report saved in ${Coverage_NAME}.xml." + ) +endfunction() # setup_target_for_coverage_gcovr_xml + +# Defines a target for running and collection code coverage information +# Builds dependencies, runs the given executable and outputs reports. +# NOTE! The executable should always have a ZERO as exit code otherwise +# the coverage generation will not complete. +# +# setup_target_for_coverage_gcovr_html( +# NAME ctest_coverage # New target name +# EXECUTABLE ctest -j ${PROCESSOR_COUNT} # Executable in PROJECT_BINARY_DIR +# DEPENDENCIES executable_target # Dependencies to build first +# BASE_DIRECTORY "../" # Base directory for report +# # (defaults to PROJECT_SOURCE_DIR) +# EXCLUDE "src/dir1/*" "src/dir2/*" # Patterns to exclude (can be relative +# # to BASE_DIRECTORY, with CMake 3.4+) +# ) +# The user can set the variable GCOVR_ADDITIONAL_ARGS to supply additional flags to the +# GCVOR command. +function(setup_target_for_coverage_gcovr_html) + + set(options NONE) + set(oneValueArgs BASE_DIRECTORY NAME) + set(multiValueArgs EXCLUDE EXECUTABLE EXECUTABLE_ARGS DEPENDENCIES) + cmake_parse_arguments(Coverage "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + if(NOT GCOVR_PATH) + message(FATAL_ERROR "gcovr not found! Aborting...") + endif() # NOT GCOVR_PATH + + # Set base directory (as absolute path), or default to PROJECT_SOURCE_DIR + if(DEFINED Coverage_BASE_DIRECTORY) + get_filename_component(BASEDIR ${Coverage_BASE_DIRECTORY} ABSOLUTE) + else() + set(BASEDIR ${PROJECT_SOURCE_DIR}) + endif() + + # Collect excludes (CMake 3.4+: Also compute absolute paths) + set(GCOVR_EXCLUDES "") + foreach(EXCLUDE ${Coverage_EXCLUDE} ${COVERAGE_EXCLUDES} ${COVERAGE_GCOVR_EXCLUDES}) + if(CMAKE_VERSION VERSION_GREATER 3.4) + get_filename_component(EXCLUDE ${EXCLUDE} ABSOLUTE BASE_DIR ${BASEDIR}) + endif() + list(APPEND GCOVR_EXCLUDES "${EXCLUDE}") + endforeach() + list(REMOVE_DUPLICATES GCOVR_EXCLUDES) + + # Combine excludes to several -e arguments + set(GCOVR_EXCLUDE_ARGS "") + foreach(EXCLUDE ${GCOVR_EXCLUDES}) + list(APPEND GCOVR_EXCLUDE_ARGS "-e") + list(APPEND GCOVR_EXCLUDE_ARGS "${EXCLUDE}") + endforeach() + + # Set up commands which will be run to generate coverage data + # Run tests + set(GCOVR_HTML_EXEC_TESTS_CMD + ${Coverage_EXECUTABLE} ${Coverage_EXECUTABLE_ARGS} + ) + # Create folder + set(GCOVR_HTML_FOLDER_CMD + ${CMAKE_COMMAND} -E make_directory ${PROJECT_BINARY_DIR}/${Coverage_NAME} + ) + # Running gcovr + set(GCOVR_HTML_CMD + ${GCOVR_PATH} --html ${Coverage_NAME}/index.html --html-details -r ${BASEDIR} ${GCOVR_ADDITIONAL_ARGS} + ${GCOVR_EXCLUDE_ARGS} --object-directory=${PROJECT_BINARY_DIR} + ) + + if(CODE_COVERAGE_VERBOSE) + message(STATUS "Executed command report") + + message(STATUS "Command to run tests: ") + string(REPLACE ";" " " GCOVR_HTML_EXEC_TESTS_CMD_SPACED "${GCOVR_HTML_EXEC_TESTS_CMD}") + message(STATUS "${GCOVR_HTML_EXEC_TESTS_CMD_SPACED}") + + message(STATUS "Command to create a folder: ") + string(REPLACE ";" " " GCOVR_HTML_FOLDER_CMD_SPACED "${GCOVR_HTML_FOLDER_CMD}") + message(STATUS "${GCOVR_HTML_FOLDER_CMD_SPACED}") + + message(STATUS "Command to generate gcovr HTML coverage data: ") + string(REPLACE ";" " " GCOVR_HTML_CMD_SPACED "${GCOVR_HTML_CMD}") + message(STATUS "${GCOVR_HTML_CMD_SPACED}") + endif() + + add_custom_target(${Coverage_NAME} + COMMAND ${GCOVR_HTML_EXEC_TESTS_CMD} + COMMAND ${GCOVR_HTML_FOLDER_CMD} + COMMAND ${GCOVR_HTML_CMD} + + BYPRODUCTS ${PROJECT_BINARY_DIR}/${Coverage_NAME}/index.html # report directory + WORKING_DIRECTORY ${PROJECT_BINARY_DIR} + DEPENDS ${Coverage_DEPENDENCIES} + VERBATIM # Protect arguments to commands + COMMENT "Running gcovr to produce HTML code coverage report." + ) + + # Show info where to find the report + add_custom_command(TARGET ${Coverage_NAME} POST_BUILD + COMMAND ; + COMMENT "Open ./${Coverage_NAME}/index.html in your browser to view the coverage report." + ) + +endfunction() # setup_target_for_coverage_gcovr_html + +# Defines a target for running and collection code coverage information +# Builds dependencies, runs the given executable and outputs reports. +# NOTE! The executable should always have a ZERO as exit code otherwise +# the coverage generation will not complete. +# +# setup_target_for_coverage_fastcov( +# NAME testrunner_coverage # New target name +# EXECUTABLE testrunner -j ${PROCESSOR_COUNT} # Executable in PROJECT_BINARY_DIR +# DEPENDENCIES testrunner # Dependencies to build first +# BASE_DIRECTORY "../" # Base directory for report +# # (defaults to PROJECT_SOURCE_DIR) +# EXCLUDE "src/dir1/" "src/dir2/" # Patterns to exclude. +# NO_DEMANGLE # Don't demangle C++ symbols +# # even if c++filt is found +# SKIP_HTML # Don't create html report +# POST_CMD perl -i -pe s!${PROJECT_SOURCE_DIR}/!!g ctest_coverage.json # E.g. for stripping source dir from file paths +# ) +function(setup_target_for_coverage_fastcov) + + set(options NO_DEMANGLE SKIP_HTML) + set(oneValueArgs BASE_DIRECTORY NAME) + set(multiValueArgs EXCLUDE EXECUTABLE EXECUTABLE_ARGS DEPENDENCIES FASTCOV_ARGS GENHTML_ARGS POST_CMD) + cmake_parse_arguments(Coverage "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + if(NOT FASTCOV_PATH) + message(FATAL_ERROR "fastcov not found! Aborting...") + endif() + + if(NOT Coverage_SKIP_HTML AND NOT GENHTML_PATH) + message(FATAL_ERROR "genhtml not found! Aborting...") + endif() + + # Set base directory (as absolute path), or default to PROJECT_SOURCE_DIR + if(Coverage_BASE_DIRECTORY) + get_filename_component(BASEDIR ${Coverage_BASE_DIRECTORY} ABSOLUTE) + else() + set(BASEDIR ${PROJECT_SOURCE_DIR}) + endif() + + # Collect excludes (Patterns, not paths, for fastcov) + set(FASTCOV_EXCLUDES "") + foreach(EXCLUDE ${Coverage_EXCLUDE} ${COVERAGE_EXCLUDES} ${COVERAGE_FASTCOV_EXCLUDES}) + list(APPEND FASTCOV_EXCLUDES "${EXCLUDE}") + endforeach() + list(REMOVE_DUPLICATES FASTCOV_EXCLUDES) + + # Conditional arguments + if(CPPFILT_PATH AND NOT ${Coverage_NO_DEMANGLE}) + set(GENHTML_EXTRA_ARGS "--demangle-cpp") + endif() + + # Set up commands which will be run to generate coverage data + set(FASTCOV_EXEC_TESTS_CMD ${Coverage_EXECUTABLE} ${Coverage_EXECUTABLE_ARGS}) + + set(FASTCOV_CAPTURE_CMD ${FASTCOV_PATH} ${Coverage_FASTCOV_ARGS} --gcov ${GCOV_PATH} + --search-directory ${BASEDIR} + --process-gcno + --output ${Coverage_NAME}.json + --exclude ${FASTCOV_EXCLUDES} + ) + + set(FASTCOV_CONVERT_CMD ${FASTCOV_PATH} + -C ${Coverage_NAME}.json --lcov --output ${Coverage_NAME}.info + ) + + if(Coverage_SKIP_HTML) + set(FASTCOV_HTML_CMD ";") + else() + set(FASTCOV_HTML_CMD ${GENHTML_PATH} ${GENHTML_EXTRA_ARGS} ${Coverage_GENHTML_ARGS} + -o ${Coverage_NAME} ${Coverage_NAME}.info + ) + endif() + + set(FASTCOV_POST_CMD ";") + if(Coverage_POST_CMD) + set(FASTCOV_POST_CMD ${Coverage_POST_CMD}) + endif() + + if(CODE_COVERAGE_VERBOSE) + message(STATUS "Code coverage commands for target ${Coverage_NAME} (fastcov):") + + message(" Running tests:") + string(REPLACE ";" " " FASTCOV_EXEC_TESTS_CMD_SPACED "${FASTCOV_EXEC_TESTS_CMD}") + message(" ${FASTCOV_EXEC_TESTS_CMD_SPACED}") + + message(" Capturing fastcov counters and generating report:") + string(REPLACE ";" " " FASTCOV_CAPTURE_CMD_SPACED "${FASTCOV_CAPTURE_CMD}") + message(" ${FASTCOV_CAPTURE_CMD_SPACED}") + + message(" Converting fastcov .json to lcov .info:") + string(REPLACE ";" " " FASTCOV_CONVERT_CMD_SPACED "${FASTCOV_CONVERT_CMD}") + message(" ${FASTCOV_CONVERT_CMD_SPACED}") + + if(NOT Coverage_SKIP_HTML) + message(" Generating HTML report: ") + string(REPLACE ";" " " FASTCOV_HTML_CMD_SPACED "${FASTCOV_HTML_CMD}") + message(" ${FASTCOV_HTML_CMD_SPACED}") + endif() + if(Coverage_POST_CMD) + message(" Running post command: ") + string(REPLACE ";" " " FASTCOV_POST_CMD_SPACED "${FASTCOV_POST_CMD}") + message(" ${FASTCOV_POST_CMD_SPACED}") + endif() + endif() + + # Setup target + add_custom_target(${Coverage_NAME} + + # Cleanup fastcov + COMMAND ${FASTCOV_PATH} ${Coverage_FASTCOV_ARGS} --gcov ${GCOV_PATH} + --search-directory ${BASEDIR} + --zerocounters + + COMMAND ${FASTCOV_EXEC_TESTS_CMD} + COMMAND ${FASTCOV_CAPTURE_CMD} + COMMAND ${FASTCOV_CONVERT_CMD} + COMMAND ${FASTCOV_HTML_CMD} + COMMAND ${FASTCOV_POST_CMD} + + # Set output files as GENERATED (will be removed on 'make clean') + BYPRODUCTS + ${Coverage_NAME}.info + ${Coverage_NAME}.json + ${Coverage_NAME}/index.html # report directory + + WORKING_DIRECTORY ${PROJECT_BINARY_DIR} + DEPENDS ${Coverage_DEPENDENCIES} + VERBATIM # Protect arguments to commands + COMMENT "Resetting code coverage counters to zero. Processing code coverage counters and generating report." + ) + + set(INFO_MSG "fastcov code coverage info report saved in ${Coverage_NAME}.info and ${Coverage_NAME}.json.") + if(NOT Coverage_SKIP_HTML) + string(APPEND INFO_MSG " Open ${PROJECT_BINARY_DIR}/${Coverage_NAME}/index.html in your browser to view the coverage report.") + endif() + # Show where to find the fastcov info report + add_custom_command(TARGET ${Coverage_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E echo ${INFO_MSG} + ) + +endfunction() # setup_target_for_coverage_fastcov + +function(append_coverage_compiler_flags) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${COVERAGE_COMPILER_FLAGS}" PARENT_SCOPE) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${COVERAGE_COMPILER_FLAGS}" PARENT_SCOPE) + set(CMAKE_Fortran_FLAGS "${CMAKE_Fortran_FLAGS} ${COVERAGE_COMPILER_FLAGS}" PARENT_SCOPE) + message(STATUS "Appending code coverage compiler flags: ${COVERAGE_COMPILER_FLAGS}") +endfunction() # append_coverage_compiler_flags + +# Setup coverage for specific library +function(append_coverage_compiler_flags_to_target name) + separate_arguments(_flag_list NATIVE_COMMAND "${COVERAGE_COMPILER_FLAGS}") + target_compile_options(${name} PRIVATE ${_flag_list}) + if(CMAKE_C_COMPILER_ID STREQUAL "GNU" OR CMAKE_Fortran_COMPILER_ID STREQUAL "GNU") + target_link_libraries(${name} PRIVATE gcov) + endif() +endfunction() diff --git a/config/CMakeLists.txt b/config/CMakeLists.txt new file mode 100644 index 0000000..c6c4cde --- /dev/null +++ b/config/CMakeLists.txt @@ -0,0 +1,4 @@ +configure_file( + "config.h.in" + "${CMAKE_BINARY_DIR}/configured_files/include/config.h" ESCAPE_QUOTES +) diff --git a/config/config.h.in b/config/config.h.in new file mode 100644 index 0000000..d95e710 --- /dev/null +++ b/config/config.h.in @@ -0,0 +1,13 @@ +#pragma once + +#include +#include + +#define PROJECT_VERSION_MAJOR @PROJECT_VERSION_MAJOR @ +#define PROJECT_VERSION_MINOR @PROJECT_VERSION_MINOR @ +#define PROJECT_VERSION_PATCH @PROJECT_VERSION_PATCH @ + +static constexpr std::string_view project_name = " @PROJECT_NAME@ "; +static constexpr std::string_view project_version = "@PROJECT_VERSION@"; +static constexpr std::string_view project_description = "@PROJECT_DESCRIPTION@"; +static constexpr std::string_view git_sha = "@GIT_SHA@"; diff --git a/gcovr.cfg b/gcovr.cfg new file mode 100644 index 0000000..b486edf --- /dev/null +++ b/gcovr.cfg @@ -0,0 +1,5 @@ +filter = src/ +exclude = external/ +exclude = tests/ +print-summary = yes +sort-percentage = yes diff --git a/sample/CMakeLists.txt b/sample/CMakeLists.txt index d9f6886..62533c7 100644 --- a/sample/CMakeLists.txt +++ b/sample/CMakeLists.txt @@ -1,6 +1,4 @@ include_directories(${BayesNet_SOURCE_DIR}/src) link_directories(${MyProject_SOURCE_DIR}/src) add_executable(main main.cc ArffFiles.cc CPPFImdlp.cpp Metrics.cpp) -add_executable(test test.cc) -target_link_libraries(main BayesNet "${TORCH_LIBRARIES}") -target_link_libraries(test "${TORCH_LIBRARIES}") \ No newline at end of file +target_link_libraries(main BayesNet "${TORCH_LIBRARIES}") \ No newline at end of file diff --git a/sample/main.cc b/sample/main.cc index 826b272..4388fbf 100644 --- a/sample/main.cc +++ b/sample/main.cc @@ -30,23 +30,23 @@ void usage(const char* path) << " -f, --file[=FILENAME]\t {diabetes, glass, iris, kdd_JapaneseVowels, letter, liver-disorders, mfeat-factors}." << endl; cout << " -p, --path[=FILENAME]\t folder where the data files are located, default " << PATH << endl; - cout << " -n, --net=[FILENAME]\t default=file parameter value" << endl; + cout << " -m, --model={AODE, KDB, SPODE, TAN}\t " << endl; } tuple parse_arguments(int argc, char** argv) { string file_name; - string network_name; + string model_name; string path = PATH; const vector long_options = { {"help", no_argument, nullptr, 'h'}, {"file", required_argument, nullptr, 'f'}, {"path", required_argument, nullptr, 'p'}, - {"net", required_argument, nullptr, 'n'}, + {"model", required_argument, nullptr, 'm'}, {nullptr, no_argument, nullptr, 0} }; while (true) { - const auto c = getopt_long(argc, argv, "hf:p:n:", long_options.data(), nullptr); + const auto c = getopt_long(argc, argv, "hf:p:m:", long_options.data(), nullptr); if (c == -1) break; switch (c) { @@ -56,8 +56,8 @@ tuple parse_arguments(int argc, char** argv) case 'f': file_name = string(optarg); break; - case 'n': - network_name = string(optarg); + case 'm': + model_name = string(optarg); break; case 'p': path = optarg; @@ -75,12 +75,22 @@ tuple parse_arguments(int argc, char** argv) usage(argv[0]); exit(1); } - if (network_name.empty()) { - network_name = file_name; - } - return make_tuple(file_name, path, network_name); + return make_tuple(file_name, path, model_name); } +inline constexpr auto hash_conv(const std::string_view sv) +{ + unsigned long hash{ 5381 }; + for (unsigned char c : sv) { + hash = ((hash << 5) + hash) ^ c; + } + return hash; +} + +inline constexpr auto operator"" _sh(const char* str, size_t len) +{ + return hash_conv(std::string_view{ str, len }); +} pair, map> discretize(vector& X, mdlp::labels_t& y, vector features) { @@ -96,39 +106,6 @@ pair, map> discretize(vectorgetName() << " States -> " << node.second->getNumStates() << endl; - cout << "-Parents:"; - for (auto parent : node.second->getParents()) { - cout << " " << parent->getName(); - } - cout << endl; - cout << "-Children:"; - for (auto child : node.second->getChildren()) { - cout << " " << child->getName(); - } - cout << endl; - } -} -void showCPDS(bayesnet::Network& network) -{ - cout << "CPDs:" << endl; - auto& nodes = network.getNodes(); - for (auto it = nodes.begin(); it != nodes.end(); it++) { - cout << "* Name: " << it->first << " " << it->second->getName() << " -> " << it->second->getNumStates() << endl; - cout << "Parents: "; - for (auto parent : it->second->getParents()) { - cout << parent->getName() << " -> " << parent->getNumStates() << ", "; - } - cout << endl; - auto cpd = it->second->getCPT(); - cout << cpd << endl; - } -} bool file_exists(const std::string& name) { @@ -140,7 +117,7 @@ bool file_exists(const std::string& name) } } -pair get_options(int argc, char** argv) +tuple get_options(int argc, char** argv) { map datasets = { {"diabetes", true}, @@ -152,58 +129,35 @@ pair get_options(int argc, char** argv) {"liver-disorders", true}, {"mfeat-factors", true}, }; + vector models = { "AODE", "KDB", "SPODE", "TAN" }; string file_name; string path; - string network_name; - tie(file_name, path, network_name) = parse_arguments(argc, argv); + string model_name; + tie(file_name, path, model_name) = parse_arguments(argc, argv); if (datasets.find(file_name) == datasets.end()) { cout << "Invalid file name: " << file_name << endl; usage(argv[0]); exit(1); } - file_name = path + file_name + ".arff"; - if (!file_exists(file_name)) { - cout << "Data File " << file_name << " does not exist" << endl; + if (!file_exists(path + file_name + ".arff")) { + cout << "Data File " << path + file_name + ".arff" << " does not exist" << endl; usage(argv[0]); exit(1); } - network_name = path + network_name + ".net"; - if (!file_exists(network_name)) { - cout << "Network File " << network_name << " does not exist" << endl; + if (find(models.begin(), models.end(), model_name) == models.end()) { + cout << "Invalid model name: " << model_name << endl; usage(argv[0]); exit(1); } - return { file_name, network_name }; + return { file_name, path, model_name }; } -void build_network(bayesnet::Network& network, string network_name, map maxes) -{ - ifstream file(network_name); - string line; - while (getline(file, line)) { - if (line[0] == '#') { - continue; - } - istringstream iss(line); - string parent, child; - if (!(iss >> parent >> child)) { - break; - } - network.addNode(parent, maxes[parent]); - network.addNode(child, maxes[child]); - network.addEdge(parent, child); - } - file.close(); -} - - int main(int argc, char** argv) { - string file_name, network_name; - tie(file_name, network_name) = get_options(argc, argv); - + string file_name, path, model_name; + tie(file_name, path, model_name) = get_options(argc, argv); auto handler = ArffFiles(); - handler.load(file_name); + handler.load(path + file_name + ".arff"); // Get Dataset X, y vector& X = handler.getX(); mdlp::labels_t& y = handler.getY(); @@ -218,91 +172,54 @@ int main(int argc, char** argv) map maxes; tie(Xd, maxes) = discretize(X, y, features); maxes[className] = *max_element(y.begin(), y.end()) + 1; - cout << "Features: "; - for (auto feature : features) { - cout << "[" << feature << "] "; - } - cout << endl; - cout << "Class name: " << className << endl; - // Build Network - // auto network = bayesnet::Network(1.0); - // build_network(network, network_name, maxes); - // network.fit(Xd, y, features, className); - // cout << "Hello, Bayesian Networks!" << endl; - // showNodesInfo(network, className); - // //showCPDS(network); - // cout << "Score: " << network.score(Xd, y) << endl; - // cout << "PyTorch version: " << TORCH_VERSION << endl; - // cout << "BayesNet version: " << network.version() << endl; - // unsigned int nthreads = std::thread::hardware_concurrency(); - // cout << "Computer has " << nthreads << " cores." << endl; - // cout << "****************** First ******************" << endl; - // auto metrics = bayesnet::Metrics(network.getSamples(), features, className, network.getClassNumStates()); - // cout << "conditionalEdgeWeight " << endl; - // auto conditional = metrics.conditionalEdgeWeights(); - // cout << conditional << endl; - // long m = features.size() + 1; - // auto matrix = torch::from_blob(conditional.data(), { m, m }); - // cout << matrix << endl; - // cout << "****************** Second ******************" << endl; - // auto metrics2 = bayesnet::Metrics(Xd, y, features, className, network.getClassNumStates()); - // cout << "conditionalEdgeWeight " << endl; - // auto conditional2 = metrics2.conditionalEdgeWeights(); - // cout << conditional2 << endl; - // long m2 = features.size() + 1; - // auto matrix2 = torch::from_blob(conditional2.data(), { m, m }); - // cout << matrix2 << endl; - cout << "****************** Preparing ******************" << endl; map> states; for (auto feature : features) { states[feature] = vector(maxes[feature]); } states[className] = vector( maxes[className]); - cout << "****************** KDB ******************" << endl; + double score; + vector lines; + vector graph; auto kdb = bayesnet::KDB(2); - kdb.fit(Xd, y, features, className, states); - for (auto line : kdb.show()) { - cout << line << endl; - } - cout << "Score: " << kdb.score(Xd, y) << endl; - ofstream file("kdb.dot"); - file << kdb.graph(); - file.close(); - cout << "****************** KDB ******************" << endl; - cout << "****************** SPODE ******************" << endl; - auto spode = bayesnet::SPODE(2); - spode.fit(Xd, y, features, className, states); - for (auto line : spode.show()) { - cout << line << endl; - } - cout << "Score: " << spode.score(Xd, y) << endl; - file.open("spode.dot"); - file << spode.graph(); - file.close(); - cout << "****************** SPODE ******************" << endl; - cout << "****************** AODE ******************" << endl; auto aode = bayesnet::AODE(); - aode.fit(Xd, y, features, className, states); - for (auto line : aode.show()) { - cout << line << endl; - } - cout << "Score: " << aode.score(Xd, y) << endl; - file.open("aode.dot"); - for (auto line : aode.graph()) - file << line; - file.close(); - cout << "****************** AODE ******************" << endl; - cout << "****************** TAN ******************" << endl; + auto spode = bayesnet::SPODE(2); auto tan = bayesnet::TAN(); - tan.fit(Xd, y, features, className, states); - for (auto line : tan.show()) { + switch (hash_conv(model_name)) { + case "AODE"_sh: + aode.fit(Xd, y, features, className, states); + lines = aode.show(); + score = aode.score(Xd, y); + graph = aode.graph(); + break; + case "KDB"_sh: + kdb.fit(Xd, y, features, className, states); + lines = kdb.show(); + score = kdb.score(Xd, y); + graph = kdb.graph(); + break; + case "SPODE"_sh: + spode.fit(Xd, y, features, className, states); + lines = spode.show(); + score = spode.score(Xd, y); + graph = spode.graph(); + break; + case "TAN"_sh: + tan.fit(Xd, y, features, className, states); + lines = tan.show(); + score = tan.score(Xd, y); + graph = tan.graph(); + break; + } + for (auto line : lines) { cout << line << endl; } - cout << "Score: " << tan.score(Xd, y) << endl; - file.open("tan.dot"); - file << tan.graph(); + cout << "Score: " << score << endl; + auto dot_file = model_name + "_" + file_name; + ofstream file(dot_file + ".dot"); + file << graph; file.close(); - cout << "****************** TAN ******************" << endl; + cout << "Graph saved in " << model_name << "_" << file_name << ".dot" << endl; + cout << "dot -Tpng -o " + dot_file + ".png " + dot_file + ".dot " << endl; return 0; } \ No newline at end of file diff --git a/sample/test.cc b/sample/test.cc deleted file mode 100644 index 1757026..0000000 --- a/sample/test.cc +++ /dev/null @@ -1,208 +0,0 @@ -// #include - -// int main() -// { -// torch::Tensor t = torch::rand({ 5, 5 }); - -// // Print original tensor -// std::cout << t << std::endl; - -// // New value -// torch::Tensor new_val = torch::tensor(10.0f); - -// // Indices for the cell you want to update -// auto index_i = torch::tensor({ 2 }); -// auto index_j = torch::tensor({ 3 }); - -// // Update cell -// t.index_put_({ index_i, index_j }, new_val); - -// // Print updated tensor -// std::cout << t << std::endl; -// } -#include -#include -#include -#include -using namespace std; -double entropy(torch::Tensor feature) -{ - torch::Tensor counts = feature.bincount(); - int totalWeight = counts.sum().item(); - torch::Tensor probs = counts.to(torch::kFloat) / totalWeight; - torch::Tensor logProbs = torch::log2(probs); - torch::Tensor entropy = -probs * logProbs; - return entropy.sum().item(); -} -// H(Y|X) = sum_{x in X} p(x) H(Y|X=x) -double conditionalEntropy(torch::Tensor firstFeature, torch::Tensor secondFeature) -{ - int numSamples = firstFeature.sizes()[0]; - torch::Tensor featureCounts = secondFeature.bincount(); - unordered_map> jointCounts; - double totalWeight = 0; - for (auto i = 0; i < numSamples; i++) { - jointCounts[secondFeature[i].item()][firstFeature[i].item()] += 1; - totalWeight += 1; - } - if (totalWeight == 0) - throw invalid_argument("Total weight should not be zero"); - double entropy = 0; - for (int value = 0; value < featureCounts.sizes()[0]; ++value) { - double p_f = featureCounts[value].item() / totalWeight; - double entropy_f = 0; - for (auto& [label, jointCount] : jointCounts[value]) { - double p_l_f = jointCount / featureCounts[value].item(); - if (p_l_f > 0) { - entropy_f -= p_l_f * log2(p_l_f); - } else { - entropy_f = 0; - } - } - entropy += p_f * entropy_f; - } - return entropy; -} - -// I(X;Y) = H(Y) - H(Y|X) -double mutualInformation(torch::Tensor firstFeature, torch::Tensor secondFeature) -{ - return entropy(firstFeature) - conditionalEntropy(firstFeature, secondFeature); -} -double entropy2(torch::Tensor feature) -{ - return torch::special::entr(feature).sum().item(); -} -int main() -{ - //int i = 3, j = 1, k = 2; // Indices for the cell you want to update - // Print original tensor - // torch::Tensor t = torch::tensor({ {1, 2, 3}, {4, 5, 6} }); // 3D tensor for this example - // auto variables = vector{ "A", "B" }; - // auto cardinalities = vector{ 5, 4 }; - // torch::Tensor values = torch::rand({ 5, 4 }); - // auto candidate = "B"; - // vector newVariables; - // vector newCardinalities; - // for (int i = 0; i < variables.size(); i++) { - // if (variables[i] != candidate) { - // newVariables.push_back(variables[i]); - // newCardinalities.push_back(cardinalities[i]); - // } - // } - // torch::Tensor newValues = values.sum(1); - // cout << "original values" << endl; - // cout << values << endl; - // cout << "newValues" << endl; - // cout << newValues << endl; - // cout << "newVariables" << endl; - // for (auto& variable : newVariables) { - // cout << variable << endl; - // } - // cout << "newCardinalities" << endl; - // for (auto& cardinality : newCardinalities) { - // cout << cardinality << endl; - // } - // auto row2 = values.index({ torch::tensor(1) }); // - // cout << "row2" << endl; - // cout << row2 << endl; - // auto col2 = values.index({ "...", 1 }); - // cout << "col2" << endl; - // cout << col2 << endl; - // auto col_last = values.index({ "...", -1 }); - // cout << "col_last" << endl; - // cout << col_last << endl; - // values.index_put_({ "...", -1 }, torch::tensor({ 1,2,3,4,5 })); - // cout << "col_last" << endl; - // cout << col_last << endl; - // auto slice2 = values.index({ torch::indexing::Slice(1, torch::indexing::None) }); - // cout << "slice2" << endl; - // cout << slice2 << endl; - // auto mask = values.index({ "...", -1 }) % 2 == 0; - // auto filter = values.index({ mask, 2 }); // Filter values - // cout << "filter" << endl; - // cout << filter << endl; - // torch::Tensor dataset = torch::tensor({ {1,0,0,1},{1,1,1,2},{0,0,0,1},{1,0,2,0},{0,0,3,0} }); - // cout << "dataset" << endl; - // cout << dataset << endl; - // cout << "entropy(dataset.indices('...', 2))" << endl; - // cout << dataset.index({ "...", 2 }) << endl; - // cout << "*********************************" << endl; - // for (int i = 0; i < 4; i++) { - // cout << "datset(" << i << ")" << endl; - // cout << dataset.index({ "...", i }) << endl; - // cout << "entropy(" << i << ")" << endl; - // cout << entropy(dataset.index({ "...", i })) << endl; - // } - // cout << "......................................" << endl; - // //cout << entropy2(dataset.index({ "...", 2 })); - // cout << "conditional entropy 0 2" << endl; - // cout << conditionalEntropy(dataset.index({ "...", 0 }), dataset.index({ "...", 2 })) << endl; - // cout << "mutualInformation(dataset.index({ '...', 0 }), dataset.index({ '...', 2 }))" << endl; - // cout << mutualInformation(dataset.index({ "...", 0 }), dataset.index({ "...", 2 })) << endl; - // auto test = torch::tensor({ .1, .2, .3 }, torch::kFloat); - // auto result = torch::zeros({ 3, 3 }, torch::kFloat); - // result.index_put_({ indices }, test); - // cout << "indices" << endl; - // cout << indices << endl; - // cout << "result" << endl; - // cout << result << endl; - // cout << "Test" << endl; - // cout << torch::triu(test.reshape(3, 3), torch::kFloat)) << endl; - - - // Create a 3x3 tensor with zeros - torch::Tensor tensor_3x3 = torch::zeros({ 3, 3 }, torch::kFloat); - - // Create a 1D tensor with the three elements you want to set in the upper corner - torch::Tensor tensor_1d = torch::tensor({ 10, 11, 12 }, torch::kFloat); - - // Set the upper corner of the 3x3 tensor - auto indices = torch::triu_indices(3, 3, 1); - for (auto i = 0; i < tensor_1d.sizes()[0]; ++i) { - auto x = indices[0][i]; - auto y = indices[1][i]; - tensor_3x3[x][y] = tensor_1d[i]; - tensor_3x3[y][x] = tensor_1d[i]; - } - // Print the resulting 3x3 tensor - std::cout << tensor_3x3 << std::endl; - vector v = { 1,2,3,4,5 }; - torch::Tensor t = torch::tensor(v); - cout << t << endl; - - - - - - - // std::cout << t << std::endl; - // std::cout << "sum(0)" << std::endl; - // std::cout << t.sum(0) << std::endl; - // std::cout << "sum(1)" << std::endl; - // std::cout << t.sum(1) << std::endl; - // std::cout << "Normalized" << std::endl; - // std::cout << t / t.sum(0) << std::endl; - - // New value - // torch::Tensor new_val = torch::tensor(10.0f); - - // // Indices for the cell you want to update - // std::vector indices; - // indices.push_back(torch::tensor(i)); // Replace i with your index for the 1st dimension - // indices.push_back(torch::tensor(j)); // Replace j with your index for the 2nd dimension - // indices.push_back(torch::tensor(k)); // Replace k with your index for the 3rd dimension - // //torch::ArrayRef indices_ref(indices); - // // Update cell - // //torch::Tensor result = torch::stack(indices); - // //torch::List> indices_list = { torch::tensor(i), torch::tensor(j), torch::tensor(k) }; - // torch::List> indices_list; - // indices_list.push_back(torch::tensor(i)); - // indices_list.push_back(torch::tensor(j)); - // indices_list.push_back(torch::tensor(k)); - // //t.index_put_({ torch::tensor(i), torch::tensor(j), torch::tensor(k) }, new_val); - // t.index_put_(indices_list, new_val); - - // // Print updated tensor - // std::cout << t << std::endl; -} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..14ff9bb --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,9 @@ +if(ENABLE_TESTING) + set(TEST_MAIN "unit_tests") + set(TEST_SOURCES main.cc ../sample/ArffFiles.cc ../sample/CPPFImdlp.cpp ../sample/Metrics.cpp + ../src/utils.cc ../src/Network.cc ../src/Node.cc ../src/Metrics.cc ../src/BaseClassifier.cc ../src/KDB.cc + ../src/TAN.cc ../src/SPODE.cc ../src/Ensemble.cc ../src/AODE.cc ../src/Mst.cc) + add_executable(${TEST_MAIN} ${TEST_SOURCES}) + target_link_libraries(${TEST_MAIN} PUBLIC "${TORCH_LIBRARIES}" Catch2::Catch2WithMain) + add_test(NAME ${TEST_MAIN} COMMAND ${TEST_MAIN}) +endif(ENABLE_TESTING) diff --git a/tests/main.cc b/tests/main.cc new file mode 100644 index 0000000..31454b0 --- /dev/null +++ b/tests/main.cc @@ -0,0 +1,102 @@ +#define CATCH_CONFIG_MAIN // This tells Catch to provide a main() - only do +#include +#include +#include +#include +#include +#include +#include +#include "../sample/ArffFiles.h" +#include "../sample/CPPFImdlp.h" +#include "../src/KDB.h" +#include "../src/TAN.h" +#include "../src/SPODE.h" +#include "../src/AODE.h" + +const string PATH = "data/"; +using namespace std; + +pair, map> discretize(vector& X, mdlp::labels_t& y, vector features) +{ + vectorXd; + map maxes; + + auto fimdlp = mdlp::CPPFImdlp(); + for (int i = 0; i < X.size(); i++) { + fimdlp.fit(X[i], y); + mdlp::labels_t& xd = fimdlp.transform(X[i]); + maxes[features[i]] = *max_element(xd.begin(), xd.end()) + 1; + Xd.push_back(xd); + } + return { Xd, maxes }; +} + +TEST_CASE("Test Bayesian Classifiers score", "[BayesNet]") +{ + auto path = "../../data/"; + map , float> scores = { + {{"diabetes", "AODE"}, 0.811198}, {{"diabetes", "KDB"}, 0.852865}, {{"diabetes", "SPODE"}, 0.802083}, {{"diabetes", "TAN"}, 0.821615}, + {{"ecoli", "AODE"}, 0.889881}, {{"ecoli", "KDB"}, 0.889881}, {{"ecoli", "SPODE"}, 0.880952}, {{"ecoli", "TAN"}, 0.892857}, + {{"glass", "AODE"}, 0.78972}, {{"glass", "KDB"}, 0.827103}, {{"glass", "SPODE"}, 0.775701}, {{"glass", "TAN"}, 0.827103}, + {{"iris", "AODE"}, 0.973333}, {{"iris", "KDB"}, 0.973333}, {{"iris", "SPODE"}, 0.973333}, {{"iris", "TAN"}, 0.973333} + }; + + string file_name = GENERATE("glass", "iris", "ecoli", "diabetes"); + auto handler = ArffFiles(); + handler.load(path + static_cast(file_name) + ".arff"); + // Get Dataset X, y + vector& X = handler.getX(); + mdlp::labels_t& y = handler.getY(); + // Get className & Features + auto className = handler.getClassName(); + vector features; + for (auto feature : handler.getAttributes()) { + features.push_back(feature.first); + } + // Discretize Dataset + vector Xd; + map maxes; + tie(Xd, maxes) = discretize(X, y, features); + maxes[className] = *max_element(y.begin(), y.end()) + 1; + map> states; + for (auto feature : features) { + states[feature] = vector(maxes[feature]); + } + states[className] = vector(maxes[className]); + SECTION("Test TAN classifier (" + file_name + ")") + { + auto clf = bayesnet::TAN(); + clf.fit(Xd, y, features, className, states); + auto score = clf.score(Xd, y); + //scores[{file_name, "TAN"}] = score; + REQUIRE(score == Catch::Approx(scores[{file_name, "TAN"}]).epsilon(1e-6)); + } + SECTION("Test KDB classifier (" + file_name + ")") + { + auto clf = bayesnet::KDB(2); + clf.fit(Xd, y, features, className, states); + auto score = clf.score(Xd, y); + //scores[{file_name, "KDB"}] = score; + REQUIRE(score == Catch::Approx(scores[{file_name, "KDB" + }]).epsilon(1e-6)); + } + SECTION("Test SPODE classifier (" + file_name + ")") + { + auto clf = bayesnet::SPODE(1); + clf.fit(Xd, y, features, className, states); + auto score = clf.score(Xd, y); + // scores[{file_name, "SPODE"}] = score; + REQUIRE(score == Catch::Approx(scores[{file_name, "SPODE"}]).epsilon(1e-6)); + } + SECTION("Test AODE classifier (" + file_name + ")") + { + auto clf = bayesnet::AODE(); + clf.fit(Xd, y, features, className, states); + auto score = clf.score(Xd, y); + // scores[{file_name, "AODE"}] = score; + REQUIRE(score == Catch::Approx(scores[{file_name, "AODE"}]).epsilon(1e-6)); + } + // for (auto scores : scores) { + // cout << "{{\"" << scores.first.first << "\", \"" << scores.first.second << "\"}, " << scores.second << "}, "; + // } +} \ No newline at end of file