From 0305eda867b657a1ddbacc58c12d50f828ef4d64 Mon Sep 17 00:00:00 2001 From: ifilot Date: Fri, 6 Mar 2026 11:57:37 +0100 Subject: [PATCH 1/6] Revising compilation procedure --- .github/workflows/build-openvdb.yml | 39 ++++-- .github/workflows/build.yml | 114 +++++++++++------ docker/Dockerfile | 54 ++++++++ src/CMakeLists.txt | 192 +++++++++++++--------------- src/cmake/modules/FindCPPUNIT.cmake | 75 ----------- src/test/CMakeLists.txt | 52 +++----- 6 files changed, 259 insertions(+), 267 deletions(-) create mode 100644 docker/Dockerfile delete mode 100644 src/cmake/modules/FindCPPUNIT.cmake diff --git a/.github/workflows/build-openvdb.yml b/.github/workflows/build-openvdb.yml index df1bc13..6224113 100644 --- a/.github/workflows/build-openvdb.yml +++ b/.github/workflows/build-openvdb.yml @@ -8,20 +8,31 @@ on: jobs: build: - runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - name: Install dependencies - run: sudo apt install -y build-essential cmake libglm-dev libtclap-dev libboost-all-dev libopenvdb-dev libtbb-dev libcppunit-dev libeigen3-dev wget liblzma-dev zlib1g-dev libbz2-dev - - name: Get sources OpenVDB 8.2 - run: wget https://github.com/AcademySoftwareFoundation/openvdb/archive/refs/tags/v8.2.0.tar.gz && tar -xvzf v8.2.0.tar.gz - - name: Compile and install OpenVDB 8.02 - run: mkdir openvdb-build && cd openvdb-build && cmake ../openvdb-8.2.0 -DCMAKE_INSTALL_PREFIX=/opt/openvdb && make -j3 && sudo make install - - name: Configure CMake - run: mkdir build && cd build && cmake -DMOD_OPENVDB=1 ../src - - name: Build - run: cd build && make -j3 - - name: Perform unit tests - run: cd build && make test + - uses: actions/checkout@v4 + - name: Install dependencies + run: | + sudo apt update + sudo apt install -y \ + build-essential \ + cmake \ + libglm-dev \ + libtclap-dev \ + libboost-all-dev \ + libopenvdb-dev \ + libtbb-dev \ + libcppunit-dev \ + libeigen3-dev \ + liblzma-dev \ + zlib1g-dev \ + libbz2-dev \ + libssl-dev \ + pkg-config + - name: Configure CMake + run: cmake -S src -B build -DMOD_OPENVDB=ON + - name: Build + run: cmake --build build --parallel + - name: Run unit tests + run: ctest --test-dir build --output-on-failure diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 71cf495..f212f0f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,52 +8,84 @@ on: jobs: build: - runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - name: Install dependencies - run: sudo apt install -y build-essential cmake libglm-dev libtclap-dev libboost-all-dev libopenvdb-dev libtbb-dev libcppunit-dev libeigen3-dev liblzma-dev zlib1g-dev libbz2-dev gcovr - - name: Configure CMake - run: mkdir build && cd build && cmake ../src -DUSE_GCOV=1 - - name: Build - run: cd build && make -j - - name: Perform unit tests - run: cd build && make test - - name: Perform code coverage - run: | - cd build - gcovr -r ../ . --xml-pretty -e ".*\.h" > coverage.xml - gcovr -r ../ . --html -e ".*\.h" > report.html - - name: Upload coverage report - uses: actions/upload-artifact@v4 - with: - name: code-coverage-report - path: ./build/report.html - - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v3 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: ./build/coverage.xml - - test-shared: + - uses: actions/checkout@v4 + - name: Install dependencies + run: | + sudo apt update + sudo apt install -y \ + build-essential \ + cmake \ + libglm-dev \ + libtclap-dev \ + libboost-all-dev \ + libopenvdb-dev \ + libtbb-dev \ + libcppunit-dev \ + libeigen3-dev \ + liblzma-dev \ + zlib1g-dev \ + libbz2-dev \ + libssl-dev \ + pkg-config \ + gcovr + - name: Configure CMake + run: cmake -S src -B build -DUSE_GCOV=ON + - name: Build + run: cmake --build build --parallel + - name: Run unit tests + run: ctest --test-dir build --output-on-failure + - name: Collect coverage report + run: | + gcovr -r . build --xml-pretty -e ".*\\.h" > build/coverage.xml + gcovr -r . build --html -e ".*\\.h" > build/report.html + - name: Upload coverage report + uses: actions/upload-artifact@v4 + with: + name: code-coverage-report + path: ./build/report.html + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ./build/coverage.xml + test-shared: runs-on: ubuntu-latest needs: build steps: - - uses: actions/checkout@v3 - - name: Install dependencies - run: sudo apt install -y build-essential cmake libglm-dev libtclap-dev libboost-all-dev libopenvdb-dev libtbb-dev libcppunit-dev libeigen3-dev liblzma-dev zlib1g-dev libbz2-dev gcovr - - name: Configure CMake - run: mkdir build && cd build && cmake ../src - - name: Build - run: cd build && make -j && sudo make install - - name: Produce compilation using shared library - run: | - cd examples && mkdir build && cd build - cmake ../shared - make -j - ldd ./den2obj-shared-example - LD_LIBRARY_PATH=/usr/local/lib ./den2obj-shared-example + - uses: actions/checkout@v4 + - name: Install dependencies + run: | + sudo apt update + sudo apt install -y \ + build-essential \ + cmake \ + libglm-dev \ + libtclap-dev \ + libboost-all-dev \ + libopenvdb-dev \ + libtbb-dev \ + libcppunit-dev \ + libeigen3-dev \ + liblzma-dev \ + zlib1g-dev \ + libbz2-dev \ + libssl-dev \ + pkg-config \ + gcovr + - name: Configure CMake + run: cmake -S src -B build + - name: Build and install + run: | + cmake --build build --parallel + sudo cmake --install build + - name: Produce compilation using shared library + run: | + cmake -S examples/shared -B examples/shared/build + cmake --build examples/shared/build --parallel + ldd examples/shared/build/den2obj-shared-example + LD_LIBRARY_PATH=/usr/local/lib ./examples/shared/build/den2obj-shared-example diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..e289fcd --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,54 @@ +FROM ubuntu:20.04 + +ENV DEBIAN_FRONTEND=noninteractive + +# Install dependencies +RUN apt-get update && apt-get install -y \ + build-essential \ + curl \ + wget \ + git \ + flex \ + bison \ + texinfo \ + libgmp-dev \ + libmpfr-dev \ + libmpc-dev \ + libisl-dev \ + zlib1g-dev \ + python3 \ + cmake \ + ninja-build \ + unzip \ + patch \ + pkg-config \ + xz-utils \ + libtclap-dev \ + libopenvdb-dev \ + libtbb-dev \ + libcppunit-dev \ + libeigen3-dev \ + liblzma-dev \ + zlib1g-dev \ + libbz2-dev \ + libssl-dev + +RUN apt-get install curl wget + +# -------- Build & install latest Boost -------- +ARG BOOST_PREFIX="/usr/local" +ARG BOOST_WITH="--with-system --with-filesystem --with-thread --with-chrono --with-date_time --with-regex --with-program_options --with-iostreams" + +WORKDIR /tmp +RUN wget https://archives.boost.io/release/1.89.0/source/boost_1_89_0.tar.bz2 +RUN tar -xvjf boost_1_89_0.tar.bz2 +WORKDIR /tmp/boost_1_89_0 +RUN ./bootstrap.sh --prefix="${BOOST_PREFIX}" +RUN ./b2 -j"$(nproc)" ${BOOST_WITH} variant=release runtime-link=shared install +RUN echo "${BOOST_PREFIX}/lib" > /etc/ld.so.conf.d/boost.conf +RUN ldconfig + +# -------- Cleanup -------- +RUN apt-get clean + +CMD ["g++", "--version"] \ No newline at end of file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 63fc22c..68c202b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -18,138 +18,124 @@ # * #*************************************************************************/ -# set minimum cmake requirements -cmake_minimum_required(VERSION 3.5) -project (den2obj) +cmake_minimum_required(VERSION 3.16) +project(den2obj VERSION 1.2.2 LANGUAGES CXX) -# change compiler directives when code coverage is required -if(USE_GCOV) - set(CMAKE_BUILD_TYPE "Debug") - - # Set global c and c++ flags - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fprofile-arcs -ftest-coverage") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fprofile-arcs -ftest-coverage") +option(USE_GCOV "Build with gcov coverage instrumentation" OFF) +option(MOD_OPENVDB "Enable OpenVDB support" OFF) +option(HAS_OPENMP "Enable OpenMP support when available" ON) +option(DISABLE_TEST "Disable building tests" OFF) - # Link flags used for creating executables - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -lgcov -fprofile-arcs -ftest-coverage") - - # Link flags used for creating shared libraries - set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -lgcov -ftest-coverage") +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + message(STATUS "Setting build type to 'Release' as none was specified.") + set(CMAKE_BUILD_TYPE Release CACHE STRING "Choose the type of build." FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "MinSizeRel" "RelWithDebInfo") endif() -# add custom directory to look for .cmake files -set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake/modules ) +set(PROGNAME "DEN2OBJ") +set(VERSION_MAJOR "${PROJECT_VERSION_MAJOR}") +set(VERSION_MINOR "${PROJECT_VERSION_MINOR}") +set(VERSION_MICRO "${PROJECT_VERSION_PATCH}") -# store git hash execute_process( COMMAND git log -1 --format=%h WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR} OUTPUT_VARIABLE GIT_HASH OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET ) -# prepare configuration file -SET(PROGNAME "DEN2OBJ") -SET(VERSION_MAJOR "1") -SET(VERSION_MINOR "2") -SET(VERSION_MICRO "2") -message(STATUS "Writing configuration file in: ${CMAKE_CURRENT_SOURCE_DIR}/config.h") -configure_file(${CMAKE_CURRENT_SOURCE_DIR}/config.h.in ${CMAKE_CURRENT_SOURCE_DIR}/config.h @ONLY) -configure_file(${CMAKE_CURRENT_SOURCE_DIR}/den2obj.pc.in ${CMAKE_BINARY_DIR}/den2obj.pc @ONLY) - -# Enable release build -if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) - message(STATUS "Setting build type to 'Release' as none was specified.") - set(CMAKE_BUILD_TYPE Release CACHE STRING "Choose the type of build." FORCE) - # Set the possible values of build type for cmake-gui - set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "MinSizeRel" "RelWithDebInfo") -endif() - -# add OS specific -SET(BOOST_INCLUDEDIR "/usr/include") -SET(BOOST_LIBRARYDIR "/usr/lib/x86_64-linux-gnu") - -find_package(OpenMP) -if (OPENMP_FOUND) - option(HAS_OPENMP "OpenMP enabled" ON) - set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${OpenMP_C_FLAGS}") - set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${OpenMP_CXX_FLAGS}") -endif() - -# set Boost -set (Boost_NO_SYSTEM_PATHS ON) -set (Boost_USE_MULTITHREADED ON) -set (Boost_USE_STATIC_LIBS ON) -set (Boost_USE_STATIC_RUNTIME OFF) -set (BOOST_ALL_DYN_LINK OFF) +message(STATUS "Writing configuration file in: ${CMAKE_CURRENT_BINARY_DIR}/config.h") +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/config.h.in ${CMAKE_CURRENT_BINARY_DIR}/config.h @ONLY) +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/den2obj.pc.in ${CMAKE_CURRENT_BINARY_DIR}/den2obj.pc @ONLY) -# Include libraries find_package(PkgConfig REQUIRED) -find_package(Boost COMPONENTS regex iostreams filesystem REQUIRED) +find_package(Boost REQUIRED COMPONENTS regex iostreams filesystem) find_package(LibLZMA REQUIRED) find_package(ZLIB REQUIRED) find_package(BZip2 REQUIRED) -find_package(CPPUNIT REQUIRED) # for unit tests -pkg_check_modules(TCLAP tclap REQUIRED) -pkg_check_modules(EIGEN eigen3 REQUIRED) -pkg_check_modules(CRYPTO libcrypto REQUIRED) # for unit tests - -# Set include folders -include_directories(${CMAKE_CURRENT_SOURCE_DIR} - ${CMAKE_BINARY_DIR} - ${EIGEN_INCLUDE_DIRS} - ${TCLAP_INCLUDE_DIR} - ${Boost_INCLUDE_DIRS} - ${CPPUNIT_INCLUDE_DIR}) - -# enable OpenVDB support +pkg_check_modules(TCLAP REQUIRED IMPORTED_TARGET tclap) +pkg_check_modules(EIGEN REQUIRED IMPORTED_TARGET eigen3) + +if(HAS_OPENMP) + find_package(OpenMP) +endif() + +set(OPENVDB_LIBS "") if(MOD_OPENVDB) - SET(OPENVDB_LIBS "openvdb" "tbb" "blosc" "-lz") - MESSAGE("Compiling with OpenVDB module") - add_compile_definitions(MOD_OPENVDB) + set(OPENVDB_LIBS openvdb tbb blosc ZLIB::ZLIB) + message(STATUS "Compiling with OpenVDB module") endif() -# Add sources -file(GLOB SOURCES "*.cpp") -list(REMOVE_ITEM SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/den2obj.cpp) +file(GLOB SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/*.cpp") +list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/den2obj.cpp") +file(GLOB DEN2OBJ_HEADERS CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/*.h") + add_library(den2objsources STATIC ${SOURCES}) add_library(den2obj SHARED ${SOURCES}) add_executable(den2obj_exec ${CMAKE_CURRENT_SOURCE_DIR}/den2obj.cpp) set_property(TARGET den2obj_exec PROPERTY OUTPUT_NAME den2obj) -file(GLOB DEN2OBJ_HEADERS "*.h") set_target_properties(den2obj PROPERTIES PUBLIC_HEADER "${DEN2OBJ_HEADERS}") -# Set C++17 -add_definitions(-std=c++17 -march=native) - -# Link libraries -#SET(CMAKE_EXE_LINKER_FLAGS "-Wl,-rpath=\$ORIGIN/lib") -target_link_libraries(den2obj_exec - den2objsources - ${OPENVDB_LIBS} - ${Boost_LIBRARIES} - ${LIBLZMA_LIBRARIES} - ${ZLIB_LIBRARIES} - ${BZIP2_LIBRARIES}) - -if(NOT DISABLE_TEST) - message("[USER] Performing unit tests") - enable_testing () - add_subdirectory("test") +foreach(target den2objsources den2obj den2obj_exec) + target_compile_features(${target} PUBLIC cxx_std_17) + target_compile_options(${target} PRIVATE -march=native) + target_include_directories(${target} + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_BINARY_DIR} + ${Boost_INCLUDE_DIRS} + ${TCLAP_INCLUDE_DIRS} + ${EIGEN_INCLUDE_DIRS} + ) + + if(OpenMP_CXX_FOUND) + target_link_libraries(${target} PUBLIC OpenMP::OpenMP_CXX) + endif() + + if(MOD_OPENVDB) + target_compile_definitions(${target} PUBLIC MOD_OPENVDB) + endif() +endforeach() + +if(USE_GCOV) + foreach(target den2objsources den2obj den2obj_exec) + target_compile_options(${target} PRIVATE --coverage) + target_link_options(${target} PRIVATE --coverage) + endforeach() +endif() + +target_link_libraries(den2objsources + PUBLIC + Boost::regex + Boost::iostreams + Boost::filesystem + LibLZMA::LibLZMA + ZLIB::ZLIB + BZip2::BZip2 + ${OPENVDB_LIBS} +) + +target_link_libraries(den2obj PUBLIC den2objsources) +target_link_libraries(den2obj_exec PRIVATE den2objsources) + +include(CTest) +if(DISABLE_TEST) + set(BUILD_TESTING OFF CACHE BOOL "" FORCE) +endif() + +if(BUILD_TESTING) + message(STATUS "[USER] Performing unit tests") + add_subdirectory(test) else() - message("[USER] Testing routine disabled") + message(STATUS "[USER] Testing routine disabled") endif() -### -# Installing -## include(GNUInstallDirs) -# install shared library and header files -install (TARGETS den2obj - DESTINATION bin - LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} - PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/den2obj) +install(TARGETS den2obj den2obj_exec + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/den2obj) -# install pkg-config files -install(FILES ${CMAKE_BINARY_DIR}/den2obj.pc DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/pkgconfig) \ No newline at end of file +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/den2obj.pc DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/pkgconfig) diff --git a/src/cmake/modules/FindCPPUNIT.cmake b/src/cmake/modules/FindCPPUNIT.cmake deleted file mode 100644 index 01014c3..0000000 --- a/src/cmake/modules/FindCPPUNIT.cmake +++ /dev/null @@ -1,75 +0,0 @@ - #************************************************************************* - # CMakeLists.txt -- This file is part of edp. * - # * - # Author: Ivo Filot * - # * - # edp is free software: you can redistribute it and/or modify * - # it under the terms of the GNU General Public License as published * - # by the Free Software Foundation, either version 3 of the License, * - # or (at your option) any later version. * - # * - # edp is distributed in the hope that it will be useful, * - # but WITHOUT ANY WARRANTY; without even the implied warranty * - # of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * - # See the GNU General Public License for more details. * - # * - # You should have received a copy of the GNU General Public License * - # along with this program. If not, see http://www.gnu.org/licenses/. * - # * - #*************************************************************************/ - -# -# Find the CppUnit includes and library -# -# This module defines -# CPPUNIT_INCLUDE_DIR, where to find tiff.h, etc. -# CPPUNIT_LIBRARIES, the libraries to link against to use CppUnit. -# CPPUNIT_FOUND, If false, do not try to use CppUnit. - -# also defined, but not for general use are -# CPPUNIT_LIBRARY, where to find the CppUnit library. -# CPPUNIT_DEBUG_LIBRARY, where to find the CppUnit library in debug -# mode. - -SET(CPPUNIT_FOUND "NO") - -FIND_PATH(CPPUNIT_INCLUDE_DIR cppunit/TestCase.h /usr/local/include /usr/include ../../../Libraries/cppunit-1.14.0-win-x64/include) - -# With Win32, important to have both -IF(WIN32) - FIND_LIBRARY(CPPUNIT_LIBRARY cppunit - ${CPPUNIT_INCLUDE_DIR}/../lib - /usr/local/lib - /usr/lib) - FIND_LIBRARY(CPPUNIT_DEBUG_LIBRARY cppunitd - ${CPPUNIT_INCLUDE_DIR}/../lib - /usr/local/lib - /usr/lib) -ELSE(WIN32) - # On unix system, debug and release have the same name - FIND_LIBRARY(CPPUNIT_LIBRARY cppunit - ${CPPUNIT_INCLUDE_DIR}/../lib - /usr/local/lib - /usr/lib) - FIND_LIBRARY(CPPUNIT_DEBUG_LIBRARY cppunit - ${CPPUNIT_INCLUDE_DIR}/../lib - /usr/local/lib - /usr/lib) -ENDIF(WIN32) - -IF(CPPUNIT_INCLUDE_DIR) - IF(CPPUNIT_LIBRARY) - SET(CPPUNIT_FOUND "YES") - SET(CPPUNIT_LIBRARIES ${CPPUNIT_LIBRARY} ${CMAKE_DL_LIBS}) - SET(CPPUNIT_DEBUG_LIBRARIES ${CPPUNIT_DEBUG_LIBRARY} ${CMAKE_DL_LIBS}) - ELSE (CPPUNIT_LIBRARY) - IF (CPPUNIT_FIND_REQUIRED) - MESSAGE(SEND_ERROR "Could not find library CppUnit.") - ENDIF (CPPUNIT_FIND_REQUIRED) - ENDIF(CPPUNIT_LIBRARY) -ELSE(CPPUNIT_INCLUDE_DIR) - IF (CPPUNIT_FIND_REQUIRED) - MESSAGE(SEND_ERROR "Could not find library CppUnit.") - ENDIF(CPPUNIT_FIND_REQUIRED) -ENDIF(CPPUNIT_INCLUDE_DIR) - diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt index f4d3c86..302fb99 100644 --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -18,72 +18,56 @@ # * #*************************************************************************/ +pkg_check_modules(CPPUNIT REQUIRED IMPORTED_TARGET cppunit) +pkg_check_modules(CRYPTO REQUIRED IMPORTED_TARGET libcrypto) + add_library(unittest STATIC unittest.cpp) +target_compile_features(unittest PUBLIC cxx_std_17) -# set executables -SET(EXECUTABLES TestIsosurface TestScalarField TestD2OFileFormat) +set(EXECUTABLES TestIsosurface TestScalarField TestD2OFileFormat) -# only add this test when not using GCOV if(NOT USE_GCOV) list(APPEND EXECUTABLES TestGenerator) endif() -# only add this test when using GCOV if(USE_GCOV) list(APPEND EXECUTABLES TestFileCreation) endif() -####################################################### -# Add executables -####################################################### add_executable(TestIsosurface test_isosurface.cpp) add_executable(TestScalarField test_scalarfield.cpp) add_executable(TestD2OFileFormat test_d2o_fileformat.cpp) -# only add this test when not using GCOV if(NOT USE_GCOV) add_executable(TestGenerator test_generator.cpp) endif() -# only add this test when using GCOV if(USE_GCOV) add_executable(TestFileCreation test_file_creation.cpp) endif() -####################################################### -# Link mkmsources and other dependencies -####################################################### -if(CMAKE_BUILD_TYPE MATCHES Debug) - SET(CPPUNIT_LIB ${CPPUNIT_DEBUG_LIBRARY}) -else() - SET(CPPUNIT_LIB ${CPPUNIT_LIBRARY}) -endif() - -# add unpacking of dataset to the test suite add_test(NAME DatasetSetup COMMAND tar -xvjf dataset.tar.bz2) add_test(NAME DatasetCleanup COMMAND rm -rvf CHGCAR_* PARCHG_* *.d2o *.cub) set_tests_properties(DatasetSetup PROPERTIES FIXTURES_SETUP Dataset) set_tests_properties(DatasetCleanup PROPERTIES FIXTURES_CLEANUP Dataset) -# configure common settings for test executables foreach(testexec ${EXECUTABLES}) + target_compile_features(${testexec} PUBLIC cxx_std_17) + if(USE_GCOV) + target_compile_options(${testexec} PRIVATE --coverage) + target_link_options(${testexec} PRIVATE --coverage) + endif() + target_link_libraries(${testexec} - unittest - den2objsources - ${OPENVDB_LIBS} - ${Boost_LIBRARIES} - ${CPPUNIT_LIB} - ${LIBLZMA_LIBRARIES} - ${ZLIB_LIBRARIES} - ${BZIP2_LIBRARIES} - ${CRYPTO_LIBRARIES} - -lgcov) - set_target_properties(${testexec} PROPERTIES COMPILE_FLAGS "--coverage") + PRIVATE + unittest + den2objsources + PkgConfig::CPPUNIT + PkgConfig::CRYPTO + ) + add_test(NAME ${testexec} COMMAND ${testexec}) set_tests_properties(${testexec} PROPERTIES FIXTURES_REQUIRED Dataset) endforeach() -####################################################### -# add testinput files -####################################################### configure_file(testinput/dataset.tar.bz2 dataset.tar.bz2 COPYONLY) From 08bff7bf336846df76a55def6c93f41775fc526d Mon Sep 17 00:00:00 2001 From: ifilot Date: Sat, 7 Mar 2026 14:27:57 +0100 Subject: [PATCH 2/6] Removing MD5 checkusum based verification --- docs/installation.rst | 4 +- src/test/CMakeLists.txt | 3 - src/test/test_file_creation.cpp | 182 ++++++++++++++++++-------------- src/test/test_file_creation.h | 25 +++-- 4 files changed, 122 insertions(+), 92 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index b17cbbb..b2f011d 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -21,7 +21,6 @@ libraries are available to you: * `GZIP `_ (gzip data compression) * `LZMA `_ (lzma data compression) * `CPPUnit `_ (unit testing) -* `OpenSSL `_ (unit testing; MD5 checksums) .. note:: * The instructions covered in this guide assume you are running a @@ -33,8 +32,7 @@ libraries are available to you: To ensure that all the packages are installed, one can run the following:: sudo apt install build-essential cmake libtclap-dev libboost-all-dev \ - pkg-config libcppunit-dev libeigen3-dev liblzma-dev zlib1g-dev libbz2-dev \ - libssl-dev + pkg-config libcppunit-dev libeigen3-dev liblzma-dev zlib1g-dev libbz2-dev Standard compilation -------------------- diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt index 302fb99..60a0e72 100644 --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -19,8 +19,6 @@ #*************************************************************************/ pkg_check_modules(CPPUNIT REQUIRED IMPORTED_TARGET cppunit) -pkg_check_modules(CRYPTO REQUIRED IMPORTED_TARGET libcrypto) - add_library(unittest STATIC unittest.cpp) target_compile_features(unittest PUBLIC cxx_std_17) @@ -63,7 +61,6 @@ foreach(testexec ${EXECUTABLES}) unittest den2objsources PkgConfig::CPPUNIT - PkgConfig::CRYPTO ) add_test(NAME ${testexec} COMMAND ${testexec}) diff --git a/src/test/test_file_creation.cpp b/src/test/test_file_creation.cpp index af15639..186694c 100644 --- a/src/test/test_file_creation.cpp +++ b/src/test/test_file_creation.cpp @@ -29,106 +29,134 @@ void TestFileCreation::tearDown() { } void TestFileCreation::test_ply_file() { - // set number of threads to 1 - omp_set_num_threads(1); - - // read scalar field - ScalarField sf("co_2pi_x.cub", ScalarFieldInputFileType::SFF_CUB); - sf.read(); - - // perform marching cubes algorithm - IsoSurface is(&sf); - is.marching_cubes(0.01); - - // construct mesh - IsoSurfaceMesh ism(&sf, &is); - ism.construct_mesh(true); + const auto ref = this->generate_mesh(); + this->assert_ply_shape("test.ply", ref); +} - // create file - ism.write_to_file("test.ply", "co molecule 2pi_x orbital test", "molecule"); +void TestFileCreation::test_stl_file() { + const auto ref = this->generate_mesh(); + this->assert_stl_shape("test.stl", ref); +} - // test md5sum - CPPUNIT_ASSERT_EQUAL(this->md5("test.ply"), std::string("ec143f6ebf9b72761803c3b091f54bf3")); +void TestFileCreation::test_obj_file() { + const auto ref = this->generate_mesh(); + this->assert_obj_shape("test.obj", ref); } -void TestFileCreation::test_stl_file() { +TestFileCreation::MeshReference TestFileCreation::generate_mesh() const { // set number of threads to 1 omp_set_num_threads(1); // read scalar field ScalarField sf("co_2pi_x.cub", ScalarFieldInputFileType::SFF_CUB); sf.read(); - + // perform marching cubes algorithm IsoSurface is(&sf); is.marching_cubes(0.01); - + // construct mesh IsoSurfaceMesh ism(&sf, &is); ism.construct_mesh(true); - // create file + // create files for all supported output formats + ism.write_to_file("test.ply", "co molecule 2pi_x orbital test", "molecule"); ism.write_to_file("test.stl", "co molecule 2pi_x orbital test", "molecule"); + ism.write_to_file("test.obj", "co molecule 2pi_x orbital test", "molecule"); - // test md5sum - CPPUNIT_ASSERT_EQUAL(this->md5("test.stl"), std::string("c2194ba639caf5092654862bb9f93298")); + return { + ism.get_vertices().size(), + ism.get_normals().size(), + ism.get_texcoords().size() / 6 + }; } -void TestFileCreation::test_obj_file() { - // set number of threads to 1 - omp_set_num_threads(1); +void TestFileCreation::assert_obj_shape(const std::string& filename, const MeshReference& ref) const { + CPPUNIT_ASSERT(std::filesystem::exists(filename)); + + std::ifstream infile(filename); + CPPUNIT_ASSERT(infile.good()); + + std::string line; + size_t vertex_lines = 0; + size_t normal_lines = 0; + size_t face_lines = 0; + + while(std::getline(infile, line)) { + if(line.rfind("v ", 0) == 0) { + vertex_lines++; + } else if(line.rfind("vn ", 0) == 0) { + normal_lines++; + } else if(line.rfind("f ", 0) == 0) { + face_lines++; + } + } - // read scalar field - ScalarField sf("co_2pi_x.cub", ScalarFieldInputFileType::SFF_CUB); - sf.read(); - - // perform marching cubes algorithm - IsoSurface is(&sf); - is.marching_cubes(0.01); - - // construct mesh - IsoSurfaceMesh ism(&sf, &is); - ism.construct_mesh(true); + CPPUNIT_ASSERT_EQUAL(ref.vertices, vertex_lines); + CPPUNIT_ASSERT_EQUAL(ref.normals, normal_lines); + CPPUNIT_ASSERT_EQUAL(ref.triangles, face_lines); +} - // create file - ism.write_to_file("test.obj", "co molecule 2pi_x orbital test", "molecule"); +void TestFileCreation::assert_ply_shape(const std::string& filename, const MeshReference& ref) const { + CPPUNIT_ASSERT(std::filesystem::exists(filename)); + + std::ifstream infile(filename, std::ios::binary); + CPPUNIT_ASSERT(infile.good()); + + std::string line; + size_t header_size = 0; + size_t header_vertices = 0; + size_t header_faces = 0; + bool seen_ply = false; + bool seen_binary_format = false; + + while(std::getline(infile, line)) { + header_size += line.size() + 1; + + if(line == "ply") { + seen_ply = true; + } + if(line == "format binary_little_endian 1.0" || line == "format binary_big_endian 1.0") { + seen_binary_format = true; + } + if(line.rfind("element vertex ", 0) == 0) { + header_vertices = std::stoul(line.substr(15)); + } + if(line.rfind("element face ", 0) == 0) { + header_faces = std::stoul(line.substr(13)); + } + if(line == "end_header") { + break; + } + } + + CPPUNIT_ASSERT(seen_ply); + CPPUNIT_ASSERT(seen_binary_format); + CPPUNIT_ASSERT_EQUAL(ref.vertices, header_vertices); + CPPUNIT_ASSERT_EQUAL(ref.triangles, header_faces); - // test md5sum - CPPUNIT_ASSERT_EQUAL(this->md5("test.obj"), std::string("e2b3e09f9c010dac99a7bc0137c187ec")); + const size_t expected_binary_size = + ref.vertices * (6 * sizeof(float)) + + ref.triangles * (sizeof(uint8_t) + 3 * sizeof(unsigned int)); + + CPPUNIT_ASSERT_EQUAL(header_size + expected_binary_size, (size_t)std::filesystem::file_size(filename)); } -/** - * @brief Calculate MD5 checksum of a file - * - * @param[in] name Path to the file - * - * @return 32 byte string containing md5 checksum - */ -std::string TestFileCreation::md5(const std::string& filename) { - // read the file - std::ifstream mfile(filename, std::ios::binary | std::ios::ate); - std::streamsize size = mfile.tellg(); - mfile.seekg(0, std::ios::beg); - char buffer[size]; - mfile.read(buffer, size); - mfile.close(); - - // output variable for hash - unsigned char hash[MD5_DIGEST_LENGTH]; - - // calculate the md5 hash - EVP_MD_CTX *ctx = EVP_MD_CTX_create(); - EVP_MD_CTX_init(ctx); - const EVP_MD *md_type = EVP_md5(); - EVP_DigestInit_ex(ctx, md_type, NULL); - EVP_DigestUpdate(ctx, buffer, size); - EVP_DigestFinal_ex(ctx, hash, NULL); - EVP_MD_CTX_destroy(ctx); - - // output as a 32-byte hex-string - std::stringstream ss; - for(int i = 0; i < MD5_DIGEST_LENGTH; i++){ - ss << std::hex << std::setw(2) << std::setfill('0') << static_cast( hash[i] ); - } - return ss.str(); -} \ No newline at end of file +void TestFileCreation::assert_stl_shape(const std::string& filename, const MeshReference& ref) const { + CPPUNIT_ASSERT(std::filesystem::exists(filename)); + + std::ifstream infile(filename, std::ios::binary); + CPPUNIT_ASSERT(infile.good()); + + // skip header + infile.seekg(80, std::ios::beg); + + uint32_t triangle_count = 0; + infile.read(reinterpret_cast(&triangle_count), sizeof(uint32_t)); + CPPUNIT_ASSERT(infile.good()); + + CPPUNIT_ASSERT_EQUAL(static_cast(ref.triangles), triangle_count); + + const size_t expected_size = 80 + sizeof(uint32_t) + ref.triangles * (12 * sizeof(float) + sizeof(uint16_t)); + CPPUNIT_ASSERT_EQUAL(expected_size, (size_t)std::filesystem::file_size(filename)); +} diff --git a/src/test/test_file_creation.h b/src/test/test_file_creation.h index 3615fd2..744ab0f 100644 --- a/src/test/test_file_creation.h +++ b/src/test/test_file_creation.h @@ -23,10 +23,9 @@ #include -// we need these for the MD5 checksums -#include -#include -#include +#include +#include +#include #include "scalar_field.h" #include "isosurface.h" @@ -34,12 +33,11 @@ /** * Test that verifies file creation (obj, stl and ply) - * + * * Because the marching cubes algorithm uses OpenMP parallellization, we need * to set the number of threads to 1 to obtain consistent results. With * higher number of cores, the results are subject to race conditions, leading - * to different (although not incorrect) results preventing the use of a simple - * MD5 checksum for file validation. + * to different (although not incorrect) results. */ class TestFileCreation : public CppUnit::TestFixture { CPPUNIT_TEST_SUITE( TestFileCreation ); @@ -56,7 +54,16 @@ class TestFileCreation : public CppUnit::TestFixture { void tearDown(); private: - std::string md5(const std::string &str); + struct MeshReference { + size_t vertices; + size_t normals; + size_t triangles; + }; + + MeshReference generate_mesh() const; + void assert_obj_shape(const std::string& filename, const MeshReference& ref) const; + void assert_ply_shape(const std::string& filename, const MeshReference& ref) const; + void assert_stl_shape(const std::string& filename, const MeshReference& ref) const; }; -#endif \ No newline at end of file +#endif From 8fb50420ef5cc6c2b09b9ee10875bd8486666f5e Mon Sep 17 00:00:00 2001 From: ifilot Date: Thu, 21 May 2026 08:27:51 +0200 Subject: [PATCH 3/6] Strengthening README.md file --- README.md | 201 +++++++++++++++++++++++++++++++++------------ src/CMakeLists.txt | 2 +- 2 files changed, 151 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index 482bf68..8b163ff 100644 --- a/README.md +++ b/README.md @@ -8,121 +8,220 @@ [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) ## Purpose -Den2Obj is a command-line tool that construct isosurfaces from densely packed -scalar fields. Den2Obj supports VASP charge files such as CHGCAR and PARCHG, -Gaussian .cube files as well as its own .d2o format. + +Den2Obj is a command-line tool for computational chemists and materials +scientists who want to visualize volumetric scalar fields from ab initio +calculations as 3D isosurface meshes. + +It reads scalar field data produced by [VASP](https://www.vasp.at/) (CHGCAR, +PARCHG, LOCPOT) and [Gaussian](https://gaussian.com/) (`.cub`) and extracts +isosurfaces using the marching cubes or marching tetrahedra algorithm. The +resulting surfaces — representing quantities such as electron density, partial +charge density, or electrostatic potential — are written to standard 3D mesh +formats (`.obj`, `.ply`) that can be directly imported into tools like Blender +or MeshLab for high-quality rendering. + +Den2Obj also supports format conversion: VASP and Gaussian files can be +converted to the native `.d2o` compressed binary format or to OpenVDB (`.vdb`) +for volumetric rendering workflows. When writing `.d2o`, Den2Obj benchmarks all +supported compression algorithms (gzip, lzma, bzip2) and automatically selects +the one that produces the smallest file, so no manual tuning is needed. ## Example images + ![Canonical valence orbitals of CH4](docs/_static/img/ch4_valence_orbitals.png) +## Quick start + +No VASP or Gaussian files needed. Den2Obj ships with built-in datasets so you +can try it immediately after compiling: + +```bash +# Generate a built-in scalar field and extract an isosurface from it +./den2obj -g genus2 -o genus2.d2o +./den2obj -i genus2.d2o -o genus2.obj -v 0.5 +``` + +The first command writes the `genus2` dataset to a `.d2o` file. The second +extracts an isosurface at isovalue 0.5 and writes a Wavefront `.obj` mesh that +can be opened directly in Blender or MeshLab. See +[Built-in datasets](#built-in-dataset-generation) for the full list. + ## Compilation instructions -### Debian Latest +### Debian-based systems (Ubuntu, Debian) -Getting the dependencies +Tested on Ubuntu 24.04 LTS (Noble) and Debian 12 (Bookworm). Install the +dependencies: ```bash -sudo apt install build-essential cmake libtclap-dev libboost-all-dev libopenvdb-dev libtbb-dev \ -pkg-config libcppunit-dev libeigen3-dev liblzma-dev zlib1g-dev libbz2-dev libssl-dev +sudo apt install build-essential cmake libtclap-dev libboost-all-dev \ +libopenvdb-dev libtbb-dev pkg-config libcppunit-dev libeigen3-dev liblzma-dev \ +zlib1g-dev libbz2-dev libssl-dev ``` -To compile, run the following commands: +Compile: ```bash git clone https://github.com/ifilot/den2obj.git cd den2obj -mkdir build -cd build +mkdir build && cd build cmake -DMOD_OPENVDB=1 ../src make -j5 ``` -### Ubuntu Latest -The stable OpenVDB library (`libopenvdb`) under Ubuntu is incompatible with the -Threading Building Blocks (`libtbb`) library. To solve this, manually compile -and install OpenVDB 8.2 using the following instructions. +## Usage -Getting the dependencies +### Isosurfaces + +Reads a scalar field from `` and extracts a surface at the given +isovalue, writing a 3D mesh to ``. The output format (`.obj` or +`.ply`) is determined by the file extension. ```bash -sudo apt install build-essential cmake libtclap-dev libboost-all-dev libopenvdb-dev libtbb-dev \ -pkg-config libcppunit-dev libeigen3-dev liblzma-dev zlib1g-dev libbz2-dev libssl-dev +den2obj -i -o -v ``` -Next download and install OpenVDB. +Example — extract an isosurface at density 0.1 from a VASP charge file: ```bash -wget https://github.com/AcademySoftwareFoundation/openvdb/archive/refs/tags/v8.2.0.tar.gz -tar -xvzf v8.2.0.tar.gz -mkdir openvdb-build && cd openvdb-build && cmake ../openvdb-8.2.0 -DCMAKE_INSTALL_PREFIX=/opt/openvdb -make -j9 && sudo make install +./den2obj -i CHGCAR -o orbital.obj -v 0.1 ``` -Thereafter, clone, configure and compile Den2Obj and link against OpenVDB 8.2. +> **Choosing an isovalue:** Den2Obj prints the minimum and maximum value of the +> scalar field on startup. Use these as a guide to pick a meaningful isovalue +> for your data. + +### Centering the structure + +By default the mesh is placed in the coordinate system of the original unit +cell. Use `-c` to shift the structure so its center lies at the origin, which +is convenient when importing into rendering software: ```bash -git clone https://github.com/ifilot/den2obj.git -cd den2obj -mkdir build -cd build -cmake -DMOD_OPENVDB=1 ../src -make -j5 +./den2obj -i CHGCAR -o orbital.obj -v 0.1 -c ``` -## Usage +### Dual isosurfaces (positive and negative lobes) -### Isosurfaces +Useful for visualizing molecular orbitals, which have both a positive and a +negative lobe. The `-d` flag runs the extraction twice — once at `+isovalue` +and once at `-isovalue` — and writes the results to two separate files with +`_pos` and `_neg` suffixes: -``` -/den2obj -i CHGCAR -o -v +```bash +./den2obj -i CHGCAR -o orbital.obj -v 0.1 -d +# produces orbital_pos.obj and orbital_neg.obj ``` -Example: -``` -./den2obj -i CHGCAR -o orbital.obj -v 0.1 -``` +### Isosurface algorithm -## Options +Use `-a` to select the triangulation algorithm. `marching-cubes` (default) is +faster; `marching-tetrahedra` subdivides each cube into tetrahedra and can +produce smoother surfaces at the cost of more triangles: -* `-c`: Center the structure, i.e. the center of the structure is placed at the origin of the coordinate system. -* `-t`: Converts one file format to another. File formats are auto-recognized based on the extensions. +```bash +./den2obj -i CHGCAR -o orbital.obj -v 0.1 -a marching-cubes +./den2obj -i CHGCAR -o orbital.obj -v 0.1 -a marching-tetrahedra +``` ### Conversions -Converting CHGCAR to OpenVDB +Use `-t` to convert between file formats without generating an isosurface. +The output format is inferred from the output file extension. + +Converting CHGCAR to D2O: + +```bash +./den2obj -i CHGCAR_xxx -o xxx.d2o -t ``` + +Converting CHGCAR to OpenVDB (requires OpenVDB build): + +```bash ./den2obj -i CHGCAR_xxx -o xxx.vdb -t ``` -Converting CHGCAR to D2O +Specifying a compression algorithm for D2O output: + +```bash +./den2obj -i CHGCAR_xxx -o xxx.d2o -t -a lzma ``` -./den2obj -i CHGCAR_xxx -o xxx.d2o -t + +Available compression algorithms: `auto` (default), `gzip`, `lzma`, `bzip2`. + +### Built-in dataset generation + +Den2Obj can generate built-in scalar field datasets without any external input +files, useful for testing and demonstrations. Use `-g` to select the dataset; +the output is always a `.d2o` file: + +```bash +./den2obj -g genus2 -o genus2.d2o +./den2obj -g benzene_homo -o benzene_homo.d2o +./den2obj -g benzene_lumo -o benzene_lumo.d2o ``` +| Dataset | Description | +|---------|-------------| +| `genus2` | Mathematical genus-2 surface | +| `benzene_homo` | HOMO of benzene (STO-3G) | +| `benzene_lumo` | LUMO of benzene (STO-3G) | + +## Options reference + +| Flag | Long form | Description | +|------|-----------|-------------| +| `-i` | `--input` | Input file (e.g. `CHGCAR`, `file.cub`, `file.d2o`) | +| `-o` | `--filename` | Output file | +| `-v` | `--isovalue` | Isovalue for isosurface generation | +| `-c` | `--center` | Center the structure at the origin | +| `-d` | `--dual` | Produce both positive and negative isosurfaces | +| `-t` | `--transform` | Convert input file to a different format (no isosurface) | +| `-a` | `--algo` | Algorithm for isosurface (`marching-cubes`, `marching-tetrahedra`) or compression for D2O (`auto`, `gzip`, `lzma`, `bzip2`) | +| `-g` | `--dataset` | Generate a built-in test dataset | + **Supported input types:** * CHGCAR * PARCHG * LOCPOT -* Gaussian cube (.cub) -* D2O files (.d2o) +* Gaussian cube (`.cub`) +* D2O files (`.d2o`) **Supported dense output types:** * D2O * OpenVDB -**Supported isosurface object types:** +**Supported isosurface output types:** * [Stanford .ply file](https://en.wikipedia.org/wiki/PLY_(file_format)) * [Wavefront .obj file](https://en.wikipedia.org/wiki/Wavefront_.obj_file) ## D2O file format -The D2O file format is native to `Den2Obj`. This file format stores the scalarfield -in binary format and uses compression to generate small files which are fast to -read from. More information on the file format can be found in the +The D2O file format is native to Den2Obj. It stores the scalar field in binary +format with compression, producing small files that load quickly. More +information can be found in the [documentation](https://ifilot.github.io/den2obj/). ## Shared library -`Den2Obj` can also be used as a shared library in your own code. See the -[examples/shared](examples/shared) for an example. \ No newline at end of file +Den2Obj can also be used as a shared library in your own code. See +[examples/shared](examples/shared) for a usage example. + +## Running tests + +To build and run the unit tests: + +```bash +git clone https://github.com/ifilot/den2obj.git +cd den2obj +mkdir build && cd build +cmake ../src +make -j5 +./src/test/unittest +``` + +## License + +Den2Obj is distributed under the [GNU General Public License v3](LICENSE). diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 68c202b..1632507 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -19,7 +19,7 @@ #*************************************************************************/ cmake_minimum_required(VERSION 3.16) -project(den2obj VERSION 1.2.2 LANGUAGES CXX) +project(den2obj VERSION 1.3.0 LANGUAGES CXX) option(USE_GCOV "Build with gcov coverage instrumentation" OFF) option(MOD_OPENVDB "Enable OpenVDB support" OFF) From eb583a5e8096e9d1860b48fe5eeeb7895b51beb0 Mon Sep 17 00:00:00 2001 From: ifilot Date: Thu, 21 May 2026 09:25:48 +0200 Subject: [PATCH 4/6] Adding additional compression methods and polishing I/O --- README.md | 21 ++- docker/Dockerfile | 4 +- docs/d2o_fileformat.rst | 7 +- docs/faq.rst | 4 +- docs/installation.rst | 7 +- docs/tutorial.rst | 4 +- docs/user_interface.rst | 6 +- examples/README.md | 10 +- examples/shared/CMakeLists.txt | 8 +- src/CMakeLists.txt | 6 +- src/cli_format.cpp | 266 +++++++++++++++++++++++++++ src/cli_format.h | 161 ++++++++++++++++ src/d2o_format.cpp | 303 +++++++++++++++++++++++++------ src/d2o_format.h | 30 ++- src/den2obj.cpp | 28 +-- src/generator.cpp | 13 +- src/generator.h | 1 - src/scalar_field.cpp | 42 +---- src/scalar_field.h | 2 +- src/test/test_d2o_fileformat.cpp | 67 +++---- src/test/test_d2o_fileformat.h | 9 +- src/test/test_file_creation.cpp | 2 +- 22 files changed, 815 insertions(+), 186 deletions(-) create mode 100644 src/cli_format.cpp create mode 100644 src/cli_format.h diff --git a/README.md b/README.md index 8b163ff..ff23af1 100644 --- a/README.md +++ b/README.md @@ -18,14 +18,15 @@ PARCHG, LOCPOT) and [Gaussian](https://gaussian.com/) (`.cub`) and extracts isosurfaces using the marching cubes or marching tetrahedra algorithm. The resulting surfaces — representing quantities such as electron density, partial charge density, or electrostatic potential — are written to standard 3D mesh -formats (`.obj`, `.ply`) that can be directly imported into tools like Blender -or MeshLab for high-quality rendering. +formats (`.obj`, `.ply`, `.stl`) that can be directly imported into tools like +Blender or MeshLab for high-quality rendering. Den2Obj also supports format conversion: VASP and Gaussian files can be converted to the native `.d2o` compressed binary format or to OpenVDB (`.vdb`) for volumetric rendering workflows. When writing `.d2o`, Den2Obj benchmarks all -supported compression algorithms (gzip, lzma, bzip2) and automatically selects -the one that produces the smallest file, so no manual tuning is needed. +supported compression algorithms (gzip, lzma, bzip2, zstd, blosc) and +automatically selects the one that produces the smallest file, so no manual +tuning is needed. ## Example images @@ -57,7 +58,7 @@ dependencies: ```bash sudo apt install build-essential cmake libtclap-dev libboost-all-dev \ libopenvdb-dev libtbb-dev pkg-config libcppunit-dev libeigen3-dev liblzma-dev \ -zlib1g-dev libbz2-dev libssl-dev +zlib1g-dev libbz2-dev libzstd-dev libblosc-dev libssl-dev ``` Compile: @@ -76,8 +77,8 @@ make -j5 ### Isosurfaces Reads a scalar field from `` and extracts a surface at the given -isovalue, writing a 3D mesh to ``. The output format (`.obj` or -`.ply`) is determined by the file extension. +isovalue, writing a 3D mesh to ``. The output format (`.obj`, +`.ply`, or `.stl`) is determined by the file extension. ```bash den2obj -i -o -v @@ -149,7 +150,8 @@ Specifying a compression algorithm for D2O output: ./den2obj -i CHGCAR_xxx -o xxx.d2o -t -a lzma ``` -Available compression algorithms: `auto` (default), `gzip`, `lzma`, `bzip2`. +Available compression algorithms: `auto` (default), `gzip`, `lzma`, `bzip2`, +`zstd`, `blosc`. ### Built-in dataset generation @@ -179,7 +181,7 @@ the output is always a `.d2o` file: | `-c` | `--center` | Center the structure at the origin | | `-d` | `--dual` | Produce both positive and negative isosurfaces | | `-t` | `--transform` | Convert input file to a different format (no isosurface) | -| `-a` | `--algo` | Algorithm for isosurface (`marching-cubes`, `marching-tetrahedra`) or compression for D2O (`auto`, `gzip`, `lzma`, `bzip2`) | +| `-a` | `--algo` | Algorithm for isosurface (`marching-cubes`, `marching-tetrahedra`) or compression for D2O (`auto`, `gzip`, `lzma`, `bzip2`, `zstd`, `blosc`) | | `-g` | `--dataset` | Generate a built-in test dataset | **Supported input types:** @@ -195,6 +197,7 @@ the output is always a `.d2o` file: **Supported isosurface output types:** * [Stanford .ply file](https://en.wikipedia.org/wiki/PLY_(file_format)) +* [Stereolithography .stl file](https://en.wikipedia.org/wiki/STL_(file_format)) * [Wavefront .obj file](https://en.wikipedia.org/wiki/Wavefront_.obj_file) ## D2O file format diff --git a/docker/Dockerfile b/docker/Dockerfile index e289fcd..e89a384 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -31,6 +31,8 @@ RUN apt-get update && apt-get install -y \ liblzma-dev \ zlib1g-dev \ libbz2-dev \ + libzstd-dev \ + libblosc-dev \ libssl-dev RUN apt-get install curl wget @@ -51,4 +53,4 @@ RUN ldconfig # -------- Cleanup -------- RUN apt-get clean -CMD ["g++", "--version"] \ No newline at end of file +CMD ["g++", "--version"] diff --git a/docs/d2o_fileformat.rst b/docs/d2o_fileformat.rst index 65cf8c6..9707d73 100644 --- a/docs/d2o_fileformat.rst +++ b/docs/d2o_fileformat.rst @@ -28,7 +28,7 @@ The organization of this file is given in the Table 1. * - 0x03-0x07 - uint32_t - 4 bytes - - Protocol identifier token (see below). Default = 2. + - Protocol identifier token (see below). Selected automatically by default. * - 0x08-0x2B - float[9] - 36 bytes @@ -57,7 +57,8 @@ To convert a ``CHGCAR`` file to ``.d2o`` file format, run the following command: .. note:: :program:`Den2Obj` will automatically look for the best compression algorithm when converting a scalar field to the ``.d2o`` format. For the majority of the - cases, this corresponds to the `LZMA type of compression `_. + cases, this corresponds to either the `LZMA type of compression `_ + or one of the newer array-oriented compression methods. Protocol tokens --------------- @@ -65,3 +66,5 @@ Protocol tokens 1. GZIP compression 2. LZMA compression 3. BZIP2 compression +4. ZSTD compression +5. Blosc compression diff --git a/docs/faq.rst b/docs/faq.rst index 2bf16e3..0a38d5c 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -25,9 +25,9 @@ If the decompressed stream is different from the original stream, this error is thrown automatically. Although it is very rare for this error to be thrown, we have seen it happening for LZMA compression. The recommended work-around is to use a different compression routine, -such as ``gzip`` or ``bzip2``. +such as ``gzip``, ``bzip2``, ``zstd`` or ``blosc``. .. seealso:: :doc:`user_interface` - Common errors and solutions for build failures. \ No newline at end of file + Common errors and solutions for build failures. diff --git a/docs/installation.rst b/docs/installation.rst index b2f011d..fe8732a 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -20,6 +20,8 @@ libraries are available to you: * `BZIP2 `_ (bzip2 data compression) * `GZIP `_ (gzip data compression) * `LZMA `_ (lzma data compression) +* `Zstandard `_ (zstd data compression) +* `Blosc `_ (blocked and shuffled data compression) * `CPPUnit `_ (unit testing) .. note:: @@ -32,7 +34,8 @@ libraries are available to you: To ensure that all the packages are installed, one can run the following:: sudo apt install build-essential cmake libtclap-dev libboost-all-dev \ - pkg-config libcppunit-dev libeigen3-dev liblzma-dev zlib1g-dev libbz2-dev + pkg-config libcppunit-dev libeigen3-dev liblzma-dev zlib1g-dev libbz2-dev \ + libzstd-dev libblosc-dev Standard compilation -------------------- @@ -74,7 +77,7 @@ Ensure that the following libraries are installed by running:: sudo apt install build-essential cmake libtclap-dev libboost-all-dev \ libopenvdb-dev libtbb-dev pkg-config libcppunit-dev libeigen3-dev \ - liblzma-dev zlib1g-dev libbz2-dev + liblzma-dev zlib1g-dev libbz2-dev libzstd-dev libblosc-dev Compilation :program:`Den2Obj` with the OpenVDB module is as follows:: diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 50e059c..1ece79b 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -56,8 +56,8 @@ You should see the following output:: Done in 1.42421 seconds. This will generate a :ref:`.d2o file ` containing the Genus 2 -scalar field named ``genus2.d2o``. Observe that :program:`Den2Obj` tests three -different compression algorithms and automatically selects the best algorithm +scalar field named ``genus2.d2o``. Observe that :program:`Den2Obj` tests the +available compression algorithms and automatically selects the best algorithm for the data compression. To construct the isosurface with an isovalue of 0.1, run:: diff --git a/docs/user_interface.rst b/docs/user_interface.rst index cc98165..f0eb2c8 100644 --- a/docs/user_interface.rst +++ b/docs/user_interface.rst @@ -38,7 +38,7 @@ of the following types ``mesh-file`` is any of the following supported formats -* `Stereolitography (.stl) file `_ +* `Stereolithography (.stl) file `_ * `Stanford (.ply) file `_ * `Wavefront (.obj) file `_ @@ -94,6 +94,8 @@ With the optional ``-a `` tag, the compression algorithm can be selected. * ``lzma`` * ``bzip2`` * ``gzip`` +* ``zstd`` +* ``blosc`` When ``auto`` is selected or when no ``-a`` directive is provided, automatically the best compression algorithm is taken by checking all possible compressions. @@ -119,4 +121,4 @@ checking the inflation ratio of all possible compression algorithms. The following datasets are available: * ``genus2`` * ``benzene_homo`` -* ``benzene_lumo`` \ No newline at end of file +* ``benzene_lumo`` diff --git a/examples/README.md b/examples/README.md index 90a8ce1..c1719b7 100644 --- a/examples/README.md +++ b/examples/README.md @@ -12,6 +12,8 @@ link against `libden2obj.so` as well as against a number of required libraries: * GZIP * LZMA * BZ2 +* ZSTD +* Blosc Besides these libraries, there is also a header-only dependency on the Eigen3 library. @@ -47,6 +49,8 @@ find_package(ZLIB REQUIRED) find_package(BZip2 REQUIRED) pkg_check_modules(DEN2OBJ den2obj REQUIRED) pkg_check_modules(EIGEN eigen3 REQUIRED) +pkg_check_modules(ZSTD libzstd REQUIRED) +pkg_check_modules(BLOSC blosc REQUIRED) ``` and finally add the required libraries to your executable @@ -57,7 +61,9 @@ target_link_libraries(den2obj-shared-example ${Boost_LIBRARIES} ${LIBLZMA_LIBRARIES} ${ZLIB_LIBRARIES} - ${BZIP2_LIBRARIES}) + ${BZIP2_LIBRARIES} + ${ZSTD_LIBRARIES} + ${BLOSC_LIBRARIES}) ``` An example of this is provided in [shared/CMakeLists.txt](shared/CMakeLists.txt). @@ -68,4 +74,4 @@ An example of this is provided in [shared/CMakeLists.txt](shared/CMakeLists.txt) mkdir build && cd build cmake ../shared make -j -``` \ No newline at end of file +``` diff --git a/examples/shared/CMakeLists.txt b/examples/shared/CMakeLists.txt index 17d79de..933b0ea 100644 --- a/examples/shared/CMakeLists.txt +++ b/examples/shared/CMakeLists.txt @@ -60,12 +60,16 @@ find_package(ZLIB REQUIRED) find_package(BZip2 REQUIRED) pkg_check_modules(DEN2OBJ den2obj REQUIRED) pkg_check_modules(EIGEN eigen3 REQUIRED) +pkg_check_modules(ZSTD libzstd REQUIRED) +pkg_check_modules(BLOSC blosc REQUIRED) # Set include folders include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_BINARY_DIR} ${EIGEN_INCLUDE_DIRS} ${DEN2OBJ_INCLUDE_DIR} + ${ZSTD_INCLUDE_DIRS} + ${BLOSC_INCLUDE_DIRS} ${Boost_INCLUDE_DIRS}) # Add sources @@ -81,4 +85,6 @@ target_link_libraries(den2obj-shared-example ${Boost_LIBRARIES} ${LIBLZMA_LIBRARIES} ${ZLIB_LIBRARIES} - ${BZIP2_LIBRARIES}) \ No newline at end of file + ${BZIP2_LIBRARIES} + ${ZSTD_LIBRARIES} + ${BLOSC_LIBRARIES}) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1632507..2fc8de4 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -56,6 +56,8 @@ find_package(ZLIB REQUIRED) find_package(BZip2 REQUIRED) pkg_check_modules(TCLAP REQUIRED IMPORTED_TARGET tclap) pkg_check_modules(EIGEN REQUIRED IMPORTED_TARGET eigen3) +pkg_check_modules(ZSTD REQUIRED IMPORTED_TARGET libzstd) +pkg_check_modules(BLOSC REQUIRED IMPORTED_TARGET blosc) if(HAS_OPENMP) find_package(OpenMP) @@ -63,7 +65,7 @@ endif() set(OPENVDB_LIBS "") if(MOD_OPENVDB) - set(OPENVDB_LIBS openvdb tbb blosc ZLIB::ZLIB) + set(OPENVDB_LIBS openvdb tbb PkgConfig::BLOSC ZLIB::ZLIB) message(STATUS "Compiling with OpenVDB module") endif() @@ -113,6 +115,8 @@ target_link_libraries(den2objsources LibLZMA::LibLZMA ZLIB::ZLIB BZip2::BZip2 + PkgConfig::ZSTD + PkgConfig::BLOSC ${OPENVDB_LIBS} ) diff --git a/src/cli_format.cpp b/src/cli_format.cpp new file mode 100644 index 0000000..23c5458 --- /dev/null +++ b/src/cli_format.cpp @@ -0,0 +1,266 @@ +/************************************************************************** + * * + * Author: Ivo Filot * + * * + * DEN2OBJ is free software: * + * you can redistribute it and/or modify it under the terms of the * + * GNU General Public License as published by the Free Software * + * Foundation, either version 3 of the License, or (at your option) * + * any later version. * + * * + * DEN2OBJ is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty * + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * + * See the GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see http://www.gnu.org/licenses/. * + * * + **************************************************************************/ + +#include "cli_format.h" + +#include +#include +#include +#include +#include + +namespace { + /** + * @brief Pad a string on the right with spaces. + * + * @param[in] value Value to pad + * @param[in] width Minimum output width + * + * @return Padded string + */ + std::string pad_right(const std::string& value, size_t width) { + if(value.size() >= width) { + return value; + } + + return value + std::string(width - value.size(), ' '); + } + + /** + * @brief Get the ANSI escape sequence for a color or style. + * + * @param[in] color Color or style + * + * @return ANSI escape sequence + */ + std::string ansi_code(CLIFormat::Color color) { + switch(color) { + case CLIFormat::Color::BOLD: + return "\033[1m"; + case CLIFormat::Color::DIM: + return "\033[2m"; + case CLIFormat::Color::CYAN: + return "\033[36m"; + case CLIFormat::Color::GREEN: + return "\033[32m"; + case CLIFormat::Color::YELLOW: + return "\033[33m"; + case CLIFormat::Color::DEFAULT: + default: + return "\033[0m"; + } + } +} + +/** + * @brief Format a byte count as kibibytes. + * + * @param[in] bytes Number of bytes + * + * @return Formatted size string + */ +std::string CLIFormat::format_kb(size_t bytes) { + std::ostringstream oss; + oss << std::fixed << std::setprecision(1) << (static_cast(bytes) / 1024.0) << " kb"; + return oss.str(); +} + +/** + * @brief Format an elapsed time in seconds. + * + * @param[in] seconds Elapsed time in seconds + * + * @return Formatted elapsed time string + */ +std::string CLIFormat::format_seconds(double seconds) { + std::ostringstream oss; + oss << std::fixed << std::setprecision(2) << seconds << " s"; + return oss.str(); +} + +/** + * @brief Check whether standard output is attached to a terminal. + * + * @return Whether standard output is a terminal + */ +bool CLIFormat::output_is_terminal() { + return isatty(STDOUT_FILENO) != 0; +} + +/** + * @brief Check whether colored output should be emitted. + * + * @return Whether ANSI color output is enabled + */ +bool CLIFormat::color_enabled() { + return CLIFormat::output_is_terminal() && std::getenv("NO_COLOR") == nullptr; +} + +/** + * @brief Wrap a string in ANSI color/style codes when enabled. + * + * @param[in] value Text to decorate + * @param[in] color Color or style to apply + * + * @return Decorated text, or the original text when color is disabled + */ +std::string CLIFormat::colorize(const std::string& value, Color color) { + if(!CLIFormat::color_enabled()) { + return value; + } + + return ansi_code(color) + value + ansi_code(Color::DEFAULT); +} + +/** + * @brief Print the program banner. + * + * @param[in] version Program version + * @param[in] git_hash Git commit hash + * @param[in] build_date Compilation date + * @param[in] build_time Compilation time + * @param[in] openvdb_enabled Whether OpenVDB support is enabled + */ +void CLIFormat::print_banner(const std::string& version, + const std::string& git_hash, + const std::string& build_date, + const std::string& build_time, + bool openvdb_enabled) { + static const size_t width = 48; + const std::string rule = "+" + std::string(width, '-') + "+"; + + std::cout << colorize(rule, Color::CYAN) << std::endl; + std::cout << colorize("| " + pad_right("DEN2OBJ " + version, width - 2) + " |", Color::BOLD) << std::endl; + std::cout << "| " << pad_right("Density fields -> meshes and volumes", width - 2) << " |" << std::endl; + std::cout << colorize(rule, Color::CYAN) << std::endl; + print_kv("Author", "Ivo Filot "); + print_kv("Website", "https://den2obj.imc-tue.nl"); + print_kv("GitHub", "https://github.com/ifilot/den2obj"); + print_kv("Build", build_date + " " + build_time); + print_kv("Git", git_hash); + if(openvdb_enabled) { + print_kv("Modules", "OpenVDB"); + } + std::cout << std::endl; +} + +/** + * @brief Print a section heading. + * + * @param[in] title Section title + */ +void CLIFormat::print_section(const std::string& title) { + std::cout << std::endl << colorize("== " + title + " ==", Color::CYAN) << std::endl; +} + +/** + * @brief Print a left-aligned key-value line. + * + * @param[in] key Field name + * @param[in] value Field value + * @param out Output stream + */ +void CLIFormat::print_kv(const std::string& key, + const std::string& value, + std::ostream& out) { + out << colorize(pad_right(key + ":", 14), Color::DIM) << value << std::endl; +} + +/** + * @brief Print the final completion summary. + * + * @param[in] elapsed Formatted elapsed time + */ +void CLIFormat::print_done_summary(const std::string& elapsed) { + print_section("Done"); + print_kv("Elapsed", elapsed); + std::cout << std::endl; +} + +/** + * @brief Construct a progress bar. + * + * @param[in] label Progress label + * @param[in] total Total number of ticks + */ +CLIFormat::ProgressBar::ProgressBar(const std::string& label, size_t total) : + label(label), + total(total), + current(0), + last_percent(static_cast(-1)), + terminal(CLIFormat::output_is_terminal()) { + if(this->terminal) { + this->render(true); + } +} + +/** + * @brief Destroy the progress bar and finish its line. + */ +CLIFormat::ProgressBar::~ProgressBar() { + if(this->current < this->total) { + this->current = this->total; + } + if(!this->terminal || this->last_percent != 100) { + this->render(true); + } + std::cout << std::endl; +} + +/** + * @brief Advance the progress bar by one tick. + */ +void CLIFormat::ProgressBar::tick() { + if(this->current < this->total) { + this->current++; + } + this->render(false); +} + +/** + * @brief Render the progress bar. + * + * @param[in] force Whether to render even if percentage did not change + */ +void CLIFormat::ProgressBar::render(bool force) { + static const size_t width = 40; + const size_t percent = this->total == 0 ? 100 : (this->current * 100) / this->total; + + if(!force && percent == this->last_percent) { + return; + } + + if(!this->terminal && (percent != 100 || !force)) { + return; + } + + const size_t filled = std::min(width, (percent * width) / 100); + const std::string bar = CLIFormat::colorize(std::string(filled, '#'), Color::GREEN) + + std::string(width - filled, '-'); + + if(this->terminal) { + std::cout << "\r"; + } + + std::cout << this->label << " [" << bar << "] " + << std::right << std::setw(3) << percent << "%" << std::flush; + + this->last_percent = percent; +} diff --git a/src/cli_format.h b/src/cli_format.h new file mode 100644 index 0000000..6c4f34a --- /dev/null +++ b/src/cli_format.h @@ -0,0 +1,161 @@ +/************************************************************************** + * * + * Author: Ivo Filot * + * * + * DEN2OBJ is free software: * + * you can redistribute it and/or modify it under the terms of the * + * GNU General Public License as published by the Free Software * + * Foundation, either version 3 of the License, or (at your option) * + * any later version. * + * * + * DEN2OBJ is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty * + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * + * See the GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see http://www.gnu.org/licenses/. * + * * + **************************************************************************/ + +#ifndef _CLI_FORMAT +#define _CLI_FORMAT + +#include +#include +#include + +namespace CLIFormat { + + enum class Color { + DEFAULT, + BOLD, + DIM, + CYAN, + GREEN, + YELLOW + }; + + /** + * @brief Format a byte count as kibibytes. + * + * @param[in] bytes Number of bytes + * + * @return Formatted size string + */ + std::string format_kb(size_t bytes); + + /** + * @brief Format an elapsed time in seconds. + * + * @param[in] seconds Elapsed time in seconds + * + * @return Formatted elapsed time string + */ + std::string format_seconds(double seconds); + + /** + * @brief Check whether standard output is attached to a terminal. + * + * @return Whether standard output is a terminal + */ + bool output_is_terminal(); + + /** + * @brief Check whether colored output should be emitted. + * + * @return Whether ANSI color output is enabled + */ + bool color_enabled(); + + /** + * @brief Wrap a string in ANSI color/style codes when enabled. + * + * @param[in] value Text to decorate + * @param[in] color Color or style to apply + * + * @return Decorated text, or the original text when color is disabled + */ + std::string colorize(const std::string& value, Color color); + + /** + * @brief Print the program banner. + * + * @param[in] version Program version + * @param[in] git_hash Git commit hash + * @param[in] build_date Compilation date + * @param[in] build_time Compilation time + * @param[in] openvdb_enabled Whether OpenVDB support is enabled + */ + void print_banner(const std::string& version, + const std::string& git_hash, + const std::string& build_date, + const std::string& build_time, + bool openvdb_enabled); + + /** + * @brief Print a section heading. + * + * @param[in] title Section title + */ + void print_section(const std::string& title); + + /** + * @brief Print a left-aligned key-value line. + * + * @param[in] key Field name + * @param[in] value Field value + * @param out Output stream + */ + void print_kv(const std::string& key, + const std::string& value, + std::ostream& out = std::cout); + + /** + * @brief Print the final completion summary. + * + * @param[in] elapsed Formatted elapsed time + */ + void print_done_summary(const std::string& elapsed); + + /** + * @brief Lightweight terminal-aware progress bar. + */ + class ProgressBar { + public: + /** + * @brief Construct a progress bar. + * + * @param[in] label Progress label + * @param[in] total Total number of ticks + */ + ProgressBar(const std::string& label, size_t total); + + /** + * @brief Destroy the progress bar and finish its line. + */ + ~ProgressBar(); + + /** + * @brief Advance the progress bar by one tick. + */ + void tick(); + + private: + /** + * @brief Render the progress bar. + * + * @param[in] force Whether to render even if percentage did not change + */ + void render(bool force); + + std::string label; + size_t total; + size_t current; + size_t last_percent; + bool terminal; + }; + +} + +#endif diff --git a/src/d2o_format.cpp b/src/d2o_format.cpp index 8d2084f..b25430e 100644 --- a/src/d2o_format.cpp +++ b/src/d2o_format.cpp @@ -19,6 +19,55 @@ **************************************************************************/ #include "d2o_format.h" +#include "cli_format.h" + +#include +#include +#include + +namespace { + std::vector> ordered_compression_algos() { + return { + {D2OFormat::CompressionAlgo::GZIP, "gzip"}, + {D2OFormat::CompressionAlgo::LZMA, "lzma"}, + {D2OFormat::CompressionAlgo::BZIP2, "bzip2"}, + {D2OFormat::CompressionAlgo::ZSTD, "zstd"}, + {D2OFormat::CompressionAlgo::BLOSC, "blosc"} + }; + } + + void print_compression_table( + const std::unordered_map>& compressed_strings, + D2OFormat::CompressionAlgo selected_algo, + size_t original_size) { + CLIFormat::print_section("Compression benchmark"); + std::cout << "+--------+-----------+--------+----------+" << std::endl; + std::cout << "| Method | Size | Ratio | Status |" << std::endl; + std::cout << "+--------+-----------+--------+----------+" << std::endl; + + for(const auto& algo : ordered_compression_algos()) { + const auto got = compressed_strings.find(algo.first); + if(got == compressed_strings.end()) { + continue; + } + + const double ratio = static_cast(got->second.size()) / + static_cast(original_size) * 100.0; + + const std::string status = algo.first == selected_algo ? "best" : ""; + const std::string padded_status = status + std::string(8 - status.size(), ' '); + + std::cout << "| " << std::left << std::setw(6) << algo.second + << " | " << std::right << std::setw(9) << CLIFormat::format_kb(got->second.size()) + << " | " << std::setw(5) << (boost::format("%0.2f") % ratio).str() << "%" + << " | " << CLIFormat::colorize(padded_status, CLIFormat::Color::GREEN) + << " |" << std::endl; + } + + std::cout << "+--------+-----------+--------+----------+" << std::endl; + CLIFormat::print_kv("Selected", D2OFormat::compression_algo_name(selected_algo)); + } +} /** * @brief Write a D2O file @@ -39,7 +88,8 @@ void D2OFormat::write_d2o_file(const std::string& filename, char buf[] = "D2O"; outfile.write(buf, 3); - std::cout << "Writing .D2O file, using compression algo id: " << (int)algo_id << std::endl; + CLIFormat::print_section("Writing D2O file"); + CLIFormat::print_kv("Compression", D2OFormat::compression_algo_name(algo_id)); // copy data to char array size_t gridptrsz = gridptr.size() * sizeof(fpt); @@ -49,12 +99,9 @@ void D2OFormat::write_d2o_file(const std::string& filename, std::string compressedstr; if(algo_id == D2OFormat::CompressionAlgo::AUTO) { // check which compression algo works best - // auto-look for best compression algo - std::cout << "Looking for best compression algorithm." << std::endl; - // determine best compression format auto compstr = d2o_compress_all(originstr); - std::unordered_map> stringsizes; + std::unordered_map> stringsizes; // determine size for each type of compression for(const auto& i : compstr) { @@ -67,10 +114,9 @@ void D2OFormat::write_d2o_file(const std::string& filename, // overwrite algo_id algo_id = got->first; - // std::cout << "Best algorithm: " << D2OFormat::algos[idx] << std::endl; + print_compression_table(compstr, algo_id, originstr.size()); } else { // use the user-supplied compression algo compressedstr = D2OFormat::compress_stream(originstr, algo_id); - // std::cout << "Overruling compression algo to: " << D2OFormat::algos[idx] << std::endl; } // verify that the compressed stream can be correctly decompressed @@ -78,7 +124,7 @@ void D2OFormat::write_d2o_file(const std::string& filename, if(!valid_decompression) { throw std::runtime_error("Decompression could not be verified. Please try again or force to use a different compression algorithm."); } else { - std::cout << "Decompression verification passed." << std::endl; + CLIFormat::print_kv("Verification", "passed"); } // capture compressed data @@ -104,7 +150,7 @@ void D2OFormat::write_d2o_file(const std::string& filename, // write floating point size uint8_t fptsz = sizeof(fpt); outfile.write((char*)&fptsz, sizeof(uint8_t)); - std::cout << "Floating point size determined at: " << (int)fptsz << " bytes" << std::endl; + CLIFormat::print_kv("Float size", std::to_string((int)fptsz) + " bytes"); // write compressed data size and compressed data uint64_t sz = compressedstr.size(); @@ -118,31 +164,18 @@ void D2OFormat::write_d2o_file(const std::string& filename, outfile.close(); std::uintmax_t size = boost::filesystem::file_size(filename); - std::cout << "Writing " << filename << " (" - << (boost::format("%0.1f") % ((float)size / 1024.f)).str() - << "kb)." << std::endl; + CLIFormat::print_kv("Output", filename); + CLIFormat::print_kv("File size", CLIFormat::format_kb(size)); } std::unordered_map> D2OFormat::d2o_compress_all(const std::string& originstr) { std::unordered_map> compressed_strings; - for(const auto& i : D2OFormat::algos) { - // skip auto tag - if(i.second == D2OFormat::CompressionAlgo::AUTO) { - continue; - } - - std::cout << "Trying " << i.first << ": "; - - const std::string compressedstr = D2OFormat::compress_stream(originstr, i.second); - compressed_strings.emplace(i.second, compressedstr); + for(const auto& i : ordered_compression_algos()) { + const std::string compressedstr = D2OFormat::compress_stream(originstr, i.first); + compressed_strings.emplace(i.first, compressedstr); size_t sz = compressedstr.size(); - std::cout << (boost::format("%0.1f") % ((float)sz / 1024.f)).str() - << " kb (" - << (boost::format("%0.2f") % ((float)sz / (float)originstr.size() * 100.0f)).str() - << " %)." << std::endl; - if(sz == 0) { throw std::runtime_error("A compression size of 0 is incorrect. Something is wrong with this file."); } @@ -151,6 +184,32 @@ std::unordered_map BLOSC_MAX_BUFFERSIZE) { + throw std::runtime_error("Input is too large for Blosc compression."); + } + + std::string compressed(originstr.size() + BLOSC_MAX_OVERHEAD, '\0'); + const int compressed_size = blosc_compress_ctx(9, + BLOSC_BITSHUFFLE, + sizeof(fpt), + originstr.size(), + originstr.data(), + &compressed[0], + compressed.size(), + "blosclz", + 0, + 1); + if(compressed_size <= 0) { + throw std::runtime_error("Blosc compression failed."); + } + + compressed.resize(compressed_size); + return compressed; + } + break; default: throw std::runtime_error("Invalid algorithm id received."); break; @@ -205,49 +310,139 @@ std::string D2OFormat::compress_stream(const std::string& originstr, D2OFormat:: } /** - * @brief Check that a compressed stream can be correctly decompressed + * @brief Decompress a stream * - * @param[in] algo_id Which compression algorithm has been used - * @param[in] verificationstr Original data stream (string) - * @param[in] compressedstr Compressed data stream (string) + * @param[in] algo_id The algorithm identifier + * @param[in] compressedstr Compressed data stream + * @param[in] expected_size Expected uncompressed byte size; 0 disables validation * - * @return Whether compressed stream can be correctly decompressed + * @return Decompressed stream */ -bool D2OFormat::check_decompression(D2OFormat::CompressionAlgo algo_id, const std::string& verificationstr, const std::string& compressedstr) { - // decompress - std::istringstream compressed(compressedstr); - boost::iostreams::filtering_istreambuf in; - +std::string D2OFormat::decompress_stream(D2OFormat::CompressionAlgo algo_id, + const std::string& compressedstr, + size_t expected_size) { switch(algo_id) { case D2OFormat::CompressionAlgo::GZIP: + case D2OFormat::CompressionAlgo::LZMA: + case D2OFormat::CompressionAlgo::BZIP2: { - // GZIP compression - std::cout << "Building GZIP decompressor" << std::endl; - in.push(boost::iostreams::gzip_decompressor()); + std::istringstream compressed(compressedstr); + boost::iostreams::filtering_istreambuf in; + + switch(algo_id) { + case D2OFormat::CompressionAlgo::GZIP: + CLIFormat::print_kv("Decompressor", "gzip"); + in.push(boost::iostreams::gzip_decompressor()); + break; + case D2OFormat::CompressionAlgo::LZMA: + CLIFormat::print_kv("Decompressor", "lzma"); + in.push(boost::iostreams::lzma_decompressor()); + break; + case D2OFormat::CompressionAlgo::BZIP2: + CLIFormat::print_kv("Decompressor", "bzip2"); + in.push(boost::iostreams::bzip2_decompressor()); + break; + default: + break; + } + + in.push(compressed); + + std::ostringstream origin; + boost::iostreams::copy(in, origin); + const std::string originstr = origin.str(); + + if(expected_size != 0 && originstr.size() != expected_size) { + throw std::runtime_error("Decompressed stream has an unexpected size."); + } + + return originstr; } break; - case D2OFormat::CompressionAlgo::LZMA: + case D2OFormat::CompressionAlgo::ZSTD: { - std::cout << "Building LZMA decompressor" << std::endl; - in.push(boost::iostreams::lzma_decompressor()); + CLIFormat::print_kv("Decompressor", "zstd"); + + const unsigned long long frame_size = ZSTD_getFrameContentSize(compressedstr.data(), compressedstr.size()); + if(frame_size == ZSTD_CONTENTSIZE_ERROR) { + throw std::runtime_error("Invalid ZSTD compressed stream."); + } + + size_t decompressed_size = expected_size; + if(decompressed_size == 0) { + if(frame_size == ZSTD_CONTENTSIZE_UNKNOWN) { + throw std::runtime_error("Cannot determine ZSTD decompressed size."); + } + decompressed_size = static_cast(frame_size); + } else if(frame_size != ZSTD_CONTENTSIZE_UNKNOWN && frame_size != decompressed_size) { + throw std::runtime_error("ZSTD decompressed stream has an unexpected size."); + } + + std::string originstr(decompressed_size, '\0'); + const size_t actual_size = ZSTD_decompress(&originstr[0], + originstr.size(), + compressedstr.data(), + compressedstr.size()); + if(ZSTD_isError(actual_size)) { + throw std::runtime_error("ZSTD decompression failed: " + std::string(ZSTD_getErrorName(actual_size))); + } + + if(actual_size != decompressed_size) { + throw std::runtime_error("ZSTD decompressed stream has an unexpected size."); + } + + return originstr; } break; - case D2OFormat::CompressionAlgo::BZIP2: + case D2OFormat::CompressionAlgo::BLOSC: { - std::cout << "Building BZIP2 decompressor" << std::endl; - in.push(boost::iostreams::bzip2_decompressor()); + CLIFormat::print_kv("Decompressor", "blosc"); + + size_t decompressed_size = 0; + size_t compressed_size = 0; + size_t block_size = 0; + blosc_cbuffer_sizes(compressedstr.data(), &decompressed_size, &compressed_size, &block_size); + + if(decompressed_size == 0 || compressed_size == 0) { + throw std::runtime_error("Invalid Blosc compressed stream."); + } + + if(expected_size != 0 && decompressed_size != expected_size) { + throw std::runtime_error("Blosc decompressed stream has an unexpected size."); + } + + std::string originstr(decompressed_size, '\0'); + const int actual_size = blosc_decompress_ctx(compressedstr.data(), + &originstr[0], + originstr.size(), + 1); + if(actual_size <= 0) { + throw std::runtime_error("Blosc decompression failed."); + } + + if(static_cast(actual_size) != decompressed_size) { + throw std::runtime_error("Blosc decompressed stream has an unexpected size."); + } + + return originstr; } break; default: - throw std::runtime_error("Invalid algorithm id encountered for decompression verification."); + throw std::runtime_error("Invalid algorithm id encountered for decompression."); break; } +} - in.push(compressed); - - std::ostringstream origin; - boost::iostreams::copy(in, origin); - const std::string originstr = origin.str(); - +/** + * @brief Check that a compressed stream can be correctly decompressed + * + * @param[in] algo_id Which compression algorithm has been used + * @param[in] verificationstr Original data stream (string) + * @param[in] compressedstr Compressed data stream (string) + * + * @return Whether compressed stream can be correctly decompressed + */ +bool D2OFormat::check_decompression(D2OFormat::CompressionAlgo algo_id, const std::string& verificationstr, const std::string& compressedstr) { + const std::string originstr = D2OFormat::decompress_stream(algo_id, compressedstr, verificationstr.size()); return verificationstr == originstr; -} \ No newline at end of file +} diff --git a/src/d2o_format.h b/src/d2o_format.h index d5cc7cf..703258e 100644 --- a/src/d2o_format.h +++ b/src/d2o_format.h @@ -47,6 +47,8 @@ namespace D2OFormat { GZIP, LZMA, BZIP2, + ZSTD, + BLOSC, }; // list of compression algos @@ -54,7 +56,9 @@ namespace D2OFormat { {"auto", CompressionAlgo::AUTO}, {"gzip", CompressionAlgo::GZIP}, {"lzma", CompressionAlgo::LZMA}, - {"bzip2", CompressionAlgo::BZIP2} + {"bzip2", CompressionAlgo::BZIP2}, + {"zstd", CompressionAlgo::ZSTD}, + {"blosc", CompressionAlgo::BLOSC} }; /** @@ -82,6 +86,28 @@ namespace D2OFormat { */ std::string compress_stream(const std::string& originstr, D2OFormat::CompressionAlgo algo_id); + /** + * @brief Get a human-readable compression algorithm name + * + * @param[in] algo_id The algorithm identifier + * + * @return Algorithm name + */ + std::string compression_algo_name(D2OFormat::CompressionAlgo algo_id); + + /** + * @brief Decompress a stream + * + * @param[in] algo_id The algorithm identifier + * @param[in] compressedstr Compressed data stream + * @param[in] expected_size Expected uncompressed byte size; 0 disables validation + * + * @return Decompressed stream + */ + std::string decompress_stream(D2OFormat::CompressionAlgo algo_id, + const std::string& compressedstr, + size_t expected_size = 0); + /** * @brief Compress data stream using all possible compression algos * @@ -104,4 +130,4 @@ namespace D2OFormat { } -#endif // _D2O_FORMAT \ No newline at end of file +#endif // _D2O_FORMAT diff --git a/src/den2obj.cpp b/src/den2obj.cpp index e2cfed7..0a9c223 100644 --- a/src/den2obj.cpp +++ b/src/den2obj.cpp @@ -27,10 +27,11 @@ #include "generator.h" #include "isosurface.h" #include "isosurface_mesh.h" +#include "cli_format.h" int main(int argc, char* argv[]) { try { - TCLAP::CmdLine cmd("Converts VASP density file to wavefront object.", ' ', PROGRAM_VERSION); + TCLAP::CmdLine cmd("Converts scalar field data to mesh and volumetric formats.", ' ', PROGRAM_VERSION); //************************************** // declare values to be parsed @@ -71,18 +72,16 @@ int main(int argc, char* argv[]) { //************************************** // Inform user about execution //************************************** - std::cout << "--------------------------------------------------------------" << std::endl; - std::cout << "Executing "<< PROGRAM_NAME << " v." << PROGRAM_VERSION << std::endl; - std::cout << "Author: Ivo Filot " << std::endl; - std::cout << "Website: https://den2obj.imc-tue.nl" << std::endl; - std::cout << "Github: https://github.com/ifilot/den2obj" << std::endl; - std::cout << "--------------------------------------------------------------" << std::endl; - std::cout << "Compilation time: " << __DATE__ << " " << __TIME__ << std::endl; - std::cout << "Git Hash: " << PROGRAM_GIT_HASH << std::endl; #ifdef MOD_OPENVDB - std::cout << "Compiled with OpenVDB module" << std::endl; + const bool openvdb_enabled = true; + #else + const bool openvdb_enabled = false; #endif - std::cout << "--------------------------------------------------------------" << std::endl; + CLIFormat::print_banner(PROGRAM_VERSION, + PROGRAM_GIT_HASH, + __DATE__, + __TIME__, + openvdb_enabled); //************************************** // parsing values @@ -121,7 +120,9 @@ int main(int argc, char* argv[]) { throw std::runtime_error("Invalid extension for dataset generation. Filename has to end in .d2o."); } - std::cout << "Building grid using dataset: " << arg_generator.getValue() << std::endl; + CLIFormat::print_section("Dataset generation"); + CLIFormat::print_kv("Dataset", arg_generator.getValue()); + CLIFormat::print_kv("Output", output_filename); gen.build_dataset(arg_generator.getValue(), output_filename, algo_id); } else { const std::string input_filename = arg_input_filename.getValue(); @@ -260,8 +261,7 @@ int main(int argc, char* argv[]) { auto end = std::chrono::system_clock::now(); std::chrono::duration elapsed_seconds = end-start; - std::cout << "-------------------------------------------------------------------------------" << std::endl; - std::cout << "Done in " << elapsed_seconds.count() << " seconds." << std::endl << std::endl; + CLIFormat::print_done_summary(CLIFormat::format_seconds(elapsed_seconds.count())); return 0; diff --git a/src/generator.cpp b/src/generator.cpp index 5a09044..341ec6f 100644 --- a/src/generator.cpp +++ b/src/generator.cpp @@ -19,6 +19,7 @@ **************************************************************************/ #include "generator.h" +#include "cli_format.h" /** * @brief Constructs a new instance. @@ -103,7 +104,7 @@ std::vector Generator::genus2(fpt sz, size_t npts) const { // build grid std::vector f(npts*npts*npts); - boost::timer::progress_display progress(npts); + CLIFormat::ProgressBar progress("Building grid", npts); for(unsigned int i=0; i Generator::genus2(fpt sz, size_t npts) const { (x*x + y*y) * (x*x + y*y) - (9*z*z - 1) * (1 - z*z); } } - ++progress; + progress.tick(); } - std::cout << std::endl; - return f; } @@ -143,7 +142,7 @@ std::vector Generator::benzene_molecular_orbital(fpt sz, size_t npts, // build grid std::vector f(npts*npts*npts); - boost::timer::progress_display progress(npts); + CLIFormat::ProgressBar progress("Building grid", npts); for(unsigned int i=0; i Generator::benzene_molecular_orbital(fpt sz, size_t npts, f[idx] = this->calculate_mo_amp(Vec3(x,y,z), mo_id); } } - ++progress; + progress.tick(); } - std::cout << std::endl; - return f; } diff --git a/src/generator.h b/src/generator.h index 5d64ec4..7dd3d97 100644 --- a/src/generator.h +++ b/src/generator.h @@ -23,7 +23,6 @@ #include #include -#include #include "d2o_format.h" #include "generator_benzene_data.h" diff --git a/src/scalar_field.cpp b/src/scalar_field.cpp index 74b7f67..6e874dd 100644 --- a/src/scalar_field.cpp +++ b/src/scalar_field.cpp @@ -733,7 +733,8 @@ void ScalarField::load_d2o_binary() { infile.read((char*)&protocol_id, sizeof(uint32_t)); // check token id - if(protocol_id == 0 || protocol_id > 3) { + static const uint32_t max_protocol_id = static_cast(D2OFormat::CompressionAlgo::BLOSC); + if(protocol_id == static_cast(D2OFormat::CompressionAlgo::AUTO) || protocol_id > max_protocol_id) { throw std::runtime_error("Invalid protocol id for d2o file: " + std::to_string(protocol_id)); } @@ -779,41 +780,13 @@ void ScalarField::load_d2o_binary() { char* data = new char[compdatasize]; infile.read(data, compdatasize); - // decompress - std::istringstream compressed(std::string(data, compdatasize)); - boost::iostreams::filtering_istreambuf in; - - switch(protocol_id) { - case 1: - { - // GZIP compression - std::cout << "Building GZIP decompressor" << std::endl; - in.push(boost::iostreams::gzip_decompressor()); - } - break; - case 2: - { - std::cout << "Building LZMA decompressor" << std::endl; - in.push(boost::iostreams::lzma_decompressor()); - } - break; - case 3: - { - std::cout << "Building BZIP2 decompressor" << std::endl; - in.push(boost::iostreams::bzip2_decompressor()); - } - break; - default: - throw std::runtime_error("Invalid protocol id for d2o file: " + std::to_string(protocol_id)); - break; - } + const std::string compressed_data(data, compdatasize); + delete[] data; + const std::string gridptrdata = D2OFormat::decompress_stream(static_cast(protocol_id), + compressed_data, + this->gridptr.size() * sizeof(fpt)); std::cout << "Decompressed data" << std::endl; - in.push(compressed); - - std::ostringstream origin; - boost::iostreams::copy(in, origin); - const std::string gridptrdata = origin.str(); memcpy(&this->gridptr[0], gridptrdata.data(), this->gridptr.size() * sizeof(fpt)); this->scalar = 1.0; @@ -821,7 +794,6 @@ void ScalarField::load_d2o_binary() { this->header_read = true; // clean up - delete[] data; infile.close(); std::cout << "Done reading D2O binary file" << std::endl; diff --git a/src/scalar_field.h b/src/scalar_field.h index cedff05..9e6cb27 100644 --- a/src/scalar_field.h +++ b/src/scalar_field.h @@ -127,7 +127,7 @@ class ScalarField{ /** * @brief Write to a binary D2O file * - * Preferred protocol is set to 2, which corresponds to LZMA compression. + * Compression algorithm is selected automatically unless explicitly supplied. */ void write_d2o_binary(const std::string filename, D2OFormat::CompressionAlgo = D2OFormat::CompressionAlgo::AUTO); diff --git a/src/test/test_d2o_fileformat.cpp b/src/test/test_d2o_fileformat.cpp index 739f219..b8e20cb 100644 --- a/src/test/test_d2o_fileformat.cpp +++ b/src/test/test_d2o_fileformat.cpp @@ -32,58 +32,35 @@ void TestD2OFileFormat::setUp() { } void TestD2OFileFormat::test_gzip_compression() { - // create scalar field - ScalarField sf(basefile, ScalarFieldInputFileType::SFF_D2O); - CPPUNIT_ASSERT_EQUAL( (uint)1000000, sf.get_size() ); - - static const std::string filename = "chgcar_ch4_gzip.d2o"; - - sf.write_d2o_binary(filename, D2OFormat::CompressionAlgo::GZIP); - CPPUNIT_ASSERT_EQUAL((uint32_t)1, this->get_protocol_id(filename)); - - const auto nrgridpts = sf.get_grid().size(); - const auto grid = sf.get_grid(); - - sf = ScalarField(filename, ScalarFieldInputFileType::SFF_D2O); - CPPUNIT_ASSERT_EQUAL(nrgridpts, sf.get_grid().size()); - - // compare vector points - for(unsigned int i=0; iassert_compression_roundtrip(D2OFormat::CompressionAlgo::GZIP, 1, "chgcar_ch4_gzip.d2o"); } void TestD2OFileFormat::test_lzma_compression() { - // create scalar field - ScalarField sf(basefile, ScalarFieldInputFileType::SFF_D2O); - CPPUNIT_ASSERT_EQUAL( (uint)1000000, sf.get_size() ); - - static const std::string filename = "chgcar_ch4_lzma.d2o"; - - sf.write_d2o_binary(filename, D2OFormat::CompressionAlgo::LZMA); - CPPUNIT_ASSERT_EQUAL((uint32_t)2, this->get_protocol_id(filename)); + this->assert_compression_roundtrip(D2OFormat::CompressionAlgo::LZMA, 2, "chgcar_ch4_lzma.d2o"); +} - const auto nrgridpts = sf.get_grid().size(); - const auto grid = sf.get_grid(); +void TestD2OFileFormat::test_bzip2_compression() { + this->assert_compression_roundtrip(D2OFormat::CompressionAlgo::BZIP2, 3, "chgcar_ch4_bzip2.d2o"); +} - sf = ScalarField(filename, ScalarFieldInputFileType::SFF_D2O); - CPPUNIT_ASSERT_EQUAL(nrgridpts, sf.get_grid().size()); +void TestD2OFileFormat::test_zstd_compression() { + this->assert_compression_roundtrip(D2OFormat::CompressionAlgo::ZSTD, 4, "chgcar_ch4_zstd.d2o"); +} - // compare vector points - for(unsigned int i=0; iassert_compression_roundtrip(D2OFormat::CompressionAlgo::BLOSC, 5, "chgcar_ch4_blosc.d2o"); } -void TestD2OFileFormat::test_bzip2_compression() { +void TestD2OFileFormat::test_autocompression() { // create scalar field ScalarField sf(basefile, ScalarFieldInputFileType::SFF_D2O); CPPUNIT_ASSERT_EQUAL( (uint)1000000, sf.get_size() ); - static const std::string filename = "chgcar_ch4_bzip2.d2o"; + static const std::string filename = "chgcar_ch4_auto.d2o"; - sf.write_d2o_binary(filename, D2OFormat::CompressionAlgo::BZIP2); - CPPUNIT_ASSERT_EQUAL((uint32_t)3, this->get_protocol_id(filename)); + sf.write_d2o_binary(filename, D2OFormat::CompressionAlgo::AUTO); + CPPUNIT_ASSERT_GREATEREQUAL((uint32_t)1, this->get_protocol_id(filename)); + CPPUNIT_ASSERT_LESSEQUAL((uint32_t)5, this->get_protocol_id(filename)); const auto nrgridpts = sf.get_grid().size(); const auto grid = sf.get_grid(); @@ -97,15 +74,15 @@ void TestD2OFileFormat::test_bzip2_compression() { } } -void TestD2OFileFormat::test_autocompression() { +void TestD2OFileFormat::assert_compression_roundtrip(D2OFormat::CompressionAlgo algo_id, + uint32_t protocol_id, + const std::string& filename) { // create scalar field ScalarField sf(basefile, ScalarFieldInputFileType::SFF_D2O); CPPUNIT_ASSERT_EQUAL( (uint)1000000, sf.get_size() ); - static const std::string filename = "chgcar_ch4_auto.d2o"; - - sf.write_d2o_binary(filename, D2OFormat::CompressionAlgo::AUTO); - CPPUNIT_ASSERT_EQUAL((uint32_t)2, this->get_protocol_id(filename)); + sf.write_d2o_binary(filename, algo_id); + CPPUNIT_ASSERT_EQUAL(protocol_id, this->get_protocol_id(filename)); const auto nrgridpts = sf.get_grid().size(); const auto grid = sf.get_grid(); @@ -132,4 +109,4 @@ uint32_t TestD2OFileFormat::get_protocol_id(const std::string& filename) { void TestD2OFileFormat::tearDown() { // do nothing -} \ No newline at end of file +} diff --git a/src/test/test_d2o_fileformat.h b/src/test/test_d2o_fileformat.h index a8e5c8d..fd308e1 100644 --- a/src/test/test_d2o_fileformat.h +++ b/src/test/test_d2o_fileformat.h @@ -32,6 +32,8 @@ class TestD2OFileFormat : public CppUnit::TestFixture CPPUNIT_TEST( test_gzip_compression ); CPPUNIT_TEST( test_lzma_compression ); CPPUNIT_TEST( test_bzip2_compression ); + CPPUNIT_TEST( test_zstd_compression ); + CPPUNIT_TEST( test_blosc_compression ); CPPUNIT_TEST( test_autocompression ); CPPUNIT_TEST_SUITE_END(); @@ -43,11 +45,16 @@ class TestD2OFileFormat : public CppUnit::TestFixture void test_gzip_compression(); void test_lzma_compression(); void test_bzip2_compression(); + void test_zstd_compression(); + void test_blosc_compression(); void test_autocompression(); private: + void assert_compression_roundtrip(D2OFormat::CompressionAlgo algo_id, + uint32_t protocol_id, + const std::string& filename); uint32_t get_protocol_id(const std::string& filename); const std::string basefile = "chgcar_ch4_base.d2o"; }; -#endif // _TEST_D2O_FileFormat \ No newline at end of file +#endif // _TEST_D2O_FileFormat diff --git a/src/test/test_file_creation.cpp b/src/test/test_file_creation.cpp index 186694c..c265ab8 100644 --- a/src/test/test_file_creation.cpp +++ b/src/test/test_file_creation.cpp @@ -67,7 +67,7 @@ TestFileCreation::MeshReference TestFileCreation::generate_mesh() const { return { ism.get_vertices().size(), ism.get_normals().size(), - ism.get_texcoords().size() / 6 + ism.get_texcoords().size() / 3 }; } From 058a0b057f081df31f6408acb0cdb0c6f97cbbb9 Mon Sep 17 00:00:00 2001 From: ifilot Date: Thu, 21 May 2026 10:02:27 +0200 Subject: [PATCH 5/6] Improving coverage --- .gitignore | 2 +- src/test/CMakeLists.txt | 11 ++--------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index 876e008..6ba8496 100644 --- a/.gitignore +++ b/.gitignore @@ -27,7 +27,7 @@ *.out *.app -build/* +build*/* CHGCAR* *.png src/config.h diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt index 60a0e72..8ab12bc 100644 --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -22,11 +22,7 @@ pkg_check_modules(CPPUNIT REQUIRED IMPORTED_TARGET cppunit) add_library(unittest STATIC unittest.cpp) target_compile_features(unittest PUBLIC cxx_std_17) -set(EXECUTABLES TestIsosurface TestScalarField TestD2OFileFormat) - -if(NOT USE_GCOV) - list(APPEND EXECUTABLES TestGenerator) -endif() +set(EXECUTABLES TestIsosurface TestScalarField TestD2OFileFormat TestGenerator) if(USE_GCOV) list(APPEND EXECUTABLES TestFileCreation) @@ -35,10 +31,7 @@ endif() add_executable(TestIsosurface test_isosurface.cpp) add_executable(TestScalarField test_scalarfield.cpp) add_executable(TestD2OFileFormat test_d2o_fileformat.cpp) - -if(NOT USE_GCOV) - add_executable(TestGenerator test_generator.cpp) -endif() +add_executable(TestGenerator test_generator.cpp) if(USE_GCOV) add_executable(TestFileCreation test_file_creation.cpp) From 63c3b29fba73d6d24d2304b7d5476b124f30cce2 Mon Sep 17 00:00:00 2001 From: ifilot Date: Thu, 21 May 2026 10:30:51 +0200 Subject: [PATCH 6/6] Updating docs --- .github/workflows/build-openvdb.yml | 38 ------------- .github/workflows/build.yml | 6 ++- .github/workflows/docs.yml | 44 ++++++++++++---- README.md | 6 +-- docker/Dockerfile | 3 +- docs/conf.py | 6 +-- docs/d2o_fileformat.rst | 12 ++--- docs/faq.rst | 2 +- docs/index.rst | 2 +- docs/installation.rst | 64 +++------------------- docs/tutorial.rst | 82 ++++------------------------- docs/user_interface.rst | 9 ++-- 12 files changed, 75 insertions(+), 199 deletions(-) delete mode 100644 .github/workflows/build-openvdb.yml diff --git a/.github/workflows/build-openvdb.yml b/.github/workflows/build-openvdb.yml deleted file mode 100644 index 6224113..0000000 --- a/.github/workflows/build-openvdb.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: OpenVDB module - -on: - push: - branches: [ "master", "develop" ] - pull_request: - branches: [ "master", "develop" ] - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - name: Install dependencies - run: | - sudo apt update - sudo apt install -y \ - build-essential \ - cmake \ - libglm-dev \ - libtclap-dev \ - libboost-all-dev \ - libopenvdb-dev \ - libtbb-dev \ - libcppunit-dev \ - libeigen3-dev \ - liblzma-dev \ - zlib1g-dev \ - libbz2-dev \ - libssl-dev \ - pkg-config - - name: Configure CMake - run: cmake -S src -B build -DMOD_OPENVDB=ON - - name: Build - run: cmake --build build --parallel - - name: Run unit tests - run: ctest --test-dir build --output-on-failure diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f212f0f..20c5fd4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,7 +28,8 @@ jobs: liblzma-dev \ zlib1g-dev \ libbz2-dev \ - libssl-dev \ + libzstd-dev \ + libblosc-dev \ pkg-config \ gcovr - name: Configure CMake @@ -74,7 +75,8 @@ jobs: liblzma-dev \ zlib1g-dev \ libbz2-dev \ - libssl-dev \ + libzstd-dev \ + libblosc-dev \ pkg-config \ gcovr - name: Configure CMake diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 15cee1a..236c574 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -9,21 +9,27 @@ on: - master permissions: - contents: write # Grants write access to the repository contents + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false jobs: build: - name: Build and Deploy Sphinx Documentation + name: Build Sphinx Documentation runs-on: ubuntu-latest steps: - name: Checkout Repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: - python-version: 3.13.1 + python-version: "3.13" - name: Install Dependencies run: | @@ -34,13 +40,33 @@ jobs: sphinx_design \ sphinx_subfigure \ myst_parser + - name: Build Documentation run: | cd docs make html - - name: Deploy to GitHub Pages - uses: peaceiris/actions-gh-pages@v3 + - name: Configure GitHub Pages + if: github.event_name == 'push' + uses: actions/configure-pages@v5 + + - name: Upload GitHub Pages artifact + if: github.event_name == 'push' + uses: actions/upload-pages-artifact@v3 with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: docs/_build/html \ No newline at end of file + path: docs/_build/html + + deploy: + name: Deploy GitHub Pages + if: github.event_name == 'push' + needs: build + runs-on: ubuntu-latest + + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/README.md b/README.md index ff23af1..7b9518a 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ dependencies: ```bash sudo apt install build-essential cmake libtclap-dev libboost-all-dev \ libopenvdb-dev libtbb-dev pkg-config libcppunit-dev libeigen3-dev liblzma-dev \ -zlib1g-dev libbz2-dev libzstd-dev libblosc-dev libssl-dev +zlib1g-dev libbz2-dev libzstd-dev libblosc-dev ``` Compile: @@ -221,8 +221,8 @@ git clone https://github.com/ifilot/den2obj.git cd den2obj mkdir build && cd build cmake ../src -make -j5 -./src/test/unittest +cmake --build . --parallel +ctest --output-on-failure ``` ## License diff --git a/docker/Dockerfile b/docker/Dockerfile index e89a384..569698d 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -32,8 +32,7 @@ RUN apt-get update && apt-get install -y \ zlib1g-dev \ libbz2-dev \ libzstd-dev \ - libblosc-dev \ - libssl-dev + libblosc-dev RUN apt-get install curl wget diff --git a/docs/conf.py b/docs/conf.py index b0e4f0b..2c65252 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -13,8 +13,6 @@ # import os # import sys # sys.path.insert(0, os.path.abspath('.')) -import sphinx_rtd_theme - # -- Project information ----------------------------------------------------- project = 'Den2Obj' @@ -31,9 +29,10 @@ 'sphinx.ext.mathjax', 'sphinx.ext.autosectionlabel', 'sphinx_rtd_theme', - 'sphinx.ext.mathjax' ] +autosectionlabel_prefix_document = True + # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -55,7 +54,6 @@ # so a file named "default.css" will overwrite the builtin "default.css". master_doc = 'index' html_static_path = ['_static'] -html_theme_options = {'display_version': True} html_logo = "_static/img/den2obj_logo_128.png" html_favicon = "_static/img/favicon.ico" html_css_files = [ diff --git a/docs/d2o_fileformat.rst b/docs/d2o_fileformat.rst index 9707d73..8f139e4 100644 --- a/docs/d2o_fileformat.rst +++ b/docs/d2o_fileformat.rst @@ -25,27 +25,27 @@ The organization of this file is given in the Table 1. - char - 3 bytes - Fixed token of "D2O" to identify the file. - * - 0x03-0x07 + * - 0x03-0x06 - uint32_t - 4 bytes - Protocol identifier token (see below). Selected automatically by default. - * - 0x08-0x2B + * - 0x07-0x2A - float[9] - 36 bytes - Unit cell matrix - * - 0x2C-0x37 + * - 0x2B-0x36 - uint32_t[3] - 12 bytes - Grid dimensions (its product is the number of data points) - * - 0x38 + * - 0x37 - uint8_t - 1 byte - Floating point bytesize (float = 4, double = 8). Mainly used for validation purposes. - * - 0x39-0x41 + * - 0x38-0x3F - uint64_t - 8 bytes - Size of the compressed data stream - * - 0x42.. + * - 0x40.. - char[DATASIZE] - DATASIZE bytes - Compressed data stream containing densely packed scalar field. diff --git a/docs/faq.rst b/docs/faq.rst index 0a38d5c..503d31a 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -20,7 +20,7 @@ Why do I see the error "Decompression could not be verified."? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ After compression, :program:`Den2Obj` checks whether the compression -is succesfull by decompressing the stream and tesing for similarity. +is successful by decompressing the stream and testing for similarity. If the decompressed stream is different from the original stream, this error is thrown automatically. Although it is very rare for this error to be thrown, we have seen it happening for LZMA compression. The diff --git a/docs/index.rst b/docs/index.rst index bac7080..fc45dd0 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -18,7 +18,7 @@ Den2Obj - A command line program for producing isosurfaces from density files :program:`Den2Obj` is a command-line tool that construct isosurfaces from densely packed scalar fields. :program:`Den2Obj` supports VASP charge files such as CHGCAR and PARCHG, Gaussian .cube files as well as its own -:ref:`.d2o file format`. +:ref:`.d2o file format`. :program:`Den2Obj` can be used together with popular 3D rendering programs such as `Blender `_ to produce stunning visuals diff --git a/docs/installation.rst b/docs/installation.rst index fe8732a..683ef65 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -33,7 +33,7 @@ libraries are available to you: To ensure that all the packages are installed, one can run the following:: - sudo apt install build-essential cmake libtclap-dev libboost-all-dev \ + sudo apt install build-essential cmake libtclap-dev libboost-all-dev \ pkg-config libcppunit-dev libeigen3-dev liblzma-dev zlib1g-dev libbz2-dev \ libzstd-dev libblosc-dev @@ -47,78 +47,28 @@ looks as follows:: cd den2obj mkdir build && cd build cmake ../src - make -j9 + cmake --build . --parallel To install :program:`Den2Obj`, you can in addition run:: - sudo make install + sudo cmake --install . which will place a copy of the ``den2obj`` executable in ``/usr/local/bin/den2obj``. -Compilation with OpenVDB module -------------------------------- - -Using the `OpenVDB `_ library, it is possible -to convert a density object to an OpenVDB file which can be used to render -the density clouds in `Blender `_. Compilation -of :program:`Den2Obj` using this module requires supplying an additional -argument to CMake. - -.. warning:: - A fairly recent version of OpenVDB is required for compatibility with - modern dependencies. The version packaged in Ubuntu 22.04 LTS is - incompatible with the current Thread Building Blocks (TBB) library. - We have tested and verified compatibility with OpenVDB on Ubuntu 24.04 LTS. - If you are working on Ubuntu 22.04 LTS, you might need to compile a recent - version of OpenVDB from source. The instructions below are for Ubuntu - 24.04 LTS. - -Ensure that the following libraries are installed by running:: - - sudo apt install build-essential cmake libtclap-dev libboost-all-dev \ - libopenvdb-dev libtbb-dev pkg-config libcppunit-dev libeigen3-dev \ - liblzma-dev zlib1g-dev libbz2-dev libzstd-dev libblosc-dev - -Compilation :program:`Den2Obj` with the OpenVDB module is as follows:: - - git clone https://github.com/ifilot/den2obj.git - cd den2obj - mkdir build && cd build - cmake -DMOD_OPENVDB=1 ../src - make -j9 - Testing ------- To test :program:`Den2Obj`, one can run the following after compilation:: - make test - -A succesfull test should produce an output similar to the one found below:: - - Running tests... - Test project /mnt/c/PROGRAMMING/CPP/den2obj/build - Start 1: DatasetSetup - 1/6 Test #1: DatasetSetup ..................... Passed 2.49 sec - Start 3: TestIsosurface - 2/6 Test #3: TestIsosurface ................... Passed 1.07 sec - Start 4: TestScalarField - 3/6 Test #4: TestScalarField .................. Passed 0.39 sec - Start 5: TestD2OFileFormat - 4/6 Test #5: TestD2OFileFormat ................ Passed 0.02 sec - Start 6: TestGenerator - 5/6 Test #6: TestGenerator .................... Passed 8.34 sec - Start 2: DatasetCleanup - 6/6 Test #2: DatasetCleanup ................... Passed 0.00 sec - - 100% tests passed, 0 tests failed out of 6 + ctest --output-on-failure - Total Test time (real) = 12.45 sec +A successful test run should report that all tests passed. The exact number of +tests and their timings can differ between standard and coverage builds. If the test is for some reason failing, one can run the following to produce more output:: - CTEST_OUTPUT_ON_FAILURE=TRUE make test + ctest --output-on-failure .. note:: diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 1ece79b..3a98de1 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -37,25 +37,11 @@ run the following:: ./den2obj -g genus2 -o genus2.d2o -You should see the following output:: - - -------------------------------------------------------------- - Executing DEN2OBJ v.1.1.0 - Author: Ivo Filot - Website: https://den2obj.imc-tue.nl - Github: https://github.com/ifilot/den2obj - -------------------------------------------------------------- - Building grid using dataset: genus2 - Looking for best compression algorithm. - Trying GZIP: 2720.5 kb (69.64 %). - Trying LZMA: 1781.2 kb (45.60 %). - Trying BZIP2: 3149.8 kb (80.63 %). - Floating point size determined at: 4 bytes - Writing genus2.d2o (1781.3kb). - ------------------------------------------------------------------------------- - Done in 1.42421 seconds. - -This will generate a :ref:`.d2o file ` containing the Genus 2 +You should see :program:`Den2Obj` print a short banner, build the scalar field, +benchmark the available compression algorithms, verify decompression, and write +``genus2.d2o``. + +This will generate a :ref:`.d2o file ` containing the Genus 2 scalar field named ``genus2.d2o``. Observe that :program:`Den2Obj` tests the available compression algorithms and automatically selects the best algorithm for the data compression. @@ -64,30 +50,8 @@ To construct the isosurface with an isovalue of 0.1, run:: ./den2obj -i genus2.d2o -o genus2.ply -v 0.1 -c -Which will give the following output:: - - -------------------------------------------------------------- - Executing DEN2OBJ v.1.1.0 - Author: Ivo Filot - Website: https://den2obj.imc-tue.nl - Github: https://github.com/ifilot/den2obj - -------------------------------------------------------------- - Opening genus2.d2o as D2O binary file - Recognizing floating point size: 4 bytes. - Reading 1823992 bytes from file. - Building decompressor - Decompressed data - Done reading D2O binary file - Read 1000000 values. - Using isovalue: 0.1 - Lowest value in scalar field: -1.85503 - Highest value in scalar field: 265 - Identified 48900 faces. - Calculating normal vectors using two-point stencil - Writing mesh as Standford Triangle Format file (.ply). - Writing as Stanford (.ply) file: genus2.ply (1196.8kb). - ------------------------------------------------------------------------------- - Done in 0.17653 seconds. +This reads the D2O file, decompresses the scalar field, reports the scalar +range, extracts the isosurface, and writes ``genus2.ply``. .. note:: Observe that we generate the isosurface using the ``-c`` directive, which @@ -137,35 +101,9 @@ via ``--algo marching-tetrahedra``:: ./den2obj -i benzene_homo.d2o -o benzene_homo.ply -v 0.03 -c -d --algo marching-tetrahedra -The following output (or similar) is generated:: - - -------------------------------------------------------------- - Executing DEN2OBJ v.1.1.0 - Author: Ivo Filot - Website: https://den2obj.imc-tue.nl - Github: https://github.com/ifilot/den2obj - -------------------------------------------------------------- - Opening benzene_homo.d2o as D2O binary file - Recognizing floating point size: 4 bytes. - Reading 11415368 bytes from file. - Building LZMA decompressor - Decompressed data - Done reading D2O binary file - Read 3375000 values. - Using isovalue: 0.03 - Lowest value in scalar field: -0.25383 - Highest value in scalar field: 0.25383 - Calculating normal vectors using two-point stencil - Centering structure - Writing mesh as Standford Triangle Format file (.ply). - Writing as Stanford (.ply) file: benzene_homo_pos.ply (4560.3kb). - Identified 59512 faces. - Calculating normal vectors using two-point stencil - Centering structure - Writing mesh as Standford Triangle Format file (.ply). - Writing as Stanford (.ply) file: benzene_homo_neg.ply (1454.4kb). - ------------------------------------------------------------------------------- - Done in 2.17096 seconds. +The command reads and decompresses the scalar field, generates one isosurface +for ``+0.03`` and one for ``-0.03``, centers both meshes, and writes the +positive and negative lobes as separate ``.ply`` files. Observe that two isosurfaces are created and stored as ``.ply`` files: diff --git a/docs/user_interface.rst b/docs/user_interface.rst index f0eb2c8..d701a87 100644 --- a/docs/user_interface.rst +++ b/docs/user_interface.rst @@ -19,7 +19,7 @@ Operational modes Isosurface generation is the default mode and is used to generate isosurfaces from scalar fields. Filetype conversion is mainly used to convert relatively large storage formats such as CHGCAR or Gaussian cube files to the compressed -:ref:`.d2o file format`. Finally, dataset generation is mainly +:ref:`.d2o file format`. Finally, dataset generation is mainly used for testing or educational purposes. Isosurface generation @@ -34,7 +34,7 @@ of the following types * `CHGCAR `_, `PARCHG `_ or `LOCPOT `_ * `Gaussian Cube file `_ -* :ref:`Den2Obj .d2o file format` +* :ref:`Den2Obj .d2o file format` ``mesh-file`` is any of the following supported formats @@ -71,12 +71,12 @@ Filetype conversion :program:`Den2Obj` offers the conversion to two different file types. -* :ref:`Den2Obj .d2o file format` +* :ref:`Den2Obj .d2o file format` * `OpenVDB `_ To perform the conversion, one executes:: - ./den2obj -i -o -t [-a ] + ./den2obj -i -o -t [-a ] For example, to convert `genus.d2o` to a `vdb` file, one runs:: @@ -119,6 +119,7 @@ When no ``-a`` is provided, automatically the best compression algorithm is used checking the inflation ratio of all possible compression algorithms. The following datasets are available: + * ``genus2`` * ``benzene_homo`` * ``benzene_lumo``