From 6e39c8dd30f1940e4a677fdeb9abb4d162ab8c61 Mon Sep 17 00:00:00 2001 From: Jakob Maljaars <jakob.maljaars@stcorp.nl> Date: Fri, 8 Apr 2022 14:58:27 +0000 Subject: [PATCH] AST-779 set up deconvolution repository --- .clang-format | 156 +++ .gitlab-ci.astron.yml | 7 + .gitlab-ci.common.yml | 126 +++ .gitmodules | 9 + CMakeLists.txt | 170 ++++ LICENSE | 676 ++++++++++++ README.md | 4 +- cmake/FindCFITSIO.cmake | 56 + cmake/FindCasacore.cmake | 277 +++++ cmake/config/radler-config-version.cmake.in | 14 + cmake/config/radler-config.cmake.in | 45 + cmake/unittest.cmake | 45 + cpp/CMakeLists.txt | 116 +++ cpp/algorithms/deconvolution_algorithm.cc | 51 + cpp/algorithms/deconvolution_algorithm.h | 141 +++ cpp/algorithms/generic_clean.cc | 208 ++++ cpp/algorithms/generic_clean.h | 46 + cpp/algorithms/iuwt/image_analysis.cc | 328 ++++++ cpp/algorithms/iuwt/image_analysis.h | 84 ++ cpp/algorithms/iuwt/iuwt_decomposition.cc | 240 +++++ cpp/algorithms/iuwt/iuwt_decomposition.h | 346 +++++++ cpp/algorithms/iuwt/iuwt_mask.cc | 36 + cpp/algorithms/iuwt/iuwt_mask.h | 86 ++ cpp/algorithms/iuwt_deconvolution.h | 47 + .../iuwt_deconvolution_algorithm.cc | 962 ++++++++++++++++++ cpp/algorithms/iuwt_deconvolution_algorithm.h | 194 ++++ cpp/algorithms/ls_deconvolution.cc | 315 ++++++ cpp/algorithms/ls_deconvolution.h | 63 ++ cpp/algorithms/more_sane.cc | 87 ++ cpp/algorithms/more_sane.h | 42 + .../multiscale/multiscale_transforms.cc | 39 + .../multiscale/multiscale_transforms.h | 196 ++++ cpp/algorithms/multiscale_algorithm.cc | 711 +++++++++++++ cpp/algorithms/multiscale_algorithm.h | 138 +++ cpp/algorithms/parallel_deconvolution.cc | 436 ++++++++ cpp/algorithms/parallel_deconvolution.h | 105 ++ cpp/algorithms/python_deconvolution.cc | 277 +++++ cpp/algorithms/python_deconvolution.h | 52 + cpp/algorithms/simple_clean.cc | 179 ++++ cpp/algorithms/simple_clean.h | 40 + cpp/algorithms/subminor_loop.cc | 232 +++++ cpp/algorithms/subminor_loop.h | 209 ++++ cpp/algorithms/test/CMakeLists.txt | 8 + cpp/algorithms/test/runtests.cc | 6 + cpp/algorithms/test/test_simple_clean.cc | 54 + .../threaded_deconvolution_tools.cc | 197 ++++ cpp/algorithms/threaded_deconvolution_tools.h | 99 ++ cpp/component_list.cc | 158 +++ cpp/component_list.h | 234 +++++ cpp/deconvolution_settings.h | 155 +++ cpp/deconvolution_table.cc | 39 + cpp/deconvolution_table.h | 159 +++ cpp/deconvolution_table_entry.h | 67 ++ cpp/demo/CMakeLists.txt | 7 + cpp/demo/multiscale_example.cc | 105 ++ cpp/image_set.cc | 464 +++++++++ cpp/image_set.h | 337 ++++++ cpp/logging/controllable_log.h | 67 ++ cpp/logging/subimage_logset.h | 104 ++ cpp/math/dijkstrasplitter.h | 320 ++++++ cpp/math/peak_finder.cc | 262 +++++ cpp/math/peak_finder.h | 129 +++ cpp/math/rms_image.cc | 96 ++ cpp/math/rms_image.h | 35 + cpp/math/test/CMakeLists.txt | 9 + cpp/math/test/runtests.cc | 6 + cpp/math/test/test_dijkstra_splitter.cc | 539 ++++++++++ cpp/math/test/test_peak_finder.cc | 220 ++++ cpp/radler.cc | 391 +++++++ cpp/radler.h | 80 ++ cpp/test/CMakeLists.txt | 9 + cpp/test/runtests.cc | 6 + cpp/test/smartptr.h | 54 + cpp/test/test_component_list.cc | 110 ++ cpp/test/test_deconvolution_table.cc | 96 ++ cpp/test/test_image_set.cc | 495 +++++++++ cpp/utils/application.h | 49 + cpp/utils/casa_mask_reader.cc | 40 + cpp/utils/casa_mask_reader.h | 26 + cpp/utils/write_model.h | 61 ++ docker/ubuntu_20_04_base | 17 + docker/ubuntu_22_04_base | 17 + external/aocommon | 1 + external/pybind11 | 1 + external/schaapcommon | 1 + scripts/run-format.sh | 21 + 86 files changed, 12940 insertions(+), 2 deletions(-) create mode 100644 .clang-format create mode 100644 .gitlab-ci.astron.yml create mode 100644 .gitlab-ci.common.yml create mode 100644 .gitmodules create mode 100644 CMakeLists.txt create mode 100644 LICENSE create mode 100644 cmake/FindCFITSIO.cmake create mode 100644 cmake/FindCasacore.cmake create mode 100644 cmake/config/radler-config-version.cmake.in create mode 100644 cmake/config/radler-config.cmake.in create mode 100644 cmake/unittest.cmake create mode 100644 cpp/CMakeLists.txt create mode 100644 cpp/algorithms/deconvolution_algorithm.cc create mode 100644 cpp/algorithms/deconvolution_algorithm.h create mode 100644 cpp/algorithms/generic_clean.cc create mode 100644 cpp/algorithms/generic_clean.h create mode 100644 cpp/algorithms/iuwt/image_analysis.cc create mode 100644 cpp/algorithms/iuwt/image_analysis.h create mode 100644 cpp/algorithms/iuwt/iuwt_decomposition.cc create mode 100644 cpp/algorithms/iuwt/iuwt_decomposition.h create mode 100644 cpp/algorithms/iuwt/iuwt_mask.cc create mode 100644 cpp/algorithms/iuwt/iuwt_mask.h create mode 100644 cpp/algorithms/iuwt_deconvolution.h create mode 100644 cpp/algorithms/iuwt_deconvolution_algorithm.cc create mode 100644 cpp/algorithms/iuwt_deconvolution_algorithm.h create mode 100644 cpp/algorithms/ls_deconvolution.cc create mode 100644 cpp/algorithms/ls_deconvolution.h create mode 100644 cpp/algorithms/more_sane.cc create mode 100644 cpp/algorithms/more_sane.h create mode 100644 cpp/algorithms/multiscale/multiscale_transforms.cc create mode 100644 cpp/algorithms/multiscale/multiscale_transforms.h create mode 100644 cpp/algorithms/multiscale_algorithm.cc create mode 100644 cpp/algorithms/multiscale_algorithm.h create mode 100644 cpp/algorithms/parallel_deconvolution.cc create mode 100644 cpp/algorithms/parallel_deconvolution.h create mode 100644 cpp/algorithms/python_deconvolution.cc create mode 100644 cpp/algorithms/python_deconvolution.h create mode 100644 cpp/algorithms/simple_clean.cc create mode 100644 cpp/algorithms/simple_clean.h create mode 100644 cpp/algorithms/subminor_loop.cc create mode 100644 cpp/algorithms/subminor_loop.h create mode 100644 cpp/algorithms/test/CMakeLists.txt create mode 100644 cpp/algorithms/test/runtests.cc create mode 100644 cpp/algorithms/test/test_simple_clean.cc create mode 100644 cpp/algorithms/threaded_deconvolution_tools.cc create mode 100644 cpp/algorithms/threaded_deconvolution_tools.h create mode 100644 cpp/component_list.cc create mode 100644 cpp/component_list.h create mode 100644 cpp/deconvolution_settings.h create mode 100644 cpp/deconvolution_table.cc create mode 100644 cpp/deconvolution_table.h create mode 100644 cpp/deconvolution_table_entry.h create mode 100644 cpp/demo/CMakeLists.txt create mode 100644 cpp/demo/multiscale_example.cc create mode 100644 cpp/image_set.cc create mode 100644 cpp/image_set.h create mode 100644 cpp/logging/controllable_log.h create mode 100644 cpp/logging/subimage_logset.h create mode 100644 cpp/math/dijkstrasplitter.h create mode 100644 cpp/math/peak_finder.cc create mode 100644 cpp/math/peak_finder.h create mode 100644 cpp/math/rms_image.cc create mode 100644 cpp/math/rms_image.h create mode 100644 cpp/math/test/CMakeLists.txt create mode 100644 cpp/math/test/runtests.cc create mode 100644 cpp/math/test/test_dijkstra_splitter.cc create mode 100644 cpp/math/test/test_peak_finder.cc create mode 100644 cpp/radler.cc create mode 100644 cpp/radler.h create mode 100644 cpp/test/CMakeLists.txt create mode 100644 cpp/test/runtests.cc create mode 100644 cpp/test/smartptr.h create mode 100644 cpp/test/test_component_list.cc create mode 100644 cpp/test/test_deconvolution_table.cc create mode 100644 cpp/test/test_image_set.cc create mode 100644 cpp/utils/application.h create mode 100644 cpp/utils/casa_mask_reader.cc create mode 100644 cpp/utils/casa_mask_reader.h create mode 100644 cpp/utils/write_model.h create mode 100644 docker/ubuntu_20_04_base create mode 100644 docker/ubuntu_22_04_base create mode 160000 external/aocommon create mode 160000 external/pybind11 create mode 160000 external/schaapcommon create mode 100755 scripts/run-format.sh diff --git a/.clang-format b/.clang-format new file mode 100644 index 00000000..caf3c006 --- /dev/null +++ b/.clang-format @@ -0,0 +1,156 @@ +--- +Language: Cpp +# BasedOnStyle: Google +AccessModifierOffset: -1 +AlignAfterOpenBracket: Align +AlignConsecutiveMacros: false +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlines: Left +AlignOperands: true +AlignTrailingComments: true +AllowAllArgumentsOnNextLine: true +AllowAllConstructorInitializersOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: All +AllowShortLambdasOnASingleLine: All +AllowShortIfStatementsOnASingleLine: WithoutElse +AllowShortLoopsOnASingleLine: true +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: true +AlwaysBreakTemplateDeclarations: Yes +BinPackArguments: true +BinPackParameters: true +BraceWrapping: + AfterCaseLabel: false + AfterClass: false + AfterControlStatement: false + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + AfterExternBlock: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Attach +BreakBeforeInheritanceComma: false +BreakInheritanceList: BeforeColon +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +BreakConstructorInitializers: BeforeColon +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: true +ColumnLimit: 80 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: true +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DerivePointerAlignment: false +DisableFormat: false +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: true +ForEachMacros: + - foreach + - Q_FOREACH + - BOOST_FOREACH +IncludeBlocks: Regroup +IncludeCategories: + - Regex: '^<ext/.*\.h>' + Priority: 2 + - Regex: '^<.*\.h>' + Priority: 1 + - Regex: '^<.*' + Priority: 2 + - Regex: '.*' + Priority: 3 +IncludeIsMainRegex: '([-_](test|unittest))?$' +IndentCaseLabels: true +IndentPPDirectives: None +IndentWidth: 2 +IndentWrappedFunctionNames: false +JavaScriptQuotes: Leave +JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: false +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBinPackProtocolList: Never +ObjCBlockIndentWidth: 2 +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 1 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyBreakTemplateDeclaration: 10 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 200 +PointerAlignment: Left +RawStringFormats: + - Language: Cpp + Delimiters: + - cc + - CC + - cpp + - Cpp + - CPP + - 'c++' + - 'C++' + CanonicalDelimiter: '' + BasedOnStyle: google + - Language: TextProto + Delimiters: + - pb + - PB + - proto + - PROTO + EnclosingFunctions: + - EqualsProto + - EquivToProto + - PARSE_PARTIAL_TEXT_PROTO + - PARSE_TEST_PROTO + - PARSE_TEXT_PROTO + - ParseTextOrDie + - ParseTextProtoOrDie + CanonicalDelimiter: '' + BasedOnStyle: google +ReflowComments: true +SortIncludes: false +SortUsingDeclarations: true +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 2 +SpacesInAngles: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: c++17 +StatementMacros: + - Q_UNUSED + - QT_REQUIRE_VERSION +TabWidth: 8 +UseTab: Never +... diff --git a/.gitlab-ci.astron.yml b/.gitlab-ci.astron.yml new file mode 100644 index 00000000..cf5e75ea --- /dev/null +++ b/.gitlab-ci.astron.yml @@ -0,0 +1,7 @@ +# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +# SPDX-License-Identifier: GPL-3.0-or-later + +# This file contains the pipelines that run on the Astron repository of Radler, +# which is at https://git.astron.nl/RD/radler + +include: .gitlab-ci.common.yml \ No newline at end of file diff --git a/.gitlab-ci.common.yml b/.gitlab-ci.common.yml new file mode 100644 index 00000000..c2cbafac --- /dev/null +++ b/.gitlab-ci.common.yml @@ -0,0 +1,126 @@ +# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +# SPDX-License-Identifier: GPL-3.0-or-later + +workflow: + rules: + # don't create a pipeline if its a commit pipeline, on a branch and that branch has open merge requests (we will get a MR build instead) + - if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS + when: never + - when: always + +stages: + - versioning + - prepare + - linting + - build + - test + +# The 'IMAGE' variables allow reusing docker images between different pipelines. +# See https://confluence.skatelescope.org/display/SE/Caching+Docker+images+using+GitLab+CI+registry +versioning: + stage: versioning + image: bitnami/git + script: + # Unshallowing ensures that 'git log' works + # - git fetch --unshallow + - git fetch --depth=1000 + - echo BASE_IMAGE_2004=${CI_REGISTRY_IMAGE}/base_2004:$(git log -n 1 --pretty=format:%H -- docker/ubuntu_20_04_base) > versions.env + - echo BASE_IMAGE_2204=${CI_REGISTRY_IMAGE}/base_2204:$(git log -n 1 --pretty=format:%H -- docker/ubuntu_22_04_base) >> versions.env + - cat versions.env + artifacts: + reports: + dotenv: versions.env + +.prepare: + stage: prepare + needs: ["versioning"] + image: docker:20.10 + services: + - docker:20.10-dind + before_script: + - echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY + script: + - | + if ! docker manifest inspect $DOCKER_IMAGE > /dev/null; then + docker build $DOCKER_BUILD_ARG --tag $DOCKER_IMAGE -f $DOCKER_FILE . + docker push $DOCKER_IMAGE + fi + # Skip the job if there are no changes to the Docker file. This shortcut only + # works for push and merge request jobs. + # A manual pipeline run will thus create missing docker images. + rules: + - changes: + - $DOCKER_FILE + +prepare-base-2004: + extends: .prepare + variables: + DOCKER_IMAGE: $BASE_IMAGE_2004 + DOCKER_FILE: docker/ubuntu_20_04_base + +prepare-base-2204: + extends: .prepare + variables: + DOCKER_IMAGE: $BASE_IMAGE_2204 + DOCKER_FILE: docker/ubuntu_22_04_base + +.needs-2004: + needs: + - job: versioning + - job: prepare-base-2004 + optional: true + image: $BASE_IMAGE_2004 + +.needs-2204: + needs: + - job: versioning + - job: prepare-base-2204 + optional: true + image: $BASE_IMAGE_2204 + +clang-format: + extends: .needs-2004 + stage: linting + script: + # Explicitly checking out submodules might become redundant + # once git fetch --unshallow works. + - git submodule update --init --recursive --checkout --depth 1 + - ./scripts/run-format.sh + +.build: + stage: build + script: + - cmake --version + - mkdir build && cd build + - cmake -DBUILD_TESTING=ON -DCMAKE_INSTALL_PREFIX=.. -DCMAKE_CXX_FLAGS="-coverage" -DCMAKE_EXE_LINKER_FLAGS="-coverage" .. + - make -j`nproc` + - make install + artifacts: + paths: + - build + +build-2004: + extends: [".needs-2004",".build"] + +build-2204: + extends: [".needs-2204",".build"] + +.test: + stage: test + script: + - cd build/ + - ctest -j`nproc` --output-on-failure -T test + +test-2004: + extends: .test + needs: ["versioning","build-2004"] + image: $BASE_IMAGE_2004 + after_script: + - gcovr -j`nproc` -r .. -e '.*/external/.*' -e '.*/CompilerIdCXX/.*' -e '.*/test/.*' + +test-2204: + extends: .test + needs: ["versioning","build-2204"] + image: $BASE_IMAGE_2204 + after_script: + - gcovr -j`nproc` -r .. -e '.*/external/.*' -e '.*/CompilerIdCXX/.*' -e '.*/test/.*' diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..819fcba2 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,9 @@ +[submodule "external/aocommon"] + path = external/aocommon + url = https://gitlab.com/aroffringa/aocommon.git +[submodule "external/schaapcommon"] + path = external/schaapcommon + url = https://git.astron.nl/RD/schaapcommon.git +[submodule "external/pybind11"] + path = external/pybind11 + url = https://github.com/pybind/pybind11.git diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 00000000..38374b06 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,170 @@ +# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +# SPDX-License-Identifier: GPL-3.0-or-later + +cmake_minimum_required(VERSION 3.8) + +# When Radler is compiled as an ExternalProject inside another project, set this +# option to On. See, e.g., the wsclean CMake file for an example. +option(COMPILE_AS_EXTERNAL_PROJECT OFF) + +set(RADLER_VERSION 0.0.0) +project(radler VERSION ${RADLER_VERSION}) +if(RADLER_VERSION MATCHES "^([0-9]+)\\.([0-9]+)\\.([0-9]+)") + set(RADLER_VERSION_MAJOR "${CMAKE_MATCH_1}") + set(RADLER_VERSION_MINOR "${CMAKE_MATCH_2}") + set(RADLER_VERSION_PATCH "${CMAKE_MATCH_3}") +else() + message(FATAL_ERROR "Failed to parse RADLER_VERSION='${RADLER_VERSION}'") +endif() + +if(POLICY CMP0076) + cmake_policy(SET CMP0076 NEW) +endif() + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED YES) +set(CMAKE_CXX_EXTENSIONS OFF) + +include(CheckCXXCompilerFlag) +if(PORTABLE) + if(DEFINED TARGET_CPU) + message(WARNING "You have selected to build PORTABLE binaries. " + "TARGET_CPU settings will be ignored.") + unset(TARGET_CPU CACHE) + endif() +endif() + +if(NOT COMPILE_AS_EXTERNAL_PROJECT) + # Include submodules + set(ExternalSubmoduleDirectories aocommon pybind11 schaapcommon) + foreach(ExternalSubmodule ${ExternalSubmoduleDirectories}) + if(NOT EXISTS ${CMAKE_SOURCE_DIR}/external/${ExternalSubmodule}) + message( + FATAL_ERROR + "The external submodule '${ExternalSubmodule}' is missing in the external/ subdirectory. " + "This is likely the result of downloading a git tarball without submodules. " + "This is not supported: git tarballs do not provide the required versioning " + "information for the submodules. Please perform a git clone of this repository." + ) + endif() + endforeach() + + # Find and include git submodules + find_package(Git QUIET) + if(GIT_FOUND AND EXISTS "${PROJECT_SOURCE_DIR}/.git") + # Update submodules as needed + option(GIT_SUBMODULE "Check submodules during build" ON) + if(GIT_SUBMODULE) + message(STATUS "Submodule update") + execute_process( + COMMAND ${GIT_EXECUTABLE} submodule update --init --recursive --checkout + --depth 1 + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + RESULT_VARIABLE GIT_SUBMOD_RESULT) + if(NOT GIT_SUBMOD_RESULT EQUAL "0") + message( + FATAL_ERROR + "git submodule update --init failed with ${GIT_SUBMOD_RESULT}, please checkout submodules" + ) + endif() + endif() + endif() +endif() + +if(COMPILE_AS_EXTERNAL_PROJECT) + message( + STATUS "Radler is compiled as an external project within another project.") + if(NOT DEFINED AOCOMMON_INCLUDE_DIR) + message( + FATAL_ERROR + "AOCOMMON_INCLUDE_DIR not specified. Please add -DAOCOMMON_INCLUDE_DIR to the CMAKE_ARGS." + ) + endif() + if(NOT DEFINED SCHAAPCOMMON_SOURCE_DIR) + message( + FATAL_ERROR + "SCHAAPCOMMON_SOURCE_DIR not specified. Please add -DSCHAAPCOMMON_SOURCE_DIR to the CMAKE_ARGS." + ) + endif() + if(NOT DEFINED PYBIND11_SOURCE_DIR) + message( + FATAL_ERROR + "PYBIND11_SOURCE_DIR not specified. Please add -DPYBIND11_SOURCE_DIR to the CMAKE_ARGS." + ) + endif() +else() + set(AOCOMMON_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/external/aocommon/include) + set(SCHAAPCOMMON_SOURCE_DIR ${CMAKE_SOURCE_DIR}/external/schaapcommon) + set(PYBIND11_SOURCE_DIR ${CMAKE_SOURCE_DIR}/external/pybind11) +endif() + +set(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake) + +# Boost date_time is needed in aocommon +find_package( + Boost + COMPONENTS date_time + REQUIRED) + +# Threads +find_package(Threads REQUIRED) + +# Find and include HDF5 +find_package( + HDF5 + COMPONENTS C CXX + REQUIRED) +add_definitions(${HDF5_DEFINITIONS} -DH5_USE_110_API) + +# Casacore +set(CASACORE_MAKE_REQUIRED_EXTERNALS_OPTIONAL TRUE) +find_package(Casacore REQUIRED COMPONENTS fits casa ms tables measures) + +# CFitsio +find_package(CFITSIO REQUIRED) + +# Python3 +find_package(PythonLibs 3 REQUIRED) +find_package(PythonInterp REQUIRED) +message(STATUS "Using python version ${PYTHON_VERSION_STRING}") + +if(COMPILE_AS_EXTERNAL_PROJECT) + add_subdirectory(${SCHAAPCOMMON_SOURCE_DIR} + ${PROJECT_BINARY_DIR}/schaapcommon) + add_subdirectory(${PYBIND11_SOURCE_DIR} ${PROJECT_BINARY_DIR}/pybind11) +else() + add_subdirectory(${SCHAAPCOMMON_SOURCE_DIR}) + add_subdirectory(${PYBIND11_SOURCE_DIR}) +endif() +target_include_directories(schaapcommon SYSTEM PUBLIC ${AOCOMMON_INCLUDE_DIR}) + +set(RADLER_TARGET_INCLUDE_DIRS + ${AOCOMMON_INCLUDE_DIR} + ${Boost_INCLUDE_DIRS} + ${CASACORE_INCLUDE_DIRS} + ${CFITSIO_INCLUDE_DIR} + ${HDF5_INCLUDE_DIRS} + ${pybind11_INCLUDE_DIRS} + ${SCHAAPCOMMON_SOURCE_DIR}/include) + +set(RADLER_TARGET_LIBS + ${Boost_DATE_TIME_LIBRARY} ${CMAKE_THREAD_LIBS_INIT} ${CASACORE_LIBRARIES} + ${CFITSIO_LIBRARY} pybind11::embed schaapcommon) + +# Source directories +add_subdirectory(cpp) + +# Compile tests +if(NOT ${COMPILE_AS_EXTERNAL_PROJECT} AND ${BUILD_TESTING}) + include(CTest) + find_package( + Boost + COMPONENTS unit_test_framework + REQUIRED) + add_subdirectory(cpp/test) + add_subdirectory(cpp/algorithms/test) + add_subdirectory(cpp/math/test) + + # demo targets are excluded from a default build + add_subdirectory(cpp/demo) +endif() diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..be8c8257 --- /dev/null +++ b/LICENSE @@ -0,0 +1,676 @@ + Copyright 2022 ASTRON + + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + 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 <http://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + <program> Copyright (C) <year> <name of author> + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +<http://www.gnu.org/licenses/>. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +<http://www.gnu.org/philosophy/why-not-lgpl.html>. diff --git a/README.md b/README.md index 62ebb4c3..7af9854f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ -# Radio Astronomical DeconvolvER +# Radio Astronomical Deconvolution Library -RADLER is a library providing functionality for deconvolving astronomical images. RADLER evolved as a stand-alone library from the w-stacking clean (WSClean) imager https://gitlab.com/aroffringa/wsclean. +Radler is a library providing functionality for deconvolving astronomical images. Radler evolved as a stand-alone library from the w-stacking clean (WSClean) imager https://gitlab.com/aroffringa/wsclean. diff --git a/cmake/FindCFITSIO.cmake b/cmake/FindCFITSIO.cmake new file mode 100644 index 00000000..bcdd2143 --- /dev/null +++ b/cmake/FindCFITSIO.cmake @@ -0,0 +1,56 @@ +# * Try to find CFITSIO. Variables used by this module: CFITSIO_ROOT_DIR - +# CFITSIO root directory Variables defined by this module: CFITSIO_FOUND - +# system has CFITSIO CFITSIO_INCLUDE_DIR - the CFITSIO include directory +# (cached) CFITSIO_INCLUDE_DIRS - the CFITSIO include directories (identical +# to CFITSIO_INCLUDE_DIR) CFITSIO_LIBRARY - the CFITSIO library (cached) +# CFITSIO_LIBRARIES - the CFITSIO libraries (identical to CFITSIO_LIBRARY) +# CFITSIO_VERSION_STRING the found version of CFITSIO, padded to 3 digits + +# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +# SPDX-License-Identifier: GPL-3.0-or-later + +if(NOT CFITSIO_FOUND) + + find_path( + CFITSIO_INCLUDE_DIR fitsio.h + HINTS ${CFITSIO_ROOT_DIR} + PATH_SUFFIXES include include/cfitsio include/libcfitsio0) + + if(CFITSIO_INCLUDE_DIR) + file(READ "${CFITSIO_INCLUDE_DIR}/fitsio.h" CFITSIO_H) + set(CFITSIO_VERSION_REGEX + ".*#define CFITSIO_VERSION[^0-9]*([0-9]+)\\.([0-9]+).*") + if("${CFITSIO_H}" MATCHES ${CFITSIO_VERSION_REGEX}) + # Pad CFITSIO minor version to three digit because 3.181 is older than + # 3.35 + string(REGEX REPLACE ${CFITSIO_VERSION_REGEX} "\\1.\\200" + CFITSIO_VERSION_STRING "${CFITSIO_H}") + string(SUBSTRING ${CFITSIO_VERSION_STRING} 0 5 CFITSIO_VERSION_STRING) + string(REGEX REPLACE "^([0-9]+)[.]([0-9]+)" "\\1" CFITSIO_VERSION_MAJOR + ${CFITSIO_VERSION_STRING}) + # CFITSIO_VERSION_MINOR will contain 80 for 3.08, 181 for 3.181 and 200 + # for 3.2 + string(REGEX REPLACE "^([0-9]+)[.]0*([0-9]+)" "\\2" CFITSIO_VERSION_MINOR + ${CFITSIO_VERSION_STRING}) + else() + set(CFITSIO_VERSION_STRING "Unknown") + endif() + endif(CFITSIO_INCLUDE_DIR) + + find_library( + CFITSIO_LIBRARY cfitsio + HINTS ${CFITSIO_ROOT_DIR} + PATH_SUFFIXES lib) + find_library(M_LIBRARY m) + mark_as_advanced(CFITSIO_INCLUDE_DIR CFITSIO_LIBRARY M_LIBRARY) + + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args( + CFITSIO + REQUIRED_VARS CFITSIO_LIBRARY M_LIBRARY CFITSIO_INCLUDE_DIR + VERSION_VAR CFITSIO_VERSION_STRING) + + set(CFITSIO_INCLUDE_DIRS ${CFITSIO_INCLUDE_DIR}) + set(CFITSIO_LIBRARIES ${CFITSIO_LIBRARY} ${M_LIBRARY}) + +endif(NOT CFITSIO_FOUND) diff --git a/cmake/FindCasacore.cmake b/cmake/FindCasacore.cmake new file mode 100644 index 00000000..1a7be36f --- /dev/null +++ b/cmake/FindCasacore.cmake @@ -0,0 +1,277 @@ +# * Try to find Casacore include dirs and libraries Usage: find_package(Casacore +# [REQUIRED] [COMPONENTS components...]) Valid components are: casa, +# coordinates, derivedmscal, fits, images, lattices, meas, measures, mirlib, +# ms, msfits, python, scimath, scimath_f, tables +# +# Note that most components are dependent on other (more basic) components. In +# that case, it suffices to specify the "top-level" components; dependent +# components will be searched for automatically. +# +# The dependency tree can be generated using the script get_casacore_deps.sh. +# For this, you need to have a complete casacore installation, built with shared +# libraries, at your disposal. +# +# The dependencies in this macro were generated against casacore release 1.7.0. +# +# Variables used by this module: CASACORE_ROOT_DIR - Casacore root +# directory. +# +# Variables defined by this module: CASACORE_FOUND - System has +# Casacore, which means that the include dir was found, as well as all libraries +# specified (not cached) CASACORE_INCLUDE_DIR - Casacore include directory +# (cached) CASACORE_INCLUDE_DIRS - Casacore include directories (not cached) +# identical to CASACORE_INCLUDE_DIR CASACORE_LIBRARIES - The Casacore +# libraries (not cached) CASA_${COMPONENT}_LIBRARY - The absolute path of +# Casacore library "component" (cached) HAVE_AIPSPP - True if +# system has Casacore (cached) for backward compatibility with AIPS++ +# HAVE_CASACORE - True if system has Casacore (cached) identical to +# CASACORE_FOUND TAQL_EXECUTABLE - The absolute path of the TaQL +# executable (cached) +# +# ATTENTION: The component names need to be in lower case, just as the casacore +# library names. However, the CMake variables use all upper case. + +# Copyright (C) 2009 ASTRON (Netherlands Institute for Radio Astronomy) P.O.Box +# 2, 7990 AA Dwingeloo, The Netherlands +# +# This file is part of the LOFAR software suite. The LOFAR software suite 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. +# +# The LOFAR software suite 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 +# the LOFAR software suite. If not, see <http://www.gnu.org/licenses/>. +# +# $Id: FindCasacore.cmake 31487 2015-04-16 11:28:17Z dijkema $ + +# * casacore_resolve_dependencies(_result) +# +# Resolve the Casacore library dependencies for the given components. The list +# of dependent libraries will be returned in the variable result. It is sorted +# from least dependent to most dependent library, so it can be directly fed to +# the linker. +# +# Usage: casacore_resolve_dependencies(result components...) +# +macro(casacore_resolve_dependencies _result) + set(${_result} ${ARGN}) + set(_index 0) + # Do a breadth-first search through the dependency graph; append to the result + # list the dependent components for each item in that list. Duplicates will be + # removed later. + while(1) + list(LENGTH ${_result} _length) + if(NOT _index LESS _length) + break() + endif(NOT _index LESS _length) + list(GET ${_result} ${_index} item) + list(APPEND ${_result} ${Casacore_${item}_DEPENDENCIES}) + math(EXPR _index "${_index}+1") + endwhile(1) + # Remove all duplicates in the current result list, while retaining only the + # last of each duplicate. + list(REVERSE ${_result}) + list(REMOVE_DUPLICATES ${_result}) + list(REVERSE ${_result}) +endmacro(casacore_resolve_dependencies _result) + +# * casacore_find_library(_name) +# +# Search for the library ${_name}. If library is found, add it to +# CASACORE_LIBRARIES; if not, add ${_name} to CASACORE_MISSING_COMPONENTS and +# set CASACORE_FOUND to false. +# +# Usage: casacore_find_library(name) +# +macro(casacore_find_library _name) + string(TOUPPER ${_name} _NAME) + find_library( + ${_NAME}_LIBRARY ${_name} + HINTS ${CASACORE_ROOT_DIR} + PATH_SUFFIXES lib) + mark_as_advanced(${_NAME}_LIBRARY) + if(${_NAME}_LIBRARY) + list(APPEND CASACORE_LIBRARIES ${${_NAME}_LIBRARY}) + else(${_NAME}_LIBRARY) + set(CASACORE_FOUND FALSE) + list(APPEND CASACORE_MISSING_COMPONENTS ${_name}) + endif(${_NAME}_LIBRARY) +endmacro(casacore_find_library _name) + +# * casacore_find_package(_name) +# +# Search for the package ${_name}. If the package is found, add the contents of +# ${_name}_INCLUDE_DIRS to CASACORE_INCLUDE_DIRS and ${_name}_LIBRARIES to +# CASACORE_LIBRARIES. +# +# If Casacore itself is required, then, strictly speaking, the packages it +# requires must be present. However, when linking against static libraries they +# may not be needed. One can override the REQUIRED setting by switching +# CASACORE_MAKE_REQUIRED_EXTERNALS_OPTIONAL to ON. Beware that this might cause +# compile and/or link errors. +# +# Usage: casacore_find_package(name [REQUIRED]) +# +macro(casacore_find_package _name) + if("${ARGN}" MATCHES "^REQUIRED$" + AND Casacore_FIND_REQUIRED + AND NOT CASACORE_MAKE_REQUIRED_EXTERNALS_OPTIONAL) + find_package(${_name} REQUIRED) + else() + find_package(${_name}) + endif() + if(${_name}_FOUND) + list(APPEND CASACORE_INCLUDE_DIRS ${${_name}_INCLUDE_DIRS}) + list(APPEND CASACORE_LIBRARIES ${${_name}_LIBRARIES}) + endif(${_name}_FOUND) +endmacro(casacore_find_package _name) + +# Define the Casacore components. +set(Casacore_components + casa + coordinates + derivedmscal + fits + images + lattices + meas + measures + mirlib + ms + msfits + python + scimath + scimath_f + tables) + +# Define the Casacore components' inter-dependencies. +set(Casacore_casa_DEPENDENCIES) +set(Casacore_coordinates_DEPENDENCIES fits measures casa) +set(Casacore_derivedmscal_DEPENDENCIES ms measures tables casa) +set(Casacore_fits_DEPENDENCIES measures tables casa) +set(Casacore_images_DEPENDENCIES + mirlib + lattices + coordinates + fits + measures + scimath + tables + casa) +set(Casacore_lattices_DEPENDENCIES tables scimath casa) +set(Casacore_meas_DEPENDENCIES measures tables casa) +set(Casacore_measures_DEPENDENCIES tables casa) +set(Casacore_mirlib_DEPENDENCIES) +set(Casacore_ms_DEPENDENCIES measures scimath tables casa) +set(Casacore_msfits_DEPENDENCIES ms fits measures tables casa) +set(Casacore_python_DEPENDENCIES casa) +set(Casacore_scimath_DEPENDENCIES scimath_f casa) +set(Casacore_scimath_f_DEPENDENCIES) +set(Casacore_tables_DEPENDENCIES casa) + +# Initialize variables. +set(CASACORE_FOUND FALSE) +set(CASACORE_DEFINITIONS) +set(CASACORE_LIBRARIES) +set(CASACORE_MISSING_COMPONENTS) + +# Search for the header file first. +if(NOT CASACORE_INCLUDE_DIR) + find_path( + CASACORE_INCLUDE_DIR casacore/casa/aips.h + HINTS ${CASACORE_ROOT_DIR} + PATH_SUFFIXES include) + mark_as_advanced(CASACORE_INCLUDE_DIR) +endif(NOT CASACORE_INCLUDE_DIR) + +# Fallback for systems that have old casacore installed in directory not called +# 'casacore' This fallback can be removed once we move to casacore 2.0 which +# always puts headers in 'casacore' +if(NOT CASACORE_INCLUDE_DIR) + find_path( + CASACORE_INCLUDE_DIR casa/aips.h + HINTS ${CASACORE_ROOT_DIR} + PATH_SUFFIXES include) + mark_as_advanced(CASACORE_INCLUDE_DIR) +endif(NOT CASACORE_INCLUDE_DIR) + +if(NOT CASACORE_INCLUDE_DIR) + set(CASACORE_ERROR_MESSAGE + "Casacore: unable to find the header file casa/aips.h.\nPlease set CASACORE_ROOT_DIR to the root directory containing Casacore." + ) +else(NOT CASACORE_INCLUDE_DIR) + # We've found the header file; let's continue. + set(CASACORE_FOUND TRUE) + # Note that new Casacore uses #include<casacore/casa/...>, while LOFAR still + # uses #include<casa/...>. Hence use both in -I path. + set(CASACORE_INCLUDE_DIRS ${CASACORE_INCLUDE_DIR} + ${CASACORE_INCLUDE_DIR}/casacore) + + # Search for some often used binaries. + find_program(TAQL_EXECUTABLE taql HINTS ${CASACORE_ROOT_DIR}/bin) + mark_as_advanced(TAQL_EXECUTABLE) + + # If the user specified components explicity, use that list; otherwise we'll + # assume that the user wants to use all components. + if(NOT Casacore_FIND_COMPONENTS) + set(Casacore_FIND_COMPONENTS ${Casacore_components}) + endif(NOT Casacore_FIND_COMPONENTS) + + # Get a list of all dependent Casacore libraries that need to be found. + casacore_resolve_dependencies(_find_components ${Casacore_FIND_COMPONENTS}) + + # Find the library for each component, and handle external dependencies + foreach(_comp ${_find_components}) + casacore_find_library(casa_${_comp}) + if(${_comp} STREQUAL casa) + casacore_find_package(HDF5) + casacore_find_library(m) + list(APPEND CASACORE_LIBRARIES ${CMAKE_DL_LIBS}) + elseif(${_comp} STREQUAL coordinates) + casacore_find_package(WCSLIB REQUIRED) + elseif(${_comp} STREQUAL fits) + casacore_find_package(CFITSIO REQUIRED) + elseif(${_comp} STREQUAL scimath_f) + casacore_find_package(LAPACK REQUIRED) + endif(${_comp} STREQUAL casa) + endforeach(_comp ${_find_components}) +endif(NOT CASACORE_INCLUDE_DIR) + +# Set HAVE_CASACORE; and HAVE_AIPSPP (for backward compatibility with AIPS++). +if(CASACORE_FOUND) + set(HAVE_CASACORE + TRUE + CACHE INTERNAL "Define if Casacore is installed") + set(HAVE_AIPSPP + TRUE + CACHE INTERNAL "Define if AIPS++/Casacore is installed") +endif(CASACORE_FOUND) + +# Compose diagnostic message if not all necessary components were found. +if(CASACORE_MISSING_COMPONENTS) + set(CASACORE_ERROR_MESSAGE + "Casacore: the following components could not be found:\n ${CASACORE_MISSING_COMPONENTS}" + ) +endif(CASACORE_MISSING_COMPONENTS) + +# Print diagnostics. +if(CASACORE_FOUND) + if(NOT Casacore_FIND_QUIETLY) + message(STATUS "Found the following Casacore components: ") + foreach(_comp ${_find_components}) + string(TOUPPER casa_${_comp} _COMP) + message(STATUS " ${_comp}: ${${_COMP}_LIBRARY}") + endforeach(_comp ${_find_components}) + endif(NOT Casacore_FIND_QUIETLY) +else(CASACORE_FOUND) + if(Casacore_FIND_REQUIRED) + message(FATAL_ERROR "${CASACORE_ERROR_MESSAGE}") + else(Casacore_FIND_REQUIRED) + message(STATUS "${CASACORE_ERROR_MESSAGE}") + endif(Casacore_FIND_REQUIRED) +endif(CASACORE_FOUND) diff --git a/cmake/config/radler-config-version.cmake.in b/cmake/config/radler-config-version.cmake.in new file mode 100644 index 00000000..59cdd0c2 --- /dev/null +++ b/cmake/config/radler-config-version.cmake.in @@ -0,0 +1,14 @@ +# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +# SPDX-License-Identifier: GPL-3.0-or-later + +set(PACKAGE_VERSION "@RADLER_VERSION@") + +# Check whether the requested PACKAGE_FIND_VERSION is compatible +if("${PACKAGE_VERSION}" VERSION_LESS "${PACKAGE_FIND_VERSION}") + set(PACKAGE_VERSION_COMPATIBLE FALSE) +else() + set(PACKAGE_VERSION_COMPATIBLE TRUE) + if ("${PACKAGE_VERSION}" VERSION_EQUAL "${PACKAGE_FIND_VERSION}") + set(PACKAGE_VERSION_EXACT TRUE) + endif() +endif() \ No newline at end of file diff --git a/cmake/config/radler-config.cmake.in b/cmake/config/radler-config.cmake.in new file mode 100644 index 00000000..cce73266 --- /dev/null +++ b/cmake/config/radler-config.cmake.in @@ -0,0 +1,45 @@ +# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +# SPDX-License-Identifier: GPL-3.0-or-later +# +# Config file for the radler library, it sets the following variables +# +# - RADLER_FOUND +# - RADLER_ROOT_DIR +# - RADLER_INCLUDE_DIR +# - RADLER_INCLUDE_DIRS (equals RADLER_INCLUDE_DIR) +# - RADLER_LIB_PATH +# - RADLER_LIB +# - RADLER_VERSION[_MAJOR/_MINOR/_PATCH] + +# Compute paths +get_filename_component(_RADLER_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" PATH) +get_filename_component(_RADLER_CMAKE_DIR_ABS "${_RADLER_CMAKE_DIR}" ABSOLUTE) +get_filename_component(_RADLER_ROOT_DIR "${_RADLER_CMAKE_DIR_ABS}/../.." ABSOLUTE) + +# Use FORCE to override cached variables +set(RADLER_ROOT_DIR "${_RADLER_ROOT_DIR}" + CACHE PATH "Radler root (prefix) directory" FORCE) + +set(RADLER_INCLUDE_DIR "${RADLER_ROOT_DIR}/include" + CACHE PATH "Radler include directory" FORCE) + +set(RADLER_INCLUDE_DIRS ${RADLER_INCLUDE_DIR}) + +set(RADLER_LIB_PATH "${RADLER_ROOT_DIR}/lib" + CACHE PATH "Radler library directory" FORCE) + +find_library(RADLER_LIB radler PATH ${RADLER_LIB_PATH} NO_DEFAULT_PATH + DOC "Radler library directory") +message(STATUS "Found Radler @RADLER_VERSION@.") +message(STATUS " Radler include dir: ${RADLER_INCLUDE_DIR}") +message(STATUS " Radler lib: ${RADLER_LIB}") + +set(RADLER_VERSION "@RADLER_VERSION@") +set(RADLER_VERSION_MAJOR @RADLER_VERSION_MAJOR@) +set(RADLER_VERSION_MINOR @RADLER_VERSION_MINOR@) +set(RADLER_VERSION_PATCH @RADLER_VERSION_PATCH@) +set(RADLER_FOUND 1) + +unset(_RADLER_ROOT_DIR) +unset(_RADLER_CMAKE_DIR) +unset(_RADLER_CMAKE_DIR_ABS) diff --git a/cmake/unittest.cmake b/cmake/unittest.cmake new file mode 100644 index 00000000..ba527eb9 --- /dev/null +++ b/cmake/unittest.cmake @@ -0,0 +1,45 @@ +# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +# SPDX-License-Identifier: GPL-3.0-or-later + +# Generic function for adding a radler unittest. It creates a 'test' that builds +# the unittest and a test that runs it. Arguments: +# +# * First argument: Module name, e.g., 'math'. +# * Next arguments: Source file names. +# +# Return value: +# +# * Sets TEST_NAME to the unit test name in the parent scope. + +function(add_unittest MODULE_NAME) + set(TEST_NAME "unittests_${MODULE_NAME}") + set(TEST_NAME + ${TEST_NAME} + PARENT_SCOPE) + set(FILENAMES ${ARGN}) + + # Add boost dynamic link flag for all test files. + # https://www.boost.org/doc/libs/1_66_0/libs/test/doc/html/boost_test/usage_variants.html + # Without this flag, linking is incorrect and boost performs duplicate + # delete() calls after running all tests, in the cleanup phase. + set_source_files_properties(${FILENAMES} PROPERTIES COMPILE_DEFINITIONS + "BOOST_TEST_DYN_LINK") + + add_executable(${TEST_NAME} ${FILENAMES}) + target_link_libraries(${TEST_NAME} radler + ${Boost_UNIT_TEST_FRAMEWORK_LIBRARY}) + target_include_directories( + ${TEST_NAME} PRIVATE $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/cpp>) + + # Add test for automatically (re)building the test if needed. + add_test(build_${TEST_NAME} ${CMAKE_COMMAND} --build ${CMAKE_BINARY_DIR} + --target ${TEST_NAME}) + set_tests_properties(build_${TEST_NAME} PROPERTIES FIXTURES_SETUP + ${TEST_NAME}) + + add_test(NAME ${TEST_NAME} COMMAND ${TEST_NAME} -f JUNIT -k ${TEST_NAME}.xml + --catch_system_error=yes) + set_tests_properties(${TEST_NAME} PROPERTIES LABELS unit FIXTURES_REQUIRED + ${TEST_NAME}) + +endfunction() diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt new file mode 100644 index 00000000..f9299f58 --- /dev/null +++ b/cpp/CMakeLists.txt @@ -0,0 +1,116 @@ +# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +# SPDX-License-Identifier: GPL-3.0-or-later + +# CMake function to keep directory structure when installing headers. +function(install_headers_with_directory HEADER_LIST) + foreach(HEADER ${HEADER_LIST}) + string(REGEX MATCH ".*\/" DIR ${HEADER}) + install(FILES ${HEADER} + DESTINATION ${CMAKE_INSTALL_PREFIX}/include/radler/${DIR}) + endforeach(HEADER) +endfunction(install_headers_with_directory) + +add_library(${PROJECT_NAME} SHARED "") + +set(RADLER_FILES + radler.cc + deconvolution_table.cc + component_list.cc + image_set.cc + algorithms/deconvolution_algorithm.cc + algorithms/generic_clean.cc + algorithms/iuwt_deconvolution_algorithm.cc + # algorithms/ls_deconvolution.cc // TODO: Complete or remove this class. + algorithms/more_sane.cc + algorithms/multiscale_algorithm.cc + algorithms/parallel_deconvolution.cc + algorithms/python_deconvolution.cc + algorithms/simple_clean.cc + algorithms/subminor_loop.cc + algorithms/threaded_deconvolution_tools.cc + algorithms/iuwt/image_analysis.cc + algorithms/iuwt/iuwt_decomposition.cc + algorithms/iuwt/iuwt_mask.cc + algorithms/multiscale/multiscale_transforms.cc + math/peak_finder.cc + math/rms_image.cc + utils/casa_mask_reader.cc) + +# A number of files perform the 'core' high-performance floating point +# operations. In these files, NaNs are avoided and thus -ffast-math is allowed. +# Note that visibilities can be NaN hence this can not be turned on for all +# files. +set_source_files_properties( + image_set.cpp + algorithms/generic_clean.cpp + algorithms/multiscale_algorithm.cpp + algorithms/threaded_deconvolution_tools.cpp + algorithms/simple_clean.cpp + algorithms/subminor_loop.cpp + algorithms/multiscale/multiscale_transforms.cpp + PROPERTIES COMPILE_FLAGS -ffast-math) + +# Using pybind11 requires using -fvisibility=hidden. See +# https://pybind11.readthedocs.io/en/stable/faq.html +set_source_files_properties(algorithms/python_deconvolution.cpp + PROPERTIES COMPILE_FLAGS -fvisibility=hidden) + +target_sources(${PROJECT_NAME} PRIVATE ${RADLER_FILES}) +target_link_libraries(${PROJECT_NAME} ${RADLER_TARGET_LIBS}) +target_include_directories(${PROJECT_NAME} SYSTEM + PUBLIC ${RADLER_TARGET_INCLUDE_DIRS}) + +# Allows including the paths relative to base algorithm, in line with Google +# style +# https://google.github.io/styleguide/cppguide.html#Names_and_Order_of_Includes +target_include_directories( + ${PROJECT_NAME} PRIVATE $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>) + +target_compile_options(radler PRIVATE -O3 -Wall -Wzero-as-null-pointer-constant) + +if(NOT PORTABLE) + if(DEFINED TARGET_CPU) + target_compile_options(radler BEFORE PRIVATE -march=${TARGET_CPU}) + else() + check_cxx_compiler_flag("-march=native" COMPILER_HAS_MARCH_NATIVE) + if(COMPILER_HAS_MARCH_NATIVE) + target_compile_options(radler BEFORE PRIVATE -march=native) + else() + message( + WARNING "The compiler doesn't support -march=native for your CPU.") + endif() + endif() +endif() + +if(NOT COMPILE_AS_EXTERNAL_PROJECT) + include(GNUInstallDirs) + install(TARGETS ${PROJECT_NAME} + LIBRARY DESTINATION ${CMAKE_INSTALL_FULL_LIBDIR}) +else() + install(TARGETS ${PROJECT_NAME} + LIBRARY DESTINATION ${CMAKE_INSTALL_PREFIX}/lib) +endif() + +set(PUBLIC_HEADERS + component_list.h + radler.h + deconvolution_settings.h + deconvolution_table.h + deconvolution_table_entry.h + image_set.h + algorithms/multiscale/multiscale_transforms.h) + +install_headers_with_directory("${PUBLIC_HEADERS}") + +if(NOT COMPILE_AS_EXTERNAL_PROJECT) + configure_file(${PROJECT_SOURCE_DIR}/cmake/config/radler-config.cmake.in + ${PROJECT_BINARY_DIR}/CMakeFiles/radler-config.cmake @ONLY) + configure_file( + ${PROJECT_SOURCE_DIR}/cmake/config/radler-config-version.cmake.in + ${PROJECT_BINARY_DIR}/CMakeFiles/radler-config-version.cmake @ONLY) + + # Install configuration files + install(FILES ${PROJECT_BINARY_DIR}/CMakeFiles/radler-config.cmake + ${PROJECT_BINARY_DIR}/CMakeFiles/radler-config-version.cmake + DESTINATION ${CMAKE_INSTALL_PREFIX}/share/radler) +endif() diff --git a/cpp/algorithms/deconvolution_algorithm.cc b/cpp/algorithms/deconvolution_algorithm.cc new file mode 100644 index 00000000..6b64b09c --- /dev/null +++ b/cpp/algorithms/deconvolution_algorithm.cc @@ -0,0 +1,51 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "algorithms/deconvolution_algorithm.h" + +#include <aocommon/system.h> + +namespace radler::algorithms { + +DeconvolutionAlgorithm::DeconvolutionAlgorithm() + : _threshold(0.0), + _majorIterThreshold(0.0), + _gain(0.1), + _mGain(1.0), + _cleanBorderRatio(0.05), + _maxIter(500), + _iterationNumber(0), + _threadCount(aocommon::system::ProcessorCount()), + _allowNegativeComponents(true), + _stopOnNegativeComponent(false), + _cleanMask(nullptr), + _logReceiver(nullptr), + _spectralFitter(schaapcommon::fitters::SpectralFittingMode::NoFitting, + 0) {} + +void DeconvolutionAlgorithm::ResizeImage(float* dest, size_t newWidth, + size_t newHeight, const float* source, + size_t width, size_t height) { + size_t srcStartX = (width - newWidth) / 2, + srcStartY = (height - newHeight) / 2; + for (size_t y = 0; y != newHeight; ++y) { + float* destPtr = dest + y * newWidth; + const float* srcPtr = source + (y + srcStartY) * width + srcStartX; + std::copy_n(srcPtr, newWidth, destPtr); + } +} + +void DeconvolutionAlgorithm::RemoveNaNsInPSF(float* psf, size_t width, + size_t height) { + float* endPtr = psf + width * height; + while (psf != endPtr) { + if (!std::isfinite(*psf)) *psf = 0.0; + ++psf; + } +} + +void DeconvolutionAlgorithm::PerformSpectralFit(float* values, size_t x, + size_t y) const { + _spectralFitter.FitAndEvaluate(values, x, y, _fittingScratch); +} +} // namespace radler::algorithms \ No newline at end of file diff --git a/cpp/algorithms/deconvolution_algorithm.h b/cpp/algorithms/deconvolution_algorithm.h new file mode 100644 index 00000000..a6d7c83a --- /dev/null +++ b/cpp/algorithms/deconvolution_algorithm.h @@ -0,0 +1,141 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef RADLER_ALGORITHMS_DECONVOLUTION_ALGORITHM_H_ +#define RADLER_ALGORITHMS_DECONVOLUTION_ALGORITHM_H_ + +#include <string> +#include <cmath> + +#include <aocommon/image.h> +#include <aocommon/logger.h> +#include <aocommon/polarization.h> +#include <aocommon/uvector.h> + +#include <schaapcommon/fitters/spectralfitter.h> + +#include "image_set.h" + +namespace radler::algorithms { + +class DeconvolutionAlgorithm { + public: + virtual ~DeconvolutionAlgorithm() {} + + virtual float ExecuteMajorIteration( + ImageSet& dataImage, ImageSet& modelImage, + const std::vector<aocommon::Image>& psfImages, + bool& reachedMajorThreshold) = 0; + + virtual std::unique_ptr<DeconvolutionAlgorithm> Clone() const = 0; + + void SetMaxNIter(size_t nIter) { _maxIter = nIter; } + + void SetThreshold(float threshold) { _threshold = threshold; } + + void SetMajorIterThreshold(float mThreshold) { + _majorIterThreshold = mThreshold; + } + + void SetGain(float gain) { _gain = gain; } + + void SetMGain(float mGain) { _mGain = mGain; } + + void SetAllowNegativeComponents(bool allowNegativeComponents) { + _allowNegativeComponents = allowNegativeComponents; + } + + void SetStopOnNegativeComponents(bool stopOnNegative) { + _stopOnNegativeComponent = stopOnNegative; + } + + void SetCleanBorderRatio(float borderRatio) { + _cleanBorderRatio = borderRatio; + } + + void SetThreadCount(size_t threadCount) { _threadCount = threadCount; } + + void SetLogReceiver(aocommon::LogReceiver& receiver) { + _logReceiver = &receiver; + } + + size_t MaxNIter() const { return _maxIter; } + float Threshold() const { return _threshold; } + float MajorIterThreshold() const { return _majorIterThreshold; } + float Gain() const { return _gain; } + float MGain() const { return _mGain; } + float CleanBorderRatio() const { return _cleanBorderRatio; } + bool AllowNegativeComponents() const { return _allowNegativeComponents; } + bool StopOnNegativeComponents() const { return _stopOnNegativeComponent; } + + void SetCleanMask(const bool* cleanMask) { _cleanMask = cleanMask; } + + size_t IterationNumber() const { return _iterationNumber; } + + void SetIterationNumber(size_t iterationNumber) { + _iterationNumber = iterationNumber; + } + + static void ResizeImage(float* dest, size_t newWidth, size_t newHeight, + const float* source, size_t width, size_t height); + + static void RemoveNaNsInPSF(float* psf, size_t width, size_t height); + + void CopyConfigFrom(const DeconvolutionAlgorithm& source) { + _threshold = source._threshold; + _gain = source._gain; + _mGain = source._mGain; + _cleanBorderRatio = source._cleanBorderRatio; + _maxIter = source._maxIter; + // skip _iterationNumber + _allowNegativeComponents = source._allowNegativeComponents; + _stopOnNegativeComponent = source._stopOnNegativeComponent; + _cleanMask = source._cleanMask; + _spectralFitter = source._spectralFitter; + } + + void SetSpectralFittingMode(schaapcommon::fitters::SpectralFittingMode mode, + size_t nTerms) { + _spectralFitter.SetMode(mode, nTerms); + } + + void SetSpectrallyForcedImages(std::vector<aocommon::Image>&& images) { + _spectralFitter.SetForcedImages(std::move(images)); + } + + void InitializeFrequencies(const aocommon::UVector<double>& frequencies, + const aocommon::UVector<float>& weights) { + _spectralFitter.SetFrequencies(frequencies.data(), weights.data(), + frequencies.size()); + } + + const schaapcommon::fitters::SpectralFitter& Fitter() const { + return _spectralFitter; + } + + void SetRMSFactorImage(aocommon::Image&& image) { + _rmsFactorImage = std::move(image); + } + const aocommon::Image& RMSFactorImage() const { return _rmsFactorImage; } + + protected: + DeconvolutionAlgorithm(); + + DeconvolutionAlgorithm(const DeconvolutionAlgorithm&) = default; + DeconvolutionAlgorithm& operator=(const DeconvolutionAlgorithm&) = default; + + void PerformSpectralFit(float* values, size_t x, size_t y) const; + + float _threshold, _majorIterThreshold, _gain, _mGain, _cleanBorderRatio; + size_t _maxIter, _iterationNumber, _threadCount; + bool _allowNegativeComponents, _stopOnNegativeComponent; + const bool* _cleanMask; + aocommon::Image _rmsFactorImage; + mutable std::vector<float> _fittingScratch; + + aocommon::LogReceiver* _logReceiver; + + schaapcommon::fitters::SpectralFitter _spectralFitter; +}; +} // namespace radler::algorithms +#endif // RADLER_ALGORITHMS_DECONVOLUTION_ALGORITHM_H_ diff --git a/cpp/algorithms/generic_clean.cc b/cpp/algorithms/generic_clean.cc new file mode 100644 index 00000000..33bbbf56 --- /dev/null +++ b/cpp/algorithms/generic_clean.cc @@ -0,0 +1,208 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "algorithms/generic_clean.h" + +#include <aocommon/image.h> +#include <aocommon/lane.h> +#include <aocommon/units/fluxdensity.h> + +#include "algorithms/subminor_loop.h" +#include "algorithms/threaded_deconvolution_tools.h" +#include "math/peak_finder.h" + +using aocommon::units::FluxDensity; + +namespace radler::algorithms { +namespace { +std::string peakDescription(const aocommon::Image& image, size_t x, size_t y) { + std::ostringstream str; + const size_t index = x + y * image.Width(); + const float peak = image[index]; + str << FluxDensity::ToNiceString(peak) << " at " << x << "," << y; + return str.str(); +} +} // namespace + +GenericClean::GenericClean(bool useSubMinorOptimization) + : _convolutionPadding(1.1), + _useSubMinorOptimization(useSubMinorOptimization) {} + +float GenericClean::ExecuteMajorIteration( + ImageSet& dirtySet, ImageSet& modelSet, + const std::vector<aocommon::Image>& psfs, bool& reachedMajorThreshold) { + const size_t width = dirtySet.Width(); + const size_t height = dirtySet.Height(); + const size_t iterationCounterAtStart = _iterationNumber; + if (_stopOnNegativeComponent) _allowNegativeComponents = true; + _convolutionWidth = ceil(_convolutionPadding * width); + _convolutionHeight = ceil(_convolutionPadding * height); + if (_convolutionWidth % 2 != 0) ++_convolutionWidth; + if (_convolutionHeight % 2 != 0) ++_convolutionHeight; + + aocommon::Image integrated(width, height); + aocommon::Image scratchA(_convolutionWidth, _convolutionHeight); + aocommon::Image scratchB(_convolutionWidth, _convolutionHeight); + dirtySet.GetLinearIntegrated(integrated); + size_t componentX = 0; + size_t componentY = 0; + std::optional<float> maxValue = + findPeak(integrated, scratchA.Data(), componentX, componentY); + if (!maxValue) { + _logReceiver->Info << "No peak found.\n"; + reachedMajorThreshold = false; + return 0.0; + } + _logReceiver->Info << "Initial peak: " + << peakDescription(integrated, componentX, componentY) + << '\n'; + float firstThreshold = this->_threshold; + float majorIterThreshold = std::max<float>( + MajorIterThreshold(), std::fabs(*maxValue) * (1.0 - this->_mGain)); + if (majorIterThreshold > firstThreshold) { + firstThreshold = majorIterThreshold; + _logReceiver->Info << "Next major iteration at: " + << FluxDensity::ToNiceString(majorIterThreshold) << '\n'; + } else if (this->_mGain != 1.0) { + _logReceiver->Info + << "Major iteration threshold reached global threshold of " + << FluxDensity::ToNiceString(this->_threshold) + << ": final major iteration.\n"; + } + + if (_useSubMinorOptimization) { + size_t startIteration = _iterationNumber; + SubMinorLoop subMinorLoop(width, height, _convolutionWidth, + _convolutionHeight, *_logReceiver); + subMinorLoop.SetIterationInfo(_iterationNumber, MaxNIter()); + subMinorLoop.SetThreshold(firstThreshold, firstThreshold * 0.99); + subMinorLoop.SetGain(Gain()); + subMinorLoop.SetAllowNegativeComponents(AllowNegativeComponents()); + subMinorLoop.SetStopOnNegativeComponent(StopOnNegativeComponents()); + subMinorLoop.SetSpectralFitter(&Fitter()); + if (!_rmsFactorImage.Empty()) + subMinorLoop.SetRMSFactorImage(_rmsFactorImage); + if (_cleanMask) subMinorLoop.SetMask(_cleanMask); + const size_t horBorderSize = std::round(width * CleanBorderRatio()); + const size_t vertBorderSize = std::round(height * CleanBorderRatio()); + subMinorLoop.SetCleanBorders(horBorderSize, vertBorderSize); + subMinorLoop.SetThreadCount(_threadCount); + + maxValue = subMinorLoop.Run(dirtySet, psfs); + + _iterationNumber = subMinorLoop.CurrentIteration(); + + _logReceiver->Info + << "Performed " << _iterationNumber << " iterations in total, " + << (_iterationNumber - startIteration) + << " in this major iteration with sub-minor optimization.\n"; + + for (size_t imageIndex = 0; imageIndex != dirtySet.size(); ++imageIndex) { + // TODO this can be multi-threaded if each thread has its own temporaries + const aocommon::Image& psf = psfs[dirtySet.PSFIndex(imageIndex)]; + subMinorLoop.CorrectResidualDirty(scratchA.Data(), scratchB.Data(), + integrated.Data(), imageIndex, + dirtySet.Data(imageIndex), psf.Data()); + + subMinorLoop.GetFullIndividualModel(imageIndex, scratchA.Data()); + float* model = modelSet.Data(imageIndex); + for (size_t i = 0; i != width * height; ++i) + model[i] += scratchA.Data()[i]; + } + } else { + ThreadedDeconvolutionTools tools(_threadCount); + size_t peakIndex = componentX + componentY * width; + + aocommon::UVector<float> peakValues(dirtySet.size()); + + while (maxValue && fabs(*maxValue) > firstThreshold && + this->_iterationNumber < this->_maxIter && + !(maxValue < 0.0f && this->_stopOnNegativeComponent)) { + if (this->_iterationNumber <= 10 || + (this->_iterationNumber <= 100 && this->_iterationNumber % 10 == 0) || + (this->_iterationNumber <= 1000 && + this->_iterationNumber % 100 == 0) || + this->_iterationNumber % 1000 == 0) + _logReceiver->Info << "Iteration " << this->_iterationNumber << ": " + << peakDescription(integrated, componentX, + componentY) + << '\n'; + + for (size_t i = 0; i != dirtySet.size(); ++i) + peakValues[i] = dirtySet[i][peakIndex]; + + PerformSpectralFit(peakValues.data(), componentX, componentY); + + for (size_t i = 0; i != dirtySet.size(); ++i) { + peakValues[i] *= this->_gain; + modelSet.Data(i)[peakIndex] += peakValues[i]; + + size_t psfIndex = dirtySet.PSFIndex(i); + + tools.SubtractImage(dirtySet.Data(i), psfs[psfIndex], componentX, + componentY, peakValues[i]); + } + + dirtySet.GetSquareIntegrated(integrated, scratchA); + maxValue = findPeak(integrated, scratchA.Data(), componentX, componentY); + + peakIndex = componentX + componentY * width; + + ++this->_iterationNumber; + } + } + if (maxValue) { + _logReceiver->Info << "Stopped on peak " + << FluxDensity::ToNiceString(*maxValue) << ", because "; + bool maxIterReached = _iterationNumber >= MaxNIter(), + finalThresholdReached = + std::fabs(*maxValue) <= _threshold || maxValue == 0.0f, + negativeReached = maxValue < 0.0f && this->_stopOnNegativeComponent, + mgainReached = std::fabs(*maxValue) <= majorIterThreshold, + didWork = (_iterationNumber - iterationCounterAtStart) != 0; + + if (maxIterReached) + _logReceiver->Info << "maximum number of iterations was reached.\n"; + else if (finalThresholdReached) + _logReceiver->Info << "the threshold was reached.\n"; + else if (negativeReached) + _logReceiver->Info << "a negative component was found.\n"; + else if (!didWork) + _logReceiver->Info << "no iterations could be performed.\n"; + else + _logReceiver->Info << "the minor-loop threshold was reached. Continuing " + "cleaning after inversion/prediction round.\n"; + reachedMajorThreshold = + mgainReached && didWork && !negativeReached && !finalThresholdReached; + return *maxValue; + } else { + _logReceiver->Info << "Deconvolution aborted.\n"; + reachedMajorThreshold = false; + return 0.0; + } +} + +std::optional<float> GenericClean::findPeak(const aocommon::Image& image, + float* scratch_buffer, size_t& x, + size_t& y) { + const float* actual_image = image.Data(); + if (!_rmsFactorImage.Empty()) { + std::copy_n(image.Data(), image.Size(), scratch_buffer); + for (size_t i = 0; i != image.Size(); ++i) { + scratch_buffer[i] *= _rmsFactorImage[i]; + } + actual_image = scratch_buffer; + } + + if (_cleanMask == nullptr) { + return math::PeakFinder::Find(actual_image, image.Width(), image.Height(), + x, y, _allowNegativeComponents, 0, + image.Height(), _cleanBorderRatio); + } else { + return math::PeakFinder::FindWithMask( + actual_image, image.Width(), image.Height(), x, y, + _allowNegativeComponents, 0, image.Height(), _cleanMask, + _cleanBorderRatio); + } +} +} // namespace radler::algorithms \ No newline at end of file diff --git a/cpp/algorithms/generic_clean.h b/cpp/algorithms/generic_clean.h new file mode 100644 index 00000000..f2a1aad8 --- /dev/null +++ b/cpp/algorithms/generic_clean.h @@ -0,0 +1,46 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef RADLER_GENERIC_CLEAN_H_ +#define RADLER_GENERIC_CLEAN_H_ + +#include <optional> + +#include <aocommon/uvector.h> + +#include "image_set.h" +#include "algorithms/deconvolution_algorithm.h" +#include "algorithms/simple_clean.h" + +namespace radler::algorithms { +/** + * This class implements a generalized version of Högbom clean. It performs a + * single-channel or joined cleaning, depending on the number of images + * provided. It can use a Clark-like optimization to speed up the cleaning. When + * multiple frequencies are provided, it can perform spectral fitting. + */ +class GenericClean : public DeconvolutionAlgorithm { + public: + explicit GenericClean(bool useSubMinorOptimization); + + float ExecuteMajorIteration(ImageSet& dirtySet, ImageSet& modelSet, + const std::vector<aocommon::Image>& psfs, + bool& reachedMajorThreshold) final override; + + virtual std::unique_ptr<DeconvolutionAlgorithm> Clone() const final override { + return std::unique_ptr<DeconvolutionAlgorithm>(new GenericClean(*this)); + } + + private: + size_t _convolutionWidth; + size_t _convolutionHeight; + const float _convolutionPadding; + bool _useSubMinorOptimization; + + // Scratch buffer should at least accomodate space for image.Size() floats + // and is only used to avoid unnecessary memory allocations. + std::optional<float> findPeak(const aocommon::Image& image, + float* scratch_buffer, size_t& x, size_t& y); +}; +} // namespace radler::algorithms +#endif diff --git a/cpp/algorithms/iuwt/image_analysis.cc b/cpp/algorithms/iuwt/image_analysis.cc new file mode 100644 index 00000000..c07c1a80 --- /dev/null +++ b/cpp/algorithms/iuwt/image_analysis.cc @@ -0,0 +1,328 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "algorithms/iuwt/image_analysis.h" + +#include <stack> + +namespace radler::algorithms::iuwt { + +bool ImageAnalysis::IsHighestOnScale0(const IUWTDecomposition& iuwt, + IUWTMask& markedMask, size_t& x, + size_t& y, size_t endScale, + float& highestScale0) { + const size_t width = iuwt.Width(), height = iuwt.Height(); + Component component(x, y, 0); + std::stack<Component> todo; + todo.push(component); + markedMask[0][x + y * width] = false; + highestScale0 = iuwt[0][x + y * width]; + float highestOtherScales = 0.0; + while (!todo.empty()) { + Component c = todo.top(); + todo.pop(); + size_t index = c.x + c.y * width; + if (c.scale == 0) { + if (exceedsThreshold(iuwt[0][index], highestScale0)) { + highestScale0 = iuwt[0][index]; + x = c.x; + y = c.y; + } + } else { + if (exceedsThreshold(iuwt[c.scale][index], highestOtherScales)) + highestOtherScales = iuwt[c.scale][index]; + } + if (c.x > 0) { + if (markedMask[c.scale][index - 1]) { + markedMask[c.scale][index - 1] = false; + todo.push(Component(c.x - 1, c.y, c.scale)); + } + } + if (c.x < width - 1) { + if (markedMask[c.scale][index + 1]) { + markedMask[c.scale][index + 1] = false; + todo.push(Component(c.x + 1, c.y, c.scale)); + } + } + if (c.y > 0) { + if (markedMask[c.scale][index - width]) { + markedMask[c.scale][index - width] = false; + todo.push(Component(c.x, c.y - 1, c.scale)); + } + } + if (c.y < height - 1) { + if (markedMask[c.scale][index + width]) { + markedMask[c.scale][index + width] = false; + todo.push(Component(c.x, c.y + 1, c.scale)); + } + } + if (c.scale > int(0)) { + if (markedMask[c.scale - 1][index]) { + markedMask[c.scale - 1][index] = false; + todo.push(Component(c.x, c.y, c.scale - 1)); + } + } + if (c.scale < int(endScale) - 1) { + if (markedMask[c.scale + 1][index]) { + markedMask[c.scale + 1][index] = false; + todo.push(Component(c.x, c.y, c.scale + 1)); + } + } + } + return std::fabs(highestScale0) > std::fabs(highestOtherScales); +} + +void ImageAnalysis::Floodfill(const IUWTDecomposition& iuwt, IUWTMask& mask, + const aocommon::UVector<float>& thresholds, + size_t minScale, size_t endScale, + const Component& component, float cleanBorder, + size_t& areaSize) { + const size_t width = iuwt.Width(), height = iuwt.Height(); + size_t xBorder = cleanBorder * width; + size_t yBorder = cleanBorder * height; + size_t minX = xBorder, maxX = width - xBorder; + size_t minY = yBorder, maxY = height - yBorder; + + areaSize = 0; + endScale = std::min<size_t>(endScale, iuwt.NScales()); + std::stack<Component> todo; + todo.push(component); + mask[component.scale][component.x + component.y * width] = true; + while (!todo.empty()) { + Component c = todo.top(); + ++areaSize; + todo.pop(); + size_t index = c.x + c.y * width; + if (c.x > minX) { + if (exceedsThreshold(iuwt[c.scale][index - 1], thresholds[c.scale]) && + !mask[c.scale][index - 1]) { + mask[c.scale][index - 1] = true; + todo.push(Component(c.x - 1, c.y, c.scale)); + } + } + if (c.x < maxX - 1) { + if (exceedsThreshold(iuwt[c.scale][index + 1], thresholds[c.scale]) && + !mask[c.scale][index + 1]) { + mask[c.scale][index + 1] = true; + todo.push(Component(c.x + 1, c.y, c.scale)); + } + } + if (c.y > minY) { + if (exceedsThreshold(iuwt[c.scale][index - width], thresholds[c.scale]) && + !mask[c.scale][index - width]) { + mask[c.scale][index - width] = true; + todo.push(Component(c.x, c.y - 1, c.scale)); + } + } + if (c.y < maxY - 1) { + if (exceedsThreshold(iuwt[c.scale][index + width], thresholds[c.scale]) && + !mask[c.scale][index + width]) { + mask[c.scale][index + width] = true; + todo.push(Component(c.x, c.y + 1, c.scale)); + } + } + if (c.scale > int(minScale)) { + if (exceedsThreshold(iuwt[c.scale - 1][index], thresholds[c.scale - 1]) && + !mask[c.scale - 1][index]) { + mask[c.scale - 1][index] = true; + todo.push(Component(c.x, c.y, c.scale - 1)); + } + } + if (c.scale < int(endScale) - 1) { + if (exceedsThreshold(iuwt[c.scale + 1][index], thresholds[c.scale + 1]) && + !mask[c.scale + 1][index]) { + mask[c.scale + 1][index] = true; + todo.push(Component(c.x, c.y, c.scale + 1)); + } + } + } +} + +void ImageAnalysis::MaskedFloodfill(const IUWTDecomposition& iuwt, + IUWTMask& mask, + const aocommon::UVector<float>& thresholds, + size_t minScale, size_t endScale, + const Component& component, + float cleanBorder, const bool* priorMask, + size_t& areaSize) { + const size_t width = iuwt.Width(), height = iuwt.Height(); + size_t xBorder = cleanBorder * width; + size_t yBorder = cleanBorder * height; + size_t minX = xBorder, maxX = width - xBorder; + size_t minY = yBorder, maxY = height - yBorder; + + areaSize = 0; + endScale = std::min<size_t>(endScale, iuwt.NScales()); + std::stack<Component> todo; + todo.push(component); + mask[component.scale][component.x + component.y * width] = true; + while (!todo.empty()) { + Component c = todo.top(); + ++areaSize; + todo.pop(); + size_t index = c.x + c.y * width; + if (c.x > minX) { + if (exceedsThreshold(iuwt[c.scale][index - 1], thresholds[c.scale]) && + !mask[c.scale][index - 1] && priorMask[index - 1]) { + mask[c.scale][index - 1] = true; + todo.push(Component(c.x - 1, c.y, c.scale)); + } + } + if (c.x < maxX - 1) { + if (exceedsThreshold(iuwt[c.scale][index + 1], thresholds[c.scale]) && + !mask[c.scale][index + 1] && priorMask[index + 1]) { + mask[c.scale][index + 1] = true; + todo.push(Component(c.x + 1, c.y, c.scale)); + } + } + if (c.y > minY) { + if (exceedsThreshold(iuwt[c.scale][index - width], thresholds[c.scale]) && + !mask[c.scale][index - width] && priorMask[index - width]) { + mask[c.scale][index - width] = true; + todo.push(Component(c.x, c.y - 1, c.scale)); + } + } + if (c.y < maxY - 1) { + if (exceedsThreshold(iuwt[c.scale][index + width], thresholds[c.scale]) && + !mask[c.scale][index + width] && priorMask[index + width]) { + mask[c.scale][index + width] = true; + todo.push(Component(c.x, c.y + 1, c.scale)); + } + } + if (c.scale > int(minScale)) { + if (exceedsThreshold(iuwt[c.scale - 1][index], thresholds[c.scale - 1]) && + !mask[c.scale - 1][index] && priorMask[index]) { + mask[c.scale - 1][index] = true; + todo.push(Component(c.x, c.y, c.scale - 1)); + } + } + if (c.scale < int(endScale) - 1) { + if (exceedsThreshold(iuwt[c.scale + 1][index], thresholds[c.scale + 1]) && + !mask[c.scale + 1][index] && priorMask[index]) { + mask[c.scale + 1][index] = true; + todo.push(Component(c.x, c.y, c.scale + 1)); + } + } + } +} + +void ImageAnalysis::SelectStructures(const IUWTDecomposition& iuwt, + IUWTMask& mask, + const aocommon::UVector<float>& thresholds, + size_t minScale, size_t endScale, + float cleanBorder, const bool* priorMask, + size_t& areaSize) { + const size_t width = iuwt.Width(), height = iuwt.Height(); + const size_t xBorder = cleanBorder * width, yBorder = cleanBorder * height, + minX = xBorder, maxX = width - xBorder, minY = yBorder, + maxY = height - yBorder; + + areaSize = 0; + + for (size_t scale = minScale; scale != endScale; ++scale) { + for (size_t y = minY; y != maxY; ++y) { + for (size_t x = minX; x != maxX; ++x) { + size_t index = x + y * width; + bool isInPriorMask = (priorMask == nullptr) || priorMask[index]; + if (exceedsThreshold(iuwt[scale][index], thresholds[scale]) && + !mask[scale][index] && isInPriorMask) { + Component component(x, y, scale); + size_t subAreaSize = 0; + if (priorMask == nullptr) + Floodfill(iuwt, mask, thresholds, minScale, endScale, component, + cleanBorder, subAreaSize); + else + MaskedFloodfill(iuwt, mask, thresholds, minScale, endScale, + component, cleanBorder, priorMask, subAreaSize); + areaSize += subAreaSize; + } + } + } + } +} + +void ImageAnalysis::FloodFill2D(const float* image, bool* mask, float threshold, + const ImageAnalysis::Component2D& component, + size_t width, size_t height, size_t& areaSize) { + areaSize = 0; + std::stack<Component2D> todo; + todo.push(component); + mask[component.x + component.y * width] = true; + while (!todo.empty()) { + Component2D c = todo.top(); + ++areaSize; + todo.pop(); + size_t index = c.x + c.y * width; + if (c.x > 0) { + if (exceedsThreshold(image[index - 1], threshold) && !mask[index - 1]) { + mask[index - 1] = true; + todo.push(Component2D(c.x - 1, c.y)); + } + } + if (c.x < width - 1) { + if (exceedsThreshold(image[index + 1], threshold) && !mask[index + 1]) { + mask[index + 1] = true; + todo.push(Component2D(c.x + 1, c.y)); + } + } + if (c.y > 0) { + if (exceedsThreshold(image[index - width], threshold) && + !mask[index - width]) { + mask[index - width] = true; + todo.push(Component2D(c.x, c.y - 1)); + } + } + if (c.y < height - 1) { + if (exceedsThreshold(image[index + width], threshold) && + !mask[index + width]) { + mask[index + width] = true; + todo.push(Component2D(c.x, c.y + 1)); + } + } + } +} + +void ImageAnalysis::FloodFill2D(const float* image, bool* mask, float threshold, + const ImageAnalysis::Component2D& component, + size_t width, size_t height, + std::vector<Component2D>& area) { + area.clear(); + std::stack<Component2D> todo; + todo.push(component); + mask[component.x + component.y * width] = true; + while (!todo.empty()) { + Component2D c = todo.top(); + area.push_back(c); + todo.pop(); + size_t index = c.x + c.y * width; + if (c.x > 0) { + if (exceedsThresholdAbs(image[index - 1], threshold) && + !mask[index - 1]) { + mask[index - 1] = true; + todo.push(Component2D(c.x - 1, c.y)); + } + } + if (c.x < width - 1) { + if (exceedsThresholdAbs(image[index + 1], threshold) && + !mask[index + 1]) { + mask[index + 1] = true; + todo.push(Component2D(c.x + 1, c.y)); + } + } + if (c.y > 0) { + if (exceedsThresholdAbs(image[index - width], threshold) && + !mask[index - width]) { + mask[index - width] = true; + todo.push(Component2D(c.x, c.y - 1)); + } + } + if (c.y < height - 1) { + if (exceedsThresholdAbs(image[index + width], threshold) && + !mask[index + width]) { + mask[index + width] = true; + todo.push(Component2D(c.x, c.y + 1)); + } + } + } +} +} // namespace radler::algorithms::iuwt \ No newline at end of file diff --git a/cpp/algorithms/iuwt/image_analysis.h b/cpp/algorithms/iuwt/image_analysis.h new file mode 100644 index 00000000..283306aa --- /dev/null +++ b/cpp/algorithms/iuwt/image_analysis.h @@ -0,0 +1,84 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef RADLER_ALGORITHMS_IUWT_IMAGE_ANALYSIS_H_ +#define RADLER_ALGORITHMS_IUWT_IMAGE_ANALYSIS_H_ + +#include "algorithms/iuwt/iuwt_decomposition.h" + +namespace radler::algorithms::iuwt { + +class ImageAnalysis { + public: + struct Component { + Component(size_t _x, size_t _y, int _scale) : x(_x), y(_y), scale(_scale) {} + + std::string ToString() const { + std::ostringstream str; + str << x << ',' << y << ", scale " << scale; + return str.str(); + } + + size_t x, y; + int scale; + }; + + struct Component2D { + Component2D(size_t _x, size_t _y) : x(_x), y(_y) {} + + std::string ToString() const { + std::ostringstream str; + str << x << ',' << y; + return str.str(); + } + + size_t x, y; + }; + + static void SelectStructures(const IUWTDecomposition& iuwt, IUWTMask& mask, + const aocommon::UVector<float>& thresholds, + size_t minScale, size_t endScale, + float cleanBorder, const bool* priorMask, + size_t& areaSize); + + static bool IsHighestOnScale0(const IUWTDecomposition& iuwt, + IUWTMask& markedMask, size_t& x, size_t& y, + size_t endScale, float& highestScale0); + + static void Floodfill(const IUWTDecomposition& iuwt, IUWTMask& mask, + const aocommon::UVector<float>& thresholds, + size_t minScale, size_t endScale, + const Component& component, float cleanBorder, + size_t& areaSize); + + static void MaskedFloodfill(const IUWTDecomposition& iuwt, IUWTMask& mask, + const aocommon::UVector<float>& thresholds, + size_t minScale, size_t endScale, + const Component& component, float cleanBorder, + const bool* priorMask, size_t& areaSize); + + static void FloodFill2D(const float* image, bool* mask, float threshold, + const Component2D& component, size_t width, + size_t height, size_t& areaSize); + + /** + * Exactly like above, but now collecting the components in the + * area vector, instead of returning just the area size. + */ + static void FloodFill2D(const float* image, bool* mask, float threshold, + const Component2D& component, size_t width, + size_t height, std::vector<Component2D>& area); + + private: + static bool exceedsThreshold(float val, float threshold) { + if (threshold >= 0.0) + return val > threshold; + else + return val < threshold || val > -threshold; + } + static bool exceedsThresholdAbs(float val, float threshold) { + return std::fabs(val) > threshold; + } +}; +} // namespace radler::algorithms::iuwt +#endif // RADLER_ALGORITHMS_IUWT_IMAGE_ANALYSIS_H_ diff --git a/cpp/algorithms/iuwt/iuwt_decomposition.cc b/cpp/algorithms/iuwt/iuwt_decomposition.cc new file mode 100644 index 00000000..a250f94e --- /dev/null +++ b/cpp/algorithms/iuwt/iuwt_decomposition.cc @@ -0,0 +1,240 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "algorithms/iuwt/iuwt_decomposition.h" + +using aocommon::Image; + +namespace radler::algorithms::iuwt { + +void IUWTDecomposition::DecomposeMT(aocommon::StaticFor<size_t>& loop, + const float* input, float* scratch, + bool includeLargest) { + Image& i1(_scales.back().Coefficients()); + i1 = Image(_width, _height); + + // The first iteration of the loop, unrolled, so that we don't have to + // copy the input into i0. + Image& coefficients0 = _scales[0].Coefficients(); + coefficients0 = Image(_width, _height); + convolveMT(loop, i1.Data(), input, scratch, _width, _height, 1); + convolveMT(loop, coefficients0.Data(), i1.Data(), scratch, _width, _height, + 1); + + // coefficients = i0 - i2 + differenceMT(loop, coefficients0.Data(), input, coefficients0.Data(), _width, + _height); + + // i0 = i1; + Image i0(i1); + + for (int scale = 1; scale != int(_scaleCount); ++scale) { + Image& coefficients = _scales[scale].Coefficients(); + coefficients = Image(_width, _height); + convolveMT(loop, i1.Data(), i0.Data(), scratch, _width, _height, scale + 1); + convolveMT(loop, coefficients.Data(), i1.Data(), scratch, _width, _height, + scale + 1); + + // coefficients = i0 - i2 + differenceMT(loop, coefficients.Data(), i0.Data(), coefficients.Data(), + _width, _height); + + // i0 = i1; + if (scale + 1 != int(_scaleCount)) { + std::copy_n(i1.Data(), _width * _height, i0.Data()); + } + } + + // The largest (residual) scales are in i1, but since the + // largest scale is aliased to i1, it's already stored there. + // Hence we can skip this: + // if(includeLargest) + // _scales.back().Coefficients() = i1; + + // Do free the memory of the largest scale if it is not necessary: + if (!includeLargest) _scales.back().Coefficients().Reset(); +} + +void IUWTDecomposition::convolveMT(aocommon::StaticFor<size_t>& loop, + float* output, const float* image, + float* scratch, size_t width, size_t height, + int scale) { + loop.Run(0, height, [&](size_t y_start, size_t y_end) { + convolveHorizontalPartial(scratch, image, width, y_start, y_end, scale); + }); + + loop.Run(0, width, [&](size_t x_start, size_t x_end) { + convolveVerticalPartialFast(output, scratch, width, height, x_start, x_end, + scale); + }); +} + +void IUWTDecomposition::differenceMT(aocommon::StaticFor<size_t>& loop, + float* dest, const float* lhs, + const float* rhs, size_t width, + size_t height) { + loop.Run(0, height, [&](size_t y_start, size_t y_end) { + differencePartial(dest, lhs, rhs, width, y_start, y_end); + }); +} + +void IUWTDecomposition::convolveHorizontalFast(float* output, + const float* image, size_t width, + size_t height, int scale) { + const size_t H_SIZE = 5; + const float h[H_SIZE] = {1.0 / 16.0, 4.0 / 16.0, 6.0 / 16.0, 4.0 / 16.0, + 1.0 / 16.0}; + int scaleDist = (1 << scale); + int dist[H_SIZE]; + size_t minX[H_SIZE], maxX[H_SIZE]; + for (int hIndex = 0; hIndex != H_SIZE; ++hIndex) { + int hShift = hIndex - H_SIZE / 2; + dist[hIndex] = (scaleDist - 1) * hShift; + minX[hIndex] = std::max<int>(0, -dist[hIndex]); + maxX[hIndex] = std::min<int>(width, width - dist[hIndex]); + } + for (size_t y = 0; y != height; ++y) { + float* outputPtr = &output[y * width]; + const float* inputPtr = &image[y * width]; + + for (size_t x = 0; x != minX[1]; ++x) { + outputPtr[x] = inputPtr[x + dist[2]] * h[2] + + inputPtr[x + dist[3]] * h[3] + + inputPtr[x + dist[4]] * h[4]; + } + + for (size_t x = minX[1]; x != minX[0]; ++x) { + outputPtr[x] = + inputPtr[x + dist[2]] * h[2] + inputPtr[x + dist[1]] * h[1] + + inputPtr[x + dist[3]] * h[3] + inputPtr[x + dist[4]] * h[4]; + } + + for (size_t x = minX[0]; x != maxX[4]; ++x) { + outputPtr[x] = + inputPtr[x + dist[2]] * h[2] + inputPtr[x + dist[1]] * h[1] + + inputPtr[x + dist[0]] * h[0] + inputPtr[x + dist[3]] * h[3] + + inputPtr[x + dist[4]] * h[4]; + } + + for (size_t x = maxX[4]; x != maxX[3]; ++x) { + outputPtr[x] = + inputPtr[x + dist[2]] * h[2] + inputPtr[x + dist[1]] * h[1] + + inputPtr[x + dist[0]] * h[0] + inputPtr[x + dist[3]] * h[3]; + } + + for (size_t x = maxX[3]; x != width; ++x) { + outputPtr[x] = inputPtr[x + dist[2]] * h[2] + + inputPtr[x + dist[1]] * h[1] + + inputPtr[x + dist[0]] * h[0]; + } + } +} + +// This version is not as fast as the one below. +void IUWTDecomposition::convolveVerticalPartialFastFailed( + float* output, const float* image, size_t width, size_t height, + size_t startX, size_t endX, int scale) { + const size_t H_SIZE = 5; + const float h[H_SIZE] = {1.0 / 16.0, 4.0 / 16.0, 6.0 / 16.0, 4.0 / 16.0, + 1.0 / 16.0}; + int scaleDist = (1 << scale); + for (size_t y = 0; y < height; ++y) { + float* outputPtr = &output[y * width]; + const float* inputPtr = &image[y * width]; + for (size_t x = startX; x != endX; ++x) outputPtr[x] = inputPtr[x] * h[2]; + } + for (int hIndex = 0; hIndex != H_SIZE; ++hIndex) { + if (hIndex != 2) { + int hShift = hIndex - H_SIZE / 2; + int dist = (scaleDist - 1) * hShift; + size_t minY = std::max<int>(0, -dist), + maxY = std::min<int>(height, height - dist); + for (size_t y = minY; y < maxY; ++y) { + float* outputPtr = &output[y * width]; + const float* inputPtr = &image[(y + dist) * width]; + for (size_t x = startX; x != endX; ++x) + outputPtr[x] += inputPtr[x] * h[hIndex]; + } + } + } +} + +void IUWTDecomposition::convolveVerticalPartialFast(float* output, + const float* image, + size_t width, size_t height, + size_t startX, size_t endX, + int scale) { + const size_t H_SIZE = 5; + const float h[H_SIZE] = {1.0 / 16.0, 4.0 / 16.0, 6.0 / 16.0, 4.0 / 16.0, + 1.0 / 16.0}; + int scaleDist = (1 << scale); + int dist[H_SIZE]; + size_t minY[H_SIZE], maxY[H_SIZE]; + for (int hIndex = 0; hIndex != H_SIZE; ++hIndex) { + int hShift = hIndex - H_SIZE / 2; + dist[hIndex] = (scaleDist - 1) * hShift; + minY[hIndex] = std::max<int>(0, -dist[hIndex]); + maxY[hIndex] = std::min<int>(height, height - dist[hIndex]); + } + + for (size_t y = 0; y != minY[1]; ++y) { + float* outputPtr = &output[y * width]; + const float* inputPtr2 = &image[(y + dist[2]) * width]; + const float* inputPtr3 = &image[(y + dist[3]) * width]; + const float* inputPtr4 = &image[(y + dist[4]) * width]; + for (size_t x = startX; x != endX; ++x) { + outputPtr[x] = + inputPtr2[x] * h[2] + inputPtr3[x] * h[3] + inputPtr4[x] * h[4]; + } + } + + for (size_t y = minY[1]; y != minY[0]; ++y) { + float* outputPtr = &output[y * width]; + const float* inputPtr1 = &image[(y + dist[1]) * width]; + const float* inputPtr2 = &image[(y + dist[2]) * width]; + const float* inputPtr3 = &image[(y + dist[3]) * width]; + const float* inputPtr4 = &image[(y + dist[4]) * width]; + for (size_t x = startX; x != endX; ++x) { + outputPtr[x] = inputPtr1[x] * h[1] + inputPtr2[x] * h[2] + + inputPtr3[x] * h[3] + inputPtr4[x] * h[4]; + } + } + + for (size_t y = minY[0]; y != maxY[4]; ++y) { + float* outputPtr = &output[y * width]; + const float* inputPtr0 = &image[(y + dist[0]) * width]; + const float* inputPtr1 = &image[(y + dist[1]) * width]; + const float* inputPtr2 = &image[(y + dist[2]) * width]; + const float* inputPtr3 = &image[(y + dist[3]) * width]; + const float* inputPtr4 = &image[(y + dist[4]) * width]; + for (size_t x = startX; x != endX; ++x) { + outputPtr[x] = inputPtr0[x] * h[0] + inputPtr1[x] * h[1] + + inputPtr2[x] * h[2] + inputPtr3[x] * h[3] + + inputPtr4[x] * h[4]; + } + } + + for (size_t y = maxY[4]; y != maxY[3]; ++y) { + float* outputPtr = &output[y * width]; + const float* inputPtr0 = &image[(y + dist[0]) * width]; + const float* inputPtr1 = &image[(y + dist[1]) * width]; + const float* inputPtr2 = &image[(y + dist[2]) * width]; + const float* inputPtr3 = &image[(y + dist[3]) * width]; + for (size_t x = startX; x != endX; ++x) { + outputPtr[x] = inputPtr0[x] * h[0] + inputPtr1[x] * h[1] + + inputPtr2[x] * h[2] + inputPtr3[x] * h[3]; + } + } + + for (size_t y = maxY[3]; y != height; ++y) { + float* outputPtr = &output[y * width]; + const float* inputPtr0 = &image[(y + dist[0]) * width]; + const float* inputPtr1 = &image[(y + dist[1]) * width]; + const float* inputPtr2 = &image[(y + dist[2]) * width]; + for (size_t x = startX; x != endX; ++x) { + outputPtr[x] = + inputPtr0[x] * h[0] + inputPtr1[x] * h[1] + inputPtr2[x] * h[2]; + } + } +} +} // namespace radler::algorithms::iuwt \ No newline at end of file diff --git a/cpp/algorithms/iuwt/iuwt_decomposition.h b/cpp/algorithms/iuwt/iuwt_decomposition.h new file mode 100644 index 00000000..c26de728 --- /dev/null +++ b/cpp/algorithms/iuwt/iuwt_decomposition.h @@ -0,0 +1,346 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef RADLER_ALGORITHMS_IUWT_DECOMPOSITION_H_ +#define RADLER_ALGORITHMS_IUWT_DECOMPOSITION_H_ + +#include <iostream> +#include <string> +#include <sstream> + +#include <aocommon/fits/fitswriter.h> +#include <aocommon/image.h> +#include <aocommon/staticfor.h> +#include <aocommon/uvector.h> + +#include "algorithms/iuwt/iuwt_mask.h" + +namespace radler::algorithms::iuwt { + +class IUWTDecompositionScale { + public: + aocommon::Image& Coefficients() { return _coefficients; } + const aocommon::Image& Coefficients() const { return _coefficients; } + float& operator[](size_t index) { return _coefficients[index]; } + const float& operator[](size_t index) const { return _coefficients[index]; } + + private: + aocommon::Image _coefficients; +}; + +class IUWTDecomposition { + public: + IUWTDecomposition(int scaleCount, size_t width, size_t height) + : _scales(scaleCount + 1), + _scaleCount(scaleCount), + _width(width), + _height(height) {} + + IUWTDecomposition* CreateTrimmed(int newScaleCount, size_t x1, size_t y1, + size_t x2, size_t y2) const { + std::unique_ptr<IUWTDecomposition> p( + new IUWTDecomposition(newScaleCount, x2 - x1, y2 - y1)); + for (int i = 0; i != newScaleCount; ++i) { + copySmallerPart(_scales[i].Coefficients(), p->_scales[i].Coefficients(), + x1, y1, x2, y2); + } + p->_scales.back().Coefficients() = aocommon::Image(_width, _height, 0.0); + return p.release(); + } + + void Convolve(aocommon::Image& image, int toScale) { + aocommon::Image scratch(image.Width(), image.Height()); + for (int scale = 0; scale != toScale; ++scale) { + convolve(image.Data(), image.Data(), scratch.Data(), _width, _height, + scale + 1); + } + } + + void DecomposeSimple(aocommon::Image& input) { + aocommon::Image scratch(input.Width(), input.Height()); + for (int scale = 0; scale != int(_scaleCount); ++scale) { + aocommon::Image& coefficients = _scales[scale].Coefficients(); + coefficients = aocommon::Image(_width, _height); + + aocommon::Image tmp(_width, _height); + convolve(tmp.Data(), input.Data(), scratch.Data(), _width, _height, + scale); + difference(coefficients.Data(), input.Data(), tmp.Data(), _width, + _height); + std::copy_n(tmp.Data(), _width * _height, input.Data()); + } + _scales.back().Coefficients() = input; + } + + void RecomposeSimple(aocommon::Image& output) { + output = _scales[0].Coefficients(); + for (size_t scale = 1; scale != _scaleCount - 1; ++scale) { + for (size_t i = 0; i != _width * _height; ++i) + output[i] += _scales[scale][i]; + } + } + + void Decompose(aocommon::StaticFor<size_t>& loop, const float* input, + float* scratch, bool includeLargest) { + DecomposeMT(loop, input, scratch, includeLargest); + } + + void DecomposeMT(aocommon::StaticFor<size_t>& loop, const float* input, + float* scratch, bool includeLargest); + + void DecomposeST(const float* input, float* scratch) { + aocommon::UVector<float> i0(input, input + _width * _height); + aocommon::Image i1(_width, _height); + aocommon::Image i2(_width, _height); + for (int scale = 0; scale != int(_scaleCount); ++scale) { + aocommon::Image& coefficients = _scales[scale].Coefficients(); + coefficients = aocommon::Image(_width, _height); + convolve(i1.Data(), i0.data(), scratch, _width, _height, scale + 1); + convolve(i2.Data(), i1.Data(), scratch, _width, _height, scale + 1); + + // coefficients = i0 - i2 + difference(coefficients.Data(), i0.data(), i2.Data(), _width, _height); + + // i0 = i1; + if (scale + 1 != int(_scaleCount)) { + std::copy_n(i1.Data(), _width * _height, i0.data()); + } + } + _scales.back().Coefficients() = i1; + } + + void Recompose(aocommon::Image& output, bool includeLargest) { + aocommon::Image scratch1(_width, _height); + aocommon::Image scratch2(_width, _height); + bool isZero; + if (includeLargest) { + output = _scales.back().Coefficients(); + isZero = false; + } else { + output = aocommon::Image(_width, _height, 0.0); + isZero = true; + } + for (int scale = int(_scaleCount) - 1; scale != -1; --scale) { + const aocommon::Image& coefficients = _scales[scale].Coefficients(); + if (isZero) { + output = coefficients; + isZero = false; + } else { + // output = output (x) IUWT + convolve(scratch2.Data(), output.Data(), scratch1.Data(), _width, + _height, scale + 1); + output = scratch2; + + // output += coefficients + for (size_t i = 0; i != output.Size(); ++i) + output[i] += coefficients[i]; + } + } + } + + size_t NScales() const { return _scaleCount; } + + size_t Width() const { return _width; } + + size_t Height() const { return _height; } + + IUWTDecompositionScale& operator[](int scale) { return _scales[scale]; } + + const IUWTDecompositionScale& operator[](int scale) const { + return _scales[scale]; + } + + void ApplyMask(const IUWTMask& mask) { + for (size_t scale = 0; scale != _scaleCount; ++scale) { + for (size_t i = 0; i != _scales[scale].Coefficients().Size(); ++i) { + if (!mask[scale][i]) _scales[scale][i] = 0.0; + } + } + _scales[_scaleCount].Coefficients() = aocommon::Image(_width, _height, 0.0); + } + + void Save(const std::string& prefix) { + std::cout << "Saving scales...\n"; + aocommon::FitsWriter writer; + writer.SetImageDimensions(_width, _height); + for (size_t scale = 0; scale != _scales.size(); ++scale) { + std::ostringstream str; + str << prefix << "-iuwt-" << scale << ".fits"; + writer.Write(str.str(), _scales[scale].Coefficients().Data()); + } + } + + static int EndScale(size_t maxImageDimension) { + return std::max(int(log2(maxImageDimension)) - 3, 2); + } + + static size_t MinImageDimension(int endScale) { + return (1 << (endScale + 3)); + } + + std::string Summary() const { + std::ostringstream str; + str << "IUWTDecomposition, NScales()=" << NScales() + << ", MinImageDimension()=" << MinImageDimension(NScales() + 1) + << ", width=" << _width << ", height=" << _height; + return str.str(); + } + + private: + static void convolveComponentHorizontal(const float* input, float* output, + size_t width, size_t height, + float val, int dist) { + size_t minX = std::max<int>(0, -dist), + maxX = std::min<int>(width, width - dist); + for (size_t y = 0; y != height; ++y) { + float* outputPtr = &output[y * width]; + const float* inputPtr = &input[y * width]; + for (size_t x = minX; x < maxX; ++x) { + outputPtr[x] += inputPtr[x + dist] * val; + } + } + } + + static void convolveComponentVertical(const float* input, float* output, + size_t width, size_t height, float val, + int dist) { + size_t minY = std::max<int>(0, -dist), + maxY = std::min<int>(height, height - dist); + for (size_t y = minY; y < maxY; ++y) { + float* outputPtr = &output[y * width]; + const float* inputPtr = &input[(y + dist) * width]; + for (size_t x = 0; x != width; ++x) { + outputPtr[x] += inputPtr[x] * val; + } + } + } + + static void convolveComponentVerticalPartial(const float* input, + float* output, size_t width, + size_t height, size_t startX, + size_t endX, float val, + int dist) { + size_t minY = std::max<int>(0, -dist), + maxY = std::min<int>(height, height - dist); + for (size_t y = minY; y < maxY; ++y) { + float* outputPtr = &output[y * width]; + const float* inputPtr = &input[(y + dist) * width]; + for (size_t x = startX; x != endX; ++x) { + outputPtr[x] += inputPtr[x] * val; + } + } + } + + static void convolve(float* output, const float* image, float* scratch, + size_t width, size_t height, int scale) { + for (size_t i = 0; i != width * height; ++i) scratch[i] = 0.0; + const size_t H_SIZE = 5; + const float h[H_SIZE] = {1.0 / 16.0, 4.0 / 16.0, 6.0 / 16.0, 4.0 / 16.0, + 1.0 / 16.0}; + int scaleDist = (1 << scale); + for (int hIndex = 0; hIndex != H_SIZE; ++hIndex) { + int hShift = hIndex - H_SIZE / 2; + convolveComponentHorizontal(image, scratch, width, height, h[hIndex], + (scaleDist - 1) * hShift); + } + for (size_t i = 0; i != width * height; ++i) output[i] = 0.0; + for (int hIndex = 0; hIndex != H_SIZE; ++hIndex) { + int hShift = hIndex - H_SIZE / 2; + convolveComponentVertical(scratch, output, width, height, h[hIndex], + (scaleDist - 1) * hShift); + } + } + + static void convolveMT(aocommon::StaticFor<size_t>& loop, float* output, + const float* image, float* scratch, size_t width, + size_t height, int scale); + + static void convolveHorizontalPartial(float* output, const float* image, + size_t width, size_t startY, + size_t endY, int scale) { + size_t startIndex = startY * width; + convolveHorizontalFast(&output[startIndex], &image[startIndex], width, + endY - startY, scale); + } + + static void convolveHorizontal(float* output, const float* image, + size_t width, size_t height, int scale) { + for (size_t i = 0; i != width * height; ++i) output[i] = 0.0; + const size_t H_SIZE = 5; + const float h[H_SIZE] = {1.0 / 16.0, 4.0 / 16.0, 6.0 / 16.0, 4.0 / 16.0, + 1.0 / 16.0}; + int scaleDist = (1 << scale); + for (int hIndex = 0; hIndex != H_SIZE; ++hIndex) { + int hShift = hIndex - H_SIZE / 2; + convolveComponentHorizontal(image, output, width, height, h[hIndex], + (scaleDist - 1) * hShift); + } + } + + static void convolveHorizontalFast(float* output, const float* image, + size_t width, size_t height, int scale); + + static void convolveVerticalPartial(float* output, const float* image, + size_t width, size_t height, + size_t startX, size_t endX, int scale) { + for (size_t i = 0; i != width * height; ++i) output[i] = 0.0; + const size_t H_SIZE = 5; + const float h[H_SIZE] = {1.0 / 16.0, 4.0 / 16.0, 6.0 / 16.0, 4.0 / 16.0, + 1.0 / 16.0}; + int scaleDist = (1 << scale); + for (int hIndex = 0; hIndex != H_SIZE; ++hIndex) { + int hShift = hIndex - H_SIZE / 2; + convolveComponentVerticalPartial(image, output, width, height, startX, + endX, h[hIndex], + (scaleDist - 1) * hShift); + } + } + + static void convolveVerticalPartialFast(float* output, const float* image, + size_t width, size_t height, + size_t startX, size_t endX, + int scale); + + static void convolveVerticalPartialFastFailed(float* output, + const float* image, + size_t width, size_t height, + size_t startX, size_t endX, + int scale); + + static void differencePartial(float* dest, const float* lhs, const float* rhs, + size_t width, size_t startY, size_t endY) { + size_t startIndex = startY * width; + difference(&dest[startIndex], &lhs[startIndex], &rhs[startIndex], width, + endY - startY); + } + + static void differenceMT(aocommon::StaticFor<size_t>& loop, float* dest, + const float* lhs, const float* rhs, size_t width, + size_t height); + + static void difference(float* dest, const float* lhs, const float* rhs, + size_t width, size_t height) { + for (size_t i = 0; i != width * height; ++i) { + dest[i] = lhs[i] - rhs[i]; + } + } + + void copySmallerPart(const aocommon::Image& input, aocommon::Image& output, + size_t x1, size_t y1, size_t x2, size_t y2) const { + size_t newWidth = x2 - x1; + output = aocommon::Image(newWidth, y2 - y1); + for (size_t y = y1; y != y2; ++y) { + const float* oldPtr = &input[y * _width]; + float* newPtr = &output[(y - y1) * newWidth]; + for (size_t x = x1; x != x2; ++x) { + newPtr[x - x1] = oldPtr[x]; + } + } + } + + private: + std::vector<IUWTDecompositionScale> _scales; + size_t _scaleCount, _width, _height; +}; +} // namespace radler::algorithms::iuwt +#endif diff --git a/cpp/algorithms/iuwt/iuwt_mask.cc b/cpp/algorithms/iuwt/iuwt_mask.cc new file mode 100644 index 00000000..c8308fcf --- /dev/null +++ b/cpp/algorithms/iuwt/iuwt_mask.cc @@ -0,0 +1,36 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "algorithms/iuwt/iuwt_mask.h" + +#include <boost/numeric/conversion/bounds.hpp> + +#include "algorithms/iuwt/iuwt_decomposition.h" + +namespace radler::algorithms::iuwt { + +std::string IUWTMask::Summary(const IUWTDecomposition& iuwt) const { + std::ostringstream str; + str << "IUWTMask with " << _masks.size() + << " scale masks (iuwt: " << iuwt.Summary() << ")\n"; + for (size_t i = 0; i != _masks.size(); ++i) { + double maxVal = boost::numeric::bounds<double>::lowest(); + double minVal = std::numeric_limits<double>::max(); + size_t count = 0; + for (size_t j = 0; j != _masks[i].size(); ++j) { + if (_masks[i][j]) { + ++count; + if (iuwt[i][j] > maxVal) maxVal = iuwt[i][j]; + if (iuwt[i][j] < minVal) minVal = iuwt[i][j]; + } + } + if (maxVal == boost::numeric::bounds<double>::lowest()) { + maxVal = std::numeric_limits<double>::quiet_NaN(); + minVal = std::numeric_limits<double>::quiet_NaN(); + } + str << "Scale " << i << ": " << count << " (" << minVal << " - " << maxVal + << ")\n"; + } + return str.str(); +} +} // namespace radler::algorithms::iuwt diff --git a/cpp/algorithms/iuwt/iuwt_mask.h b/cpp/algorithms/iuwt/iuwt_mask.h new file mode 100644 index 00000000..b456c5ca --- /dev/null +++ b/cpp/algorithms/iuwt/iuwt_mask.h @@ -0,0 +1,86 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef RADLER_ALGORITHMS_IUWT_IUWT_MASK_H_ +#define RADLER_ALGORITHMS_IUWT_IUWT_MASK_H_ + +#include <vector> +#include <string> +#include <sstream> +#include <limits> + +#include <aocommon/uvector.h> + +namespace radler::algorithms::iuwt { +class IUWTMask { + public: + IUWTMask(int scaleCount, size_t width, size_t height) + : _masks(scaleCount), _width(width), _height(height) { + for (int i = 0; i != scaleCount; ++i) + _masks[i].assign(width * height, false); + } + + IUWTMask* CreateTrimmed(size_t x1, size_t y1, size_t x2, size_t y2) const { + std::unique_ptr<IUWTMask> p(new IUWTMask(ScaleCount(), x2 - x1, y2 - y1)); + + for (size_t i = 0; i != _masks.size(); ++i) + copySmallerPart(_masks[i], p->_masks[i], x1, y1, x2, y2); + + return p.release(); + } + + aocommon::UVector<bool>& operator[](int scale) { return _masks[scale]; } + const aocommon::UVector<bool>& operator[](int scale) const { + return _masks[scale]; + } + std::string Summary() const { + std::ostringstream str; + str << "IUWTMask with " << _masks.size() << " scale masks.\n"; + for (size_t i = 0; i != _masks.size(); ++i) { + size_t count = 0; + for (size_t j = 0; j != _masks[i].size(); ++j) { + if (_masks[i][j]) ++count; + } + str << "Scale " << i << ": " << count << '\n'; + } + return str.str(); + } + std::string Summary(const class IUWTDecomposition& iuwt) const; + + size_t HighestActiveScale() const { + for (int m = _masks.size() - 1; m != -1; --m) { + for (size_t i = 0; i != _masks[m].size(); ++i) { + if (_masks[m][i]) return m; + } + } + return 0; // avoid compiler warning + } + + void TransferScaleUpwards(size_t fromScale) { + if (fromScale > 0) { + size_t toScale = fromScale - 1; + for (size_t i = 0; i != _masks[fromScale].size(); ++i) { + if (_masks[fromScale][i]) _masks[toScale][i] = true; + } + } + } + size_t ScaleCount() const { return _masks.size(); } + + private: + std::vector<aocommon::UVector<bool>> _masks; + size_t _width, _height; + + void copySmallerPart(const aocommon::UVector<bool>& input, + aocommon::UVector<bool>& output, size_t x1, size_t y1, + size_t x2, size_t y2) const { + size_t newWidth = x2 - x1; + output.resize(newWidth * (y2 - y1)); + for (size_t y = y1; y != y2; ++y) { + const bool* oldPtr = &input[y * _width]; + bool* newPtr = &output[(y - y1) * newWidth]; + for (size_t x = x1; x != x2; ++x) newPtr[x - x1] = oldPtr[x]; + } + } +}; +} // namespace radler::algorithms::iuwt +#endif diff --git a/cpp/algorithms/iuwt_deconvolution.h b/cpp/algorithms/iuwt_deconvolution.h new file mode 100644 index 00000000..4b13897d --- /dev/null +++ b/cpp/algorithms/iuwt_deconvolution.h @@ -0,0 +1,47 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef RADLER_ALGORITHMS_IUWT_DECONVOLUTION_H_ +#define RADLER_ALGORITHMS_IUWT_DECONVOLUTION_H_ + +#include <memory> +#include <string> + +#include <aocommon/uvector.h> + +#include "image_set.h" +#include "algorithms/deconvolution_algorithm.h" +#include "algorithms/iuwt_deconvolution_algorithm.h" + +// TODO: consider merging IUWTDeconvolutionAlgorithms into this class. + +namespace radler::algorithms { + +class IUWTDeconvolution : public DeconvolutionAlgorithm { + public: + IUWTDeconvolution() : _useSNRTest(false) {} + + float ExecuteMajorIteration(ImageSet& dataImage, ImageSet& modelImage, + const std::vector<aocommon::Image>& psfImages, + bool& reachedMajorThreshold) final override { + IUWTDeconvolutionAlgorithm algorithm( + dataImage.Width(), dataImage.Height(), _gain, _mGain, _cleanBorderRatio, + _allowNegativeComponents, _cleanMask, _threshold, _useSNRTest); + float val = algorithm.PerformMajorIteration( + _iterationNumber, MaxNIter(), modelImage, dataImage, psfImages, + reachedMajorThreshold); + if (_iterationNumber >= MaxNIter()) reachedMajorThreshold = false; + return val; + } + + std::unique_ptr<DeconvolutionAlgorithm> Clone() const final override { + return std::make_unique<IUWTDeconvolution>(*this); + } + + void SetUseSNRTest(bool useSNRTest) { _useSNRTest = useSNRTest; } + + private: + bool _useSNRTest; +}; +} // namespace radler::algorithms +#endif // RADLER_ALGORITHMS_IUWT_DECONVOLUTION_H_ diff --git a/cpp/algorithms/iuwt_deconvolution_algorithm.cc b/cpp/algorithms/iuwt_deconvolution_algorithm.cc new file mode 100644 index 00000000..5a8e05f5 --- /dev/null +++ b/cpp/algorithms/iuwt_deconvolution_algorithm.cc @@ -0,0 +1,962 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "algorithms/iuwt_deconvolution_algorithm.h" + +#include <algorithm> +#include <iostream> + +#include <boost/numeric/conversion/bounds.hpp> + +#include <aocommon/image.h> +#include <aocommon/system.h> + +#include <schaapcommon/fft/convolution.h> +#include <schaapcommon/fitters/gaussianfitter.h> + +#include "image_set.h" +#include "algorithms/iuwt/image_analysis.h" + +using aocommon::Image; + +namespace radler::algorithms { + +using iuwt::ImageAnalysis; +using iuwt::IUWTDecomposition; +using iuwt::IUWTMask; + +IUWTDeconvolutionAlgorithm::IUWTDeconvolutionAlgorithm( + size_t width, size_t height, float gain, float mGain, float cleanBorder, + bool allowNegativeComponents, const bool* mask, float absoluteThreshold, + float thresholdSigmaLevel, float tolerance, bool useSNRTest) + : _width(width), + _height(height), + _gain(gain), + _mGain(mGain), + _cleanBorder(cleanBorder), + _mask(mask), + _absoluteThreshold(absoluteThreshold), + _thresholdSigmaLevel(thresholdSigmaLevel), + _tolerance(tolerance), + _allowNegativeComponents(allowNegativeComponents), + _useSNRTest(useSNRTest) {} + +void IUWTDeconvolutionAlgorithm::measureRMSPerScale( + const float* image, const float* convolvedImage, float* scratch, + size_t endScale, std::vector<ScaleResponse>& psfResponse) { + IUWTDecomposition imageIUWT(endScale, _width, _height); + imageIUWT.Decompose(*_staticFor, image, scratch, false); + + _psfMaj = 2.0; + _psfMin = 2.0; + _psfPA = 0.0; + double fl = 0.0; + schaapcommon::fitters::GaussianFitter fitter; + fitter.Fit2DGaussianCentred(image, _width, _height, 2.0, _psfMaj, _psfMin, + _psfPA); + _psfVolume = (M_PI / 4.0) * _psfMaj * _psfMin / M_LOG2E; + + double v = 1.0, x = _width / 2, y = _height / 2; + double bMaj = _psfMaj, bMin = _psfMin, bPA = _psfPA; + fitter.Fit2DGaussianFull(image, _width, _height, v, x, y, bMaj, bMin, bPA, + &fl); + + psfResponse.resize(endScale); + for (size_t scale = 0; scale != endScale; ++scale) { + psfResponse[scale].rms = rms(imageIUWT[scale].Coefficients()); + psfResponse[scale].peakResponse = + centralPeak(imageIUWT[scale].Coefficients()); + bMaj = 2.0; + bMin = 2.0; + bPA = 0.0; + v = 1.0; + x = _width / 2; + y = _height / 2; + fitter.Fit2DGaussianFull(imageIUWT[scale].Coefficients().Data(), _width, + _height, v, x, y, bMaj, bMin, bPA, &fl); + psfResponse[scale].bMaj = bMaj; + psfResponse[scale].bMin = bMin; + psfResponse[scale].bPA = bPA; + } + + imageIUWT.Decompose(*_staticFor, imageIUWT[1].Coefficients().Data(), scratch, + false); + for (size_t scale = 0; scale != endScale; ++scale) { + psfResponse[scale].peakResponseToNextScale = + centralPeak(imageIUWT[scale].Coefficients()); + } + + imageIUWT.Decompose(*_staticFor, convolvedImage, scratch, false); + + for (size_t scale = 0; scale != endScale; ++scale) { + psfResponse[scale].convolvedPeakResponse = + centralPeak(imageIUWT[scale].Coefficients()); + } + + aocommon::UVector<float> thresholds(imageIUWT.NScales()); + for (size_t i = 0; i != imageIUWT.NScales(); ++i) { + thresholds[i] = psfResponse[0].convolvedPeakResponse * _tolerance; + } + IUWTMask mask(imageIUWT.NScales(), _width, _height); + ImageAnalysis::Component component(_width / 2, _height / 2, 0); + size_t areaSize; + ImageAnalysis::Floodfill(imageIUWT, mask, thresholds, 0, + std::min<size_t>(endScale, 2), component, 0.0, + areaSize); + aocommon::UVector<bool> markedMask0(mask[0].size(), false); + ImageAnalysis::Component2D c2D(_width / 2, _height / 2); + float threshold = psfResponse[0].convolvedPeakResponse * _tolerance; + ImageAnalysis::FloodFill2D(imageIUWT[0].Coefficients().Data(), + markedMask0.data(), threshold, c2D, _width, + _height, psfResponse[0].convolvedArea); +} + +float IUWTDeconvolutionAlgorithm::mad(const float* dest) { + Image v(_width, _height); + for (size_t i = 0; i != _width * _height; ++i) v[i] = std::fabs(dest[i]); + size_t mid = (_width * _height) / 2; + std::nth_element(v.begin(), v.begin() + mid, v.end()); + return v[mid] / 0.674559; +} + +float IUWTDeconvolutionAlgorithm::getMaxAbsWithoutMask(const Image& data, + size_t& x, size_t& y, + size_t width) { + size_t height = data.Size() / width; + size_t xBorder = _cleanBorder * width; + size_t yBorder = _cleanBorder * height; + size_t minX = xBorder, maxX = width - xBorder; + size_t minY = yBorder, maxY = height - yBorder; + x = width; + y = height; + + float maxVal = boost::numeric::bounds<float>::lowest(); + for (size_t yi = minY; yi != maxY; ++yi) { + const float* dataPtr = data.Data() + yi * width; + for (size_t xi = minX; xi != maxX; ++xi) { + float val = + _allowNegativeComponents ? std::fabs(dataPtr[xi]) : dataPtr[xi]; + if (val > maxVal) { + maxVal = val; + x = xi; + y = yi; + } + } + } + return maxVal; +} + +float IUWTDeconvolutionAlgorithm::getMaxAbsWithMask(const Image& data, + size_t& x, size_t& y, + size_t width) { + size_t height = data.Size() / width; + size_t xBorder = _cleanBorder * width; + size_t yBorder = _cleanBorder * height; + size_t minX = xBorder, maxX = width - xBorder; + size_t minY = yBorder, maxY = height - yBorder; + x = width; + y = height; + + float maxVal = boost::numeric::bounds<float>::lowest(); + for (size_t yi = minY; yi != maxY; ++yi) { + const float* dataPtr = data.Data() + yi * width; + const bool* maskPtr = _mask + yi * width; + for (size_t xi = minX; xi != maxX; ++xi) { + if (maskPtr[xi]) { + float val = + _allowNegativeComponents ? std::fabs(dataPtr[xi]) : dataPtr[xi]; + if (val > maxVal) { + maxVal = val; + x = xi; + y = yi; + } + } + } + } + return maxVal; +} + +float IUWTDeconvolutionAlgorithm::dotProduct(const Image& lhs, + const Image& rhs) { + float sum = 0.0; + for (size_t i = 0; i != lhs.Size(); ++i) sum += lhs[i] * rhs[i]; + return sum; +} + +void IUWTDeconvolutionAlgorithm::factorAdd(float* dest, const float* rhs, + float factor, size_t width, + size_t height) { + for (size_t i = 0; i != width * height; ++i) dest[i] += rhs[i] * factor; +} + +void IUWTDeconvolutionAlgorithm::factorAdd(Image& dest, const Image& rhs, + float factor) { + for (size_t i = 0; i != dest.Size(); ++i) dest[i] += rhs[i] * factor; +} + +void IUWTDeconvolutionAlgorithm::Subtract(float* dest, const Image& rhs) { + for (size_t i = 0; i != rhs.Size(); ++i) dest[i] -= rhs[i]; +} + +void IUWTDeconvolutionAlgorithm::boundingBox(size_t& x1, size_t& y1, size_t& x2, + size_t& y2, const Image& image, + size_t width, size_t height) { + float mP = *std::max_element(image.begin(), image.end()); + float mN = *std::min_element(image.begin(), image.end()); + float m = std::max(mP, -mN); + x1 = width; + x2 = 0; + y1 = height; + y2 = 0; + for (size_t y = 0; y != height; ++y) { + const float* ptr = image.Data() + y * width; + for (size_t x = 0; x != x1; ++x) { + if (std::fabs(ptr[x]) > m * 0.01) { + x1 = x; + break; + } + } + for (size_t x = width - 1; x != x2; --x) { + if (std::fabs(ptr[x]) > m * 0.01) { + x2 = x; + break; + } + } + } + x2++; + for (size_t y = 0; y != height; ++y) { + const float* ptr = image.Data() + y * width; + for (size_t x = 0; x != width; ++x) { + if (std::fabs(ptr[x]) > m * 0.01) { + if (y1 > y) y1 = y; + if (y2 < y) y2 = y + 1; + } + } + } +} + +void IUWTDeconvolutionAlgorithm::adjustBox(size_t& x1, size_t& y1, size_t& x2, + size_t& y2, size_t width, + size_t height, int endScale) { + const int minBoxSize = std::max<int>( + 128, IUWTDecomposition::MinImageDimension(endScale) * 3 / 2); + + int boxWidth = x2 - x1; + int boxHeight = y2 - y1; + int newX1 = x1 - 0.5 * boxWidth, newX2 = x2 + 0.5 * boxWidth, + newY1 = y1 - 0.5 * boxHeight, newY2 = y2 + 0.5 * boxHeight; + + if (newX2 - newX1 < minBoxSize) { + int mid = 0.5 * (int(x1) + int(x2)); + newX1 = mid - minBoxSize / 2; + newX2 = mid + minBoxSize / 2; + } + if (newY2 - newY1 < minBoxSize) { + int mid = 0.5 * (int(y1) + int(y2)); + newY1 = mid - minBoxSize / 2; + newY2 = mid + minBoxSize / 2; + } + if (newX1 >= 0) + x1 = newX1; + else + x1 = 0; + if (newX2 < int(width)) + x2 = newX2; + else + x2 = width; + if (newY1 >= 0) + y1 = newY1; + else + y1 = 0; + if (newY2 < int(height)) + y2 = newY2; + else + y2 = height; + while ((x2 - x1) % 8 != 0) x2--; + while ((y2 - y1) % 8 != 0) y2--; +} + +void IUWTDeconvolutionAlgorithm::trim(Image& dest, const float* source, + size_t oldWidth, size_t x1, size_t y1, + size_t x2, size_t y2) { + // We do this so that dest and source can be the same image. + Image scratch((x2 - x1), (y2 - y1)); + for (size_t y = y1; y != y2; ++y) { + const float* oldPtr = &source[y * oldWidth]; + float* newPtr = &scratch[(y - y1) * (x2 - x1)]; + for (size_t x = x1; x != x2; ++x) { + newPtr[x - x1] = oldPtr[x]; + } + } + dest = std::move(scratch); +} + +void IUWTDeconvolutionAlgorithm::untrim(Image& image, size_t width, + size_t height, size_t x1, size_t y1, + size_t x2, size_t y2) { + Image scratch(width, height, 0.0); + std::copy_n(image.Data(), image.Width() * image.Height(), scratch.Data()); + image = scratch; + size_t y = y2; + while (y != y1) { + --y; + float* newPtr = &image[y * width]; + float* oldPtr = &image[(y - y1) * (x2 - x1)]; + size_t x = x2; + while (x != x1) { + --x; + newPtr[x] = oldPtr[x - x1]; + } + } + for (size_t y = 0; y != y1; ++y) { + float* ptr = &image[y * width]; + for (size_t x = 0; x != width; ++x) ptr[x] = 0; + } + for (size_t y = y1; y != y2; ++y) { + float* ptr = &image[y * width]; + for (size_t x = 0; x != x1; ++x) ptr[x] = 0.0; + for (size_t x = x2; x != width; ++x) ptr[x] = 0.0; + } + for (size_t y = y2; y != height; ++y) { + float* ptr = &image[y * width]; + for (size_t x = 0; x != width; ++x) ptr[x] = 0; + } +} + +float IUWTDeconvolutionAlgorithm::sum(const Image& img) const { + float s = 0.0; + for (size_t i = 0; i != img.Size(); ++i) s += img[i]; + return s; +} + +float IUWTDeconvolutionAlgorithm::snr(const IUWTDecomposition& noisyImg, + const IUWTDecomposition& model) const { + float mSum = 0.0, nSum = 0.0; + for (size_t scale = 0; scale != noisyImg.NScales(); ++scale) { + const Image& n = noisyImg[scale].Coefficients(); + const Image& m = model[scale].Coefficients(); + for (size_t i = 0; i != n.Size(); ++i) { + mSum += m[i] * m[i]; + float d = m[i] - n[i]; + nSum += d * d; + } + } + return mSum / nSum; +} + +float IUWTDeconvolutionAlgorithm::rmsDiff(const Image& a, const Image& b) { + float sum = 0.0; + for (size_t i = 0; i != a.Size(); ++i) { + float d = a[i] - b[i]; + sum += d * d; + } + return std::sqrt(sum / a.Size()); +} + +float IUWTDeconvolutionAlgorithm::rms(const Image& image) { + float sum = 0.0; + for (size_t i = 0; i != image.Size(); ++i) { + float v = image[i]; + sum += v * v; + } + return std::sqrt(sum / image.Size()); +} + +bool IUWTDeconvolutionAlgorithm::runConjugateGradient( + IUWTDecomposition& iuwt, const IUWTMask& mask, Image& maskedDirty, + Image& structureModel, Image& scratch, const Image& psfKernel, size_t width, + size_t height) { + Image gradient = maskedDirty; + float modelSNR = 0.0; + + IUWTDecomposition initialDirtyIUWT(iuwt); + + for (size_t minorIter = 0; minorIter != 20; ++minorIter) { + // scratch = gradient (x) psf + scratch = gradient; + schaapcommon::fft::Convolve(scratch.Data(), psfKernel.Data(), width, height, + _staticFor->NThreads()); + + // calc: IUWT gradient (x) psf + iuwt.Decompose(*_staticFor, scratch.Data(), scratch.Data(), false); + + // calc: mask IUWT gradient (x) psf + iuwt.ApplyMask(mask); + + // scratch = IUWT^-1 mask IUWT gradient (x) psf + iuwt.Recompose(scratch, false); + + // stepsize = <residual, residual> / <gradient, scratch> + float gradientDotScratch = dotProduct(gradient, scratch); + if (gradientDotScratch == 0.0) return false; + float stepSize = dotProduct(maskedDirty, maskedDirty) / gradientDotScratch; + + // model_i+1 = model_i + stepsize * gradient + factorAdd(structureModel.Data(), gradient.Data(), stepSize, width, height); + + // For Dabbech's approach (see below) : + // Image scratch2 = maskedDirty; + + float gradStepDen = dotProduct(maskedDirty, maskedDirty); + if (gradStepDen == 0.0) return false; + // residual_i+1 = residual_i - stepsize * scratch + factorAdd(maskedDirty.Data(), scratch.Data(), -stepSize, width, height); + + // PyMORESANE uses this: + // gradstep = <residual_i+1, residual_i+1> / <residual_i, residual_i> + // float gradStep = dotProduct(maskedDirty, maskedDirty) / gradStepDen; + // But in MORESANE's paper A. Dabbech says this: + // gradstep = <residual_i+1 - residual_i, residual_i+1> / <residual_i, + // residual_i> scratch = maskedDirty; subtract(scratch, scratch2); float + // gradStep = dotProduct(scratch, maskedDirty) / gradStepDen; + float gradStep = dotProduct(maskedDirty, maskedDirty) / gradStepDen; + + // gradient_i+1 = residual_i+1 + gradstep * gradient_i + scratch = gradient; + gradient = maskedDirty; + factorAdd(gradient.Data(), scratch.Data(), gradStep, width, height); + + // scratch = mask IUWT PSF (x) model + scratch = structureModel; + schaapcommon::fft::Convolve(scratch.Data(), psfKernel.Data(), width, height, + _staticFor->NThreads()); + iuwt.Decompose(*_staticFor, scratch.Data(), scratch.Data(), false); + iuwt.ApplyMask(mask); + + float previousSNR = modelSNR; + modelSNR = snr(iuwt, initialDirtyIUWT); + if (modelSNR > 100 && minorIter > 2) { + std::cout << "Converged after " << minorIter << " iterations.\n"; + return true; + } else if (modelSNR < previousSNR && minorIter > 5) { + if (modelSNR > 3) { + std::cout << "SNR decreased after " << minorIter + << " iterations (SNR=" << modelSNR << ").\n"; + return true; + } + } + } + if (modelSNR <= 3.0) { + std::cout << "Failed to converge (SNR=" << modelSNR << ").\n"; + structureModel = Image(width, height, 0.0); + return false; + } + return true; +} + +struct PointSource { + float x, y, flux; + bool operator<(const PointSource& rhs) const { return flux < rhs.flux; } +}; + +bool IUWTDeconvolutionAlgorithm::findAndDeconvolveStructure( + IUWTDecomposition& iuwt, Image& dirty, const Image& psf, + const Image& psfKernel, const std::vector<Image>& psfs, Image& scratch, + ImageSet& structureModel, size_t curEndScale, size_t curMinScale, + std::vector<IUWTDeconvolutionAlgorithm::ValComponent>& maxComponents) { + iuwt.Decompose(*_staticFor, dirty.Data(), scratch.Data(), false); + aocommon::UVector<float> thresholds(curEndScale); + _rmses.resize(curEndScale); + for (size_t scale = 0; scale != curEndScale; ++scale) { + float r = mad(iuwt[scale].Coefficients().Data()); + _rmses[scale] = r; + thresholds[scale] = r * (_thresholdSigmaLevel * 4.0 / 5.0); + } + + scratch = dirty; + maxComponents.resize(curEndScale); + for (size_t scale = 0; scale != curEndScale; ++scale) { + size_t x, y; + float maxAbsCoef = getMaxAbs(iuwt[scale].Coefficients(), x, y, _width); + maxComponents[scale].x = x; + maxComponents[scale].y = y; + maxComponents[scale].scale = scale; + maxComponents[scale].val = maxAbsCoef; + } + + float maxVal = -1.0; + size_t maxX = 0, maxY = 0; + int maxValScale = -1; + for (size_t scale = 0; scale != curEndScale; ++scale) { + // Considerations for this section: + // - Scale 0 should be chosen if the input corresponds to the PSF. + // Therefore, a peak on scale 1 should be at least: + // (PSF peak on scale 1) * (peak on scale 0) / (PSF (x) scale 1 peak + // response) Such that anything smaller than scale 1 will be considered + // scale 0. + + const ValComponent& val = maxComponents[scale]; + float absCoef = val.val / _psfResponse[scale].rms; + // std::cout << scale << ">=" << curMinScale << " && " << absCoef << " > " + // << maxVal << " && " << val.val << " > " << _rmses[scale]*_thresholdLevel + // << "\n"; + if (scale >= curMinScale && absCoef > maxVal && + val.val > _rmses[scale] * _thresholdSigmaLevel && + val.val > _rmses[scale] / _rmses[0] * _absoluteThreshold) { + maxX = val.x; + maxY = val.y; + maxValScale = scale; + if (scale == 0) { + float lowestRMS = std::min(_psfResponse[0].rms, _psfResponse[1].rms); + maxVal = val.val / lowestRMS * _psfResponse[1].peakResponse / + _psfResponse[0].peakResponseToNextScale; + } else + maxVal = absCoef; + } + } + if (maxValScale == -1) { + std::cout << "No significant pixel found.\n"; + return false; + } + + maxVal = iuwt[maxValScale][maxX + maxY * _width]; + std::cout << "Most significant pixel: " << maxX << ',' << maxY << "=" + << maxVal << " (" << maxVal / _rmses[maxValScale] << "σ) on scale " + << maxValScale << '\n'; + + if (std::fabs(maxVal) < thresholds[maxValScale]) { + std::cout << "Most significant pixel is in the noise, stopping.\n"; + return false; + } + + float scaleMaxAbsVal = std::fabs(maxVal); + for (size_t scale = 0; scale != curEndScale; ++scale) { + if (thresholds[scale] < _tolerance * scaleMaxAbsVal) { + thresholds[scale] = _tolerance * scaleMaxAbsVal; + } + if (maxVal < 0.0) thresholds[scale] = -thresholds[scale]; + } + + ImageAnalysis::Component maxComp(maxX, maxY, maxValScale); + return fillAndDeconvolveStructure( + iuwt, dirty, structureModel, scratch, psf, psfKernel, psfs, curEndScale, + curMinScale, _width, _height, thresholds, maxComp, true, _mask); +} + +bool IUWTDeconvolutionAlgorithm::fillAndDeconvolveStructure( + IUWTDecomposition& iuwt, Image& dirty, ImageSet& structureModelFull, + Image& scratch, const Image& psf, const Image& psfKernel, + const std::vector<Image>& psfs, size_t curEndScale, size_t curMinScale, + size_t width, size_t height, const aocommon::UVector<float>& thresholds, + const ImageAnalysis::Component& maxComp, bool allowTrimming, + const bool* priorMask) { + IUWTMask mask(curEndScale, width, height); + size_t areaSize; + ImageAnalysis::SelectStructures(iuwt, mask, thresholds, curMinScale, + curEndScale, _cleanBorder, priorMask, + areaSize); + std::cout << "Flood-filled area contains " << areaSize + << " significant components.\n"; + + iuwt.ApplyMask(mask); + iuwt.Recompose(scratch, false); + + // Find bounding box + size_t x1, y1, x2, y2; + boundingBox(x1, y1, x2, y2, scratch, width, height); + adjustBox(x1, y1, x2, y2, width, height, maxComp.scale + 1); + if (allowTrimming && ((x2 - x1) < width || (y2 - y1) < height)) { + _curBoxXStart = x1; + _curBoxXEnd = x2; + _curBoxYStart = y1; + _curBoxYEnd = y2; + std::cout << "Bounding box: (" << x1 << ',' << y1 << ")-(" << x2 << ',' + << y2 << ")\n"; + size_t newWidth = x2 - x1, newHeight = y2 - y1; + trim(dirty, dirty, x1, y1, x2, y2); + Image smallPSF; + + trimPsf(smallPSF, psf, newWidth, newHeight); + + Image smallPSFKernel(smallPSF.Width(), smallPSF.Height()); + schaapcommon::fft::PrepareConvolutionKernel( + smallPSFKernel.Data(), smallPSF.Data(), newWidth, newHeight, + _staticFor->NThreads()); + + scratch = Image(dirty.Width(), dirty.Height()); + + int maxScale = + std::max(IUWTDecomposition::EndScale(std::min(x2 - x1, y2 - y1)), + maxComp.scale + 1); + if (maxScale < int(curEndScale)) { + std::cout << "Bounding box too small for largest scale of " << curEndScale + << " -- ignoring scales>=" << maxScale + << " in this iteration.\n"; + curEndScale = maxScale; + } + std::unique_ptr<IUWTDecomposition> trimmedIUWT( + iuwt.CreateTrimmed(curEndScale, x1, y1, x2, y2)); + + std::unique_ptr<ImageSet> trimmedStructureModel( + structureModelFull.Trim(x1, y1, x2, y2, width)); + + aocommon::UVector<bool> trimmedPriorMask; + bool* trimmedPriorMaskPtr; + if (priorMask == nullptr) + trimmedPriorMaskPtr = nullptr; + else { + trimmedPriorMask.resize(newWidth * newHeight); + trimmedPriorMaskPtr = trimmedPriorMask.data(); + Image::TrimBox(trimmedPriorMaskPtr, x1, y1, newWidth, newHeight, + priorMask, width, height); + } + + ImageAnalysis::Component newMaxComp(maxComp.x - x1, maxComp.y - y1, + maxComp.scale); + bool result = fillAndDeconvolveStructure( + *trimmedIUWT, dirty, *trimmedStructureModel, scratch, smallPSF, + smallPSFKernel, psfs, curEndScale, curMinScale, x2 - x1, y2 - y1, + thresholds, newMaxComp, false, trimmedPriorMaskPtr); + for (size_t i = 0; i != structureModelFull.size(); ++i) { + std::copy_n((*trimmedStructureModel)[i].Data(), (y2 - y1) * (x2 - x1), + scratch.Data()); + untrim(scratch, width, height, x1, y1, x2, y2); + std::copy_n(scratch.Data(), width * height, structureModelFull.Data(i)); + } + + dirty = Image(scratch.Width(), scratch.Height()); + _curBoxXStart = 0; + _curBoxXEnd = width; + _curBoxYStart = 0; + _curBoxYEnd = height; + return result; + } else { + if (curEndScale <= 3) { + // TODO: remove? + // bool pointSourcesWereFound = extractPointSources(iuwt, mask, + // dirty.Data(), structureModel.Data()); if(pointSourcesWereFound) + // return + // true; + } + + // get undeconvolved dirty + iuwt.Decompose(*_staticFor, dirty.Data(), scratch.Data(), false); + + iuwt.ApplyMask(mask); + iuwt.Recompose(scratch, false); + + Image maskedDirty = scratch; + + Image structureModel(width, height, 0.0); + bool success = runConjugateGradient(iuwt, mask, maskedDirty, structureModel, + scratch, psfKernel, width, height); + if (!success) return false; + + float rmsBefore = rms(dirty); + scratch = structureModel; + schaapcommon::fft::Convolve(scratch.Data(), psfKernel.Data(), width, height, + _staticFor->NThreads()); + maskedDirty = dirty; // we use maskedDirty as temporary + factorAdd(maskedDirty.Data(), scratch.Data(), -_gain, width, height); + float rmsAfter = rms(maskedDirty); + if (rmsAfter > rmsBefore) { + std::cout << "RMS got worse: " << rmsBefore << " -> " << rmsAfter << '\n'; + return false; + } + + // TODO when only one image is available, this is not necessary + performSubImageFitAll(iuwt, mask, structureModel, scratch, maskedDirty, + maxComp, structureModelFull, psf, psfs, dirty); + + return true; + } +} + +void IUWTDeconvolutionAlgorithm::performSubImageFitAll( + IUWTDecomposition& iuwt, const IUWTMask& mask, const Image& structureModel, + Image& scratchA, Image& scratchB, const ImageAnalysis::Component& maxComp, + ImageSet& fittedModel, const Image& psf, const std::vector<Image>& psfs, + const Image& dirty) { + size_t width = iuwt.Width(), height = iuwt.Height(); + + if (_dirtySet->size() == 1) { + // With only one image, we don't have to refit + Image img(width, height); + std::copy_n(structureModel.Data(), width * height, img.begin()); + fittedModel.SetImage(0, std::move(img)); + } else { + std::cout << "Fitting structure in images: " << std::flush; + aocommon::UVector<float> correctionFactors; + scratchA = dirty; + performSubImageFitSingle(iuwt, mask, structureModel, scratchB, maxComp, psf, + scratchA, nullptr, correctionFactors); + + fittedModel = 0.0; + + for (size_t imgIndex = 0; imgIndex != _dirtySet->size(); ++imgIndex) { + std::cout << '.' << std::flush; + const aocommon::Image& subPsf = psfs[_dirtySet->PSFIndex(imgIndex)]; + + trim(scratchA, (*_dirtySet)[imgIndex], _curBoxXStart, _curBoxYStart, + _curBoxXEnd, _curBoxYEnd); + + Image smallSubPsf; + const Image* subPsfImage; + if (_width != width || _height != height) { + trimPsf(smallSubPsf, subPsf, width, height); + subPsfImage = &smallSubPsf; + } else { + subPsfImage = &subPsf; + } + + performSubImageFitSingle(iuwt, mask, structureModel, scratchB, maxComp, + *subPsfImage, scratchA, + fittedModel.Data(imgIndex), correctionFactors); + } + std::cout << '\n'; + } +} + +void IUWTDeconvolutionAlgorithm::performSubImageFitSingle( + IUWTDecomposition& iuwt, const IUWTMask& mask, const Image& structureModel, + Image& scratchB, const ImageAnalysis::Component& maxComp, const Image& psf, + Image& subDirty, float* fittedSubModel, + aocommon::UVector<float>& correctionFactors) { + size_t width = iuwt.Width(), height = iuwt.Height(); + + Image psfKernel(width, height); + schaapcommon::fft::PrepareConvolutionKernel( + psfKernel.Data(), psf.Data(), width, height, _staticFor->NThreads()); + + Image& maskedDirty = scratchB; + + iuwt.Decompose(*_staticFor, subDirty.Data(), subDirty.Data(), false); + iuwt.ApplyMask(mask); + iuwt.Recompose(maskedDirty, false); + aocommon::UVector<bool> mask2d(structureModel.Size(), false); + float peakLevel = std::fabs(structureModel[maxComp.y * width + maxComp.x]); + size_t componentIndex = 0; + for (size_t y = 0; y != height; ++y) { + bool* maskRow = &mask2d[y * width]; + const float* modelRow = &structureModel[y * width]; + for (size_t x = 0; x != width; ++x) { + if (!maskRow[x] && std::fabs(modelRow[x]) > peakLevel * 1e-4) { + std::vector<ImageAnalysis::Component2D> area; + ImageAnalysis::Component2D comp(x, y); + ImageAnalysis::FloodFill2D(structureModel.Data(), mask2d.data(), + peakLevel * 1e-4, comp, width, height, area); + // Find bounding box and copy active pixels to subDirty + subDirty = Image(width, height, 0.0); + size_t boxX1 = width, boxX2 = 0, boxY1 = height, boxY2 = 0; + for (std::vector<ImageAnalysis::Component2D>::const_iterator a = + area.begin(); + a != area.end(); ++a) { + size_t index = a->x + a->y * width; + boxX1 = std::min(a->x, boxX1); + boxX2 = std::max(a->x, boxX2); + boxY1 = std::min(a->y, boxY1); + boxY2 = std::max(a->y, boxY2); + subDirty[index] = structureModel[index]; + } + adjustBox(boxX1, boxY1, boxX2, boxY2, width, height, iuwt.NScales()); + + float factor = performSubImageComponentFitBoxed( + iuwt, mask, area, subDirty, maskedDirty, psf, psfKernel, boxX1, + boxY1, boxX2, boxY2); + + // if no fittedSubModel was given, we just need to store the factors. + // Otherwise, scale the deconvolved model and add it to the contaminated + // model. + if (fittedSubModel != nullptr) { + const float integratedFactor = correctionFactors[componentIndex]; + if (std::isfinite(factor) && std::isfinite(integratedFactor) && + integratedFactor != 0.0) { + for (std::vector<ImageAnalysis::Component2D>::const_iterator a = + area.begin(); + a != area.end(); ++a) { + size_t index = a->x + a->y * width; + fittedSubModel[index] += + structureModel[index] * factor / integratedFactor; + } + } + ++componentIndex; + } else { + correctionFactors.push_back(factor); + } + } + } + } +} + +float IUWTDeconvolutionAlgorithm::performSubImageComponentFitBoxed( + IUWTDecomposition& iuwt, const IUWTMask& mask, + const std::vector<ImageAnalysis::Component2D>& area, Image& model, + Image& maskedDirty, const Image& psf, const Image& psfKernel, size_t x1, + size_t y1, size_t x2, size_t y2) { + const size_t width = iuwt.Width(); + const size_t height = iuwt.Height(); + if (x1 > 0 || y1 > 0 || x2 < width || y2 < height) { + size_t newWidth = x2 - x1, newHeight = y2 - y1; + IUWTDecomposition smallIUWTW(iuwt.NScales(), newWidth, newHeight); + std::unique_ptr<IUWTMask> smallMask(mask.CreateTrimmed(x1, y1, x2, y2)); + Image smallModel; + trim(smallModel, model, x1, y1, x2, y2); + + Image smallPsf; + trimPsf(smallPsf, psf, newWidth, newHeight); + Image smallPsfKernel(smallPsf.Width(), smallPsf.Height()); + schaapcommon::fft::PrepareConvolutionKernel( + smallPsfKernel.Data(), smallPsf.Data(), newWidth, newHeight, + _staticFor->NThreads()); + + Image smallMaskedDirty; + trim(smallMaskedDirty, maskedDirty, x1, y1, x2, y2); + + float factor = + performSubImageComponentFit(smallIUWTW, *smallMask, area, smallModel, + smallMaskedDirty, smallPsfKernel, x1, y1); + return factor; + } else { + return performSubImageComponentFit(iuwt, mask, area, model, maskedDirty, + psfKernel, 0, 0); + } +} + +float IUWTDeconvolutionAlgorithm::performSubImageComponentFit( + IUWTDecomposition& iuwt, const IUWTMask& mask, + const std::vector<ImageAnalysis::Component2D>& area, Image& model, + Image& maskedDirty, const Image& psfKernel, size_t xOffset, + size_t yOffset) { + const size_t width = iuwt.Width(), height = iuwt.Height(); + // Calculate IUWT^-1 mask IUWT model (x) PSF + schaapcommon::fft::Convolve(model.Data(), psfKernel.Data(), width, height, + _staticFor->NThreads()); + iuwt.Decompose(*_staticFor, model.Data(), model.Data(), false); + iuwt.ApplyMask(mask); + iuwt.Recompose(model, false); + + float modelSum = 0.0, dirtySum = 0.0; + for (std::vector<ImageAnalysis::Component2D>::const_iterator a = area.begin(); + a != area.end(); ++a) { + size_t index = (a->x - xOffset) + (a->y - yOffset) * width; + modelSum += model[index]; + dirtySum += maskedDirty[index]; + } + // std::cout << "factor=" << dirtySum << " / " << modelSum << " = " << + // dirtySum/modelSum << '\n'; + if (modelSum == 0.0 || !std::isfinite(dirtySum) || !std::isfinite(modelSum)) + return 0.0; + else + return dirtySum / modelSum; +} + +float IUWTDeconvolutionAlgorithm::PerformMajorIteration( + size_t& iterCounter, size_t nIter, ImageSet& modelSet, ImageSet& dirtySet, + const std::vector<aocommon::Image>& psfs, bool& reachedMajorThreshold) { + aocommon::StaticFor<size_t> static_for(aocommon::system::ProcessorCount()); + _staticFor = &static_for; + + reachedMajorThreshold = false; + if (iterCounter == nIter) return 0.0; + + _modelSet = &modelSet; + _dirtySet = &dirtySet; + + _curBoxXStart = 0; + _curBoxXEnd = _width; + _curBoxYStart = 0; + _curBoxYEnd = _height; + + Image dirty(_width, _height); + dirtySet.GetLinearIntegrated(dirty); + Image psf(_width, _height); + dirtySet.GetIntegratedPSF(psf, psfs); + + int maxScale = IUWTDecomposition::EndScale(std::min(_width, _height)); + int curEndScale = 2; + + // Prepare the PSF for convolutions later on + Image psfKernel(_width, _height); + schaapcommon::fft::PrepareConvolutionKernel( + psfKernel.Data(), psf.Data(), _width, _height, static_for.NThreads()); + + std::cout << "Measuring PSF...\n"; + { + Image convolvedPSF(psf); + Image scratch(_width, _height); + + schaapcommon::fft::Convolve(convolvedPSF.Data(), psfKernel.Data(), _width, + _height, static_for.NThreads()); + measureRMSPerScale(psf.Data(), convolvedPSF.Data(), scratch.Data(), + maxScale, _psfResponse); + } + + ImageSet structureModel(modelSet, _width, _height); + + auto iuwt = std::make_unique<IUWTDecomposition>(curEndScale, _width, _height); + + Image dirtyBeforeIteration; + + float maxValue = 0.0; + size_t curMinScale = 0; + reachedMajorThreshold = false; + bool doContinue = true; + std::vector<ValComponent> initialComponents; + do { + std::cout << "*** Deconvolution iteration " << iterCounter << " ***\n"; + dirtyBeforeIteration = dirty; + schaapcommon::fft::PrepareConvolutionKernel( + psfKernel.Data(), psf.Data(), _width, _height, static_for.NThreads()); + std::vector<ValComponent> maxComponents; + Image scratch(_width, _height); + bool succeeded = findAndDeconvolveStructure( + *iuwt, dirty, psf, psfKernel, psfs, scratch, structureModel, + curEndScale, curMinScale, maxComponents); + + if (succeeded) { + structureModel *= _gain; + modelSet += structureModel; + + // Calculate: dirty = dirty - structureModel (x) psf + for (size_t i = 0; i != dirtySet.size(); ++i) { + scratch = structureModel[i]; + size_t psfIndex = dirtySet.PSFIndex(i); + schaapcommon::fft::PrepareConvolutionKernel( + psfKernel.Data(), psfs[psfIndex].Data(), _width, _height, + static_for.NThreads()); + schaapcommon::fft::Convolve(scratch.Data(), psfKernel.Data(), _width, + _height, static_for.NThreads()); + Subtract(dirtySet.Data(i), scratch); + } + dirtySet.GetLinearIntegrated(dirty); + + while (maxComponents.size() > initialComponents.size()) { + initialComponents.push_back(maxComponents[initialComponents.size()]); + } + maxValue = 0.0; + for (size_t c = 0; c != initialComponents.size(); ++c) { + std::cout << initialComponents[c].val << " now " << maxComponents[c].val + << '\n'; + maxValue = std::max(maxValue, maxComponents[c].val); + if (std::fabs(maxComponents[c].val) < + std::fabs(initialComponents[c].val) * (1.0 - _mGain)) { + std::cout << "Scale " << c << " reached mGain (starting level: " + << initialComponents[c].val + << ", now: " << maxComponents[c].val << ").\n"; + reachedMajorThreshold = true; + } + } + if (reachedMajorThreshold) break; + } else { + if (int(curMinScale) + 1 < curEndScale) { + ++curMinScale; + std::cout << "=> Min scale now " << curMinScale << '\n'; + } else { + curMinScale = 0; + if (curEndScale != maxScale) { + ++curEndScale; + std::cout << "=> Scale now " << curEndScale << ".\n"; + iuwt.reset(new IUWTDecomposition(curEndScale, _width, _height)); + } else { + std::cout << "Max scale reached: finished all scales, quiting.\n"; + doContinue = false; + } + } + dirty = dirtyBeforeIteration; + } + + ++iterCounter; + } while (iterCounter != nIter && doContinue); + return maxValue; +} +} // namespace radler::algorithms \ No newline at end of file diff --git a/cpp/algorithms/iuwt_deconvolution_algorithm.h b/cpp/algorithms/iuwt_deconvolution_algorithm.h new file mode 100644 index 00000000..15e5dbb7 --- /dev/null +++ b/cpp/algorithms/iuwt_deconvolution_algorithm.h @@ -0,0 +1,194 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef RADLER_ALGORITHMS_IUWT_DECONVOLUTION_ALGORITHM_H_ +#define RADLER_ALGORITHMS_IUWT_DECONVOLUTION_ALGORITHM_H_ + +#include <vector> + +#include <aocommon/fits/fitswriter.h> +#include <aocommon/image.h> +#include <aocommon/staticfor.h> +#include <aocommon/uvector.h> + +#include "image_set.h" +#include "algorithms/iuwt/image_analysis.h" +#include "algorithms/iuwt/iuwt_decomposition.h" + +namespace radler::algorithms { + +class IUWTDeconvolutionAlgorithm { + public: + IUWTDeconvolutionAlgorithm(size_t width, size_t height, float gain, + float mGain, float cleanBorder, + bool allowNegativeComponents, const bool* mask, + float absoluteThreshold, + float thresholdSigmaLevel = 4.0, + float tolerance = 0.75, bool useSNRTest = true); + + float PerformMajorIteration(size_t& iterCounter, size_t nIter, + ImageSet& modelSet, ImageSet& dirtySet, + const std::vector<aocommon::Image>& psfs, + bool& reachedMajorThreshold); + + void Subtract(float* dest, const aocommon::Image& rhs); + void Subtract(aocommon::Image& dest, const aocommon::Image& rhs) { + Subtract(dest.Data(), rhs); + } + + private: + struct ValComponent { + ValComponent() {} + ValComponent(size_t _x, size_t _y, int _scale, float _val = 0.0) + : x(_x), y(_y), scale(_scale), val(_val) {} + + std::string ToString() const { + std::ostringstream str; + str << x << ',' << y << ", scale " << scale; + return str.str(); + } + + size_t x, y; + int scale; + float val; + }; + + struct ScaleResponse { + float rms, peakResponse, peakResponseToNextScale, convolvedPeakResponse; + double bMaj, bMin, bPA; + size_t convolvedArea; + }; + + float getMaxAbsWithoutMask(const aocommon::Image& data, size_t& x, size_t& y, + size_t width); + float getMaxAbsWithMask(const aocommon::Image& data, size_t& x, size_t& y, + size_t width); + float getMaxAbs(const aocommon::Image& data, size_t& x, size_t& y, + size_t width) { + if (_mask == nullptr) + return getMaxAbsWithoutMask(data, x, y, width); + else + return getMaxAbsWithMask(data, x, y, width); + } + + void measureRMSPerScale(const float* image, const float* convolvedImage, + float* scratch, size_t endScale, + std::vector<ScaleResponse>& psfResponse); + + float mad(const float* dest); + + float dotProduct(const aocommon::Image& lhs, const aocommon::Image& rhs); + + void factorAdd(float* dest, const float* rhs, float factor, size_t width, + size_t height); + + void factorAdd(aocommon::Image& dest, const aocommon::Image& rhs, + float factor); + + void boundingBox(size_t& x1, size_t& y1, size_t& x2, size_t& y2, + const aocommon::Image& image, size_t width, size_t height); + + void adjustBox(size_t& x1, size_t& y1, size_t& x2, size_t& y2, size_t width, + size_t height, int endScale); + + void trim(aocommon::Image& dest, const float* source, size_t oldWidth, + size_t x1, size_t y1, size_t x2, size_t y2); + + void trim(aocommon::Image& dest, const aocommon::Image& source, size_t x1, + size_t y1, size_t x2, size_t y2) { + trim(dest, source.Data(), source.Width(), x1, y1, x2, y2); + } + + void trimPsf(aocommon::Image& dest, const aocommon::Image& source, + size_t newWidth, size_t newHeight) { + const size_t oldWidth = source.Width(); + const size_t oldHeight = source.Height(); + trim(dest, source, (oldWidth - newWidth) / 2, (oldHeight - newHeight) / 2, + (oldWidth + newWidth) / 2, (oldHeight + newHeight) / 2); + } + + void untrim(aocommon::Image& image, size_t width, size_t height, size_t x1, + size_t y1, size_t x2, size_t y2); + + float sum(const aocommon::Image& img) const; + + float snr(const iuwt::IUWTDecomposition& noisyImg, + const iuwt::IUWTDecomposition& model) const; + + float rmsDiff(const aocommon::Image& a, const aocommon::Image& b); + + float rms(const aocommon::Image& image); + + bool runConjugateGradient(iuwt::IUWTDecomposition& iuwt, + const iuwt::IUWTMask& mask, + aocommon::Image& maskedDirty, + aocommon::Image& structureModel, + aocommon::Image& scratch, + const aocommon::Image& psfKernel, size_t width, + size_t height); + + bool fillAndDeconvolveStructure( + iuwt::IUWTDecomposition& iuwt, aocommon::Image& dirty, + ImageSet& structureModelFull, aocommon::Image& scratch, + const aocommon::Image& psf, const aocommon::Image& psfKernel, + const std::vector<aocommon::Image>& psfs, size_t curEndScale, + size_t curMinScale, size_t width, size_t height, + const aocommon::UVector<float>& thresholds, + const iuwt::ImageAnalysis::Component& maxComp, bool allowTrimming, + const bool* priorMask); + + bool findAndDeconvolveStructure( + iuwt::IUWTDecomposition& iuwt, aocommon::Image& dirty, + const aocommon::Image& psf, const aocommon::Image& psfKernel, + const std::vector<aocommon::Image>& psfs, aocommon::Image& scratch, + ImageSet& structureModelFull, size_t curEndScale, size_t curMinScale, + std::vector<ValComponent>& maxComponents); + + void performSubImageFitAll( + iuwt::IUWTDecomposition& iuwt, const iuwt::IUWTMask& mask, + const aocommon::Image& structureModel, aocommon::Image& scratchA, + aocommon::Image& scratchB, const iuwt::ImageAnalysis::Component& maxComp, + ImageSet& fittedModel, const aocommon::Image& psf, + const std::vector<aocommon::Image>& psfs, const aocommon::Image& dirty); + + void performSubImageFitSingle( + iuwt::IUWTDecomposition& iuwt, const iuwt::IUWTMask& mask, + const aocommon::Image& structureModel, aocommon::Image& scratchB, + const iuwt::ImageAnalysis::Component& maxComp, const aocommon::Image& psf, + aocommon::Image& subDirty, float* fittedSubModel, + aocommon::UVector<float>& correctionFactors); + + float performSubImageComponentFitBoxed( + iuwt::IUWTDecomposition& iuwt, const iuwt::IUWTMask& mask, + const std::vector<iuwt::ImageAnalysis::Component2D>& area, + aocommon::Image& scratch, aocommon::Image& maskedDirty, + const aocommon::Image& psf, const aocommon::Image& psfKernel, size_t x1, + size_t y1, size_t x2, size_t y2); + + float performSubImageComponentFit( + iuwt::IUWTDecomposition& iuwt, const iuwt::IUWTMask& mask, + const std::vector<iuwt::ImageAnalysis::Component2D>& area, + aocommon::Image& scratch, aocommon::Image& maskedDirty, + const aocommon::Image& psfKernel, size_t xOffset, size_t yOffset); + + float centralPeak(const aocommon::Image& data) { + return data[_width / 2 + (_height / 2) * _width]; + } + + size_t _width, _height; + size_t _curBoxXStart, _curBoxXEnd; + size_t _curBoxYStart, _curBoxYEnd; + float _gain, _mGain, _cleanBorder; + const bool* _mask; + float _absoluteThreshold, _thresholdSigmaLevel, _tolerance; + double _psfMaj, _psfMin, _psfPA, _psfVolume; + aocommon::UVector<float> _rmses; + aocommon::FitsWriter _writer; + std::vector<ScaleResponse> _psfResponse; + bool _allowNegativeComponents, _useSNRTest; + ImageSet* _modelSet; + ImageSet* _dirtySet; + aocommon::StaticFor<size_t>* _staticFor; +}; +} // namespace radler::algorithms +#endif // RADLER_ALGORITHMS_IUWT_DECONVOLUTION_ALGORITHM_H_ diff --git a/cpp/algorithms/ls_deconvolution.cc b/cpp/algorithms/ls_deconvolution.cc new file mode 100644 index 00000000..5ef4060d --- /dev/null +++ b/cpp/algorithms/ls_deconvolution.cc @@ -0,0 +1,315 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "algorithms/ls_deconvolution.h" + +#include <gsl/gsl_vector.h> +#include <gsl/gsl_multifit_nlin.h> +#include <gsl/gsl_multifit.h> + +#include <aocommon/logger.h> + +#define OUTPUT_LSD_DEBUG_INFO 1 + +using aocommon::Logger; + +namespace radler::algorithms { + +struct LSDeconvolutionData { + LSDeconvolution* parent; + gsl_multifit_fdfsolver* solver; + aocommon::UVector<std::pair<size_t, size_t>> maskPositions; + size_t width, height; + double regularization; + const double* dirty; + const double* psf; + + static int fitting_func(const gsl_vector* xvec, void* data, gsl_vector* f) { + const LSDeconvolutionData& lsData = + *reinterpret_cast<LSDeconvolutionData*>(data); + + size_t i = 0, midX = lsData.width + (lsData.width / 2), + midY = lsData.height + (lsData.height / 2); +#ifdef OUTPUT_LSD_DEBUG_INFO + double rmsSum = 0.0, cost = 0.0, peakFlux = 0.0; +#endif + // e = (y - sum modelledflux)^2 + mu modelledflux + for (size_t y = 0; y != lsData.height; ++y) { + for (size_t x = 0; x != lsData.width; ++x) { + double modelledFlux = 0.0; + for (size_t p = 0; p != lsData.maskPositions.size(); ++p) { + int pX = lsData.maskPositions[p].first, + pY = lsData.maskPositions[p].second; + double pVal = gsl_vector_get(xvec, p); + int psfX = (x + midX - pX) % lsData.width; + int psfY = (y + midY - pY) % lsData.height; + modelledFlux += lsData.psf[psfX + psfY * lsData.width] * pVal; + } +#ifdef OUTPUT_LSD_DEBUG_INFO + double pixelCost = + (lsData.dirty[i] - modelledFlux) * (lsData.dirty[i] - modelledFlux); + rmsSum += pixelCost; + cost += pixelCost; +#endif + + gsl_vector_set(f, i, lsData.dirty[i] - modelledFlux); + ++i; + } + } + double totalModelledFlux = 0.0; + for (size_t p = 0; p != lsData.maskPositions.size(); ++p) { + totalModelledFlux += std::fabs(gsl_vector_get(xvec, p)); +#ifdef OUTPUT_LSD_DEBUG_INFO + cost += std::fabs(gsl_vector_get(xvec, p)) * lsData.regularization; + peakFlux = std::max(peakFlux, gsl_vector_get(xvec, p)); +#endif + } + // gsl_vector_set(f, lsData.width*lsData.height, + // sqrt(lsData.regularization*totalModelledFlux)); + gsl_vector_set(f, lsData.width * lsData.height, + lsData.regularization * totalModelledFlux); + +#ifdef OUTPUT_LSD_DEBUG_INFO + Logger::Debug << "Current RMS=" + << sqrt(rmsSum / (lsData.height * lsData.width)) + << ", mean flux in model=" + << totalModelledFlux / lsData.maskPositions.size() + << ", peak=" << peakFlux << ", total cost=" << cost << '\n'; +#endif + + return GSL_SUCCESS; + } + + static int fitting_func_deriv(const gsl_vector* xvec, void* data, + gsl_matrix* J) { +#ifdef OUTPUT_LSD_DEBUG_INFO + // Logger::Info << "Calculating Jacobian... " << std::flush; +#endif + const LSDeconvolutionData& lsData = + *reinterpret_cast<LSDeconvolutionData*>(data); + + size_t i = 0, midX = lsData.width + (lsData.width / 2), + midY = lsData.height + (lsData.height / 2); + for (size_t y = 0; y != lsData.height; ++y) { + for (size_t x = 0; x != lsData.width; ++x) { + for (size_t p = 0; p != lsData.maskPositions.size(); ++p) { + int pX = lsData.maskPositions[p].first, + pY = lsData.maskPositions[p].second; + // double pVal = gsl_vector_get(xvec, p); + int psfX = (x + midX - pX) % lsData.width; + int psfY = (y + midY - pY) % lsData.height; + + gsl_matrix_set(J, i, p, -lsData.psf[psfX + psfY * lsData.width]); + } + ++i; + } + } + for (size_t p = 0; p != lsData.maskPositions.size(); ++p) { + // f = sqrt | pval | + // = sqrt sqrt(pval^2) + // f'= 2pval * 0.5/sqrt(pval^2) * 0.5/sqrt(sqrt(pval^2)) + // f'= pval/( 2.0 * |pval| * sqrt(|pval|) ) + double dpval = gsl_vector_get(xvec, p); + /*if(dpval < 0.0) + dpval = -0.5/sqrt(-dpval); + else if(dpval > 0.0) + dpval = 0.5/sqrt(dpval); + else + dpval = 0.0;*/ + if (dpval < 0.0) dpval = -dpval; + // else if(dpval > 0.0) + // dpval = dpval; + // else + // dpval = 0.0; + gsl_matrix_set(J, lsData.width * lsData.height, p, + dpval * lsData.regularization); + } +#ifdef OUTPUT_LSD_DEBUG_INFO + // Logger::Info << "DONE\n"; +#endif + return GSL_SUCCESS; + } + + static int fitting_func_both(const gsl_vector* x, void* data, gsl_vector* f, + gsl_matrix* J) { + fitting_func(x, data, f); + fitting_func_deriv(x, data, J); + return GSL_SUCCESS; + } +}; + +LSDeconvolution::LSDeconvolution() : _data(new LSDeconvolutionData()) {} + +LSDeconvolution::LSDeconvolution(const LSDeconvolution& source) + : DeconvolutionAlgorithm(), _data(new LSDeconvolutionData(*source._data)) {} + +LSDeconvolution::~LSDeconvolution() {} + +void LSDeconvolution::getMaskPositions( + aocommon::UVector<std::pair<size_t, size_t>>& maskPositions, + const bool* mask, size_t width, size_t height) { + const bool* maskPtr = mask; + for (size_t y = 0; y != height; ++y) { + for (size_t x = 0; x != width; ++x) { + if (*maskPtr) { + maskPositions.push_back(std::make_pair(x, y)); + } + ++maskPtr; + } + } +} + +void LSDeconvolution::linearFit(double* dataImage, double* modelImage, + const double* psfImage, size_t width, + size_t height, + bool& /*reachedMajorThreshold*/) { + aocommon::UVector<std::pair<size_t, size_t>> maskPositions; + getMaskPositions(maskPositions, _cleanMask, width, height); + Logger::Info << "Running LSDeconvolution with " << maskPositions.size() + << " parameters.\n"; + + // y = X c + // - y is vector of N, N=number of data points (pixels in image) + // y_i = pixel value i + // - x is vector of N x M, M=number of parameters (pixels in mask) + // x_ij = (pixel value i) * (psf value j) + + size_t n = width * height; + size_t pn = maskPositions.size(); + + gsl_matrix* x = gsl_matrix_calloc(n, pn); + gsl_vector* y = gsl_vector_alloc(n); + gsl_vector* c = gsl_vector_calloc(pn); + gsl_matrix* cov = gsl_matrix_alloc(pn, pn); + double chisq; + gsl_multifit_linear_workspace* work = gsl_multifit_linear_alloc(n, pn); + + for (size_t i = 0; i != n; ++i) gsl_vector_set(y, i, dataImage[i]); + + size_t i = 0, midX = width + (width / 2), midY = height + (height / 2); + for (size_t yi = 0; yi != height; ++yi) { + for (size_t xi = 0; xi != width; ++xi) { + for (size_t p = 0; p != pn; ++p) { + int pX = maskPositions[p].first, pY = maskPositions[p].second; + int psfX = (xi + midX - pX) % width; + int psfY = (yi + midY - pY) % height; + + gsl_matrix_set(x, i, p, psfImage[psfX + psfY * width]); + } + ++i; + } + } + + Logger::Info << "psf(0,0) = " + << psfImage[midX % width + (midY % height) * width] << "\n"; + Logger::Info << "Fitting... "; + Logger::Info.Flush(); + int result = gsl_multifit_linear(x, y, c, cov, &chisq, work); + Logger::Info << "result=" << gsl_strerror(result) << "\n"; + gsl_multifit_linear_free(work); + + for (size_t i = 0; i != n; ++n) modelImage[i] = 0.0; + + for (size_t p = 0; p != pn; ++p) { + size_t pX = maskPositions[p].first, pY = maskPositions[p].second; + modelImage[pY * width + pX] = gsl_vector_get(c, p); + } + + for (size_t y = 0; y != height; ++y) { + size_t index = y * width; + for (size_t x = 0; x != width; ++x) { + double val = dataImage[index]; + for (size_t p = 0; p != pn; ++p) { + int pX = maskPositions[p].first, pY = maskPositions[p].second; + double pVal = gsl_vector_get(c, p); + int psfX = (x + midX - pX) % width; + int psfY = (y + midY - pY) % height; + val -= psfImage[psfX + psfY * width] * pVal; + } + dataImage[index] = val; + ++index; + } + } + + gsl_matrix_free(x); + gsl_vector_free(y); + gsl_vector_free(c); + gsl_matrix_free(cov); +} + +void LSDeconvolution::nonLinearFit(double* dataImage, double* modelImage, + const double* psfImage, size_t width, + size_t height, + bool& /*reachedMajorThreshold*/) { + if (this->_cleanMask == 0) throw std::runtime_error("No mask available"); + + getMaskPositions(_data->maskPositions, _cleanMask, width, height); + size_t parameterCount = _data->maskPositions.size(), + dataCount = width * height + 1; + Logger::Info << "Running LSDeconvolution with " << parameterCount + << " parameters.\n"; + + const gsl_multifit_fdfsolver_type* T = gsl_multifit_fdfsolver_lmsder; + _data->solver = gsl_multifit_fdfsolver_alloc(T, dataCount, parameterCount); + _data->dirty = dataImage; + _data->psf = psfImage; + _data->width = width; + _data->height = height; + _data->parent = this; + _data->regularization = 0.1; + + gsl_multifit_function_fdf fdf; + fdf.f = &LSDeconvolutionData::fitting_func; + fdf.df = &LSDeconvolutionData::fitting_func_deriv; + fdf.fdf = &LSDeconvolutionData::fitting_func_both; + fdf.n = dataCount; + fdf.p = parameterCount; + fdf.params = &*_data; + + aocommon::UVector<double> parameterArray(parameterCount, 0.0); + gsl_vector_view initialVals = + gsl_vector_view_array(parameterArray.data(), parameterCount); + gsl_multifit_fdfsolver_set(_data->solver, &fdf, &initialVals.vector); + + int status; + size_t iter = 0; + do { + iter++; + status = gsl_multifit_fdfsolver_iterate(_data->solver); + + if (status) break; + + status = gsl_multifit_test_delta(_data->solver->dx, _data->solver->x, 1e-4, + 1e-4); + + } while (status == GSL_CONTINUE && iter < 100); + Logger::Info << "niter=" << iter << ", status=" << gsl_strerror(status) + << "\n"; + + for (size_t p = 0; p != parameterCount; ++p) { + size_t pX = _data->maskPositions[p].first, + pY = _data->maskPositions[p].second; + modelImage[pY * width + pX] = gsl_vector_get(_data->solver->x, p); + } + + size_t midX = width + (width / 2), midY = height + (height / 2); + for (size_t y = 0; y != height; ++y) { + size_t index = y * width; + for (size_t x = 0; x != width; ++x) { + double val = dataImage[index]; + for (size_t p = 0; p != parameterCount; ++p) { + int pX = _data->maskPositions[p].first, + pY = _data->maskPositions[p].second; + double pVal = gsl_vector_get(_data->solver->x, p); + int psfX = (x + midX - pX) % width; + int psfY = (y + midY - pY) % height; + val -= psfImage[psfX + psfY * width] * pVal; + } + dataImage[index] = val; + ++index; + } + } + + gsl_multifit_fdfsolver_free(_data->solver); +} +} // namespace radler::algorithms \ No newline at end of file diff --git a/cpp/algorithms/ls_deconvolution.h b/cpp/algorithms/ls_deconvolution.h new file mode 100644 index 00000000..cf3ca13f --- /dev/null +++ b/cpp/algorithms/ls_deconvolution.h @@ -0,0 +1,63 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef RADLER_ALGORITHMS_LSDECONVOLUTION_H_ +#define RADLER_ALGORITHMS_LSDECONVOLUTION_H_ + +#include <memory> +#include <string> + +#include <aocommon/uvector.h> + +#include "image_set.h" +#include "algorithms/deconvolution_algorithm.h" + +// TODO: LSDeconvolution algorithm is currently in +// a somewhat experimental stage and is not even compiled. + +namespace radler::algorithms { +struct LSDeconvolutionData; + +class LSDeconvolution : public DeconvolutionAlgorithm { + public: + LSDeconvolution(); + ~LSDeconvolution(); + + LSDeconvolution(const LSDeconvolution& source); + + float ExecuteMajorIteration(ImageSet& dataImage, ImageSet& modelImage, + const std::vector<aocommon::Image>& psfImages, + bool& reachedMajorThreshold) final override { + ExecuteMajorIteration(dataImage[0], modelImage[0], psfImages[0], + dataImage.Width(), dataImage.Height(), + reachedMajorThreshold); + return 0.0; + } + + std::unique_ptr<DeconvolutionAlgorithm> Clone() const final override { + return std::make_unique<LSDeconvolution>(*this); + } + + void ExecuteMajorIteration(double* dataImage, double* modelImage, + const double* psfImage, size_t width, + size_t height, bool& reachedMajorThreshold) { + nonLinearFit(dataImage, modelImage, psfImage, width, height, + reachedMajorThreshold); + } + + private: + void getMaskPositions( + aocommon::UVector<std::pair<size_t, size_t>>& maskPositions, + const bool* mask, size_t width, size_t height); + + void linearFit(double* dataImage, double* modelImage, const double* psfImage, + size_t width, size_t height, bool& reachedMajorThreshold); + + void nonLinearFit(double* dataImage, double* modelImage, + const double* psfImage, size_t width, size_t height, + bool& reachedMajorThreshold); + + std::unique_ptr<LSDeconvolutionData> _data; +}; +} // namespace radler::algorithms +#endif diff --git a/cpp/algorithms/more_sane.cc b/cpp/algorithms/more_sane.cc new file mode 100644 index 00000000..ca85b176 --- /dev/null +++ b/cpp/algorithms/more_sane.cc @@ -0,0 +1,87 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "algorithms/more_sane.h" + +#include <aocommon/image.h> +#include <aocommon/fits/fitsreader.h> +#include <aocommon/fits/fitswriter.h> +#include <aocommon/logger.h> + +#include <schaapcommon/fft/convolution.h> + +#include "utils/application.h" + +namespace radler::algorithms { +void MoreSane::ExecuteMajorIteration(float* residualData, float* modelData, + const aocommon::Image& psfImage) { + const size_t width = psfImage.Width(); + const size_t height = psfImage.Height(); + if (_iterationNumber != 0) { + aocommon::Logger::Info << "Convolving model with psf...\n"; + aocommon::Image preparedPsf(width, height); + schaapcommon::fft::PrepareConvolutionKernel( + preparedPsf.Data(), psfImage.Data(), width, height, _threadCount); + schaapcommon::fft::Convolve(modelData, preparedPsf.Data(), width, height, + _threadCount); + aocommon::Logger::Info << "Adding model back to residual...\n"; + for (size_t i = 0; i != width * height; ++i) + residualData[i] += modelData[i]; + } + std::ostringstream outputStr; + outputStr << _prefixName << "-tmp-moresaneoutput" << _iterationNumber; + const std::string dirtyName(_prefixName + "-tmp-moresaneinput-dirty.fits"), + psfName(_prefixName + "-tmp-moresaneinput-psf.fits"), + maskName(_prefixName + "-tmp-moresaneinput-mask.fits"), + outputName(outputStr.str()); + aocommon::FitsWriter writer; + writer.SetImageDimensions(width, height); + if (_cleanMask != nullptr) writer.WriteMask(maskName, _cleanMask); + writer.Write(dirtyName, residualData); + writer.Write(psfName, psfImage.Data()); + + std::ostringstream commandLine; + commandLine << "time python \"" << _moresaneLocation << "\" "; + if (!_allowNegativeComponents) commandLine << "-ep "; + if (_cleanMask != nullptr) commandLine << "-m \"" << maskName + "\" "; + if (!_moresaneArguments.empty()) commandLine << _moresaneArguments << ' '; + commandLine << "\"" << dirtyName << "\" \"" << psfName << "\" \"" + << outputName << '\"'; + if (!_moresaneSigmaLevels.empty()) { + commandLine << " -sl " + << _moresaneSigmaLevels[std::min( + _iterationNumber, _moresaneSigmaLevels.size() - 1)] + << " "; + } + + utils::Application::Run(commandLine.str()); + + aocommon::FitsReader modelReader(outputName + "_model.fits"); + modelReader.Read(modelData); + aocommon::FitsReader residualReader(outputName + "_residual.fits"); + residualReader.Read(residualData); + + unlink(dirtyName.c_str()); + unlink(psfName.c_str()); + unlink(maskName.c_str()); + unlink((outputName + "_model.fits").c_str()); + unlink((outputName + "_residual.fits").c_str()); +} + +float MoreSane::ExecuteMajorIteration( + ImageSet& dataImage, ImageSet& modelImage, + const std::vector<aocommon::Image>& psfImages, + bool& reachedMajorThreshold) { + for (size_t i = 0; i != dataImage.size(); ++i) { + float* residualData = dataImage.Data(i); + float* modelData = modelImage.Data(i); + const aocommon::Image psfImage = psfImages[dataImage.PSFIndex(i)]; + ExecuteMajorIteration(residualData, modelData, psfImage); + } + + ++_iterationNumber; + + reachedMajorThreshold = _iterationNumber < _maxIter; + return 0.0; +} +} // namespace radler::algorithms \ No newline at end of file diff --git a/cpp/algorithms/more_sane.h b/cpp/algorithms/more_sane.h new file mode 100644 index 00000000..78085bd8 --- /dev/null +++ b/cpp/algorithms/more_sane.h @@ -0,0 +1,42 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef RADLER_ALGORITHMS_MORESANE_H_ +#define RADLER_ALGORITHMS_MORESANE_H_ + +#include <string> + +#include "image_set.h" +#include "algorithms/deconvolution_algorithm.h" + +namespace radler::algorithms { +class MoreSane : public DeconvolutionAlgorithm { + public: + MoreSane(const std::string& moreSaneLocation, + const std::string& moresaneArguments, + const std::vector<double>& moresaneSigmaLevels, + const std::string& prefixName) + : _moresaneLocation(moreSaneLocation), + _moresaneArguments(moresaneArguments), + _moresaneSigmaLevels(moresaneSigmaLevels), + _prefixName(prefixName) {} + + float ExecuteMajorIteration(ImageSet& dataImage, ImageSet& modelImage, + const std::vector<aocommon::Image>& psfImages, + bool& reachedMajorThreshold) final override; + + virtual std::unique_ptr<DeconvolutionAlgorithm> Clone() const final override { + return std::unique_ptr<DeconvolutionAlgorithm>(new MoreSane(*this)); + } + + void ExecuteMajorIteration(float* residualData, float* modelData, + const aocommon::Image& psfImage); + + private: + const std::string _moresaneLocation, _moresaneArguments; + + const std::vector<double> _moresaneSigmaLevels; + const std::string _prefixName; +}; +} // namespace radler::algorithms +#endif // RADLER_ALGORITHMS_MORESANE_H_ diff --git a/cpp/algorithms/multiscale/multiscale_transforms.cc b/cpp/algorithms/multiscale/multiscale_transforms.cc new file mode 100644 index 00000000..5f48ae0f --- /dev/null +++ b/cpp/algorithms/multiscale/multiscale_transforms.cc @@ -0,0 +1,39 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "algorithms/multiscale/multiscale_transforms.h" + +#include <schaapcommon/fft/convolution.h> + +using aocommon::Image; + +namespace radler::algorithms::multiscale { + +void MultiScaleTransforms::Transform(std::vector<Image>& images, Image& scratch, + float scale) { + size_t kernelSize; + Image shape = MakeShapeFunction(scale, kernelSize); + + scratch = 0.0; + + schaapcommon::fft::PrepareSmallConvolutionKernel( + scratch.Data(), _width, _height, shape.Data(), kernelSize, _threadCount); + for (Image& image : images) + schaapcommon::fft::Convolve(image.Data(), scratch.Data(), _width, _height, + _threadCount); +} + +void MultiScaleTransforms::PrepareTransform(float* kernel, float scale) { + size_t kernelSize; + Image shape = MakeShapeFunction(scale, kernelSize); + + std::fill_n(kernel, _width * _height, 0.0); + + schaapcommon::fft::PrepareSmallConvolutionKernel( + kernel, _width, _height, shape.Data(), kernelSize, _threadCount); +} + +void MultiScaleTransforms::FinishTransform(float* image, const float* kernel) { + schaapcommon::fft::Convolve(image, kernel, _width, _height, _threadCount); +} +} // namespace radler::algorithms::multiscale diff --git a/cpp/algorithms/multiscale/multiscale_transforms.h b/cpp/algorithms/multiscale/multiscale_transforms.h new file mode 100644 index 00000000..9f019dc8 --- /dev/null +++ b/cpp/algorithms/multiscale/multiscale_transforms.h @@ -0,0 +1,196 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef RADLER_ALGORITHMS_MULTISCALE_MULTISCALE_TRANSFORMS_H_ +#define RADLER_ALGORITHMS_MULTISCALE_MULTISCALE_TRANSFORMS_H_ + +#include <cmath> +#include <initializer_list> + +#include <aocommon/image.h> +#include <aocommon/uvector.h> + +#include <vector> + +namespace radler::algorithms::multiscale { + +enum class Shape { TaperedQuadraticShape, GaussianShape }; + +class MultiScaleTransforms { + public: + MultiScaleTransforms(size_t width, size_t height, Shape shape) + : _width(width), _height(height), _shape(shape), _threadCount(1) {} + + void PrepareTransform(float* kernel, float scale); + void FinishTransform(float* image, const float* kernel); + + void Transform(aocommon::Image& image, aocommon::Image& scratch, + float scale) { + std::vector<aocommon::Image> images(1, std::move(image)); + Transform(images, scratch, scale); + image = std::move(images[0]); + } + + void Transform(std::vector<aocommon::Image>& images, aocommon::Image& scratch, + float scale); + + size_t Width() const { return _width; } + size_t Height() const { return _height; } + + static float KernelIntegratedValue(float scaleInPixels, size_t maxN, + Shape shape) { + size_t n; + aocommon::Image kernel = MakeShapeFunction(scaleInPixels, n, maxN, shape); + + float value = 0.0; + for (float& x : kernel) value += x; + + return value; + } + + static float KernelPeakValue(double scaleInPixels, size_t maxN, Shape shape) { + size_t n; + aocommon::Image kernel = MakeShapeFunction(scaleInPixels, n, maxN, shape); + return kernel[n / 2 + (n / 2) * n]; + } + + static void AddShapeComponent(float* image, size_t width, size_t height, + float scaleSizeInPixels, size_t x, size_t y, + float gain, Shape shape) { + size_t n; + aocommon::Image kernel = + MakeShapeFunction(scaleSizeInPixels, n, std::min(width, height), shape); + int left; + if (x > n / 2) + left = x - n / 2; + else + left = 0; + int top; + if (y > n / 2) + top = y - n / 2; + else + top = 0; + size_t right = std::min(x + (n + 1) / 2, width); + size_t bottom = std::min(y + (n + 1) / 2, height); + for (size_t yi = top; yi != bottom; ++yi) { + float* imagePtr = &image[yi * width]; + const float* kernelPtr = + &kernel.Data()[(yi + n / 2 - y) * n + left + n / 2 - x]; + for (size_t xi = left; xi != right; ++xi) { + imagePtr[xi] += *kernelPtr * gain; + ++kernelPtr; + } + } + } + + static aocommon::Image MakeShapeFunction(float scaleSizeInPixels, size_t& n, + size_t maxN, Shape shape) { + switch (shape) { + default: + case Shape::TaperedQuadraticShape: + return makeTaperedQuadraticShapeFunction(scaleSizeInPixels, n); + case Shape::GaussianShape: + return makeGaussianFunction(scaleSizeInPixels, n, maxN); + } + } + + aocommon::Image MakeShapeFunction(float scaleSizeInPixels, size_t& n) { + return MakeShapeFunction(scaleSizeInPixels, n, std::min(_width, _height), + _shape); + } + + static float GaussianSigma(float scaleSizeInPixels) { + return scaleSizeInPixels * (3.0 / 16.0); + } + + void SetThreadCount(size_t threadCount) { _threadCount = threadCount; } + + private: + size_t _width, _height; + enum Shape _shape; + size_t _threadCount; + + static size_t taperedQuadraticKernelSize(double scaleInPixels) { + return size_t(ceil(scaleInPixels * 0.5) * 2.0) + 1; + } + + static aocommon::Image makeTaperedQuadraticShapeFunction( + double scaleSizeInPixels, size_t& n) { + n = taperedQuadraticKernelSize(scaleSizeInPixels); + aocommon::Image output(n, n); + taperedQuadraticShapeFunction(n, output, scaleSizeInPixels); + return output; + } + + static aocommon::Image makeGaussianFunction(double scaleSizeInPixels, + size_t& n, size_t maxN) { + float sigma = GaussianSigma(scaleSizeInPixels); + + n = int(ceil(sigma * 12.0 / 2.0)) * 2 + 1; // bounding box of 12 sigma + if (n > maxN) { + n = maxN; + if ((n % 2) == 0 && n > 0) --n; + } + if (n < 1) n = 1; + if (sigma == 0.0) { + sigma = 1.0; + n = 1; + } + aocommon::Image output(n, n); + const float mu = int(n / 2); + const float twoSigmaSquared = 2.0 * sigma * sigma; + float sum = 0.0; + float* outputPtr = output.Data(); + aocommon::UVector<float> gaus(n); + for (int i = 0; i != int(n); ++i) { + float vI = float(i) - mu; + gaus[i] = std::exp(-vI * vI / twoSigmaSquared); + } + for (int y = 0; y != int(n); ++y) { + for (int x = 0; x != int(n); ++x) { + *outputPtr = gaus[x] * gaus[y]; + sum += *outputPtr; + ++outputPtr; + } + } + float normFactor = 1.0 / sum; + output *= normFactor; + return output; + } + + static void taperedQuadraticShapeFunction(size_t n, aocommon::Image& output2d, + double scaleSizeInPixels) { + if (scaleSizeInPixels == 0.0) + output2d[0] = 1.0; + else { + float sum = 0.0; + float* outputPtr = output2d.Data(); + for (int y = 0; y != int(n); ++y) { + float dy = y - 0.5 * (n - 1); + float dydy = dy * dy; + for (int x = 0; x != int(n); ++x) { + float dx = x - 0.5 * (n - 1); + float r = std::sqrt(dx * dx + dydy); + *outputPtr = + hannWindowFunction(r, n) * shapeFunction(r / scaleSizeInPixels); + sum += *outputPtr; + ++outputPtr; + } + } + float normFactor = 1.0 / sum; + output2d *= normFactor; + } + } + + static float hannWindowFunction(float x, size_t n) { + return (x * 2 <= n + 1) + ? (0.5 * (1.0 + std::cos(2.0 * M_PI * x / double(n + 1)))) + : 0.0; + } + + static float shapeFunction(float x) { + return (x < 1.0) ? (1.0 - x * x) : 0.0; + } +}; +} // namespace radler::algorithms::multiscale +#endif // RADLER_ALGORITHMS_MULTISCALE_MULTISCALE_TRANSFORMS_H_ diff --git a/cpp/algorithms/multiscale_algorithm.cc b/cpp/algorithms/multiscale_algorithm.cc new file mode 100644 index 00000000..a9102dcd --- /dev/null +++ b/cpp/algorithms/multiscale_algorithm.cc @@ -0,0 +1,711 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "algorithms/multiscale_algorithm.h" + +#include <optional> + +#include <aocommon/image.h> +#include <aocommon/logger.h> +#include <aocommon/units/fluxdensity.h> + +#include "component_list.h" +#include "algorithms/subminor_loop.h" +#include "math/peak_finder.h" +#include "multiscale/multiscale_transforms.h" + +using aocommon::Image; +using aocommon::Logger; +using aocommon::units::FluxDensity; + +namespace radler::algorithms { + +MultiScaleAlgorithm::MultiScaleAlgorithm(double beamSize, double pixelScaleX, + double pixelScaleY) + : _convolutionPadding(1.1), + _beamSizeInPixels(beamSize / std::max(pixelScaleX, pixelScaleY)), + _multiscaleScaleBias(0.6), + _multiscaleGain(0.2), + _scaleShape(multiscale::Shape::TaperedQuadraticShape), + _maxScales(0), + _trackPerScaleMasks(false), + _usePerScaleMasks(false), + _fastSubMinorLoop(true), + _trackComponents(false) { + if (_beamSizeInPixels <= 0.0) _beamSizeInPixels = 1; +} + +MultiScaleAlgorithm::~MultiScaleAlgorithm() { + aocommon::Logger::Info << "Multi-scale cleaning summary:\n"; + size_t sumComponents = 0; + float sumFlux = 0.0; + for (size_t scaleIndex = 0; scaleIndex != _scaleInfos.size(); ++scaleIndex) { + const ScaleInfo& scaleEntry = _scaleInfos[scaleIndex]; + aocommon::Logger::Info << "- Scale " << round(scaleEntry.scale) + << " px, nr of components cleaned: " + << scaleEntry.nComponentsCleaned << " (" + << FluxDensity::ToNiceString( + scaleEntry.totalFluxCleaned) + << ")\n"; + sumComponents += scaleEntry.nComponentsCleaned; + sumFlux += scaleEntry.totalFluxCleaned; + } + aocommon::Logger::Info << "Total: " << sumComponents << " components (" + << FluxDensity::ToNiceString(sumFlux) << ")\n"; +} + +float MultiScaleAlgorithm::ExecuteMajorIteration( + ImageSet& dirtySet, ImageSet& modelSet, + const std::vector<aocommon::Image>& psfs, bool& reachedMajorThreshold) { + // Rough overview of the procedure: + // Convolve integrated image (all scales) + // Find integrated peak & scale + // Minor loop: + // - Convolve individual images at fixed scale + // - Subminor loop: + // - Measure individual peaks per individually convolved image + // - Subtract convolved PSF from individual images + // - Subtract twice convolved PSF from individually convolved images + // - Find integrated peak at fixed scale + // - Convolve integrated image (all scales) + // - Find integrated peak & scale + // + // (This excludes creating the convolved PSFs and twice-convolved PSFs + // at the appropriate moments). + + const size_t width = dirtySet.Width(); + const size_t height = dirtySet.Height(); + + if (_stopOnNegativeComponent) _allowNegativeComponents = true; + // The threads always need to be stopped at the end of this function, so we + // use a scoped unique ptr. + std::unique_ptr<ThreadedDeconvolutionTools> tools( + new ThreadedDeconvolutionTools(_threadCount)); + + initializeScaleInfo(std::min(width, height)); + + if (_trackPerScaleMasks) { + // Note that in a second round the nr of scales can be different (due to + // different width/height, e.g. caused by a different subdivision in + // parallel cleaning). + for (const aocommon::UVector<bool>& mask : _scaleMasks) { + if (mask.size() != width * height) + throw std::runtime_error( + "Invalid automask size in multiscale algorithm"); + } + while (_scaleMasks.size() < _scaleInfos.size()) { + _scaleMasks.emplace_back(width * height, false); + } + } + if (_trackComponents) { + if (_componentList == nullptr) + _componentList.reset(new ComponentList(width, height, _scaleInfos.size(), + dirtySet.size())); + else if (_componentList->Width() != width || + _componentList->Height() != height) { + throw std::runtime_error("Error in component list dimensions!"); + } + } + if (!_rmsFactorImage.Empty() && + (_rmsFactorImage.Width() != width || _rmsFactorImage.Height() != height)) + throw std::runtime_error("Error in RMS factor image dimensions!"); + + bool hasHitThresholdInSubLoop = false; + size_t thresholdCountdown = std::max(size_t(8), _scaleInfos.size() * 3 / 2); + + Image scratch, scratchB, integratedScratch; + // scratch and scratchB are used by the subminorloop, which convolves the + // images and requires therefore more space. This space depends on the scale, + // so here the required size for the largest scale is calculated. + size_t scratchWidth, scratchHeight; + getConvolutionDimensions(_scaleInfos.size() - 1, width, height, scratchWidth, + scratchHeight); + scratch = Image(scratchWidth, scratchHeight); + scratchB = Image(scratchWidth, scratchHeight); + integratedScratch = Image(width, height); + std::unique_ptr<std::unique_ptr<Image[]>[]> convolvedPSFs( + new std::unique_ptr<Image[]>[dirtySet.PSFCount()]); + dirtySet.GetIntegratedPSF(integratedScratch, psfs); + convolvePSFs(convolvedPSFs[0], integratedScratch, scratch, true); + + // If there's only one, the integrated equals the first, so we can skip this + if (dirtySet.PSFCount() > 1) { + for (size_t i = 0; i != dirtySet.PSFCount(); ++i) { + convolvePSFs(convolvedPSFs[i], psfs[i], scratch, false); + } + } + + multiscale::MultiScaleTransforms msTransforms(width, height, _scaleShape); + msTransforms.SetThreadCount(_threadCount); + + size_t scaleWithPeak; + findActiveScaleConvolvedMaxima(dirtySet, integratedScratch, scratch, true, + tools.get()); + if (!selectMaximumScale(scaleWithPeak)) { + _logReceiver->Warn << "No peak found during multi-scale cleaning! Aborting " + "deconvolution.\n"; + reachedMajorThreshold = false; + return 0.0; + } + + bool isFinalThreshold = false; + float mGainThreshold = + std::fabs(_scaleInfos[scaleWithPeak].maxUnnormalizedImageValue * + _scaleInfos[scaleWithPeak].biasFactor) * + (1.0 - _mGain); + mGainThreshold = std::max(mGainThreshold, MajorIterThreshold()); + float firstThreshold = mGainThreshold; + if (_threshold > firstThreshold) { + firstThreshold = _threshold; + isFinalThreshold = true; + } + + _logReceiver->Info + << "Starting multi-scale cleaning. Start peak=" + << FluxDensity::ToNiceString( + _scaleInfos[scaleWithPeak].maxUnnormalizedImageValue * + _scaleInfos[scaleWithPeak].biasFactor) + << ", major iteration threshold=" + << FluxDensity::ToNiceString(firstThreshold); + if (isFinalThreshold) _logReceiver->Info << " (final)"; + _logReceiver->Info << '\n'; + + ImageSet individualConvolvedImages(dirtySet, width, height); + + // + // The minor iteration loop + // + while (_iterationNumber < MaxNIter() && + std::fabs(_scaleInfos[scaleWithPeak].maxUnnormalizedImageValue * + _scaleInfos[scaleWithPeak].biasFactor) > firstThreshold && + (!StopOnNegativeComponents() || + _scaleInfos[scaleWithPeak].maxUnnormalizedImageValue >= 0.0) && + thresholdCountdown > 0) { + // Create double-convolved PSFs & individually convolved images for this + // scale + std::vector<Image> transformList; + transformList.reserve(dirtySet.PSFCount() + dirtySet.size()); + for (size_t i = 0; i != dirtySet.PSFCount(); ++i) { + transformList.push_back(convolvedPSFs[i][scaleWithPeak]); + } + for (size_t i = 0; i != dirtySet.size(); ++i) { + transformList.emplace_back(width, height); + std::copy_n(dirtySet.Data(i), width * height, + transformList.back().Data()); + } + if (_scaleInfos[scaleWithPeak].scale != 0.0) { + msTransforms.Transform(transformList, scratch, + _scaleInfos[scaleWithPeak].scale); + } + + std::vector<Image> twiceConvolvedPSFs; + twiceConvolvedPSFs.reserve(dirtySet.PSFCount()); + for (size_t i = 0; i != dirtySet.PSFCount(); ++i) { + twiceConvolvedPSFs.emplace_back(std::move(transformList[i])); + } + for (size_t i = 0; i != dirtySet.size(); ++i) { + individualConvolvedImages.SetImage( + i, std::move(transformList[i + dirtySet.PSFCount()])); + } + + // + // The sub-minor iteration loop for this scale + // + float subIterationGainThreshold = + std::fabs(_scaleInfos[scaleWithPeak].maxUnnormalizedImageValue * + _scaleInfos[scaleWithPeak].biasFactor) * + (1.0 - _multiscaleGain); + float firstSubIterationThreshold = subIterationGainThreshold; + if (firstThreshold > firstSubIterationThreshold) { + firstSubIterationThreshold = firstThreshold; + if (!hasHitThresholdInSubLoop) { + _logReceiver->Info << "Subminor loop is near minor loop threshold. " + "Initiating countdown.\n"; + hasHitThresholdInSubLoop = true; + } + thresholdCountdown--; + _logReceiver->Info << '(' << thresholdCountdown << ") "; + } + // TODO we could chose to run the non-fast loop until we hit e.g. 10 + // iterations in a scale, because the fast loop takes more constant time and + // is only efficient when doing many iterations. + if (_fastSubMinorLoop) { + size_t subMinorStartIteration = _iterationNumber; + size_t convolutionWidth, convolutionHeight; + getConvolutionDimensions(scaleWithPeak, width, height, convolutionWidth, + convolutionHeight); + SubMinorLoop subLoop(width, height, convolutionWidth, convolutionHeight, + *_logReceiver); + subLoop.SetIterationInfo(_iterationNumber, MaxNIter()); + subLoop.SetThreshold( + firstSubIterationThreshold / _scaleInfos[scaleWithPeak].biasFactor, + subIterationGainThreshold / _scaleInfos[scaleWithPeak].biasFactor); + subLoop.SetGain(_scaleInfos[scaleWithPeak].gain); + subLoop.SetAllowNegativeComponents(AllowNegativeComponents()); + subLoop.SetStopOnNegativeComponent(StopOnNegativeComponents()); + subLoop.SetThreadCount(_threadCount); + const size_t scaleBorder = + size_t(ceil(_scaleInfos[scaleWithPeak].scale * 0.5)), + horBorderSize = std::max<size_t>( + round(width * _cleanBorderRatio), scaleBorder), + vertBorderSize = std::max<size_t>( + round(height * _cleanBorderRatio), scaleBorder); + subLoop.SetCleanBorders(horBorderSize, vertBorderSize); + if (!_rmsFactorImage.Empty()) subLoop.SetRMSFactorImage(_rmsFactorImage); + if (_usePerScaleMasks) + subLoop.SetMask(_scaleMasks[scaleWithPeak].data()); + else if (_cleanMask) + subLoop.SetMask(_cleanMask); + subLoop.SetSpectralFitter(&Fitter()); + + subLoop.Run(individualConvolvedImages, twiceConvolvedPSFs); + + _iterationNumber = subLoop.CurrentIteration(); + _scaleInfos[scaleWithPeak].nComponentsCleaned += + (_iterationNumber - subMinorStartIteration); + _scaleInfos[scaleWithPeak].totalFluxCleaned += subLoop.FluxCleaned(); + + for (size_t imageIndex = 0; imageIndex != dirtySet.size(); ++imageIndex) { + // TODO this can be multi-threaded if each thread has its own + // temporaries + const aocommon::Image& psf = + convolvedPSFs[dirtySet.PSFIndex(imageIndex)][scaleWithPeak]; + subLoop.CorrectResidualDirty(scratch.Data(), scratchB.Data(), + integratedScratch.Data(), imageIndex, + dirtySet.Data(imageIndex), psf.Data()); + + subLoop.GetFullIndividualModel(imageIndex, scratch.Data()); + if (imageIndex == 0) { + if (_trackPerScaleMasks) + subLoop.UpdateAutoMask(_scaleMasks[scaleWithPeak].data()); + if (_trackComponents) + subLoop.UpdateComponentList(*_componentList, scaleWithPeak); + } + if (_scaleInfos[scaleWithPeak].scale != 0.0) { + std::vector<Image> transformList{std::move(scratch)}; + msTransforms.Transform(transformList, integratedScratch, + _scaleInfos[scaleWithPeak].scale); + scratch = std::move(transformList[0]); + } + float* model = modelSet.Data(imageIndex); + for (size_t i = 0; i != width * height; ++i) + model[i] += scratch.Data()[i]; + } + + } else { // don't use the Clark optimization + const ScaleInfo& maxScaleInfo = _scaleInfos[scaleWithPeak]; + while (_iterationNumber < MaxNIter() && + std::fabs(maxScaleInfo.maxUnnormalizedImageValue * + maxScaleInfo.biasFactor) > firstSubIterationThreshold && + (!StopOnNegativeComponents() || + _scaleInfos[scaleWithPeak].maxUnnormalizedImageValue >= 0.0)) { + aocommon::UVector<float> componentValues; + measureComponentValues(componentValues, scaleWithPeak, + individualConvolvedImages); + const size_t x = maxScaleInfo.maxImageValueX; + const size_t y = maxScaleInfo.maxImageValueY; + PerformSpectralFit(componentValues.data(), x, y); + + for (size_t imgIndex = 0; imgIndex != dirtySet.size(); ++imgIndex) { + // Subtract component from individual, non-deconvolved images + componentValues[imgIndex] = + componentValues[imgIndex] * maxScaleInfo.gain; + + const aocommon::Image& psf = + convolvedPSFs[dirtySet.PSFIndex(imgIndex)][scaleWithPeak]; + tools->SubtractImage(dirtySet.Data(imgIndex), psf, x, y, + componentValues[imgIndex]); + + // Subtract twice convolved PSFs from convolved images + tools->SubtractImage(individualConvolvedImages.Data(imgIndex), + twiceConvolvedPSFs[dirtySet.PSFIndex(imgIndex)], + x, y, componentValues[imgIndex]); + // TODO this is incorrect, but why is the residual without + // Cotton-Schwab still OK ? Should test + // tools->SubtractImage(individualConvolvedImages[imgIndex], psf, + // width, height, x, y, componentValues[imgIndex]); + + // Adjust model + addComponentToModel(modelSet, imgIndex, scaleWithPeak, + componentValues[imgIndex]); + } + if (_trackComponents) { + _componentList->Add(x, y, scaleWithPeak, componentValues.data()); + } + + // Find maximum for this scale + individualConvolvedImages.GetLinearIntegrated(integratedScratch); + findPeakDirect(integratedScratch, scratch, scaleWithPeak); + _logReceiver->Debug + << "Scale now " + << std::fabs(_scaleInfos[scaleWithPeak].maxUnnormalizedImageValue * + _scaleInfos[scaleWithPeak].biasFactor) + << '\n'; + + ++_iterationNumber; + } + } + + activateScales(scaleWithPeak); + + findActiveScaleConvolvedMaxima(dirtySet, integratedScratch, scratch, false, + tools.get()); + + if (!selectMaximumScale(scaleWithPeak)) { + _logReceiver->Warn << "No peak found in main loop of multi-scale " + "cleaning! Aborting deconvolution.\n"; + reachedMajorThreshold = false; + return 0.0; + } + + _logReceiver->Info + << "Iteration " << _iterationNumber << ", scale " + << round(_scaleInfos[scaleWithPeak].scale) << " px : " + << FluxDensity::ToNiceString( + _scaleInfos[scaleWithPeak].maxUnnormalizedImageValue * + _scaleInfos[scaleWithPeak].biasFactor) + << " at " << _scaleInfos[scaleWithPeak].maxImageValueX << ',' + << _scaleInfos[scaleWithPeak].maxImageValueY << '\n'; + } + + bool maxIterReached = _iterationNumber >= MaxNIter(), + negativeReached = + StopOnNegativeComponents() && + _scaleInfos[scaleWithPeak].maxUnnormalizedImageValue < 0.0; + // finalThresholdReached = + // std::fabs(_scaleInfos[scaleWithPeak].maxUnnormalizedImageValue * + // _scaleInfos[scaleWithPeak].biasFactor) <= _threshold; + + if (maxIterReached) + _logReceiver->Info << "Cleaning finished because maximum number of " + "iterations was reached.\n"; + else if (negativeReached) + _logReceiver->Info + << "Cleaning finished because a negative component was found.\n"; + else if (isFinalThreshold) + _logReceiver->Info + << "Cleaning finished because the final threshold was reached.\n"; + else + _logReceiver->Info << "Minor loop finished, continuing cleaning after " + "inversion/prediction round.\n"; + + reachedMajorThreshold = + !maxIterReached && !isFinalThreshold && !negativeReached; + return _scaleInfos[scaleWithPeak].maxUnnormalizedImageValue * + _scaleInfos[scaleWithPeak].biasFactor; +} + +void MultiScaleAlgorithm::initializeScaleInfo(size_t minWidthHeight) { + if (_manualScaleList.empty()) { + if (_scaleInfos.empty()) { + size_t scaleIndex = 0; + double scale = _beamSizeInPixels * 2.0; + do { + _scaleInfos.push_back(ScaleInfo()); + ScaleInfo& newEntry = _scaleInfos.back(); + if (scaleIndex == 0) + newEntry.scale = 0.0; + else + newEntry.scale = scale; + newEntry.kernelPeak = multiscale::MultiScaleTransforms::KernelPeakValue( + scale, minWidthHeight, _scaleShape); + + scale *= 2.0; + ++scaleIndex; + } while (scale < minWidthHeight * 0.5 && + (_maxScales == 0 || scaleIndex < _maxScales)); + } else { + while (!_scaleInfos.empty() && + _scaleInfos.back().scale >= minWidthHeight * 0.5) { + _logReceiver->Info + << "Scale size " << _scaleInfos.back().scale + << " does not fit in cleaning region: removing scale.\n"; + _scaleInfos.erase(_scaleInfos.begin() + _scaleInfos.size() - 1); + } + } + } + if (!_manualScaleList.empty() && _scaleInfos.empty()) { + std::sort(_manualScaleList.begin(), _manualScaleList.end()); + for (size_t scaleIndex = 0; scaleIndex != _manualScaleList.size(); + ++scaleIndex) { + _scaleInfos.push_back(ScaleInfo()); + ScaleInfo& newEntry = _scaleInfos.back(); + newEntry.scale = _manualScaleList[scaleIndex]; + newEntry.kernelPeak = multiscale::MultiScaleTransforms::KernelPeakValue( + newEntry.scale, minWidthHeight, _scaleShape); + } + } +} + +void MultiScaleAlgorithm::convolvePSFs(std::unique_ptr<Image[]>& convolvedPSFs, + const Image& psf, Image& scratch, + bool isIntegrated) { + multiscale::MultiScaleTransforms msTransforms(psf.Width(), psf.Height(), + _scaleShape); + msTransforms.SetThreadCount(_threadCount); + convolvedPSFs.reset(new Image[_scaleInfos.size()]); + if (isIntegrated) _logReceiver->Info << "Scale info:\n"; + const double firstAutoScaleSize = _beamSizeInPixels * 2.0; + for (size_t scaleIndex = 0; scaleIndex != _scaleInfos.size(); ++scaleIndex) { + ScaleInfo& scaleEntry = _scaleInfos[scaleIndex]; + + convolvedPSFs[scaleIndex] = psf; + + if (isIntegrated) { + if (scaleEntry.scale != 0.0) + msTransforms.Transform(convolvedPSFs[scaleIndex], scratch, + scaleEntry.scale); + + scaleEntry.psfPeak = + convolvedPSFs[scaleIndex] + [psf.Width() / 2 + (psf.Height() / 2) * psf.Width()]; + // We normalize this factor to 1 for scale 0, so: + // factor = (psf / kernel) / (psf0 / kernel0) = psf * kernel0 / (kernel * + // psf0) + // scaleEntry.biasFactor = std::max(1.0, + // scaleEntry.psfPeak * scaleInfos[0].kernelPeak / + // (scaleEntry.kernelPeak * scaleInfos[0].psfPeak)); + double expTerm; + if (scaleEntry.scale == 0.0 || _scaleInfos.size() < 2) + expTerm = 0.0; + else + expTerm = std::log2(scaleEntry.scale / firstAutoScaleSize); + scaleEntry.biasFactor = + std::pow(_multiscaleScaleBias, -double(expTerm)) * 1.0; + + // I tried this, but wasn't perfect: + // _gain * _scaleInfos[0].kernelPeak / scaleEntry.kernelPeak; + scaleEntry.gain = _gain / scaleEntry.psfPeak; + + scaleEntry.isActive = true; + + if (scaleEntry.scale == 0.0) { + convolvedPSFs[scaleIndex] = psf; + } + + _logReceiver->Info << "- Scale " << round(scaleEntry.scale) + << ", bias factor=" + << round(scaleEntry.biasFactor * 10.0) / 10.0 + << ", psfpeak=" << scaleEntry.psfPeak + << ", gain=" << scaleEntry.gain + << ", kernel peak=" << scaleEntry.kernelPeak << '\n'; + } else { + if (scaleEntry.scale != 0.0) + msTransforms.Transform(convolvedPSFs[scaleIndex], scratch, + scaleEntry.scale); + } + } +} + +void MultiScaleAlgorithm::findActiveScaleConvolvedMaxima( + const ImageSet& imageSet, Image& integratedScratch, Image& scratch, + bool reportRMS, ThreadedDeconvolutionTools* tools) { + multiscale::MultiScaleTransforms msTransforms(imageSet.Width(), + imageSet.Height(), _scaleShape); + // ImageBufferAllocator::Ptr convolvedImage; + //_allocator.Allocate(_width*_height, convolvedImage); + imageSet.GetLinearIntegrated(integratedScratch); + aocommon::UVector<float> transformScales; + aocommon::UVector<size_t> transformIndices; + std::vector<aocommon::UVector<bool>> transformScaleMasks; + for (size_t scaleIndex = 0; scaleIndex != _scaleInfos.size(); ++scaleIndex) { + ScaleInfo& scaleEntry = _scaleInfos[scaleIndex]; + if (scaleEntry.isActive) { + if (scaleEntry.scale == 0) { + // Don't convolve scale 0: this is the delta function scale + findPeakDirect(integratedScratch, scratch, scaleIndex); + if (reportRMS) + scaleEntry.rms = ThreadedDeconvolutionTools::RMS( + integratedScratch, imageSet.Width() * imageSet.Height()); + } else { + transformScales.push_back(scaleEntry.scale); + transformIndices.push_back(scaleIndex); + if (_usePerScaleMasks) + transformScaleMasks.push_back(_scaleMasks[scaleIndex]); + } + } + } + std::vector<ThreadedDeconvolutionTools::PeakData> results; + + tools->FindMultiScalePeak(&msTransforms, integratedScratch, transformScales, + results, _allowNegativeComponents, _cleanMask, + transformScaleMasks, _cleanBorderRatio, + _rmsFactorImage, reportRMS); + + for (size_t i = 0; i != results.size(); ++i) { + ScaleInfo& scaleEntry = _scaleInfos[transformIndices[i]]; + scaleEntry.maxNormalizedImageValue = + results[i].normalizedValue.value_or(0.0); + scaleEntry.maxUnnormalizedImageValue = + results[i].unnormalizedValue.value_or(0.0); + scaleEntry.maxImageValueX = results[i].x; + scaleEntry.maxImageValueY = results[i].y; + if (reportRMS) scaleEntry.rms = results[i].rms; + } + if (reportRMS) { + _logReceiver->Info << "RMS per scale: {"; + for (size_t scaleIndex = 0; scaleIndex != _scaleInfos.size(); + ++scaleIndex) { + ScaleInfo& scaleEntry = _scaleInfos[scaleIndex]; + if (scaleIndex != 0) _logReceiver->Info << ", "; + _logReceiver->Info << round(scaleEntry.scale) << ": " + << FluxDensity::ToNiceString(scaleEntry.rms); + } + _logReceiver->Info << "}\n"; + } +} + +bool MultiScaleAlgorithm::selectMaximumScale(size_t& scaleWithPeak) { + // Find max component + std::map<float, size_t> peakToScaleMap; + for (size_t i = 0; i != _scaleInfos.size(); ++i) { + if (_scaleInfos[i].isActive) { + float maxVal = std::fabs(_scaleInfos[i].maxUnnormalizedImageValue * + _scaleInfos[i].biasFactor); + peakToScaleMap.insert(std::make_pair(maxVal, i)); + } + } + if (peakToScaleMap.empty()) { + scaleWithPeak = size_t(-1); + return false; + } else { + std::map<float, size_t>::const_reverse_iterator mapIter = + peakToScaleMap.rbegin(); + scaleWithPeak = mapIter->second; + return true; + } +} + +void MultiScaleAlgorithm::activateScales(size_t scaleWithLastPeak) { + for (size_t i = 0; i != _scaleInfos.size(); ++i) { + bool doActivate = + i == scaleWithLastPeak || + /*i == runnerUp ||*/ std::fabs( + _scaleInfos[i].maxUnnormalizedImageValue) * + _scaleInfos[i].biasFactor > + std::fabs( + _scaleInfos[scaleWithLastPeak].maxUnnormalizedImageValue) * + (1.0 - _gain) * _scaleInfos[scaleWithLastPeak].biasFactor; + if (!_scaleInfos[i].isActive && doActivate) { + _logReceiver->Debug << "Scale " << _scaleInfos[i].scale + << " is now significant and is activated.\n"; + _scaleInfos[i].isActive = true; + } else if (_scaleInfos[i].isActive && !doActivate) { + _logReceiver->Debug << "Scale " << _scaleInfos[i].scale + << " is insignificant and is deactivated.\n"; + _scaleInfos[i].isActive = false; + } + } +} + +void MultiScaleAlgorithm::measureComponentValues( + aocommon::UVector<float>& componentValues, size_t scaleIndex, + ImageSet& imageSet) { + const ScaleInfo& scale = _scaleInfos[scaleIndex]; + componentValues.resize(imageSet.size()); + _logReceiver->Debug << "Measuring " << scale.maxImageValueX << ',' + << scale.maxImageValueY << ", scale " << scale.scale + << ", integrated=" << scale.maxUnnormalizedImageValue + << ":"; + for (size_t i = 0; i != imageSet.size(); ++i) { + componentValues[i] = imageSet[i][scale.maxImageValueX + + scale.maxImageValueY * imageSet.Width()]; + _logReceiver->Debug << ' ' << componentValues[i]; + } + _logReceiver->Debug << '\n'; +} + +void MultiScaleAlgorithm::addComponentToModel(ImageSet& modelSet, + size_t imgIndex, + size_t scaleWithPeak, + float componentValue) { + const size_t x = _scaleInfos[scaleWithPeak].maxImageValueX; + const size_t y = _scaleInfos[scaleWithPeak].maxImageValueY; + float* modelData = modelSet.Data(imgIndex); + if (_scaleInfos[scaleWithPeak].scale == 0.0) { + modelData[x + modelSet.Width() * y] += componentValue; + } else { + multiscale::MultiScaleTransforms::AddShapeComponent( + modelData, modelSet.Width(), modelSet.Height(), + _scaleInfos[scaleWithPeak].scale, x, y, componentValue, _scaleShape); + } + + _scaleInfos[scaleWithPeak].nComponentsCleaned++; + _scaleInfos[scaleWithPeak].totalFluxCleaned += componentValue; + + if (_trackPerScaleMasks) { + _scaleMasks[scaleWithPeak][x + modelSet.Width() * y] = true; + } +} + +void MultiScaleAlgorithm::findPeakDirect(const aocommon::Image& image, + aocommon::Image& scratch, + size_t scaleIndex) { + ScaleInfo& scaleInfo = _scaleInfos[scaleIndex]; + const size_t horBorderSize = std::round(image.Width() * _cleanBorderRatio); + const size_t vertBorderSize = std::round(image.Height() * _cleanBorderRatio); + const float* actualImage; + if (_rmsFactorImage.Empty()) { + actualImage = image.Data(); + } else { + scratch = image; + scratch *= _rmsFactorImage; + actualImage = scratch.Data(); + } + + std::optional<float> maxValue; + if (_usePerScaleMasks) + maxValue = math::PeakFinder::FindWithMask( + actualImage, image.Width(), image.Height(), scaleInfo.maxImageValueX, + scaleInfo.maxImageValueY, _allowNegativeComponents, 0, image.Height(), + _scaleMasks[scaleIndex].data(), horBorderSize, vertBorderSize); + else if (_cleanMask == nullptr) + maxValue = math::PeakFinder::Find( + actualImage, image.Width(), image.Height(), scaleInfo.maxImageValueX, + scaleInfo.maxImageValueY, _allowNegativeComponents, 0, image.Height(), + horBorderSize, vertBorderSize); + else + maxValue = math::PeakFinder::FindWithMask( + actualImage, image.Width(), image.Height(), scaleInfo.maxImageValueX, + scaleInfo.maxImageValueY, _allowNegativeComponents, 0, image.Height(), + _cleanMask, horBorderSize, vertBorderSize); + + scaleInfo.maxUnnormalizedImageValue = maxValue.value_or(0.0); + if (_rmsFactorImage.Empty()) + scaleInfo.maxNormalizedImageValue = maxValue.value_or(0.0); + else + scaleInfo.maxNormalizedImageValue = + maxValue.value_or(0.0) / + _rmsFactorImage[scaleInfo.maxImageValueX + + scaleInfo.maxImageValueY * image.Width()]; +} + +static size_t calculateGoodFFTSize(size_t n) { + size_t bestfac = 2 * n; + /* NOTE: Starting from f2=2 here instead from f2=1 as usual, because the + result needs to be even. */ + for (size_t f2 = 2; f2 < bestfac; f2 *= 2) + for (size_t f23 = f2; f23 < bestfac; f23 *= 3) + for (size_t f235 = f23; f235 < bestfac; f235 *= 5) + for (size_t f2357 = f235; f2357 < bestfac; f2357 *= 7) + if (f2357 >= n) bestfac = f2357; + return bestfac; +} + +void MultiScaleAlgorithm::getConvolutionDimensions( + size_t scaleIndex, size_t width, size_t height, size_t& width_result, + size_t& height_result) const { + double scale = _scaleInfos[scaleIndex].scale; + // The factor of 1.5 comes from some superficial experience with diverging + // runs. It's supposed to be a balance between diverging runs caused by + // insufficient padding on one hand, and taking up too much memory on the + // other. I've seen divergence when padding=1.1, width=1500, max scale=726 + // and conv width=1650. Divergence occurred on scale 363. Was solved with conv + // width=2250. 2250 = 1.1*(363*factor + 1500) --> factor = 1.5 And solved + // with conv width=2000. 2000 = 1.1*(363*factor + 1500) --> factor = 0.8 + width_result = ceil(_convolutionPadding * (scale * 1.5 + width)); + height_result = ceil(_convolutionPadding * (scale * 1.5 + height)); + width_result = calculateGoodFFTSize(width_result); + height_result = calculateGoodFFTSize(height_result); +} +} // namespace radler::algorithms \ No newline at end of file diff --git a/cpp/algorithms/multiscale_algorithm.h b/cpp/algorithms/multiscale_algorithm.h new file mode 100644 index 00000000..2877dfa8 --- /dev/null +++ b/cpp/algorithms/multiscale_algorithm.h @@ -0,0 +1,138 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef RADLER_ALGORITHMS_MULTISCALE_ALGORITHM_H_ +#define RADLER_ALGORITHMS_MULTISCALE_ALGORITHM_H_ + +#include <vector> + +#include <aocommon/cloned_ptr.h> +#include <aocommon/image.h> +#include <aocommon/uvector.h> + +#include "component_list.h" +#include "deconvolution_algorithm.h" +#include "image_set.h" +#include "algorithms/threaded_deconvolution_tools.h" +#include "algorithms/multiscale/multiscale_transforms.h" + +namespace radler::algorithms { + +class MultiScaleAlgorithm : public DeconvolutionAlgorithm { + public: + MultiScaleAlgorithm(double beamSize, double pixelScaleX, double pixelScaleY); + ~MultiScaleAlgorithm(); + + std::unique_ptr<DeconvolutionAlgorithm> Clone() const final override { + return std::make_unique<MultiScaleAlgorithm>(*this); + } + + void SetManualScaleList(const std::vector<double>& scaleList) { + _manualScaleList = scaleList; + } + + float ExecuteMajorIteration(ImageSet& dataImage, ImageSet& modelImage, + const std::vector<aocommon::Image>& psfImages, + bool& reachedMajorThreshold) final override; + + void SetAutoMaskMode(bool trackPerScaleMasks, bool usePerScaleMasks) { + _trackPerScaleMasks = trackPerScaleMasks; + _usePerScaleMasks = usePerScaleMasks; + } + void SetTrackComponents(bool trackComponents) { + _trackComponents = trackComponents; + } + void SetUseFastSubMinorLoop(bool fastSubMinorLoop) { + _fastSubMinorLoop = fastSubMinorLoop; + } + void SetMultiscaleScaleBias(float bias) { _multiscaleScaleBias = bias; } + void SetMultiscaleGain(float gain) { _multiscaleGain = gain; } + void SetConvolutionPadding(float padding) { + assert(padding >= 1.0); + _convolutionPadding = padding; + } + void SetShape(multiscale::Shape shape) { _scaleShape = shape; } + size_t ScaleCount() const { return _scaleInfos.size(); } + void ClearComponentList() { _componentList.reset(); } + ComponentList& GetComponentList() { return *_componentList; } + const ComponentList& GetComponentList() const { return *_componentList; } + float ScaleSize(size_t scaleIndex) const { + return _scaleInfos[scaleIndex].scale; + } + size_t GetScaleMaskCount() const { return _scaleMasks.size(); } + void SetScaleMaskCount(size_t n) { _scaleMasks.resize(n); } + aocommon::UVector<bool>& GetScaleMask(size_t index) { + return _scaleMasks[index]; + } + void SetMaxScales(size_t maxScales) { _maxScales = maxScales; } + + private: + float _convolutionPadding; + double _beamSizeInPixels; + float _multiscaleScaleBias; + float _multiscaleGain; + multiscale::Shape _scaleShape; + size_t _maxScales; + // ThreadedDeconvolutionTools* _tools; + + struct ScaleInfo { + ScaleInfo() + : scale(0.0), + psfPeak(0.0), + kernelPeak(0.0), + biasFactor(0.0), + gain(0.0), + maxNormalizedImageValue(0.0), + maxUnnormalizedImageValue(0.0), + rms(0.0), + maxImageValueX(0), + maxImageValueY(0), + isActive(false), + nComponentsCleaned(0), + totalFluxCleaned(0.0) {} + + float scale; + float psfPeak, kernelPeak, biasFactor, gain; + + /** + * The difference between the normalized and unnormalized value is + * that the unnormalized value is relative to the RMS factor. + */ + float maxNormalizedImageValue, maxUnnormalizedImageValue; + float rms; + size_t maxImageValueX, maxImageValueY; + bool isActive; + size_t nComponentsCleaned; + float totalFluxCleaned; + }; + std::vector<MultiScaleAlgorithm::ScaleInfo> _scaleInfos; + std::vector<double> _manualScaleList; + + bool _trackPerScaleMasks, _usePerScaleMasks, _fastSubMinorLoop, + _trackComponents; + std::vector<aocommon::UVector<bool>> _scaleMasks; + aocommon::cloned_ptr<ComponentList> _componentList; + + void initializeScaleInfo(size_t minWidthHeight); + void convolvePSFs(std::unique_ptr<aocommon::Image[]>& convolvedPSFs, + const aocommon::Image& psf, aocommon::Image& scratch, + bool isIntegrated); + void findActiveScaleConvolvedMaxima(const ImageSet& imageSet, + aocommon::Image& integratedScratch, + aocommon::Image& scratch, bool reportRMS, + ThreadedDeconvolutionTools* tools); + bool selectMaximumScale(size_t& scaleWithPeak); + void activateScales(size_t scaleWithLastPeak); + void measureComponentValues(aocommon::UVector<float>& componentValues, + size_t scaleIndex, ImageSet& imageSet); + void addComponentToModel(ImageSet& modelSet, size_t imgIndex, + size_t scaleWithPeak, float componentValue); + + void findPeakDirect(const aocommon::Image& image, aocommon::Image& scratch, + size_t scaleIndex); + + void getConvolutionDimensions(size_t scaleIndex, size_t width, size_t height, + size_t& width_out, size_t& height_out) const; +}; +} // namespace radler::algorithms +#endif // RADLER_ALGORITHMS_MULTISCALE_ALGORITHM_H_ diff --git a/cpp/algorithms/parallel_deconvolution.cc b/cpp/algorithms/parallel_deconvolution.cc new file mode 100644 index 00000000..910f24b9 --- /dev/null +++ b/cpp/algorithms/parallel_deconvolution.cc @@ -0,0 +1,436 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "algorithms/parallel_deconvolution.h" + +#include <aocommon/parallelfor.h> +#include <aocommon/units/fluxdensity.h> + +#include <schaapcommon/fft/convolution.h> + +#include "algorithms/multiscale_algorithm.h" +#include "math/dijkstrasplitter.h" + +using aocommon::Image; +using aocommon::Logger; + +namespace radler::algorithms { + +ParallelDeconvolution::ParallelDeconvolution( + const DeconvolutionSettings& deconvolutionSettings) + : _horImages(0), + _verImages(0), + _settings(deconvolutionSettings), + _allocator(nullptr), + _mask(nullptr), + _trackPerScaleMasks(false), + _usePerScaleMasks(false) { + // Make all FFTWF plan calls inside ParallelDeconvolution + // thread safe. + schaapcommon::fft::MakeFftwfPlannerThreadSafe(); +} + +ParallelDeconvolution::~ParallelDeconvolution() {} + +ComponentList ParallelDeconvolution::GetComponentList( + const DeconvolutionTable& table) const { + // TODO make this work with subimages + ComponentList list; + if (_settings.useMultiscale) { + // If no parallel deconvolution was used, the component list must be + // retrieved from the deconvolution algorithm. + if (_algorithms.size() == 1) { + list = static_cast<MultiScaleAlgorithm*>(_algorithms.front().get()) + ->GetComponentList(); + } else { + list = *_componentList; + } + } else { + const size_t w = _settings.trimmedImageWidth; + const size_t h = _settings.trimmedImageHeight; + ImageSet modelSet(table, _settings.squaredJoins, + _settings.linkedPolarizations, w, h); + modelSet.LoadAndAverage(false); + list = ComponentList(w, h, modelSet); + } + list.MergeDuplicates(); + return list; +} + +const DeconvolutionAlgorithm& ParallelDeconvolution::MaxScaleCountAlgorithm() + const { + if (_settings.useMultiscale) { + MultiScaleAlgorithm* maxAlgorithm = + static_cast<MultiScaleAlgorithm*>(_algorithms.front().get()); + for (size_t i = 1; i != _algorithms.size(); ++i) { + MultiScaleAlgorithm* mAlg = + static_cast<MultiScaleAlgorithm*>(_algorithms[i].get()); + if (mAlg->ScaleCount() > maxAlgorithm->ScaleCount()) { + maxAlgorithm = mAlg; + } + } + return *maxAlgorithm; + } else { + return FirstAlgorithm(); + } +} + +void ParallelDeconvolution::SetAlgorithm( + std::unique_ptr<DeconvolutionAlgorithm> algorithm) { + if (_settings.parallelDeconvolutionMaxSize == 0) { + _algorithms.resize(1); + _algorithms.front() = std::move(algorithm); + } else { + const size_t width = _settings.trimmedImageWidth; + const size_t height = _settings.trimmedImageHeight; + size_t maxSubImageSize = _settings.parallelDeconvolutionMaxSize; + _horImages = (width + maxSubImageSize - 1) / maxSubImageSize, + _verImages = (height + maxSubImageSize - 1) / maxSubImageSize; + _algorithms.resize(_horImages * _verImages); + _algorithms.front() = std::move(algorithm); + size_t threadsPerAlg = + (_settings.parallelDeconvolutionMaxThreads + _algorithms.size() - 1) / + _algorithms.size(); + _algorithms.front()->SetThreadCount(threadsPerAlg); + Logger::Debug << "Parallel deconvolution will use " << _algorithms.size() + << " subimages.\n"; + for (size_t i = 1; i != _algorithms.size(); ++i) + _algorithms[i] = _algorithms.front()->Clone(); + } +} + +void ParallelDeconvolution::SetRMSFactorImage(Image&& image) { + if (_settings.parallelDeconvolutionMaxSize == 0) + _algorithms.front()->SetRMSFactorImage(std::move(image)); + else + _rmsImage = std::move(image); +} + +void ParallelDeconvolution::SetThreshold(double threshold) { + for (auto& alg : _algorithms) alg->SetThreshold(threshold); +} + +void ParallelDeconvolution::SetAutoMaskMode(bool trackPerScaleMasks, + bool usePerScaleMasks) { + _trackPerScaleMasks = trackPerScaleMasks; + _usePerScaleMasks = usePerScaleMasks; + for (auto& alg : _algorithms) { + class MultiScaleAlgorithm& algorithm = + static_cast<class MultiScaleAlgorithm&>(*alg); + algorithm.SetAutoMaskMode(trackPerScaleMasks, usePerScaleMasks); + } +} + +void ParallelDeconvolution::SetCleanMask(const bool* mask) { + if (_algorithms.size() == 1) + _algorithms.front()->SetCleanMask(mask); + else + _mask = mask; +} + +void ParallelDeconvolution::SetSpectrallyForcedImages( + std::vector<Image>&& images) { + if (_algorithms.size() == 1) + _algorithms.front()->SetSpectrallyForcedImages(std::move(images)); + else + _spectrallyForcedImages = std::move(images); +} + +void ParallelDeconvolution::runSubImage( + SubImage& subImg, ImageSet& dataImage, const ImageSet& modelImage, + ImageSet& resultModel, const std::vector<aocommon::Image>& psfImages, + double majorIterThreshold, bool findPeakOnly, std::mutex& mutex) { + const size_t width = _settings.trimmedImageWidth; + const size_t height = _settings.trimmedImageHeight; + + std::unique_ptr<ImageSet> subModel, subData; + { + std::lock_guard<std::mutex> lock(mutex); + subData = dataImage.Trim(subImg.x, subImg.y, subImg.x + subImg.width, + subImg.y + subImg.height, width); + // Because the model of this subimage might extend outside of its boundaries + // (because of multiscale components), the model is placed back on the image + // by adding its values. This requires that values outside the boundary are + // set to zero at this point, otherwise multiple subimages could add the + // same sources. + subModel = modelImage.TrimMasked( + subImg.x, subImg.y, subImg.x + subImg.width, subImg.y + subImg.height, + width, subImg.boundaryMask.data()); + } + + // Construct the smaller psfs + std::vector<Image> subPsfs; + subPsfs.reserve(psfImages.size()); + for (size_t i = 0; i != psfImages.size(); ++i) { + subPsfs.emplace_back(psfImages[i].Trim(subImg.width, subImg.height)); + } + _algorithms[subImg.index]->SetCleanMask(subImg.mask.data()); + + // Construct smaller RMS image if necessary + if (!_rmsImage.Empty()) { + Image subRmsImage = + _rmsImage.TrimBox(subImg.x, subImg.y, subImg.width, subImg.height); + _algorithms[subImg.index]->SetRMSFactorImage(std::move(subRmsImage)); + } + + // If a forced spectral image is active, trim it to the subimage size + if (!_spectrallyForcedImages.empty()) { + std::vector<Image> subSpectralImages(_spectrallyForcedImages.size()); + for (size_t i = 0; i != _spectrallyForcedImages.size(); ++i) { + subSpectralImages[i] = _spectrallyForcedImages[i].TrimBox( + subImg.x, subImg.y, subImg.width, subImg.height); + } + _algorithms[subImg.index]->SetSpectrallyForcedImages( + std::move(subSpectralImages)); + } + + size_t maxNIter = _algorithms[subImg.index]->MaxNIter(); + if (findPeakOnly) + _algorithms[subImg.index]->SetMaxNIter(0); + else + _algorithms[subImg.index]->SetMajorIterThreshold(majorIterThreshold); + + if (_usePerScaleMasks || _trackPerScaleMasks) { + std::lock_guard<std::mutex> lock(mutex); + MultiScaleAlgorithm& msAlg = + static_cast<class MultiScaleAlgorithm&>(*_algorithms[subImg.index]); + // During the first iteration, msAlg will not have scales/masks yet and the + // nr scales has also not been determined yet. + if (!_scaleMasks.empty()) { + // Here we set the scale mask for the multiscale algorithm. + // The maximum number of scales in the previous iteration can be found by + // _scaleMasks.size() Not all msAlgs might have used that many scales, so + // we have to take this into account + msAlg.SetScaleMaskCount( + std::max(msAlg.GetScaleMaskCount(), _scaleMasks.size())); + for (size_t i = 0; i != msAlg.GetScaleMaskCount(); ++i) { + aocommon::UVector<bool>& output = msAlg.GetScaleMask(i); + output.assign(subImg.width * subImg.height, false); + if (i < _scaleMasks.size()) + Image::TrimBox(output.data(), subImg.x, subImg.y, subImg.width, + subImg.height, _scaleMasks[i].data(), width, height); + } + } + } + + subImg.peak = _algorithms[subImg.index]->ExecuteMajorIteration( + *subData, *subModel, subPsfs, subImg.reachedMajorThreshold); + + // Since this was an RMS image specifically for this subimage size, we free it + // immediately + _algorithms[subImg.index]->SetRMSFactorImage(Image()); + + if (_trackPerScaleMasks) { + std::lock_guard<std::mutex> lock(mutex); + MultiScaleAlgorithm& msAlg = + static_cast<class MultiScaleAlgorithm&>(*_algorithms[subImg.index]); + if (_scaleMasks.empty()) { + _scaleMasks.resize(msAlg.ScaleCount()); + for (aocommon::UVector<bool>& scaleMask : _scaleMasks) + scaleMask.assign(width * height, false); + } + for (size_t i = 0; i != msAlg.ScaleCount(); ++i) { + const aocommon::UVector<bool>& msMask = msAlg.GetScaleMask(i); + if (i < _scaleMasks.size()) + Image::CopyMasked(_scaleMasks[i].data(), subImg.x, subImg.y, width, + msMask.data(), subImg.width, subImg.height, + subImg.boundaryMask.data()); + } + } + + if (_settings.saveSourceList && _settings.useMultiscale) { + std::lock_guard<std::mutex> lock(mutex); + MultiScaleAlgorithm& algorithm = + static_cast<MultiScaleAlgorithm&>(*_algorithms[subImg.index]); + if (!_componentList) + _componentList.reset(new ComponentList( + width, height, algorithm.ScaleCount(), dataImage.size())); + _componentList->Add(algorithm.GetComponentList(), subImg.x, subImg.y); + algorithm.ClearComponentList(); + } + + if (findPeakOnly) { + _algorithms[subImg.index]->SetMaxNIter(maxNIter); + } else { + std::lock_guard<std::mutex> lock(mutex); + dataImage.CopyMasked(*subData, subImg.x, subImg.y, + subImg.boundaryMask.data()); + resultModel.AddSubImage(*subModel, subImg.x, subImg.y); + } +} + +void ParallelDeconvolution::ExecuteMajorIteration( + ImageSet& dataImage, ImageSet& modelImage, + const std::vector<aocommon::Image>& psfImages, + bool& reachedMajorThreshold) { + if (_algorithms.size() == 1) { + aocommon::ForwardingLogReceiver fwdReceiver; + _algorithms.front()->SetLogReceiver(fwdReceiver); + _algorithms.front()->ExecuteMajorIteration(dataImage, modelImage, psfImages, + reachedMajorThreshold); + } else { + executeParallelRun(dataImage, modelImage, psfImages, reachedMajorThreshold); + } +} + +void ParallelDeconvolution::executeParallelRun( + ImageSet& dataImage, ImageSet& modelImage, + const std::vector<aocommon::Image>& psfImages, + bool& reachedMajorThreshold) { + const size_t width = dataImage.Width(); + const size_t height = dataImage.Height(); + const size_t avgHSubImageSize = width / _horImages; + const size_t avgVSubImageSize = height / _verImages; + + Image image(width, height); + Image dividingLine(width, height, 0.0); + aocommon::UVector<bool> largeScratchMask(width * height); + dataImage.GetLinearIntegrated(image); + + math::DijkstraSplitter divisor(width, height); + + struct VerticalArea { + aocommon::UVector<bool> mask; + size_t x, width; + }; + std::vector<VerticalArea> verticalAreas(_horImages); + + Logger::Info << "Calculating edge paths...\n"; + aocommon::ParallelFor<size_t> splitLoop(_settings.threadCount); + + // Divide into columns (i.e. construct the vertical lines) + splitLoop.Run(1, _horImages, [&](size_t divNr, size_t) { + size_t splitStart = width * divNr / _horImages - avgHSubImageSize / 4, + splitEnd = width * divNr / _horImages + avgHSubImageSize / 4; + divisor.DivideVertically(image.Data(), dividingLine.Data(), splitStart, + splitEnd); + }); + for (size_t divNr = 0; divNr != _horImages; ++divNr) { + size_t midX = divNr * width / _horImages + avgHSubImageSize / 2; + VerticalArea& area = verticalAreas[divNr]; + divisor.FloodVerticalArea(dividingLine.Data(), midX, + largeScratchMask.data(), area.x, area.width); + area.mask.resize(area.width * height); + Image::TrimBox(area.mask.data(), area.x, 0, area.width, height, + largeScratchMask.data(), width, height); + } + + // Make the rows (horizontal lines) + dividingLine = 0.0f; + splitLoop.Run(1, _verImages, [&](size_t divNr, size_t) { + size_t splitStart = height * divNr / _verImages - avgVSubImageSize / 4, + splitEnd = height * divNr / _verImages + avgVSubImageSize / 4; + divisor.DivideHorizontally(image.Data(), dividingLine.Data(), splitStart, + splitEnd); + }); + + Logger::Info << "Calculating bounding boxes and submasks...\n"; + + // Find the bounding boxes and clean masks for each subimage + aocommon::UVector<bool> mask(width * height); + std::vector<SubImage> subImages; + for (size_t y = 0; y != _verImages; ++y) { + size_t midY = y * height / _verImages + avgVSubImageSize / 2; + size_t hAreaY, hAreaWidth; + divisor.FloodHorizontalArea(dividingLine.Data(), midY, + largeScratchMask.data(), hAreaY, hAreaWidth); + + for (size_t x = 0; x != _horImages; ++x) { + subImages.emplace_back(); + SubImage& subImage = subImages.back(); + subImage.index = subImages.size() - 1; + const VerticalArea& vArea = verticalAreas[x]; + divisor.GetBoundingMask(vArea.mask.data(), vArea.x, vArea.width, + largeScratchMask.data(), mask.data(), subImage.x, + subImage.y, subImage.width, subImage.height); + Logger::Debug << "Subimage " << subImages.size() << " at (" << subImage.x + << "," << subImage.y << ") - (" + << subImage.x + subImage.width << "," + << subImage.y + subImage.height << ")\n"; + subImage.mask.resize(subImage.width * subImage.height); + Image::TrimBox(subImage.mask.data(), subImage.x, subImage.y, + subImage.width, subImage.height, mask.data(), width, + height); + subImage.boundaryMask = subImage.mask; + // If a user mask is active, take the union of that mask with the boundary + // mask (note that 'mask' is reused as a scratch space) + if (_mask != nullptr) { + Image::TrimBox(mask.data(), subImage.x, subImage.y, subImage.width, + subImage.height, _mask, width, height); + for (size_t i = 0; i != subImage.mask.size(); ++i) + subImage.mask[i] = subImage.mask[i] && mask[i]; + } + } + } + verticalAreas.clear(); + + // Initialize loggers + std::mutex mutex; + _logs.Initialize(_horImages, _verImages); + for (size_t i = 0; i != _algorithms.size(); ++i) + _algorithms[i]->SetLogReceiver(_logs[i]); + + // Find the starting peak over all subimages + aocommon::ParallelFor<size_t> loop(_settings.parallelDeconvolutionMaxThreads); + ImageSet resultModel(modelImage, modelImage.Width(), modelImage.Height()); + resultModel = 0.0; + loop.Run(0, _algorithms.size(), [&](size_t index, size_t) { + _logs.Activate(index); + runSubImage(subImages[index], dataImage, modelImage, resultModel, psfImages, + 0.0, true, mutex); + _logs.Deactivate(index); + + _logs[index].Mute(false); + _logs[index].Info << "Sub-image " << index << " returned peak position.\n"; + _logs[index].Mute(true); + }); + double maxValue = 0.0; + size_t indexOfMax = 0; + for (SubImage& img : subImages) { + if (img.peak > maxValue) { + maxValue = img.peak; + indexOfMax = img.index; + } + } + Logger::Info << "Subimage " << (indexOfMax + 1) << " has maximum peak of " + << aocommon::units::FluxDensity::ToNiceString(maxValue) << ".\n"; + double mIterThreshold = maxValue * (1.0 - _settings.deconvolutionMGain); + + // Run the deconvolution + loop.Run(0, _algorithms.size(), [&](size_t index, size_t) { + _logs.Activate(index); + runSubImage(subImages[index], dataImage, modelImage, resultModel, psfImages, + mIterThreshold, false, mutex); + _logs.Deactivate(index); + + _logs[index].Mute(false); + _logs[index].Info << "Sub-image " << index + << " finished its deconvolution iteration.\n"; + _logs[index].Mute(true); + }); + modelImage.SetImages(std::move(resultModel)); + + _rmsImage.Reset(); + + size_t subImagesFinished = 0; + reachedMajorThreshold = false; + bool reachedMaxNIter = false; + for (SubImage& img : subImages) { + if (!img.reachedMajorThreshold) ++subImagesFinished; + if (_algorithms[img.index]->IterationNumber() >= + _algorithms[img.index]->MaxNIter()) + reachedMaxNIter = true; + } + Logger::Info << subImagesFinished << " / " << subImages.size() + << " sub-images finished"; + reachedMajorThreshold = (subImagesFinished != subImages.size()); + if (reachedMajorThreshold && !reachedMaxNIter) + Logger::Info << ": Continue next major iteration.\n"; + else if (reachedMajorThreshold && reachedMaxNIter) { + Logger::Info << ", but nr. of iterations reached at least once: " + "Deconvolution finished.\n"; + reachedMajorThreshold = false; + } else + Logger::Info << ": Deconvolution finished.\n"; +} +} // namespace radler::algorithms \ No newline at end of file diff --git a/cpp/algorithms/parallel_deconvolution.h b/cpp/algorithms/parallel_deconvolution.h new file mode 100644 index 00000000..22377f1b --- /dev/null +++ b/cpp/algorithms/parallel_deconvolution.h @@ -0,0 +1,105 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef RADLER_ALGORITHMS_PARALLEL_DECONVOLUTION_H_ +#define RADLER_ALGORITHMS_PARALLEL_DECONVOLUTION_H_ + +#include <memory> +#include <mutex> +#include <vector> + +#include <aocommon/image.h> +#include <aocommon/uvector.h> + +#include "component_list.h" +#include "deconvolution_settings.h" +#include "image_set.h" +#include "algorithms/deconvolution_algorithm.h" +#include "logging/subimage_logset.h" + +namespace radler::algorithms { + +class ParallelDeconvolution { + public: + ParallelDeconvolution(const DeconvolutionSettings& deconvolutionSettings); + + ~ParallelDeconvolution(); + + DeconvolutionAlgorithm& FirstAlgorithm() { return *_algorithms.front(); } + const DeconvolutionAlgorithm& FirstAlgorithm() const { + return *_algorithms.front(); + } + + ComponentList GetComponentList(const DeconvolutionTable& table) const; + + /** + * @brief Same as @c FirstAlgorithm , except that for a multi-scale clean + * the algorithm with the maximum number of scale counts is returned. + */ + const DeconvolutionAlgorithm& MaxScaleCountAlgorithm() const; + + void SetAllocator(class ImageBufferAllocator* allocator) { + _allocator = allocator; + } + + void SetAlgorithm(std::unique_ptr<DeconvolutionAlgorithm> algorithm); + + void SetRMSFactorImage(aocommon::Image&& image); + + void SetThreshold(double threshold); + + bool IsInitialized() const { return !_algorithms.empty(); } + + void SetAutoMaskMode(bool trackPerScaleMasks, bool usePerScaleMasks); + + void SetCleanMask(const bool* mask); + + void SetSpectrallyForcedImages(std::vector<aocommon::Image>&& images); + + void ExecuteMajorIteration(ImageSet& dataImage, ImageSet& modelImage, + const std::vector<aocommon::Image>& psfImages, + bool& reachedMajorThreshold); + + void FreeDeconvolutionAlgorithms() { + _algorithms.clear(); + _mask = nullptr; + } + + private: + void executeParallelRun(ImageSet& dataImage, ImageSet& modelImage, + const std::vector<aocommon::Image>& psfImages, + bool& reachedMajorThreshold); + + struct SubImage { + size_t index, x, y, width, height; + // Mask to be used during deconvoution (combines user mask with the + // boundary mask) + aocommon::UVector<bool> mask; + // Selects the pixels inside this subimage + aocommon::UVector<bool> boundaryMask; + double peak; + bool reachedMajorThreshold; + }; + + void runSubImage(SubImage& subImg, ImageSet& dataImage, + const ImageSet& modelImage, ImageSet& resultModel, + const std::vector<aocommon::Image>& psfImages, + double majorIterThreshold, bool findPeakOnly, + std::mutex& mutex); + + std::vector<std::unique_ptr<DeconvolutionAlgorithm>> _algorithms; + logging::SubImageLogSet _logs; + size_t _horImages; + size_t _verImages; + // Radler::_settings outlives ParallelDeconvolution::_settings + const DeconvolutionSettings& _settings; + ImageBufferAllocator* _allocator; + const bool* _mask; + std::vector<aocommon::Image> _spectrallyForcedImages; + bool _trackPerScaleMasks, _usePerScaleMasks; + std::vector<aocommon::UVector<bool>> _scaleMasks; + std::unique_ptr<class ComponentList> _componentList; + aocommon::Image _rmsImage; +}; +} // namespace radler::algorithms +#endif diff --git a/cpp/algorithms/python_deconvolution.cc b/cpp/algorithms/python_deconvolution.cc new file mode 100644 index 00000000..c5801c09 --- /dev/null +++ b/cpp/algorithms/python_deconvolution.cc @@ -0,0 +1,277 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "algorithms/python_deconvolution.h" + +#include <pybind11/attr.h> +#include <pybind11/embed.h> +#include <pybind11/eval.h> +#include <pybind11/numpy.h> +#include <pybind11/pybind11.h> +#include <pybind11/stl.h> + +namespace radler::algorithms { + +struct PyChannel { + double frequency, weight; +}; + +class PySpectralFitter { + public: + PySpectralFitter(schaapcommon::fitters::SpectralFitter& fitter) + : _fitter(fitter) {} + + pybind11::array_t<double> fit(pybind11::array_t<double> values, size_t x, + size_t y) { + if (values.ndim() != 1) + throw std::runtime_error( + "spectral_fitter.fit(): Invalid dimensions of values array"); + if (size_t(values.shape()[0]) != _fitter.NFrequencies()) + throw std::runtime_error( + "spectral_fitter.fit(): Incorrect size of values array"); + aocommon::UVector<float> vec(_fitter.NFrequencies()); + pybind11::buffer_info info = values.request(); + const unsigned char* buffer = static_cast<const unsigned char*>(info.ptr); + for (size_t i = 0; i != _fitter.NFrequencies(); ++i) { + vec[i] = *reinterpret_cast<const double*>(buffer + info.strides[0] * i); + } + std::vector<float> result; + _fitter.Fit(result, vec.data(), x, y); + + pybind11::buffer_info resultBuf( + nullptr, // ask NumPy to allocate + sizeof(double), pybind11::format_descriptor<double>::value, 1, + {ptrdiff_t(_fitter.NTerms())}, {sizeof(double)} // Stride + ); + pybind11::array_t<double> pyResult(resultBuf); + std::copy_n(result.data(), _fitter.NTerms(), + static_cast<double*>(pyResult.request(true).ptr)); + return pyResult; + } + + pybind11::array_t<double> fit_and_evaluate(pybind11::array_t<double> values, + size_t x, size_t y) { + if (values.ndim() != 1) + throw std::runtime_error( + "spectral_fitter.fit_and_evaluate(): Invalid dimensions of values " + "array"); + if (size_t(values.shape()[0]) != _fitter.NFrequencies()) + throw std::runtime_error( + "spectral_fitter.fit_and_evaluate(): Incorrect size of values array"); + aocommon::UVector<float> vec(_fitter.NFrequencies()); + pybind11::buffer_info info = values.request(); + const unsigned char* buffer = static_cast<const unsigned char*>(info.ptr); + for (size_t i = 0; i != _fitter.NFrequencies(); ++i) { + vec[i] = *reinterpret_cast<const double*>(buffer + info.strides[0] * i); + } + + std::vector<float> fittingScratch; + _fitter.FitAndEvaluate(vec.data(), x, y, fittingScratch); + + pybind11::buffer_info resultBuf( + nullptr, // ask NumPy to allocate + sizeof(double), pybind11::format_descriptor<double>::value, 1, + {ptrdiff_t(_fitter.NFrequencies())}, {sizeof(double)} // Stride + ); + pybind11::array_t<double> pyResult(resultBuf); + std::copy_n(vec.data(), _fitter.NFrequencies(), + static_cast<double*>(pyResult.request(true).ptr)); + return pyResult; + } + + private: + schaapcommon::fitters::SpectralFitter& _fitter; +}; + +struct PyMetaData { + public: + PyMetaData(schaapcommon::fitters::SpectralFitter& _spectral_fitter) + : spectral_fitter(_spectral_fitter) {} + + std::vector<PyChannel> channels; + size_t iteration_number; + double final_threshold; + double gain; + size_t max_iterations; + double major_iter_threshold; + double mgain; + PySpectralFitter spectral_fitter; + bool square_joined_channels; +}; + +PythonDeconvolution::PythonDeconvolution(const std::string& filename) + : _filename(filename), _guard(new pybind11::scoped_interpreter()) { + pybind11::module main = pybind11::module::import("__main__"); + pybind11::object scope = main.attr("__dict__"); + pybind11::eval_file(_filename, scope); + _deconvolveFunction = std::make_unique<pybind11::function>( + main.attr("deconvolve").cast<pybind11::function>()); + + pybind11::class_<PyChannel>(main, "Channel") + .def_readwrite("frequency", &PyChannel::frequency) + .def_readwrite("weight", &PyChannel::weight); + + pybind11::class_<PyMetaData>(main, "MetaData") + .def_readonly("channels", &PyMetaData::channels) + .def_readonly("final_threshold", &PyMetaData::final_threshold) + .def_readwrite("iteration_number", &PyMetaData::iteration_number) + .def_readonly("gain", &PyMetaData::gain) + .def_readonly("max_iterations", &PyMetaData::max_iterations) + .def_readonly("major_iter_threshold", &PyMetaData::major_iter_threshold) + .def_readonly("mgain", &PyMetaData::mgain) + .def_readonly("spectral_fitter", &PyMetaData::spectral_fitter) + .def_readonly("square_joined_channels", + &PyMetaData::square_joined_channels); + + pybind11::class_<PySpectralFitter>(main, "SpectralFitter") + .def("fit", &PySpectralFitter::fit) + .def("fit_and_evaluate", &PySpectralFitter::fit_and_evaluate); +} + +PythonDeconvolution::PythonDeconvolution(const PythonDeconvolution& other) + : _filename(other._filename), + _guard(other._guard), + _deconvolveFunction( + std::make_unique<pybind11::function>(*other._deconvolveFunction)) {} + +PythonDeconvolution::~PythonDeconvolution() {} + +void PythonDeconvolution::setBuffer(const ImageSet& imageSet, double* ptr) { + size_t nFreq = imageSet.NDeconvolutionChannels(); + size_t nPol = imageSet.size() / imageSet.NDeconvolutionChannels(); + + for (size_t freq = 0; freq != nFreq; ++freq) { + for (size_t pol = 0; pol != nPol; ++pol) { + const aocommon::Image& image = imageSet[freq * nPol + pol]; + std::copy_n(image.Data(), image.Size(), ptr); + ptr += image.Size(); + } + } +} + +void PythonDeconvolution::getBuffer(ImageSet& imageSet, const double* ptr) { + size_t nFreq = imageSet.NDeconvolutionChannels(); + size_t nPol = imageSet.size() / imageSet.NDeconvolutionChannels(); + + for (size_t freq = 0; freq != nFreq; ++freq) { + for (size_t pol = 0; pol != nPol; ++pol) { + const size_t imageIndex = freq * nPol + pol; + float* img = imageSet.Data(imageIndex); + const size_t imageSize = imageSet[imageIndex].Size(); + std::copy_n(ptr, imageSize, img); + ptr += imageSize; + ; + } + } +} + +void PythonDeconvolution::setPsf(const std::vector<aocommon::Image>& psfs, + double* pyPtr, size_t width, size_t height) { + size_t nFreq = psfs.size(); + + for (size_t freq = 0; freq != nFreq; ++freq) { + const float* psf = psfs[freq].Data(); + + for (size_t y = 0; y != height; ++y) { + for (size_t x = 0; x != width; ++x) pyPtr[x] = psf[x]; + + pyPtr += width; + psf += width; + } + } +} + +float PythonDeconvolution::ExecuteMajorIteration( + ImageSet& dirtySet, ImageSet& modelSet, + const std::vector<aocommon::Image>& psfs, bool& reachedMajorThreshold) { + const size_t width = dirtySet.Width(); + const size_t height = dirtySet.Height(); + size_t nFreq = dirtySet.NDeconvolutionChannels(); + size_t nPol = dirtySet.size() / dirtySet.NDeconvolutionChannels(); + + pybind11::object result; + + // A new context block is started to destroy the python data arrays asap + { + // Create Residual array + pybind11::buffer_info residualBuf( + nullptr, // ask NumPy to allocate + sizeof(double), pybind11::format_descriptor<double>::value, 4, + {ptrdiff_t(nFreq), ptrdiff_t(nPol), ptrdiff_t(height), + ptrdiff_t(width)}, + {sizeof(double) * width * height * nPol, + sizeof(double) * width * height, sizeof(double) * width, + sizeof(double)} // Strides + ); + pybind11::array_t<double> pyResiduals(residualBuf); + setBuffer(dirtySet, static_cast<double*>(pyResiduals.request(true).ptr)); + + // Create Model array + pybind11::buffer_info modelBuf( + nullptr, sizeof(double), pybind11::format_descriptor<double>::value, 4, + {ptrdiff_t(nFreq), ptrdiff_t(nPol), ptrdiff_t(height), + ptrdiff_t(width)}, + {sizeof(double) * width * height * nPol, + sizeof(double) * width * height, sizeof(double) * width, + sizeof(double)}); + pybind11::array_t<double> pyModel(modelBuf); + setBuffer(modelSet, static_cast<double*>(pyModel.request(true).ptr)); + + // Create PSF array + pybind11::buffer_info psfBuf( + nullptr, sizeof(double), pybind11::format_descriptor<double>::value, 3, + {ptrdiff_t(nFreq), ptrdiff_t(height), ptrdiff_t(width)}, + {sizeof(double) * width * height, sizeof(double) * width, + sizeof(double)}); + pybind11::array_t<double> pyPsfs(psfBuf); + setPsf(psfs, static_cast<double*>(pyPsfs.request(true).ptr), width, height); + + PyMetaData meta(_spectralFitter); + meta.channels.resize(_spectralFitter.NFrequencies()); + for (size_t i = 0; i != _spectralFitter.NFrequencies(); ++i) { + meta.channels[i].frequency = _spectralFitter.Frequency(i); + meta.channels[i].weight = _spectralFitter.Weight(i); + } + meta.gain = _gain; + meta.iteration_number = _iterationNumber; + meta.major_iter_threshold = _majorIterThreshold; + meta.max_iterations = _maxIter; + meta.mgain = _mGain; + meta.final_threshold = _threshold; + + // Run the python code + result = (*_deconvolveFunction)(std::move(pyResiduals), std::move(pyModel), + std::move(pyPsfs), &meta); + + _iterationNumber = meta.iteration_number; + } + + // Extract the results + pybind11::object resultDict; + try { + resultDict = result.cast<pybind11::dict>(); + } catch (std::exception&) { + throw std::runtime_error( + "In python deconvolution code: Return value of deconvolve() should be " + "a dictionary"); + } + const bool isComplete = + resultDict.contains("residual") && resultDict.contains("model") && + resultDict.contains("level") && resultDict.contains("continue"); + if (!isComplete) + throw std::runtime_error( + "In python deconvolution code: Dictionary returned by deconvolve() is " + "missing items; should have 'residual', 'model', 'level' and " + "'continue'"); + pybind11::array_t<double> residualRes = + resultDict["residual"].cast<pybind11::array_t<double>>(); + getBuffer(dirtySet, static_cast<const double*>(residualRes.request().ptr)); + pybind11::array_t<double> modelRes = + resultDict["model"].cast<pybind11::array_t<double>>(); + getBuffer(modelSet, static_cast<const double*>(modelRes.request().ptr)); + + double level = resultDict["level"].cast<double>(); + reachedMajorThreshold = resultDict["continue"].cast<bool>(); + return level; +} +} // namespace radler::algorithms \ No newline at end of file diff --git a/cpp/algorithms/python_deconvolution.h b/cpp/algorithms/python_deconvolution.h new file mode 100644 index 00000000..7f37ca6a --- /dev/null +++ b/cpp/algorithms/python_deconvolution.h @@ -0,0 +1,52 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef RADLER_ALGORITHMS_PYTHON_DECONVOLUTION_H_ +#define RADLER_ALGORITHMS_PYTHON_DECONVOLUTION_H_ + +#include <aocommon/uvector.h> + +#include <schaapcommon/fitters/spectralfitter.h> + +#include "image_set.h" +#include "algorithms/deconvolution_algorithm.h" + +namespace pybind11 { +// Forward declarations to keep pybind11 symbols internal. +class scoped_interpreter; +class function; +} // namespace pybind11 + +namespace radler::algorithms { + +class PythonDeconvolution : public DeconvolutionAlgorithm { + public: + PythonDeconvolution(const std::string& filename); + + PythonDeconvolution(const PythonDeconvolution& other); + + ~PythonDeconvolution() override; + + float ExecuteMajorIteration(ImageSet& dirtySet, ImageSet& modelSet, + const std::vector<aocommon::Image>& psfs, + bool& reachedMajorThreshold) final override; + + virtual std::unique_ptr<DeconvolutionAlgorithm> Clone() const final override { + return std::make_unique<PythonDeconvolution>(*this); + } + + private: + std::string _filename; + // A Python interpreter can not be restarted, so the interpreter + // needs to live for the entire run + std::shared_ptr<pybind11::scoped_interpreter> _guard; + std::unique_ptr<pybind11::function> _deconvolveFunction; + + void setBuffer(const ImageSet& imageSet, double* pyPtr); + void setPsf(const std::vector<aocommon::Image>& psfs, double* pyPtr, + size_t width, size_t height); + void getBuffer(ImageSet& imageSet, const double* pyPtr); +}; +} // namespace radler::algorithms + +#endif // RADLER_ALGORITHMS_PYTHON_DECONVOLUTION_H_ diff --git a/cpp/algorithms/simple_clean.cc b/cpp/algorithms/simple_clean.cc new file mode 100644 index 00000000..ee19c851 --- /dev/null +++ b/cpp/algorithms/simple_clean.cc @@ -0,0 +1,179 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "algorithms/simple_clean.h" + +#include <algorithm> + +#ifdef __SSE__ +#define USE_INTRINSICS +#endif + +#ifdef USE_INTRINSICS +#include <emmintrin.h> +#include <immintrin.h> +#endif + +namespace radler { +namespace algorithms { +void SimpleClean::SubtractImage(float* image, const float* psf, size_t width, + size_t height, size_t x, size_t y, + float factor) { + size_t startX, startY, endX, endY; + int offsetX = (int)x - width / 2, offsetY = (int)y - height / 2; + + if (offsetX > 0) + startX = offsetX; + else + startX = 0; + + if (offsetY > 0) + startY = offsetY; + else + startY = 0; + + endX = x + width / 2; + if (endX > width) endX = width; + + bool isAligned = ((endX - startX) % 2) == 0; + if (!isAligned) --endX; + + endY = y + height / 2; + if (endY > height) endY = height; + + for (size_t ypos = startY; ypos != endY; ++ypos) { + float* imageIter = image + ypos * width + startX; + const float* psfIter = psf + (ypos - offsetY) * width + startX - offsetX; + for (size_t xpos = startX; xpos != endX; xpos++) { + // I've SSE-ified this, but it didn't improve speed at all :-/ + // (Compiler probably already did it) + *imageIter -= (*psfIter * factor); + //*(imageIter+1) = *(imageIter+1) - (*(psfIter+1) * factor); + ++imageIter; + ++psfIter; + } + } +} + +void SimpleClean::PartialSubtractImage(float* image, const float* psf, + size_t width, size_t height, size_t x, + size_t y, float factor, size_t startY, + size_t endY) { + size_t startX, endX; + int offsetX = (int)x - width / 2, offsetY = (int)y - height / 2; + + if (offsetX > 0) + startX = offsetX; + else + startX = 0; + + if (offsetY > (int)startY) startY = offsetY; + + endX = x + width / 2; + if (endX > width) endX = width; + + bool isAligned = ((endX - startX) % 2) == 0; + if (!isAligned) --endX; + + endY = std::min(y + height / 2, endY); + + for (size_t ypos = startY; ypos < endY; ++ypos) { + float* imageIter = image + ypos * width + startX; + const float* psfIter = psf + (ypos - offsetY) * width + startX - offsetX; + for (size_t xpos = startX; xpos != endX; xpos += 2) { + *imageIter = *imageIter - (*psfIter * factor); + *(imageIter + 1) = *(imageIter + 1) - (*(psfIter + 1) * factor); + imageIter += 2; + psfIter += 2; + } + if (!isAligned) *imageIter -= *psfIter * factor; + } +} + +void SimpleClean::PartialSubtractImage(float* image, size_t imgWidth, + size_t /*imgHeight*/, const float* psf, + size_t psfWidth, size_t psfHeight, + size_t x, size_t y, float factor, + size_t startY, size_t endY) { + size_t startX, endX; + int offsetX = (int)x - psfWidth / 2, offsetY = (int)y - psfHeight / 2; + + if (offsetX > 0) + startX = offsetX; + else + startX = 0; + + if (offsetY > (int)startY) startY = offsetY; + + endX = std::min(x + psfWidth / 2, imgWidth); + + bool isAligned = ((endX - startX) % 2) == 0; + if (!isAligned) --endX; + + endY = std::min(y + psfHeight / 2, endY); + + for (size_t ypos = startY; ypos < endY; ++ypos) { + float* imageIter = image + ypos * imgWidth + startX; + const float* psfIter = psf + (ypos - offsetY) * psfWidth + startX - offsetX; + for (size_t xpos = startX; xpos != endX; xpos += 2) { + *imageIter = *imageIter - (*psfIter * factor); + *(imageIter + 1) = *(imageIter + 1) - (*(psfIter + 1) * factor); + imageIter += 2; + psfIter += 2; + } + if (!isAligned) *imageIter -= *psfIter * factor; + } +} + +#if defined __AVX__ && defined USE_INTRINSICS +void SimpleClean::PartialSubtractImageAVX(double* image, size_t imgWidth, + size_t /*imgHeight*/, + const double* psf, size_t psfWidth, + size_t psfHeight, size_t x, size_t y, + double factor, size_t startY, + size_t endY) { + size_t startX, endX; + int offsetX = (int)x - psfWidth / 2, offsetY = (int)y - psfHeight / 2; + + if (offsetX > 0) + startX = offsetX; + else + startX = 0; + + if (offsetY > (int)startY) startY = offsetY; + + endX = std::min(x + psfWidth / 2, imgWidth); + + size_t unAlignedCount = (endX - startX) % 4; + endX -= unAlignedCount; + + endY = std::min(y + psfHeight / 2, endY); + + const __m256d mFactor = _mm256_set1_pd(-factor); + for (size_t ypos = startY; ypos < endY; ++ypos) { + double* imageIter = image + ypos * imgWidth + startX; + const double* psfIter = + psf + (ypos - offsetY) * psfWidth + startX - offsetX; + for (size_t xpos = startX; xpos != endX; xpos += 4) { + __m256d imgVal = _mm256_loadu_pd(imageIter), + psfVal = _mm256_loadu_pd(psfIter); +#ifdef __FMA__ + _mm256_storeu_pd(imageIter, _mm256_fmadd_pd(psfVal, mFactor, imgVal)); +#else + _mm256_storeu_pd(imageIter, + _mm256_add_pd(imgVal, _mm256_mul_pd(psfVal, mFactor))); +#endif + imageIter += 4; + psfIter += 4; + } + for (size_t xpos = endX; xpos != endX + unAlignedCount; ++xpos) { + *imageIter -= *psfIter * factor; + ++imageIter; + ++psfIter; + } + } +} + +#endif +} // namespace algorithms +} // namespace radler diff --git a/cpp/algorithms/simple_clean.h b/cpp/algorithms/simple_clean.h new file mode 100644 index 00000000..e906d388 --- /dev/null +++ b/cpp/algorithms/simple_clean.h @@ -0,0 +1,40 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef RADLER_ALGORITHMS_SIMPLE_CLEAN_H_ +#define RADLER_ALGORITHMS_SIMPLE_CLEAN_H_ + +#include <cstring> + +#ifdef __SSE__ +#define USE_INTRINSICS +#endif + +namespace radler::algorithms { + +class SimpleClean { + public: + SimpleClean() = delete; + static void SubtractImage(float* image, const float* psf, size_t width, + size_t height, size_t x, size_t y, float factor); + + static void PartialSubtractImage(float* image, const float* psf, size_t width, + size_t height, size_t x, size_t y, + float factor, size_t startY, size_t endY); + + static void PartialSubtractImage(float* image, size_t imgWidth, + size_t imgHeight, const float* psf, + size_t psfWidth, size_t psfHeight, size_t x, + size_t y, float factor, size_t startY, + size_t endY); + +#if defined __AVX__ && defined USE_INTRINSICS + static void PartialSubtractImageAVX(double* image, size_t imgWidth, + size_t imgHeight, const double* psf, + size_t psfWidth, size_t psfHeight, + size_t x, size_t y, double factor, + size_t startY, size_t endY); +#endif +}; +} // namespace radler::algorithms +#endif // RADLER_ALGORITHMS_SIMPLE_CLEAN_H_ diff --git a/cpp/algorithms/subminor_loop.cc b/cpp/algorithms/subminor_loop.cc new file mode 100644 index 00000000..2d1fdd00 --- /dev/null +++ b/cpp/algorithms/subminor_loop.cc @@ -0,0 +1,232 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "subminor_loop.h" + +#include <schaapcommon/fft/convolution.h> + +using aocommon::Image; + +namespace radler::algorithms { + +template <bool AllowNegatives> +size_t SubMinorModel::GetMaxComponent(Image& scratch, float& maxValue) const { + _residual->GetLinearIntegrated(scratch); + if (!_rmsFactorImage.Empty()) { + for (size_t i = 0; i != size(); ++i) scratch[i] *= _rmsFactorImage[i]; + } + size_t maxComponent = 0; + maxValue = scratch[0]; + for (size_t i = 0; i != size(); ++i) { + float value; + if (AllowNegatives) + value = std::fabs(scratch[i]); + else + value = scratch[i]; + if (value > maxValue) { + maxComponent = i; + maxValue = value; + } + } + maxValue = scratch[maxComponent]; // If it was negative, make sure a negative + // value is returned + return maxComponent; +} + +std::optional<float> SubMinorLoop::Run( + ImageSet& convolvedResidual, + const std::vector<aocommon::Image>& twiceConvolvedPsfs) { + _subMinorModel = SubMinorModel(_width, _height); + + findPeakPositions(convolvedResidual); + + _subMinorModel.MakeSets(convolvedResidual); + if (!_rmsFactorImage.Empty()) + _subMinorModel.MakeRMSFactorImage(_rmsFactorImage); + _logReceiver.Debug << "Number of components selected > " << _threshold << ": " + << _subMinorModel.size() << '\n'; + + if (_subMinorModel.size() == 0) return std::optional<float>(); + + Image scratch(_subMinorModel.size(), 1); + float maxValue; + size_t maxComponent = _subMinorModel.GetMaxComponent( + scratch, maxValue, _allowNegativeComponents); + std::vector<float> fittingScratch; + + while (std::fabs(maxValue) > _threshold && + _currentIteration < _maxIterations && + (!_stopOnNegativeComponent || maxValue >= 0.0)) { + aocommon::UVector<float> componentValues(_subMinorModel.Residual().size()); + for (size_t imgIndex = 0; imgIndex != _subMinorModel.Residual().size(); + ++imgIndex) + componentValues[imgIndex] = + _subMinorModel.Residual()[imgIndex][maxComponent] * _gain; + _fluxCleaned += maxValue * _gain; + + const size_t x = _subMinorModel.X(maxComponent), + y = _subMinorModel.Y(maxComponent); + + if (_fitter) + _fitter->FitAndEvaluate(componentValues.data(), x, y, fittingScratch); + + for (size_t imgIndex = 0; imgIndex != _subMinorModel.Model().size(); + ++imgIndex) + _subMinorModel.Model().Data(imgIndex)[maxComponent] += + componentValues[imgIndex]; + + /* + Commented out because even in verbose mode this is a bit too verbose, but + useful in case divergence occurs: _logReceiver.Debug << x << ", " << y << " + " << maxValue << " -> "; for(size_t imgIndex=0; + imgIndex!=_clarkModel.Model().size(); ++imgIndex) _logReceiver.Debug << + componentValues[imgIndex] << ' '; _logReceiver.Debug << '\n'; + */ + for (size_t imgIndex = 0; imgIndex != _subMinorModel.Residual().size(); + ++imgIndex) { + float* image = _subMinorModel.Residual().Data(imgIndex); + const aocommon::Image& psf = + twiceConvolvedPsfs[_subMinorModel.Residual().PSFIndex(imgIndex)]; + float psfFactor = componentValues[imgIndex]; + for (size_t px = 0; px != _subMinorModel.size(); ++px) { + int psfX = _subMinorModel.X(px) - x + _width / 2; + int psfY = _subMinorModel.Y(px) - y + _height / 2; + if (psfX >= 0 && psfX < int(_width) && psfY >= 0 && psfY < int(_height)) + image[px] -= psf[psfX + psfY * _width] * psfFactor; + } + } + + maxComponent = _subMinorModel.GetMaxComponent(scratch, maxValue, + _allowNegativeComponents); + ++_currentIteration; + } + return maxValue; +} + +void SubMinorModel::MakeSets(const ImageSet& residualSet) { + _residual = std::make_unique<ImageSet>(residualSet, size(), 1); + _model = std::make_unique<ImageSet>(residualSet, size(), 1); + *_model = 0.0; + for (size_t imgIndex = 0; imgIndex != _model->size(); ++imgIndex) { + const float* sourceResidual = residualSet[imgIndex].Data(); + float* destResidual = _residual->Data(imgIndex); + for (size_t pxIndex = 0; pxIndex != size(); ++pxIndex) { + size_t srcIndex = + _positions[pxIndex].second * _width + _positions[pxIndex].first; + destResidual[pxIndex] = sourceResidual[srcIndex]; + } + } +} + +void SubMinorModel::MakeRMSFactorImage(Image& rmsFactorImage) { + _rmsFactorImage = Image(size(), 1); + for (size_t pxIndex = 0; pxIndex != size(); ++pxIndex) { + size_t srcIndex = + _positions[pxIndex].second * _width + _positions[pxIndex].first; + _rmsFactorImage[pxIndex] = rmsFactorImage[srcIndex]; + } +} + +void SubMinorLoop::findPeakPositions(ImageSet& convolvedResidual) { + Image integratedScratch(_width, _height); + convolvedResidual.GetLinearIntegrated(integratedScratch); + + if (!_rmsFactorImage.Empty()) { + integratedScratch *= _rmsFactorImage; + } + + const size_t xiStart = _horizontalBorder, + xiEnd = std::max<long>(xiStart, _width - _horizontalBorder), + yiStart = _verticalBorder, + yiEnd = std::max<long>(yiStart, _height - _verticalBorder); + + if (_mask) { + for (size_t y = yiStart; y != yiEnd; ++y) { + const bool* maskPtr = _mask + y * _width; + float* imagePtr = integratedScratch.Data() + y * _width; + for (size_t x = xiStart; x != xiEnd; ++x) { + float value; + if (_allowNegativeComponents) + value = fabs(imagePtr[x]); + else + value = imagePtr[x]; + if (value >= _threshold && maskPtr[x]) _subMinorModel.AddPosition(x, y); + } + } + } else { + for (size_t y = yiStart; y != yiEnd; ++y) { + float* imagePtr = integratedScratch.Data() + y * _width; + for (size_t x = xiStart; x != xiEnd; ++x) { + float value; + if (_allowNegativeComponents) + value = fabs(imagePtr[x]); + else + value = imagePtr[x]; + if (value >= _threshold) _subMinorModel.AddPosition(x, y); + } + } + } +} + +void SubMinorLoop::GetFullIndividualModel(size_t imageIndex, + float* individualModelImg) const { + std::fill(individualModelImg, individualModelImg + _width * _height, 0.0); + const float* data = _subMinorModel.Model()[imageIndex].Data(); + for (size_t px = 0; px != _subMinorModel.size(); ++px) { + individualModelImg[_subMinorModel.FullIndex(px)] = data[px]; + } +} + +void SubMinorLoop::CorrectResidualDirty(float* scratchA, float* scratchB, + float* scratchC, size_t imageIndex, + float* residual, + const float* singleConvolvedPsf) const { + // Get padded kernel in scratchB + Image::Untrim(scratchA, _paddedWidth, _paddedHeight, singleConvolvedPsf, + _width, _height); + schaapcommon::fft::PrepareConvolutionKernel(scratchB, scratchA, _paddedWidth, + _paddedHeight, _threadCount); + + // Get padded model image in scratchA + GetFullIndividualModel(imageIndex, scratchC); + Image::Untrim(scratchA, _paddedWidth, _paddedHeight, scratchC, _width, + _height); + + // Convolve and store in scratchA + schaapcommon::fft::Convolve(scratchA, scratchB, _paddedWidth, _paddedHeight, + _threadCount); + + // Trim the result into scratchC + Image::Trim(scratchC, _width, _height, scratchA, _paddedWidth, _paddedHeight); + + for (size_t i = 0; i != _width * _height; ++i) residual[i] -= scratchC[i]; +} + +void SubMinorLoop::UpdateAutoMask(bool* mask) const { + for (size_t imageIndex = 0; imageIndex != _subMinorModel.Model().size(); + ++imageIndex) { + const aocommon::Image& image = _subMinorModel.Model()[imageIndex]; + for (size_t px = 0; px != _subMinorModel.size(); ++px) { + if (image[px] != 0.0) mask[_subMinorModel.FullIndex(px)] = true; + } + } +} + +void SubMinorLoop::UpdateComponentList(ComponentList& list, + size_t scaleIndex) const { + aocommon::UVector<float> values(_subMinorModel.Model().size()); + for (size_t px = 0; px != _subMinorModel.size(); ++px) { + bool isNonZero = false; + for (size_t imageIndex = 0; imageIndex != _subMinorModel.Model().size(); + ++imageIndex) { + values[imageIndex] = _subMinorModel.Model()[imageIndex][px]; + if (values[imageIndex] != 0.0) isNonZero = true; + } + if (isNonZero) { + size_t posIndex = _subMinorModel.FullIndex(px); + size_t x = posIndex % _width, y = posIndex / _width; + list.Add(x, y, scaleIndex, values.data()); + } + } +} +} // namespace radler::algorithms \ No newline at end of file diff --git a/cpp/algorithms/subminor_loop.h b/cpp/algorithms/subminor_loop.h new file mode 100644 index 00000000..0228a922 --- /dev/null +++ b/cpp/algorithms/subminor_loop.h @@ -0,0 +1,209 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef RADLER_ALGORITHMS_SUB_MINOR_LOOP_H_ +#define RADLER_ALGORITHMS_SUB_MINOR_LOOP_H_ + +#include <cstring> +#include <optional> +#include <vector> + +#include <aocommon/image.h> +#include <aocommon/logger.h> + +#include "component_list.h" +#include "image_set.h" + +namespace radler::algorithms { +/** + * In multi-scale, a subminor optimized loop looks like this: + * + * IterateAndMakeModel(): + * - Make a set S with positions of all the components larger than 'threshold', + * which are also in the mask + * - Find the largest component in S + * Loop { + * - Measure the largest component per frequency (from S) + * - Store the model component in S + * - Subtract this component multiplied with the twice convolved PSF and gain + * from all components in S (per individual image) + * - Find the new largest component in S + * } + * + * CorrectResidualDirty(): + * For each individual image { + * - Put the model components from S onto a full image (using + * GetFullIndividualModel()) + * - Convolve the model with the SingleConvolvedPSF + * - Subtract the convolved model from the residual + * } + * + * Finalization: + * - Put the model components from S onto a full image (using + * GetFullIndividualModel()) + * - Convolve the model image with the scale kernel + * - Add the model components to the full model + * + * A subminor loop has some correspondance with the so-called Clark + * optimization. However, this implementation has some differences, e.g. by + * collecting a list of threshold components prior of entering the subminor + * loop. + */ + +class SubMinorModel { + public: + SubMinorModel(size_t width, size_t /*height*/) : _width(width) {} + + void AddPosition(size_t x, size_t y) { + _positions.push_back(std::make_pair(x, y)); + } + + /** + * Return number of selected pixels. + */ + size_t size() const { return _positions.size(); } + + void MakeSets(const ImageSet& templateSet); + void MakeRMSFactorImage(aocommon::Image& rmsFactorImage); + + ImageSet& Residual() { return *_residual; } + const ImageSet& Residual() const { return *_residual; } + + ImageSet& Model() { return *_model; } + const ImageSet& Model() const { return *_model; } + + size_t X(size_t index) const { return _positions[index].first; } + size_t Y(size_t index) const { return _positions[index].second; } + size_t FullIndex(size_t index) const { return X(index) + Y(index) * _width; } + template <bool AllowNegatives> + size_t GetMaxComponent(aocommon::Image& scratch, float& maxValue) const; + size_t GetMaxComponent(aocommon::Image& scratch, float& maxValue, + bool allowNegatives) const { + if (allowNegatives) + return GetMaxComponent<true>(scratch, maxValue); + else + return GetMaxComponent<false>(scratch, maxValue); + } + + private: + std::vector<std::pair<size_t, size_t>> _positions; + std::unique_ptr<ImageSet> _residual, _model; + aocommon::Image _rmsFactorImage; + size_t _width; +}; + +class SubMinorLoop { + public: + SubMinorLoop(size_t width, size_t height, size_t convolutionWidth, + size_t convolutionHeight, aocommon::LogReceiver& logReceiver) + : _width(width), + _height(height), + _paddedWidth(convolutionWidth), + _paddedHeight(convolutionHeight), + _threshold(0.0), + _consideredPixelThreshold(0.0), + _gain(0.0), + _horizontalBorder(0), + _verticalBorder(0), + _currentIteration(0), + _maxIterations(0), + _allowNegativeComponents(true), + _stopOnNegativeComponent(false), + _mask(nullptr), + _fitter(nullptr), + _subMinorModel(width, height), + _fluxCleaned(0.0), + _logReceiver(logReceiver), + _threadCount(1) {} + + /** + * @param threshold The threshold to which this subminor run should clean + * @param consideredPixelThreshold The threshold that is used to determine + * whether a pixel is considered. Typically, this is similar to threshold, but + * it can be set lower if it is important that all peak values are below the + * threshold, as otherwise some pixels might not be considered but get + * increased by the cleaning, thereby stay above the threshold. This is + * important for making multi-scale clean efficient near a stopping threshold. + */ + void SetThreshold(float threshold, float consideredPixelThreshold) { + _threshold = threshold; + _consideredPixelThreshold = consideredPixelThreshold; + } + + void SetIterationInfo(size_t currentIteration, size_t maxIterations) { + _currentIteration = currentIteration; + _maxIterations = maxIterations; + } + + void SetGain(float gain) { _gain = gain; } + + void SetAllowNegativeComponents(bool allowNegativeComponents) { + _allowNegativeComponents = allowNegativeComponents; + } + + void SetStopOnNegativeComponent(bool stopOnNegativeComponent) { + _stopOnNegativeComponent = stopOnNegativeComponent; + } + + void SetSpectralFitter(const schaapcommon::fitters::SpectralFitter* fitter) { + _fitter = fitter; + } + + void SetCleanBorders(size_t horizontalBorder, size_t verticalBorder) { + _horizontalBorder = horizontalBorder; + _verticalBorder = verticalBorder; + } + + void SetMask(const bool* mask) { _mask = mask; } + + void SetRMSFactorImage(const aocommon::Image& image) { + _rmsFactorImage = image; + } + + void SetThreadCount(size_t threadCount) { _threadCount = threadCount; } + + size_t CurrentIteration() const { return _currentIteration; } + + float FluxCleaned() const { return _fluxCleaned; } + + std::optional<float> Run( + ImageSet& convolvedResidual, + const std::vector<aocommon::Image>& twiceConvolvedPsfs); + + /** + * The produced model is convolved with the given psf, and the result is + * subtracted from the given residual image. To be called after Run(). After + * this method, the residual will hold the result of the subminor loop run. + * scratchA and scratchB need to be able to store the full padded image + * (_untrimmedWidth x _untrimmedHeight). scratchC only needs to store the + * trimmed size (_width x _height). + */ + void CorrectResidualDirty(float* scratchA, float* scratchB, float* scratchC, + size_t imageIndex, float* residual, + const float* singleConvolvedPsf) const; + + void GetFullIndividualModel(size_t imageIndex, + float* individualModelImg) const; + + void UpdateAutoMask(bool* mask) const; + + void UpdateComponentList(ComponentList& list, size_t scaleIndex) const; + + private: + void findPeakPositions(ImageSet& convolvedResidual); + + size_t _width, _height, _paddedWidth, _paddedHeight; + float _threshold, _consideredPixelThreshold, _gain; + size_t _horizontalBorder, _verticalBorder; + size_t _currentIteration, _maxIterations; + bool _allowNegativeComponents, _stopOnNegativeComponent; + const bool* _mask; + const schaapcommon::fitters::SpectralFitter* _fitter; + SubMinorModel _subMinorModel; + float _fluxCleaned; + aocommon::Image _rmsFactorImage; + aocommon::LogReceiver& _logReceiver; + size_t _threadCount; +}; +} // namespace radler::algorithms +#endif // RADLER_ALGORITHMS_SUB_MINOR_LOOP_H_ diff --git a/cpp/algorithms/test/CMakeLists.txt b/cpp/algorithms/test/CMakeLists.txt new file mode 100644 index 00000000..eaef1206 --- /dev/null +++ b/cpp/algorithms/test/CMakeLists.txt @@ -0,0 +1,8 @@ +# Copyright (C) 2020 ASTRON (Netherlands Institute for Radio Astronomy) +# SPDX-License-Identifier: GPL-3.0-or-later + +include(${PROJECT_SOURCE_DIR}/cmake/unittest.cmake) + +add_definitions(-DBOOST_ERROR_CODE_HEADER_ONLY) + +add_unittest(radler_algorithms runtests.cc test_simple_clean.cc) diff --git a/cpp/algorithms/test/runtests.cc b/cpp/algorithms/test/runtests.cc new file mode 100644 index 00000000..294636fe --- /dev/null +++ b/cpp/algorithms/test/runtests.cc @@ -0,0 +1,6 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#define BOOST_TEST_MODULE radler_algorithms + +#include <boost/test/unit_test.hpp> \ No newline at end of file diff --git a/cpp/algorithms/test/test_simple_clean.cc b/cpp/algorithms/test/test_simple_clean.cc new file mode 100644 index 00000000..9e77d346 --- /dev/null +++ b/cpp/algorithms/test/test_simple_clean.cc @@ -0,0 +1,54 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#include <boost/test/unit_test.hpp> + +#include "algorithms/simple_clean.h" + +#include <aocommon/uvector.h> + +#include <random> + +namespace radler::algorithms { +BOOST_AUTO_TEST_SUITE(simple_clean) + +template <typename NumT> +struct NoiseFixture { + NoiseFixture() : n(2048), psf(n * n, 0.0), img(n * n), normal_dist(0.0, 1.0) { + mt.seed(42); + for (size_t i = 0; i != n * n; ++i) { + img[i] = normal_dist(mt); + } + } + + size_t n; + aocommon::UVector<NumT> psf, img; + std::mt19937 mt; + std::normal_distribution<NumT> normal_dist; +}; + +const size_t nRepeats = + 3; /* This should be set to 100 to assert the performance */ + +BOOST_AUTO_TEST_CASE(partialSubtractImagePerformance) { + NoiseFixture<float> f; + size_t x = f.n / 2, y = f.n / 2; + for (size_t repeat = 0; repeat != nRepeats; ++repeat) + SimpleClean::PartialSubtractImage(f.img.data(), f.n, f.n, f.psf.data(), f.n, + f.n, x, y, 0.5, 0, f.n / 2); + BOOST_CHECK(true); +} + +#if defined __AVX__ && !defined FORCE_NON_AVX +BOOST_AUTO_TEST_CASE(partialSubtractImageAVXPerformance) { + NoiseFixture<double> f; + size_t x = f.n / 2, y = f.n / 2; + for (size_t repeat = 0; repeat != nRepeats; ++repeat) + SimpleClean::PartialSubtractImageAVX(f.img.data(), f.n, f.n, f.psf.data(), + f.n, f.n, x, y, 0.5, 0, f.n / 2); + BOOST_CHECK(true); +} +#endif + +BOOST_AUTO_TEST_SUITE_END() +} // namespace radler::algorithms \ No newline at end of file diff --git a/cpp/algorithms/threaded_deconvolution_tools.cc b/cpp/algorithms/threaded_deconvolution_tools.cc new file mode 100644 index 00000000..33b4b59f --- /dev/null +++ b/cpp/algorithms/threaded_deconvolution_tools.cc @@ -0,0 +1,197 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "algorithms/threaded_deconvolution_tools.h" + +#include <memory> + +#include <aocommon/image.h> + +#include "algorithms/simple_clean.h" +#include "math/peak_finder.h" + +using aocommon::Image; + +namespace radler::algorithms { + +ThreadedDeconvolutionTools::ThreadedDeconvolutionTools(size_t threadCount) + : _taskLanes(threadCount), + _resultLanes(threadCount), + _threadCount(threadCount) { + for (size_t i = 0; i != _threadCount; ++i) { + _taskLanes[i].resize(1); + _resultLanes[i].resize(1); + _threadGroup.emplace_back(&ThreadedDeconvolutionTools::threadFunc, this, + &_taskLanes[i], &_resultLanes[i]); + } +} + +ThreadedDeconvolutionTools::~ThreadedDeconvolutionTools() { + for (size_t i = 0; i != _threadCount; ++i) { + _taskLanes[i].write_end(); + } + + for (std::thread& t : _threadGroup) t.join(); +} + +void ThreadedDeconvolutionTools::SubtractImage(float* image, + const aocommon::Image& psf, + size_t x, size_t y, + float factor) { + for (size_t thr = 0; thr != _threadCount; ++thr) { + auto task = std::make_unique<SubtractionTask>(); + task->image = image; + task->psf = &psf; + task->x = x; + task->y = y; + task->factor = factor; + task->startY = psf.Height() * thr / _threadCount; + task->endY = psf.Height() * (thr + 1) / _threadCount; + if (thr == _threadCount - 1) { + (*task)(); + } else { + _taskLanes[thr].write(std::move(task)); + } + } + for (size_t thr = 0; thr != _threadCount - 1; ++thr) { + std::unique_ptr<ThreadResult> result; + _resultLanes[thr].read(result); + } +} + +std::unique_ptr<ThreadedDeconvolutionTools::ThreadResult> +ThreadedDeconvolutionTools::SubtractionTask::operator()() { + SimpleClean::PartialSubtractImage(image, psf->Data(), psf->Width(), + psf->Height(), x, y, factor, startY, endY); + return std::unique_ptr<ThreadedDeconvolutionTools::ThreadResult>(); +} + +void ThreadedDeconvolutionTools::FindMultiScalePeak( + multiscale::MultiScaleTransforms* msTransforms, const Image& image, + const aocommon::UVector<float>& scales, + std::vector<ThreadedDeconvolutionTools::PeakData>& results, + bool allowNegativeComponents, const bool* mask, + const std::vector<aocommon::UVector<bool>>& scaleMasks, float borderRatio, + const Image& rmsFactorImage, bool calculateRMS) { + size_t imageIndex = 0; + size_t nextThread = 0; + size_t resultIndex = 0; + + results.resize(scales.size()); + + size_t size = std::min(scales.size(), _threadCount); + std::vector<Image> imageData; + std::vector<Image> scratchData; + imageData.reserve(size); + scratchData.reserve(size); + for (size_t i = 0; i != size; ++i) { + imageData.emplace_back(msTransforms->Width(), msTransforms->Height()); + scratchData.emplace_back(msTransforms->Width(), msTransforms->Height()); + } + + while (imageIndex < scales.size()) { + std::unique_ptr<FindMultiScalePeakTask> task(new FindMultiScalePeakTask()); + task->msTransforms = msTransforms; + imageData[nextThread] = image; + task->image = &imageData[nextThread]; + task->scratch = &scratchData[nextThread]; + task->scale = scales[imageIndex]; + task->allowNegativeComponents = allowNegativeComponents; + if (scaleMasks.empty()) + task->mask = mask; + else + task->mask = scaleMasks[imageIndex].data(); + task->borderRatio = borderRatio; + task->calculateRMS = calculateRMS; + task->rmsFactorImage = &rmsFactorImage; + _taskLanes[nextThread].write(std::move(task)); + + ++nextThread; + if (nextThread == _threadCount) { + for (size_t thr = 0; thr != nextThread; ++thr) { + std::unique_ptr<ThreadResult> result; + _resultLanes[thr].read(result); + results[resultIndex].normalizedValue = + static_cast<FindMultiScalePeakResult&>(*result).normalizedValue; + results[resultIndex].unnormalizedValue = + static_cast<FindMultiScalePeakResult&>(*result).unnormalizedValue; + results[resultIndex].x = + static_cast<FindMultiScalePeakResult&>(*result).x; + results[resultIndex].y = + static_cast<FindMultiScalePeakResult&>(*result).y; + results[resultIndex].rms = + static_cast<FindMultiScalePeakResult&>(*result).rms; + ++resultIndex; + } + nextThread = 0; + } + ++imageIndex; + } + for (size_t thr = 0; thr != nextThread; ++thr) { + std::unique_ptr<ThreadResult> result; + _resultLanes[thr].read(result); + results[resultIndex].unnormalizedValue = + static_cast<FindMultiScalePeakResult&>(*result).unnormalizedValue; + results[resultIndex].normalizedValue = + static_cast<FindMultiScalePeakResult&>(*result).normalizedValue; + results[resultIndex].x = static_cast<FindMultiScalePeakResult&>(*result).x; + results[resultIndex].y = static_cast<FindMultiScalePeakResult&>(*result).y; + results[resultIndex].rms = + static_cast<FindMultiScalePeakResult&>(*result).rms; + ++resultIndex; + } +} + +std::unique_ptr<ThreadedDeconvolutionTools::ThreadResult> +ThreadedDeconvolutionTools::FindMultiScalePeakTask::operator()() { + msTransforms->Transform(*image, *scratch, scale); + const size_t width = msTransforms->Width(); + const size_t height = msTransforms->Height(); + const size_t scaleBorder = size_t(std::ceil(scale * 0.5)); + const size_t horBorderSize = + std::max<size_t>(std::round(width * borderRatio), scaleBorder); + const size_t vertBorderSize = + std::max<size_t>(std::round(height * borderRatio), scaleBorder); + std::unique_ptr<FindMultiScalePeakResult> result( + new FindMultiScalePeakResult()); + if (calculateRMS) + result->rms = RMS(*image, width * height); + else + result->rms = -1.0; + if (rmsFactorImage->Empty()) { + if (mask == nullptr) + result->unnormalizedValue = math::PeakFinder::Find( + image->Data(), width, height, result->x, result->y, + allowNegativeComponents, 0, height, horBorderSize, vertBorderSize); + else + result->unnormalizedValue = math::PeakFinder::FindWithMask( + image->Data(), width, height, result->x, result->y, + allowNegativeComponents, 0, height, mask, horBorderSize, + vertBorderSize); + + result->normalizedValue = result->unnormalizedValue; + } else { + for (size_t i = 0; i != rmsFactorImage->Size(); ++i) + (*scratch)[i] = (*image)[i] * (*rmsFactorImage)[i]; + + if (mask == nullptr) + result->unnormalizedValue = math::PeakFinder::Find( + scratch->Data(), width, height, result->x, result->y, + allowNegativeComponents, 0, height, horBorderSize, vertBorderSize); + else + result->unnormalizedValue = math::PeakFinder::FindWithMask( + scratch->Data(), width, height, result->x, result->y, + allowNegativeComponents, 0, height, mask, horBorderSize, + vertBorderSize); + + if (result->unnormalizedValue) { + result->normalizedValue = + (*result->unnormalizedValue) / + (*rmsFactorImage)[result->x + result->y * width]; + } else { + result->normalizedValue.reset(); + } + } + return result; +} +} // namespace radler::algorithms \ No newline at end of file diff --git a/cpp/algorithms/threaded_deconvolution_tools.h b/cpp/algorithms/threaded_deconvolution_tools.h new file mode 100644 index 00000000..034ac0b5 --- /dev/null +++ b/cpp/algorithms/threaded_deconvolution_tools.h @@ -0,0 +1,99 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef RADLER_ALGORITHMS_THREADED_DECONVOLUTION_TOOLS_H_ +#define RADLER_ALGORITHMS_THREADED_DECONVOLUTION_TOOLS_H_ + +#include <cmath> +#include <optional> +#include <thread> +#include <vector> + +#include <aocommon/image.h> +#include <aocommon/lane.h> +#include <aocommon/uvector.h> + +#include "algorithms/multiscale/multiscale_transforms.h" + +namespace radler::algorithms { + +class ThreadedDeconvolutionTools { + public: + explicit ThreadedDeconvolutionTools(size_t threadCount); + ~ThreadedDeconvolutionTools(); + + struct PeakData { + std::optional<float> normalizedValue, unnormalizedValue; + float rms; + size_t x, y; + }; + + void SubtractImage(float* image, const aocommon::Image& psf, size_t x, + size_t y, float factor); + + void FindMultiScalePeak( + multiscale::MultiScaleTransforms* msTransforms, + const aocommon::Image& image, const aocommon::UVector<float>& scales, + std::vector<PeakData>& results, bool allowNegativeComponents, + const bool* mask, const std::vector<aocommon::UVector<bool>>& scaleMasks, + float borderRatio, const aocommon::Image& rmsFactorImage, + bool calculateRMS); + + static float RMS(const aocommon::Image& image, size_t n) { + float result = 0.0; + for (size_t i = 0; i != n; ++i) result += image[i] * image[i]; + return std::sqrt(result / float(n)); + } + + private: + struct ThreadResult {}; + struct FindMultiScalePeakResult : public ThreadResult { + std::optional<float> unnormalizedValue, normalizedValue; + float rms; + size_t x, y; + }; + + struct ThreadTask { + virtual std::unique_ptr<ThreadResult> operator()() = 0; + virtual ~ThreadTask() {} + }; + struct SubtractionTask : public ThreadTask { + virtual std::unique_ptr<ThreadResult> operator()(); + + float* image; + const aocommon::Image* psf; + size_t x, y; + float factor; + size_t startY, endY; + }; + + struct FindMultiScalePeakTask : public ThreadTask { + virtual std::unique_ptr<ThreadResult> operator()(); + + multiscale::MultiScaleTransforms* msTransforms; + aocommon::Image* image; + aocommon::Image* scratch; + float scale; + bool allowNegativeComponents; + const bool* mask; + float borderRatio; + bool calculateRMS; + const aocommon::Image* rmsFactorImage; + }; + + std::vector<aocommon::Lane<std::unique_ptr<ThreadTask>>> _taskLanes; + std::vector<aocommon::Lane<std::unique_ptr<ThreadResult>>> _resultLanes; + size_t _threadCount; + std::vector<std::thread> _threadGroup; + + void threadFunc(aocommon::Lane<std::unique_ptr<ThreadTask>>* taskLane, + aocommon::Lane<std::unique_ptr<ThreadResult>>* resultLane) { + std::unique_ptr<ThreadTask> task; + while (taskLane->read(task)) { + std::unique_ptr<ThreadResult> result = (*task)(); + resultLane->write(std::move(result)); + } + } +}; +} // namespace radler::algorithms +#endif diff --git a/cpp/component_list.cc b/cpp/component_list.cc new file mode 100644 index 00000000..3f41b1d2 --- /dev/null +++ b/cpp/component_list.cc @@ -0,0 +1,158 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "component_list.h" + +#include "algorithms/deconvolution_algorithm.h" +#include "algorithms/multiscale_algorithm.h" + +#include <aocommon/imagecoordinates.h> + +#include "radler.h" +#include "utils/write_model.h" + +using aocommon::ImageCoordinates; + +namespace radler { +void ComponentList::WriteSources(const Radler& radler, + const std::string& filename, + long double pixel_scale_x, + long double pixel_scale_y, + long double phase_centre_ra, + long double phase_centre_dec) const { + const algorithms::DeconvolutionAlgorithm& deconvolution_algorithm = + radler.MaxScaleCountAlgorithm(); + if (const auto* multiscale_algorithm = + dynamic_cast<const algorithms::MultiScaleAlgorithm*>( + &deconvolution_algorithm)) { + Write(filename, *multiscale_algorithm, pixel_scale_x, pixel_scale_y, + phase_centre_ra, phase_centre_dec); + } else { + WriteSingleScale(filename, deconvolution_algorithm, pixel_scale_x, + pixel_scale_y, phase_centre_ra, phase_centre_dec); + } +} + +void ComponentList::Write(const std::string& filename, + const algorithms::MultiScaleAlgorithm& multiscale, + long double pixelScaleX, long double pixelScaleY, + long double phaseCentreRA, + long double phaseCentreDec) const { + aocommon::UVector<double> scaleSizes(NScales()); + for (size_t scaleIndex = 0; scaleIndex != NScales(); ++scaleIndex) + scaleSizes[scaleIndex] = multiscale.ScaleSize(scaleIndex); + write(filename, multiscale.Fitter(), scaleSizes, pixelScaleX, pixelScaleY, + phaseCentreRA, phaseCentreDec); +} + +void ComponentList::WriteSingleScale( + const std::string& filename, + const algorithms::DeconvolutionAlgorithm& algorithm, + long double pixelScaleX, long double pixelScaleY, long double phaseCentreRA, + long double phaseCentreDec) const { + aocommon::UVector<double> scaleSizes(1, 0); + write(filename, algorithm.Fitter(), scaleSizes, pixelScaleX, pixelScaleY, + phaseCentreRA, phaseCentreDec); +} + +void ComponentList::write(const std::string& filename, + const schaapcommon::fitters::SpectralFitter& fitter, + const aocommon::UVector<double>& scaleSizes, + long double pixelScaleX, long double pixelScaleY, + long double phaseCentreRA, + long double phaseCentreDec) const { + if (_componentsAddedSinceLastMerge != 0) { + throw std::runtime_error( + "ComponentList::write called while there are yet unmerged components. " + "Run ComponentList::MergeDuplicates() first."); + } + + if (fitter.Mode() == schaapcommon::fitters::SpectralFittingMode::NoFitting && + _nFrequencies > 1) + throw std::runtime_error( + "Can't write component list, because you have not specified a spectral " + "fitting method. You probably want to add '-fit-spectral-pol'."); + + std::ofstream file(filename); + bool useLogSI = false; + switch (fitter.Mode()) { + case schaapcommon::fitters::SpectralFittingMode::NoFitting: + case schaapcommon::fitters::SpectralFittingMode::Polynomial: + useLogSI = false; + break; + case schaapcommon::fitters::SpectralFittingMode::LogPolynomial: + useLogSI = true; + break; + } + utils::WriteHeaderForSpectralTerms(file, fitter.ReferenceFrequency()); + std::vector<float> terms; + for (size_t scaleIndex = 0; scaleIndex != NScales(); ++scaleIndex) { + const ScaleList& list = _listPerScale[scaleIndex]; + size_t componentIndex = 0; + const double scale = scaleSizes[scaleIndex]; + // Using the FWHM formula for a Gaussian + const double + fwhm = + 2.0L * sqrtl(2.0L * logl(2.0L)) * + algorithms::multiscale::MultiScaleTransforms::GaussianSigma(scale), + scaleFWHML = fwhm * pixelScaleX * (180.0 * 60.0 * 60.0 / M_PI), + scaleFWHMM = fwhm * pixelScaleY * (180.0 * 60.0 * 60.0 / M_PI); + size_t valueIndex = 0; + for (size_t index = 0; index != list.positions.size(); ++index) { + const size_t x = list.positions[index].x; + const size_t y = list.positions[index].y; + aocommon::UVector<float> spectrum(_nFrequencies); + for (size_t frequency = 0; frequency != _nFrequencies; ++frequency) { + spectrum[frequency] = list.values[valueIndex]; + ++valueIndex; + } + if (_nFrequencies == 1) + terms.assign(1, spectrum[0]); + else + fitter.Fit(terms, spectrum.data(), x, y); + long double l, m; + ImageCoordinates::XYToLM<long double>(x, y, pixelScaleX, pixelScaleY, + _width, _height, l, m); + long double ra, dec; + ImageCoordinates::LMToRaDec(l, m, phaseCentreRA, phaseCentreDec, ra, dec); + std::ostringstream name; + name << 's' << scaleIndex << 'c' << componentIndex; + if (scale == 0.0) + radler::utils::WritePolynomialPointComponent( + file, name.str(), ra, dec, useLogSI, terms, + fitter.ReferenceFrequency()); + else { + radler::utils::WritePolynomialGaussianComponent( + file, name.str(), ra, dec, useLogSI, terms, + fitter.ReferenceFrequency(), scaleFWHML, scaleFWHMM, 0.0); + } + ++componentIndex; + } + } +} + +void ComponentList::loadFromImageSet(ImageSet& imageSet, size_t scaleIndex) { + _componentsAddedSinceLastMerge = 0; + _listPerScale[scaleIndex].positions.clear(); + _listPerScale[scaleIndex].values.clear(); + for (size_t y = 0; y != _height; ++y) { + const size_t rowIndex = y * _width; + for (size_t x = 0; x != _width; ++x) { + const size_t posIndex = rowIndex + x; + bool isNonZero = false; + for (size_t imageIndex = 0; imageIndex != imageSet.size(); ++imageIndex) { + if (imageSet[imageIndex][posIndex] != 0.0) { + isNonZero = true; + break; + } + } + if (isNonZero) { + _listPerScale[scaleIndex].positions.emplace_back(x, y); + for (size_t imageIndex = 0; imageIndex != imageSet.size(); ++imageIndex) + _listPerScale[scaleIndex].values.push_back( + imageSet[imageIndex][posIndex]); + } + } + } +} +} // namespace radler \ No newline at end of file diff --git a/cpp/component_list.h b/cpp/component_list.h new file mode 100644 index 00000000..a22e3764 --- /dev/null +++ b/cpp/component_list.h @@ -0,0 +1,234 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef RADLER_COMPONENT_LIST_H_ +#define RADLER_COMPONENT_LIST_H_ + +#include <vector> + +#include <aocommon/image.h> +#include <aocommon/uvector.h> + +#include <schaapcommon/fitters/spectralfitter.h> + +#include "image_set.h" + +namespace radler { +class Radler; + +namespace algorithms { +class DeconvolutionAlgorithm; +class MultiScaleAlgorithm; +} // namespace algorithms + +class ComponentList { + public: + ComponentList() + : _width(0), + _height(0), + _nFrequencies(0), + _componentsAddedSinceLastMerge(0), + _maxComponentsBeforeMerge(0), + _listPerScale() {} + + /** + * Constructor for single-scale clean + */ + ComponentList(size_t width, size_t height, ImageSet& imageSet) + : _width(width), + _height(height), + _nFrequencies(imageSet.size()), + _componentsAddedSinceLastMerge(0), + _maxComponentsBeforeMerge(100000), + _listPerScale(1) { + loadFromImageSet(imageSet, 0); + } + + /** + * Constructor for multi-scale clean + */ + ComponentList(size_t width, size_t height, size_t nScales, + size_t nFrequencies) + : _width(width), + _height(height), + _nFrequencies(nFrequencies), + _componentsAddedSinceLastMerge(0), + _maxComponentsBeforeMerge(100000), + _listPerScale(nScales) {} + + struct Position { + Position(size_t _x, size_t _y) : x(_x), y(_y) {} + size_t x, y; + }; + + void Add(size_t x, size_t y, size_t scaleIndex, const float* values) { + _listPerScale[scaleIndex].values.push_back(values, values + _nFrequencies); + _listPerScale[scaleIndex].positions.emplace_back(x, y); + ++_componentsAddedSinceLastMerge; + if (_componentsAddedSinceLastMerge >= _maxComponentsBeforeMerge) + MergeDuplicates(); + } + + void Add(const ComponentList& other, int offsetX, int offsetY) { + assert(other._nFrequencies == _nFrequencies); + if (other.NScales() > NScales()) SetNScales(other.NScales()); + for (size_t scale = 0; scale != other.NScales(); ++scale) { + const ScaleList& list = other._listPerScale[scale]; + for (size_t i = 0; i != list.positions.size(); ++i) { + Add(list.positions[i].x + offsetX, list.positions[i].y + offsetY, scale, + &list.values[i * _nFrequencies]); + } + } + } + + void WriteSources(const Radler& radler, const std::string& filename, + long double pixel_scale_x, long double pixel_scale_y, + long double phase_centre_ra, + long double phase_centre_dec) const; + + /** + * @brief Write component lists over all scales, typically + * used for writing components of a multiscale clean. + */ + void Write(const std::string& filename, + const algorithms::MultiScaleAlgorithm& multiscale, + long double pixelScaleX, long double pixelScaleY, + long double phaseCentreRA, long double phaseCentreDec) const; + + void WriteSingleScale(const std::string& filename, + const algorithms::DeconvolutionAlgorithm& algorithm, + long double pixelScaleX, long double pixelScaleY, + long double phaseCentreRA, + long double phaseCentreDec) const; + + void MergeDuplicates() { + if (_componentsAddedSinceLastMerge != 0) { + for (size_t scaleIndex = 0; scaleIndex != _listPerScale.size(); + ++scaleIndex) { + mergeDuplicates(scaleIndex); + } + _componentsAddedSinceLastMerge = 0; + } + } + + void Clear() { + for (ScaleList& list : _listPerScale) { + list.positions.clear(); + list.values.clear(); + } + } + + size_t Width() const { return _width; } + size_t Height() const { return _height; } + + size_t ComponentCount(size_t scaleIndex) const { + return _listPerScale[scaleIndex].positions.size(); + } + + void GetComponent(size_t scaleIndex, size_t index, size_t& x, size_t& y, + float* values) const { + assert(scaleIndex < _listPerScale.size()); + assert(index < _listPerScale[scaleIndex].positions.size()); + x = _listPerScale[scaleIndex].positions[index].x; + y = _listPerScale[scaleIndex].positions[index].y; + for (size_t f = 0; f != _nFrequencies; ++f) + values[f] = _listPerScale[scaleIndex].values[index * _nFrequencies + f]; + } + + /** + * @brief Multiply the components for a given scale index, position index and + * channel index with corresponding (primary beam) correction factors. + */ + inline void MultiplyScaleComponent(size_t scaleIndex, size_t positionIndex, + size_t channel, double correctionFactor) { + assert(scaleIndex < _listPerScale.size()); + assert(positionIndex < _listPerScale[scaleIndex].positions.size()); + assert(channel < _nFrequencies); + float& value = _listPerScale[scaleIndex] + .values[channel + positionIndex * _nFrequencies]; + value *= correctionFactor; + } + + /** + * @brief Get vector of positions per scale index. + */ + const aocommon::UVector<Position>& GetPositions(size_t scaleIndex) const { + assert(scaleIndex < _listPerScale.size()); + return _listPerScale[scaleIndex].positions; + } + + size_t NScales() const { return _listPerScale.size(); } + + size_t NFrequencies() const { return _nFrequencies; } + + void SetNScales(size_t nScales) { _listPerScale.resize(nScales); } + + private: + struct ScaleList { + /** + * This list contains nFrequencies values for each + * component, such that _positions[i] corresponds with the values + * starting at _values[i * _nFrequencies]. + */ + aocommon::UVector<float> values; + aocommon::UVector<Position> positions; + }; + + void write(const std::string& filename, + const schaapcommon::fitters::SpectralFitter& fitter, + const aocommon::UVector<double>& scaleSizes, + long double pixelScaleX, long double pixelScaleY, + long double phaseCentreRA, long double phaseCentreDec) const; + + void loadFromImageSet(ImageSet& imageSet, size_t scaleIndex); + + void mergeDuplicates(size_t scaleIndex) { + ScaleList& list = _listPerScale[scaleIndex]; + aocommon::UVector<float> newValues; + aocommon::UVector<Position> newPositions; + + std::vector<aocommon::Image> images(_nFrequencies); + for (aocommon::Image& image : images) + image = aocommon::Image(_width, _height, 0.0); + size_t valueIndex = 0; + for (size_t index = 0; index != list.positions.size(); ++index) { + size_t position = + list.positions[index].x + list.positions[index].y * _width; + for (size_t frequency = 0; frequency != _nFrequencies; ++frequency) { + images[frequency][position] += list.values[valueIndex]; + valueIndex++; + } + } + + list.values.clear(); + list.positions.clear(); + + for (size_t imageIndex = 0; imageIndex != images.size(); ++imageIndex) { + aocommon::Image& image = images[imageIndex]; + size_t posIndex = 0; + for (size_t y = 0; y != _height; ++y) { + for (size_t x = 0; x != _width; ++x) { + if (image[posIndex] != 0.0) { + for (size_t i = 0; i != images.size(); ++i) { + newValues.push_back(images[i][posIndex]); + images[i][posIndex] = 0.0; + } + newPositions.emplace_back(x, y); + } + ++posIndex; + } + } + } + std::swap(_listPerScale[scaleIndex].values, newValues); + std::swap(_listPerScale[scaleIndex].positions, newPositions); + } + + size_t _width; + size_t _height; + size_t _nFrequencies; + size_t _componentsAddedSinceLastMerge; + size_t _maxComponentsBeforeMerge; + std::vector<ScaleList> _listPerScale; +}; +} // namespace radler +#endif diff --git a/cpp/deconvolution_settings.h b/cpp/deconvolution_settings.h new file mode 100644 index 00000000..2a8cda16 --- /dev/null +++ b/cpp/deconvolution_settings.h @@ -0,0 +1,155 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef RADLER_DECONVOLUTION_SETTINGS_H_ +#define RADLER_DECONVOLUTION_SETTINGS_H_ + +#include <set> + +#include <aocommon/polarization.h> +#include <aocommon/system.h> + +#include <schaapcommon/fitters/spectralfitter.h> + +#include "algorithms/multiscale/multiscale_transforms.h" + +namespace radler { +/** + * @brief The value of LocalRmsMethod describes how the RMS map + * should be used. + */ +enum class LocalRmsMethod { kNone, kRmsWindow, kRmsAndMinimumWindow }; + +struct DeconvolutionSettings { + DeconvolutionSettings(); + + /** + * @{ + * Settings that are duplicates from top level settings, and also used outside + * deconvolution. + */ + size_t trimmedImageWidth; + size_t trimmedImageHeight; + size_t channelsOut; + double pixelScaleX; + double pixelScaleY; + size_t threadCount; + std::string prefixName; + /** @} */ + + /** + * @{ + * These settings strictly pertain to deconvolution only. + */ + std::set<aocommon::PolarizationEnum> linkedPolarizations; + size_t parallelDeconvolutionMaxSize; + size_t parallelDeconvolutionMaxThreads; + double deconvolutionThreshold; + double deconvolutionGain; + double deconvolutionMGain; + bool autoDeconvolutionThreshold; + bool autoMask; + double autoDeconvolutionThresholdSigma; + double autoMaskSigma; + LocalRmsMethod localRMSMethod; + double localRMSWindow; + std::string localRMSImage; + bool saveSourceList; + size_t deconvolutionIterationCount; + size_t majorIterationCount; + bool allowNegativeComponents; + bool stopOnNegativeComponents; + bool useMultiscale; + bool useSubMinorOptimization; + bool squaredJoins; + double spectralCorrectionFrequency; + std::vector<float> spectralCorrection; + bool multiscaleFastSubMinorLoop; + double multiscaleGain; + double multiscaleDeconvolutionScaleBias; + size_t multiscaleMaxScales; + double multiscaleConvolutionPadding; + std::vector<double> multiscaleScaleList; + algorithms::multiscale::Shape multiscaleShapeFunction; + double deconvolutionBorderRatio; + std::string fitsDeconvolutionMask; + std::string casaDeconvolutionMask; + bool horizonMask; + double horizonMaskDistance; + std::string pythonDeconvolutionFilename; + bool useMoreSaneDeconvolution; + bool useIUWTDeconvolution; + bool iuwtSNRTest; + std::string moreSaneLocation; + std::string moreSaneArgs; + std::vector<double> moreSaneSigmaLevels; + schaapcommon::fitters::SpectralFittingMode spectralFittingMode; + size_t spectralFittingTerms; + std::string forcedSpectrumFilename; + /** + * The number of channels used during deconvolution. This can be used to + * image with more channels than deconvolution. Before deconvolution, + * channels are averaged, and after deconvolution they are interpolated. + * If it is 0, all channels should be used. + */ + size_t deconvolutionChannelCount; + /** @} */ +}; + +inline DeconvolutionSettings::DeconvolutionSettings() + : trimmedImageWidth(0), + trimmedImageHeight(0), + channelsOut(1), + pixelScaleX(0.0), + pixelScaleY(0.0), + threadCount(aocommon::system::ProcessorCount()), + prefixName("wsclean"), + linkedPolarizations(), + parallelDeconvolutionMaxSize(0), + parallelDeconvolutionMaxThreads(0), + deconvolutionThreshold(0.0), + deconvolutionGain(0.1), + deconvolutionMGain(1.0), + autoDeconvolutionThreshold(false), + autoMask(false), + autoDeconvolutionThresholdSigma(0.0), + autoMaskSigma(0.0), + localRMSMethod(LocalRmsMethod::kNone), + localRMSWindow(25.0), + localRMSImage(), + saveSourceList(false), + deconvolutionIterationCount(0), + majorIterationCount(20), + allowNegativeComponents(true), + stopOnNegativeComponents(false), + useMultiscale(false), + useSubMinorOptimization(true), + squaredJoins(false), + spectralCorrectionFrequency(0.0), + spectralCorrection(), + multiscaleFastSubMinorLoop(true), + multiscaleGain(0.2), + multiscaleDeconvolutionScaleBias(0.6), + multiscaleMaxScales(0), + multiscaleConvolutionPadding(1.1), + multiscaleScaleList(), + multiscaleShapeFunction( + algorithms::multiscale::Shape::TaperedQuadraticShape), + deconvolutionBorderRatio(0.0), + fitsDeconvolutionMask(), + casaDeconvolutionMask(), + horizonMask(false), + horizonMaskDistance(0.0), + pythonDeconvolutionFilename(), + useMoreSaneDeconvolution(false), + useIUWTDeconvolution(false), + iuwtSNRTest(false), + moreSaneLocation(), + moreSaneArgs(), + spectralFittingMode( + schaapcommon::fitters::SpectralFittingMode::NoFitting), + spectralFittingTerms(0), + forcedSpectrumFilename(), + deconvolutionChannelCount(0) {} +} // namespace radler +#endif diff --git a/cpp/deconvolution_table.cc b/cpp/deconvolution_table.cc new file mode 100644 index 00000000..4928b76a --- /dev/null +++ b/cpp/deconvolution_table.cc @@ -0,0 +1,39 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "deconvolution_table.h" + +#include <algorithm> +#include <cassert> + +namespace radler { + +DeconvolutionTable::DeconvolutionTable(int n_original_groups, + int n_deconvolution_groups, + int channel_index_offset) + : entries_(), + channel_index_offset_(channel_index_offset), + original_groups_(std::max(n_original_groups, 1)), + deconvolution_groups_((n_deconvolution_groups <= 0) + ? original_groups_.size() + : std::min(original_groups_.size(), + size_t(n_deconvolution_groups))) { + // Create an entry in deconvolution_groups for each original group. + for (int i = 0; i < n_original_groups; ++i) { + int deconvolution_index = + i * deconvolution_groups_.size() / n_original_groups; + deconvolution_groups_[deconvolution_index].push_back(i); + } +} + +void DeconvolutionTable::AddEntry( + std::unique_ptr<DeconvolutionTableEntry> entry) { + const size_t original_channel_index = entry->original_channel_index; + assert(original_channel_index < original_groups_.size()); + + entry->index = entries_.size(); + entries_.push_back(std::move(entry)); + + original_groups_[original_channel_index].push_back(entries_.back().get()); +} +} // namespace radler diff --git a/cpp/deconvolution_table.h b/cpp/deconvolution_table.h new file mode 100644 index 00000000..41386fb1 --- /dev/null +++ b/cpp/deconvolution_table.h @@ -0,0 +1,159 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef RADLER_DECONVOLUTION_TABLE_H_ +#define RADLER_DECONVOLUTION_TABLE_H_ + +#include "deconvolution_table_entry.h" + +#include <functional> +#include <memory> +#include <vector> + +namespace radler { +/** + * The DeconvolutionTable contains DeconvolutionTableEntry's and groups entries + * that have the same squaredDeconvolutionIndex. + */ +class DeconvolutionTable { + public: + using Entries = std::vector<std::unique_ptr<DeconvolutionTableEntry>>; + using Group = std::vector<const DeconvolutionTableEntry*>; + + /** + * Iterator-like class which (only) supports a range-based loop over entries. + * + * Dereferencing this class yields a reference to the actual object instead + * of a reference to the pointer for the object. + */ + class EntryIteratorLite { + using BaseIterator = Entries::const_iterator; + + public: + explicit EntryIteratorLite(BaseIterator base_iterator) + : base_iterator_(base_iterator) {} + + const DeconvolutionTableEntry& operator*() const { + return **base_iterator_; + } + EntryIteratorLite& operator++() { + ++base_iterator_; + return *this; + } + bool operator!=(const EntryIteratorLite& other) const { + return base_iterator_ != other.base_iterator_; + } + bool operator==(const EntryIteratorLite& other) const { + return base_iterator_ == other.base_iterator_; + } + + private: + BaseIterator base_iterator_; + }; + + /** + * @brief Constructs a new DeconvolutionTable object. + * + * @param n_original_groups The number of original channel groups. When adding + * entries, their original channel index must be less than the number of + * original groups. If the value is zero or less, one group is used. + * @param n_deconvolution_groups The number of deconvolution groups. + * A deconvolution group consist of one or more channel groups, which are then + * joinedly deconvolved. + * If the value is zero or less, or larger than the number of original groups, + * all channels are deconvolved separately. + * @param channel_index_offset The index of the first channel in the caller. + * Must be >= 0. + */ + explicit DeconvolutionTable(int n_original_groups, int n_deconvolution_groups, + int channel_index_offset = 0); + + /** + * @return The table entries, grouped by their original channel index. + * @see AddEntry() + */ + const std::vector<Group>& OriginalGroups() const { return original_groups_; } + + /** + * @return The original group indices for each deconvolution group. + */ + const std::vector<std::vector<int>>& DeconvolutionGroups() const { + return deconvolution_groups_; + } + + /** + * Find the first group of original channels, given a deconvolution group + * index. + * + * @param deconvolution_index Index for a deconvolution group. Must be less + * than the number of deconvolution groups. + * @return A reference to the first original group for the deconvolution + * group. + */ + const Group& FirstOriginalGroup(size_t deconvolution_index) const { + return original_groups_[deconvolution_groups_[deconvolution_index].front()]; + } + + /** + * begin() and end() allow writing range-based loops over all entries. + * @{ + */ + EntryIteratorLite begin() const { + return EntryIteratorLite(entries_.begin()); + } + EntryIteratorLite end() const { return EntryIteratorLite(entries_.end()); } + /** @} */ + + /** + * @brief Adds an entry to the table. + * + * The original channel index of the entry determines the original group for + * the entry. It must be less than the number of original channel groups, as + * given in the constructor. + * + * @param entry A new entry. + */ + void AddEntry(std::unique_ptr<DeconvolutionTableEntry> entry); + + /** + * @return A reference to the first entry. + */ + const DeconvolutionTableEntry& Front() const { return *entries_.front(); } + + /** + * @return The number of entries in the table. + */ + size_t Size() const { return entries_.size(); } + + /** + * @return The channel index offset, which was set in the constructor. + */ + size_t GetChannelIndexOffset() const { return channel_index_offset_; } + + private: + Entries entries_; + + /** + * A user of the DeconvolutionTable may use different channel indices than + * the DeconvolutionTable. This offset is the difference between those + * indices. + * For example, with three channels, the DeconvolutionTable indices are always + * 0, 1, and 2. When the user indices are 4, 5, and 6, this offset will be 4. + */ + const std::size_t channel_index_offset_; + + /** + * An original group has entries with equal original channel indices. + */ + std::vector<Group> original_groups_; + + /** + * A deconvolution group consists of one or more original groups, which + * are deconvolved together. Each entry contains the indices of the original + * groups that are part of the deconvolution group. + */ + std::vector<std::vector<int>> deconvolution_groups_; +}; +} // namespace radler + +#endif diff --git a/cpp/deconvolution_table_entry.h b/cpp/deconvolution_table_entry.h new file mode 100644 index 00000000..125cfdb2 --- /dev/null +++ b/cpp/deconvolution_table_entry.h @@ -0,0 +1,67 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef RADLER_DECONVOLUTION_TABLE_ENTRY_H_ +#define RADLER_DECONVOLUTION_TABLE_ENTRY_H_ + +#include <memory> +#include <vector> + +#include <aocommon/imageaccessor.h> +#include <aocommon/polarization.h> + +namespace radler { +struct DeconvolutionTableEntry { + double CentralFrequency() const { + return 0.5 * (band_start_frequency + band_end_frequency); + } + + /** + * Index of the entry in its DeconvolutionTable. + */ + size_t index = 0; + + /** + * Note that mses might have overlapping frequencies. + */ + double band_start_frequency = 0.0; + double band_end_frequency = 0.0; + + aocommon::PolarizationEnum polarization = aocommon::PolarizationEnum::StokesI; + + /** + * Entries with equal original channel indices are 'joinedly' deconvolved by + * adding their squared flux density values together. Normally, all the + * polarizations from a single channel / timestep form such a group. + * + * When the number of deconvolution channels is less than the number of + * original channels, entries in multiple groups are 'joinedly' deconvolved. + */ + size_t original_channel_index = 0; + size_t original_interval_index = 0; + + /** + * A number that scales with the estimated inverse-variance of the image. It + * can be used when averaging images or fitting functions through the images + * to get the optimal sensitivity. It is set after the first inversion. + */ + double image_weight = 0.0; + + /** + * Image accessor for the PSF image for this entry. This accessor is only used + * for the first entry of each channel group. + */ + std::unique_ptr<aocommon::ImageAccessor> psf_accessor; + + /** + * Image accessor for the model image for this entry. + */ + std::unique_ptr<aocommon::ImageAccessor> model_accessor; + + /** + * Image accessor for the residual image for this entry. + */ + std::unique_ptr<aocommon::ImageAccessor> residual_accessor; +}; +} // namespace radler +#endif diff --git a/cpp/demo/CMakeLists.txt b/cpp/demo/CMakeLists.txt new file mode 100644 index 00000000..dbfb393f --- /dev/null +++ b/cpp/demo/CMakeLists.txt @@ -0,0 +1,7 @@ +# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +# SPDX-License-Identifier: GPL-3.0-or-later + +add_executable(multiscale_example multiscale_example.cc) +target_link_libraries(multiscale_example PRIVATE radler) +target_include_directories(multiscale_example + PRIVATE $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/cpp>) diff --git a/cpp/demo/multiscale_example.cc b/cpp/demo/multiscale_example.cc new file mode 100644 index 00000000..761b134d --- /dev/null +++ b/cpp/demo/multiscale_example.cc @@ -0,0 +1,105 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +// NOTE: +// - This file hasn't been tested on real input. +// - Rather than being a demo/example, this file might be the basis for a +// multiscale algorithm test. + +#include <iostream> +#include <memory> + +#include <aocommon/image.h> +#include <aocommon/imageaccessor.h> +#include <aocommon/fits/fitsreader.h> +#include <aocommon/fits/fitswriter.h> + +#include <schaapcommon/fitters/spectralfitter.h> + +#include "image_set.h" +#include "deconvolution_table.h" +#include "algorithms/multiscale_algorithm.h" + +namespace { + +class MinimalImageAccessor final : public aocommon::ImageAccessor { + public: + MinimalImageAccessor(const aocommon::Image& image, + aocommon::FitsWriter writer, + const std::string& output_fits) + : _image(image), _writer(writer), _output_fits(output_fits) {} + ~MinimalImageAccessor() override {} + + void Load(aocommon::Image& image) const override { image = _image; } + + void Store(const aocommon::Image& image) override { + _writer.Write(_output_fits, image.Data()); + } + + private: + const aocommon::Image _image; + const aocommon::FitsWriter _writer; + const std::string _output_fits; +}; +} // namespace + +int main(int argc, char* argv[]) { + if (argc != 3) { + std::cout << "Syntax: mscaleexample <image> <psf>\n"; + } else { + aocommon::FitsReader imgReader(argv[1]); + aocommon::FitsReader psfReader(argv[2]); + const double beamScale = + imgReader.BeamMajorAxisRad() / imgReader.PixelSizeX(); + const size_t width = imgReader.ImageWidth(); + const size_t height = imgReader.ImageHeight(); + const size_t n_channels = 1; + + aocommon::Image image(width, height); + aocommon::Image psf(width, height); + imgReader.Read(image.Data()); + psfReader.Read(psf.Data()); + aocommon::Image model(width, height, 0.0); + + aocommon::FitsWriter writer(imgReader); + + std::unique_ptr<radler::DeconvolutionTable> table = + std::make_unique<radler::DeconvolutionTable>(n_channels, n_channels); + + auto e = std::make_unique<radler::DeconvolutionTableEntry>(); + e->polarization = imgReader.Polarization(); + e->band_start_frequency = imgReader.Frequency(); + e->band_end_frequency = imgReader.Frequency(); + e->image_weight = 1.0; + e->psf_accessor = + std::make_unique<MinimalImageAccessor>(psf, writer, "psf.fits"); + e->model_accessor = + std::make_unique<MinimalImageAccessor>(model, writer, "model.fits"); + e->residual_accessor = + std::make_unique<MinimalImageAccessor>(image, writer, "residual.fits"); + table->AddEntry(std::move(e)); + + radler::ImageSet residualSet(*table, false, {}, width, height); + radler::ImageSet modelSet(*table, false, {}, width, height); + + const double gain = 0.1; + const bool allowNegativeComponents = true; + const double borderRatio = 0.05; + bool reachedThreshold = false; + + radler::algorithms::MultiScaleAlgorithm algorithm( + beamScale, imgReader.PixelSizeX(), imgReader.PixelSizeY()); + algorithm.SetGain(gain); + algorithm.SetAllowNegativeComponents(allowNegativeComponents); + algorithm.SetCleanBorderRatio(borderRatio); + algorithm.ExecuteMajorIteration(residualSet, modelSet, {psf}, + reachedThreshold); + + residualSet.AssignAndStoreResidual(); + modelSet.InterpolateAndStoreModel( + schaapcommon::fitters::SpectralFitter( + schaapcommon::fitters::SpectralFittingMode::NoFitting, 0), + 1); + } + return 0; +} diff --git a/cpp/image_set.cc b/cpp/image_set.cc new file mode 100644 index 00000000..00aa3b7c --- /dev/null +++ b/cpp/image_set.cc @@ -0,0 +1,464 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "image_set.h" + +#include <cassert> + +#include <aocommon/logger.h> +#include <aocommon/staticfor.h> + +using aocommon::Image; +using aocommon::Logger; + +namespace radler { + +namespace { +void assignMultiply(aocommon::Image& lhs, const aocommon::Image& rhs, + float factor) { + // As this function is used in cases where rhs.Size() is larger than + // lhs.Size(), this method can't be easily migrated to aocommon. Maybe + // consider a stricter enforcement of lhs.Size() and rhs.Size() to be equal? + const size_t image_size = lhs.Size(); + assert(rhs.Size() >= image_size); + for (size_t i = 0; i != image_size; ++i) lhs[i] = rhs[i] * factor; +} +} // namespace + +ImageSet::ImageSet( + const DeconvolutionTable& table, bool squared_joins, + const std::set<aocommon::PolarizationEnum>& linked_polarizations, + size_t width, size_t height) + : _images(), + _squareJoinedChannels(squared_joins), + _deconvolutionTable(table), + _imageIndexToPSFIndex(), + _linkedPolarizations(linked_polarizations) { + const size_t nPol = table.OriginalGroups().front().size(); + const size_t nImages = nPol * NDeconvolutionChannels(); + assert(nImages >= 1); + _images.reserve(nImages); + for (size_t i = 0; i < nImages; ++i) { + _images.emplace_back(width, height); + } + _imageIndexToPSFIndex.resize(nImages); + + initializePolFactor(); + initializeIndices(); + aocommon::UVector<double> frequencies; + CalculateDeconvolutionFrequencies(table, frequencies, _weights); +} + +ImageSet::ImageSet(const ImageSet& image_set, size_t width, size_t height) + : ImageSet(image_set._deconvolutionTable, image_set._squareJoinedChannels, + image_set._linkedPolarizations, width, height) {} + +void ImageSet::initializeIndices() { + _entryIndexToImageIndex.reserve(_deconvolutionTable.Size()); + size_t imgIndex = 0; + for (const std::vector<int>& group : + _deconvolutionTable.DeconvolutionGroups()) { + const size_t deconvolutionChannelStartIndex = imgIndex; + for (const int originalIndex : group) { + imgIndex = deconvolutionChannelStartIndex; + + for ([[maybe_unused]] const DeconvolutionTableEntry* entry : + _deconvolutionTable.OriginalGroups()[originalIndex]) { + assert(entry->index == _entryIndexToImageIndex.size()); + _entryIndexToImageIndex.push_back(imgIndex); + + ++imgIndex; + } + } + } + + for (size_t chIndex = 0; chIndex != NDeconvolutionChannels(); ++chIndex) { + const DeconvolutionTable::Group& originalGroup = + _deconvolutionTable.FirstOriginalGroup(chIndex); + for (const DeconvolutionTableEntry* entry : originalGroup) { + const size_t imageIndex = _entryIndexToImageIndex[entry->index]; + _imageIndexToPSFIndex[imageIndex] = chIndex; + } + } +} + +void ImageSet::SetImages(ImageSet&& source) { + _images = std::move(source._images); + // Note: 'source' becomes invalid now: Since its _images becomes empty, + // Width() and Height() will fail. Move semantics allow this case, though: + // The state of 'source' is unknown and the destructor will not fail. +} + +void ImageSet::LoadAndAverage(bool use_residual_image) { + for (Image& image : _images) { + image = 0.0; + } + + Image scratch(Width(), Height()); + + aocommon::UVector<double> averagedWeights(_images.size(), 0.0); + size_t imgIndex = 0; + for (const std::vector<int>& group : + _deconvolutionTable.DeconvolutionGroups()) { + const size_t deconvolutionChannelStartIndex = imgIndex; + for (const int originalIndex : group) { + // The next loop iterates over the polarizations. The logic in the next + // loop makes sure that images of the same polarizations and that belong + // to the same deconvolution channel are averaged together. + imgIndex = deconvolutionChannelStartIndex; + for (const DeconvolutionTableEntry* entry_ptr : + _deconvolutionTable.OriginalGroups()[originalIndex]) { + if (use_residual_image) { + entry_ptr->residual_accessor->Load(scratch); + } else { + entry_ptr->model_accessor->Load(scratch); + } + _images[imgIndex].AddWithFactor(scratch, entry_ptr->image_weight); + averagedWeights[imgIndex] += entry_ptr->image_weight; + ++imgIndex; + } + } + } + + for (size_t i = 0; i != _images.size(); ++i) { + _images[i] *= 1.0 / averagedWeights[i]; + } +} + +std::vector<aocommon::Image> ImageSet::LoadAndAveragePSFs() { + const size_t image_size = Width() * Height(); + + std::vector<aocommon::Image> psfImages; + psfImages.reserve(NDeconvolutionChannels()); + for (size_t i = 0; i < NDeconvolutionChannels(); ++i) { + psfImages.emplace_back(Width(), Height(), 0.0); + } + + Image scratch(Width(), Height()); + + aocommon::UVector<double> averagedWeights(NDeconvolutionChannels(), 0.0); + for (size_t groupIndex = 0; groupIndex != NOriginalChannels(); ++groupIndex) { + const size_t chIndex = + (groupIndex * NDeconvolutionChannels()) / NOriginalChannels(); + const DeconvolutionTable::Group& channelGroup = + _deconvolutionTable.OriginalGroups()[groupIndex]; + const DeconvolutionTableEntry& entry = *channelGroup.front(); + const double inputChannelWeight = entry.image_weight; + entry.psf_accessor->Load(scratch); + for (size_t i = 0; i != image_size; ++i) { + psfImages[chIndex][i] += scratch[i] * inputChannelWeight; + } + averagedWeights[chIndex] += inputChannelWeight; + } + + for (size_t chIndex = 0; chIndex != NDeconvolutionChannels(); ++chIndex) { + const double factor = + averagedWeights[chIndex] == 0.0 ? 0.0 : 1.0 / averagedWeights[chIndex]; + for (size_t i = 0; i != image_size; ++i) { + psfImages[chIndex][i] *= factor; + } + } + + return psfImages; +} + +void ImageSet::InterpolateAndStoreModel( + const schaapcommon::fitters::SpectralFitter& fitter, size_t threadCount) { + if (NDeconvolutionChannels() == NOriginalChannels()) { + size_t imgIndex = 0; + for (const DeconvolutionTableEntry& e : _deconvolutionTable) { + e.model_accessor->Store(_images[imgIndex]); + ++imgIndex; + } + } else { + Logger::Info << "Interpolating from " << NDeconvolutionChannels() << " to " + << NOriginalChannels() << " channels...\n"; + + // TODO should use spectralimagefitter to do the interpolation of images; + // here we should just unpack the data structure + + // The following loop will make an 'image' with at each pixel + // the terms of the fit. By doing this first, it is not necessary + // to have all channel images in memory at the same time. + // TODO: this assumes that polarizations are not joined! + size_t nTerms = fitter.NTerms(); + aocommon::UVector<float> termsImage(Width() * Height() * nTerms); + aocommon::StaticFor<size_t> loop(threadCount); + loop.Run(0, Height(), [&](size_t yStart, size_t yEnd) { + aocommon::UVector<float> spectralPixel(NDeconvolutionChannels()); + std::vector<float> termsPixel; + for (size_t y = yStart; y != yEnd; ++y) { + size_t px = y * Width(); + for (size_t x = 0; x != Width(); ++x) { + bool isZero = true; + for (size_t s = 0; s != _images.size(); ++s) { + float value = _images[s][px]; + spectralPixel[s] = value; + isZero = isZero && (value == 0.0); + } + float* termsPtr = &termsImage[px * nTerms]; + // Skip fitting if it is zero; most of model images will be zero, so + // this can save a lot of time. + if (isZero) { + std::fill_n(termsPtr, nTerms, 0.0); + } else { + fitter.Fit(termsPixel, spectralPixel.data(), x, y); + std::copy_n(termsPixel.cbegin(), nTerms, termsPtr); + } + ++px; + } + } + }); + + // Now that we know the fit for each pixel, evaluate the function for each + // pixel of each output channel. + Image scratch(Width(), Height()); + for (const DeconvolutionTableEntry& e : _deconvolutionTable) { + double freq = e.CentralFrequency(); + loop.Run(0, Width() * Height(), [&](size_t pxStart, size_t pxEnd) { + std::vector<float> termsPixel; + for (size_t px = pxStart; px != pxEnd; ++px) { + const float* termsPtr = &termsImage[px * nTerms]; + termsPixel.assign(termsPtr, termsPtr + nTerms); + scratch[px] = fitter.Evaluate(termsPixel, freq); + } + }); + + e.model_accessor->Store(scratch); + } + } +} + +void ImageSet::AssignAndStoreResidual() { + Logger::Info << "Assigning from " << NDeconvolutionChannels() << " to " + << NOriginalChannels() << " channels...\n"; + + size_t imgIndex = 0; + for (const std::vector<int>& group : + _deconvolutionTable.DeconvolutionGroups()) { + const size_t deconvolutionChannelStartIndex = imgIndex; + for (const int originalIndex : group) { + imgIndex = deconvolutionChannelStartIndex; + + for (const DeconvolutionTableEntry* entry : + _deconvolutionTable.OriginalGroups()[originalIndex]) { + entry->residual_accessor->Store(_images[imgIndex]); + ++imgIndex; + } + } + } +} + +void ImageSet::getSquareIntegratedWithNormalChannels(Image& dest, + Image& scratch) const { + // In case only one frequency channel is used, we do not have to use + // 'scratch', which saves copying and normalizing the data. + if (NDeconvolutionChannels() == 1) { + const DeconvolutionTable::Group& originalGroup = + _deconvolutionTable.OriginalGroups().front(); + if (originalGroup.size() == 1) { + const DeconvolutionTableEntry& entry = *originalGroup.front(); + dest = entryToImage(entry); + } else { + const bool useAllPolarizations = _linkedPolarizations.empty(); + bool isFirst = true; + for (const DeconvolutionTableEntry* entry_ptr : originalGroup) { + if (useAllPolarizations || + _linkedPolarizations.count(entry_ptr->polarization) != 0) { + if (isFirst) { + dest = entryToImage(*entry_ptr); + dest.Square(); + isFirst = false; + } else { + dest.AddSquared(entryToImage(*entry_ptr)); + } + } + } + dest.SqrtWithFactor(std::sqrt(_polarizationNormalizationFactor)); + } + } else { + double weightSum = 0.0; + bool isFirstChannel = true; + + for (size_t chIndex = 0; chIndex != NDeconvolutionChannels(); ++chIndex) { + const DeconvolutionTable::Group& originalGroup = + _deconvolutionTable.FirstOriginalGroup(chIndex); + const double groupWeight = _weights[chIndex]; + // if the groupWeight is zero, the image might contain NaNs, so we + // shouldn't add it to the total in that case. + if (groupWeight != 0.0) { + weightSum += groupWeight; + if (originalGroup.size() == 1) { + const DeconvolutionTableEntry& entry = *originalGroup.front(); + scratch = entryToImage(entry); + } else { + const bool useAllPolarizations = _linkedPolarizations.empty(); + bool isFirstPolarization = true; + for (const DeconvolutionTableEntry* entry_ptr : originalGroup) { + if (useAllPolarizations || + _linkedPolarizations.count(entry_ptr->polarization) != 0) { + if (isFirstPolarization) { + scratch = entryToImage(*entry_ptr); + scratch.Square(); + isFirstPolarization = false; + } else { + scratch.AddSquared(entryToImage(*entry_ptr)); + } + } + } + scratch.Sqrt(); + } + } + + if (isFirstChannel) { + assignMultiply(dest, scratch, groupWeight); + isFirstChannel = false; + } else { + dest.AddWithFactor(scratch, groupWeight); + } + } + dest *= std::sqrt(_polarizationNormalizationFactor) / weightSum; + } +} + +void ImageSet::getSquareIntegratedWithSquaredChannels(Image& dest) const { + bool isFirst = true; + const bool useAllPolarizations = _linkedPolarizations.empty(); + double weightSum = 0.0; + + for (size_t chIndex = 0; chIndex != NDeconvolutionChannels(); ++chIndex) { + const double groupWeight = _weights[chIndex]; + if (groupWeight != 0.0) { + weightSum += groupWeight; + const DeconvolutionTable::Group& originalGroup = + _deconvolutionTable.FirstOriginalGroup(chIndex); + for (const DeconvolutionTableEntry* entry_ptr : originalGroup) { + if (useAllPolarizations || + _linkedPolarizations.count(entry_ptr->polarization) != 0) { + if (isFirst) { + dest = entryToImage(*entry_ptr); + dest.SquareWithFactor(groupWeight); + isFirst = false; + } else { + dest.AddSquared(entryToImage(*entry_ptr), groupWeight); + } + } + } + } + } + + if (weightSum > 0.0) { + dest.SqrtWithFactor( + std::sqrt(_polarizationNormalizationFactor / weightSum)); + } else { + // Effectively multiplying with a 0.0 weighting factor + dest = 0.0; + } +} + +void ImageSet::getLinearIntegratedWithNormalChannels(Image& dest) const { + const bool useAllPolarizations = _linkedPolarizations.empty(); + if (_deconvolutionTable.DeconvolutionGroups().size() == 1 && + _deconvolutionTable.OriginalGroups().front().size() == 1) { + const DeconvolutionTable::Group& originalGroup = + _deconvolutionTable.OriginalGroups().front(); + const DeconvolutionTableEntry& entry = *originalGroup.front(); + dest = entryToImage(entry); + } else { + bool isFirst = true; + double weightSum = 0.0; + for (size_t chIndex = 0; chIndex != NDeconvolutionChannels(); ++chIndex) { + const DeconvolutionTable::Group& originalGroup = + _deconvolutionTable.FirstOriginalGroup(chIndex); + const double groupWeight = _weights[chIndex]; + // if the groupWeight is zero, the image might contain NaNs, so we + // shouldn't add it to the total in that case. + if (groupWeight != 0.0) { + weightSum += groupWeight; + for (const DeconvolutionTableEntry* entry_ptr : originalGroup) { + if (useAllPolarizations || + _linkedPolarizations.count(entry_ptr->polarization) != 0) { + if (isFirst) { + assignMultiply(dest, entryToImage(*entry_ptr), groupWeight); + isFirst = false; + } else { + dest.AddWithFactor(entryToImage(*entry_ptr), groupWeight); + } + } + } + } + } + if (weightSum > 0.0) + dest *= _polarizationNormalizationFactor / weightSum; + else + dest = 0.0; + } +} + +void ImageSet::CalculateDeconvolutionFrequencies( + const DeconvolutionTable& groupTable, + aocommon::UVector<double>& frequencies, aocommon::UVector<float>& weights) { + const size_t nInputChannels = groupTable.OriginalGroups().size(); + const size_t nDeconvolutionChannels = groupTable.DeconvolutionGroups().size(); + frequencies.assign(nDeconvolutionChannels, 0.0); + weights.assign(nDeconvolutionChannels, 0.0); + std::vector<double> unweightedFrequencies(nDeconvolutionChannels, 0.0); + std::vector<size_t> counts(nDeconvolutionChannels, 0); + for (size_t i = 0; i != nInputChannels; ++i) { + const DeconvolutionTableEntry& entry = + *groupTable.OriginalGroups()[i].front(); + const double freq = entry.CentralFrequency(); + const double weight = entry.image_weight; + const size_t deconvolutionChannel = + i * nDeconvolutionChannels / nInputChannels; + + frequencies[deconvolutionChannel] += freq * weight; + weights[deconvolutionChannel] += weight; + + unweightedFrequencies[deconvolutionChannel] += freq; + ++counts[deconvolutionChannel]; + } + for (size_t i = 0; i != nDeconvolutionChannels; ++i) { + // Even when there is no data for a given frequency and the weight + // is zero, it is still desirable to have a proper value for the frequency + // (e.g. for extrapolating flux). + if (weights[i] > 0.0) + frequencies[i] /= weights[i]; + else + frequencies[i] = unweightedFrequencies[i] / counts[i]; + } +} + +void ImageSet::GetIntegratedPSF(Image& dest, + const std::vector<aocommon::Image>& psfs) { + assert(psfs.size() == NDeconvolutionChannels()); + + const size_t image_size = Width() * Height(); + + if (NDeconvolutionChannels() == 1) { + dest = psfs.front(); + } else { + bool isFirst = true; + double weightSum = 0.0; + for (size_t channel = 0; channel != NDeconvolutionChannels(); ++channel) { + assert(psfs[channel].Size() == image_size); + + const double groupWeight = _weights[channel]; + // if the groupWeight is zero, the image might contain NaNs, so we + // shouldn't add it to the total in that case. + if (groupWeight != 0.0) { + weightSum += groupWeight; + if (isFirst) { + dest = psfs[channel]; + dest *= groupWeight; + isFirst = false; + } else { + dest.AddWithFactor(psfs[channel], groupWeight); + } + } + } + const double factor = weightSum == 0.0 ? 0.0 : 1.0 / weightSum; + dest *= factor; + } +} +} // namespace radler \ No newline at end of file diff --git a/cpp/image_set.h b/cpp/image_set.h new file mode 100644 index 00000000..b15fcc93 --- /dev/null +++ b/cpp/image_set.h @@ -0,0 +1,337 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef RADLER_IMAGE_SET_H_ +#define RADLER_IMAGE_SET_H_ + +#include <map> +#include <memory> +#include <vector> + +#include <aocommon/image.h> +#include <aocommon/uvector.h> + +#include <schaapcommon/fitters/spectralfitter.h> + +#include "deconvolution_table.h" + +namespace radler { + +class ImageSet { + public: + ImageSet(const DeconvolutionTable& table, bool squared_joins, + const std::set<aocommon::PolarizationEnum>& linked_polarizations, + size_t width, size_t height); + + /** + * Constructs an ImageSet with the same settings as an existing ImageSet + * object, but a different image size. + * + * @param image_set An existing ImageSet. + * @param width The image width for the new ImageSet. + * @param height The image height for the new ImageSet. + */ + ImageSet(const ImageSet& image_set, size_t width, size_t height); + + ImageSet(const ImageSet&) = delete; + ImageSet& operator=(const ImageSet&) = delete; + + aocommon::Image Release(size_t imageIndex) { + return std::move(_images[imageIndex]); + } + + void SetImage(size_t imageIndex, aocommon::Image&& data) { + assert(data.Width() == Width() && data.Height() == Height()); + _images[imageIndex] = std::move(data); + } + + /** + * Replace the images of this ImageSet. The images may be of a different size. + * Both ImageSets are expected to be for the same deconvolution configuration: + * besides the images and their dimension, no fields are changed. + */ + void SetImages(ImageSet&& source); + + /** + * @param use_residual_images: True: Load residual images. False: Load model + * images. + */ + void LoadAndAverage(bool use_residual_images); + + std::vector<aocommon::Image> LoadAndAveragePSFs(); + + void InterpolateAndStoreModel( + const schaapcommon::fitters::SpectralFitter& fitter, size_t threadCount); + + void AssignAndStoreResidual(); + + /** + * This function will calculate the integration over all images, squaring + * images that are in the same squared-image group. For example, with + * a squared group of [I, Q, ..] and another group [I2, Q2, ...], this + * will calculate: + * + * sqrt(I^2 + Q^2 + ..) + sqrt(I2^2 + Q2^2 ..) + .. + * ---------------------------------------------- + * 1 + 1 + .. + * + * If the 'squared groups' are of size 1, the average of the groups will be + * returned (i.e., without square-rooting the square). + * + * If the squared joining option is set in the provided wsclean settings, the + * behaviour of this method changes. In that case, it will return the square + * root of the average squared value: + * + * I^2 + Q^2 + .. + I2^2 + Q2^2 .. + .. + * sqrt( --------------------------------------- ) + * 1 + 1 + .. + * + * These formulae are such that the values will have normal flux values. + * @param dest Pre-allocated output array that will be filled with the + * integrated image. + * @param scratch Pre-allocated scratch space, same size as image. + */ + void GetSquareIntegrated(aocommon::Image& dest, + aocommon::Image& scratch) const { + if (_squareJoinedChannels) + getSquareIntegratedWithSquaredChannels(dest); + else + getSquareIntegratedWithNormalChannels(dest, scratch); + } + + /** + * This function will calculate the 'linear' integration over all images, + * unless joined channels are requested to be squared. The method will return + * the weighted average of all images. Normally, + * @ref GetSquareIntegrated + * should be used for peak finding, but in case negative values should remain + * negative, such as with multiscale (otherwise a sidelobe will be fitted with + * large scales), this function can be used. + * @param dest Pre-allocated output array that will be filled with the average + * values. + */ + void GetLinearIntegrated(aocommon::Image& dest) const { + if (_squareJoinedChannels) + getSquareIntegratedWithSquaredChannels(dest); + else + getLinearIntegratedWithNormalChannels(dest); + } + + void GetIntegratedPSF(aocommon::Image& dest, + const std::vector<aocommon::Image>& psfs); + + size_t NOriginalChannels() const { + return _deconvolutionTable.OriginalGroups().size(); + } + + size_t PSFCount() const { return NDeconvolutionChannels(); } + + size_t NDeconvolutionChannels() const { + return _deconvolutionTable.DeconvolutionGroups().size(); + } + + ImageSet& operator=(float val) { + for (aocommon::Image& image : _images) image = val; + return *this; + } + + /** + * Exposes image data. + * + * ImageSet only exposes a non-const pointer to the image data. When exposing + * non-const reference to the images themselves, the user could change the + * image size and violate the invariant that all images have equal sizes. + * @param index An image index. + * @return A non-const pointer to the data area for the image. + */ + float* Data(size_t index) { return _images[index].Data(); } + + /** + * Exposes the images in the image set. + * + * Creating a non-const version of this operator is not desirable. See Data(). + * + * @param index An image index. + * @return A const reference to the image with the given index. + */ + const aocommon::Image& operator[](size_t index) const { + return _images[index]; + } + + size_t size() const { return _images.size(); } + + size_t PSFIndex(size_t imageIndex) const { + return _imageIndexToPSFIndex[imageIndex]; + } + + const DeconvolutionTable& Table() const { return _deconvolutionTable; } + + std::unique_ptr<ImageSet> Trim(size_t x1, size_t y1, size_t x2, size_t y2, + size_t oldWidth) const { + auto p = std::make_unique<ImageSet>(*this, x2 - x1, y2 - y1); + for (size_t i = 0; i != _images.size(); ++i) { + copySmallerPart(_images[i], p->_images[i], x1, y1, x2, y2, oldWidth); + } + return p; + } + + /** + * Like Trim(), but only copies values that are in the mask. All other values + * are set to zero. + * @param mask A mask of size (x2-x1) x (y2-y1) + */ + std::unique_ptr<ImageSet> TrimMasked(size_t x1, size_t y1, size_t x2, + size_t y2, size_t oldWidth, + const bool* mask) const { + std::unique_ptr<ImageSet> p = Trim(x1, y1, x2, y2, oldWidth); + for (aocommon::Image& image : p->_images) { + for (size_t pixel = 0; pixel != image.Size(); ++pixel) { + if (!mask[pixel]) image[pixel] = 0.0; + } + } + return p; + } + + void CopyMasked(const ImageSet& fromImageSet, size_t toX, size_t toY, + const bool* fromMask) { + for (size_t i = 0; i != _images.size(); ++i) { + aocommon::Image::CopyMasked( + _images[i].Data(), toX, toY, _images[i].Width(), + fromImageSet._images[i].Data(), fromImageSet._images[i].Width(), + fromImageSet._images[i].Height(), fromMask); + } + } + + /** + * Place all images in @c from onto the images in this ImageSet at a + * given position. The dimensions of @c from can be smaller or equal + * to ones in this. + */ + void AddSubImage(const ImageSet& from, size_t toX, size_t toY) { + for (size_t i = 0; i != _images.size(); ++i) { + aocommon::Image::AddSubImage(_images[i].Data(), toX, toY, + _images[i].Width(), from._images[i].Data(), + from._images[i].Width(), + from._images[i].Height()); + } + } + + ImageSet& operator*=(float factor) { + for (aocommon::Image& image : _images) image *= factor; + return *this; + } + + ImageSet& operator+=(const ImageSet& other) { + for (size_t i = 0; i != size(); ++i) _images[i] += other._images[i]; + return *this; + } + + void FactorAdd(ImageSet& rhs, double factor) { + for (size_t i = 0; i != size(); ++i) + _images[i].AddWithFactor(rhs._images[i], factor); + } + + bool SquareJoinedChannels() const { return _squareJoinedChannels; } + + const std::set<aocommon::PolarizationEnum>& LinkedPolarizations() const { + return _linkedPolarizations; + } + + size_t Width() const { return _images.front().Width(); } + + size_t Height() const { return _images.front().Height(); } + + static void CalculateDeconvolutionFrequencies( + const DeconvolutionTable& groupTable, + aocommon::UVector<double>& frequencies, + aocommon::UVector<float>& weights); + + private: + void initializeIndices(); + + void initializePolFactor() { + const DeconvolutionTable::Group& firstChannelGroup = + _deconvolutionTable.OriginalGroups().front(); + std::set<aocommon::PolarizationEnum> pols; + bool all_stokes_without_i = true; + for (const DeconvolutionTableEntry* entry : firstChannelGroup) { + if (_linkedPolarizations.empty() || + _linkedPolarizations.count(entry->polarization) != 0) { + if (!aocommon::Polarization::IsStokes(entry->polarization) || + entry->polarization == aocommon::Polarization::StokesI) + all_stokes_without_i = false; + pols.insert(entry->polarization); + } + } + const bool isDual = + pols.size() == 2 && aocommon::Polarization::HasDualPolarization(pols); + const bool isFull = + pols.size() == 4 && + (aocommon::Polarization::HasFullLinearPolarization(pols) || + aocommon::Polarization::HasFullCircularPolarization(pols)); + if (all_stokes_without_i) + _polarizationNormalizationFactor = 1.0 / pols.size(); + else if (isDual || isFull) + _polarizationNormalizationFactor = 0.5; + else + _polarizationNormalizationFactor = 1.0; + } + + static void copySmallerPart(const aocommon::Image& input, + aocommon::Image& output, size_t x1, size_t y1, + size_t x2, size_t y2, size_t oldWidth) { + size_t newWidth = x2 - x1; + for (size_t y = y1; y != y2; ++y) { + const float* oldPtr = &input[y * oldWidth]; + float* newPtr = &output[(y - y1) * newWidth]; + for (size_t x = x1; x != x2; ++x) { + newPtr[x - x1] = oldPtr[x]; + } + } + } + + static void copyToLarger(aocommon::Image& to, size_t toX, size_t toY, + size_t toWidth, const aocommon::Image& from, + size_t fromWidth, size_t fromHeight) { + for (size_t y = 0; y != fromHeight; ++y) { + std::copy(from.Data() + y * fromWidth, from.Data() + (y + 1) * fromWidth, + to.Data() + toX + (toY + y) * toWidth); + } + } + + static void copyToLarger(aocommon::Image& to, size_t toX, size_t toY, + size_t toWidth, const aocommon::Image& from, + size_t fromWidth, size_t fromHeight, + const bool* fromMask) { + for (size_t y = 0; y != fromHeight; ++y) { + for (size_t x = 0; x != fromWidth; ++x) { + if (fromMask[y * fromWidth + x]) + to[toX + (toY + y) * toWidth + x] = from[y * fromWidth + x]; + } + } + } + + void getSquareIntegratedWithNormalChannels(aocommon::Image& dest, + aocommon::Image& scratch) const; + + void getSquareIntegratedWithSquaredChannels(aocommon::Image& dest) const; + + void getLinearIntegratedWithNormalChannels(aocommon::Image& dest) const; + + const aocommon::Image& entryToImage( + const DeconvolutionTableEntry& entry) const { + return _images[_entryIndexToImageIndex[entry.index]]; + } + + std::vector<aocommon::Image> _images; + // Weight of each deconvolution channels + aocommon::UVector<float> _weights; + bool _squareJoinedChannels; + const DeconvolutionTable& _deconvolutionTable; + std::vector<size_t> _entryIndexToImageIndex; + aocommon::UVector<size_t> _imageIndexToPSFIndex; + float _polarizationNormalizationFactor; + std::set<aocommon::PolarizationEnum> _linkedPolarizations; +}; +} // namespace radler +#endif // RADLER_IMAGE_SET_H_ diff --git a/cpp/logging/controllable_log.h b/cpp/logging/controllable_log.h new file mode 100644 index 00000000..9a1fea35 --- /dev/null +++ b/cpp/logging/controllable_log.h @@ -0,0 +1,67 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef RADLER_LOGGING_CONTROLLABLE_LOG_H_ +#define RADLER_LOGGING_CONTROLLABLE_LOG_H_ + +#include <mutex> +#include <string> +#include <vector> + +#include <aocommon/logger.h> + +namespace radler::logging { + +class ControllableLog final : public aocommon::LogReceiver { + public: + ControllableLog(std::mutex* mutex) + : _mutex(mutex), _isMuted(false), _isActive(true) {} + + virtual ~ControllableLog() {} + + void Mute(bool mute) { _isMuted = mute; } + bool IsMuted() const { return _isMuted; } + + void Activate(bool active) { _isActive = active; } + bool IsActive() const { return _isActive; } + + void SetTag(const std::string& tag) { _tag = tag; } + void SetOutputOnce(const std::string& str) { _outputOnce = str; } + + private: + void Output(enum aocommon::Logger::LoggerLevel level, + const std::string& str) override { + if (!str.empty()) { + std::lock_guard<std::mutex> lock(*_mutex); + + bool skip = ((level == aocommon::Logger::kDebugLevel || + level == aocommon::Logger::kInfoLevel) && + _isMuted) || + (level == aocommon::Logger::kDebugLevel && + !aocommon::Logger::IsVerbose()); + + if (!skip) { + _lineBuffer += str; + if (_lineBuffer.back() == '\n') { + if (!_outputOnce.empty()) { + Forward(level, _outputOnce); + _outputOnce.clear(); + } + Forward(level, _tag); + Forward(level, _lineBuffer); + _lineBuffer.clear(); + } + } + } + } + + std::mutex* _mutex; + std::string _tag; + bool _isMuted; + bool _isActive; + std::string _lineBuffer; + std::string _outputOnce; +}; +} // namespace radler::logging + +#endif diff --git a/cpp/logging/subimage_logset.h b/cpp/logging/subimage_logset.h new file mode 100644 index 00000000..6f8396b6 --- /dev/null +++ b/cpp/logging/subimage_logset.h @@ -0,0 +1,104 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef RADLER_LOGGING_SUBIMAGELOGSET_H_ +#define RADLER_LOGGING_SUBIMAGELOGSET_H_ + +#include "logging/controllable_log.h" + +namespace radler::logging { +/** + * @brief Thread safe logger for the set of subimages in the deconvolution. + */ +class SubImageLogSet { + public: + SubImageLogSet() : n_horizontal_(0), n_vertical_(0) {} + + void Initialize(size_t n_horizontal, size_t n_vertical) { + std::lock_guard<std::mutex> lock(output_mutex_); + n_horizontal_ = n_horizontal; + n_vertical_ = n_vertical; + const size_t n = n_horizontal * n_vertical; + logs_.clear(); + logs_.reserve(n); + for (size_t i = 0; i != n; ++i) { + logs_.emplace_back(&output_mutex_); + logs_[i].SetTag("P" + std::to_string(i) + ": "); + logs_[i].Mute(true); + logs_[i].Activate(false); + } + } + + ControllableLog& operator[](size_t index) { return logs_[index]; } + + void Activate(size_t index) { + std::lock_guard<std::mutex> o_lock(output_mutex_); + if (!logs_[index].IsActive()) { + logs_[index].Activate(true); + + UnmuteMostCentral(); + } + } + + void Deactivate(size_t index) { + std::lock_guard<std::mutex> o_lock(output_mutex_); + if (logs_[index].IsActive()) { + logs_[index].Mute(true); + logs_[index].SetOutputOnce(std::string()); + logs_[index].Activate(false); + + UnmuteMostCentral(); + } + } + + private: + void UnmuteMostCentral() { + size_t unmuted_log = logs_.size(); + for (size_t i = 0; i != logs_.size(); ++i) { + if (!logs_[i].IsMuted()) unmuted_log = i; + logs_[i].SetTag("P" + std::to_string(i) + ": "); + logs_[i].Mute(true); + } + + // Find an active subimage that is as close as possible to + // the centre (since these often contain the most interesting info) + bool found = false; + size_t si_x = 0; + size_t si_y = 0; + size_t si_d = 0; + for (size_t y = 0; y != n_vertical_; ++y) { + for (size_t x = 0; x != n_horizontal_; ++x) { + const size_t index = y * n_horizontal_ + x; + if (logs_[index].IsActive()) { + const int dx = int(x) - int(n_horizontal_) / 2; + const int dy = int(y) - int(n_vertical_) / 2; + const size_t dist_squared = dx * dx + dy * dy; + if (!found || dist_squared < si_d) { + si_x = x; + si_y = y; + si_d = dist_squared; + found = true; + } + } + } + } + + if (found) { + const size_t index = si_y * n_horizontal_ + si_x; + logs_[index].Mute(false); + logs_[index].SetTag(" "); + + if (index != unmuted_log) { + logs_[index].SetOutputOnce("Switching to output of subimage " + + std::to_string(index) + "\n"); + } + } + } + + std::mutex output_mutex_; + std::vector<ControllableLog> logs_; + size_t n_horizontal_; + size_t n_vertical_; +}; +} // namespace radler::logging +#endif \ No newline at end of file diff --git a/cpp/math/dijkstrasplitter.h b/cpp/math/dijkstrasplitter.h new file mode 100644 index 00000000..6aaf768f --- /dev/null +++ b/cpp/math/dijkstrasplitter.h @@ -0,0 +1,320 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef RADLER_MATH_DIJKSTRASPLITTER_H_ +#define RADLER_MATH_DIJKSTRASPLITTER_H_ + +#include <aocommon/uvector.h> + +#include <algorithm> +#include <cmath> +#include <limits> +#include <queue> + +namespace radler::math { + +class DijkstraSplitter { + public: + DijkstraSplitter(size_t width, size_t height) + : _width(width), _height(height) {} + + struct Coord { + Coord() = default; + Coord(size_t _x, size_t _y) : x(_x), y(_y) {} + size_t x, y; + }; + + struct Visit { + float distance; + Coord to, from; + bool operator<(const Visit& rhs) const { return distance > rhs.distance; } + }; + + void AddVerticalDivider(const float* image, float* scratch, float* output, + size_t x1, size_t x2) const { + DivideVertically(image, scratch, x1, x2); + for (size_t y = 0; y != _height; ++y) { + for (size_t i = y * _width + x1; i != y * _width + x2; ++i) + output[i] += scratch[i]; + } + } + + /** + * Find the shortest vertical path through an image. The path is + * constrained to lie between vertical lines given by x1 and x2. + * The output is set to 1 for pixels that are part of the path, and + * set to 0 otherwise. The reason it's a floating point is because + * it is also used as scratch. + */ + void DivideVertically(const float* image, float* output, size_t x1, + size_t x2) const { + using visitset = std::priority_queue<Visit>; + visitset visits; + + for (size_t x = x1; x != x2; ++x) { + Visit visit; + visit.distance = 0.0; + visit.to = Coord(x, 0); + visit.from = Coord(x, 0); + visits.push(visit); + } + aocommon::UVector<Coord> path((x2 - x1) * _height); + fillColumns(output, x1, x2, std::numeric_limits<float>::max()); + Visit visit; + while (!visits.empty()) { + visit = visits.top(); + visits.pop(); + size_t x = visit.to.x, y = visit.to.y; + if (y == _height) break; + float curDistance = output[x + y * _width]; + float newDistance = visit.distance + std::fabs(image[x + y * _width]); + if (newDistance < curDistance) { + output[x + y * _width] = newDistance; + path[x - x1 + y * (x2 - x1)] = visit.from; + visit.distance = newDistance; + visit.from = visit.to; + if (x > x1) { + visit.to = Coord(x - 1, y + 1); + visits.push(visit); + visit.to = Coord(x - 1, y); + visits.push(visit); + } + visit.to = Coord(x, y + 1); + visits.push(visit); + if (x < x2 - 1) { + visit.to = Coord(x + 1, y + 1); + visits.push(visit); + visit.to = Coord(x + 1, y); + visits.push(visit); + } + } + } + fillColumns(output, x1, x2, 0.0); + Coord pCoord = visit.from; + while (pCoord.y > 0) { + output[pCoord.x + pCoord.y * _width] = 1.0; + pCoord = path[pCoord.x - x1 + pCoord.y * (x2 - x1)]; + } + output[pCoord.x] = 1.0; + } + + void AddHorizontalDivider(const float* image, float* scratch, float* output, + size_t y1, size_t y2) const { + DivideHorizontally(image, scratch, y1, y2); + for (size_t i = y1 * _width; i != y2 * _width; ++i) output[i] += scratch[i]; + } + + /** + * Like DivideVertically, but for horizontal paths. The path is + * constrained to lie between horizontal lines given by y1 and y2. + */ + void DivideHorizontally(const float* image, float* output, size_t y1, + size_t y2) const { + using visitset = std::priority_queue<Visit>; + visitset visits; + + for (size_t y = y1; y != y2; ++y) { + Visit visit; + visit.distance = 0.0; + visit.to = Coord(0, y); + visit.from = Coord(0, y); + visits.push(visit); + } + aocommon::UVector<Coord> path(_width * (y2 - y1)); + std::fill(output + y1 * _width, output + y2 * _width, + std::numeric_limits<float>::max()); + Visit visit; + while (!visits.empty()) { + visit = visits.top(); + visits.pop(); + size_t x = visit.to.x, y = visit.to.y; + if (x == _width) break; + float curDistance = output[x + y * _width]; + float newDistance = visit.distance + std::fabs(image[x + y * _width]); + if (newDistance < curDistance) { + output[x + y * _width] = newDistance; + path[x + (y - y1) * _width] = visit.from; + visit.distance = newDistance; + visit.from = visit.to; + if (y > y1) { + visit.to = Coord(x + 1, y - 1); + visits.push(visit); + visit.to = Coord(x, y - 1); + visits.push(visit); + } + visit.to = Coord(x + 1, y); + visits.push(visit); + if (y < y2 - 1) { + visit.to = Coord(x + 1, y + 1); + visits.push(visit); + visit.to = Coord(x, y + 1); + visits.push(visit); + } + } + } + std::fill(output + y1 * _width, output + y2 * _width, 0.0); + Coord pCoord = visit.from; + while (pCoord.x > 0) { + output[pCoord.x + pCoord.y * _width] = 1.0; + pCoord = path[pCoord.x + (pCoord.y - y1) * _width]; + } + output[pCoord.y * _width] = 1.0; + } + + /** + * Mask the space between (typically) two vertical divisions. + * @param subdivision An image that is the result of earlier calls + * to DivideVertically(). + * @param subImgX An x-position that is in between the two splits. + * @param mask A mask image for which pixels will be set to true if + * and only if they are part of the area specified by the + * two divisions. + * @param [out] x The left side of the bounding box of the divisions. + * @param [out] subWidth The bounding width of the two divisions. + */ + void FloodVerticalArea(const float* subdivision, size_t subImgX, bool* mask, + size_t& x, size_t& subWidth) const { + std::fill(mask, mask + _width * _height, false); + x = _width; + size_t x2 = 0; + for (size_t y = 0; y != _height; ++y) { + const float* dRow = &subdivision[y * _width]; + bool* maskRow = &mask[y * _width]; + int xIter = subImgX; + // Move to the left until a border is hit + while (xIter >= 0 && dRow[xIter] == 0.0) { + maskRow[xIter] = true; + --xIter; + } + // Continue to the left through the border + while (xIter >= 0 && dRow[xIter] != 0.0) { + maskRow[xIter] = true; + --xIter; + } + x = std::min<size_t>(x, xIter + 1); + xIter = subImgX + 1; + // Move to the right until a border is hit + while (size_t(xIter) < _width && dRow[xIter] == 0.0) { + maskRow[xIter] = true; + ++xIter; + } + x2 = std::max<size_t>(x2, xIter); + } + if (x2 < x) + subWidth = 0; + else + subWidth = x2 - x; + } + + /** + * Like FloodVerticalArea(), but for horizontal flooding. + */ + void FloodHorizontalArea(const float* subdivision, size_t subImgY, bool* mask, + size_t& y, size_t& subHeight) const { + std::fill(mask, mask + _width * _height, false); + y = _height; + size_t y2 = 0; + for (size_t x = 0; x != _width; ++x) { + int yIter = subImgY; + // Move up until a border is hit + while (yIter >= 0 && subdivision[yIter * _width + x] == 0.0) { + mask[yIter * _width + x] = true; + --yIter; + } + // Continue to the left through the border + while (yIter >= 0 && subdivision[yIter * _width + x] != 0.0) { + mask[yIter * _width + x] = true; + --yIter; + } + y = std::min<size_t>(y, yIter + 1); + yIter = subImgY + 1; + // Move to the right until a border is hit + while (size_t(yIter) < _height && + subdivision[yIter * _width + x] == 0.0) { + mask[yIter * _width + x] = true; + ++yIter; + } + y2 = std::max<size_t>(y2, yIter); + } + if (y2 < y) + subHeight = 0; + else + subHeight = y2 - y; + } + + /** + * Combines a horizontally and vertically filled area and extracts a + * single mask where the areas overlap. + * @param vMask Mask returned by FloodHorizontalArea(), but trimmed + * to have the specified width. + * @param vMaskX x-value returned by FloodHorizontalArea(). + * @param vMaskWidth Width return by FloodHorizontalArea(), and width + * of vMask. + * @param hMask Mask returned by FloodVerticalArea(). + * @param [in,out] mask Result + * @param [out] subX Bounding box x-value + * @param [out] subY Bounding box y-value + * @param [out] subWidth Bounding box width + * @param [out] subHeight Bounding box height + */ + void GetBoundingMask(const bool* vMask, size_t vMaskX, size_t vMaskWidth, + const bool* hMask, bool* mask, size_t& subX, + size_t& subY, size_t& subWidth, + size_t& subHeight) const { + subX = vMaskWidth + vMaskX; + subY = _height; + size_t subX2 = 0, subY2 = 0; + for (size_t y = 0; y != _height; ++y) { + const bool* vMaskRow = &vMask[y * vMaskWidth]; + const bool* hMaskRow = &hMask[y * _width]; + bool* maskRow = &mask[y * _width]; + for (size_t x = 0; x != vMaskWidth; ++x) { + size_t hx = x + vMaskX; + if (vMaskRow[x] && hMaskRow[hx]) { + maskRow[hx] = true; + subX = std::min(hx, subX); + subY = std::min(y, subY); + subX2 = std::max(hx, subX2); + subY2 = std::max(y, subY2); + } else + maskRow[hx] = false; + } + } + if (subX2 < subX) { + subWidth = 0; + subHeight = 0; + } else { + subWidth = subX2 + 1 - subX; + subHeight = subY2 + 1 - subY; + } + // If dimensions start off even, keep subimages even too + if (_width % 2 == 0) { + if (subWidth % 2 != 0) { + ++subWidth; + if (subWidth + subX >= _width) --subX; + } + } + if (_height % 2 == 0) { + if (subHeight % 2 != 0) { + ++subHeight; + if (subHeight + subY >= _height) --subY; + } + } + } + + private: + /** + * This function sets a rectangular area given by 0 <= y < height and xStart + * <= x < xEnd. + */ + void fillColumns(float* output, size_t xStart, size_t xEnd, + float newValue) const { + for (size_t y = 0; y != _height; ++y) { + std::fill(output + _width * y + xStart, output + _width * y + xEnd, + newValue); + } + } + size_t _width, _height; +}; +} // namespace radler::math +#endif diff --git a/cpp/math/peak_finder.cc b/cpp/math/peak_finder.cc new file mode 100644 index 00000000..6ef3e1f0 --- /dev/null +++ b/cpp/math/peak_finder.cc @@ -0,0 +1,262 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "math/peak_finder.h" + +#ifdef __SSE__ +#define USE_INTRINSICS +#endif + +#ifdef USE_INTRINSICS +#include <emmintrin.h> +#include <immintrin.h> +#endif + +#include <limits> + +namespace radler::math { + +std::optional<float> PeakFinder::Simple(const float* image, size_t width, + size_t height, size_t& x, size_t& y, + bool allowNegativeComponents, + size_t startY, size_t endY, + size_t horizontalBorder, + size_t verticalBorder) { + float peakMax = std::numeric_limits<float>::min(); + size_t peakIndex = width * height; + + size_t xiStart = horizontalBorder, xiEnd = width - horizontalBorder; + size_t yiStart = std::max(startY, verticalBorder), + yiEnd = std::min(endY, height - verticalBorder); + if (xiEnd < xiStart) xiEnd = xiStart; + if (yiEnd < yiStart) yiEnd = yiStart; + + for (size_t yi = yiStart; yi != yiEnd; ++yi) { + size_t index = yi * width + xiStart; + for (size_t xi = xiStart; xi != xiEnd; ++xi) { + float value = image[index]; + if (allowNegativeComponents) value = std::fabs(value); + if (value > peakMax) { + peakIndex = index; + peakMax = std::fabs(value); + } + ++value; + ++index; + } + } + if (peakIndex == width * height) { + x = width; + y = height; + return std::optional<float>(); + } else { + x = peakIndex % width; + y = peakIndex / width; + return image[x + y * width]; + } +} + +std::optional<double> PeakFinder::Simple(const double* image, size_t width, + size_t height, size_t& x, size_t& y, + bool allowNegativeComponents, + size_t startY, size_t endY, + size_t horizontalBorder, + size_t verticalBorder) { + double peakMax = std::numeric_limits<double>::min(); + size_t peakIndex = width * height; + + size_t xiStart = horizontalBorder, xiEnd = width - horizontalBorder; + size_t yiStart = std::max(startY, verticalBorder), + yiEnd = std::min(endY, height - verticalBorder); + if (xiEnd < xiStart) xiEnd = xiStart; + if (yiEnd < yiStart) yiEnd = yiStart; + + for (size_t yi = yiStart; yi != yiEnd; ++yi) { + size_t index = yi * width + xiStart; + for (size_t xi = xiStart; xi != xiEnd; ++xi) { + double value = image[index]; + if (allowNegativeComponents) value = std::fabs(value); + if (value > peakMax) { + peakIndex = index; + peakMax = std::fabs(value); + } + ++value; + ++index; + } + } + if (peakIndex == width * height) { + x = width; + y = height; + return {}; + } else { + x = peakIndex % width; + y = peakIndex / width; + return image[x + y * width]; + } +} + +std::optional<float> PeakFinder::FindWithMask( + const float* image, size_t width, size_t height, size_t& x, size_t& y, + bool allowNegativeComponents, size_t startY, size_t endY, + const bool* cleanMask, size_t horizontalBorder, size_t verticalBorder) { + float peakMax = std::numeric_limits<float>::min(); + x = width; + y = height; + + size_t xiStart = horizontalBorder, xiEnd = width - horizontalBorder; + size_t yiStart = std::max(startY, verticalBorder), + yiEnd = std::min(endY, height - verticalBorder); + if (xiEnd < xiStart) xiEnd = xiStart; + if (yiEnd < yiStart) yiEnd = yiStart; + + for (size_t yi = yiStart; yi != yiEnd; ++yi) { + const float* imgIter = &image[yi * width + xiStart]; + const bool* cleanMaskPtr = &cleanMask[yi * width + xiStart]; + for (size_t xi = xiStart; xi != xiEnd; ++xi) { + float value = *imgIter; + if (allowNegativeComponents) value = std::fabs(value); + if (value > peakMax && *cleanMaskPtr) { + x = xi; + y = yi; + peakMax = std::fabs(value); + } + ++imgIter; + ++cleanMaskPtr; + } + } + if (y == height) + return std::optional<float>(); + else + return image[x + y * width]; +} + +#if defined __AVX__ && defined USE_INTRINSICS && !defined FORCE_NON_AVX +template <bool AllowNegativeComponent> +std::optional<double> PeakFinder::AVX(const double* image, size_t width, + size_t height, size_t& x, size_t& y, + size_t startY, size_t endY, + size_t horizontalBorder, + size_t verticalBorder) { + double peakMax = std::numeric_limits<double>::min(); + size_t peakIndex = 0; + + __m256d mPeakMax = _mm256_set1_pd(peakMax); + + size_t xiStart = horizontalBorder, xiEnd = width - horizontalBorder; + size_t yiStart = std::max(startY, verticalBorder), + yiEnd = std::min(endY, height - verticalBorder); + if (xiEnd < xiStart) xiEnd = xiStart; + if (yiEnd < yiStart) yiEnd = yiStart; + + for (size_t yi = yiStart; yi != yiEnd; ++yi) { + size_t index = yi * width + xiStart; + const double* const endPtr = image + yi * width + xiEnd - 4; + const double* i = image + index; + for (; i < endPtr; i += 4) { + __m256d val = _mm256_loadu_pd(i); + if (AllowNegativeComponent) { + __m256d negVal = _mm256_sub_pd(_mm256_set1_pd(0.0), val); + val = _mm256_max_pd(val, negVal); + } + int mask = _mm256_movemask_pd(_mm256_cmp_pd(val, mPeakMax, _CMP_GT_OQ)); + if (mask != 0) { + for (size_t di = 0; di != 4; ++di) { + double value = i[di]; + if (AllowNegativeComponent) value = std::fabs(value); + if (value > peakMax) { + peakIndex = index + di; + peakMax = std::fabs(i[di]); + mPeakMax = _mm256_set1_pd(peakMax); + } + } + } + index += 4; + } + for (; i != endPtr + 4; ++i) { + double value = *i; + if (AllowNegativeComponent) value = std::fabs(value); + if (value > peakMax) { + peakIndex = index; + peakMax = std::fabs(*i); + } + ++index; + } + } + x = peakIndex % width; + y = peakIndex / width; + return image[x + y * width]; +} + +template std::optional<double> PeakFinder::AVX<false>( + const double* image, size_t width, size_t height, size_t& x, size_t& y, + size_t startY, size_t endY, size_t horizontalBorder, size_t verticalBorder); +template std::optional<double> PeakFinder::AVX<true>( + const double* image, size_t width, size_t height, size_t& x, size_t& y, + size_t startY, size_t endY, size_t horizontalBorder, size_t verticalBorder); + +template <bool AllowNegativeComponent> +std::optional<float> PeakFinder::AVX(const float* image, size_t width, + size_t height, size_t& x, size_t& y, + size_t startY, size_t endY, + size_t horizontalBorder, + size_t verticalBorder) { + float peakMax = std::numeric_limits<float>::min(); + size_t peakIndex = 0; + + __m256 mPeakMax = _mm256_set1_ps(peakMax); + + size_t xiStart = horizontalBorder, xiEnd = width - horizontalBorder; + size_t yiStart = std::max(startY, verticalBorder), + yiEnd = std::min(endY, height - verticalBorder); + if (xiEnd < xiStart) xiEnd = xiStart; + if (yiEnd < yiStart) yiEnd = yiStart; + + for (size_t yi = yiStart; yi != yiEnd; ++yi) { + size_t index = yi * width + xiStart; + const float* const endPtr = image + yi * width + xiEnd - 8; + const float* i = image + index; + for (; i < endPtr; i += 8) { + __m256 val = _mm256_loadu_ps(i); + if (AllowNegativeComponent) { + __m256 negVal = _mm256_sub_ps(_mm256_set1_ps(0.0), val); + val = _mm256_max_ps(val, negVal); + } + int mask = _mm256_movemask_ps(_mm256_cmp_ps(val, mPeakMax, _CMP_GT_OQ)); + if (mask != 0) { + for (size_t di = 0; di != 8; ++di) { + double value = i[di]; + if (AllowNegativeComponent) value = std::fabs(value); + if (value > peakMax) { + peakIndex = index + di; + peakMax = std::fabs(i[di]); + mPeakMax = _mm256_set1_ps(peakMax); + } + } + } + index += 8; + } + for (; i != endPtr + 8; ++i) { + double value = *i; + if (AllowNegativeComponent) value = std::fabs(value); + if (value > peakMax) { + peakIndex = index; + peakMax = std::fabs(*i); + } + ++index; + } + } + x = peakIndex % width; + y = peakIndex / width; + return image[x + y * width]; +} + +template std::optional<float> PeakFinder::AVX<false>( + const float* image, size_t width, size_t height, size_t& x, size_t& y, + size_t startY, size_t endY, size_t horizontalBorder, size_t verticalBorder); +template std::optional<float> PeakFinder::AVX<true>( + const float* image, size_t width, size_t height, size_t& x, size_t& y, + size_t startY, size_t endY, size_t horizontalBorder, size_t verticalBorder); + +#else +#warning "Not using AVX optimized version of FindPeak()!" +#endif // __AVX__ +} // namespace radler::math \ No newline at end of file diff --git a/cpp/math/peak_finder.h b/cpp/math/peak_finder.h new file mode 100644 index 00000000..5ffba1ba --- /dev/null +++ b/cpp/math/peak_finder.h @@ -0,0 +1,129 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef RADLER_COMPONENTS_PEAK_FINDER_H_ +#define RADLER_COMPONENTS_PEAK_FINDER_H_ + +#include <cmath> +#include <cstring> +#include <optional> + +#ifdef __SSE__ +#define USE_INTRINSICS +#endif + +namespace radler::math { + +class PeakFinder { + public: + PeakFinder() = delete; + + static std::optional<float> Simple(const float* image, size_t width, + size_t height, size_t& x, size_t& y, + bool allowNegativeComponents, + size_t startY, size_t endY, + size_t horizontalBorder, + size_t verticalBorder); + + static std::optional<double> Simple(const double* image, size_t width, + size_t height, size_t& x, size_t& y, + bool allowNegativeComponents, + size_t startY, size_t endY, + size_t horizontalBorder, + size_t verticalBorder); + +#if defined __AVX__ && defined USE_INTRINSICS && !defined FORCE_NON_AVX + template <bool AllowNegativeComponent> + static std::optional<float> AVX(const float* image, size_t width, + size_t height, size_t& x, size_t& y, + size_t startY, size_t endY, + size_t horizontalBorder, + size_t verticalBorder); + + static std::optional<float> AVX(const float* image, size_t width, + size_t height, size_t& x, size_t& y, + bool allowNegativeComponents, size_t startY, + size_t endY, size_t horizontalBorder, + size_t verticalBorder) { + if (allowNegativeComponents) + return AVX<true>(image, width, height, x, y, startY, endY, + horizontalBorder, verticalBorder); + else + return AVX<false>(image, width, height, x, y, startY, endY, + horizontalBorder, verticalBorder); + } + + template <bool AllowNegativeComponent> + static std::optional<double> AVX(const double* image, size_t width, + size_t height, size_t& x, size_t& y, + size_t startY, size_t endY, + size_t horizontalBorder, + size_t verticalBorder); + + static std::optional<double> AVX(const double* image, size_t width, + size_t height, size_t& x, size_t& y, + bool allowNegativeComponents, size_t startY, + size_t endY, size_t horizontalBorder, + size_t verticalBorder) { + if (allowNegativeComponents) + return AVX<true>(image, width, height, x, y, startY, endY, + horizontalBorder, verticalBorder); + else + return AVX<false>(image, width, height, x, y, startY, endY, + horizontalBorder, verticalBorder); + } +#endif + + /** + * Find peaks with a relative border ratio. + */ + template <typename NumT> + static std::optional<NumT> Find(const NumT* image, size_t width, + size_t height, size_t& x, size_t& y, + bool allowNegativeComponents, size_t startY, + size_t endY, float borderRatio) { + return Find(image, width, height, x, y, allowNegativeComponents, startY, + endY, round(width * borderRatio), round(height * borderRatio)); + } + + /** + * Find peaks with a fixed border. + */ + template <typename NumT> + static std::optional<NumT> Find(const NumT* image, size_t width, + size_t height, size_t& x, size_t& y, + bool allowNegativeComponents, size_t startY, + size_t endY, size_t horizontalBorder, + size_t verticalBorder) { +#if defined __AVX__ && defined USE_INTRINSICS && !defined FORCE_NON_AVX + return AVX(image, width, height, x, y, allowNegativeComponents, startY, + endY, horizontalBorder, verticalBorder); +#else + return Simple(image, width, height, x, y, allowNegativeComponents, startY, + endY, horizontalBorder, verticalBorder); +#endif + } + + static std::optional<float> FindWithMask(const float* image, size_t width, + size_t height, size_t& x, size_t& y, + bool allowNegativeComponents, + const bool* cleanMask); + + static std::optional<float> FindWithMask(const float* image, size_t width, + size_t height, size_t& x, size_t& y, + bool allowNegativeComponents, + size_t startY, size_t endY, + const bool* cleanMask, + float borderRatio) { + return FindWithMask(image, width, height, x, y, allowNegativeComponents, + startY, endY, cleanMask, round(width * borderRatio), + round(height * borderRatio)); + } + + static std::optional<float> FindWithMask( + const float* image, size_t width, size_t height, size_t& x, size_t& y, + bool allowNegativeComponents, size_t startY, size_t endY, + const bool* cleanMask, size_t horizontalBorder, size_t verticalBorder); +}; +} // namespace radler::math +#endif diff --git a/cpp/math/rms_image.cc b/cpp/math/rms_image.cc new file mode 100644 index 00000000..dbf0ec12 --- /dev/null +++ b/cpp/math/rms_image.cc @@ -0,0 +1,96 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "math/rms_image.h" + +#include <aocommon/image.h> +#include <aocommon/staticfor.h> + +#include <schaapcommon/fft/restoreimage.h> + +using aocommon::Image; + +namespace radler::math { + +void RMSImage::Make(Image& rmsOutput, const Image& inputImage, + double windowSize, long double beamMaj, long double beamMin, + long double beamPA, long double pixelScaleL, + long double pixelScaleM, size_t threadCount) { + Image image(inputImage); + image.Square(); + rmsOutput = Image(image.Width(), image.Height(), 0.0); + + schaapcommon::fft::RestoreImage(rmsOutput.Data(), image.Data(), image.Width(), + image.Height(), beamMaj * windowSize, + beamMin * windowSize, beamPA, pixelScaleL, + pixelScaleM, threadCount); + + const double s = std::sqrt(2.0 * M_PI); + const long double sigmaMaj = beamMaj / (2.0L * sqrtl(2.0L * logl(2.0L))); + const long double sigmaMin = beamMin / (2.0L * sqrtl(2.0L * logl(2.0L))); + const double norm = 1.0 / (s * sigmaMaj / pixelScaleL * windowSize * s * + sigmaMin / pixelScaleL * windowSize); + for (auto& val : rmsOutput) val = std::sqrt(val * norm); +} + +void RMSImage::SlidingMinimum(Image& output, const Image& input, + size_t windowSize, size_t threadCount) { + const size_t width = input.Width(); + output = Image(width, input.Height()); + Image temp(output); + + aocommon::StaticFor<size_t> loop(threadCount); + + loop.Run(0, input.Height(), [&](size_t yStart, size_t yEnd) { + for (size_t y = yStart; y != yEnd; ++y) { + float* outRowptr = &temp[y * width]; + const float* inRowptr = &input[y * width]; + for (size_t x = 0; x != width; ++x) { + size_t left = std::max(x, windowSize / 2) - windowSize / 2; + size_t right = std::min(x, width - windowSize / 2) + windowSize / 2; + outRowptr[x] = *std::min_element(inRowptr + left, inRowptr + right); + } + } + }); + + loop.Run(0, width, [&](size_t xStart, size_t xEnd) { + aocommon::UVector<float> vals; + for (size_t x = xStart; x != xEnd; ++x) { + for (size_t y = 0; y != input.Height(); ++y) { + size_t top = std::max(y, windowSize / 2) - windowSize / 2; + size_t bottom = + std::min(y, input.Height() - windowSize / 2) + windowSize / 2; + vals.clear(); + for (size_t winY = top; winY != bottom; ++winY) + vals.push_back(temp[winY * width + x]); + output[y * width + x] = *std::min_element(vals.begin(), vals.end()); + } + } + }); +} + +void RMSImage::SlidingMaximum(Image& output, const Image& input, + size_t windowSize, size_t threadCount) { + Image flipped(input); + flipped.Negate(); + SlidingMinimum(output, flipped, windowSize, threadCount); + output.Negate(); +} + +void RMSImage::MakeWithNegativityLimit( + Image& rmsOutput, const Image& inputImage, double windowSize, + long double beamMaj, long double beamMin, long double beamPA, + long double pixelScaleL, long double pixelScaleM, size_t threadCount) { + Make(rmsOutput, inputImage, windowSize, beamMaj, beamMin, beamPA, pixelScaleL, + pixelScaleM, threadCount); + Image slidingMinimum(inputImage.Width(), inputImage.Height()); + double beamInPixels = std::max(beamMaj / pixelScaleL, 1.0L); + SlidingMinimum(slidingMinimum, inputImage, windowSize * beamInPixels, + threadCount); + for (size_t i = 0; i != rmsOutput.Size(); ++i) { + rmsOutput[i] = std::max<float>(rmsOutput[i], + std::abs(slidingMinimum[i]) * (1.5 / 5.0)); + } +} + +} // namespace radler::math diff --git a/cpp/math/rms_image.h b/cpp/math/rms_image.h new file mode 100644 index 00000000..cd5bee38 --- /dev/null +++ b/cpp/math/rms_image.h @@ -0,0 +1,35 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef RADLER_MATH_RMS_IMAGE_H_ +#define RADLER_MATH_RMS_IMAGE_H_ + +#include <aocommon/image.h> + +namespace radler::math { +class RMSImage { + public: + static void Make(aocommon::Image& rmsOutput, + const aocommon::Image& inputImage, double windowSize, + long double beamMaj, long double beamMin, long double beamPA, + long double pixelScaleL, long double pixelScaleM, + size_t threadCount); + + static void SlidingMinimum(aocommon::Image& output, + const aocommon::Image& input, size_t windowSize, + size_t threadCount); + + static void SlidingMaximum(aocommon::Image& output, + const aocommon::Image& input, size_t windowSize, + size_t threadCount); + + static void MakeWithNegativityLimit(aocommon::Image& rmsOutput, + const aocommon::Image& inputImage, + double windowSize, long double beamMaj, + long double beamMin, long double beamPA, + long double pixelScaleL, + long double pixelScaleM, + size_t threadCount); +}; +} // namespace radler::math +#endif // RADLER_MATH_RMS_IMAGE_H_ diff --git a/cpp/math/test/CMakeLists.txt b/cpp/math/test/CMakeLists.txt new file mode 100644 index 00000000..b3d1d7b2 --- /dev/null +++ b/cpp/math/test/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright (C) 2020 ASTRON (Netherlands Institute for Radio Astronomy) +# SPDX-License-Identifier: GPL-3.0-or-later + +include(${PROJECT_SOURCE_DIR}/cmake/unittest.cmake) + +add_definitions(-DBOOST_ERROR_CODE_HEADER_ONLY) + +add_unittest(radler_math runtests.cc test_dijkstra_splitter.cc + test_peak_finder.cc) diff --git a/cpp/math/test/runtests.cc b/cpp/math/test/runtests.cc new file mode 100644 index 00000000..fbab4717 --- /dev/null +++ b/cpp/math/test/runtests.cc @@ -0,0 +1,6 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#define BOOST_TEST_MODULE radler_math + +#include <boost/test/unit_test.hpp> \ No newline at end of file diff --git a/cpp/math/test/test_dijkstra_splitter.cc b/cpp/math/test/test_dijkstra_splitter.cc new file mode 100644 index 00000000..462bb584 --- /dev/null +++ b/cpp/math/test/test_dijkstra_splitter.cc @@ -0,0 +1,539 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "math/dijkstrasplitter.h" + +#include <random> + +#include <aocommon/image.h> + +#include <boost/test/unit_test.hpp> + +using aocommon::Image; + +namespace radler { + +using math::DijkstraSplitter; + +BOOST_AUTO_TEST_SUITE(dijkstra_splitter) + +Image MakeImage(size_t width, const std::string& str) { + const size_t height = str.size() / width; + BOOST_CHECK_EQUAL(width * height, str.size()); + Image image(width, height); + + for (size_t y = 0; y != height; ++y) { + for (size_t x = 0; x != width; ++x) { + if (str[x + y * width] == 'X') + image[x + y * width] = 0.1f; + else + image[x + y * width] = 10.0f; + } + } + return image; +} + +std::string PathStr(const Image& pathImage) { + std::ostringstream str; + for (size_t y = 0; y != pathImage.Height(); ++y) { + for (size_t x = 0; x != pathImage.Width(); ++x) { + if (pathImage[y * pathImage.Width() + x] == 0.0f) + str << ' '; + else + str << 'X'; + } + str << '\n'; + } + return str.str(); +} + +std::string PathStr(const aocommon::UVector<bool>& mask, size_t width) { + size_t height = mask.size() / width; + BOOST_CHECK_EQUAL(mask.size(), width * height); + std::ostringstream str; + for (size_t y = 0; y != height; ++y) { + for (size_t x = 0; x != width; ++x) { + if (mask[y * width + x]) + str << 'X'; + else + str << ' '; + } + str << '\n'; + } + return str.str(); +} + +std::string InputColumnStr(const Image& pathImage, size_t x) { + std::ostringstream str; + for (size_t y = 0; y != pathImage.Height(); ++y) { + if (pathImage[y * pathImage.Width() + x] == 10.0f) + str << ' '; + else + str << 'X'; + } + return str.str(); +} + +std::string InputRowStr(const Image& pathImage, size_t y) { + std::ostringstream str; + for (size_t x = 0; x != pathImage.Width(); ++x) { + if (pathImage[y * pathImage.Width() + x] == 10.0f) + str << ' '; + else + str << 'X'; + } + return str.str(); +} + +BOOST_AUTO_TEST_CASE(vertical) { + Image image = MakeImage(10, + "X " + " X " + " X " + " XXX " + " X " + " X" + " X " + " XXXX " + " X " + " XX "); + Image output(image.Width(), image.Height(), 0.0f); + const DijkstraSplitter splitter(image.Width(), image.Height()); + splitter.DivideVertically(image.Data(), output.Data(), 0, image.Width()); + + BOOST_CHECK_EQUAL(PathStr(output), + "X \n" + " X \n" + " X \n" + " XX \n" + " X \n" + " X \n" + " X \n" + " XXXX \n" + " X \n" + " X \n"); +} + +BOOST_AUTO_TEST_CASE(vertical_constrained) { + Image input = MakeImage(10, + " X X " + " X " + " X " + " XXX " + " X " + "XX X" + " XX " + " XXXX " + " X " + " XX "); + Image output(input); + const DijkstraSplitter splitter(input.Width(), input.Height()); + splitter.DivideVertically(input.Data(), output.Data(), 2, 8); + + BOOST_CHECK_EQUAL(PathStr(output), + "XX X XX\n" + "XX X XX\n" + "XXX XX\n" + "XX XX XX\n" + "XX X XX\n" + "XX X XX\n" + "XX X XX\n" + "XX X XX\n" + "XX X XX\n" + "XX X XX\n"); + // The input shouldn't have changed for the columns that were excluded: + BOOST_CHECK_EQUAL(InputColumnStr(output, 0), " X "); + BOOST_CHECK_EQUAL(InputColumnStr(output, 1), "XX X "); + BOOST_CHECK_EQUAL(InputColumnStr(output, 8), " X "); + BOOST_CHECK_EQUAL(InputColumnStr(output, 9), " X "); +} + +BOOST_AUTO_TEST_CASE(horizontal) { + Image input = MakeImage(10, + " X " + " " + " X " + " XXXXXX " + " X " + " X X X" + " X X " + " X X " + " X X " + "X XX X"); + Image output(input.Width(), input.Height(), 0.0f); + const DijkstraSplitter splitter(input.Width(), input.Height()); + splitter.DivideHorizontally(input.Data(), output.Data(), 0, input.Width()); + + BOOST_CHECK_EQUAL(PathStr(output), + " \n" + " \n" + " \n" + " XX \n" + " X X \n" + " X X \n" + " X X \n" + " X X \n" + " X X \n" + "X X\n"); +} + +BOOST_AUTO_TEST_CASE(horizontal_constrained) { + Image image = MakeImage(10, + " XXX " + " XXXXXX " + " X XXX" + "X XXX " + " XX " + "X X" + "XX " + " X X" + " XXXXXX " + " XXXX "); + Image output(image); + const DijkstraSplitter splitter(image.Width(), image.Height()); + splitter.DivideHorizontally(image.Data(), output.Data(), 2, 8); + + BOOST_CHECK_EQUAL(PathStr(output), + "XXXXXXXXXX\n" + "XXXXXXXXXX\n" + " X XXX\n" + "X X XXX \n" + " X \n" + " \n" + " \n" + " \n" + "XXXXXXXXXX\n" + "XXXXXXXXXX\n"); + // The input shouldn't have changed for the rows that were excluded: + BOOST_CHECK_EQUAL(InputRowStr(output, 0), " XXX "); + BOOST_CHECK_EQUAL(InputRowStr(output, 1), " XXXXXX "); + BOOST_CHECK_EQUAL(InputRowStr(output, 8), " XXXXXX "); + BOOST_CHECK_EQUAL(InputRowStr(output, 9), " XXXX "); +} + +BOOST_AUTO_TEST_CASE(flood_vertical_area) { + const size_t width = 9, height = 9; + Image image = MakeImage(width, + " X " + " X " + " X " + " X " + " X " + " XXX " + " X " + " X " + " X "); + DijkstraSplitter splitter(width, height); + Image scratch(image), dividingLines(width, height, 0.0f); + splitter.AddVerticalDivider(image.Data(), scratch.Data(), + dividingLines.Data(), 2, 7); + + BOOST_CHECK_EQUAL(PathStr(dividingLines), + " X \n" + " X \n" + " X \n" + " X \n" + " X \n" + " XXX \n" + " X \n" + " X \n" + " X \n"); + + aocommon::UVector<bool> mask(width * height); + size_t subX, subWidth; + splitter.FloodVerticalArea(dividingLines.Data(), 1, mask.data(), subX, + subWidth); + BOOST_CHECK_EQUAL(subX, 0u); + BOOST_CHECK_EQUAL(subWidth, 6u); + BOOST_CHECK_EQUAL(PathStr(mask, width), + "XXX \n" + "XXXX \n" + "XXXX \n" + "XXX \n" + "XX \n" + "XXX \n" + "XXXXXX \n" + "XXXXXX \n" + "XXXXXX \n"); + + splitter.FloodVerticalArea(dividingLines.Data(), 7, mask.data(), subX, + subWidth); + BOOST_CHECK_EQUAL(subX, 2u); + BOOST_CHECK_EQUAL(subWidth, 7u); + BOOST_CHECK_EQUAL(PathStr(mask, width), + " XXXXXX\n" + " XXXXX\n" + " XXXXX\n" + " XXXXXX\n" + " XXXXXXX\n" + " XXXXXX\n" + " XXX\n" + " XXX\n" + " XXX\n"); +} + +BOOST_AUTO_TEST_CASE(flood_horizontal_area) { + const size_t width = 9, height = 9; + Image image = MakeImage(width, + " " + " " + " XX X" + " X X X " + " X X X " + " X X X " + "X X " + " " + " "); + DijkstraSplitter splitter(width, height); + Image scratch(image), dividingLines(width, height, 0.0f); + splitter.AddHorizontalDivider(image.Data(), scratch.Data(), + dividingLines.Data(), 2, 7); + + BOOST_CHECK_EQUAL(PathStr(dividingLines), + " \n" + " \n" + " XX X\n" + " X X X \n" + " X X X \n" + " X X X \n" + "X X \n" + " \n" + " \n"); + + aocommon::UVector<bool> mask(width * height); + size_t subY, subHeight; + splitter.FloodHorizontalArea(dividingLines.Data(), 1, mask.data(), subY, + subHeight); + BOOST_CHECK_EQUAL(subY, 0u); + BOOST_CHECK_EQUAL(subHeight, 6u); + BOOST_CHECK_EQUAL(PathStr(mask, width), + "XXXXXXXXX\n" + "XXXXXXXXX\n" + "XX XXXX \n" + "X XX \n" + "X X \n" + "X X \n" + " \n" + " \n" + " \n"); + + splitter.FloodHorizontalArea(dividingLines.Data(), 7, mask.data(), subY, + subHeight); + BOOST_CHECK_EQUAL(subY, 2u); + BOOST_CHECK_EQUAL(subHeight, 7u); + BOOST_CHECK_EQUAL(PathStr(mask, width), + " \n" + " \n" + " XX X\n" + " XXXX XX\n" + " XXXXX XX\n" + " XXXXX XX\n" + "XXXXXXXXX\n" + "XXXXXXXXX\n" + "XXXXXXXXX\n"); +} + +BOOST_AUTO_TEST_CASE(get_bounding_mask) { + const size_t width = 9, height = 9; + Image image = MakeImage(width, + " X " + " X " + " X " + " X " + "XXXXXXXXX" + " X " + " X " + " X " + " X "); + DijkstraSplitter splitter(width, height); + Image dividingLines(width, height, 0.0f); + splitter.DivideVertically(image.Data(), dividingLines.Data(), 3, 6); + + aocommon::UVector<bool> mask(width * height); + size_t subXL, subXR, subY, subWidthL, subWidthR, subHeight; + + splitter.FloodVerticalArea(dividingLines.Data(), 1, mask.data(), subXL, + subWidthL); + BOOST_CHECK_EQUAL(subXL, 0u); + BOOST_CHECK_EQUAL(subWidthL, 4u); + aocommon::UVector<bool> maskL(subWidthL * height); + Image::TrimBox(maskL.data(), subXL, 0, subWidthL, height, mask.data(), width, + height); + BOOST_CHECK_EQUAL(PathStr(maskL, subWidthL), + "XXXX\n" + "XXXX\n" + "XXXX\n" + "XXXX\n" + "XXX \n" + "XXXX\n" + "XXXX\n" + "XXXX\n" + "XXXX\n"); + + splitter.FloodVerticalArea(dividingLines.Data(), 7, mask.data(), subXR, + subWidthR); + BOOST_CHECK_EQUAL(subXR, 3u); + BOOST_CHECK_EQUAL(subWidthR, 6u); + aocommon::UVector<bool> maskR(subWidthR * height); + Image::TrimBox(maskR.data(), subXR, 0, subWidthR, height, mask.data(), width, + height); + BOOST_CHECK_EQUAL(PathStr(maskR, subWidthR), + " XXXXX\n" + " XXXXX\n" + " XXXXX\n" + " XXXXX\n" + "XXXXXX\n" + " XXXXX\n" + " XXXXX\n" + " XXXXX\n" + " XXXXX\n"); + + dividingLines = 0.0f; + splitter.DivideHorizontally(image.Data(), dividingLines.Data(), 3, 6); + + splitter.FloodHorizontalArea(dividingLines.Data(), 1, mask.data(), subY, + subHeight); + BOOST_CHECK_EQUAL(subY, 0u); + BOOST_CHECK_EQUAL(subHeight, 4u); + BOOST_CHECK_EQUAL(PathStr(mask, width), + "XXXXXXXXX\n" + "XXXXXXXXX\n" + "XXXXXXXXX\n" + "XXXX XXXX\n" + " \n" + " \n" + " \n" + " \n" + " \n"); + + aocommon::UVector<bool> output(width * height, false); + size_t maskX, maskY, maskWidth, maskHeight; + + splitter.GetBoundingMask(maskL.data(), subXL, subWidthL, mask.data(), + output.data(), maskX, maskY, maskWidth, maskHeight); + BOOST_CHECK_EQUAL(maskX, 0u); + BOOST_CHECK_EQUAL(maskY, 0u); + BOOST_CHECK_EQUAL(maskWidth, 4u); + BOOST_CHECK_EQUAL(maskHeight, 4u); + BOOST_CHECK_EQUAL(PathStr(output, width), + "XXXX \n" + "XXXX \n" + "XXXX \n" + "XXXX \n" + " \n" + " \n" + " \n" + " \n" + " \n"); + + output.assign(width * height, false); + splitter.GetBoundingMask(maskR.data(), subXR, subWidthR, mask.data(), + output.data(), maskX, maskY, maskWidth, maskHeight); + BOOST_CHECK_EQUAL(maskX, 4u); + BOOST_CHECK_EQUAL(maskY, 0u); + BOOST_CHECK_EQUAL(maskWidth, 5u); + BOOST_CHECK_EQUAL(maskHeight, 4u); + BOOST_CHECK_EQUAL(PathStr(output, width), + " XXXXX\n" + " XXXXX\n" + " XXXXX\n" + " XXXX\n" + " \n" + " \n" + " \n" + " \n" + " \n"); + + splitter.FloodHorizontalArea(dividingLines.Data(), 7, mask.data(), subY, + subHeight); + BOOST_CHECK_EQUAL(subY, 3u); + BOOST_CHECK_EQUAL(subHeight, 6u); + + output.assign(width * height, false); + splitter.GetBoundingMask(maskL.data(), subXL, subWidthL, mask.data(), + output.data(), maskX, maskY, maskWidth, maskHeight); + BOOST_CHECK_EQUAL(maskX, 0u); + BOOST_CHECK_EQUAL(maskY, 4u); + BOOST_CHECK_EQUAL(maskWidth, 4u); + BOOST_CHECK_EQUAL(maskHeight, 5u); + BOOST_CHECK_EQUAL(PathStr(output, width), + " \n" + " \n" + " \n" + " \n" + "XXX \n" + "XXXX \n" + "XXXX \n" + "XXXX \n" + "XXXX \n"); + + output.assign(width * height, false); + splitter.GetBoundingMask(maskR.data(), subXR, subWidthR, mask.data(), + output.data(), maskX, maskY, maskWidth, maskHeight); + BOOST_CHECK_EQUAL(maskX, 3u); + BOOST_CHECK_EQUAL(maskY, 3u); + BOOST_CHECK_EQUAL(maskWidth, 6u); + BOOST_CHECK_EQUAL(maskHeight, 6u); + BOOST_CHECK_EQUAL(PathStr(output, width), + " \n" + " \n" + " \n" + " X \n" + " XXXXXX\n" + " XXXXX\n" + " XXXXX\n" + " XXXXX\n" + " XXXXX\n"); +} + +BOOST_AUTO_TEST_CASE(get_bounding_mask_on_noise) { + const size_t width = 80, height = 80; + Image image(width, height); + std::mt19937 rnd; + std::normal_distribution<float> gaus(0.0f, 1.0f); + + for (size_t repeat = 0; repeat != 1000; ++repeat) { + for (size_t i = 0; i != width * height; ++i) image[i] = gaus(rnd); + + DijkstraSplitter splitter(width, height); + Image dividingLinesV(width, height, 0.0f); + Image dividingLinesH(width, height, 0.0f); + splitter.DivideVertically(image.Data(), dividingLinesV.Data(), width / 4, + width * 3 / 4); + splitter.DivideHorizontally(image.Data(), dividingLinesH.Data(), height / 4, + height * 3 / 4); + + aocommon::UVector<bool> maskL(width * height), maskR(width * height), + maskT(width * height), maskB(width * height), + mask1(width * height, false), mask2(width * height, false), + mask3(width * height, false), mask4(width * height, false); + size_t subX, subY, subWidth, subHeight; + splitter.FloodVerticalArea(dividingLinesV.Data(), width / 8, maskL.data(), + subX, subWidth); + splitter.FloodVerticalArea(dividingLinesV.Data(), width * 7 / 8, + maskR.data(), subX, subWidth); + splitter.FloodHorizontalArea(dividingLinesH.Data(), width / 8, maskT.data(), + subY, subHeight); + splitter.FloodHorizontalArea(dividingLinesH.Data(), width * 7 / 8, + maskB.data(), subY, subHeight); + splitter.GetBoundingMask(maskL.data(), 0, width, maskT.data(), mask1.data(), + subX, subY, subWidth, subHeight); + splitter.GetBoundingMask(maskR.data(), 0, width, maskT.data(), mask2.data(), + subX, subY, subWidth, subHeight); + splitter.GetBoundingMask(maskL.data(), 0, width, maskB.data(), mask3.data(), + subX, subY, subWidth, subHeight); + splitter.GetBoundingMask(maskR.data(), 0, width, maskB.data(), mask4.data(), + subX, subY, subWidth, subHeight); + for (size_t i = 0; i != mask1.size(); ++i) { + size_t n = 0; + if (mask1[n]) ++n; + if (mask2[n]) ++n; + if (mask3[n]) ++n; + if (mask4[n]) ++n; + BOOST_CHECK_EQUAL(n, 1u); + } + } +} + +BOOST_AUTO_TEST_SUITE_END() +} // namespace radler \ No newline at end of file diff --git a/cpp/math/test/test_peak_finder.cc b/cpp/math/test/test_peak_finder.cc new file mode 100644 index 00000000..116ae1f3 --- /dev/null +++ b/cpp/math/test/test_peak_finder.cc @@ -0,0 +1,220 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#include <boost/test/unit_test.hpp> + +#include "math/peak_finder.h" + +#include <aocommon/uvector.h> + +#include <random> + +namespace radler { + +using math::PeakFinder; + +BOOST_AUTO_TEST_SUITE(peak_finder) + +const size_t nRepeats = + 3; /* This should be set to 100 to assert the performance */ + +#if defined __AVX__ && !defined FORCE_NON_AVX +template <typename NumT> +struct CleanTestFixture { + size_t x, y; + aocommon::UVector<NumT> img; + + CleanTestFixture(size_t n = 16) : x(size_t(-1)), y(size_t(-1)), img(n, 0) {} + void findPeak(size_t width = 4, size_t height = 2, size_t ystart = 0, + size_t yend = 2) { + PeakFinder::AVX(img.data(), width, height, x, y, true, ystart, yend, 0, 0); + } +}; +#endif + +struct NoiseFixture { + NoiseFixture() : n(2048), psf(n * n, 0.0), img(n * n), normal_dist(0.0, 1.0) { + mt.seed(42); + for (size_t i = 0; i != n * n; ++i) { + img[i] = normal_dist(mt); + } + } + + size_t n; + aocommon::UVector<double> psf, img; + std::mt19937 mt; + std::normal_distribution<double> normal_dist; +}; + +#if defined __AVX__ && !defined FORCE_NON_AVX +BOOST_AUTO_TEST_CASE(findPeakAVX1Double) { + CleanTestFixture<double> f; + f.img[0] = 1; + f.findPeak(); + BOOST_CHECK_EQUAL(f.x, 0u); + BOOST_CHECK_EQUAL(f.y, 0u); +} + +BOOST_AUTO_TEST_CASE(findPeakAVX2Double) { + CleanTestFixture<double> f; + f.img[0] = 1; + f.img[1] = 2; + f.findPeak(); + BOOST_CHECK_EQUAL(f.x, 1u); + BOOST_CHECK_EQUAL(f.y, 0u); +} + +BOOST_AUTO_TEST_CASE(findPeakAVX3Double) { + CleanTestFixture<double> f; + f.img[0] = 1; + f.img[1] = 2; + f.img[4] = 3; + f.findPeak(); + BOOST_CHECK_EQUAL(f.x, 0u); + BOOST_CHECK_EQUAL(f.y, 1u); +} + +BOOST_AUTO_TEST_CASE(findPeakAVX4Double) { + CleanTestFixture<double> f; + f.img[0] = 1; + f.img[1] = 2; + f.img[4] = 3; + f.img[7] = 4; + f.findPeak(); + BOOST_CHECK_EQUAL(f.x, 3u); + BOOST_CHECK_EQUAL(f.y, 1u); +} + +BOOST_AUTO_TEST_CASE(findPeakAVX5Double) { + CleanTestFixture<double> f; + f.img[0] = 1; + f.img[1] = 2; + f.img[4] = 3; + f.img[7] = 4; + f.img[15] = 6; + f.findPeak(4, 4, 0, 4); + BOOST_CHECK_EQUAL(f.x, 3u); + BOOST_CHECK_EQUAL(f.y, 3u); +} + +BOOST_AUTO_TEST_CASE(findPeakAVX6Double) { + CleanTestFixture<double> f; + f.img[0] = 1; + f.img[1] = 2; + f.img[4] = 3; + f.img[7] = 4; + f.img[15] = 6; + f.img[14] = 5; + f.findPeak(3, 5, 0, 5); + BOOST_CHECK_EQUAL(f.x, 2u); + BOOST_CHECK_EQUAL(f.y, 4u); +} + +BOOST_AUTO_TEST_CASE(findPeakAVX1Single) { + CleanTestFixture<float> f(36); + f.img[0] = 1; + f.findPeak(6, 6, 0, 6); + BOOST_CHECK_EQUAL(f.x, 0u); + BOOST_CHECK_EQUAL(f.y, 0u); +} + +BOOST_AUTO_TEST_CASE(findPeakAVX2Single) { + CleanTestFixture<float> f(36); + f.img[0] = 1; + f.img[1] = 2; + f.findPeak(6, 6, 0, 6); + BOOST_CHECK_EQUAL(f.x, 1u); + BOOST_CHECK_EQUAL(f.y, 0u); +} + +BOOST_AUTO_TEST_CASE(findPeakAVX3Single) { + CleanTestFixture<float> f(36); + f.img[0] = 1; + f.img[1] = 2; + f.img[6] = 3; + f.findPeak(6, 6, 0, 6); + BOOST_CHECK_EQUAL(f.x, 0u); + BOOST_CHECK_EQUAL(f.y, 1u); +} + +BOOST_AUTO_TEST_CASE(findPeakAVX4Single) { + CleanTestFixture<float> f(36); + f.img[0] = 1; + f.img[1] = 2; + f.img[6] = 3; + f.img[9] = 4; + f.findPeak(6, 6, 0, 6); + BOOST_CHECK_EQUAL(f.x, 3u); + BOOST_CHECK_EQUAL(f.y, 1u); +} + +BOOST_AUTO_TEST_CASE(findPeakAVX5Single) { + CleanTestFixture<float> f(36); + f.img[0] = 1; + f.img[1] = 2; + f.img[6] = 3; + f.img[9] = 4; + f.img[35] = 6; + f.findPeak(6, 6, 0, 6); + BOOST_CHECK_EQUAL(f.x, 5u); + BOOST_CHECK_EQUAL(f.y, 5u); +} + +BOOST_AUTO_TEST_CASE(findPeakAVX6Single) { + CleanTestFixture<float> f(36); + f.img[0] = 1; + f.img[1] = 2; + f.img[6] = 3; + f.img[9] = 4; + f.img[35] = 6; + f.findPeak(2, 18, 0, 18); + BOOST_CHECK_EQUAL(f.x, 1u); + BOOST_CHECK_EQUAL(f.y, 17u); +} + +BOOST_AUTO_TEST_CASE(findPeakAVX7Single) { + CleanTestFixture<float> f(38); + f.img[0] = 1; + f.img[1] = 2; + f.img[6] = 3; + f.img[9] = 4; + f.img[35] = 6; + f.img[37] = 7; + f.findPeak(6, 6, 0, 6); + BOOST_CHECK_EQUAL(f.x, 5u); + BOOST_CHECK_EQUAL(f.y, 5u); +} + +#endif + +BOOST_AUTO_TEST_CASE(findPeakPerformanceDouble) { + NoiseFixture f; + for (size_t repeat = 0; repeat != nRepeats; ++repeat) { + size_t x, y; + PeakFinder::Find(f.img.data(), f.n, f.n, x, y, true, 0, f.n / 2, 0.0); + } + BOOST_CHECK(true); +} + +BOOST_AUTO_TEST_CASE(findPeakSimplePerformanceDouble) { + NoiseFixture f; + for (size_t repeat = 0; repeat != nRepeats; ++repeat) { + size_t x, y; + PeakFinder::Simple(f.img.data(), f.n, f.n, x, y, true, 0, f.n / 2, 0, 0); + } + BOOST_CHECK(true); +} + +#if defined __AVX__ && !defined FORCE_NON_AVX +BOOST_AUTO_TEST_CASE(findPeakAVXPerformanceDouble) { + NoiseFixture f; + for (size_t repeat = 0; repeat != nRepeats; ++repeat) { + size_t x, y; + PeakFinder::AVX(f.img.data(), f.n, f.n, x, y, true, 0, f.n / 2, 0, 0); + } + BOOST_CHECK(true); +} +#endif + +BOOST_AUTO_TEST_SUITE_END() +} // namespace radler diff --git a/cpp/radler.cc b/cpp/radler.cc new file mode 100644 index 00000000..9a23ab04 --- /dev/null +++ b/cpp/radler.cc @@ -0,0 +1,391 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "radler.h" + +#include <aocommon/fits/fitsreader.h> +#include <aocommon/image.h> +#include <aocommon/imagecoordinates.h> +#include <aocommon/logger.h> +#include <aocommon/units/fluxdensity.h> + +#include <schaapcommon/fft/convolution.h> + +#include "algorithms/generic_clean.h" +#include "algorithms/iuwt_deconvolution.h" +#include "algorithms/more_sane.h" +#include "algorithms/multiscale_algorithm.h" +#include "algorithms/parallel_deconvolution.h" +#include "algorithms/python_deconvolution.h" +#include "algorithms/simple_clean.h" + +#include "image_set.h" +#include "math/rms_image.h" +#include "utils/casa_mask_reader.h" + +using aocommon::FitsReader; +using aocommon::FitsWriter; +using aocommon::Image; +using aocommon::ImageCoordinates; +using aocommon::Logger; +using aocommon::units::FluxDensity; + +namespace radler { + +Radler::Radler(const DeconvolutionSettings& deconvolutionSettings) + : _settings(deconvolutionSettings), + _table(), + _parallelDeconvolution( + std::make_unique<algorithms::ParallelDeconvolution>(_settings)), + _autoMaskIsFinished(false), + _imgWidth(_settings.trimmedImageWidth), + _imgHeight(_settings.trimmedImageHeight), + _pixelScaleX(_settings.pixelScaleX), + _pixelScaleY(_settings.pixelScaleY), + _autoMask(), + _beamSize(0.0) { + // Ensure that all FFTWF plan calls inside Radler are + // thread safe. + schaapcommon::fft::MakeFftwfPlannerThreadSafe(); +} + +Radler::~Radler() { FreeDeconvolutionAlgorithms(); } + +ComponentList Radler::GetComponentList() const { + return _parallelDeconvolution->GetComponentList(*_table); +} + +const algorithms::DeconvolutionAlgorithm& Radler::MaxScaleCountAlgorithm() + const { + return _parallelDeconvolution->MaxScaleCountAlgorithm(); +} + +void Radler::Perform(bool& reachedMajorThreshold, size_t majorIterationNr) { + assert(_table); + + Logger::Info.Flush(); + Logger::Info << " == Deconvolving (" << majorIterationNr << ") ==\n"; + + ImageSet residualSet(*_table, _settings.squaredJoins, + _settings.linkedPolarizations, _imgWidth, _imgHeight); + ImageSet modelSet(*_table, _settings.squaredJoins, + _settings.linkedPolarizations, _imgWidth, _imgHeight); + + Logger::Debug << "Loading residual images...\n"; + residualSet.LoadAndAverage(true); + Logger::Debug << "Loading model images...\n"; + modelSet.LoadAndAverage(false); + + Image integrated(_imgWidth, _imgHeight); + residualSet.GetLinearIntegrated(integrated); + Logger::Debug << "Calculating standard deviation...\n"; + double stddev = integrated.StdDevFromMAD(); + Logger::Info << "Estimated standard deviation of background noise: " + << FluxDensity::ToNiceString(stddev) << '\n'; + if (_settings.autoMask && _autoMaskIsFinished) { + // When we are in the second phase of automasking, don't use + // the RMS background anymore + _parallelDeconvolution->SetRMSFactorImage(Image()); + } else { + if (!_settings.localRMSImage.empty()) { + Image rmsImage(_imgWidth, _imgHeight); + FitsReader reader(_settings.localRMSImage); + reader.Read(rmsImage.Data()); + // Normalize the RMS image + stddev = rmsImage.Min(); + Logger::Info << "Lowest RMS in image: " + << FluxDensity::ToNiceString(stddev) << '\n'; + if (stddev <= 0.0) + throw std::runtime_error( + "RMS image can only contain values > 0, but contains values <= " + "0.0"); + for (float& value : rmsImage) { + if (value != 0.0) value = stddev / value; + } + _parallelDeconvolution->SetRMSFactorImage(std::move(rmsImage)); + } else if (_settings.localRMSMethod != LocalRmsMethod::kNone) { + Logger::Debug << "Constructing local RMS image...\n"; + Image rmsImage; + // TODO this should use full beam parameters + switch (_settings.localRMSMethod) { + case LocalRmsMethod::kNone: + assert(false); + break; + case LocalRmsMethod::kRmsWindow: + math::RMSImage::Make(rmsImage, integrated, _settings.localRMSWindow, + _beamSize, _beamSize, 0.0, _pixelScaleX, + _pixelScaleY, _settings.threadCount); + break; + case LocalRmsMethod::kRmsAndMinimumWindow: + math::RMSImage::MakeWithNegativityLimit( + rmsImage, integrated, _settings.localRMSWindow, _beamSize, + _beamSize, 0.0, _pixelScaleX, _pixelScaleY, + _settings.threadCount); + break; + } + // Normalize the RMS image relative to the threshold so that Jy remains + // Jy. + stddev = rmsImage.Min(); + Logger::Info << "Lowest RMS in image: " + << FluxDensity::ToNiceString(stddev) << '\n'; + for (float& value : rmsImage) { + if (value != 0.0) value = stddev / value; + } + _parallelDeconvolution->SetRMSFactorImage(std::move(rmsImage)); + } + } + if (_settings.autoMask && !_autoMaskIsFinished) + _parallelDeconvolution->SetThreshold(std::max( + stddev * _settings.autoMaskSigma, _settings.deconvolutionThreshold)); + else if (_settings.autoDeconvolutionThreshold) + _parallelDeconvolution->SetThreshold( + std::max(stddev * _settings.autoDeconvolutionThresholdSigma, + _settings.deconvolutionThreshold)); + integrated.Reset(); + + Logger::Debug << "Loading PSFs...\n"; + const std::vector<aocommon::Image> psfImages = + residualSet.LoadAndAveragePSFs(); + + if (_settings.useMultiscale) { + if (_settings.autoMask) { + if (_autoMaskIsFinished) + _parallelDeconvolution->SetAutoMaskMode(false, true); + else + _parallelDeconvolution->SetAutoMaskMode(true, false); + } + } else { + if (_settings.autoMask && _autoMaskIsFinished) { + if (_autoMask.empty()) { + _autoMask.resize(_imgWidth * _imgHeight); + for (size_t imgIndex = 0; imgIndex != modelSet.size(); ++imgIndex) { + const aocommon::Image& image = modelSet[imgIndex]; + for (size_t i = 0; i != _imgWidth * _imgHeight; ++i) { + _autoMask[i] = (image[i] == 0.0) ? false : true; + } + } + } + _parallelDeconvolution->SetCleanMask(_autoMask.data()); + } + } + + _parallelDeconvolution->ExecuteMajorIteration( + residualSet, modelSet, psfImages, reachedMajorThreshold); + + if (!reachedMajorThreshold && _settings.autoMask && !_autoMaskIsFinished) { + Logger::Info << "Auto-masking threshold reached; continuing next major " + "iteration with deeper threshold and mask.\n"; + _autoMaskIsFinished = true; + reachedMajorThreshold = true; + } + + if (_settings.majorIterationCount != 0 && + majorIterationNr >= _settings.majorIterationCount) { + reachedMajorThreshold = false; + Logger::Info << "Maximum number of major iterations was reached: not " + "continuing deconvolution.\n"; + } + + if (_settings.deconvolutionIterationCount != 0 && + _parallelDeconvolution->FirstAlgorithm().IterationNumber() >= + _settings.deconvolutionIterationCount) { + reachedMajorThreshold = false; + Logger::Info + << "Maximum number of minor deconvolution iterations was reached: not " + "continuing deconvolution.\n"; + } + + residualSet.AssignAndStoreResidual(); + modelSet.InterpolateAndStoreModel( + _parallelDeconvolution->FirstAlgorithm().Fitter(), _settings.threadCount); +} + +void Radler::InitializeDeconvolutionAlgorithm( + std::unique_ptr<DeconvolutionTable> table, double beamSize, + size_t threadCount) { + _beamSize = beamSize; + _autoMaskIsFinished = false; + _autoMask.clear(); + FreeDeconvolutionAlgorithms(); + _table = std::move(table); + if (_table->OriginalGroups().empty()) + throw std::runtime_error("Nothing to clean"); + + if (!std::isfinite(_beamSize)) { + Logger::Warn << "No proper beam size available in deconvolution!\n"; + _beamSize = 0.0; + } + + std::unique_ptr<class algorithms::DeconvolutionAlgorithm> algorithm; + + if (!_settings.pythonDeconvolutionFilename.empty()) { + algorithm.reset(new algorithms::PythonDeconvolution( + _settings.pythonDeconvolutionFilename)); + } else if (_settings.useMoreSaneDeconvolution) { + algorithm.reset(new algorithms::MoreSane( + _settings.moreSaneLocation, _settings.moreSaneArgs, + _settings.moreSaneSigmaLevels, _settings.prefixName)); + } else if (_settings.useIUWTDeconvolution) { + algorithms::IUWTDeconvolution* method = new algorithms::IUWTDeconvolution; + method->SetUseSNRTest(_settings.iuwtSNRTest); + algorithm.reset(method); + } else if (_settings.useMultiscale) { + algorithms::MultiScaleAlgorithm* msAlgorithm = + new algorithms::MultiScaleAlgorithm(beamSize, _pixelScaleX, + _pixelScaleY); + msAlgorithm->SetManualScaleList(_settings.multiscaleScaleList); + msAlgorithm->SetMultiscaleScaleBias( + _settings.multiscaleDeconvolutionScaleBias); + msAlgorithm->SetMaxScales(_settings.multiscaleMaxScales); + msAlgorithm->SetMultiscaleGain(_settings.multiscaleGain); + msAlgorithm->SetShape(_settings.multiscaleShapeFunction); + msAlgorithm->SetTrackComponents(_settings.saveSourceList); + msAlgorithm->SetConvolutionPadding(_settings.multiscaleConvolutionPadding); + msAlgorithm->SetUseFastSubMinorLoop(_settings.multiscaleFastSubMinorLoop); + algorithm.reset(msAlgorithm); + } else { + algorithm.reset( + new algorithms::GenericClean(_settings.useSubMinorOptimization)); + } + + algorithm->SetMaxNIter(_settings.deconvolutionIterationCount); + algorithm->SetThreshold(_settings.deconvolutionThreshold); + algorithm->SetGain(_settings.deconvolutionGain); + algorithm->SetMGain(_settings.deconvolutionMGain); + algorithm->SetCleanBorderRatio(_settings.deconvolutionBorderRatio); + algorithm->SetAllowNegativeComponents(_settings.allowNegativeComponents); + algorithm->SetStopOnNegativeComponents(_settings.stopOnNegativeComponents); + algorithm->SetThreadCount(threadCount); + algorithm->SetSpectralFittingMode(_settings.spectralFittingMode, + _settings.spectralFittingTerms); + + ImageSet::CalculateDeconvolutionFrequencies(*_table, _channelFrequencies, + _channelWeights); + algorithm->InitializeFrequencies(_channelFrequencies, _channelWeights); + _parallelDeconvolution->SetAlgorithm(std::move(algorithm)); + + if (!_settings.forcedSpectrumFilename.empty()) { + Logger::Debug << "Reading " << _settings.forcedSpectrumFilename << ".\n"; + FitsReader reader(_settings.forcedSpectrumFilename); + if (reader.ImageWidth() != _imgWidth || reader.ImageHeight() != _imgHeight) + throw std::runtime_error( + "The image width of the forced spectrum fits file does not match the " + "imaging size"); + std::vector<Image> terms(1); + terms[0] = Image(_imgWidth, _imgHeight); + reader.Read(terms[0].Data()); + _parallelDeconvolution->SetSpectrallyForcedImages(std::move(terms)); + } + + readMask(*_table); +} + +void Radler::FreeDeconvolutionAlgorithms() { + _parallelDeconvolution->FreeDeconvolutionAlgorithms(); + _table.reset(); +} + +bool Radler::IsInitialized() const { + return _parallelDeconvolution->IsInitialized(); +} + +size_t Radler::IterationNumber() const { + return _parallelDeconvolution->FirstAlgorithm().IterationNumber(); +} + +void Radler::RemoveNaNsInPSF(float* psf, size_t width, size_t height) { + float* endPtr = psf + width * height; + while (psf != endPtr) { + if (!std::isfinite(*psf)) *psf = 0.0; + ++psf; + } +} + +void Radler::readMask(const DeconvolutionTable& groupTable) { + bool hasMask = false; + if (!_settings.fitsDeconvolutionMask.empty()) { + FitsReader maskReader(_settings.fitsDeconvolutionMask, true, true); + if (maskReader.ImageWidth() != _imgWidth || + maskReader.ImageHeight() != _imgHeight) + throw std::runtime_error( + "Specified Fits file mask did not have same dimensions as output " + "image!"); + aocommon::UVector<float> maskData(_imgWidth * _imgHeight); + if (maskReader.NFrequencies() == 1) { + Logger::Debug << "Reading mask '" << _settings.fitsDeconvolutionMask + << "'...\n"; + maskReader.Read(maskData.data()); + } else if (maskReader.NFrequencies() == _settings.channelsOut) { + Logger::Debug << "Reading mask '" << _settings.fitsDeconvolutionMask + << "' (" << (groupTable.Front().original_channel_index + 1) + << ")...\n"; + maskReader.ReadIndex(maskData.data(), + groupTable.Front().original_channel_index); + } else { + std::stringstream msg; + msg << "The number of frequencies in the specified fits mask (" + << maskReader.NFrequencies() + << ") does not match the number of requested output channels (" + << _settings.channelsOut << ")"; + throw std::runtime_error(msg.str()); + } + _cleanMask.assign(_imgWidth * _imgHeight, false); + for (size_t i = 0; i != _imgWidth * _imgHeight; ++i) + _cleanMask[i] = (maskData[i] != 0.0); + + hasMask = true; + } else if (!_settings.casaDeconvolutionMask.empty()) { + if (_cleanMask.empty()) { + Logger::Info << "Reading CASA mask '" << _settings.casaDeconvolutionMask + << "'...\n"; + _cleanMask.assign(_imgWidth * _imgHeight, false); + utils::CasaMaskReader maskReader(_settings.casaDeconvolutionMask); + if (maskReader.Width() != _imgWidth || maskReader.Height() != _imgHeight) + throw std::runtime_error( + "Specified CASA mask did not have same dimensions as output " + "image!"); + maskReader.Read(_cleanMask.data()); + } + + hasMask = true; + } + + if (_settings.horizonMask) { + if (!hasMask) { + _cleanMask.assign(_imgWidth * _imgHeight, true); + hasMask = true; + } + + double fovSq = M_PI_2 - _settings.horizonMaskDistance; + if (fovSq < 0.0) fovSq = 0.0; + if (fovSq <= M_PI_2) + fovSq = std::sin(fovSq); + else // a negative horizon distance was given + fovSq = 1.0 - _settings.horizonMaskDistance; + fovSq = fovSq * fovSq; + bool* ptr = _cleanMask.data(); + + for (size_t y = 0; y != _imgHeight; ++y) { + for (size_t x = 0; x != _imgWidth; ++x) { + double l, m; + ImageCoordinates::XYToLM(x, y, _pixelScaleX, _pixelScaleY, _imgWidth, + _imgHeight, l, m); + if (l * l + m * m >= fovSq) *ptr = false; + ++ptr; + } + } + + Logger::Info << "Saving horizon mask...\n"; + Image image(_imgWidth, _imgHeight); + for (size_t i = 0; i != _imgWidth * _imgHeight; ++i) + image[i] = _cleanMask[i] ? 1.0 : 0.0; + + FitsWriter writer; + writer.SetImageDimensions(_imgWidth, _imgHeight, _settings.pixelScaleX, + _settings.pixelScaleY); + writer.Write(_settings.prefixName + "-horizon-mask.fits", image.Data()); + } + + if (hasMask) _parallelDeconvolution->SetCleanMask(_cleanMask.data()); +} +} // namespace radler \ No newline at end of file diff --git a/cpp/radler.h b/cpp/radler.h new file mode 100644 index 00000000..a1fde1ea --- /dev/null +++ b/cpp/radler.h @@ -0,0 +1,80 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef RADLER_RADLER_H_ +#define RADLER_RADLER_H_ + +#include <cstring> + +#include <aocommon/polarization.h> +#include <aocommon/uvector.h> + +#include "component_list.h" +#include "deconvolution_settings.h" + +namespace radler { + +class DeconvolutionTable; +struct DeconvolutionTableEntry; +namespace algorithms { +class ParallelDeconvolution; +} + +/** + * @brief Main interfacing class of the Radio Astronomical Deconvolution + * Library. + * + */ +class Radler { + public: + explicit Radler(const DeconvolutionSettings& deconvolutionSettings); + ~Radler(); + + ComponentList GetComponentList() const; + + /** + * @brief Exposes a const reference to either the first algorithm, or - in + * case of a multiscale clean - the algorithm with the maximum number of scale + * counts. + */ + const algorithms::DeconvolutionAlgorithm& MaxScaleCountAlgorithm() const; + + void Perform(bool& reachedMajorThreshold, size_t majorIterationNr); + + void InitializeDeconvolutionAlgorithm( + std::unique_ptr<DeconvolutionTable> table, double beamSize, + size_t threadCount); + + void FreeDeconvolutionAlgorithms(); + + bool IsInitialized() const; + // { return _parallelDeconvolution.IsInitialized(); } + + /// Return IterationNumber of the underlying \c DeconvolutionAlgorithm + size_t IterationNumber() const; + + static void RemoveNaNsInPSF(float* psf, size_t width, size_t height); + + private: + void readMask(const DeconvolutionTable& groupTable); + + const DeconvolutionSettings _settings; + + std::unique_ptr<DeconvolutionTable> _table; + + std::unique_ptr<algorithms::ParallelDeconvolution> _parallelDeconvolution; + + aocommon::UVector<bool> _cleanMask; + + bool _autoMaskIsFinished; + aocommon::UVector<double> _channelFrequencies; + aocommon::UVector<float> _channelWeights; + size_t _imgWidth; + size_t _imgHeight; + double _pixelScaleX; + double _pixelScaleY; + aocommon::UVector<bool> _autoMask; + double _beamSize; +}; +} // namespace radler +#endif diff --git a/cpp/test/CMakeLists.txt b/cpp/test/CMakeLists.txt new file mode 100644 index 00000000..fcf54609 --- /dev/null +++ b/cpp/test/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright (C) 2020 ASTRON (Netherlands Institute for Radio Astronomy) +# SPDX-License-Identifier: GPL-3.0-or-later + +include(${PROJECT_SOURCE_DIR}/cmake/unittest.cmake) + +add_definitions(-DBOOST_ERROR_CODE_HEADER_ONLY) + +add_unittest(radler_main runtests.cc test_component_list.cc + test_deconvolution_table.cc test_image_set.cc) diff --git a/cpp/test/runtests.cc b/cpp/test/runtests.cc new file mode 100644 index 00000000..17278478 --- /dev/null +++ b/cpp/test/runtests.cc @@ -0,0 +1,6 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#define BOOST_TEST_MODULE radler_main + +#include <boost/test/unit_test.hpp> \ No newline at end of file diff --git a/cpp/test/smartptr.h b/cpp/test/smartptr.h new file mode 100644 index 00000000..3a5fde54 --- /dev/null +++ b/cpp/test/smartptr.h @@ -0,0 +1,54 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef RADLER_TEST_UNIQUE_PTR_H_ +#define RADLER_TEST_UNIQUE_PTR_H_ + +#include <boost/test/unit_test.hpp> + +namespace radler::test { + +/** + * Helper class for using unique pointers in tests. + * + * Use case: When using unique pointers, tests often also store raw pointers + * for accessing objects after std::moving their unique pointer. + * + * This class supports this common pattern: Instead of storing both a unique + * pointer and a raw pointer, tests can use a single test::UniquePtr variable. + * + * The perfect forwarding constructor also simplifies initialization: + * "std::unique_ptr<X> x(new X(...));" becomes: "test::UniquePtr<X> x(...);" + * + * @tparam T Object type for the unique pointer. + */ +template <class T> +class UniquePtr { + public: + /** + * Constructor. It uses perfect forwarding for calling T's constructor. + */ + template <typename... Args> + UniquePtr(Args&&... args) + : _unique(new T(std::forward<Args>(args)...)), _raw(_unique.get()) {} + + /** + * Extract the unique_ptr from the helper class. + * The test code may only call this function once. + */ + std::unique_ptr<T> take() { + BOOST_TEST_REQUIRE(static_cast<bool>(_unique)); + return std::move(_unique); + } + + T& operator*() { return *_raw; } + T* operator->() { return _raw; } + T* get() { return _raw; } + + private: + std::unique_ptr<T> _unique; + T* const _raw; +}; + +} // namespace radler::test +#endif // RADLER_TEST_UNIQUE_PTR_H_ \ No newline at end of file diff --git a/cpp/test/test_component_list.cc b/cpp/test/test_component_list.cc new file mode 100644 index 00000000..e6affbbd --- /dev/null +++ b/cpp/test/test_component_list.cc @@ -0,0 +1,110 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#include <boost/test/unit_test.hpp> + +#include "component_list.h" + +namespace radler { + +BOOST_AUTO_TEST_SUITE(component_list) + +struct ComponentListFixture { + ComponentListFixture() : list(512, 512, 4, 3) { + aocommon::UVector<float> values = {1.0, 2.0, 3.0}; + list.Add(256, 256, 1, values.data()); + values = {5.0, 6.0, 7.0}; + list.Add(256, 256, 1, values.data()); + values = {8.0, 9.0, 10.0}; + list.Add(511, 511, 0, values.data()); + values = {11.0, 12.0, 13.0}; + list.Add(13, 42, 3, values.data()); + + list.MergeDuplicates(); + } + + ComponentList list; + aocommon::UVector<float> values; +}; + +BOOST_FIXTURE_TEST_CASE(adding_values, ComponentListFixture) { + BOOST_CHECK_EQUAL(list.ComponentCount(0), 1u); + BOOST_CHECK_EQUAL(list.ComponentCount(1), 1u); + BOOST_CHECK_EQUAL(list.ComponentCount(2), 0u); + BOOST_CHECK_EQUAL(list.ComponentCount(3), 1u); + + size_t x; + size_t y; + aocommon::UVector<float> values(3); + + list.GetComponent(0, 0, x, y, values.data()); + BOOST_CHECK_EQUAL(x, 511u); + BOOST_CHECK_EQUAL(y, 511u); + BOOST_CHECK_CLOSE_FRACTION(values[0], 8.0, 1e-5); + BOOST_CHECK_CLOSE_FRACTION(values[1], 9.0, 1e-5); + BOOST_CHECK_CLOSE_FRACTION(values[2], 10.0, 1e-5); + + list.GetComponent(1, 0, x, y, values.data()); + BOOST_CHECK_EQUAL(x, 256u); + BOOST_CHECK_EQUAL(y, 256u); + BOOST_CHECK_CLOSE_FRACTION(values[0], 6.0, 1e-5); + BOOST_CHECK_CLOSE_FRACTION(values[1], 8.0, 1e-5); + BOOST_CHECK_CLOSE_FRACTION(values[2], 10.0, 1e-5); + + list.GetComponent(3, 0, x, y, values.data()); + BOOST_CHECK_EQUAL(x, 13u); + BOOST_CHECK_EQUAL(y, 42u); + BOOST_CHECK_CLOSE_FRACTION(values[0], 11.0, 1e-5); + BOOST_CHECK_CLOSE_FRACTION(values[1], 12.0, 1e-5); + BOOST_CHECK_CLOSE_FRACTION(values[2], 13.0, 1e-5); +} + +BOOST_FIXTURE_TEST_CASE(get_position, ComponentListFixture) { + BOOST_CHECK_EQUAL(list.GetPositions(0).size(), 1u); + BOOST_CHECK_EQUAL(list.GetPositions(1).size(), 1u); + BOOST_CHECK_EQUAL(list.GetPositions(2).size(), 0u); + BOOST_CHECK_EQUAL(list.GetPositions(3).size(), 1u); + + BOOST_CHECK_EQUAL(list.GetPositions(0)[0].x, 511u); + BOOST_CHECK_EQUAL(list.GetPositions(0)[0].y, 511u); + + BOOST_CHECK_EQUAL(list.GetPositions(1)[0].x, 256u); + BOOST_CHECK_EQUAL(list.GetPositions(1)[0].y, 256u); + + BOOST_CHECK_EQUAL(list.GetPositions(3)[0].x, 13u); + BOOST_CHECK_EQUAL(list.GetPositions(3)[0].y, 42u); +} + +BOOST_FIXTURE_TEST_CASE(multiply_scale_component, ComponentListFixture) { + for (size_t i = 0; i != list.NScales(); ++i) { + // Second scale is empty (captured in DEBUG mode via an assert) + if (i != 2u) { + for (size_t j = 0; j != list.NFrequencies(); ++j) { + list.MultiplyScaleComponent(i, 0, j, static_cast<double>(j + 1u)); + } + } + } + + size_t x; + size_t y; + aocommon::UVector<float> values(3); + + list.GetComponent(0, 0, x, y, values.data()); + BOOST_CHECK_CLOSE_FRACTION(values[0], 8.0, 1e-5); + BOOST_CHECK_CLOSE_FRACTION(values[1], 18.0, 1e-5); + BOOST_CHECK_CLOSE_FRACTION(values[2], 30.0, 1e-5); + + list.GetComponent(1, 0, x, y, values.data()); + BOOST_CHECK_CLOSE_FRACTION(values[0], 6.0, 1e-5); + BOOST_CHECK_CLOSE_FRACTION(values[1], 16.0, 1e-5); + BOOST_CHECK_CLOSE_FRACTION(values[2], 30.0, 1e-5); + + list.GetComponent(3, 0, x, y, values.data()); + BOOST_CHECK_CLOSE_FRACTION(values[0], 11.0, 1e-5); + BOOST_CHECK_CLOSE_FRACTION(values[1], 24.0, 1e-5); + BOOST_CHECK_CLOSE_FRACTION(values[2], 39.0, 1e-5); +} + +BOOST_AUTO_TEST_SUITE_END() + +} // namespace radler diff --git a/cpp/test/test_deconvolution_table.cc b/cpp/test/test_deconvolution_table.cc new file mode 100644 index 00000000..b3ba8551 --- /dev/null +++ b/cpp/test/test_deconvolution_table.cc @@ -0,0 +1,96 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "deconvolution_table.h" + +#include <array> + +#include <boost/test/unit_test.hpp> + +#include "test/smartptr.h" + +namespace radler { + +BOOST_AUTO_TEST_SUITE(deconvolutiontable) + +BOOST_AUTO_TEST_CASE(constructor) { + const size_t kTableSize = 42; + + DeconvolutionTable table(kTableSize, kTableSize); + + BOOST_TEST(table.OriginalGroups().size() == kTableSize); + for (const DeconvolutionTable::Group& group : table.OriginalGroups()) { + BOOST_TEST(group.empty()); + } + + BOOST_TEST_REQUIRE(table.DeconvolutionGroups().size() == kTableSize); + for (int index = 0; index < int(kTableSize); ++index) { + BOOST_TEST(table.DeconvolutionGroups()[index] == + std::vector<int>(1, index)); + } + + BOOST_TEST((table.begin() == table.end())); + BOOST_TEST(table.Size() == 0); +} + +BOOST_AUTO_TEST_CASE(single_deconvolution_group) { + DeconvolutionTable table(7, 1); + const std::vector<std::vector<int>> kExpectedGroups{{0, 1, 2, 3, 4, 5, 6}}; + BOOST_TEST_REQUIRE(table.DeconvolutionGroups() == kExpectedGroups); +} + +BOOST_AUTO_TEST_CASE(multiple_deconvolution_groups) { + DeconvolutionTable table(7, 3); + const std::vector<std::vector<int>> kExpectedGroups{ + {0, 1, 2}, {3, 4}, {5, 6}}; + BOOST_TEST_REQUIRE(table.DeconvolutionGroups() == kExpectedGroups); +} + +BOOST_AUTO_TEST_CASE(too_many_deconvolution_groups) { + DeconvolutionTable table(7, 42); + const std::vector<std::vector<int>> kExpectedGroups{{0}, {1}, {2}, {3}, + {4}, {5}, {6}}; + BOOST_TEST_REQUIRE(table.DeconvolutionGroups() == kExpectedGroups); +} + +BOOST_AUTO_TEST_CASE(add_entries) { + DeconvolutionTable table(3, 1); + + std::array<test::UniquePtr<DeconvolutionTableEntry>, 3> entries; + entries[0]->original_channel_index = 1; + entries[1]->original_channel_index = 0; + entries[2]->original_channel_index = 1; + + for (test::UniquePtr<DeconvolutionTableEntry>& entry : entries) { + table.AddEntry(entry.take()); + // table.AddEntry(std::move(entry)); + } + + // Check if the OriginalGroups have the correct size and correct entries. + const std::vector<DeconvolutionTable::Group>& original_groups = + table.OriginalGroups(); + BOOST_TEST_REQUIRE(original_groups.size() == 3); + + BOOST_TEST_REQUIRE(original_groups[0].size() == 1); + BOOST_TEST_REQUIRE(original_groups[1].size() == 2); + BOOST_TEST(original_groups[2].empty()); + + BOOST_TEST(original_groups[0][0] == entries[1].get()); + BOOST_TEST(original_groups[1][0] == entries[0].get()); + BOOST_TEST(original_groups[1][1] == entries[2].get()); + + // Check if a range based loop, which uses begin() and end(), yields the + // entries. + size_t index = 0; + for (const DeconvolutionTableEntry& entry : table) { + BOOST_TEST(&entry == entries[index].get()); + ++index; + } + + // Finally, check Front() and Size(). + BOOST_TEST(&table.Front() == entries.front().get()); + BOOST_TEST(table.Size() == entries.size()); +} + +BOOST_AUTO_TEST_SUITE_END() +} // namespace radler \ No newline at end of file diff --git a/cpp/test/test_image_set.cc b/cpp/test/test_image_set.cc new file mode 100644 index 00000000..74c67999 --- /dev/null +++ b/cpp/test/test_image_set.cc @@ -0,0 +1,495 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "image_set.h" + +#include <memory> + +#include <boost/test/unit_test.hpp> + +#include <aocommon/image.h> +#include <aocommon/polarization.h> + +#include <schaapcommon/fitters/spectralfitter.h> + +using aocommon::Image; +using aocommon::PolarizationEnum; + +namespace radler { + +namespace { + +/** + * @brief Dummy image accessor that doesn't allow doing anything. + */ +class DummyImageAccessor final : public aocommon::ImageAccessor { + public: + DummyImageAccessor() {} + ~DummyImageAccessor() override {} + + void Load(Image&) const override { + throw std::logic_error("Unexpected DummyImageAccessor::Load() call"); + } + + void Store(const Image&) override { + throw std::logic_error("Unexpected DummyImageAccessor::Store() call"); + } +}; + +/** + * @brief Mimimal image accessor that only admits loading an image. Required + * to test @c LoadAndAverage functionality. + */ +class LoadOnlyImageAccessor final : public aocommon::ImageAccessor { + public: + LoadOnlyImageAccessor(const aocommon::Image& image) : _image(image) {} + ~LoadOnlyImageAccessor() override {} + + void Load(Image& image) const override { image = _image; } + + void Store(const Image&) override { + throw std::logic_error("Unexpected MimimalImageAccessor::Store() call"); + } + + private: + const aocommon::Image _image; +}; + +} // namespace + +struct ImageSetFixtureBase { + ImageSetFixtureBase() {} + + void initTable(size_t n_original_channels, size_t n_deconvolution_channels) { + table = std::make_unique<DeconvolutionTable>(n_original_channels, + n_deconvolution_channels); + } + + void addToImageSet(size_t outChannel, PolarizationEnum pol, + size_t frequencyMHz, double imageWeight = 1.0) { + auto e = std::make_unique<DeconvolutionTableEntry>(); + e->original_channel_index = outChannel; + e->polarization = pol; + e->band_start_frequency = frequencyMHz; + e->band_end_frequency = frequencyMHz; + e->image_weight = imageWeight; + e->psf_accessor = std::make_unique<DummyImageAccessor>(); + e->model_accessor = std::make_unique<LoadOnlyImageAccessor>(modelImage); + e->residual_accessor = std::make_unique<DummyImageAccessor>(); + table->AddEntry(std::move(e)); + } + + void checkLinearValue(size_t index, float value, const ImageSet& dset) { + Image dest(2, 2, 1.0); + dset.GetLinearIntegrated(dest); + BOOST_CHECK_CLOSE_FRACTION(dest[index], value, 1e-6); + } + + void checkSquaredValue(size_t index, float value, const ImageSet& dset) { + Image dest(2, 2, 1.0), scratch(2, 2); + dset.GetSquareIntegrated(dest, scratch); + BOOST_CHECK_CLOSE_FRACTION(dest[index], value, 1e-6); + } + + std::unique_ptr<DeconvolutionTable> table; + aocommon::Image modelImage; +}; + +template <size_t NDeconvolutionChannels> +struct ImageSetFixture : public ImageSetFixtureBase { + ImageSetFixture() { + initTable(2, NDeconvolutionChannels); + addToImageSet(0, aocommon::Polarization::XX, 100); + addToImageSet(0, aocommon::Polarization::YY, 100); + addToImageSet(1, aocommon::Polarization::XX, 200); + addToImageSet(1, aocommon::Polarization::YY, 200); + } +}; + +BOOST_AUTO_TEST_SUITE(imageset) + +BOOST_FIXTURE_TEST_CASE(constructor_1, ImageSetFixture<1>) { + const bool kSquareJoinedChannels = false; + const std::set<aocommon::PolarizationEnum> kLinkedPolarizations; + ImageSet dset(*table, kSquareJoinedChannels, kLinkedPolarizations, 2, 2); + BOOST_CHECK_EQUAL(&dset.Table(), table.get()); + BOOST_CHECK_EQUAL(dset.NOriginalChannels(), 2u); + BOOST_CHECK_EQUAL(dset.PSFCount(), 1u); + BOOST_CHECK_EQUAL(dset.NDeconvolutionChannels(), 1u); + BOOST_CHECK_EQUAL(dset.SquareJoinedChannels(), kSquareJoinedChannels); + BOOST_CHECK(dset.LinkedPolarizations() == kLinkedPolarizations); +} + +BOOST_FIXTURE_TEST_CASE(constructor_2, ImageSetFixture<2>) { + const bool kSquareJoinedChannels = true; + const std::set<aocommon::PolarizationEnum> kLinkedPolarizations{ + aocommon::PolarizationEnum::StokesI, aocommon::PolarizationEnum::StokesQ, + aocommon::PolarizationEnum::StokesU, aocommon::PolarizationEnum::StokesV}; + ImageSet dset(*table, kSquareJoinedChannels, kLinkedPolarizations, 2, 2); + BOOST_CHECK_EQUAL(dset.NOriginalChannels(), 2u); + BOOST_CHECK_EQUAL(dset.PSFCount(), 2u); + BOOST_CHECK_EQUAL(dset.NDeconvolutionChannels(), 2u); + BOOST_CHECK_EQUAL(dset.SquareJoinedChannels(), kSquareJoinedChannels); + BOOST_CHECK(dset.LinkedPolarizations() == kLinkedPolarizations); +} + +BOOST_FIXTURE_TEST_CASE(xxNormalization, ImageSetFixtureBase) { + initTable(1, 1); + addToImageSet(0, aocommon::Polarization::XX, 100); + ImageSet dset(*table, false, {aocommon::Polarization::XX}, 2, 2); + dset = 0.0; + dset.Data(0)[1] = 5.0; + checkLinearValue(1, 5.0, dset); + checkSquaredValue(1, 5.0, dset); +} + +BOOST_FIXTURE_TEST_CASE(iNormalization, ImageSetFixtureBase) { + initTable(1, 1); + addToImageSet(0, aocommon::Polarization::StokesI, 100); + ImageSet dset(*table, false, {aocommon::Polarization::StokesI}, 2, 2); + dset = 0.0; + dset.Data(0)[2] = 6.0; + checkLinearValue(2, 6.0, dset); + checkSquaredValue(2, 6.0, dset); +} + +BOOST_FIXTURE_TEST_CASE(i_2channel_Normalization, ImageSetFixtureBase) { + initTable(2, 2); + addToImageSet(0, aocommon::Polarization::StokesI, 100); + addToImageSet(1, aocommon::Polarization::StokesI, 200); + ImageSet dset(*table, false, {aocommon::Polarization::StokesI}, 2, 2); + dset = 0.0; + dset.Data(0)[0] = 12.0; + dset.Data(1)[0] = 13.0; + checkLinearValue(0, 12.5, dset); + checkSquaredValue(0, 12.5, dset); +} + +BOOST_FIXTURE_TEST_CASE(i_2channel_NaNs, ImageSetFixtureBase) { + initTable(2, 2); + addToImageSet(0, aocommon::Polarization::StokesI, 100, 0.0); + addToImageSet(1, aocommon::Polarization::StokesI, 200, 1.0); + ImageSet dset(*table, false, {aocommon::Polarization::StokesI}, 2, 2); + dset = 0.0; + dset.Data(0)[0] = std::numeric_limits<float>::quiet_NaN(); + dset.Data(1)[0] = 42.0; + checkLinearValue(0, 42.0f, dset); + checkSquaredValue(0, 42.0f, dset); +} + +BOOST_FIXTURE_TEST_CASE(xxyyNormalization, ImageSetFixtureBase) { + initTable(1, 1); + addToImageSet(0, aocommon::Polarization::XX, 100); + addToImageSet(0, aocommon::Polarization::YY, 100); + const std::set<PolarizationEnum> kLinkedPolarizations{ + aocommon::Polarization::XX, aocommon::Polarization::YY}; + ImageSet dset(*table, false, kLinkedPolarizations, 2, 2); + dset = 0.0; + dset.Data(0)[3] = 7.0; + dset.Data(1)[3] = 8.0; + checkLinearValue(3, 7.5, dset); + dset.Data(0)[3] = -7.0; + checkSquaredValue(3, std::sqrt((7.0 * 7.0 + 8.0 * 8.0) * 0.5), dset); +} + +BOOST_FIXTURE_TEST_CASE(iqNormalization, ImageSetFixtureBase) { + initTable(1, 1); + addToImageSet(0, aocommon::Polarization::StokesI, 100); + addToImageSet(0, aocommon::Polarization::StokesQ, 100); + const std::set<PolarizationEnum> kLinkedPolarizations{ + aocommon::Polarization::StokesI, aocommon::Polarization::StokesQ}; + ImageSet dset(*table, false, kLinkedPolarizations, 2, 2); + dset = 0.0; + dset.Data(0)[0] = 6.0; + dset.Data(1)[0] = -1.0; + checkLinearValue(0, 5.0, dset); + checkSquaredValue(0, std::sqrt(6.0 * 6.0 + -1.0 * -1.0), dset); +} + +BOOST_FIXTURE_TEST_CASE(linkedINormalization, ImageSetFixtureBase) { + initTable(1, 1); + addToImageSet(0, aocommon::Polarization::StokesI, 100); + addToImageSet(0, aocommon::Polarization::StokesQ, 100); + ImageSet dset(*table, false, {aocommon::Polarization::StokesI}, 2, 2); + dset = 0.0; + dset.Data(0)[0] = 3.0; + dset.Data(1)[0] = -1.0; + checkLinearValue(0, 3.0, dset); + checkSquaredValue(0, 3.0, dset); +} + +BOOST_FIXTURE_TEST_CASE(iquvNormalization, ImageSetFixtureBase) { + initTable(1, 1); + addToImageSet(0, aocommon::Polarization::StokesI, 100); + addToImageSet(0, aocommon::Polarization::StokesQ, 100); + addToImageSet(0, aocommon::Polarization::StokesU, 100); + addToImageSet(0, aocommon::Polarization::StokesV, 100); + const std::set<PolarizationEnum> kLinkedPolarizations{ + aocommon::Polarization::StokesI, aocommon::Polarization::StokesQ, + aocommon::Polarization::StokesU, aocommon::Polarization::StokesV}; + ImageSet dset(*table, false, kLinkedPolarizations, 2, 2); + dset = 0.0; + dset.Data(0)[0] = 9.0; + dset.Data(1)[0] = 0.2; + dset.Data(2)[0] = 0.2; + dset.Data(3)[0] = 0.2; + checkLinearValue(0, 9.6, dset); + checkSquaredValue(0, std::sqrt(9.0 * 9.0 + 3.0 * 0.2 * 0.2), dset); +} + +BOOST_FIXTURE_TEST_CASE(xx_xy_yx_yyNormalization, ImageSetFixtureBase) { + initTable(1, 1); + addToImageSet(0, aocommon::Polarization::XX, 100); + addToImageSet(0, aocommon::Polarization::XY, 100); + addToImageSet(0, aocommon::Polarization::YX, 100); + addToImageSet(0, aocommon::Polarization::YY, 100); + const std::set<PolarizationEnum> kLinkedPolarizations{ + aocommon::Polarization::XX, aocommon::Polarization::XY, + aocommon::Polarization::YX, aocommon::Polarization::YY}; + ImageSet dset(*table, false, kLinkedPolarizations, 2, 2); + dset = 0.0; + dset.Data(0)[1] = 10.0; + dset.Data(1)[1] = 0.25; + dset.Data(2)[1] = 0.25; + dset.Data(3)[1] = 10.0; + checkLinearValue(1, 10.25f, dset); + checkSquaredValue( + 1, std::sqrt((10.0f * 10.0f * 2.0f + 0.25f * 0.25f * 2.0f) * 0.5f), dset); +} + +BOOST_FIXTURE_TEST_CASE(xx_xy_yx_yy_2channel_Normalization, + ImageSetFixtureBase) { + initTable(2, 2); + addToImageSet(0, aocommon::Polarization::XX, 100); + addToImageSet(0, aocommon::Polarization::XY, 100); + addToImageSet(0, aocommon::Polarization::YX, 100); + addToImageSet(0, aocommon::Polarization::YY, 100); + addToImageSet(1, aocommon::Polarization::XX, 200); + addToImageSet(1, aocommon::Polarization::XY, 200); + addToImageSet(1, aocommon::Polarization::YX, 200); + addToImageSet(1, aocommon::Polarization::YY, 200); + const std::set<PolarizationEnum> kLinkedPolarizations{ + aocommon::Polarization::XX, aocommon::Polarization::XY, + aocommon::Polarization::YX, aocommon::Polarization::YY}; + ImageSet dset(*table, false, kLinkedPolarizations, 2, 2); + dset = 0.0; + dset.Data(0)[2] = 5.0; + dset.Data(1)[2] = 0.1; + dset.Data(2)[2] = 0.2; + dset.Data(3)[2] = 6.0; + dset.Data(4)[2] = 7.0; + dset.Data(5)[2] = 0.3; + dset.Data(6)[2] = 0.4; + dset.Data(7)[2] = 8.0; + double sqVal1 = 0.0, sqVal2 = 0.0; + for (size_t i = 0; i != 4; ++i) { + sqVal1 += dset[i][2] * dset[i][2]; + sqVal2 += dset[i + 4][2] * dset[i + 4][2]; + } + checkLinearValue(2, 27.0 * 0.25, dset); + checkSquaredValue( + 2, (std::sqrt(sqVal1 * 0.5) + std::sqrt(sqVal2 * 0.5)) * 0.5, dset); +} + +BOOST_FIXTURE_TEST_CASE(qu_squared_2channel_Normalization, + ImageSetFixtureBase) { + initTable(2, 2); + addToImageSet(0, aocommon::Polarization::StokesQ, 100); + addToImageSet(0, aocommon::Polarization::StokesU, 100); + addToImageSet(1, aocommon::Polarization::StokesQ, 100); + addToImageSet(1, aocommon::Polarization::StokesU, 100); + const std::set<PolarizationEnum> kJoinedPolarizations{ + aocommon::Polarization::StokesQ, aocommon::Polarization::StokesU}; + const bool kSquaredJoins = true; + ImageSet dset(*table, kSquaredJoins, kJoinedPolarizations, 2, 2); + dset = 0.0; + const size_t kCheckedPixel = 2; + dset.Data(0)[kCheckedPixel] = 5.0; + dset.Data(1)[kCheckedPixel] = 6.0; + dset.Data(2)[kCheckedPixel] = 7.0; + dset.Data(3)[kCheckedPixel] = 8.0; + double sqVal = 0.0; + for (size_t i = 0; i != 4; ++i) { + sqVal += dset[i][kCheckedPixel] * dset[i][kCheckedPixel]; + } + checkSquaredValue(kCheckedPixel, std::sqrt(sqVal / 4.0), dset); +} + +BOOST_FIXTURE_TEST_CASE(linked_xx_yy_2channel_Normalization, + ImageSetFixtureBase) { + initTable(2, 2); + addToImageSet(0, aocommon::Polarization::XX, 100); + addToImageSet(0, aocommon::Polarization::XY, 100); + addToImageSet(0, aocommon::Polarization::YX, 100); + addToImageSet(0, aocommon::Polarization::YY, 100); + addToImageSet(1, aocommon::Polarization::XX, 200); + addToImageSet(1, aocommon::Polarization::XY, 200); + addToImageSet(1, aocommon::Polarization::YX, 200); + addToImageSet(1, aocommon::Polarization::YY, 200); + const std::set<PolarizationEnum> kLinkedPolarizations{ + aocommon::Polarization::XX, aocommon::Polarization::YY}; + ImageSet dset(*table, false, kLinkedPolarizations, 2, 2); + dset = 0.0; + dset.Data(0)[2] = 7.5; + dset.Data(1)[2] = 0.1; + dset.Data(2)[2] = -0.2; + dset.Data(3)[2] = 6.5; + dset.Data(4)[2] = 8.5; + dset.Data(5)[2] = 0.3; + dset.Data(6)[2] = -0.4; + dset.Data(7)[2] = 9.5; + double sqVal1 = dset[0][2] * dset[0][2] + dset[3][2] * dset[3][2], + sqVal2 = dset[4][2] * dset[4][2] + dset[7][2] * dset[7][2]; + checkLinearValue(2, 32.0 * 0.25, dset); + checkSquaredValue( + 2, (std::sqrt(sqVal1 * 0.5) + std::sqrt(sqVal2 * 0.5)) * 0.5, dset); +} + +BOOST_FIXTURE_TEST_CASE(linked_xx_2channel_Normalization, ImageSetFixtureBase) { + initTable(2, 2); + addToImageSet(0, aocommon::Polarization::XX, 100); + addToImageSet(0, aocommon::Polarization::XY, 100); + addToImageSet(0, aocommon::Polarization::YX, 100); + addToImageSet(0, aocommon::Polarization::YY, 100); + addToImageSet(1, aocommon::Polarization::XX, 200); + addToImageSet(1, aocommon::Polarization::XY, 200); + addToImageSet(1, aocommon::Polarization::YX, 200); + addToImageSet(1, aocommon::Polarization::YY, 200); + ImageSet dset(*table, false, {aocommon::Polarization::XX}, 2, 2); + dset = 0.0; + dset.Data(0)[2] = 7.5; + dset.Data(1)[2] = 0.1; + dset.Data(2)[2] = -0.2; + dset.Data(3)[2] = 6.5; + dset.Data(4)[2] = 8.5; + dset.Data(5)[2] = 0.3; + dset.Data(6)[2] = -0.4; + dset.Data(7)[2] = 9.5; + double sqVal1 = dset[0][2] * dset[0][2], sqVal2 = dset[4][2] * dset[4][2]; + checkLinearValue(2, 32.0 * 0.25, dset); + checkSquaredValue(2, (std::sqrt(sqVal1) + std::sqrt(sqVal2)) * 0.5, dset); +} + +BOOST_FIXTURE_TEST_CASE(deconvchannels_normalization, ImageSetFixtureBase) { + initTable(4, 2); + addToImageSet(0, aocommon::Polarization::StokesI, 100, 1); + addToImageSet(1, aocommon::Polarization::StokesI, 200, 1); + addToImageSet(2, aocommon::Polarization::StokesI, 300, 2); + addToImageSet(3, aocommon::Polarization::StokesI, 400, 2); + ImageSet dset(*table, false, {aocommon::Polarization::StokesI}, 2, 2); + dset = 0.0; + dset.Data(0)[0] = 10.0; + dset.Data(1)[0] = 13.0; + checkLinearValue(0, 12.0, dset); + checkSquaredValue(0, 12.0, dset); +} + +BOOST_FIXTURE_TEST_CASE(deconvchannels_zeroweight, ImageSetFixtureBase) { + initTable(4, 2); + addToImageSet(0, aocommon::Polarization::StokesI, 100, 1); + addToImageSet(1, aocommon::Polarization::StokesI, 200, 0); + addToImageSet(2, aocommon::Polarization::StokesI, 300, 2); + addToImageSet(3, aocommon::Polarization::StokesI, 400, 2); + ImageSet dset(*table, false, {aocommon::Polarization::StokesI}, 2, 2); + dset = 0.0; + dset.Data(0)[0] = 10.0; + dset.Data(1)[0] = 5.0; + checkLinearValue(0, 6.0, dset); + checkSquaredValue(0, 6.0, dset); +} + +BOOST_FIXTURE_TEST_CASE(deconvchannels_divisor, ImageSetFixtureBase) { + initTable(16, 3); + for (size_t ch = 0; ch != table->OriginalGroups().size(); ++ch) { + addToImageSet(ch, aocommon::Polarization::StokesI, 100 + ch, 1); + } + ImageSet dset(*table, false, {aocommon::Polarization::StokesI}, 2, 2); + dset = 0.0; + for (size_t ch = 0; ch != table->DeconvolutionGroups().size(); ++ch) { + dset.Data(ch)[0] = 7.0; + } + checkLinearValue(0, 7.0, dset); + checkSquaredValue(0, 7.0, dset); + + BOOST_CHECK_EQUAL(dset.PSFIndex(0), 0u); + BOOST_CHECK_EQUAL(dset.PSFIndex(1), 1u); + BOOST_CHECK_EQUAL(dset.PSFIndex(2), 2u); +} + +BOOST_FIXTURE_TEST_CASE(psfindex, ImageSetFixtureBase) { + initTable(4, 2); + for (size_t ch = 0; ch != table->OriginalGroups().size(); ++ch) { + addToImageSet(ch, aocommon::Polarization::XX, 100, 1); + addToImageSet(ch, aocommon::Polarization::XY, 200, 0); + addToImageSet(ch, aocommon::Polarization::YX, 300, 2); + addToImageSet(ch, aocommon::Polarization::YY, 400, 2); + } + const std::set<PolarizationEnum> kLinkedPolarizations{ + aocommon::Polarization::XX, aocommon::Polarization::XY, + aocommon::Polarization::YX, aocommon::Polarization::YY}; + ImageSet dset(*table, false, kLinkedPolarizations, 2, 2); + + BOOST_CHECK_EQUAL(dset.PSFIndex(0), 0u); + BOOST_CHECK_EQUAL(dset.PSFIndex(1), 0u); + BOOST_CHECK_EQUAL(dset.PSFIndex(2), 0u); + BOOST_CHECK_EQUAL(dset.PSFIndex(3), 0u); + BOOST_CHECK_EQUAL(dset.PSFIndex(4), 1u); + BOOST_CHECK_EQUAL(dset.PSFIndex(5), 1u); + BOOST_CHECK_EQUAL(dset.PSFIndex(6), 1u); + BOOST_CHECK_EQUAL(dset.PSFIndex(7), 1u); +} + +BOOST_FIXTURE_TEST_CASE(load_and_average, ImageSetFixtureBase) { + initTable(6, 2); + const size_t nPol = 2; + const PolarizationEnum pols[nPol] = {PolarizationEnum::XX, + PolarizationEnum::YY}; + const size_t width = 7; + const size_t height = 9; + const std::vector<double> weights{4.0, 4.0, 0.0, 0.0, 1.0, 1.0}; + Image storedImage(width, height); + for (size_t ch = 0; ch != table->OriginalGroups().size(); ++ch) { + for (size_t p = 0; p != nPol; ++p) { + size_t index = ch * nPol + p; + storedImage = (1 << index); // assign the entire image to 2^index + modelImage = storedImage; + addToImageSet(ch, pols[p], 100 + ch, weights[ch]); + } + } + const std::set<PolarizationEnum> kLinkedPolarizations{ + aocommon::Polarization::XX, aocommon::Polarization::YY}; + + ImageSet imageSet(*table, false, kLinkedPolarizations, width, height); + imageSet.LoadAndAverage(false); + // The first image has all values set to 2^0, the second image 2^1, etc... + // The XX polarizations of deconvolution channel 1 consists of + // images 0, 2 and 4. These have been weighted with 4, 4, 0: + BOOST_CHECK_CLOSE_FRACTION(imageSet[0 * nPol + 0][0], + double(1 * 4 + 4 * 4 + 16 * 0) / 8.0, 1e-6); + // The YY polarizations consists of images 1, 3 and 5, weights 4, 4, 0: + BOOST_CHECK_CLOSE_FRACTION(imageSet[0 * nPol + 1][0], + double(2 * 4 + 8 * 4 + 32 * 0) / 8.0, 1e-6); + // The XX polarizations of deconvolution channel 2 consists of images 6, 8 and + // 10 Weights 0, 1, 1 + BOOST_CHECK_CLOSE_FRACTION(imageSet[1 * nPol + 0][0], + double(64 * 0 + 256 * 1 + 1024 * 1) / 2.0, 1e-6); + // YY: images 7, 9, 10, weights 0, 1, 1 + BOOST_CHECK_CLOSE_FRACTION(imageSet[1 * nPol + 1][0], + double(128 * 0 + 512 * 1 + 2048 * 1) / 2.0, 1e-6); + + // The total linear integrated sum should be a complete + // weighting of all input channels + Image linearIntegrated(width, height); + imageSet.GetLinearIntegrated(linearIntegrated); + BOOST_CHECK_CLOSE_FRACTION( + linearIntegrated[0], + double(1 * 4 + 4 * 4 + 16 * 0 + 2 * 4 + 8 * 4 + 32 * 0 + 64 * 0 + + 256 * 1 + 1024 * 1 + 128 * 0 + 512 * 1 + 2048 * 1) / + 20.0, + 1e-6); + + BOOST_CHECK_THROW(imageSet.LoadAndAverage(true), std::logic_error); +} + +BOOST_AUTO_TEST_SUITE_END() +} // namespace radler diff --git a/cpp/utils/application.h b/cpp/utils/application.h new file mode 100644 index 00000000..2610a98a --- /dev/null +++ b/cpp/utils/application.h @@ -0,0 +1,49 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef RADLER_UTILS_APPLICATION_H_ +#define RADLER_UTILS_APPLICATION_H_ + +#include <aocommon/logger.h> + +#include <sys/types.h> +#include <sys/wait.h> + +#include <unistd.h> + +namespace radler::utils { + +class Application { + public: + static void Run(const std::string& commandLine) { + aocommon::Logger::Info << "Running: " << commandLine << '\n'; + const char* commandLineCStr = commandLine.c_str(); + int pid = vfork(); + switch (pid) { + case -1: // Error + throw std::runtime_error( + "Could not vfork() new process for executing command line " + "application"); + case 0: // Child + execl("/bin/sh", "sh", "-c", commandLineCStr, NULL); + _exit(127); + } + // Wait for process to terminate + int pStatus; + do { + int pidReturn; + do { + pidReturn = waitpid(pid, &pStatus, 0); + } while (pidReturn == -1 && errno == EINTR); + } while (!WIFEXITED(pStatus) && !WIFSIGNALED(pStatus)); + if (WIFEXITED(pStatus)) { + // all good + // const int exitStatus = WEXITSTATUS(pStatus); + } else { + throw std::runtime_error( + "Running command line application returned an error"); + } + } +}; +} // namespace radler::utils +#endif // RADLER_UTILS_APPLICATION_H_ diff --git a/cpp/utils/casa_mask_reader.cc b/cpp/utils/casa_mask_reader.cc new file mode 100644 index 00000000..13cc3f56 --- /dev/null +++ b/cpp/utils/casa_mask_reader.cc @@ -0,0 +1,40 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "utils/casa_mask_reader.h" + +#include <casacore/tables/Tables/Table.h> +#include <casacore/tables/Tables/ArrayColumn.h> + +#include <aocommon/uvector.h> + +namespace radler::utils { + +CasaMaskReader::CasaMaskReader(const std::string& path) : _path(path) { + casacore::Table table(path); + casacore::ROArrayColumn<float> mapCol(table, "map"); + casacore::IPosition shape = mapCol.shape(0); + _width = shape(0); + _height = shape(1); + _nPolarizations = shape(2); + _nChannels = shape(3); +} + +void CasaMaskReader::Read(bool* mask) { + casacore::Table table(_path); + casacore::ROArrayColumn<float> mapCol(table, "map"); + casacore::Array<float> data(mapCol.get(0)); + for (size_t i = 0; i != _width * _height; ++i) mask[i] = false; + casacore::Array<float>::contiter iter = data.cbegin(); + bool* maskPtr = mask; + for (size_t j = 0; j != _nChannels * _nPolarizations; ++j) { + for (size_t y = 0; y != _height; ++y) { + for (size_t x = 0; x != _width; ++x) { + *maskPtr = *maskPtr || (*iter != 0.0); + ++iter; + ++maskPtr; + } + } + } +} +} // namespace radler::utils \ No newline at end of file diff --git a/cpp/utils/casa_mask_reader.h b/cpp/utils/casa_mask_reader.h new file mode 100644 index 00000000..0c8ccda0 --- /dev/null +++ b/cpp/utils/casa_mask_reader.h @@ -0,0 +1,26 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef RADLER_UTILS_CASA_MASK_READER_H_ +#define RADLER_UTILS_CASA_MASK_READER_H_ + +#include <string> + +namespace radler::utils { +class CasaMaskReader { + public: + explicit CasaMaskReader(const std::string& path); + + void Read(bool* mask); + + size_t Width() const { return _width; } + size_t Height() const { return _height; } + size_t NPolarizations() const { return _nPolarizations; } + size_t NChannels() const { return _nChannels; } + + private: + std::string _path; + size_t _width, _height, _nPolarizations, _nChannels; +}; +} // namespace radler::utils +#endif diff --git a/cpp/utils/write_model.h b/cpp/utils/write_model.h new file mode 100644 index 00000000..6a3bfa91 --- /dev/null +++ b/cpp/utils/write_model.h @@ -0,0 +1,61 @@ +// Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef RADLER_UTILS_MODEL_WRITE_MODEL_H_ +#define RADLER_UTILS_MODEL_WRITE_MODEL_H_ + +#include <fstream> + +#include <boost/filesystem/operations.hpp> + +#include <aocommon/radeccoord.h> +#include <aocommon/uvector.h> + +namespace radler::utils { +inline void WriteHeaderForSpectralTerms(std::ostream& stream, + double reference_frequency) { + stream.precision(15); + stream << "Format = Name, Type, Ra, Dec, I, SpectralIndex, LogarithmicSI, " + "ReferenceFrequency='" + << reference_frequency << "', MajorAxis, MinorAxis, Orientation\n"; +} + +inline void AddSiTerms(std::ostream& stream, + const std::vector<float>& si_terms) { + assert(!si_terms.empty()); + stream << si_terms.front() << ",["; + if (si_terms.size() >= 2) { + stream << si_terms[1]; + for (size_t i = 2; i != si_terms.size(); ++i) { + stream << ',' << si_terms[i]; + } + } + stream << ']'; +} + +inline void WritePolynomialPointComponent( + std::ostream& stream, const std::string& name, long double ra, + long double dec, bool use_log_si, + const std::vector<float>& polarization_terms, + double reference_frequency_hz) { + stream << name << ",POINT," << aocommon::RaDecCoord::RAToString(ra, ':') + << ',' << aocommon::RaDecCoord::DecToString(dec, '.') << ','; + AddSiTerms(stream, polarization_terms); + stream << "," << (use_log_si ? "true" : "false") << "," + << reference_frequency_hz << ",,,\n"; +} + +inline void WritePolynomialGaussianComponent( + std::ostream& stream, const std::string& name, long double ra, + long double dec, bool use_log_si, + const std::vector<float>& polarization_terms, double reference_frequency_hz, + double maj, double min, double position_angle) { + stream << name << ",GAUSSIAN," << aocommon::RaDecCoord::RAToString(ra, ':') + << ',' << aocommon::RaDecCoord::DecToString(dec, '.') << ','; + AddSiTerms(stream, polarization_terms); + stream << "," << (use_log_si ? "true" : "false") << "," + << reference_frequency_hz << "," << maj << ',' << min << ',' + << position_angle << "\n"; +} +} // namespace radler::utils +#endif diff --git a/docker/ubuntu_20_04_base b/docker/ubuntu_20_04_base new file mode 100644 index 00000000..12d94cfb --- /dev/null +++ b/docker/ubuntu_20_04_base @@ -0,0 +1,17 @@ +# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +# SPDX-License-Identifier: GPL-3.0-or-later + +# base +FROM ubuntu:20.04 +RUN export DEBIAN_FRONTEND=noninteractive && apt-get update && apt-get upgrade -y && \ +# install astronomy packages + apt-get -y install libcfitsio-dev wcslib-dev libfftw3-dev libgsl-dev \ +# install misc packages + wget git make cmake g++ doxygen \ +# install dependencies + libboost-all-dev libhdf5-dev libpython3-dev python3-pip \ + casacore-dev casacore-tools clang-format-12 \ +# The formatter needs a binary named 'clang-format', not 'clang-format-12'. +# Same for clang-tidy-12. + && ln -sf clang-format-12 /usr/bin/clang-format \ + && python3 -m pip install gcovr==5.0 cmake-format \ No newline at end of file diff --git a/docker/ubuntu_22_04_base b/docker/ubuntu_22_04_base new file mode 100644 index 00000000..ccaf7009 --- /dev/null +++ b/docker/ubuntu_22_04_base @@ -0,0 +1,17 @@ +# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +# SPDX-License-Identifier: GPL-3.0-or-later + +# base +FROM ubuntu:22.04 +RUN export DEBIAN_FRONTEND=noninteractive && apt-get update && apt-get upgrade -y && \ +# install astronomy packages + apt-get -y install libcfitsio-dev wcslib-dev libfftw3-dev libgsl-dev \ +# install misc packages + wget git make cmake g++ doxygen \ +# install dependencies + libboost-all-dev libhdf5-dev libpython3-dev python3-pip \ + casacore-dev casacore-tools clang-format-12 \ +# The formatter needs a binary named 'clang-format', not 'clang-format-12'. +# Same for clang-tidy-12. + && ln -sf clang-format-12 /usr/bin/clang-format \ + && python3 -m pip install gcovr==5.0 cmake-format \ No newline at end of file diff --git a/external/aocommon b/external/aocommon new file mode 160000 index 00000000..415a44c7 --- /dev/null +++ b/external/aocommon @@ -0,0 +1 @@ +Subproject commit 415a44c78bc23509e74930f318a495ac3e4dd0ab diff --git a/external/pybind11 b/external/pybind11 new file mode 160000 index 00000000..914c06fb --- /dev/null +++ b/external/pybind11 @@ -0,0 +1 @@ +Subproject commit 914c06fb252b6cc3727d0eedab6736e88a3fcb01 diff --git a/external/schaapcommon b/external/schaapcommon new file mode 160000 index 00000000..f62867e3 --- /dev/null +++ b/external/schaapcommon @@ -0,0 +1 @@ +Subproject commit f62867e3e52bbccd0a1dbf059f829d8a9947e968 diff --git a/scripts/run-format.sh b/scripts/run-format.sh new file mode 100755 index 00000000..f0f1c49d --- /dev/null +++ b/scripts/run-format.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +# SPDX-License-Identifier: GPL-3.0-or-later + +#The directory that contains the source files. +SOURCE_DIR=$(dirname "$0")/.. + +#Directories that must be excluded from formatting. These paths are +#relative to SOURCE_DIR. +EXCLUDE_DIRS=(external build CMake) + +#The patterns of the C++ source files, which clang-format should format. +CXX_SOURCES=(*.cc *.h) + +#The patterns of the CMake source files, which cmake-format should format. +CMAKE_SOURCES=(CMakeLists.txt *.cmake) + +#End script configuration. + +#The common formatting script has further documentation. +source $(dirname "$0")/../external/aocommon/scripts/format.sh -- GitLab