diff --git a/sophiread/.clang-format b/sophiread/.clang-format index 173a76c..97b0c10 100644 --- a/sophiread/.clang-format +++ b/sophiread/.clang-format @@ -246,5 +246,3 @@ WhitespaceSensitiveMacros: - BOOST_PP_STRINGIZE - NS_SWIFT_NAME - CF_SWIFT_NAME -... - diff --git a/sophiread/CMakeLists.txt b/sophiread/CMakeLists.txt index dc258fe..f101296 100644 --- a/sophiread/CMakeLists.txt +++ b/sophiread/CMakeLists.txt @@ -1,43 +1,65 @@ +# Sophiread CMakeLists.txt cmake_minimum_required(VERSION 3.20) execute_process(COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/scripts/version.sh -s print WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} - OUTPUT_VARIABLE SOPHIREAD_VERSION) + OUTPUT_VARIABLE SOPHIREAD_VERSION + RESULT_VARIABLE VERSION_RESULT + OUTPUT_STRIP_TRAILING_WHITESPACE +) +if(NOT VERSION_RESULT EQUAL 0) + message(FATAL_ERROR "Failed to determine version") +endif() project("Sophiread" VERSION ${SOPHIREAD_VERSION}) -# set(CMAKE_BUILD_TYPE DEBUG) +# This is to avoid accidentally using Homebrew header/lib instead of the one from micromamba +if(DEFINED ENV{CONDA_PREFIX}) + set(CMAKE_PREFIX_PATH $ENV{CONDA_PREFIX} ${CMAKE_PREFIX_PATH}) +endif() + +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE Release) +endif() + set(CMAKE_CXX_STANDARD 20) # set(CMAKE_AUTOMOC ON) # for meta object compiler, needed for Qt5 # Dependencies find_package(Eigen3 REQUIRED) -find_package(spdlog REQUIRED) find_package(HDF5 REQUIRED COMPONENTS CXX) find_package(GTest REQUIRED) +find_package(nlohmann_json 3.2.0 REQUIRED) +find_package(TBB REQUIRED) +find_package(TIFF REQUIRED) +find_package(spdlog 1.8.0 REQUIRED) +find_package(fmt 7.0.0 REQUIRED) # find_package(Qt5 COMPONENTS Widgets REQUIRED) -include_directories(${HDF5_INCLUDE_DIRS}) - -link_directories( - $ENV{CONDA_PREFIX}/lib -) - # Testing setup enable_testing() include(GoogleTest) -file(COPY ${CMAKE_SOURCE_DIR}/resources/data DESTINATION ${CMAKE_BINARY_DIR}) -# Add compiler flags -add_compile_options( - -O3 - -std=c++20 - -march=native - -ffast-math - -pthread - -Wall - -Wextra +# Copy resources +add_custom_command( + OUTPUT ${CMAKE_BINARY_DIR}/data + COMMAND ${CMAKE_COMMAND} -E copy_directory + ${CMAKE_SOURCE_DIR}/resources/data + ${CMAKE_BINARY_DIR}/data + DEPENDS ${CMAKE_SOURCE_DIR}/resources/data ) +add_custom_target(copy_resources ALL DEPENDS ${CMAKE_BINARY_DIR}/data) + +# Add compiler flags +if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + add_compile_options( + -march=native + -ffast-math + -pthread + -Wall + -Wextra + ) +endif() # Add Sophiread library # NOTE: this will take care of the testing as well @@ -65,6 +87,13 @@ if(DOXYGEN_FOUND) COMMENT "Generating API documentation with Doxygen" VERBATIM ) + if(APPLE) + set(OPEN_COMMAND open) + elseif(UNIX) + set(OPEN_COMMAND xdg-open) + elseif(WIN32) + set(OPEN_COMMAND start) + endif() add_custom_command(TARGET docs POST_BUILD COMMAND ${CMAKE_COMMAND} -E chdir ${CMAKE_BINARY_DIR}/docs/html diff --git a/sophiread/FastSophiread/CMakeLists.txt b/sophiread/FastSophiread/CMakeLists.txt index 19353c2..57d5fe7 100644 --- a/sophiread/FastSophiread/CMakeLists.txt +++ b/sophiread/FastSophiread/CMakeLists.txt @@ -1,15 +1,4 @@ -# Config SophireadLib -include_directories( - include - $ENV{CONDA_PREFIX}/include - ${EIGEN3_INCLUDE_DIR} - ${HDF5_INCLUDE_DIRS} -) - -link_directories( - $ENV{CONDA_PREFIX}/lib -) - +# Config FastSophireadLib set(SRC_FAST_FILES src/abs.cpp src/centroid.cpp @@ -21,164 +10,78 @@ set(SRC_FAST_FILES # ------------- SophireadLibFast -------------- # add_library(FastSophiread ${SRC_FAST_FILES}) - -# ----------------- TESTS ----------------- # -# DiskIO Tests -add_executable( - SophireadTests_diskio - tests/test_disk_io.cpp -) -target_link_libraries( - SophireadTests_diskio - FastSophiread - tbb - spdlog::spdlog - GTest::GTest - GTest::Main - ${HDF5_LIBRARIES} -) -gtest_discover_tests(SophireadTests_diskio) -# Hit Test -add_executable( - SophireadTests_hit - tests/test_hit.cpp -) -target_link_libraries( - SophireadTests_hit - FastSophiread - pthread - tbb - spdlog::spdlog - GTest::GTest - GTest::Main -) -gtest_discover_tests(SophireadTests_hit) -# TPX3 Test -add_executable( - SophireadTests_tpx3 - tests/test_tpx3.cpp -) -target_link_libraries( - SophireadTests_tpx3 - FastSophiread - pthread - tbb - spdlog::spdlog - GTest::GTest - GTest::Main - ${HDF5_LIBRARIES} -) -gtest_discover_tests(SophireadTests_tpx3) -# ABS Test -add_executable( - SophireadTests_abs - tests/test_abs.cpp -) -target_link_libraries( - SophireadTests_abs - FastSophiread - pthread - tbb - spdlog::spdlog - GTest::GTest - GTest::Main -) -gtest_discover_tests(SophireadTests_abs) -# Centroid Test -add_executable( - SophireadTests_centroid - tests/test_centroid.cpp -) -target_link_libraries( - SophireadTests_centroid - FastSophiread - pthread - tbb - spdlog::spdlog - GTest::GTest - GTest::Main -) -gtest_discover_tests(SophireadTests_centroid) -# Fast Gaussian Test -add_executable( - SophireadTests_fastgaussian - tests/test_fastgaussian.cpp +target_include_directories(FastSophiread + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/include + $ENV{CONDA_PREFIX}/include + ${EIGEN3_INCLUDE_DIR} + ${HDF5_INCLUDE_DIRS} ) -target_link_libraries( - SophireadTests_fastgaussian - FastSophiread - pthread - tbb - spdlog::spdlog - GTest::GTest - GTest::Main +target_link_directories(FastSophiread + PRIVATE + $ENV{CONDA_PREFIX}/lib ) -gtest_discover_tests(SophireadTests_fastgaussian) + +# ----------------- TESTS ----------------- # +function(add_sophiread_test test_name) + add_executable(${test_name} tests/test_${test_name}.cpp) + target_link_libraries(${test_name} + PRIVATE + FastSophiread + pthread + TBB::tbb + spdlog::spdlog + GTest::GTest + GTest::Main + ${ARGN} + ) + gtest_discover_tests(${test_name}) +endfunction() +# Add tests +add_sophiread_test(disk_io ${HDF5_LIBRARIES}) +add_sophiread_test(hit) +add_sophiread_test(tpx3 ${HDF5_LIBRARIES}) +add_sophiread_test(abs) +add_sophiread_test(centroid) +add_sophiread_test(fastgaussian) + # ------------------ Benchmarks ------------------ # -# Raw2Hits Benchmark -add_executable( - SophireadBenchmarks_raw2hits - benchmarks/benchmark_raw2hits.cpp -) -target_link_libraries( - SophireadBenchmarks_raw2hits - FastSophiread - pthread - tbb - spdlog::spdlog - ${HDF5_LIBRARIES} -) -add_custom_command(TARGET SophireadBenchmarks_raw2hits POST_BUILD - COMMAND ${CMAKE_COMMAND} -E create_symlink - ${PROJECT_BINARY_DIR}/FastSophiread/SophireadBenchmarks_raw2hits - ${PROJECT_BINARY_DIR}/FastSophireadBenchmarks_raw2hits.app -) -# Hits2Events Benchmark -add_executable( - SophireadBenchmarks_hits2events - benchmarks/benchmark_hits2events.cpp -) -target_link_libraries( - SophireadBenchmarks_hits2events - FastSophiread - pthread - tbb - spdlog::spdlog -) -add_custom_command(TARGET SophireadBenchmarks_hits2events POST_BUILD - COMMAND ${CMAKE_COMMAND} -E create_symlink - ${PROJECT_BINARY_DIR}/FastSophiread/SophireadBenchmarks_hits2events - ${PROJECT_BINARY_DIR}/FastSophireadBenchmarks_hits2events.app -) -# Raw2Events Benchmark -add_executable( - SophireadBenchmarks_raw2events - benchmarks/benchmark_raw2events.cpp -) -target_link_libraries( - SophireadBenchmarks_raw2events - FastSophiread - pthread - tbb - spdlog::spdlog - ${HDF5_LIBRARIES} -) -add_custom_command(TARGET SophireadBenchmarks_raw2events POST_BUILD - COMMAND ${CMAKE_COMMAND} -E create_symlink - ${PROJECT_BINARY_DIR}/FastSophiread/SophireadBenchmarks_raw2events - ${PROJECT_BINARY_DIR}/FastSophireadBenchmarks_raw2events.app -) -# CLI -add_executable( - FastSophireadBenchmarksCLI +# Define a function to add benchmark targets +function(add_sophiread_benchmark NAME) + set(TARGET_NAME SophireadBenchmarks_${NAME}) + add_executable(${TARGET_NAME} benchmarks/benchmark_${NAME}.cpp) + + target_link_libraries(${TARGET_NAME} + PRIVATE + FastSophiread + pthread + TBB::tbb + spdlog::spdlog + ${ARGN} # Additional libraries passed as arguments + ) + + add_custom_command(TARGET ${TARGET_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E create_symlink + ${PROJECT_BINARY_DIR}/FastSophiread/${TARGET_NAME} + ${PROJECT_BINARY_DIR}/FastSophireadBenchmarks_${NAME}.app + ) +endfunction() + +# Add benchmarks +add_sophiread_benchmark(raw2hits ${HDF5_LIBRARIES}) +add_sophiread_benchmark(hits2events) +add_sophiread_benchmark(raw2events ${HDF5_LIBRARIES}) + +# ----------------- CLI ----------------- # +add_executable(FastSophireadBenchmarksCLI benchmarks/benchmark_mmap.cpp ) -target_link_libraries( - FastSophireadBenchmarksCLI +target_link_libraries(FastSophireadBenchmarksCLI + PRIVATE FastSophiread pthread - tbb + TBB::tbb spdlog::spdlog ${HDF5_LIBRARIES} ) diff --git a/sophiread/FastSophiread/benchmarks/benchmark_mmap.cpp b/sophiread/FastSophiread/benchmarks/benchmark_mmap.cpp index 34f613e..e8169a6 100644 --- a/sophiread/FastSophiread/benchmarks/benchmark_mmap.cpp +++ b/sophiread/FastSophiread/benchmarks/benchmark_mmap.cpp @@ -75,19 +75,19 @@ void saveEventsToHDF5(const std::string out_file_name, const std::vector tof_ns(events.size()); - std::transform(events.begin(), events.end(), tof_ns.begin(), + std::transform(events.cbegin(), events.cend(), tof_ns.begin(), [](const Neutron &event) { return event.getTOF_ns(); }); H5::DataSet tof_ns_dataset = group.createDataSet("tof", float_type, dataspace); tof_ns_dataset.write(tof_ns.data(), float_type); // -- write Nhits std::vector nhits(events.size()); - std::transform(events.begin(), events.end(), nhits.begin(), + std::transform(events.cbegin(), events.cend(), nhits.begin(), [](const Neutron &event) { return event.getNHits(); }); H5::DataSet nhits_dataset = group.createDataSet("nHits", int_type, dataspace); nhits_dataset.write(nhits.data(), int_type); // -- write TOT std::vector tot(events.size()); - std::transform(events.begin(), events.end(), tot.begin(), [](const Neutron &event) { return event.getTOT(); }); + std::transform(events.cbegin(), events.cend(), tot.begin(), [](const Neutron &event) { return event.getTOT(); }); H5::DataSet tot_dataset = group.createDataSet("tot", float_type, dataspace); tot_dataset.write(tot.data(), float_type); // -- close file @@ -260,7 +260,7 @@ int main(int argc, char* argv[]) spdlog::debug("@{:p}, {}", raw_data.map, raw_data.max); - if ( raw_data.map == NULL ) + if ( raw_data.map == nullptr ) { spdlog::error("Insufficient memory: {}", in_tpx3); exit(EXIT_FAILURE); diff --git a/sophiread/FastSophiread/include/hit.h b/sophiread/FastSophiread/include/hit.h index 1850689..af8fa9e 100644 --- a/sophiread/FastSophiread/include/hit.h +++ b/sophiread/FastSophiread/include/hit.h @@ -24,7 +24,9 @@ #include #include -class Hit { +#include "iposition_tof.h" + +class Hit : public IPositionTOF { public: // default constructor Hit() : m_x(0), m_y(0), m_tot(0), m_toa(0), m_ftoa(0), m_tof(0), m_spidertime(0){}; @@ -77,6 +79,11 @@ class Hit { return ss.str(); }; + // Implement the interface methods + double iGetX() const override { return static_cast(getX()); } + double iGetY() const override { return static_cast(getY()); } + double iGetTOF_ns() const override { return getTOF_ns(); } + private: // raw packet directly read from tpx3. int m_x, m_y; // pixel coordinates diff --git a/sophiread/FastSophiread/include/iposition_tof.h b/sophiread/FastSophiread/include/iposition_tof.h new file mode 100644 index 0000000..6f09783 --- /dev/null +++ b/sophiread/FastSophiread/include/iposition_tof.h @@ -0,0 +1,30 @@ +/** + * @file iposition_tof.h + * @author Chen Zhang (zhangc@orn.gov) + * @brief Interface for neutron and hit + * @version 0.1 + * @date 2024-08-20 + * + * @copyright Copyright (c) 2024 + * This program 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. + * + * This program 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 . + */ +#pragma once + +class IPositionTOF { +public: + virtual ~IPositionTOF() = default; + virtual double iGetX() const = 0; + virtual double iGetY() const = 0; + virtual double iGetTOF_ns() const = 0; +}; diff --git a/sophiread/FastSophiread/include/neutron.h b/sophiread/FastSophiread/include/neutron.h index 54fc5b2..f39e43f 100644 --- a/sophiread/FastSophiread/include/neutron.h +++ b/sophiread/FastSophiread/include/neutron.h @@ -24,7 +24,9 @@ #include #include -class Neutron { +#include "iposition_tof.h" + +class Neutron : public IPositionTOF { public: Neutron(const double x, const double y, const double tof, const double tot, const int nHits) : m_x(x), m_y(y), m_tof(tof), m_tot(tot), m_nHits(nHits){}; @@ -34,6 +36,7 @@ class Neutron { double getTOT() const { return m_tot; } double getTOF() const { return m_tof; }; double getTOF_ns() const { return m_tof * m_scale_to_ns_40mhz; }; + double getTOT_ns() const { return m_tot * m_scale_to_ns_40mhz; }; int getNHits() const { return m_nHits; }; std::string toString() const { @@ -42,6 +45,11 @@ class Neutron { return ss.str(); }; + // Implement the interface methods + double iGetX() const override { return getX(); } + double iGetY() const override { return getY(); } + double iGetTOF_ns() const override { return getTOF_ns(); } + private: double m_x, m_y; // pixel coordinates double m_tof; // time of flight diff --git a/sophiread/FastSophiread/src/disk_io.cpp b/sophiread/FastSophiread/src/disk_io.cpp index be45c0f..bb9a475 100644 --- a/sophiread/FastSophiread/src/disk_io.cpp +++ b/sophiread/FastSophiread/src/disk_io.cpp @@ -348,8 +348,8 @@ void saveOrAppendNeutronToHDF5(const std::string &out_file_name, ForwardIterator { {"x", [](const Neutron &neutron) { return neutron.getX(); }}, {"y", [](const Neutron &neutron) { return neutron.getY(); }}, - {"tof", [](const Neutron &neutron) { return neutron.getTOF(); }}, - {"tot", [](const Neutron &neutron) { return neutron.getTOT(); }}, + {"tof_ns", [](const Neutron &neutron) { return neutron.getTOF_ns(); }}, + {"tot_ns", [](const Neutron &neutron) { return neutron.getTOT_ns(); }}, {"nHits", [](const Neutron &neutron) { return neutron.getNHits(); }}, }, append); diff --git a/sophiread/README.md b/sophiread/README.md index 0739f89..49c678a 100644 --- a/sophiread/README.md +++ b/sophiread/README.md @@ -1,19 +1,13 @@ -Sophiread ---------- +# Sophiread -Sophiread is a simple, fast, and extensible toolkit for reading and processing -raw data from the Timepix3 chip. +Sophiread is a simple, fast, and extensible toolkit for reading and processing raw data (`*.tpx3`) from the Timepix3 chip. It provides both command line (CLI) and graphical user interface (GUI) interfaces. -Sophiread is written in C++ and Qt (with `qwt` extension). -The streaming data client, `SophireadStream`, is using a 3rd party library, [readerwriterqueue](https://github.com/cameron314/readerwriterqueue) -to read and process the raw data packet concurrently. +> As of 2024-08-22, the GUI application is still undergoing development to use the new fast sophiread backend, only command line applications are available. -How to build ------------- +## How to build -Sophiread is built using `cmake` and `make` under a sandboxed environment with -`conda` (see [conda](https://conda.io/docs/)). +Sophiread is built using `cmake` and `make` under a sandboxed environment with `conda` (see [conda](https://conda.io/docs/)). The following steps have been tested under MaxOS and Linux, and it is in theory possible to build it under Windows. - Install `conda` or equivalent pacakge manager such as `miniconda`, `mamba`, `micromamba`, etc. @@ -58,48 +52,50 @@ The following steps have been tested under MaxOS and Linux, and it is in theory - `sophiread--Linux.sh`: an installer for Linux - For Mac users with m-series chip, please make the following adjustment: - - Create env with `CONDA_SUBDIR=osx-64 conda create -f environment_mac.yml` - - Currently `mlpack` does not have a `arm64` package from conda, so we need to fallback to x86-64 using Rosetta 2 under the hood. - Install [MacTex](https://www.tug.org/mactex/) before building the documentation. - - __DO NOT__ install `mlpack` from homebrew. - - `mlpack` from homebrew can lead to linking error when building the DBSCAN object. -Use the CLI ------------ +## Use the CLI The CLI is a simple command line interface to read and process raw data from the Timepix3 chip. It is a single executable file, `Sophiread`, which can be found in the `build` directory. The current version of the CLI supports the following input arguments: ```bash -Sophiread [-i input_tpx3] [-u user_defined_params_list] [-H output_hits_HDF5] [-E output_event_HDF5] [-v] +Sophiread -i -H -E [-u ] [-T ] [-f ] [-m ] [-d] [-v] ``` -- `-i`: input raw Timepix3 data file. (Mandatory) -- `-u`: parse user-defined parameter list for clustering algorithsm. Please refer to the `user_defined_params.txt` template for reference. (Optional) -- `-H`: output processed hits with corresponding cluster labels as HDF5 archive. (Optional) -- `-E`: output processed neutron events as HDF5 archive. (Optional) -- `-v`: verbose mode. (Optional) +- `-i `: Input TPX3 file +- `-H `: Output hits HDF5 file +- `-E `: Output events HDF5 file +- `-u `: User configuration JSON file (optional) +- `-T `: Output folder for TIFF TOF images (optional) +- `-f `: Base name for TIFF files (default: tof_image) +- `-m `: TOF mode: 'hit' or 'neutron' (default: neutron) +- `-d`: Enable debug logging +- `-v`: Enable verbose logging -Alternatively, you can use `SophireadStream` to process the raw data packet concurrently. -From the user perspective, it is the same as `Sophiread` but it is much faster for larger data set and has a better memory management. -Unlike `Sophiread`, `SophireadStream` has a much simpler interface: +One **important** thing to check before using this software is that you need to check your chip layout before using it. +By default, `Sophiread` is assuming the detector has a `2x2` layout with a 5 pixel gap between chips. +Each chip has `512x512` pixels. +If your chip has different spec, you will need to modify the source code to make it work for your detector. + +A temporary auto reduction code binary is also available for the commission of [VENUS](https://neutrons.ornl.gov/venus), `venus_auto_reducer`: ```bash -SophireadStream +venus_auto_reducer -i -o [-u ] [-f ] [-m ] [-c ] [-v] [-d] ``` -Use the GUI ------------ - -The GUI is a graphical user interface to read and process raw data from the Timepix3 chip. -It is a single executable file, `SophireadGUI`, which can be found in the `build` directory. -Once the GUI is launched, you can open a raw data file by clicking the `Load Data` button on the top left corner. -The GUI will process the data and display the clustered neutron events in the 2D histogram. +- `-i `: Input directory with TPX3 files +- `-o `: Output directory for TIFF files +- `-u `: User configuration JSON file (optional) +- `-f `: Base name for TIFF files (default: tof_image) +- `-m `: TOF mode: 'hit' or 'neutron' (default: neutron) +- `-c `: Check interval in seconds (default: 5) +- `-d`: Debug output +- `-v`: Verbose output -Important note --------------- +## Important note -The raw data file is a binary file with a specific format, so it is not recommended to open it with a text editor. +The raw data file is a binary file with a specific format, please **DO NOT** try to open it with a text editor as it can corrupt the bytes inside. Additionally, super-pixeling (also known as super resolution) is used to increase the spatial resolution of the data, i.e. bumping the `512x512` native resolution to `4028x4028`. This is done by splitting each pixel into 8x8 sub-pixels via peak fitting. diff --git a/sophiread/SophireadCLI/CMakeLists.txt b/sophiread/SophireadCLI/CMakeLists.txt index c809c4e..cd23498 100644 --- a/sophiread/SophireadCLI/CMakeLists.txt +++ b/sophiread/SophireadCLI/CMakeLists.txt @@ -2,25 +2,46 @@ include_directories( include ${PROJECT_SOURCE_DIR}/FastSophiread/include + ${TIFF_INCLUDE_DIRS} ) # Configure the commandline application set(SRC_FILES src/user_config.cpp - src/sophiread.cpp + src/json_config_parser.cpp + src/sophiread_core.cpp ) # ----------------- CLI APPLICATION ----------------- # add_executable(Sophiread ${SRC_FILES} + src/sophiread.cpp ) set_target_properties(Sophiread PROPERTIES VERSION ${PROJECT_VERSION}) -target_link_libraries( - Sophiread - FastSophiread - tbb - spdlog::spdlog - ${HDF5_LIBRARIES} +target_link_libraries(Sophiread + PRIVATE + FastSophiread + TBB::tbb + spdlog::spdlog + fmt::fmt + nlohmann_json::nlohmann_json + ${HDF5_LIBRARIES} + ${TIFF_LIBRARIES} +) +# +add_executable(venus_auto_reducer +${SRC_FILES} + src/venus_auto_reducer.cpp +) +target_link_libraries(venus_auto_reducer + PRIVATE + FastSophiread + TBB::tbb + spdlog::spdlog + fmt::fmt + nlohmann_json::nlohmann_json + ${HDF5_LIBRARIES} + ${TIFF_LIBRARIES} ) # ----------------- TESTS ----------------- # @@ -30,22 +51,61 @@ add_executable( tests/test_user_config.cpp src/user_config.cpp ) -target_link_libraries( - UserConfigTest - FastSophiread - spdlog::spdlog - GTest::GTest - GTest::Main +target_link_libraries(UserConfigTest + PRIVATE + FastSophiread + spdlog::spdlog + GTest::GTest + GTest::Main ) gtest_discover_tests(UserConfigTest) +# Json config test +add_executable( + JsonConfigParserTest + tests/test_json_config_parser.cpp + src/json_config_parser.cpp +) +target_link_libraries(JsonConfigParserTest + PRIVATE + FastSophiread + spdlog::spdlog + GTest::GTest + GTest::Main + nlohmann_json::nlohmann_json +) +gtest_discover_tests(JsonConfigParserTest) +# core test +add_executable( + SophireadCoreTest + tests/test_sophiread_core.cpp + src/sophiread_core.cpp + src/json_config_parser.cpp +) +target_link_libraries(SophireadCoreTest + PRIVATE + FastSophiread + spdlog::spdlog + GTest::GTest + GTest::Main + nlohmann_json::nlohmann_json + ${HDF5_LIBRARIES} + ${TIFF_LIBRARIES} + TBB::tbb +) +gtest_discover_tests(SophireadCoreTest) # ----------------- SYMLINK ----------------- # # symlink the executable to the build directory add_custom_command(TARGET Sophiread POST_BUILD - COMMAND ${CMAKE_COMMAND} -E create_symlink + COMMAND ${CMAKE_COMMAND} -E create_symlink ${PROJECT_BINARY_DIR}/SophireadCLI/Sophiread ${PROJECT_BINARY_DIR}/Sophiread ) +add_custom_command(TARGET venus_auto_reducer POST_BUILD + COMMAND ${CMAKE_COMMAND} -E create_symlink + ${PROJECT_BINARY_DIR}/SophireadCLI/venus_auto_reducer + ${PROJECT_BINARY_DIR}/venus_auto_reducer +) # ----------------- INSTALL ----------------- # if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) diff --git a/sophiread/SophireadCLI/include/iconfig.h b/sophiread/SophireadCLI/include/iconfig.h new file mode 100644 index 0000000..a9b4120 --- /dev/null +++ b/sophiread/SophireadCLI/include/iconfig.h @@ -0,0 +1,38 @@ +/** + * @file iconfig.h + * @author Chen Zhang (zhangc@orn.gov) + * @brief Config interface + * @version 0.1 + * @date 2024-08-16 + * + * @copyright Copyright (c) 2024 + * This program 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. + * + * This program 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 . + */ +#pragma once + +#include +#include + +class IConfig { +public: + virtual ~IConfig() = default; + + virtual double getABSRadius() const = 0; + virtual unsigned long int getABSMinClusterSize() const = 0; + virtual unsigned long int getABSSpiderTimeRange() const = 0; + virtual std::vector getTOFBinEdges() const = 0; + virtual double getSuperResolution() const = 0; + + virtual std::string toString() const = 0; +}; \ No newline at end of file diff --git a/sophiread/SophireadCLI/include/json_config_parser.h b/sophiread/SophireadCLI/include/json_config_parser.h new file mode 100644 index 0000000..f1e1dd2 --- /dev/null +++ b/sophiread/SophireadCLI/include/json_config_parser.h @@ -0,0 +1,57 @@ +/** + * @file json_config_parser.h + * @author Chen Zhang (zhangc@orn.gov) + * @brief Class to store user-defined configuration (JSON) for clustering algorithms. + * @version 0.1 + * @date 2024-08-16 + * + * @copyright Copyright (c) 2024 + * This program 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. + * + * This program 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 . + */ +#pragma once + +#include +#include +#include +#include "iconfig.h" +#include "tof_binning.h" + +class JSONConfigParser : public IConfig { +public: + static JSONConfigParser createDefault(); + static JSONConfigParser fromFile(const std::string& filepath); + + double getABSRadius() const override; + unsigned long int getABSMinClusterSize() const override; + unsigned long int getABSSpiderTimeRange() const override; + std::vector getTOFBinEdges() const override; + double getSuperResolution() const override; + + std::string toString() const override; + +private: + JSONConfigParser(const nlohmann::json& config); + nlohmann::json m_config; + TOFBinning m_tof_binning; + + void parseTOFBinning(); + + // Default values + static constexpr double DEFAULT_ABS_RADIUS = 5.0; + static constexpr unsigned long int DEFAULT_ABS_MIN_CLUSTER_SIZE = 1; + static constexpr unsigned long int DEFAULT_ABS_SPIDER_TIME_RANGE = 75; + static constexpr int DEFAULT_TOF_BINS = 1500; + static constexpr double DEFAULT_TOF_MAX = 16.7e-3; // 16.7 milliseconds + static constexpr double DEFAULT_SUPER_RESOLUTION = 1.0; +}; diff --git a/sophiread/SophireadCLI/include/sophiread_core.h b/sophiread/SophireadCLI/include/sophiread_core.h new file mode 100644 index 0000000..b90c991 --- /dev/null +++ b/sophiread/SophireadCLI/include/sophiread_core.h @@ -0,0 +1,48 @@ +/** + * @file sophiread_core.h + * @author Chen Zhang (zhangc@orn.gov) + * @brief Core functions for CLI application + * @version 0.1 + * @date 2024-08-21 + * + * @copyright Copyright (c) 2024 + * This program 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. + * + * This program 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 . + */ +#pragma once + +#include +#include +#include "abs.h" +#include "tpx3_fast.h" +#include "iconfig.h" + +namespace sophiread { + +std::vector timedReadDataToCharVec(const std::string& in_tpx3); +std::vector timedFindTPX3H(const std::vector& rawdata); +void timedLocateTimeStamp(std::vector& batches, const std::vector& rawdata); +void timedProcessing(std::vector& batches, const std::vector& raw_data, const IConfig& config); +void timedSaveHitsToHDF5(const std::string& out_hits, std::vector& batches); +void timedSaveEventsToHDF5(const std::string& out_events, std::vector& batches); +std::vector>> timedCreateTOFImages( + const std::vector& batches, + double super_resolution, + const std::vector& tof_bin_edges, + const std::string& mode); +void timedSaveTOFImagingToTIFF( + const std::string& out_tof_imaging, + const std::vector>>& tof_images, + const std::vector& tof_bin_edges, + const std::string& tof_filename_base); +} // namespace sophiread diff --git a/sophiread/SophireadCLI/include/tiff_types.h b/sophiread/SophireadCLI/include/tiff_types.h new file mode 100644 index 0000000..f85a4ab --- /dev/null +++ b/sophiread/SophireadCLI/include/tiff_types.h @@ -0,0 +1,18 @@ +/** + * @file tiff_types.h + * @author Chen Zhang (zhangc@ornl.gov) + * @brief Define TIFF bit depth + * @version 0.1 + * @date 2024-08-23 + * + * @copyright Copyright (c) 2024 + * SPDX - License - Identifier: GPL - 3.0 + + */ +#pragma once + +#include + +// Define bit-depth types for TIFF images +using TIFF8Bit = uint8_t; // 8-bit +using TIFF16Bit = uint16_t; // 16-bit +using TIFF32Bit = uint32_t; // 32-bit \ No newline at end of file diff --git a/sophiread/SophireadCLI/include/tof_binning.h b/sophiread/SophireadCLI/include/tof_binning.h new file mode 100644 index 0000000..b79167e --- /dev/null +++ b/sophiread/SophireadCLI/include/tof_binning.h @@ -0,0 +1,56 @@ +/** + * @file tof_binning.h + * @author Chen Zhang (zhangc@orn.gov) + * @brief TOF binning + * @version 0.1 + * @date 2024-08-16 + * + * @copyright Copyright (c) 2024 + * This program 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. + * + * This program 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 . + */ +#pragma once + +#include +#include + +struct TOFBinning { + std::optional num_bins; + std::optional tof_max; + std::vector custom_edges; + + // Default constructor + TOFBinning() : num_bins(1500), tof_max(1.0/60) {} + + bool isUniform() const { + return num_bins.has_value() && tof_max.has_value() && custom_edges.empty(); + } + + bool isCustom() const { + return !custom_edges.empty(); + } + + std::vector getBinEdges() const { + if (isCustom()) { + return custom_edges; + } + + int bins = num_bins.value_or(1500); + double max = tof_max.value_or(1.0/60); + std::vector edges(bins + 1); + for (int i = 0; i <= bins; ++i) { + edges[i] = max * i / bins; + } + return edges; + } +}; \ No newline at end of file diff --git a/sophiread/SophireadCLI/include/user_config.h b/sophiread/SophireadCLI/include/user_config.h index 3113ad7..5e8238e 100644 --- a/sophiread/SophireadCLI/include/user_config.h +++ b/sophiread/SophireadCLI/include/user_config.h @@ -23,35 +23,39 @@ #pragma once #include +#include "iconfig.h" +#include "tof_binning.h" -class UserConfig { +class UserConfig : public IConfig { public: - UserConfig(){}; - UserConfig(const double abs_radius, unsigned long int abs_min_cluster_size, unsigned long int abs_spider_time_range) - : m_abs_radius(abs_radius), - m_abs_min_cluster_size(abs_min_cluster_size), - m_abs_spider_time_range(abs_spider_time_range){}; + UserConfig(); + UserConfig(double abs_radius, unsigned long int abs_min_cluster_size, unsigned long int abs_spider_time_range); - double getABSRadius() const { return m_abs_radius; }; - void setABSRadius(const double abs_radius) { m_abs_radius = abs_radius; }; + double getABSRadius() const override { return m_abs_radius; } + void setABSRadius(double abs_radius) { m_abs_radius = abs_radius; } - unsigned long int getABSMinClusterSize() const { return m_abs_min_cluster_size; }; - void setABSMinClusterSize(const unsigned long int abs_min_cluster_size) { - m_abs_min_cluster_size = abs_min_cluster_size; - }; + unsigned long int getABSMinClusterSize() const override { return m_abs_min_cluster_size; } + void setABSMinClusterSize(unsigned long int abs_min_cluster_size) { m_abs_min_cluster_size = abs_min_cluster_size; } - unsigned long int getABSSpidertimeRange() const { return m_abs_spider_time_range; }; - void setABSSpidertimeRange(const unsigned long int abs_spider_time_range) { - m_abs_spider_time_range = abs_spider_time_range; - }; + unsigned long int getABSSpiderTimeRange() const override { return m_abs_spider_time_range; } + void setABSSpiderTimeRange(unsigned long int abs_spider_time_range) { m_abs_spider_time_range = abs_spider_time_range; } - std::string toString() const; + std::vector getTOFBinEdges() const override { return m_tof_binning.getBinEdges(); } + void setTOFBinning(const TOFBinning& tof_binning) { m_tof_binning = tof_binning; } + void setCustomTOFBinEdges(const std::vector& edges) { m_tof_binning.custom_edges = edges; } + + // no super resolution for old config format + double getSuperResolution() const override {return m_super_resolution; } + void setSuperResolution(double super_resolution) {m_super_resolution = super_resolution; } + + std::string toString() const override; private: - // ABS members (see abs.h for details) double m_abs_radius = 5.0; unsigned long int m_abs_min_cluster_size = 1; unsigned long int m_abs_spider_time_range = 75; + TOFBinning m_tof_binning; + double m_super_resolution = 1.0; }; -UserConfig parseUserDefinedConfigurationFile(const std::string& filepath); \ No newline at end of file +UserConfig parseUserDefinedConfigurationFile(const std::string& filepath); diff --git a/sophiread/SophireadCLI/src/json_config_parser.cpp b/sophiread/SophireadCLI/src/json_config_parser.cpp new file mode 100644 index 0000000..e2bef14 --- /dev/null +++ b/sophiread/SophireadCLI/src/json_config_parser.cpp @@ -0,0 +1,149 @@ +/** + * @file json_config_parser.cpp + * @author Chen Zhang (zhangc@orn.gov) + * @brief Class to store user-defined configuration (JSON) for clustering algorithms. + * @version 0.1 + * @date 2024-08-16 + * + * @copyright Copyright (c) 2024 + * This program 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. + * + * This program 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 . + */ +#include "json_config_parser.h" +#include +#include + + +JSONConfigParser JSONConfigParser::createDefault() { + nlohmann::json default_config = { + {"abs", { + {"radius", DEFAULT_ABS_RADIUS}, + {"min_cluster_size", DEFAULT_ABS_MIN_CLUSTER_SIZE}, + {"spider_time_range", DEFAULT_ABS_SPIDER_TIME_RANGE} + }}, + {"tof_imaging", { + {"uniform_bins", { + {"num_bins", DEFAULT_TOF_BINS}, + {"end", DEFAULT_TOF_MAX} + }}, + {"super_resolution", DEFAULT_SUPER_RESOLUTION} + }} + }; + + return JSONConfigParser(default_config); +} + + +/** + * @brief Build JSONConfigParser from a given file + * @param filepath Path to the JSON configuration file + * @return JSONConfigParser + */ +JSONConfigParser JSONConfigParser::fromFile(const std::string& filepath) { + std::ifstream file(filepath); + if (!file.is_open()) { + throw std::runtime_error("Failed to open configuration file: " + filepath); + } + + nlohmann::json config; + try { + file >> config; + } catch (const nlohmann::json::exception& e) { + throw std::runtime_error("Error parsing JSON file: " + std::string(e.what())); + } + + return JSONConfigParser(config); +} + +/** + * @brief Construct a new JSONConfigParser::JSONConfigParser object + * @param config JSON configuration object + */ +JSONConfigParser::JSONConfigParser(const nlohmann::json& config) : m_config(config) { + parseTOFBinning(); +} + +/** + * @brief Get the ABS Radius + * @return double + */ +double JSONConfigParser::getABSRadius() const { + return m_config.value("/abs/radius"_json_pointer, DEFAULT_ABS_RADIUS); +} + +/** + * @brief Get the ABS Min Cluster Size + * @return unsigned long int + */ +unsigned long int JSONConfigParser::getABSMinClusterSize() const { + return m_config.value("/abs/min_cluster_size"_json_pointer, DEFAULT_ABS_MIN_CLUSTER_SIZE); +} + +/** + * @brief Get the ABS Spider Time Range + * @return unsigned long int + */ +unsigned long int JSONConfigParser::getABSSpiderTimeRange() const { + return m_config.value("/abs/spider_time_range"_json_pointer, DEFAULT_ABS_SPIDER_TIME_RANGE); +} + +/** + * @brief Get the TOF Bin Edges + * @return std::vector + */ +std::vector JSONConfigParser::getTOFBinEdges() const { + return m_tof_binning.getBinEdges(); +} + +double JSONConfigParser::getSuperResolution() const { + return m_config.value("/tof_imaging/super_resolution"_json_pointer, DEFAULT_SUPER_RESOLUTION); +} + +/** + * @brief Parse the TOF binning configuration + */ +void JSONConfigParser::parseTOFBinning() { + if (m_config.contains("/tof_imaging/bin_edges"_json_pointer)) { + m_tof_binning.custom_edges = m_config["/tof_imaging/bin_edges"_json_pointer].get>(); + } else if (m_config.contains("/tof_imaging/uniform_bins"_json_pointer)) { + const auto& uniform = m_config["/tof_imaging/uniform_bins"_json_pointer]; + m_tof_binning.num_bins = uniform.value("num_bins", DEFAULT_TOF_BINS); + m_tof_binning.tof_max = uniform.value("end", DEFAULT_TOF_MAX); + } else { + // Use default values + m_tof_binning.num_bins = DEFAULT_TOF_BINS; + m_tof_binning.tof_max = DEFAULT_TOF_MAX; + } +} + +/** + * @brief Get a string representation of the configuration + * @return std::string + */ +std::string JSONConfigParser::toString() const { + std::stringstream ss; + ss << "ABS: radius=" << getABSRadius() + << ", min_cluster_size=" << getABSMinClusterSize() + << ", spider_time_range=" << getABSSpiderTimeRange(); + + if (m_tof_binning.isCustom()) { + ss << ", Custom TOF binning with " << m_tof_binning.custom_edges.size() - 1 << " bins"; + } else { + ss << ", TOF bins=" << m_tof_binning.num_bins.value_or(DEFAULT_TOF_BINS) + << ", TOF max=" << (m_tof_binning.tof_max.value_or(DEFAULT_TOF_MAX) * 1000) << " ms"; + } + + ss << ", Super Resolution=" << getSuperResolution(); + + return ss.str(); +} diff --git a/sophiread/SophireadCLI/src/sophiread.cpp b/sophiread/SophireadCLI/src/sophiread.cpp index 1ca517b..b2b4f0a 100644 --- a/sophiread/SophireadCLI/src/sophiread.cpp +++ b/sophiread/SophireadCLI/src/sophiread.cpp @@ -1,137 +1,120 @@ /** + * @file sophiread.cpp + * @author Chen Zhang (zhangc@orn.gov) + * @author Su-Ann Chong (chongs@ornl.gov) * @brief CLI for reading Timepix3 raw data and parse it into neutron event * files and a tiff image (for visual inspection). + * @date 2024-08-19 * + * @copyright Copyright (c) 2024 + * This program 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. + * + * This program 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 . */ #include #include #include +#include #include #include #include #include +#include #include "abs.h" #include "disk_io.h" #include "tpx3_fast.h" #include "user_config.h" - -/** - * @brief Timed read raw data to char vector. - * - * @param[in] in_tpx3 - * @return std::vector - */ -std::vector timedReadDataToCharVec(const std::string &in_tpx3) { - auto start = std::chrono::high_resolution_clock::now(); - auto raw_data = readTPX3RawToCharVec(in_tpx3); - auto end = std::chrono::high_resolution_clock::now(); - auto elapsed = std::chrono::duration_cast(end - start).count(); - spdlog::info("Read raw data: {} s", elapsed / 1e6); - - return raw_data; +#include "json_config_parser.h" +#include "sophiread_core.h" + +struct ProgramOptions { + std::string input_tpx3; + std::string output_hits; + std::string output_events; + std::string config_file; + std::string output_tof_imaging; + std::string tof_filename_base = "tof_image"; + std::string tof_mode = "neutron"; + bool debug_logging = false; + bool verbose = false; +}; + +void print_usage(const char* program_name) { + spdlog::info("Usage: {} -i -H -E [-u ] [-T ] [-f ] [-m ] [-d] [-v]", program_name); + spdlog::info("Options:"); + spdlog::info(" -i Input TPX3 file"); + spdlog::info(" -H Output hits HDF5 file"); + spdlog::info(" -E Output events HDF5 file"); + spdlog::info(" -u User configuration JSON file (optional)"); + spdlog::info(" -T Output folder for TIFF TOF images (optional)"); + spdlog::info(" -f Base name for TIFF files (default: tof_image)"); + spdlog::info(" -m TOF mode: 'hit' or 'neutron' (default: neutron)"); + spdlog::info(" -d Enable debug logging"); + spdlog::info(" -v Enable verbose logging"); } -/** - * @brief Timed find TPX3H. - * - * @param[in] rawdata - * @return std::vector - */ -std::vector timedFindTPX3H(const std::vector &rawdata) { - auto start = std::chrono::high_resolution_clock::now(); - auto batches = findTPX3H(rawdata); - auto end = std::chrono::high_resolution_clock::now(); - auto elapsed = std::chrono::duration_cast(end - start).count(); - spdlog::info("Locate all headers: {} s", elapsed / 1e6); - - return batches; -} - -/** - * @brief Timed locate timestamp. - * - * @param[in, out] batches - * @param[in] rawdata - */ -void timedLocateTimeStamp(std::vector &batches, const std::vector &rawdata) { - auto start = std::chrono::high_resolution_clock::now(); - unsigned long tdc_timestamp = 0; - unsigned long long gdc_timestamp = 0; - unsigned long timer_lsb32 = 0; - for (auto &tpx3 : batches) { - updateTimestamp(tpx3, rawdata, tdc_timestamp, gdc_timestamp, timer_lsb32); - } - auto end = std::chrono::high_resolution_clock::now(); - auto elapsed = std::chrono::duration_cast(end - start).count(); - spdlog::info("Locate all timestamps: {} s", elapsed / 1e6); -} +ProgramOptions parse_arguments(int argc, char* argv[]) { + ProgramOptions options; + int opt; + + while ((opt = getopt(argc, argv, "i:H:E:u:T:f:m:dv")) != -1) { + switch (opt) { + case 'i': + options.input_tpx3 = optarg; + break; + case 'H': + options.output_hits = optarg; + break; + case 'E': + options.output_events = optarg; + break; + case 'u': + options.config_file = optarg; + break; + case 'T': + options.output_tof_imaging = optarg; + break; + case 'f': + options.tof_filename_base = optarg; + break; + case 'm': + options.tof_mode = optarg; + break; + case 'd': + options.debug_logging = true; + break; + case 'v': + options.verbose = true; + break; + default: + print_usage(argv[0]); + throw std::runtime_error(std::string("Invalid argument: ") + static_cast(optopt)); + } + } -/** - * @brief Timed hits extraction and clustering via multi-threading. - * - * @param[in, out] batches - * @param[in] rawdata - * @param[in] config - */ -void timedProcessing(std::vector &batches, const std::vector &raw_data, const UserConfig &config) { - auto start = std::chrono::high_resolution_clock::now(); - tbb::parallel_for(tbb::blocked_range(0, batches.size()), [&](const tbb::blocked_range &r) { - // Define ABS algorithm with user-defined parameters for each thread - auto abs_alg_mt = - std::make_unique(config.getABSRadius(), config.getABSMinClusterSize(), config.getABSSpidertimeRange()); - - for (size_t i = r.begin(); i != r.end(); ++i) { - auto &tpx3 = batches[i]; - extractHits(tpx3, raw_data); - - abs_alg_mt->reset(); - abs_alg_mt->set_method("centroid"); - abs_alg_mt->fit(tpx3.hits); - - tpx3.neutrons = abs_alg_mt->get_events(tpx3.hits); + // Validate required arguments + if (options.input_tpx3.empty()) { + print_usage(argv[0]); + throw std::runtime_error("Missing required arguments"); } - }); - auto end = std::chrono::high_resolution_clock::now(); - auto elapsed = std::chrono::duration_cast(end - start).count(); - spdlog::info("Process all hits -> neutrons: {} s", elapsed / 1e6); -} -/** - * @brief Timed save hits to HDF5. - * - * @param[in] out_hits - * @param[in] hits - */ -void timedSaveHitsToHDF5(const std::string &out_hits, std::vector &batches) { - auto start = std::chrono::high_resolution_clock::now(); - // move all hits into a single vector - std::vector hits; - for (const auto &tpx3 : batches) { - auto tpx3_hits = tpx3.hits; - hits.insert(hits.end(), tpx3_hits.begin(), tpx3_hits.end()); - } - // save hits to HDF5 file - saveHitsToHDF5(out_hits, hits); - auto end = std::chrono::high_resolution_clock::now(); - auto elapsed = std::chrono::duration_cast(end - start).count(); - spdlog::info("Save hits to HDF5: {} s", elapsed / 1e6); -} + // Validate TOF mode + if (options.tof_mode != "hit" && options.tof_mode != "neutron") { + throw std::runtime_error("Invalid TOF mode. Use 'hit' or 'neutron'."); + } -void timedSaveEventsToHDF5(const std::string &out_events, std::vector &batches) { - auto start = std::chrono::high_resolution_clock::now(); - // move all events into a single vector - std::vector events; - for (const auto &tpx3 : batches) { - auto tpx3_events = tpx3.neutrons; - events.insert(events.end(), tpx3_events.begin(), tpx3_events.end()); - } - // save events to HDF5 file - saveNeutronToHDF5(out_events, events); - auto end = std::chrono::high_resolution_clock::now(); - auto elapsed = std::chrono::duration_cast(end - start).count(); - spdlog::info("Save events to HDF5: {} s", elapsed / 1e6); + return options; } /** @@ -142,84 +125,76 @@ void timedSaveEventsToHDF5(const std::string &out_events, std::vector &bat * @return int */ int main(int argc, char *argv[]) { - // processing command line arguments - std::string in_tpx3; - std::string out_hits; - std::string out_events; - std::string user_defined_params; - bool verbose = false; - int opt; - - // help message string - std::string help_msg = "Usage: " + std::string(argv[0]) + " [-i input_tpx3] " + " [-H output_hits_HDF5] " + - " [-E output_event_HDF5] " + " [-u user_defined_params]" + " [-v]"; - - // parse command line arguments - while ((opt = getopt(argc, argv, "i:H:E:u:v")) != -1) { - switch (opt) { - case 'i': // input file - in_tpx3 = optarg; - break; - case 'H': // output hits file (HDF5) - out_hits = optarg; - break; - case 'E': // output event file - out_events = optarg; - break; - case 'u': // user-defined params - user_defined_params = optarg; - break; - case 'v': - verbose = true; - break; - default: - spdlog::error(help_msg); + try { + ProgramOptions options = parse_arguments(argc, argv); + + // Set logging level based on debug and verbose flags + if (options.debug_logging) { + spdlog::set_level(spdlog::level::debug); + spdlog::debug("Debug logging enabled"); + } else if (options.verbose) { + spdlog::set_level(spdlog::level::info); + } else { + spdlog::set_level(spdlog::level::warn); + } + + spdlog::info("Input file: {}", options.input_tpx3); + spdlog::info("Output hits file: {}", options.output_hits); + spdlog::info("Output events file: {}", options.output_events); + spdlog::info("Configuration file: {}", options.config_file); + spdlog::info("TOF imaging folder: {}", options.output_tof_imaging); + spdlog::info("TOF filename base: {}", options.tof_filename_base); + spdlog::info("TOF mode: {}", options.tof_mode); + + // Load configuration + std::unique_ptr config; + if (!options.config_file.empty()) { + std::string extension = std::filesystem::path(options.config_file).extension().string(); + if (extension == ".json") { + config = std::make_unique(JSONConfigParser::fromFile(options.config_file)); + } else { + spdlog::warn("Deprecated configuration format detected. Please switch to JSON format."); + config = std::make_unique(parseUserDefinedConfigurationFile(options.config_file)); + } + } else { + spdlog::info("No configuration file provided. Using default JSON configuration."); + config = std::make_unique(JSONConfigParser::createDefault()); + } + + spdlog::info("Configuration: {}", config->toString()); + + // Process raw data + auto start = std::chrono::high_resolution_clock::now(); + auto raw_data = sophiread::timedReadDataToCharVec(options.input_tpx3); + auto batches = sophiread::timedFindTPX3H(raw_data); + sophiread::timedLocateTimeStamp(batches, raw_data); + sophiread::timedProcessing(batches, raw_data, *config); + auto end = std::chrono::high_resolution_clock::now(); + auto elapsed = std::chrono::duration_cast(end - start).count(); + spdlog::info("Total processing time: {} s", elapsed / 1e6); + + // Release memory of raw data + std::vector().swap(raw_data); + + // Generate and save TOF images + if (!options.output_tof_imaging.empty()) { + auto tof_images = sophiread::timedCreateTOFImages(batches, config->getSuperResolution(), config->getTOFBinEdges(), options.tof_mode); + sophiread::timedSaveTOFImagingToTIFF(options.output_tof_imaging, tof_images, config->getTOFBinEdges(), options.tof_filename_base); + } + + // Save hits and events + if (!options.output_hits.empty()) { + sophiread::timedSaveHitsToHDF5(options.output_hits, batches); + } + + if (!options.output_events.empty()) { + sophiread::timedSaveEventsToHDF5(options.output_events, batches); + } + + } catch (const std::exception& e) { + spdlog::error("Error: {}", e.what()); return 1; } - } - - // If provided user-defined params, parse it - UserConfig config; - if (!user_defined_params.empty()) { - config = parseUserDefinedConfigurationFile(user_defined_params); - } - - // recap - if (verbose) { - spdlog::info("Input file: {}", in_tpx3); - spdlog::info("Output hits file: {}", out_hits); - spdlog::info("Output events file: {}", out_events); - } - - // read raw data - if (in_tpx3.empty()) { - spdlog::error("Error: no input file specified."); - spdlog::error(help_msg); - return 1; - } - - // process file to hits - auto start = std::chrono::high_resolution_clock::now(); - auto raw_data = timedReadDataToCharVec(in_tpx3); - auto batches = timedFindTPX3H(raw_data); - timedLocateTimeStamp(batches, raw_data); - timedProcessing(batches, raw_data, config); - auto end = std::chrono::high_resolution_clock::now(); - auto elapsed = std::chrono::duration_cast(end - start).count(); - spdlog::info("Total processing time: {} s", elapsed / 1e6); - - // release memory of raw data - std::vector().swap(raw_data); - - // save hits to HDF5 file - if (!out_hits.empty()) { - timedSaveHitsToHDF5(out_hits, batches); - } - - // save events to HDF5 file - if (!out_events.empty()) { - timedSaveEventsToHDF5(out_events, batches); - } - - return 0; + + return 0; } diff --git a/sophiread/SophireadCLI/src/sophiread_core.cpp b/sophiread/SophireadCLI/src/sophiread_core.cpp new file mode 100644 index 0000000..41cb203 --- /dev/null +++ b/sophiread/SophireadCLI/src/sophiread_core.cpp @@ -0,0 +1,384 @@ +/** + * @file sophiread_core.cpp + * @author Chen Zhang (zhangc@orn.gov) + * @author Su-Ann Chong (chongs@ornl.gov) + * @brief CLI for reading Timepix3 raw data and parse it into neutron event + * files and a tiff image (for visual inspection). + * @date 2024-08-21 + * + * @copyright Copyright (c) 2024 + * This program 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. + * + * This program 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 . + */ +#include +#include +#include +#include +#include +#include +#include "disk_io.h" +#include "sophiread_core.h" +#include "tiff_types.h" + +namespace sophiread { + +/** + * @brief Timed read raw data to char vector. + * + * @param[in] in_tpx3 + * @return std::vector + */ +std::vector timedReadDataToCharVec(const std::string &in_tpx3) { + auto start = std::chrono::high_resolution_clock::now(); + auto raw_data = readTPX3RawToCharVec(in_tpx3); + auto end = std::chrono::high_resolution_clock::now(); + auto elapsed = std::chrono::duration_cast(end - start).count(); + spdlog::info("Read raw data: {} s", elapsed / 1e6); + + return raw_data; +} + +/** + * @brief Timed find TPX3H. + * + * @param[in] rawdata + * @return std::vector + */ +std::vector timedFindTPX3H(const std::vector &rawdata) { + auto start = std::chrono::high_resolution_clock::now(); + auto batches = findTPX3H(rawdata); + auto end = std::chrono::high_resolution_clock::now(); + auto elapsed = std::chrono::duration_cast(end - start).count(); + spdlog::info("Locate all headers: {} s", elapsed / 1e6); + + return batches; +} + +/** + * @brief Timed locate timestamp. + * + * @param[in, out] batches + * @param[in] rawdata + */ +void timedLocateTimeStamp(std::vector &batches, const std::vector &rawdata) { + auto start = std::chrono::high_resolution_clock::now(); + unsigned long tdc_timestamp = 0; + unsigned long long gdc_timestamp = 0; + unsigned long timer_lsb32 = 0; + for (auto &tpx3 : batches) { + updateTimestamp(tpx3, rawdata, tdc_timestamp, gdc_timestamp, timer_lsb32); + } + auto end = std::chrono::high_resolution_clock::now(); + auto elapsed = std::chrono::duration_cast(end - start).count(); + spdlog::info("Locate all timestamps: {} s", elapsed / 1e6); +} + +/** + * @brief Timed hits extraction and clustering via multi-threading. + * + * @param[in, out] batches + * @param[in] rawdata + * @param[in] config + */ +void timedProcessing(std::vector &batches, const std::vector &raw_data, const IConfig &config) { + auto start = std::chrono::high_resolution_clock::now(); + tbb::parallel_for(tbb::blocked_range(0, batches.size()), [&](const tbb::blocked_range &r) { + // Define ABS algorithm with user-defined parameters for each thread + auto abs_alg_mt = + std::make_unique(config.getABSRadius(), config.getABSMinClusterSize(), config.getABSSpiderTimeRange()); + + for (size_t i = r.begin(); i != r.end(); ++i) { + auto &tpx3 = batches[i]; + extractHits(tpx3, raw_data); + + abs_alg_mt->reset(); + abs_alg_mt->set_method("centroid"); + abs_alg_mt->fit(tpx3.hits); + + tpx3.neutrons = abs_alg_mt->get_events(tpx3.hits); + } + }); + auto end = std::chrono::high_resolution_clock::now(); + auto elapsed = std::chrono::duration_cast(end - start).count(); + spdlog::info("Process all hits -> neutrons: {} s", elapsed / 1e6); +} + +/** + * @brief Timed save hits to HDF5. + * + * @param[in] out_hits + * @param[in] hits + */ +void timedSaveHitsToHDF5(const std::string &out_hits, std::vector &batches) { + auto start = std::chrono::high_resolution_clock::now(); + // move all hits into a single vector + std::vector hits; + for (const auto &tpx3 : batches) { + const auto &tpx3_hits = tpx3.hits; + hits.insert(hits.end(), tpx3_hits.cbegin(), tpx3_hits.cend()); + } + // save hits to HDF5 file + saveHitsToHDF5(out_hits, hits); + auto end = std::chrono::high_resolution_clock::now(); + auto elapsed = std::chrono::duration_cast(end - start).count(); + spdlog::info("Save hits to HDF5: {} s", elapsed / 1e6); +} + +/** + * @brief Timed save events to HDF5. + * + * @param[in] out_events + * @param[in] batches + */ +void timedSaveEventsToHDF5(const std::string &out_events, std::vector &batches) { + auto start = std::chrono::high_resolution_clock::now(); + // move all events into a single vector + std::vector events; + for (const auto &tpx3 : batches) { + const auto &tpx3_events = tpx3.neutrons; + events.insert(events.end(), tpx3_events.cbegin(), tpx3_events.cend()); + } + // save events to HDF5 file + saveNeutronToHDF5(out_events, events); + auto end = std::chrono::high_resolution_clock::now(); + auto elapsed = std::chrono::duration_cast(end - start).count(); + spdlog::info("Save events to HDF5: {} s", elapsed / 1e6); +} + +/** + * @brief Timed create TOF images. + * + * This function creates time-of-flight (TOF) images based on the provided batches of TPX3 data. The TOF images are 2D histograms that represent the distribution of neutron events in space for different TOF bins. The function takes into account the super resolution, TOF bin edges, and the mode of operation (hit or neutron) to generate the TOF images. + * + * @param[in] batches The vector of TPX3 batches containing the neutron events. + * @param[in] super_resolution The super resolution factor used to calculate the dimensions of each 2D histogram. + * @param[in] tof_bin_edges The vector of TOF bin edges used to determine the TOF bin for each neutron event. + * @param[in] mode The mode of operation, either "hit" or "neutron", which determines the type of events to process. + * @return std::vector>> The vector of TOF images, where each TOF image is a 2D histogram representing the distribution of neutron events in space for a specific TOF bin. + */ +std::vector>> timedCreateTOFImages( + const std::vector& batches, + double super_resolution, + const std::vector& tof_bin_edges, + const std::string& mode) { + + auto start = std::chrono::high_resolution_clock::now(); + + // Initialize the TOF images container + std::vector>> tof_images(tof_bin_edges.size() - 1); + + // Sanity checks + if (tof_bin_edges.size() < 2) { + spdlog::error("Invalid TOF bin edges: at least 2 edges are required"); + return {}; + } + if (batches.empty()) { + spdlog::error("No batches to process"); + return tof_images; + } + + // Calculate the dimensions of each 2D histogram based on super_resolution + // one chip: 0-255 pixel pos + // gap: 5 + // total: 0-255 + 5 + 0-255 -> 517 (TPX3@VENUS only) + int dim_x = static_cast(517 * super_resolution); + int dim_y = static_cast(517 * super_resolution); + + spdlog::debug("Creating TOF images with dimensions: {} x {}", dim_x, dim_y); + spdlog::debug("tof_bin_edges size: {}", tof_bin_edges.size()); + if (!tof_bin_edges.empty()) { + spdlog::debug("First bin edge: {}, Last bin edge: {}", tof_bin_edges.front(), tof_bin_edges.back()); + } + + // Initialize each TOF bin's 2D histogram + for (auto& tof_image : tof_images) { + tof_image.resize(dim_y, std::vector(dim_x, 0)); + } + + // Process neutrons from all batches + size_t total_entries = 0; + size_t binned_entries = 0; + + for (size_t batch_index = 0; batch_index < batches.size(); ++batch_index) { + const auto& batch = batches[batch_index]; + spdlog::debug("Processing batch {}", batch_index); + + std::vector entries; + if (mode == "hit") { + entries.reserve(batch.hits.size()); + for (const auto& hit : batch.hits) { + entries.push_back(static_cast(&hit)); + } + } else { + entries.reserve(batch.neutrons.size()); + for (const auto& neutron : batch.neutrons) { + entries.push_back(static_cast(&neutron)); + } + } + + if (entries.empty()) { + spdlog::debug("Batch {} is empty", batch_index); + continue; + } + + for (const auto& entry : entries) { + total_entries++; + const double tof_ns = entry->iGetTOF_ns(); + const double tof_s = tof_ns/1e9; + + // Find the correct TOF bin + // NOTE: tof_bin_edges are in sec, and tof_ns are in nano secs + spdlog::debug("tof_ns: {}, tof_ns/1e9: {}", tof_ns, tof_s); + + if (const auto it = std::lower_bound(tof_bin_edges.cbegin(), tof_bin_edges.cend(), tof_s); it != tof_bin_edges.cbegin()) { + const size_t bin_index = std::distance(tof_bin_edges.cbegin(), it) - 1; + + // Calculate the x and y indices in the 2D histogram + const int x = std::round(entry->iGetX() * super_resolution); + const int y = std::round(entry->iGetY() * super_resolution); + + // Ensure x and y are within bounds + if (x >= 0 && x < dim_x && y >= 0 && y < dim_y) { + // Increment the count in the appropriate bin and position + tof_images[bin_index][y][x]++; + binned_entries++; + } + } + } + } + + const auto end = std::chrono::high_resolution_clock::now(); + const auto elapsed = std::chrono::duration_cast(end - start).count(); + spdlog::info("TOF image creation time: {} s", elapsed / 1e6); + spdlog::info("Total entries: {}, Binned entries: {}", total_entries, binned_entries); + + return tof_images; +} + +/** + * @brief Timed save TOF imaging to TIFF. + * + * @param[in] out_tof_imaging + * @param[in] batches + * @param[in] tof_bin_edges + * @param[in] tof_filename_base + */ +void timedSaveTOFImagingToTIFF( + const std::string& out_tof_imaging, + const std::vector>>& tof_images, + const std::vector& tof_bin_edges, + const std::string& tof_filename_base) +{ + auto start = std::chrono::high_resolution_clock::now(); + + // 1. Create output directory if it doesn't exist + if (!std::filesystem::exists(out_tof_imaging)) { + std::filesystem::create_directories(out_tof_imaging); + spdlog::info("Created output directory: {}", out_tof_imaging); + } + + // 2. Initialize vector for spectral data + std::vector spectral_counts(tof_images.size(), 0); + + // 3. Iterate through each TOF bin and save TIFF files + tbb::parallel_for(tbb::blocked_range(0, tof_images.size()), [&](const tbb::blocked_range& range) { + for (size_t bin = range.begin(); bin < range.end(); ++bin) { + // Construct filename + std::string filename = fmt::format("{}/{}_bin_{:04d}.tiff", out_tof_imaging, tof_filename_base, bin + 1); + + // prepare container and fill with current hist2d + const uint32_t width = tof_images[bin][0].size(); + const uint32_t height = tof_images[bin].size(); + std::vector> accumulated_image = tof_images[bin]; + + // check if file already exist + if (std::filesystem::exists(filename)) { + TIFF* existing_tif = TIFFOpen(filename.c_str(), "r"); + if (existing_tif) { + uint32_t existing_width, existing_height; + TIFFGetField(existing_tif, TIFFTAG_IMAGEWIDTH, &existing_width); + TIFFGetField(existing_tif, TIFFTAG_IMAGELENGTH, &existing_height); + + if (existing_width == width && existing_height == height) { + // Dimensions match, proceed with accumulation + for (uint32_t row = 0; row < height; ++row) { + std::vector scanline(width); + TIFFReadScanline(existing_tif, scanline.data(), row); + for (uint32_t col = 0; col < width; ++col) { + accumulated_image[row][col] += scanline[col]; + } + } + spdlog::debug("Accumulated counts for existing file: {}", filename); + } else { + spdlog::error("Dimension mismatch for file: {}. Expected {}x{}, got {}x{}. Overwriting.", + filename, width, height, existing_width, existing_height); + } + TIFFClose(existing_tif); + } else { + spdlog::error("Failed to open existing TIFF file for reading: {}", filename); + } + } + + // Write or update TIFF file + TIFF* tif = TIFFOpen(filename.c_str(), "w"); + if (tif) { + const uint32_t width = tof_images[bin][0].size(); + const uint32_t height = tof_images[bin].size(); + + TIFFSetField(tif, TIFFTAG_IMAGEWIDTH, width); + TIFFSetField(tif, TIFFTAG_IMAGELENGTH, height); + TIFFSetField(tif, TIFFTAG_SAMPLESPERPIXEL, 1); + TIFFSetField(tif, TIFFTAG_BITSPERSAMPLE, 32); + TIFFSetField(tif, TIFFTAG_ORIENTATION, ORIENTATION_TOPLEFT); + TIFFSetField(tif, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG); + TIFFSetField(tif, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_MINISBLACK); + + for (uint32_t row = 0; row < height; ++row) { + TIFFWriteScanline(tif, accumulated_image[row].data(), row); + } + + TIFFClose(tif); + spdlog::debug("Wrote TIFF file: {}", filename); + } else { + spdlog::error("Failed to open TIFF file for writing: {}", filename); + } + + // Accumulate counts for spectral file + spectral_counts[bin] = std::accumulate(accumulated_image.cbegin(), accumulated_image.cend(), static_cast(0), + [](const auto sum, const auto & row) { + return sum + std::accumulate(row.cbegin(), row.cend(), 0ULL); + }); + } + }); + + // 4. Write spectral file + std::string spectral_filename = fmt::format("{}/{}_Spectra.txt", out_tof_imaging, tof_filename_base); + // Write spectral data to file + std::ofstream spectral_file(spectral_filename); + if (spectral_file.is_open()) { + spectral_file << "shutter_time,counts\n"; + for (size_t bin = 0; bin < tof_bin_edges.size() - 1; ++bin) { + spectral_file << tof_bin_edges[bin + 1] << "," << spectral_counts[bin] << "\n"; + } + spectral_file.close(); + spdlog::info("Wrote spectral file: {}", spectral_filename); + } else { + spdlog::error("Failed to open spectra file for writing: {}", spectral_filename); + } + + auto end = std::chrono::high_resolution_clock::now(); + auto duration = std::chrono::duration_cast(end - start); + spdlog::info("TIFF and spectra file writing completed in {} ms", duration.count()); +} + +} // namespace sophiread \ No newline at end of file diff --git a/sophiread/SophireadCLI/src/user_config.cpp b/sophiread/SophireadCLI/src/user_config.cpp index 3a5c3f0..e2fd5e5 100644 --- a/sophiread/SophireadCLI/src/user_config.cpp +++ b/sophiread/SophireadCLI/src/user_config.cpp @@ -28,16 +28,40 @@ #include #include +/** + * @brief Construct a new UserConfig object with default values. + */ +UserConfig::UserConfig() : m_abs_radius(5.0), m_abs_min_cluster_size(1), m_abs_spider_time_range(75), m_tof_binning(), m_super_resolution(1.0) {} + +/** + * @brief Construct a new UserConfig object with user-defined values + */ +UserConfig::UserConfig(double abs_radius, unsigned long int abs_min_cluster_size, unsigned long int abs_spider_time_range) + : m_abs_radius(abs_radius), m_abs_min_cluster_size(abs_min_cluster_size), m_abs_spider_time_range(abs_spider_time_range), m_tof_binning(), m_super_resolution(1.0) {} + /** * @brief Helper function to convert a user configuration to a string for console output. * - * @return std::string + * @return std::string User configuration as a string */ std::string UserConfig::toString() const { std::stringstream ss; - ss << "ABS: radius=" << m_abs_radius << ", min_cluster_size=" << m_abs_min_cluster_size + ss << "ABS: radius=" << m_abs_radius + << ", min_cluster_size=" << m_abs_min_cluster_size << ", spider_time_range=" << m_abs_spider_time_range; + // Add TOF binning information + if (m_tof_binning.isUniform()) { + ss << ", TOF bins=" << m_tof_binning.num_bins.value_or(0) + << ", TOF max=" << (m_tof_binning.tof_max.value_or(0.0) * 1000) << " ms"; // Convert to milliseconds + } else if (m_tof_binning.isCustom()) { + ss << ", Custom TOF binning with " << m_tof_binning.custom_edges.size() - 1 << " bins"; + } else { + ss << ", TOF binning not set"; + } + + ss << ", Super Resolution=" << m_super_resolution; + return ss.str(); } @@ -45,7 +69,7 @@ std::string UserConfig::toString() const { * @brief Parse the user-defined configuration file and return a UserConfig object. * * @param[in] filepath - * @return UserConfig + * @return UserConfig User-defined configuration */ UserConfig parseUserDefinedConfigurationFile(const std::string& filepath) { // Check if the file exists @@ -87,8 +111,15 @@ UserConfig parseUserDefinedConfigurationFile(const std::string& filepath) { } else if (name == "spider_time_range") { int value; ss >> value; - user_config.setABSSpidertimeRange(value); - } else { + user_config.setABSSpiderTimeRange(value); + } else if (name == "tof_bins") { + int value; + ss >> value; + } else if (name == "tof_max") { + double value; + ss >> value; + } + else { spdlog::warn("Unknown parameter {} in the user-defined configuration file.", name); } } diff --git a/sophiread/SophireadCLI/src/venus_auto_reducer.cpp b/sophiread/SophireadCLI/src/venus_auto_reducer.cpp new file mode 100644 index 0000000..bee3155 --- /dev/null +++ b/sophiread/SophireadCLI/src/venus_auto_reducer.cpp @@ -0,0 +1,257 @@ +/** + * @file venus_auto_reducer.cpp + * @author Chen Zhang (zhangc@orn.gov) + * @brief Auto reducer demo application for VENUS@SNS + * @version 0.1 + * @date 2024-08-21 + * + * @copyright Copyright (c) 2024 + * This program 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. + * + * This program 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 . + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "sophiread_core.h" +#include "json_config_parser.h" +#include +#include +namespace fs = std::filesystem; + +struct ProgramOptions { + std::string input_dir; + std::string output_dir; + std::string config_file; + std::string tiff_base = "tof_image"; + std::string tof_mode = "neutron"; + int check_interval = 5; + bool verbose = false; + bool debug = false; +}; + +void print_usage(const char* program_name) { + spdlog::info("Usage: {} -i -o [-u ] [-f ] [-m ] [-c ] [-v] [-d]", program_name); + spdlog::info("Options:"); + spdlog::info(" -i Input directory with TPX3 files"); + spdlog::info(" -o Output directory for TIFF files"); + spdlog::info(" -u User configuration JSON file (optional)"); + spdlog::info(" -f Base name for TIFF files (default: tof_image)"); + spdlog::info(" -m TOF mode: 'hit' or 'neutron' (default: neutron)"); + spdlog::info(" -c Check interval in seconds (default: 5)"); + spdlog::info(" -d Debug output"); + spdlog::info(" -v Verbose output"); +} + +ProgramOptions parse_arguments(int argc, char* argv[]) { + ProgramOptions options; + int opt; + + while ((opt = getopt(argc, argv, "i:o:u:f:m:c:dv")) != -1) { + switch (opt) { + case 'i': + options.input_dir = optarg; + break; + case 'o': + options.output_dir = optarg; + break; + case 'u': + options.config_file = optarg; + break; + case 'f': + options.tiff_base = optarg; + break; + case 'm': + options.tof_mode = optarg; + break; + case 'c': + options.check_interval = std::stoi(optarg); + break; + case 'd': + options.debug = true; + break; + case 'v': + options.verbose = true; + break; + default: + print_usage(argv[0]); + throw std::runtime_error("Invalid argument"); + } + } + + // Validate required arguments + if (options.input_dir.empty() || options.output_dir.empty()) { + print_usage(argv[0]); + throw std::runtime_error("Missing required arguments"); + } + + // Validate tof_mode + if (options.tof_mode != "hit" && options.tof_mode != "neutron") { + throw std::runtime_error("Invalid TOF mode. Use 'hit' or 'neutron'."); + } + + // Validate check_interval + if (options.check_interval <= 0) { + throw std::runtime_error("Check interval must be a positive integer."); + } + + return options; +} + +/** + * @brief Process all existing tpx3 files + * + * @param[in] input_dir + * @param[in] output_dir + * @param[in] tiff_base + * @param[in] tof_mode + * @param[in] config + * @param[in, out] processed_files + */ +void process_existing_files(const std::string& input_dir, const std::string& output_dir, + const std::string& tiff_base, const std::string& tof_mode, + const IConfig& config, std::unordered_set& processed_files) { + spdlog::info("Processing existing files in {}", input_dir); + + // NOTE: we need to process files sequentially as we are accumulating the counts to the + // same set of tiff files in the output folder + for (const auto& entry : fs::directory_iterator(input_dir)) { + if (entry.is_regular_file() && entry.path().extension() == ".tpx3") { + std::string filename = entry.path().stem().string(); + + if (processed_files.find(filename) != processed_files.end()) { + spdlog::debug("Skipping already processed file: {}", filename); + continue; + } + + spdlog::info("Processing file: {}", entry.path().string()); + + try { + // Read the TPX3 file + auto raw_data = sophiread::timedReadDataToCharVec(entry.path().string()); + + // Find TPX3 headers + auto batches = sophiread::timedFindTPX3H(raw_data); + + // Process the data + sophiread::timedLocateTimeStamp(batches, raw_data); + sophiread::timedProcessing(batches, raw_data, config); + + // Generate output file name + std::string output_file = fs::path(output_dir) / (tiff_base + "_bin_xxxx.tiff"); + + // Create TOF images + auto tof_images = sophiread::timedCreateTOFImages(batches, config.getSuperResolution(), config.getTOFBinEdges(), tof_mode); + + // Save TOF images + sophiread::timedSaveTOFImagingToTIFF(output_dir, tof_images, config.getTOFBinEdges(), tiff_base); + + spdlog::info("Processed and saved: {}", output_file); + + // record processed file + processed_files.insert(entry.path().stem().string()); + } catch (const std::exception& e) { + spdlog::error("Error processing file {}: {}", entry.path().string(), e.what()); + } + } + } + + // Create a string to hold the formatted output + std::ostringstream oss; + oss << "Processed files: ["; + + // Loop through the set and concatenate elements + for (auto it = processed_files.begin(); it != processed_files.end(); ++it) { + if (it != processed_files.begin()) { + oss << ", "; // Add a comma before each element except the first + } + oss << *it; + } + oss << "]"; + spdlog::info(oss.str()); +} + +void monitor_directory(const std::string& input_dir, const std::string& output_dir, + const std::string& tiff_base, const std::string& tof_mode, + const IConfig& config, std::unordered_set& processed_files, + int check_interval) { + spdlog::info("Starting directory monitoring: {}", input_dir); + spdlog::info("Check interval: {} seconds", check_interval); + + while (true) { + // Check for *.nxs.h5 file + for (const auto& entry : fs::directory_iterator(input_dir)) { + if (entry.is_regular_file() && entry.path().extension() == ".h5" && + entry.path().stem().extension() == ".nxs") { + spdlog::info("Found *.nxs.h5 file. Stopping monitoring."); + return; + } + } + + // Process any new files + process_existing_files(input_dir, output_dir, tiff_base, tof_mode, config, processed_files); + + // Wait before next check + std::this_thread::sleep_for(std::chrono::seconds(check_interval)); + } +} + +int main(int argc, char* argv[]) { + try { + ProgramOptions options = parse_arguments(argc, argv); + + // Set logging level based on verbose flag + if (options.debug){ + spdlog::set_level(spdlog::level::debug); + spdlog::debug("Debug logging enabled"); + } else if (options.verbose) { + spdlog::set_level(spdlog::level::info); + } else { + spdlog::set_level(spdlog::level::warn); + } + + spdlog::info("Input directory: {}", options.input_dir); + spdlog::info("Output directory: {}", options.output_dir); + spdlog::info("Config file: {}", options.config_file); + spdlog::info("TIFF base name: {}", options.tiff_base); + spdlog::info("TOF mode: {}", options.tof_mode); + + // Load configuration + std::unique_ptr config; + if (!options.config_file.empty()) { + spdlog::info("Config file: {}", options.config_file); + config = std::make_unique(JSONConfigParser::fromFile(options.config_file)); + } else { + spdlog::info("Using default configuration"); + config = std::make_unique(JSONConfigParser::createDefault()); + } + + spdlog::info("Configuration: {}", config->toString()); + + // Process existing files + std::unordered_set processed_files; + monitor_directory(options.input_dir, options.output_dir, options.tiff_base, + options.tof_mode, *config, processed_files, options.check_interval); + + } catch (const std::exception& e) { + spdlog::error("Error: {}", e.what()); + return 1; + } + + return 0; +} \ No newline at end of file diff --git a/sophiread/SophireadCLI/tests/test_json_config_parser.cpp b/sophiread/SophireadCLI/tests/test_json_config_parser.cpp new file mode 100644 index 0000000..6c70f5e --- /dev/null +++ b/sophiread/SophireadCLI/tests/test_json_config_parser.cpp @@ -0,0 +1,139 @@ +/** + * @file test_json_config_parser.cpp + * @author Chen Zhang (zhangc@orn.gov) + * @brief Unit tests for JSONConfigParser class. + * @date 2024-08-16 + * + * @copyright Copyright (c) 2024 + * This program 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. + * + * This program 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 . + */ +#include "json_config_parser.h" +#include +#include +#include + +class JSONConfigParserTest : public ::testing::Test { +protected: + void SetUp() override { + // This setup will be used for the uniform binning test + std::ofstream config_file("test_config_uniform.json"); + config_file << R"({ + "abs": { + "radius": 6.0, + "min_cluster_size": 2, + "spider_time_range": 80 + }, + "tof_imaging": { + "uniform_bins": { + "num_bins": 1000, + "end": 0.0167 + }, + "super_resolution": 2.0 + } + })"; + config_file.close(); + + // Setup for custom binning test + std::ofstream config_file_custom("test_config_custom.json"); + config_file_custom << R"({ + "abs": { + "radius": 7.0, + "min_cluster_size": 3, + "spider_time_range": 85 + }, + "tof_imaging": { + "bin_edges": [0, 0.001, 0.002, 0.005, 0.01, 0.0167] + } + })"; + config_file_custom.close(); + + // Setup for default values test + std::ofstream config_file_default("test_config_default.json"); + config_file_default << R"({ + "abs": {} + })"; + config_file_default.close(); + } + + void TearDown() override { + std::remove("test_config_uniform.json"); + std::remove("test_config_custom.json"); + std::remove("test_config_default.json"); + } +}; + +TEST_F(JSONConfigParserTest, ParsesSuperResolutionCorrectly) { + auto config = JSONConfigParser::fromFile("test_config_uniform.json"); + EXPECT_DOUBLE_EQ(config.getSuperResolution(), 2.0); +} + +TEST_F(JSONConfigParserTest, ParsesUniformConfigCorrectly) { + auto config = JSONConfigParser::fromFile("test_config_uniform.json"); + + EXPECT_DOUBLE_EQ(config.getABSRadius(), 6.0); + EXPECT_EQ(config.getABSMinClusterSize(), 2); + EXPECT_EQ(config.getABSSpiderTimeRange(), 80); + + auto bin_edges = config.getTOFBinEdges(); + EXPECT_EQ(bin_edges.size(), 1001); // 1000 bins + 1 + EXPECT_DOUBLE_EQ(bin_edges.front(), 0); + EXPECT_DOUBLE_EQ(bin_edges.back(), 0.0167); +} + +TEST_F(JSONConfigParserTest, ParsesCustomConfigCorrectly) { + auto config = JSONConfigParser::fromFile("test_config_custom.json"); + + EXPECT_DOUBLE_EQ(config.getABSRadius(), 7.0); + EXPECT_EQ(config.getABSMinClusterSize(), 3); + EXPECT_EQ(config.getABSSpiderTimeRange(), 85); + + auto bin_edges = config.getTOFBinEdges(); + EXPECT_EQ(bin_edges.size(), 6); + EXPECT_DOUBLE_EQ(bin_edges[0], 0); + EXPECT_DOUBLE_EQ(bin_edges[1], 0.001); + EXPECT_DOUBLE_EQ(bin_edges[2], 0.002); + EXPECT_DOUBLE_EQ(bin_edges[3], 0.005); + EXPECT_DOUBLE_EQ(bin_edges[4], 0.01); + EXPECT_DOUBLE_EQ(bin_edges[5], 0.0167); +} + +TEST_F(JSONConfigParserTest, UsesDefaultValuesCorrectly) { + auto config = JSONConfigParser::fromFile("test_config_default.json"); + + EXPECT_DOUBLE_EQ(config.getABSRadius(), 5.0); + EXPECT_EQ(config.getABSMinClusterSize(), 1); + EXPECT_EQ(config.getABSSpiderTimeRange(), 75); + + auto bin_edges = config.getTOFBinEdges(); + EXPECT_EQ(bin_edges.size(), 1501); // 1500 bins + 1 + EXPECT_DOUBLE_EQ(bin_edges.front(), 0); + EXPECT_DOUBLE_EQ(bin_edges.back(), 0.0167); + EXPECT_DOUBLE_EQ(config.getSuperResolution(), 1.0); +} + +TEST_F(JSONConfigParserTest, ThrowsOnMissingFile) { + EXPECT_THROW(JSONConfigParser::fromFile("non_existent.json"), std::runtime_error); +} + +TEST_F(JSONConfigParserTest, ToStringMethodWorksCorrectly) { + auto config = JSONConfigParser::fromFile("test_config_uniform.json"); + std::string result = config.toString(); + + EXPECT_TRUE(result.find("radius=6") != std::string::npos); + EXPECT_TRUE(result.find("min_cluster_size=2") != std::string::npos); + EXPECT_TRUE(result.find("spider_time_range=80") != std::string::npos); + EXPECT_TRUE(result.find("TOF bins=1000") != std::string::npos); + EXPECT_TRUE(result.find("TOF max=16.7 ms") != std::string::npos); + EXPECT_TRUE(result.find("Super Resolution=2") != std::string::npos); +} diff --git a/sophiread/SophireadCLI/tests/test_sophiread_core.cpp b/sophiread/SophireadCLI/tests/test_sophiread_core.cpp new file mode 100644 index 0000000..cd634cf --- /dev/null +++ b/sophiread/SophireadCLI/tests/test_sophiread_core.cpp @@ -0,0 +1,151 @@ +/** + * @file: test_sophiread_core.cpp + * @author: Chen Zhang (zhangc@orn.gov) + * @brief: Unit tests for the Sophiread Core module. + * @date: 2024-08-21 + * + * @copyright Copyright (c) 2024 + * This program 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. + * + * This program 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 . + */ +#include +#include +#include +#include "sophiread_core.h" +#include "json_config_parser.h" + +class SophireadCoreTest : public ::testing::Test { +protected: + std::vector generateMockTPX3Data(int num_packets = 10) { + std::vector data; + + for (int i = 0; i < num_packets; ++i) { + // Header packet + data.push_back('T'); + data.push_back('P'); + data.push_back('X'); + data.push_back('3'); + data.push_back(0); // chip_layout_type + data.push_back(0); // some random data + data.push_back(8); // data_packet_size (low byte) + data.push_back(0); // data_packet_size (high byte) + + // Data packet (8 bytes) + for (int j = 0; j < 8; ++j) { + data.push_back(0); + } + } + return data; + } + + std::vector generateMockTPX3Batches(int num_batches = 2, int hits_per_batch = 5) { + std::vector batches; + for (int i = 0; i < num_batches; ++i) { + TPX3 batch(i * 100, hits_per_batch, i % 3); // index, num_packets, chip_layout_type + + // Add mock hits + for (int j = 0; j < hits_per_batch; ++j) { + char mock_packet[8] = {0}; // Mock packet data + batch.emplace_back(mock_packet, 1000 + j, 2000 + j); + } + + // Add mock neutrons (derived from hits) + for (const auto& hit : batch.hits) { + batch.neutrons.emplace_back( + hit.getX(), hit.getY(), + hit.getTOF(), hit.getTOT(), + 1 // nHits, assume 1 hit per neutron for simplicity + ); + } + + batches.push_back(std::move(batch)); + } + return batches; + } + + void SetUp() override { + // Create a small test TPX3 file + auto test_data = generateMockTPX3Data(100); + std::ofstream test_file("test.tpx3", std::ios::binary); + test_file.write(test_data.data(), test_data.size()); + test_file.close(); + } + + void TearDown() override { + // Remove the test file + std::filesystem::remove("test.tpx3"); + } +}; + +TEST_F(SophireadCoreTest, TimedReadDataToCharVec) { + auto data = sophiread::timedReadDataToCharVec("test.tpx3"); + EXPECT_EQ(data.size(), 1600); // 100 * (8 + 8) bytes +} + +TEST_F(SophireadCoreTest, TimedFindTPX3H) { + auto rawdata = generateMockTPX3Data(100); + auto batches = sophiread::timedFindTPX3H(rawdata); + EXPECT_EQ(batches.size(), 100); +} + +TEST_F(SophireadCoreTest, TimedLocateTimeStamp) { + std::vector raw_data(8000, 'T'); // Simulating TPX3 data + std::vector batches = { TPX3(0, 10, 0) }; // Create a dummy TPX3 batch + sophiread::timedLocateTimeStamp(batches, raw_data); + // Add assertions based on expected behavior +} + +TEST_F(SophireadCoreTest, TimedProcessing) { + std::vector raw_data(8000, 'T'); // Simulating TPX3 data + std::vector batches = { TPX3(0, 10, 0) }; // Create a dummy TPX3 batch + JSONConfigParser config = JSONConfigParser::createDefault(); + sophiread::timedProcessing(batches, raw_data, config); + // Add assertions based on expected behavior +} + +TEST_F(SophireadCoreTest, TimedSaveHitsToHDF5) { + std::vector batches = generateMockTPX3Batches(2, 5); // Create a dummy TPX3 batch + sophiread::timedSaveHitsToHDF5("test_hits.h5", batches); + EXPECT_TRUE(std::filesystem::exists("test_hits.h5")); + std::filesystem::remove("test_hits.h5"); +} + +TEST_F(SophireadCoreTest, TimedSaveEventsToHDF5) { + std::vector batches = generateMockTPX3Batches(2, 5); // Create a dummy TPX3 batch + sophiread::timedSaveEventsToHDF5("test_events.h5", batches); + EXPECT_TRUE(std::filesystem::exists("test_events.h5")); + std::filesystem::remove("test_events.h5"); +} + +TEST_F(SophireadCoreTest, TimedCreateTOFImages) { + std::vector batches = generateMockTPX3Batches(2, 5); // Create a dummy TPX3 batch + std::vector tof_bin_edges = {0.0, 0.1, 0.2, 0.3}; + auto images = sophiread::timedCreateTOFImages(batches, 1.0, tof_bin_edges, "neutron"); + EXPECT_EQ(images.size(), 3); // 3 bins +} + +TEST_F(SophireadCoreTest, TimedSaveTOFImagingToTIFF) { + std::vector>> tof_images(3, std::vector>(10, std::vector(10, 1))); + std::vector tof_bin_edges = {0.0, 0.1, 0.2, 0.3}; + sophiread::timedSaveTOFImagingToTIFF("test_tof", tof_images, tof_bin_edges, "test"); + EXPECT_TRUE(std::filesystem::exists("test_tof/test_bin_0001.tiff")); + EXPECT_TRUE(std::filesystem::exists("test_tof/test_bin_0002.tiff")); + EXPECT_TRUE(std::filesystem::exists("test_tof/test_bin_0003.tiff")); + EXPECT_TRUE(std::filesystem::exists("test_tof/test_Spectra.txt")); + std::filesystem::remove_all("test_tof"); +} + +int main(int argc, char **argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} \ No newline at end of file diff --git a/sophiread/SophireadCLI/tests/test_user_config.cpp b/sophiread/SophireadCLI/tests/test_user_config.cpp index 436eda1..8c75d35 100644 --- a/sophiread/SophireadCLI/tests/test_user_config.cpp +++ b/sophiread/SophireadCLI/tests/test_user_config.cpp @@ -20,16 +20,89 @@ * along with this program. If not, see . */ #include - #include - #include "user_config.h" +#include "tof_binning.h" + +// Test default constructor +TEST(UserConfigTest, DefaultConstructor) { + UserConfig config; + EXPECT_DOUBLE_EQ(config.getABSRadius(), 5.0); + EXPECT_EQ(config.getABSMinClusterSize(), 1); + EXPECT_EQ(config.getABSSpiderTimeRange(), 75); + + auto tof_edges = config.getTOFBinEdges(); + EXPECT_EQ(tof_edges.size(), 1501); // 1500 bins + 1 + EXPECT_DOUBLE_EQ(tof_edges.front(), 0.0); + EXPECT_DOUBLE_EQ(tof_edges.back(), 1.0/60); +} + +// Test parameterized constructor +TEST(UserConfigTest, ParameterizedConstructor) { + UserConfig config(10.0, 5, 100); + EXPECT_DOUBLE_EQ(config.getABSRadius(), 10.0); + EXPECT_EQ(config.getABSMinClusterSize(), 5); + EXPECT_EQ(config.getABSSpiderTimeRange(), 100); + + // TOF binning should still be default + auto tof_edges = config.getTOFBinEdges(); + EXPECT_EQ(tof_edges.size(), 1501); + EXPECT_DOUBLE_EQ(tof_edges.front(), 0.0); + EXPECT_DOUBLE_EQ(tof_edges.back(), 1.0/60); +} + +// Test setters +TEST(UserConfigTest, Setters) { + UserConfig config; + config.setABSRadius(15.0); + config.setABSMinClusterSize(10); + config.setABSSpiderTimeRange(150); + + EXPECT_DOUBLE_EQ(config.getABSRadius(), 15.0); + EXPECT_EQ(config.getABSMinClusterSize(), 10); + EXPECT_EQ(config.getABSSpiderTimeRange(), 150); +} + +// Test TOF binning setter +TEST(UserConfigTest, TOFBinningSetter) { + UserConfig config; + TOFBinning custom_binning; + custom_binning.num_bins = 1000; + custom_binning.tof_max = 20000.0; + config.setTOFBinning(custom_binning); + + auto tof_edges = config.getTOFBinEdges(); + EXPECT_EQ(tof_edges.size(), 1001); // 1000 bins + 1 + EXPECT_DOUBLE_EQ(tof_edges.front(), 0.0); + EXPECT_DOUBLE_EQ(tof_edges.back(), 20000.0); +} -// Test toString method of UserConfig class +// Test custom TOF bin edges +TEST(UserConfigTest, CustomTOFBinEdges) { + UserConfig config; + std::vector custom_edges = {0.0, 100.0, 200.0, 300.0, 400.0}; + config.setCustomTOFBinEdges(custom_edges); + + auto tof_edges = config.getTOFBinEdges(); + EXPECT_EQ(tof_edges.size(), 5); + EXPECT_DOUBLE_EQ(tof_edges[0], 0.0); + EXPECT_DOUBLE_EQ(tof_edges[1], 100.0); + EXPECT_DOUBLE_EQ(tof_edges[2], 200.0); + EXPECT_DOUBLE_EQ(tof_edges[3], 300.0); + EXPECT_DOUBLE_EQ(tof_edges[4], 400.0); +} + +// Test toString method TEST(UserConfigTest, ToStringMethod) { - UserConfig config(20.0, 30, 500000); - std::string expected = "ABS: radius=20, min_cluster_size=30, spider_time_range=500000"; - ASSERT_EQ(config.toString(), expected); + UserConfig config(20.0, 30, 500); + std::string result = config.toString(); + // print the result + std::cout << result << std::endl; + EXPECT_TRUE(result.find("radius=20") != std::string::npos); + EXPECT_TRUE(result.find("min_cluster_size=30") != std::string::npos); + EXPECT_TRUE(result.find("spider_time_range=500") != std::string::npos); + EXPECT_TRUE(result.find("TOF bins=1500") != std::string::npos); + EXPECT_TRUE(result.find("TOF max=16.6667 ms") != std::string::npos); } // Test parsing a valid configuration file @@ -39,13 +112,19 @@ TEST(UserConfigTest, ParseValidConfigurationFile) { testFile << "# ABS\n"; testFile << "abs_radius 20.0\n"; testFile << "abs_min_cluster_size 30\n"; - testFile << "spider_time_range 500000\n"; + testFile << "spider_time_range 500\n"; testFile.close(); UserConfig config = parseUserDefinedConfigurationFile("testConfig.txt"); - ASSERT_DOUBLE_EQ(config.getABSRadius(), 20.0); - ASSERT_EQ(config.getABSMinClusterSize(), 30); - ASSERT_EQ(config.getABSSpidertimeRange(), 500000); + EXPECT_DOUBLE_EQ(config.getABSRadius(), 20.0); + EXPECT_EQ(config.getABSMinClusterSize(), 30); + EXPECT_EQ(config.getABSSpiderTimeRange(), 500); + + // TOF binning should still be default + auto tof_edges = config.getTOFBinEdges(); + EXPECT_EQ(tof_edges.size(), 1501); + EXPECT_DOUBLE_EQ(tof_edges.front(), 0.0); + EXPECT_DOUBLE_EQ(tof_edges.back(), 1.0/60); // Cleanup std::remove("testConfig.txt"); @@ -61,9 +140,9 @@ TEST(UserConfigTest, ParseInvalidConfigurationFile) { // It should ignore the unknown parameter and use the default value instead UserConfig config = parseUserDefinedConfigurationFile("testInvalidConfig.txt"); - ASSERT_DOUBLE_EQ(config.getABSRadius(), 5.0); // Default value - ASSERT_EQ(config.getABSMinClusterSize(), 1); // Default value - ASSERT_EQ(config.getABSSpidertimeRange(), 75); // Default value + EXPECT_DOUBLE_EQ(config.getABSRadius(), 5.0); // Default value + EXPECT_EQ(config.getABSMinClusterSize(), 1); // Default value + EXPECT_EQ(config.getABSSpiderTimeRange(), 75); // Default value // Cleanup std::remove("testInvalidConfig.txt"); @@ -72,4 +151,4 @@ TEST(UserConfigTest, ParseInvalidConfigurationFile) { int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); -} +} \ No newline at end of file diff --git a/sophiread/environment_linux.yml b/sophiread/environment_linux.yml index 01570bf..8cb9f08 100644 --- a/sophiread/environment_linux.yml +++ b/sophiread/environment_linux.yml @@ -14,6 +14,7 @@ dependencies: - cereal - tbb-devel - spdlog + - nlohmann_json # Not Windows, OpenGL implementation: diff --git a/sophiread/environment_mac.yml b/sophiread/environment_mac.yml index a0e71ae..c5a6bb9 100644 --- a/sophiread/environment_mac.yml +++ b/sophiread/environment_mac.yml @@ -13,6 +13,7 @@ dependencies: - eigen - tbb-devel - spdlog + - nlohmann_json # Not Windows, OpenGL implementation: # - mesalib>=18.0.0 diff --git a/sophiread/resources/data/example_config.json b/sophiread/resources/data/example_config.json new file mode 100644 index 0000000..90acc4a --- /dev/null +++ b/sophiread/resources/data/example_config.json @@ -0,0 +1,16 @@ +{ + "abs": { + "radius": 5.0, + "min_cluster_size": 1, + "spider_time_range": 75 + }, + "tof_imaging": { + "uniform_bins": { + "num_bins": 1500, + "end": 0.0167 + }, + "super_resolution": 2.0, + "_bin_edges": [0, 0.001, 0.002, 0.005, 0.01, 0.0167], + "_comment": "use either uniform_bins or bin_edges, otherwise bin_edges will be used" + } +} \ No newline at end of file