diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index 19dba4252..8c074b603 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -39,12 +39,14 @@ jobs: matrix: os: - ubuntu-20.04 - - macos-11 + - macos-14 cc: - gcc-10 + - gcc-11 - clang cxx: - g++-10 + - g++-11 - clang++ mpi: - "on" @@ -53,6 +55,10 @@ jobs: - "on" - "off" exclude: + - cc: gcc-11 + cxx: clang++ + - cc: clang + cxx: g++-11 - cc: gcc-10 cxx: clang++ - cc: clang @@ -60,7 +66,15 @@ jobs: - os: ubuntu-20.04 cc: clang cxx: clang++ - - os: macos-11 + - os: macos-14 + cxx: g++-10 + - os: macos-14 + cc: gcc-10 + - os: ubuntu-20.04 + cxx: g++-11 + - os: ubuntu-20.04 + cc: gcc-11 + - os: macos-14 mpi: "on" - cxx: clang++ omp: "on" @@ -80,17 +94,14 @@ jobs: run: | sudo apt update sudo apt install openmpi-bin libopenmpi-dev ccache casacore-dev + pip install conan - name: Install Dependencies on MacOS if: ${{ contains(matrix.os, 'macos') }} run: | - # Brew update has bugs but we don't seem to be affected, see - # https://github.com/actions/setup-python/issues/577 - # brew update is very slow because it's updating a lot of unrelated packages - # workflow seems to run fine with default versions - # brew update - brew install open-mpi libomp ccache - echo "CMAKE_PREFIX_PATH=/usr/local/opt/libomp" >> $GITHUB_ENV + brew install gcc libtiff open-mpi libomp libyaml ccache conan + echo "CMAKE_PREFIX_PATH=/opt/homebrew/opt/libomp" >> $GITHUB_ENV + echo "/opt/homebrew/opt/ccache/libexec" >> $GITHUB_PATH - name: Install Tensorflow API on Ubuntu # TODO could this be combined with mac version somehow? if/else? @@ -114,12 +125,6 @@ jobs: with: python-version: '3.10' - - name: Install Conan - id: conan - uses: turtlebrowser/get-conan@main - with: - version: 1.60.1 - - name: Prepare ccache timestamp id: ccache_cache_timestamp run: echo "{date_and_time}={$(date +'%Y-%m-%d-%H;%M;%S')}" >> $GITHUB_OUTPUT @@ -140,25 +145,17 @@ jobs: # - name: Clear ccache # run: ccache --clear - - name: create sopt package on gcc - if: ${{ contains(matrix.cxx, 'g++-10') }} - run: conan create ${{github.workspace}}/sopt --build missing -s:b compiler.libcxx=libstdc++11 -o:b mpi=${{matrix.mpi}} -o:b openmp=${{matrix.omp}} -pr:h=default -pr:b=default - - - name: create sopt package on apple-clang - if: ${{ contains(matrix.cxx, 'clang++') }} - run: conan create ${{github.workspace}}/sopt --build missing -o:b mpi=${{matrix.mpi}} -o:b openmp=${{matrix.omp}} -pr:h=default -pr:b=default - - - name: Conan install on gcc - if: ${{ contains(matrix.cxx, 'g++-10') }} - run: conan install ${{github.workspace}} -if ${{github.workspace}}/build -s compiler.libcxx=libstdc++11 --build missing -o mpi=${{matrix.mpi}} -o openmp=${{matrix.omp}} -pr:h=default -pr:b=default + - name: Build sopt + run: | + conan profile detect + conan create ${{github.workspace}}/sopt --build missing -s compiler.cppstd=17 -o dompi=${{matrix.mpi}} -o openmp=${{matrix.omp}} - - name: Conan install on apple-clang - if: ${{ contains(matrix.cxx, 'clang++') }} - run: conan install ${{github.workspace}} -if ${{github.workspace}}/build --build missing -o mpi=${{matrix.mpi}} -o openmp=${{matrix.omp}} -pr:h=default -pr:b=default + - name: Dependencies + run: conan install ${{github.workspace}} -of ${{github.workspace}}/build -s compiler.cppstd=17 --build missing -o docasa=off -o dompi=${{matrix.mpi}} -o openmp=${{matrix.omp}} - - name: Build + - name: Install # Build your program with the given configuration - run: conan build ${{github.workspace}} -bf ${{github.workspace}}/build + run: conan build ${{github.workspace}} -of ${{github.workspace}}/build -s compiler.cppstd=17 -o docasa=off -o dompi=${{matrix.mpi}} -o openmp=${{matrix.omp}} - name: Test working-directory: ${{github.workspace}}/build diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index b040e1032..3f82fa0d6 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -23,8 +23,10 @@ jobs: - name: Install dependencies run: | sudo apt update - sudo apt install openmpi-bin libopenmpi-dev casacore-dev doxygen graphviz - if [[ "$USE_CONAN" = 0 ]]; then + sudo apt install openmpi-bin libopenmpi-dev ccache casacore-dev doxygen graphviz + if [[ "$USE_CONAN" = 1 ]]; then + pip install conan + else sudo apt install libeigen3-dev libspdlog-dev libtiff-dev libcfitsio-dev sudo apt install libbenchmark-dev libboost-all-dev libyaml-cpp-dev git clone https://github.com/catchorg/Catch2.git -b v3.4.0 @@ -48,13 +50,6 @@ jobs: version: 2.11.0 os: linux - - name: Install Conan - id: conan - if: env.USE_CONAN != '0' - uses: turtlebrowser/get-conan@main - with: - version: 1.60.1 - - name: Checkout cppflow repo uses: actions/checkout@v3 with: @@ -65,7 +60,8 @@ jobs: - name: Create cppflow package run: | if [[ "USE_CONAN" = 1 ]]; then - conan create ./cppflow/ -pr:h=default -pr:b=default + conan detect profile + conan create ./cppflow -s compiler.cppstd=17 else mkdir cppflow/build cd cppflow/build @@ -84,7 +80,7 @@ jobs: - name: Create SOPT package run : | if [[ "USE_CONAN" = 1 ]]; then - conan create ./sopt --build missing -o mpi=off -o openmp=off -o docs=off -o cppflow=on -pr:h=default -pr:b=default + conan create ./sopt -s compiler.cppstd=17 --build missing -o dompi=off -o docasa=off -o openmp=off -o docs=off -o cppflow=on else export CMAKE_PREFIX_PATH=${{github.workspace}}/cppflow/build:$CMAKE_PREFIX_PATH #export CMAKE_PREFIX_PATH=${{github.workspace}}/Catch2/build/lib/cmake:$CMAKE_PREFIX_PATH @@ -92,7 +88,7 @@ jobs: #export CMAKE_PREFIX_PATH=${{github.workspace}}/build/lib/cmake:$CMAKE_PREFIX_PATH mkdir sopt/build cd sopt/build - cmake .. -DCMAKE_INSTALL_PREFIX=${PWD} -Ddompi=OFF -Dopenmp=OFF -Ddocs=OFF -Dcppflow=ON + cmake .. -DCMAKE_INSTALL_PREFIX=${PWD} -Ddocasa=OFF -Ddompi=OFF -Dopenmp=OFF -Ddocs=OFF -Dcppflow=ON #cmake .. -DCMAKE_INSTALL_PREFIX=${{github.workspace}}/build -Ddompi=OFF -Dopenmp=OFF -Ddocs=OFF -Dcppflow=ON make -j$(nproc --ignore 1) install fi @@ -102,7 +98,7 @@ jobs: if [[ "USE_CONAN" = 1 ]]; then # Doxygen currently broken in Conan v1 and v2 #conan install doxygen/1.9.4@#2af713e135f12722e3536808017ba086 --update - conan install ${{github.workspace}} -if ${{github.workspace}}/build --build missing -o mpi=off -o openmp=off -o docs=on -o cppflow=on -pr:h=default -pr:b=default + conan install ${{github.workspace}} -if ${{github.workspace}}/build -s compiler.cppstd=17 --build missing -o docasa=off -o dompi=off -o openmp=off -o docs=on -o cppflow=on else export CMAKE_PREFIX_PATH=${{github.workspace}}/cppflow/build:$CMAKE_PREFIX_PATH export CMAKE_PREFIX_PATH=${{github.workspace}}/build:$CMAKE_PREFIX_PATH @@ -114,7 +110,7 @@ jobs: - name: Build run: | if [[ "USE_CONAN" = 1 ]]; then - conan build ${{github.workspace}} -bf ${{github.workspace}}/build + conan build ${{github.workspace}} -of ${{github.workspace}}/build -s compiler.cppstd=17 -o docasa=off -o dompi=off -o openmp=off -o docs=on -o cppflow=on else export CMAKE_PREFIX_PATH=${{github.workspace}}/cppflow/build:$CMAKE_PREFIX_PATH ##export CMAKE_PREFIX_PATH=${{github.workspace}}/fftw-3.3.10/build/lib/cmake:$CMAKE_PREFIX_PATH diff --git a/CMakeLists.txt b/CMakeLists.txt index dcae1325b..ac2d65a5c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,7 +9,6 @@ option(tests "Enable testing" on) option(examples "Compile examples" off) option(benchmarks "Enable benchmarking" off) option(openmp "Enable OpenMP" on) -option(logging "Enable logging" on) option(dompi "Enable MPI" on) option(doaf "Enable ArrayFire" off) option(docimg "Enable CImg" off) @@ -38,15 +37,15 @@ set(CMAKE_CXX_EXTENSIONS OFF) # sets up rpath so libraries can be found include(rpath) -# adds logging variables -include(logging) - # include exernal dependencies include(dependencies) +set(PURIFY_TEST_LOG_LEVEL critical CACHE STRING "Level when logging tests") +set_property(CACHE PURIFY_TEST_LOG_LEVEL + PROPERTY STRINGS off critical error warn info debug trace) + # If PURIFY_OPENMP is set to False in dependencies, link libpthread here so that the linker finds it # Following advice from https://stackoverflow.com/questions/1620918/cmake-and-libpthread - if(NOT PURIFY_OPENMP) set(CMAKE_THREAD_LIBS_INIT "-lpthread") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pthread") diff --git a/README.md b/README.md index e29651e88..5a8118fa1 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ This documentation outlines the necessary and optional [dependencies](#dependenc In order to build **PURIFY**, you should have the following installed on your system. - [CMake](http://www.cmake.org/) v3.5.1 A free software that allows cross-platform compilation -- [conan](https://conan.io/) v1.60.1 `C/C++` package manager. **NOTE** Conan 2.0 and later are not supported. +- [conan](https://conan.io/) v2.0.11 `C/C++` package manager. **NOTE** Conan v1 is no loner supported. - [GCC](https://gcc.gnu.org) v7.3.0 GNU compiler for `C++` - [OpenMP](http://openmp.org/wp/) v4.8.4 - Optional - Speeds up some of the operations. - [MPI](https://www.open-mpi.org) v3.1.1 - Optional - Parallelisation paradigm to speed up operations. diff --git a/cmake_files/dependencies.cmake b/cmake_files/dependencies.cmake index 55c26f46b..14518ce20 100644 --- a/cmake_files/dependencies.cmake +++ b/cmake_files/dependencies.cmake @@ -40,10 +40,6 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Linux") endif(THREADS_FOUND) endif() -if(logging) - find_package(spdlog REQUIRED) -endif() - if(docs) cmake_policy(SET CMP0057 NEW) find_package(Doxygen REQUIRED dot) @@ -60,30 +56,15 @@ if(cppflow) endif() # Always find open-mp, since it may be used by sopt -find_package(OpenMP) -if(openmp AND NOT OPENMP_FOUND) - message(STATUS "Could not find OpenMP. Compiling without.") -endif() -set(PURIFY_OPENMP_FFTW FALSE) -if(openmp AND OPENMP_FOUND) - # Set PURIFY_OPENMP to TRUE when OpenMP is both found and requested - set(PURIFY_OPENMP TRUE) - - # Add the OpenMP Library - add_library(openmp::openmp INTERFACE IMPORTED GLOBAL) - - # Set compiler and linker options to the defaults for CXX - # TODO: Should this be done automatically? - # Check when we update CMake and the OpenMP linking to - # https://cliutils.gitlab.io/modern-cmake/chapters/packages/OpenMP.html - # possibly using - # https://cmake.org/cmake/help/latest/module/FindOpenMP.html - set_target_properties(openmp::openmp PROPERTIES - INTERFACE_COMPILE_OPTIONS "${OpenMP_CXX_FLAGS}" - INTERFACE_LINK_LIBRARIES "${OpenMP_CXX_FLAGS}") -else() - # Set to FALSE when OpenMP is not found or not requested - set(PURIFY_OPENMP FALSE) +if (openmp) + find_package(OpenMP) + if (OPENMP_FOUND) + # Set PURIFY_OPENMP to TRUE when OpenMP is both found and requested + set(PURIFY_OPENMP TRUE) + else() + # Set to FALSE when OpenMP is not found or not requested + message(STATUS "Could not find OpenMP. Compiling without.") + endif() endif() find_package(fftw3 NAMES FFTW3 REQUIRED) diff --git a/cmake_files/logging.cmake b/cmake_files/logging.cmake deleted file mode 100644 index fcfce75f6..000000000 --- a/cmake_files/logging.cmake +++ /dev/null @@ -1,13 +0,0 @@ -# Setup logging -set(PURIFY_LOGGER_NAME "purify" CACHE STRING "NAME of the logger") -set(PURIFY_COLOR_LOGGING true CACHE BOOL "Whether to add color to the log") -if(logging) - set(PURIFY_DO_LOGGING 1) - set(PURIFY_TEST_LOG_LEVEL critical CACHE STRING "Level when logging tests") - set_property(CACHE PURIFY_TEST_LOG_LEVEL PROPERTY STRINGS - off critical error warn info debug trace) -else() - unset(PURIFY_DO_LOGGING) - set(PURIFY_TEST_LOG_LEVEL off) -endif() - diff --git a/conanfile.py b/conanfile.py index bdf02c868..d7094e990 100644 --- a/conanfile.py +++ b/conanfile.py @@ -8,7 +8,7 @@ class PurifyConan(ConanFile): version = "3.0.1" url = "https://github.com/astro-informatics/purify" license = "GPL-2.0" - descpriton = "PURIFY is an open-source collection of routines written in C++ available under the license below. It implements different tools and high-level to perform radio interferometric imaging, i.e. to recover images from the Fourier measurements taken by radio interferometric telescopes." + description = "PURIFY is an open-source collection of routines written in C++ available under the license below. It implements different tools and high-level to perform radio interferometric imaging, i.e. to recover images from the Fourier measurements taken by radio interferometric telescopes." settings = "os", "compiler", "build_type", "arch" @@ -19,37 +19,30 @@ class PurifyConan(ConanFile): "examples":['on','off'], "tests":['on','off'], "benchmarks":['on','off'], - "logging":['on','off'], "openmp":['on','off'], - "mpi":['on','off'], + "dompi":['on','off'], "coverage":['on','off'], "af": ['on', 'off'], "cimg": ['on','off'], - "casa": ['on','off'], + "docasa": ['on','off'], "cppflow": ['on', 'off']} default_options = {"docs": 'off', "examples":'off', "tests": 'on', "benchmarks": 'off', - "logging": 'on', - "openmp": 'on', - "mpi": 'on', + "openmp": 'off', + "dompi": 'off', "coverage": 'off', "af": 'off', "cimg": 'off', - "casa": 'off', + "docasa": 'on', "cppflow": 'off'} def configure(self): - if self.options.cppflow == 'on': - self.options["sopt"].cppflow = 'on' - if self.options.logging == 'off': - self.options["sopt"].logging = 'off' - if self.options.mpi == 'off': - self.options["sopt"].mpi = 'off' - if self.options.openmp == 'off': - self.options["sopt"].openmp = 'off' + self.options["sopt"].cppflow = self.options.cppflow + self.options["sopt"].dompi = self.options.dompi + self.options["sopt"].openmp = self.options.openmp # When building the sopt package, switch off sopt tests and examples, # they are not going to be run. self.options["sopt"].examples = 'off' @@ -68,9 +61,6 @@ def requirements(self): if self.options.examples == 'on': self.requires("libtiff/4.5.1") - if self.options.logging == 'on': - self.requires("spdlog/1.12.0") - if self.options.docs == 'on': self.requires("doxygen/1.9.2") @@ -83,26 +73,25 @@ def requirements(self): def generate(self): tc = CMakeToolchain(self) - tc.variables['docs'] = self.options.docs - tc.variables['examples'] = self.options.examples - tc.variables['tests'] = self.options.tests - tc.variables['benchmarks'] = self.options.benchmarks - tc.variables['logging'] = self.options.logging - tc.variables['openmp'] = self.options.openmp - tc.variables['dompi'] = self.options.mpi - tc.variables['coverage'] = self.options.coverage - tc.variables['doaf'] = self.options.af - tc.variables['docimg'] = self.options.cimg - tc.variables['docasa'] = self.options.casa - tc.variables['cppflow'] = self.options.cppflow + tc.cache_variables['docs'] = self.options.docs + tc.cache_variables['examples'] = self.options.examples + tc.cache_variables['tests'] = self.options.tests + tc.cache_variables['benchmarks'] = self.options.benchmarks + tc.cache_variables['openmp'] = self.options.openmp + tc.cache_variables['dompi'] = self.options.dompi + tc.cache_variables['coverage'] = self.options.coverage + tc.cache_variables['doaf'] = self.options.af + tc.cache_variables['docimg'] = self.options.cimg + tc.cache_variables['docasa'] = self.options.docasa + tc.cache_variables['cppflow'] = self.options.cppflow # List cases where we don't use ccache if ('GITHUB_ACTIONS' in os.environ.keys() and self.options.docs == 'off'): - tc.variables['CMAKE_C_COMPILER_LAUNCHER'] = "ccache" - tc.variables['CMAKE_CXX_COMPILER_LAUNCHER'] = "ccache" + tc.cache_variables['CMAKE_C_COMPILER_LAUNCHER'] = "ccache" + tc.cache_variables['CMAKE_CXX_COMPILER_LAUNCHER'] = "ccache" - tc.variables['CMAKE_VERBOSE_MAKEFILE:BOOL'] = "ON" - tc.variables['MPIEXEC_MAX_NUMPROCS'] = 2 + tc.cache_variables['CMAKE_VERBOSE_MAKEFILE:BOOL'] = "ON" + tc.cache_variables['MPIEXEC_MAX_NUMPROCS'] = 2 tc.generate() deps = CMakeDeps(self) diff --git a/cpp/example/casa.cc b/cpp/example/casa.cc index 25c0854c4..9d15d4d73 100644 --- a/cpp/example/casa.cc +++ b/cpp/example/casa.cc @@ -17,7 +17,6 @@ } int main(int, char **) { - purify::logging::initialize(); purify::logging::set_level(purify::default_logging_level()); // Loads a measurement set auto const ngc3256_filename = purify::notinstalled::ngc3256_ms(); diff --git a/cpp/example/compare_wprojection.cc b/cpp/example/compare_wprojection.cc index b8a52190b..43d98bd3f 100644 --- a/cpp/example/compare_wprojection.cc +++ b/cpp/example/compare_wprojection.cc @@ -22,7 +22,6 @@ int main(int nargs, char const **args) { ARGS_MACRO(imsize, 2, 256, t_uint) ARGS_MACRO(cell, 3, 2400, t_real) #undef ARGS_MACRO - purify::logging::initialize(); purify::logging::set_level("debug"); // Gridding example auto const oversample_ratio = 2; diff --git a/cpp/example/generate_vis_data.cc b/cpp/example/generate_vis_data.cc index 10245f625..1c368e4c4 100644 --- a/cpp/example/generate_vis_data.cc +++ b/cpp/example/generate_vis_data.cc @@ -13,7 +13,6 @@ int main(int nargs, char const **args) { using namespace purify; using namespace purify::notinstalled; - purify::logging::initialize(); purify::logging::set_level(purify::default_logging_level()); const std::string &pos_filename = mwa_filename("Phase1_config.txt"); diff --git a/cpp/example/gridding.cc b/cpp/example/gridding.cc index 698076f08..cddeb044e 100644 --- a/cpp/example/gridding.cc +++ b/cpp/example/gridding.cc @@ -8,7 +8,6 @@ using namespace purify; int main(int nargs, char const **args) { - purify::logging::initialize(); purify::logging::set_level("debug"); // Gridding example auto const oversample_ratio = 2; diff --git a/cpp/example/image_wproj_chirp.cc b/cpp/example/image_wproj_chirp.cc index 228990f1b..d8b8acf72 100644 --- a/cpp/example/image_wproj_chirp.cc +++ b/cpp/example/image_wproj_chirp.cc @@ -23,7 +23,6 @@ int main(int nargs, char const **args) { ARGS_MACRO(Jw_max, 4, 0, t_uint) ARGS_MACRO(radial, 5, true, bool) #undef ARGS_MACRO - purify::logging::initialize(); purify::logging::set_level("debug"); // Gridding example auto const oversample_ratio = 2; diff --git a/cpp/example/mem_w_algos.cc b/cpp/example/mem_w_algos.cc index d21e251bd..41cc6f048 100644 --- a/cpp/example/mem_w_algos.cc +++ b/cpp/example/mem_w_algos.cc @@ -12,7 +12,6 @@ using namespace purify::notinstalled; int main(int nargs, char const **args) { auto const session = sopt::mpi::init(nargs, args); auto const world = sopt::mpi::Communicator::World(); - purify::logging::initialize(); purify::logging::set_level("debug"); // Gridding example auto const oversample_ratio = 2; diff --git a/cpp/example/padmm_mpi_random_coverage.cc b/cpp/example/padmm_mpi_random_coverage.cc index d839a9a90..18994ec29 100644 --- a/cpp/example/padmm_mpi_random_coverage.cc +++ b/cpp/example/padmm_mpi_random_coverage.cc @@ -153,8 +153,6 @@ std::shared_ptr> padmm_factory( } int main(int nargs, char const **args) { - sopt::logging::initialize(); - purify::logging::initialize(); sopt::logging::set_level("debug"); purify::logging::set_level("debug"); auto const session = sopt::mpi::init(nargs, args); diff --git a/cpp/example/padmm_mpi_real_data.cc b/cpp/example/padmm_mpi_real_data.cc index 117809a50..0eb766947 100644 --- a/cpp/example/padmm_mpi_real_data.cc +++ b/cpp/example/padmm_mpi_real_data.cc @@ -153,8 +153,6 @@ std::shared_ptr> padmm_factory( } int main(int nargs, char const **args) { - sopt::logging::initialize(); - purify::logging::initialize(); sopt::logging::set_level("debug"); purify::logging::set_level("debug"); auto const session = sopt::mpi::init(nargs, args); diff --git a/cpp/example/padmm_random_coverage.cc b/cpp/example/padmm_random_coverage.cc index 3801e33b2..f22be4ed6 100644 --- a/cpp/example/padmm_random_coverage.cc +++ b/cpp/example/padmm_random_coverage.cc @@ -133,10 +133,8 @@ void padmm(const std::string &name, const Image &M31, const std::stri } int main(int, char **) { - sopt::logging::initialize(); - purify::logging::initialize(); // sopt::logging::set_level("debug"); - // purify::logging::set_level("debug"); + // purify::logging::set_level("debug"); const std::string &name = "M31"; const t_real FoV = 15; // deg const t_real max_w = 15.; // lambda diff --git a/cpp/example/padmm_real_data.cc b/cpp/example/padmm_real_data.cc index ac0899cf1..6140c2f50 100644 --- a/cpp/example/padmm_real_data.cc +++ b/cpp/example/padmm_real_data.cc @@ -145,8 +145,6 @@ void padmm(const std::string &name, const t_uint &imsizex, const t_uint &imsizey } int main(int, char **) { - sopt::logging::initialize(); - purify::logging::initialize(); sopt::logging::set_level("debug"); purify::logging::set_level("debug"); const std::string &name = "real_data"; diff --git a/cpp/example/padmm_reweighted_simulation.cc b/cpp/example/padmm_reweighted_simulation.cc index 56419a3ec..90eeec1cf 100644 --- a/cpp/example/padmm_reweighted_simulation.cc +++ b/cpp/example/padmm_reweighted_simulation.cc @@ -27,8 +27,6 @@ int main(int nargs, char const **args) { using namespace purify; using namespace purify::notinstalled; - sopt::logging::initialize(); - purify::logging::initialize(); sopt::logging::set_level("debug"); std::string const kernel = args[1]; diff --git a/cpp/example/padmm_simulation.cc b/cpp/example/padmm_simulation.cc index fb2d200ad..1200e7a7b 100644 --- a/cpp/example/padmm_simulation.cc +++ b/cpp/example/padmm_simulation.cc @@ -25,8 +25,6 @@ int main(int nargs, char const **args) { using namespace purify; using namespace purify::notinstalled; - sopt::logging::initialize(); - purify::logging::initialize(); sopt::logging::set_level("debug"); purify::logging::set_level("debug"); diff --git a/cpp/example/plot_wkernel.cc b/cpp/example/plot_wkernel.cc index 0661e85ff..4d5d1966d 100644 --- a/cpp/example/plot_wkernel.cc +++ b/cpp/example/plot_wkernel.cc @@ -33,7 +33,6 @@ int main(int nargs, char const **args) { ARGS_MACRO(radial, 6, false, bool) #undef ARGS_MACRO - purify::logging::initialize(); purify::logging::set_level("debug"); t_uint const J = 4; t_int const Jw = 30; diff --git a/cpp/example/sara_padmm_random_coverage.cc b/cpp/example/sara_padmm_random_coverage.cc index 92f44977c..7094e8ab5 100644 --- a/cpp/example/sara_padmm_random_coverage.cc +++ b/cpp/example/sara_padmm_random_coverage.cc @@ -20,7 +20,6 @@ int main(int, char **) { using namespace purify; using namespace purify::notinstalled; - sopt::logging::initialize(); sopt::logging::set_level("info"); std::string const fitsfile = image_filename("M31.fits"); diff --git a/cpp/example/sdmm_m31_simulation.cc b/cpp/example/sdmm_m31_simulation.cc index ca0b4a481..c14644d42 100644 --- a/cpp/example/sdmm_m31_simulation.cc +++ b/cpp/example/sdmm_m31_simulation.cc @@ -20,8 +20,6 @@ int main(int nargs, char const **args) { using namespace purify; using namespace purify::notinstalled; - sopt::logging::initialize(); - purify::logging::initialize(); if (nargs != 6) { PURIFY_CRITICAL(" Wrong number of arguments!"); diff --git a/cpp/example/time_w_algos.cc b/cpp/example/time_w_algos.cc index 2add71d20..4cf00c351 100644 --- a/cpp/example/time_w_algos.cc +++ b/cpp/example/time_w_algos.cc @@ -12,7 +12,6 @@ using namespace purify::notinstalled; int main(int nargs, char const **args) { auto const session = sopt::mpi::init(nargs, args); auto const world = sopt::mpi::Communicator::World(); - purify::logging::initialize(); purify::logging::set_level("debug"); t_int const conj = (nargs > 1) ? std::stod(static_cast(args[1])) : 0; const t_int iters = (nargs > 2) ? std::stod(static_cast(args[2])) : 1; diff --git a/cpp/example/wavelet_decomposition.cc b/cpp/example/wavelet_decomposition.cc index 1ecb36a9a..8dde64e84 100644 --- a/cpp/example/wavelet_decomposition.cc +++ b/cpp/example/wavelet_decomposition.cc @@ -10,7 +10,6 @@ using namespace purify; using namespace purify::notinstalled; int main(int nargs, char const **args) { - purify::logging::initialize(); purify::logging::set_level("debug"); auto const input_name = (nargs > 1) ? static_cast(args[1]) : image_filename("M31.fits"); diff --git a/cpp/main.cc b/cpp/main.cc index 20a7ba539..97d74e49b 100644 --- a/cpp/main.cc +++ b/cpp/main.cc @@ -25,8 +25,6 @@ using namespace purify; int main(int argc, const char **argv) { std::srand(static_cast(std::time(0))); std::mt19937 mersnne(std::time(0)); - sopt::logging::initialize(); - purify::logging::initialize(); // Read config file path from command line if (argc == 1) { diff --git a/cpp/purify/CMakeLists.txt b/cpp/purify/CMakeLists.txt index b459cca3c..8a1e75075 100644 --- a/cpp/purify/CMakeLists.txt +++ b/cpp/purify/CMakeLists.txt @@ -16,8 +16,8 @@ endfunction() configure_file(config.in.h "${PROJECT_BINARY_DIR}/include/purify/config.h") set(HEADERS - logging.h kernels.h pfitsio.h logging.disabled.h types.h - IndexMapping.h logging.enabled.h utilities.h operators.h wproj_utilities.h + logging.h kernels.h pfitsio.h types.h + IndexMapping.h utilities.h operators.h wproj_utilities.h cimg.h uvfits.h convolution.h measurement_operator_factory.h wavelet_operator_factory.h distribute.h update_factory.h convergence_factory.h @@ -32,7 +32,7 @@ set(HEADERS fly_operators.h "${PROJECT_BINARY_DIR}/include/purify/config.h") -set(SOURCES utilities.cc pfitsio.cc +set(SOURCES utilities.cc pfitsio.cc logging.cc kernels.cc wproj_utilities.cc operators.cc uvfits.cc yaml-parser.cc read_measurements.cc distribute.cc integration.cc wide_field_utilities.cc wkernel_integration.cc wproj_operators.cc uvw_utilities.cc) @@ -55,9 +55,11 @@ set(version "${Purify_VERSION_MAJOR}.${Purify_VERSION_MINOR}.${Purify_VERSION_PA set(soversion "${Purify_VERSION_MAJOR}.${Purify_VERSION_MINOR}") set_target_properties(libpurify PROPERTIES VERSION ${version} SOVERSION ${soversion}) set_target_properties(libpurify PROPERTIES OUTPUT_NAME purify) + if(PURIFY_OPENMP) target_link_libraries(libpurify OpenMP::OpenMP_CXX) endif() + if(PURIFY_MPI) target_link_libraries(libpurify ${MPI_LIBRARIES}) target_include_directories(libpurify SYSTEM PUBLIC ${MPI_CXX_INCLUDE_PATH}) @@ -105,9 +107,11 @@ target_link_libraries(libpurify Boost::filesystem Boost::system ) + if(PURIFY_CASACORE) target_link_libraries(libpurify ${CasaCore_LIBRARIES}) endif() + if(TARGET openmp::openmp) target_link_libraries(libpurify openmp::openmp) endif() @@ -115,6 +119,7 @@ endif() if(PURIFY_CUBATURE_LOOKUP) add_dependencies(libpurify Lookup-Cubature) endif() + if(PURIFY_CASACORE_LOOKUP) add_dependencies(libpurify Lookup-CasaCore) endif() diff --git a/cpp/purify/config.in.h b/cpp/purify/config.in.h index 66d815518..922779613 100644 --- a/cpp/purify/config.in.h +++ b/cpp/purify/config.in.h @@ -7,9 +7,6 @@ #define PURIFY_HAS_NOT_USING #endif -//! Whether to do logging or not -#cmakedefine PURIFY_DO_LOGGING - //! Whether to do openmp #cmakedefine PURIFY_OPENMP @@ -45,14 +42,6 @@ inline std::tuple version_tuple() { inline std::string gitref() { return "@Purify_GITREF@"; } //! Default logging level inline std::string default_logging_level() { return "@PURIFY_TEST_LOG_LEVEL@"; } -//! Default logger name -inline std::string default_logger_name() { return "@PURIFY_LOGGER_NAME@"; } -//! Wether to add color to the logger -inline constexpr bool color_logger() { - // clang-format off - return @PURIFY_COLOR_LOGGING@; - // clang-format on -} } // namespace purify #endif diff --git a/cpp/purify/logging.cc b/cpp/purify/logging.cc new file mode 100644 index 000000000..b9384132a --- /dev/null +++ b/cpp/purify/logging.cc @@ -0,0 +1,185 @@ +#include "purify/logging.h" +#include +#include +#include +#include + +using namespace std; + +namespace purify::logging { + +thread_local Log::LogMap Log::existingLogs; +thread_local Log::LevelMap Log::defaultLevels; +bool Log::showTimestamp = false; +bool Log::showLogLevel = true; +bool Log::showLoggerName = true; +bool Log::useShellColors = true; +const int Log::end_color; + +Log::Log(const string& name) : _name(name), _level(info) {} + +Log::Log(const string& name, int level) : _name(name), _level(level) {} + +/// @todo Add single static setLevel +void _updateLevels(const Log::LevelMap& defaultLevels, Log::LogMap& existingLogs) { + /// @todo Check ordering - "Foo" should come before "Foo.Bar" + for (Log::LevelMap::const_iterator lev = defaultLevels.begin(); lev != defaultLevels.end(); + ++lev) { + for (Log::LogMap::iterator log = existingLogs.begin(); log != existingLogs.end(); ++log) { + if (log->first.find(lev->first) == 0) { + log->second.setLevel(lev->second); + } + } + } +} + +void Log::setLevel(const string& name, int level) { + defaultLevels[name] = level; + // cout << name << " -> " << level << '\n'; + _updateLevels(defaultLevels, existingLogs); +} + +void Log::setLevels(const LevelMap& logLevels) { + for (LevelMap::const_iterator lev = logLevels.begin(); lev != logLevels.end(); ++lev) { + defaultLevels[lev->first] = lev->second; + } + _updateLevels(defaultLevels, existingLogs); +} + +Log& Log::getLog(const string& name) { + auto theLog = existingLogs.find(name); + if (theLog == existingLogs.end()) { + int level = info; + // Try running through all parent classes to find an existing level + string tmpname = name; + bool triedAllParents = false; + while (!triedAllParents) { + // Is there a default level? + if (defaultLevels.find(tmpname) != defaultLevels.end()) { + level = defaultLevels.find(tmpname)->second; + break; + } + // Is there already such a logger? (NB. tmpname != name in later iterations) + if (existingLogs.find(tmpname) != existingLogs.end()) { + level = existingLogs.find(tmpname)->second.getLevel(); + break; + } + // Crop the string back to the next parent level + size_t lastDot = tmpname.find_last_of("."); + if (lastDot != string::npos) { + tmpname = tmpname.substr(0, lastDot); + } else { + triedAllParents = true; + } + } + // for (LevelMap::const_iterator l = defaultLevels.begin(); l != defaultLevels.end(); ++l) { + // + // } + + // emplace returns pair + auto result = existingLogs.emplace(name, Log(name, level)); + theLog = result.first; + } + return theLog->second; +} + +string Log::getLevelName(int level) { + switch (level) { + case trace: + return "trace"; + case debug: + return "debug"; + case info: + return "info"; + case warn: + return "warn"; + case error: + return "error"; + case critical: + return "critical"; + default: + return ""; + } +} + +string Log::getColorCode(int level) { + // Skip codes if + if (!Log::useShellColors) return ""; + const static bool IS_TTY = isatty(1); + if (!IS_TTY) return ""; + + static const ColorCodes TTY_CODES = { + {trace, "\033[0;36m"}, {debug, "\033[0;34m"}, {info, "\033[0;32m"}, {warn, "\033[0;33m"}, + {error, "\033[0;31m"}, {critical, "\033[0;31m"}, {end_color, "\033[0m"} // end-color code + }; + try { + return TTY_CODES.at(level); + } catch (...) { + return ""; + } +} + +Log::Level Log::getLevelFromName(const string& level) { + if (level == "trace") return trace; + if (level == "debug") return debug; + if (level == "info") return info; + if (level == "warn") return warn; + if (level == "error") return error; + if (level == "critical") return critical; + throw std::runtime_error("Couldn't create a log level from string '" + level + "'"); +} + +string Log::formatMessage(int level, const string& message) { + string out; + out += getColorCode(level); + + if (Log::showLoggerName) { + out += getName(); + // out += ": "; + } + + if (Log::showLogLevel) { + out += Log::getLevelName(level); + out += " "; + } + + if (Log::showTimestamp) { + time_t rawtime; + time(&rawtime); + char* timestr = ctime(&rawtime); + timestr[24] = ' '; + out += timestr; + out += " "; + } + + out += getColorCode(end_color); + out += " "; + out += message; + + return out; +} + +void Log::log(int level, const string& message) { + if (isActive(level)) { + if (level > warning) + cerr << formatMessage(level, message) << '\n'; + else + cout << formatMessage(level, message) << '\n'; + } +} + +ostream& operator<<(Log& log, int level) { + if (log.isActive(level)) { + if (level > Log::warning) { + cerr << log.formatMessage(level, ""); + return cerr; + } else { + cout << log.formatMessage(level, ""); + return cout; + } + } else { + static ostream devNull(nullptr); + return devNull; + } +} +} // namespace purify::logging diff --git a/cpp/purify/logging.disabled.h b/cpp/purify/logging.disabled.h deleted file mode 100644 index 30241ac53..000000000 --- a/cpp/purify/logging.disabled.h +++ /dev/null @@ -1,24 +0,0 @@ -#ifndef PURIFY_LOGGING_DISABLED_H -#define PURIFY_LOGGING_DISABLED_H - -#include "purify/config.h" -#include -#include - -namespace purify::logging { -//! Name of the purify logger -const std::string name_prefix = "purify::"; - -inline std::shared_ptr initialize(std::string const &) { return nullptr; } -inline std::shared_ptr initialize() { return nullptr; } -inline std::shared_ptr get(std::string const &) { return nullptr; } -inline std::shared_ptr get() { return nullptr; } -inline void set_level(std::string const &, std::string const &){}; -inline void set_level(std::string const &){}; -inline bool has_level(std::string const &, std::string const &) { return false; } -} // namespace purify::logging - -//! \macro For internal use only -#define PURIFY_LOG_(...) ((void)0) - -#endif diff --git a/cpp/purify/logging.enabled.h b/cpp/purify/logging.enabled.h deleted file mode 100644 index 3d276e32a..000000000 --- a/cpp/purify/logging.enabled.h +++ /dev/null @@ -1,79 +0,0 @@ -#ifndef PURIFY_LOGGING_ENABLED_H -#define PURIFY_LOGGING_ENABLED_H - -#include "purify/config.h" -#include -#include -#include - -using spdlogPtr = std::shared_ptr; - -namespace purify::logging { - -void set_level(std::string const &level, std::string const &name = ""); - -//! \brief Initializes a logger. -//! \details Logger only exists as long as return is kept alive. -inline spdlogPtr initialize(std::string const &name = "") { - const std::string loggerName = default_logger_name() + name; - const spdlogPtr result = spdlog::stdout_logger_mt(default_logger_name() + name); - if (!spdlog::get(loggerName)) spdlog::register_logger(result); - set_level(default_logging_level(), name); - return result; -} - -//! Returns shared pointer to logger or null if it does not exist -inline spdlogPtr get(std::string const &name = "") { - return spdlog::get(default_logger_name() + name); -} - -//! \brief Sets logging level -//! \details Levels can be one of -//! - "trace" -//! - "debug" -//! - "info" -//! - "warn" -//! - "err" -//! - "critical" -//! - "off" -inline void set_level(std::string const &level, std::string const &name) { - const spdlogPtr logger = get(name); - if (not logger) throw std::runtime_error("No logger by the name of " + std::string(name)); -#define PURIFY_MACRO(LEVEL) \ - if (level == #LEVEL) logger->set_level(spdlog::level::LEVEL) - PURIFY_MACRO(trace); - else PURIFY_MACRO(debug); - else PURIFY_MACRO(info); - else PURIFY_MACRO(warn); - else PURIFY_MACRO(err); - else PURIFY_MACRO(critical); - else PURIFY_MACRO(off); -#undef PURIFY_MACRO - else throw std::runtime_error("Unknown logging level " + std::string(level)); -} - -inline bool has_level(std::string const &level, std::string const &name = "") { - const spdlogPtr logger = get(name); - if (not logger) return false; - -#define PURIFY_MACRO(LEVEL) \ - if (level == #LEVEL) return logger->level() >= spdlog::level::LEVEL - PURIFY_MACRO(trace); - else PURIFY_MACRO(debug); - else PURIFY_MACRO(info); - else PURIFY_MACRO(warn); - else PURIFY_MACRO(err); - else PURIFY_MACRO(critical); - else PURIFY_MACRO(off); -#undef PURIFY_MACRO - else throw std::runtime_error("Unknown logging level " + std::string(level)); -} - -} // namespace purify::logging - -//! \macro For internal use only -#define PURIFY_LOG_(NAME, TYPE, ...) \ - if (auto purify_logging_##__func__##_##__LINE__ = purify::logging::get(NAME)) \ - purify_logging_##__func__##_##__LINE__->TYPE(__VA_ARGS__) - -#endif diff --git a/cpp/purify/logging.h b/cpp/purify/logging.h index a2267c6f4..df07af005 100644 --- a/cpp/purify/logging.h +++ b/cpp/purify/logging.h @@ -2,32 +2,208 @@ #define PURIFY_LOGGING_H #include "purify/config.h" +#include +#include +#include +#include +#include +#include +#include -#ifdef PURIFY_DO_LOGGING -#include "purify/logging.enabled.h" -#else -#include "purify/logging.disabled.h" -#endif +namespace purify::logging { + +/// @brief Logging system for controlled & formatted writing to stdout +class Log { + public: + /// Log priority levels. + enum Level { + trace = 0, + debug = 10, + info = 20, + warn = 30, + warning = 30, + error = 40, + critical = 50, + always = 50 + }; + static const int end_color = -10; ///< Special "level-like" code to end coloring + + /// Typedef for a collection of named logs. + using LogMap = std::map; + + /// Typedef for a collection of named log levels. + using LevelMap = std::map; + + /// @brief Typedef for a collection of shell color codes, accessed by log level. + using ColorCodes = std::map; + + private: + /// A static map of existing logs: we don't make more loggers than necessary. + thread_local static LogMap existingLogs; + + /// A static map of default log levels. + thread_local static LevelMap defaultLevels; + + /// Show timestamp? + static bool showTimestamp; + + /// Show log level? + static bool showLogLevel; + + /// Show logger name? + static bool showLoggerName; + + /// Use shell colour escape codes? + static bool useShellColors; + + public: + /// Set the log levels + static void setLevel(const std::string& name, int level); + static void setLevels(const LevelMap& logLevels); + + protected: + /// @name Hidden constructors etc. + /// @{ + + /// Constructor 1 + Log(const std::string& name); + + /// Constructor 2 + Log(const std::string& name, int level); + + /// @} + + /// @brief Get the TTY code string for coloured messages + static std::string getColorCode(int level); + + public: + /// Get a logger with the given name. The level will be taken from the + /// "requestedLevels" static map or will be INFO by default. + static Log& getLog(const std::string& name); + + /// Get the priority level of this logger. + int getLevel() const { return _level; } + + /// Set the priority level of this logger. + Log& setLevel(int level) { + _level = level; + return *this; + } + + /// Get a log level enum from a string. + static Level getLevelFromName(const std::string& level); + + /// Get the std::string representation of a log level. + static std::string getLevelName(int level); + + /// Get the name of this logger. + std::string getName() const { return _name; } + + /// Set the name of this logger. + Log& setName(const std::string& name) { + _name = name; + return *this; + } + + /// Will this log level produce output on this logger at the moment? + bool isActive(int level) const { return (level >= _level); } + + private: + /// This logger's name + std::string _name; + + /// Threshold level for this logger. + int _level; + + protected: + /// Write a message at a particular level. + void log(int level, const std::string& message); + + /// Turn a message string into the current log format. + std::string formatMessage(int level, const std::string& message); + + public: + /// The streaming operator can use Log's internals. + friend std::ostream& operator<<(Log& log, int level); +}; + +/// Streaming output to a logger must have a Log::Level/int as its first argument. +std::ostream& operator<<(Log& log, int level); + +/// Access method to default Log object +inline Log& getLog() { return Log::getLog("purify::"); } + +/// Method to set the logging level of the default Log object +inline void set_level(const std::string& level) { getLog().setLevel(Log::getLevelFromName(level)); } + +/// Helper method to ireplace a set of curly braces with +/// the template argument @a arg in a string stream +template +void applyFormat(std::stringstream& ss, char*& pos, Arg&& arg) { + char* delim = strstr(pos, "{}"); + if (delim != NULL) { + ss << std::string(pos, delim - pos) << std::forward(arg); + pos = delim + 2; + } else { + throw std::runtime_error("Insufficient placeholders for number of arguments!"); + } +} + +/// Helper method to construct formatted string +template +inline std::string mkFormattedString(const char* txt, Args&&... args) { + std::string mys = txt; + std::stringstream rtn; + char* pos = (char*)txt; + ((void)applyFormat(rtn, pos, std::forward(args)), ...); + rtn << std::string(pos); + return rtn.str(); +} + +inline std::string mkFormattedString(const std::string& txt) { + return mkFormattedString(txt.data()); +} + +/// @defgroup logmacros Logging macros +/// @{ + +/// @def PURIFY_MSG_LVL +/// @brief Neat CPU-conserving logging macros. Use by preference! +/// @note Only usable in classes where a getLog() method is provided +#define PURIFY_MSG_LVL(lvl, ...) \ + do { \ + if (purify::logging::getLog().isActive(lvl)) { \ + purify::logging::getLog() << lvl << purify::logging::mkFormattedString(__VA_ARGS__) << '\n'; \ + } \ + } while (0) + +//! \macro For internal use only +#define PURIFY_LOG_(TYPE, ...) PURIFY_MSG_LVL(purify::logging::Log::TYPE, __VA_ARGS__) + +/// @} + +} // namespace purify::logging //! \macro Normal but signigicant condition -#define PURIFY_CRITICAL(...) PURIFY_LOG_(, critical, __VA_ARGS__) +#define PURIFY_CRITICAL(...) PURIFY_LOG_(critical, __VA_ARGS__) //! \macro Something is definitely wrong, algorithm exits -#define PURIFY_ERROR(...) PURIFY_LOG_(, error, __VA_ARGS__) +#define PURIFY_ERROR(...) PURIFY_LOG_(error, __VA_ARGS__) //! \macro Something might be going wrong -#define PURIFY_WARN(...) PURIFY_LOG_(, warn, __VA_ARGS__) +#define PURIFY_WARN(...) PURIFY_LOG_(warn, __VA_ARGS__) //! \macro Informational message about normal condition //! \details Say "Residuals == " -#define PURIFY_INFO(...) PURIFY_LOG_(, info, __VA_ARGS__) +#define PURIFY_INFO(...) PURIFY_LOG_(info, __VA_ARGS__) //! \macro Output some debugging -#define PURIFY_DEBUG(...) PURIFY_LOG_(, debug, __VA_ARGS__) +#define PURIFY_DEBUG(...) PURIFY_LOG_(debug, __VA_ARGS__) //! \macro Output internal values of no interest to anyone //! \details Except maybe when debugging. -#define PURIFY_TRACE(...) PURIFY_LOG_(, trace, __VA_ARGS__) +#define PURIFY_TRACE(...) PURIFY_LOG_(trace, __VA_ARGS__) //! High priority message -#define PURIFY_HIGH_LOG(...) PURIFY_LOG_(, critical, __VA_ARGS__) +#define PURIFY_HIGH_LOG(...) PURIFY_LOG_(critical, __VA_ARGS__) //! Medium priority message -#define PURIFY_MEDIUM_LOG(...) PURIFY_LOG_(, info, __VA_ARGS__) +#define PURIFY_MEDIUM_LOG(...) PURIFY_LOG_(info, __VA_ARGS__) //! Low priority message -#define PURIFY_LOW_LOG(...) PURIFY_LOG_(, debug, __VA_ARGS__) +#define PURIFY_LOW_LOG(...) PURIFY_LOG_(debug, __VA_ARGS__) + #endif diff --git a/cpp/tests/common_catch_main.cc b/cpp/tests/common_catch_main.cc index c53d72582..d79823b54 100644 --- a/cpp/tests/common_catch_main.cc +++ b/cpp/tests/common_catch_main.cc @@ -18,8 +18,5 @@ int main(int argc, char **argv) { return returnCode; mersenne.reset(new std::mt19937_64(session.configData().rngSeed)); - sopt::logging::initialize(); - purify::logging::initialize(); - return session.run(); } diff --git a/cpp/tests/common_mpi_catch_main.cc b/cpp/tests/common_mpi_catch_main.cc index 9c0f62758..e36222cd8 100644 --- a/cpp/tests/common_mpi_catch_main.cc +++ b/cpp/tests/common_mpi_catch_main.cc @@ -47,9 +47,6 @@ int main(int argc, const char **argv) { return returnCode; mersenne.reset(new std::mt19937_64(session.configData().rngSeed)); - sopt::logging::initialize(); - purify::logging::initialize(); - auto const result = session.run(); return result; diff --git a/sopt b/sopt index dfb7a2752..2d5378083 160000 --- a/sopt +++ b/sopt @@ -1 +1 @@ -Subproject commit dfb7a27520a45c2faa60540685236769ed50d2ba +Subproject commit 2d5378083a1cced7e0727ca8d8ae809d25cd9cb1