From c880d6a252cd98baf71aa440e2daf7352eb4a2d4 Mon Sep 17 00:00:00 2001 From: AliceMazzoleni99 Date: Wed, 16 Sep 2026 13:53:02 +0200 Subject: [PATCH] Dev/python bindings for strixels (#364) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - added release notes - added documentation for python - added python bindings --------- Co-authored-by: vhinger Co-authored-by: Erik Fröjdh --- CMakeLists.txt | 5 +- RELEASE.md | 1 + docs/src/index.rst | 1 + docs/src/python/strixelremap/index.rst | 43 ++ .../python/strixelremap/pyInclusiveROI.rst | 17 + .../python/strixelremap/pyPredefinedMaps.rst | 40 ++ .../pyPredefinedSensorConfigs.rst | 151 ++++++ .../strixelremap/pySensorConfiguration.rst | 57 +++ .../pyStrixelPixelRemapAlgorithm.rst | 12 + .../BaseStrixelPixelMap.hpp | 461 ++++++++++++++++++ .../StrixelPixelRemapping/InclusiveROI.hpp | 4 + .../StrixelPixelMaps.hpp | 196 ++++++++ .../StrixelPixelRemapAlgorithm.hpp | 141 ------ .../StrixelPixelRemapConfig.hpp | 1 + .../StrixelPixelRemapDefs.hpp | 12 +- .../StrixelPixelRemapFormat.hpp | 16 +- .../StrixelPixelRemapGenerate.hpp | 446 ----------------- python/CMakeLists.txt | 2 + python/aare/RawFile.py | 61 ++- python/aare/__init__.py | 2 + .../strixelremap/StrixelRemapFactories.py | 41 ++ python/aare/strixelremap/__init__.py | 6 + python/benchmarks/remap_benchmark.py | 36 ++ python/src/StrixelRemap/bind_InclusiveROI.hpp | 114 +++++ .../StrixelRemap/bind_PredefinedVariables.hpp | 151 ++++++ .../StrixelRemap/bind_StrixelPixelMapDefs.hpp | 182 +++++++ python/src/StrixelRemap/bind_StrixelRemap.hpp | 214 ++++++++ python/src/bind_Defs.hpp | 16 + python/src/module.cpp | 28 ++ python/tests/test_RawFile.py | 17 + python/tests/test_StrixelPixelRemapAPI.py | 222 +++++++++ python/tests/test_helpers/RawFileHelpers.py | 108 ++++ .../StrixelPixelRemapAlgorithm.cpp | 149 ------ .../StrixelPixelRemapAlgorithm.test.cpp | 118 +++-- .../StrixelPixelRemapFormat.cpp | 76 +-- 35 files changed, 2321 insertions(+), 826 deletions(-) create mode 100644 docs/src/python/strixelremap/index.rst create mode 100644 docs/src/python/strixelremap/pyInclusiveROI.rst create mode 100644 docs/src/python/strixelremap/pyPredefinedMaps.rst create mode 100644 docs/src/python/strixelremap/pyPredefinedSensorConfigs.rst create mode 100644 docs/src/python/strixelremap/pySensorConfiguration.rst create mode 100644 docs/src/python/strixelremap/pyStrixelPixelRemapAlgorithm.rst create mode 100644 include/aare/StrixelPixelRemapping/BaseStrixelPixelMap.hpp create mode 100644 include/aare/StrixelPixelRemapping/StrixelPixelMaps.hpp delete mode 100644 include/aare/StrixelPixelRemapping/StrixelPixelRemapAlgorithm.hpp delete mode 100644 include/aare/StrixelPixelRemapping/StrixelPixelRemapGenerate.hpp create mode 100644 python/aare/strixelremap/StrixelRemapFactories.py create mode 100644 python/aare/strixelremap/__init__.py create mode 100644 python/benchmarks/remap_benchmark.py create mode 100644 python/src/StrixelRemap/bind_InclusiveROI.hpp create mode 100644 python/src/StrixelRemap/bind_PredefinedVariables.hpp create mode 100644 python/src/StrixelRemap/bind_StrixelPixelMapDefs.hpp create mode 100644 python/src/StrixelRemap/bind_StrixelRemap.hpp create mode 100644 python/tests/test_StrixelPixelRemapAPI.py create mode 100644 python/tests/test_helpers/RawFileHelpers.py delete mode 100644 src/StrixelPixelRemapping/StrixelPixelRemapAlgorithm.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 097df0c9..17a9cfcd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -367,11 +367,11 @@ set(PUBLICHEADERS include/aare/hist/PedestalTrackingPixelHistogram.hpp include/aare/GainMap.hpp include/aare/StrixelPixelRemapping/InclusiveROI.hpp - include/aare/StrixelPixelRemapping/StrixelPixelRemapAlgorithm.hpp include/aare/StrixelPixelRemapping/StrixelPixelRemapConfig.hpp include/aare/StrixelPixelRemapping/StrixelPixelRemapDefs.hpp include/aare/StrixelPixelRemapping/StrixelPixelRemapFormat.hpp - include/aare/StrixelPixelRemapping/StrixelPixelRemapGenerate.hpp + include/aare/StrixelPixelRemapping/BaseStrixelPixelMap.hpp + include/aare/StrixelPixelRemapping/StrixelPixelMaps.hpp include/aare/ROI.hpp include/aare/ROIGeometry.hpp include/aare/DetectorGeometry.hpp @@ -415,7 +415,6 @@ set(SourceFiles ${CMAKE_CURRENT_SOURCE_DIR}/src/RawFile.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/RawMasterFile.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/RawSubFile.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/StrixelPixelRemapping/StrixelPixelRemapAlgorithm.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/StrixelPixelRemapping/StrixelPixelRemapFormat.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/to_string.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/utils/task.cpp diff --git a/RELEASE.md b/RELEASE.md index 29beb5ae..54eede05 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -24,6 +24,7 @@ - Added the ``AARE_TUNE_LOCAL`` CMake option to build with ``-march=native`` and ``-mtune=native`` when supported. Binaries built with this option are specific to the local CPU and may not be portable. +- Support for PixeltoStrixelRemapping. Precomputedt maps are available of the most common sensor-module configurations of iLGAD abnd TEW sensors on the Jungfrau module. - Added string representator in python for Cluster and Eta - Added roi slice method in python for easy slicing of numpy arrays ``array[roi.slice()]``. - added context manager for ``aare.RawMasterFile`` diff --git a/docs/src/index.rst b/docs/src/index.rst index 94500e3e..fc544c6e 100644 --- a/docs/src/index.rst +++ b/docs/src/index.rst @@ -31,6 +31,7 @@ AARE python/experimental/index python/histogram/index python/pedestal/index + python/strixelremap/index pyFit diff --git a/docs/src/python/strixelremap/index.rst b/docs/src/python/strixelremap/index.rst new file mode 100644 index 00000000..a193d661 --- /dev/null +++ b/docs/src/python/strixelremap/index.rst @@ -0,0 +1,43 @@ +Pixel to Strixel Remapping +============================ + + +.. toctree:: + :caption: Pixel to Strixel Remapping + :maxdepth: 1 + + pySensorConfiguration + pyStrixelPixelRemapAlgorithm + pyInclusiveROI + pyPredefinedMaps + pyPredefinedSensorConfigs + + +Example Usage +------------- + +.. code:: python + + from aare import strixelremap + import numpy as np + + from aare import RawFile + + file = RawFile("path/to/master_file.json") # Load a raw file with strixel data + + _, frame = file.read_frame() # Read the first frame of the file + + rx_roi = frame.master().rois[0] # Get ROI of the frame + + # Get the pixel to strixel map for the 25 µm pitch strixels placed on Chip1 + pixel_to_strixel_map_25um = strixelremap.jungfrau_ilgad_singlechip_25um_strixel_map(user_roi = strixelremap.toInclusiveROI(rx_roi), placement = strixelremap.Chip1) + + + # map(row, col) gives flattened index of the pixel mapped to strixel at (row, col) + order_map = pixel_to_strixel_map_25um.map + + strixels = np.empty(order_map.shape, dtype=np.uint16) + + # Apply the remapping to a given strixel data array + strixelremap.apply_remap(frame.astype(np.uint16), order_map, strixels) + \ No newline at end of file diff --git a/docs/src/python/strixelremap/pyInclusiveROI.rst b/docs/src/python/strixelremap/pyInclusiveROI.rst new file mode 100644 index 00000000..a2a2e15e --- /dev/null +++ b/docs/src/python/strixelremap/pyInclusiveROI.rst @@ -0,0 +1,17 @@ +Inclusive ROI +================ + +Class for Region of Interest (ROI) using inclusive bounds. +The class is primarily used in the context of pixel-to-strixel remapping. + + +.. py:currentmodule:: aare.strixelremap + +.. autoclass:: InclusiveROI + :special-members: __init__ + :members: + +.. autofunction:: toInclusiveROI + +.. autofunction:: toHalfopenROI + diff --git a/docs/src/python/strixelremap/pyPredefinedMaps.rst b/docs/src/python/strixelremap/pyPredefinedMaps.rst new file mode 100644 index 00000000..d54bdf1f --- /dev/null +++ b/docs/src/python/strixelremap/pyPredefinedMaps.rst @@ -0,0 +1,40 @@ +Predefined Pixel to Strixel Maps +================================ + +Includes predefined pixel to strixel maps for the sensor configurations introduced in :ref:`python_predefined_sensor_configs`. + +Predefined Maps for 2x2 cm iLGAD Sensor +---------------------------------------- + +.. py:currentmodule:: aare.strixelremap + +.. autofunction:: jungfrau_ilgad_singlechip_25um_strixel_map + +.. autofunction:: jungfrau_ilgad_singlechip_15um_strixel_map + +.. autofunction:: jungfrau_ilgad_singlechip_18um_strixel_map + +.. autofunction:: jungfrau_ilgad_strixel_maps + + +Predefined Maps for 2x2 cm TEW Sensor +---------------------------------------- + +.. autofunction:: jungfrau_tew_singlechip_25um_strixel_map + +.. autofunction:: jungfrau_tew_singlechip_15um_strixel_map + +.. autofunction:: jungfrau_tew_singlechip_18um_strixel_map + +.. autofunction:: jungfrau_tew_strixel_maps + + +Predefined Maps for 4x4 cm iLGAD Sensor +---------------------------------------- + +.. autofunction:: jungfrau_ilgad_quadbottom_25um_strixel_map + +.. autofunction:: jungfrau_ilgad_quadtop_25um_strixel_map + +.. autofunction:: jungfrau_ilgad_quad_25um_strixel_map + diff --git a/docs/src/python/strixelremap/pyPredefinedSensorConfigs.rst b/docs/src/python/strixelremap/pyPredefinedSensorConfigs.rst new file mode 100644 index 00000000..43614321 --- /dev/null +++ b/docs/src/python/strixelremap/pyPredefinedSensorConfigs.rst @@ -0,0 +1,151 @@ + +.. _python_predefined_sensor_configs: + +Predefined Sensor Configurations +================================= + +Includes all predefined sensor configurations for Jungfrau iLGAD (inverse Low-Gain Avalache Diode) and Jungfrau TEW (Thin Entrance Window) sensors. + +The PSI Photon Science Detector Group produces Jungfrau modules with three different sensor types: 2x2 cm iLGAD, 2x2 cm TEW and 4x4 cm iLGAD. + +Below are the predefined sensor configurations for all three sensor types. Detailed information about the sensor geometry, placement and rotation on the module, strixel types and their placement can be found in the dedicated sensor sections further below. + + +Predefined Sensor Configurations +--------------------------------- + +.. py:currentmodule:: aare.strixelremap + +.. data:: SingleChipMP_iLGAD + + Sensor configuration of the 2x2 cm iLGAD sensor with all strixel groups. + +.. data:: SingleChipMP_TEW + + Sensor configuration of the 2x2 cm TEW sensor with all strixel groups. + +.. data:: Quad_iLGAD + + Sensor configuration of the 4x4 cm iLGAD sensor with all strixel groups. + +.. _python_predefined_strixel_groups: + +Predefined Strixel Groups +--------------------------------- + +.. data:: StrxP25 + + Strixel geometry for 25 µm pitch strixels on iLGAD sensors (multiplicity = 3) + +.. data:: StrxP15 + + Strixel geometry for 15 µm pitch strixels on iLGAD sensors (multiplicity = 5) + +.. data:: StrxP18 + + Strixel geometry for 18.75 µm pitch strixels on iLGAD sensors (multiplicity = 4) + +.. data:: StrxP37 + + Strixel geometry for 37.5 µm pitch strixels on iLGAD sensors (multiplicity = 2) + + +.. _python_predefined_sensor_placements: + +Predefined Sensor Placements on Junfrau Modules +------------------------------------------------ + +The PSI Photon Science Detector Group produces Jungfrau modules with a 2x2 cm iLGAD/TEW sensor placed on the position of the second chip (Chip1) and the seventh chip (Chip6) of the module. +The larger 4x4cm iLGAD sensors are typically placed on the module's quad (Chip1+Chip2+Chip5+Chip6). + +.. data:: Chip1 + + Placement of the 2x2cm iLGAD sensor on the second chip (Chip1) of the Jungfrau module, with no rotation applied. + +.. data:: Chip6 + + Placement of the 2x2cm iLGAD sensor on the seventh chip (Chip6) of the Jungfrau module, with a 180-degree rotation applied. + +.. data:: Quad + + Placement of the 4x4cm iLGAD sensor on the quad (Chip1+Chip2+Chip5+Chip6) of the Jungfrau module, with no rotation applied. + +2x2 cm iLGAD Sensor: +--------------------- + +The 2x2 cm iLGAD sensor has a guardring that extends into (and renders unusable for photon detection) part of the active pixel area with height and width of 9 pixels each and spawns over 256x256 pixels. + +.. data:: SingleChipMP_iLGAD_pix + + Pixel geometry for the 2x2 cm iLGAD sensor. + +The PSI Photon Science Detector Group produces Jungfrau modules with 2x2 cm iLGAD sensors placed on the position of the second chip (Chip1) ``Chip1`` and the seventh chip (Chip6) ``Chip6`` of the module. +See :ref:`python_predefined_sensor_placements` for more information. + +The sensor is partitioned into three strixel groups ``StrxP25``, ``StrxP15`` and ``StrxP18``. See :ref:`python_predefined_strixel_groups` for more information. + +Strixel group ``StrxP25`` covers the bottom 25 % of the sensor's pixel area (minus the guard ring pixels). Strixel Group ``StrxP15`` covers the next 25 % of the sensor's pixel area. Strixel group ``StrxP18`` covers the top 50 % of the sensor's pixel, whereby it is divided into two partitions (each 25 % of the pixel area) with different dimensioning of the metal layers on top of strixel implants. + +.. data:: SingleChipMP_iLGAD_P25 + + Strixel group of 25 µm pitch strixels on the 2x2 cm iLGAD sensor. + +.. data:: SingleChipMP_iLGAD_P15 + + Strixel group of 15 µm pitch strixels on the 2x2 cm iLGAD sensor. + +.. data:: SingleChipMP_iLGAD_P18 + + Strixel group of 18.75 µm pitch strixels on the 2x2 cm iLGAD sensor. + +2x2 cm TEW Sensor: +--------------------- + +The 2x2 cm TEW sensor has no guardring extending into the active pixel area and spawns over 256x256 pixels. + +.. data:: SingleChipMP_TEW_pix + + Pixel geometry for the 2x2 cm TEW sensor. + +The Photon Detector Group produces Jungfrau modules with 2x2 cm TEW sensors placed on the position of the second chip (Chip1) ``Chip1`` and the seventh chip (Chip6) ``Chip6`` of the module. +See :ref:`python_predefined_sensor_placements` for more information. + +The sensor is again partitioned into three pixel groups ``StrxP25``, ``StrxP15`` and ``StrxP18``. See :ref:`python_predefined_strixel_groups` for more information. + +The partition is the same as for the 2x2 cm iLGAD sensor. + +.. data:: SingleChipMP_TEW_P25 + + Strixel group of 25 µm pitch strixels on the 2x2 cm TEW sensor. + +.. data:: SingleChipMP_TEW_P15 + + Strixel group of 15 µm pitch strixels on the 2x2 cm TEW sensor. + +.. data:: SingleChipMP_TEW_P18 + + Strixel group of 18.75 µm pitch strixels on the 2x2 cm TEW sensor. + +4x4 cm iLGAD Sensor: +--------------------- + +The 4x4 cm iLGAD sensor has a guardring height and width of 9 pixels each and spawns over 512x512 pixels. + +.. data:: Quad_iLGAD_pix + + Pixel geometry for the 4x4 cm iLGAD sensor. + +The 4x4 cm iLGAD sensor is typically placed on the module's quad (Chip1+Chip2+Chip5+Chip6) ``Quad``. See :ref:`python_predefined_sensor_placements` for more information. + +The sensor is partitioned into two strixel groups ``StrxP25``. See :ref:`python_predefined_strixel_groups` for more information. The bottom sensor half uses forward pixel-to-strixel routing and the top sensor half uses backward pixel to strixel routing. + +.. data:: Quad_iLGAD_bottomhalf + + Strixel group of 25 µm pitch strixels located on the bottom half of 4x4 cm iLGAD sensor. + +.. data:: Quad_iLGAD_tophalf + + Strixel group of 25 µm pitch strixels located on the top half of 4x4 cm iLGAD sensor. + + + diff --git a/docs/src/python/strixelremap/pySensorConfiguration.rst b/docs/src/python/strixelremap/pySensorConfiguration.rst new file mode 100644 index 00000000..046d2158 --- /dev/null +++ b/docs/src/python/strixelremap/pySensorConfiguration.rst @@ -0,0 +1,57 @@ +Sensor configurations +===================== + +Helper structs to define the sensor configuration, such as sensor pixel geometry, placement and rotation of the sensor on the module, the different strixel types on the sensor and its placement. + +.. py:currentmodule:: aare.strixelremap + +.. autoclass:: Rotation + :members: + +.. autoclass:: ModuloOrdering + :members: + +.. autoclass:: BondShift + :members: + :special-members: __init__ + +.. autoclass:: Guardring + :members: + :special-members: __init__ + +.. autoclass:: GroupRouting + :members: + :special-members: __init__ + +.. autoclass:: SensorPixelGeometry + :members: + :special-members: __init__ + + +.. autoclass:: GroupStrixelGeometry + :members: + :special-members: __init__ + +.. autoclass:: GroupConfig + :members: + :special-members: __init__ + +.. autoclass:: SensorModulePlacement + :members: + :special-members: __init__ + +.. autoclass:: StrixelGroupToPixelMap + :members: + :special-members: __init__ + +**Helper functions to create SensorConfig with N pixel groups:** + +.. autofunction:: SensorConfig + :noindex: + + +.. code:: python + + from aare import strixelremap + + SensorConfig(strixelremap.SingleChipMP_iLGAD_pix, [strixelremap.StrxP15, strixelremap.StrxP25]) # returns a SensorConfig_2PixelGroups \ No newline at end of file diff --git a/docs/src/python/strixelremap/pyStrixelPixelRemapAlgorithm.rst b/docs/src/python/strixelremap/pyStrixelPixelRemapAlgorithm.rst new file mode 100644 index 00000000..24cd0ecf --- /dev/null +++ b/docs/src/python/strixelremap/pyStrixelPixelRemapAlgorithm.rst @@ -0,0 +1,12 @@ +Strixel to Pixel Remapping +============================== + +.. py:currentmodule:: aare.strixelremap + +.. autofunction:: strixel_to_pixel_map + +.. autofunction:: strixel_to_pixel_maps + :noindex: + +.. autofunction:: apply_remap + diff --git a/include/aare/StrixelPixelRemapping/BaseStrixelPixelMap.hpp b/include/aare/StrixelPixelRemapping/BaseStrixelPixelMap.hpp new file mode 100644 index 00000000..4d0a7f86 --- /dev/null +++ b/include/aare/StrixelPixelRemapping/BaseStrixelPixelMap.hpp @@ -0,0 +1,461 @@ +#pragma once +#include "aare/StrixelPixelRemapping/StrixelPixelRemapDefs.hpp" + +namespace aare::remap { + +using namespace defs; + +// Internal geometry helper +namespace detail { +/** + * @brief Apply physical transformations to a sensor-local ROI. + * + * IMPORTANT: + * Bond shifts are applied before rotation. + * The order is intentional because bond shifts are defined in the + * sensor's native coordinate system. + * + * This function is not intended to be exposed to the public + */ +InclusiveROI inline update_pixel_group_placement( + InclusiveROI roi, defs::SensorPixelGeometry const &pixel, + defs::BondShift bond_shift, defs::Rotation rot) { + // If there is a bond shift, translate the roi + if (bond_shift.x != 0 || bond_shift.y != 0) + roi = aare::inclusiveroi::geom::translate(roi, bond_shift.x, + bond_shift.y); + + // If there is a rotation given, mirror in X and Y (emulates a rotation) + if (rot == defs::Rotation::Rotate180) + roi = aare::inclusiveroi::geom::mirrorXY(roi, pixel.num_pix_x / 2, + pixel.num_pix_y / 2); + + return roi; +} +} // namespace detail + +namespace detail { +struct StrixelPixelMapBindingAccess; +} + +template class StrixelPixelMap { + friend struct detail::StrixelPixelMapBindingAccess; + + public: + StrixelPixelMap(const SensorConfig &sensor_config, + const SensorModulePlacement &module_placement, + const BondShift &bond_shift = BondShift{0, 0}); + virtual ~StrixelPixelMap() = default; + + /** + * @brief Calculate the strixel-to-pixel order maps for all strixel groups + * based on the user-specified ROI. + * @param user_roi User-specified ROI in the module's native coordinate + * system. + */ + virtual void calculate_map(const ROI &user_roi); + + /** + * @brief Apply the strixel-to-pixel remapping to an input array. + * @param user_roi User-specified ROI in the module's native coordinate + * system. + * @param input Input array to be remapped. + * @return std::array, N> Remapped arrays for each strixel + * group. + */ + template + std::array, M> operator()(const ROI &user_roi, + const NDView input); + + /** + * @brief Apply the strixel-to-pixel remapping to an input array. + * @param input Input array to be remapped. + * @return std::array, N> Remapped arrays for each strixel + * group. + * @throws std::runtime_error If the input shape does not match the user ROI + * shape. + * @note This overload assumes that the user ROI has already been set and + * the map calculated using `calculate_map()`. + */ + template + std::array, M> operator()(const NDView input) const; + + /** + * @brief Apply the strixel-to-pixel remapping to an input array. + * @param input Input array to be remapped. + * @param output Preallocated array to store the remapped results for each + * strixel group. + * @throws std::runtime_error If the output shape does not match the user + * ROI shape. + * @note This overload assumes that the user ROI has already been set and + * the map calculated using `calculate_map()`. + */ + template + void operator()(const NDView input, + std::array, M> &output) const; + + /** + * @brief Get the strixel-to-pixel maps for all strixel groups. + * @return std::array Array of + * strixel-to-pixel maps. Maps are empty if the groups are not covered by + * the user ROI. + */ + std::array get_group_maps() const { + return m_group_maps; + } + + const std::array & + get_group_maps_ref() const { + return m_group_maps; + } + + /** + * @brief Get the strixel-to-pixel map for a specific strixel group. + * @param group_index Index of the strixel group (0-based). + * @return const defs::StrixelGroupToPixelMap& Strixel-to-pixel map for the + * specified group. Map is empty if the group is not covered by the user + * ROI. + * @throws std::out_of_range If the group_index is out of bounds (not in [0, + * M)). + */ + const defs::StrixelGroupToPixelMap & + get_group_map(std::size_t group_index) const { + if (group_index >= M) { + throw std::out_of_range("group_index out of bounds"); + } + return m_group_maps[group_index]; + } + + protected: + /** + * @brief Build the strixel-to-pixel order map for one strixel group. + * The strixel mapping is determined by the group's multiplicity and + * modulo ordering. A reversed modulo ordering reverses the ordering + * within each multiplicity chunk; it does not reverse the complete + * strixel column ordering. + * + * @param group_config Configuration of the strixel group to be mapped. + * @param pixel Sensor pixel geometry to which the group + * is connected. + * @param placement Sensor placement and orientation on the module. + * @param roi_user User-specified ROI in the module's native coordinate + * system. + * @param bond_shift Physical bonding shift in x and y directions defined in + * the sensor's native coordinate system (before it is oriented on the + * module). + * @return A StrixelGroupToPixelMap describing the mapping from strixel + * coordinates to pixel indices in the user-provided ROI. + * - map(strixel_row,strixel_col) = pixel_index_in_user_roi + * - Invalid or unmapped strixel positions are initialized to -1. + * @throws std::logic_error For negative or zero strixel multiplicity. + * @throws std::logic_error If the group ROI width is not divisible by + * the strixel multiplicity. + */ + defs::StrixelGroupToPixelMap + strixel_to_pixel_map(defs::GroupConfig const &group_config); + + /** + * @brief Applies all the group maps to the input. + * @param input Original array + * @return std::array, N> Remapped arrays for each group + * @throws std::invalid_argument If the output shape does not match the + * order map shape for any group. + */ + template + std::array, M> apply_remap(const NDView input) const; + + /** + * @brief Applies a given remapping rule to an input array. + * @param input Original array + * @param order_map Rule for remapping (e.g. the output of a map generator) + * @param output Remapped array + */ + template + void apply_group_remap(const NDView input, NDView output, + const NDView order_map) const; + + protected: + InclusiveROI m_user_roi{}; + std::array + m_group_maps{}; // TODO: do we need to store effective ROI? + SensorConfig m_sensorconfig{}; + + private: + SensorModulePlacement m_module_placement{}; + BondShift m_bond_shift{}; +}; + +template +StrixelPixelMap::StrixelPixelMap( + const SensorConfig &sensor_config, + const SensorModulePlacement &module_placement, const BondShift &bond_shift) + : m_sensorconfig(sensor_config), m_module_placement(module_placement), + m_bond_shift(bond_shift) {} + +template +void StrixelPixelMap::calculate_map(const ROI &user_roi) { + m_user_roi = toInclusiveROI(user_roi); + + for (size_t i = 0; i < N; ++i) { + m_group_maps[i] = strixel_to_pixel_map(m_sensorconfig.group_configs[i]); + } +} + +template +template +std::array, M> +StrixelPixelMap::operator()(const ROI &user_roi, + const NDView input) { + if (m_user_roi.is_empty()) { + calculate_map(user_roi); + return apply_remap(input); + } else if (toInclusiveROI(user_roi) != m_user_roi) { + calculate_map(user_roi); + return apply_remap(input); + } else { + return apply_remap(input); + } +} + +template +template +std::array, M> +StrixelPixelMap::operator()(const NDView input) const { + if (input.shape() != + std::array{m_user_roi.height(), m_user_roi.width()}) { + throw std::runtime_error( + fmt::format("shape mismatch between input and map: input shape = " + "({},{}), expected shape = ({},{})", + input.shape()[0], input.shape()[1], m_user_roi.height(), + m_user_roi.width())); + } + + return apply_remap(input); +} + +template +template +void StrixelPixelMap::operator()( + const NDView input, std::array, M> &output) const { + + if (input.shape() != + std::array{m_user_roi.height(), m_user_roi.width()}) { + throw std::runtime_error( + fmt::format("shape mismatch between input and map: input shape = " + "({},{}), expected shape = ({},{})", + input.shape()[0], input.shape()[1], m_user_roi.height(), + m_user_roi.width())); + } + + if (output.size() != m_group_maps.size()) { + throw std::runtime_error( + "output array size does not match number of group maps"); + } + + for (size_t i = 0; i < m_group_maps.size(); ++i) { + if (m_group_maps[i].empty()) { + output[i] = NDArray{}; + continue; + } + apply_group_remap(input, output[i].view(), m_group_maps[i].map.view()); + } +} + +template +template +std::array, M> +StrixelPixelMap::apply_remap(const NDView input) const { + + // TODO: maybe vector is better - empty ROIs - write tests !!! + std::array, M> outputs; + + for (size_t i = 0; i < m_group_maps.size(); ++i) { + if (m_group_maps[i].empty()) { + outputs[i] = NDArray{}; + continue; + } + outputs[i] = NDArray{m_group_maps[i].map.shape()}; + apply_group_remap(input, outputs[i].view(), m_group_maps[i].map.view()); + } + + return outputs; +} + +template +template +void StrixelPixelMap::apply_group_remap( + const NDView input, NDView output, + const NDView order_map) const { + + if (output.shape() != order_map.shape()) { + throw std::invalid_argument( + "output shape does not match order map shape"); + } + + const auto nrows = order_map.shape(0); + const auto ncols = order_map.shape(1); + + for (ssize_t row = 0; row < nrows; ++row) { + for (ssize_t col = 0; col < ncols; ++col) { + + auto flat_index = order_map(row, col); + + // Intentionally not-mapped pixel in order_map (e.g. guard ring + // pixels) + if (flat_index < 0) { + output(row, col) = T{}; + continue; + } + + // Corrupt map, must throw + if (flat_index >= input.size()) { + throw std::runtime_error( + "order map contains an invalid pixel index."); + } + + // Correctly mapped pixel + output(row, col) = input[flat_index]; + } + } +} + +template +defs::StrixelGroupToPixelMap StrixelPixelMap::strixel_to_pixel_map( + defs::GroupConfig const &group_config) { + + const int multiplicity = group_config.strixel.multiplicity; + + // Defensive check to be sure misconfiguration is avoided + if (multiplicity <= 0) + throw std::logic_error("Strixel multiplicity must be positive"); + + // The group must contain an integer number of strixel columns. + const auto group_width = group_config.placement_on_sensor.width(); + + if (group_width % multiplicity != 0) + throw std::logic_error( + "Group ROI width must be divisible by strixel multiplicity"); + + // const int total_strixel_columns = group_width / multiplicity; + + // Determine the ordering of strixels within each multiplicity group. + std::vector mods(multiplicity); + std::iota(mods.begin(), mods.end(), 0); + + if (group_config.routing.mod_order == defs::ModuloOrdering::Reverse) + std::reverse(mods.begin(), mods.end()); + + // -- 1) Rebase the user ROI (rx_roi) into sensor-local coordinates + const InclusiveROI roi_user_local = inclusiveroi::geom::rebaseROI( + m_user_roi, m_module_placement.placement_on_module); + LOG(logDEBUG) + << "aare::remap::algo::strixel_to_pixel_map: Transformed user ROI: " + << roi_user_local << std::endl; + + LOG(logDEBUG) << "aare::remap::algo::strixel_to_pixel_map: Group ROI " + "before transformation (as in global config)" + << group_config.placement_on_sensor << '\n'; + + // -- 2) Apply the physical bond shift first, sensor rotation second. + const InclusiveROI roi_group = detail::update_pixel_group_placement( + group_config.placement_on_sensor, m_sensorconfig.pixel, m_bond_shift, + m_module_placement.rotation); + + LOG(logDEBUG) << "aare::remap::algo::strixel_to_pixel_map: Group ROI after " + "transformation (as in local transformation) " + << roi_group << '\n'; + + // -- 3) Compute effective ROI = intersection( roi_user, roi_group ) + // Only pixels covered by both the user ROI and the transformed group + // contribute to this map. + const InclusiveROI effective_roi = + inclusiveroi::geom::intersect(roi_user_local, roi_group); + + // If ROIs don't intersect, return empty + if (effective_roi.xmax < effective_roi.xmin || + effective_roi.ymax < effective_roi.ymin) { + LOG(logWARNING) + << "User-supplied ROI does not intersect with configured " + "strixel ROI, returned map is empty!\n"; + return {{}, InclusiveROI::emptyROI()}; + } + + LOG(logDEBUG) << "aare::remap::algo::strixel_to_pixel_map: Result of " + "intersecting ROIs " + << effective_roi << '\n'; + + /****************************** + * Core of the algorithm + * + * Local lambda: + * Convert a sensor-local pixel coordinate into the corresponding + * local strixel coordinate. + * (Could be a separate function if preferred.) + ******************************/ + auto pixel_to_strixel = [&](int x, int y) { + const int dx = x - roi_group.xmin; + const int dy = y - roi_group.ymin; + + const int mod = dx % multiplicity; + const int col = dx / multiplicity; + const int row = dy * multiplicity + mods[mod]; + + return std::pair{row, col}; + }; + + //-- 4) Determine the range of strixel coordinates touched by the effective + // ROI. + // + // Since effective_roi is contained in roi_group: + // dx = x - roi_group.xmin >= 0 + // dy = y - roi_group.ymin >= 0 + // + // The strixel column is dx / multiplicity. + // Each pixel row maps onto a complete block of `multiplicity` + // strixel rows, regardless of the modulo ordering. + const int min_col = (effective_roi.xmin - roi_group.xmin) / multiplicity; + + const int max_col = (effective_roi.xmax - roi_group.xmin) / multiplicity; + + const int min_row = (effective_roi.ymin - roi_group.ymin) * multiplicity; + + // Catch the first row that is out of bounds (next multiplicity group) and + // calculate -1 + const int max_row = + (effective_roi.ymax - roi_group.ymin + 1) * multiplicity - 1; + + // Now from the found bounds of the strixel grid, we define the space to + // allocate for the order map + const int nrows = max_row - min_row + 1; + const int ncols = max_col - min_col + 1; + + // And allocate + aare::NDArray map({nrows, ncols}, -1); + + LOG(logDEBUG) + << "aare::remap::algo::strixel_to_pixel_map: Resulting strixel grid: (" + << map.shape(0) << ", " << map.shape(1) << ")" << '\n'; + + // -- 5) Populate the strixel-to-user-pixel map. + for (int y = effective_roi.ymin; y <= effective_roi.ymax; ++y) { + for (int x = effective_roi.xmin; x <= effective_roi.xmax; ++x) { + + auto [row, col] = pixel_to_strixel(x, y); + + const int map_col = col - min_col; + const int map_row = row - min_row; + + // index into !!!ORIGINAL USER ROI GRID!!! + const ssize_t user_pixel = + static_cast(y - roi_user_local.ymin) * + roi_user_local.width() + + (x - roi_user_local.xmin); + + map(map_row, map_col) = user_pixel; + } + } + + return {map, effective_roi}; +} + +} // namespace aare::remap diff --git a/include/aare/StrixelPixelRemapping/InclusiveROI.hpp b/include/aare/StrixelPixelRemapping/InclusiveROI.hpp index 14609aa8..28a56d5e 100644 --- a/include/aare/StrixelPixelRemapping/InclusiveROI.hpp +++ b/include/aare/StrixelPixelRemapping/InclusiveROI.hpp @@ -45,6 +45,10 @@ struct InclusiveROI { ymax == other.ymax; } + constexpr bool operator!=(InclusiveROI const &other) const noexcept { + return !(*this == other); + } + static InclusiveROI emptyROI() noexcept { return {0, -1, 0, -1}; } // TODO (nice to have) // static InclusiveROI from_shape(ssize_t width, ssize_t height); diff --git a/include/aare/StrixelPixelRemapping/StrixelPixelMaps.hpp b/include/aare/StrixelPixelRemapping/StrixelPixelMaps.hpp new file mode 100644 index 00000000..f649a35a --- /dev/null +++ b/include/aare/StrixelPixelRemapping/StrixelPixelMaps.hpp @@ -0,0 +1,196 @@ +#include "aare/StrixelPixelRemapping/BaseStrixelPixelMap.hpp" +#include "aare/StrixelPixelRemapping/StrixelPixelRemapConfig.hpp" + +namespace aare::remap { + +namespace detail { +// ============================================================ +// Internal implementation details +// ============================================================ +// Current quick-and-dirty inclusion of a helper function to combine group maps. +// This becomes necessary for sensors like Quad_iLGAD that combine two +// independent mapping regions separated by invalid gap rows. (Used to live as +// `combine_maps` in RemapAlgorithm, but arguably should not be part of that +// public API). Eventually, one could instead split RemapGenerate into hpp and +// cpp and make this one live in an unnamed namespace inside the cpp so that it +// cannot be exposed to the user through the public API (if we want to avoid +// that) +/** + * @brief Combine TWO vertically ordered group remapping maps. + * + * Concatenates the maps in the order provided, inserting `gap_rows` + * rows of invalid entries (-1) between consecutive groups. + * + * The input maps must be ordered in the desired output order. + * Both maps must have the same number of columns. + * + * The returned effective ROI is the bounding ROI covering the + * effective ROIs of both input groups. It describes the physical + * source-pixel region and does not encode the artificial strixel + * gap rows in the output map. + * + * @param first Group map that comes first in output space. + * @param second Group map that comes second in output space. + * @param gap_rows Number of invalid strixel rows inserted between groups. + * + * @return Combined strixel-to-pixel map. + * + * @throws std::logic_error if group maps have different widths. + */ +defs::StrixelGroupToPixelMap +combine_group_maps(defs::StrixelGroupToPixelMap const &first, + defs::StrixelGroupToPixelMap const &second, + size_t gap_rows) { + + const ssize_t ncols = first.map.shape(1); + + // Make sure both maps have the same width. + if (second.map.shape(1) != ncols) { + throw std::logic_error("Cannot combine maps with different numbers " + "of columns"); + } + + // Check effective ROIs line up + if (first.effective_roi.xmin != second.effective_roi.xmin || + first.effective_roi.xmax != second.effective_roi.xmax) { + throw std::logic_error( + "Cannot combine group maps with different x extents"); + } + + // Calculate total number of output rows. + ssize_t total_rows = 0; + total_rows = first.map.shape(0) + second.map.shape(0) + + static_cast(gap_rows); + + // Allocate and initialize with -1. + // + // -1 represents an output strixel position that has + // no corresponding input pixel. + NDArray combined({total_rows, ncols}, -1); + + // Copy maps into the combined output. + auto copy_map = [&](auto const &source, ssize_t destination_row) { + const ssize_t nrows = source.map.shape(0); + + for (ssize_t row = 0; row < nrows; ++row) { + for (ssize_t col = 0; col < ncols; ++col) { + combined(destination_row + row, col) = source.map(row, col); + } + } + }; + + const ssize_t first_row = 0; + const ssize_t second_row = first.map.shape(0) + gap_rows; + + copy_map(first, first_row); + copy_map(second, second_row); + + // Bounding pixel ROI covered by the combined groups. + InclusiveROI effective_roi = first.effective_roi; + + effective_roi.xmin = + std::min(effective_roi.xmin, second.effective_roi.xmin); + effective_roi.xmax = + std::max(effective_roi.xmax, second.effective_roi.xmax); + effective_roi.ymin = + std::min(effective_roi.ymin, second.effective_roi.ymin); + effective_roi.ymax = + std::max(effective_roi.ymax, second.effective_roi.ymax); + + return {std::move(combined), effective_roi}; +} + +} // namespace detail + +/** + * @brief StrixeltoPixelMap for a JUNGFRAU single-chip multi-pitch iLGAD sensor + */ +class Jungfrau_iLGAD_StrixelPixelMap : public aare::remap::StrixelPixelMap<3> { + + public: + /** + * @brief Construct a new Jungfrau_iLGAD_StrixelPixelMap object. + * @param module_placement Placement and orientation of the sensor on the + * module. + * @param bond_shift Bonding shift applied before the configured sensor + * rotation. + */ + Jungfrau_iLGAD_StrixelPixelMap( + const defs::SensorModulePlacement &module_placement, + const defs::BondShift &bond_shift = {0, 0}) + : StrixelPixelMap<3>(config::jungfrau::SingleChipMP_iLGAD, + module_placement, bond_shift) {} +}; + +/** + * @brief StrixeltoPixelMap for a JUNGFRAU single-chip multi-pitch TEW sensor + */ +class Jungfrau_TEW_StrixelPixelMap : public aare::remap::StrixelPixelMap<3> { + + public: + /** + * @brief Construct a new Jungfrau_TEW_StrixelPixelMap object. + * @param module_placement Placement and orientation of the sensor on the + * module. + * @param bond_shift Bonding shift applied before the configured sensor + * rotation. + */ + Jungfrau_TEW_StrixelPixelMap( + const defs::SensorModulePlacement &module_placement, + const defs::BondShift &bond_shift = {0, 0}) + : StrixelPixelMap<3>(config::jungfrau::SingleChipMP_TEW, + module_placement, bond_shift) {} +}; + +/** + * @brief StrixeltoPixelMap for a JUNGFRAU single-chip quad iLGAD sensor + * @note the individual strixel groups are combined into a single map with a gap + * of invalid strixel rows in between. + */ +class Jungfrau_iLGAD_Quad_StrixelPixelMap + : public aare::remap::StrixelPixelMap<2, 1> { + + public: + /** + * @brief Construct a new Jungfrau_iLGAD_Quad_StrixelPixelMap object. + * @param bond_shift Bonding shift applied before the configured sensor + * rotation. + */ + Jungfrau_iLGAD_Quad_StrixelPixelMap(const defs::BondShift &bond_shift = {0, + 0}) + : StrixelPixelMap<2, 1>(config::jungfrau::Quad_iLGAD, + aare::remap::config::jungfrau::Quad, + bond_shift) {} + + /** + * @brief Calculate the strixel-to-pixel order maps for all strixel groups + * based on the user-specified ROI. + * @param user_roi User-specified ROI in the module's native coordinate + * system. + * @note This implementation combines the two strixel groups into a single + * map with a gap of invalid strixel rows in between. + */ + void calculate_map(const ROI &user_roi) override { + m_user_roi = toInclusiveROI(user_roi); + + auto quad_bottom_half = + strixel_to_pixel_map(m_sensorconfig.group_configs[0]); + auto quad_top_half = + strixel_to_pixel_map(m_sensorconfig.group_configs[1]); + + if (quad_bottom_half.map.size() == 0) { + m_group_maps[0] = quad_top_half; + return; + } else if (quad_top_half.map.size() == 0) { + m_group_maps[0] = quad_bottom_half; + return; + } else { + // TODO: good idea to combine? + m_group_maps[0] = detail::combine_group_maps( + quad_bottom_half, quad_top_half, + config::jungfrau::Quad_iLGAD_strixel_gap_rows); + } + } +}; + +} // namespace aare::remap diff --git a/include/aare/StrixelPixelRemapping/StrixelPixelRemapAlgorithm.hpp b/include/aare/StrixelPixelRemapping/StrixelPixelRemapAlgorithm.hpp deleted file mode 100644 index cdb7782c..00000000 --- a/include/aare/StrixelPixelRemapping/StrixelPixelRemapAlgorithm.hpp +++ /dev/null @@ -1,141 +0,0 @@ -#pragma once - -#include "aare/StrixelPixelRemapping/StrixelPixelRemapDefs.hpp" - -namespace aare::remap::algo { - -// Internal geometry helper -namespace detail { -/** - * @brief Apply physical transformations to a sensor-local ROI. - * - * IMPORTANT: - * Bond shifts are applied before rotation. - * The order is intentional because bond shifts are defined in the - * sensor's native coordinate system. - * - * This function is not intended to be exposed to the public - */ -InclusiveROI inline update_pixel_group_placement( - InclusiveROI roi, defs::SensorPixelGeometry const &pixel, - defs::BondShift bond_shift, defs::Rotation rot) { - // If there is a bond shift, translate the roi - if (bond_shift.x != 0 || bond_shift.y != 0) - roi = aare::inclusiveroi::geom::translate(roi, bond_shift.x, - bond_shift.y); - - // If there is a rotation given, mirror in X and Y (emulates a rotation) - if (rot == defs::Rotation::Rotate180) - roi = aare::inclusiveroi::geom::mirrorXY(roi, pixel.num_pix_x / 2, - pixel.num_pix_y / 2); - - return roi; -} -} // namespace detail - -/** - * @brief Build the strixel-to-pixel order map for one strixel group. - * The strixel mapping is determined by the group's multiplicity and - * modulo ordering. A reversed modulo ordering reverses the ordering - * within each multiplicity chunk; it does not reverse the complete - * strixel column ordering. - * - * @param group_config Configuration of the strixel group to be mapped. - * @param pixel Sensor pixel geometry to which the group - * is connected. - * @param placement Sensor placement and orientation on the module. - * @param roi_user User-specified ROI in the module's native coordinate system. - * @param bond_shift Physical bonding shift in x and y directions defined in the - * sensor's native coordinate system (before it is oriented on - * the module). - * @return A StrixelGroupToPixelMap describing the mapping from strixel - * coordinates to pixel indices in the user-provided ROI. - * - map(strixel_row,strixel_col) = pixel_index_in_user_roi - * - Invalid or unmapped strixel positions are initialized to -1. - * @throws std::logic_error For negative or zero strixel multiplicity. - * @throws std::logic_error If the group ROI width is not divisible by - * the strixel multiplicity. - */ -defs::StrixelGroupToPixelMap -strixel_to_pixel_map(defs::GroupConfig const &group_config, - defs::SensorPixelGeometry const &pixel, - defs::SensorModulePlacement const &placement, - InclusiveROI const &user_roi, - defs::BondShift bond_shift = {0, 0}); - -/** - * @brief Build the strixel-to-pixel order maps for all strixel groups in a - * sensor. - * @param sensor_config Configuration of the sensor, including all - * configurations of the strixel groups. - * @param placement Sensor placement and orientation on the module. - * @param roi_user User-specified ROI in the module's native coordinate system. - * @param bond_shift Bonding shift in x and y directions. - * @return A vector of StrixelGroupToPixelMap containing the mappings for all - * strixel groups. - */ -template -std::array -strixel_to_pixel_maps(defs::SensorConfig const &sensor_config, - defs::SensorModulePlacement const &placement, - InclusiveROI const &roi_user, - defs::BondShift bond_shift = {0, 0}) { - - std::array maps; - - for (size_t i = 0; i < N; ++i) { - maps[i] = strixel_to_pixel_map(sensor_config.group_configs[i], - sensor_config.pixel, placement, roi_user, - bond_shift); - } - - return maps; -} - -/** - * Public API: - * Applies a given remapping rule to an input array. - * - * \param input Original array - * \param order_map Rule for remapping (e.g. the output of a map generator) - * \param output Remapped array - */ -template -void ApplyRemap(NDView input, NDView order_map, - NDArray &output) { - - if (output.shape() != order_map.shape()) { - throw std::invalid_argument( - "ApplyRemap: output shape does not match order map shape"); - } - - const auto nrows = order_map.shape(0); - const auto ncols = order_map.shape(1); - - for (ssize_t row = 0; row < nrows; ++row) { - for (ssize_t col = 0; col < ncols; ++col) { - - auto flat_index = order_map(row, col); - - // Intentionally not-mapped pixel in order_map (e.g. guard ring - // pixels) - if (flat_index < 0) { - output(row, col) = T{}; - continue; - } - - // Corrupt map, must throw - if (static_cast(flat_index) >= input.size()) { - throw std::runtime_error( - "ApplyRemap: order map contains an invalid pixel index."); - } - - // Correctly mapped pixel - output(row, col) = input[flat_index]; - // Long version - // T const &value = input[flat_index]; - // output(row, col) = value; - } - } -} -} // namespace aare::remap::algo \ No newline at end of file diff --git a/include/aare/StrixelPixelRemapping/StrixelPixelRemapConfig.hpp b/include/aare/StrixelPixelRemapping/StrixelPixelRemapConfig.hpp index 093a85ed..2c5ba0ba 100644 --- a/include/aare/StrixelPixelRemapping/StrixelPixelRemapConfig.hpp +++ b/include/aare/StrixelPixelRemapping/StrixelPixelRemapConfig.hpp @@ -1,3 +1,4 @@ +#pragma once #include "aare/StrixelPixelRemapping/StrixelPixelRemapDefs.hpp" namespace aare::remap::config::jungfrau { diff --git a/include/aare/StrixelPixelRemapping/StrixelPixelRemapDefs.hpp b/include/aare/StrixelPixelRemapping/StrixelPixelRemapDefs.hpp index 6b83aab9..000ce9c5 100644 --- a/include/aare/StrixelPixelRemapping/StrixelPixelRemapDefs.hpp +++ b/include/aare/StrixelPixelRemapping/StrixelPixelRemapDefs.hpp @@ -166,7 +166,7 @@ struct StrixelGroupToPixelMap { * strixel group. Each value is a flattened index into the original * user-provided ROI. */ - NDArray map; + NDArray map{}; /** * @brief Effective pixel ROI covered by this map. @@ -176,7 +176,15 @@ struct StrixelGroupToPixelMap { * * The local coordinate system of `map` is aligned with this ROI. */ - InclusiveROI effective_roi; + InclusiveROI effective_roi{}; + + /** + * @brief Check if the map is empty + * @return true if the map is empty, false otherwise + */ + bool empty() const { + return map.size() == 0 && effective_roi == InclusiveROI::emptyROI(); + } }; } // namespace aare::remap::defs \ No newline at end of file diff --git a/include/aare/StrixelPixelRemapping/StrixelPixelRemapFormat.hpp b/include/aare/StrixelPixelRemapping/StrixelPixelRemapFormat.hpp index 262306e5..609511a0 100644 --- a/include/aare/StrixelPixelRemapping/StrixelPixelRemapFormat.hpp +++ b/include/aare/StrixelPixelRemapping/StrixelPixelRemapFormat.hpp @@ -3,9 +3,15 @@ #include "aare/StrixelPixelRemapping/StrixelPixelRemapDefs.hpp" namespace aare::remap::format { -// static inline std::string to_string(defs::Rotation); -// static inline std::string to_string(defs::SensorPixelGeometry const &g); -static inline std::string to_string(defs::GroupStrixelGeometry const &g); -static inline std::string to_string(defs::GroupConfig const &c); -inline std::ostream &operator<<(std::ostream &os, defs::GroupConfig const &c); + +std::string to_string(defs::Rotation r); +std::string to_string(defs::ModuloOrdering mo); +std::string to_string(const defs::SensorPixelGeometry &g); +std::string to_string(const defs::GroupStrixelGeometry &g); +std::string to_string(const defs::GroupRouting &r); +std::string to_string(const InclusiveROI &roi); +std::string to_string(const defs::GroupConfig &c); +std::string to_string(const defs::SensorModulePlacement &p); +std::ostream &operator<<(std::ostream &os, const defs::GroupConfig &c); + } // namespace aare::remap::format \ No newline at end of file diff --git a/include/aare/StrixelPixelRemapping/StrixelPixelRemapGenerate.hpp b/include/aare/StrixelPixelRemapping/StrixelPixelRemapGenerate.hpp deleted file mode 100644 index f584a23b..00000000 --- a/include/aare/StrixelPixelRemapping/StrixelPixelRemapGenerate.hpp +++ /dev/null @@ -1,446 +0,0 @@ -#pragma once - -#include "aare/StrixelPixelRemapping/StrixelPixelRemapAlgorithm.hpp" -#include "aare/StrixelPixelRemapping/StrixelPixelRemapConfig.hpp" - -namespace aare::remap::generate { - -// ============================================================ -// Internal implementation details -// ============================================================ -// Current quick-and-dirty inclusion of a helper function to combine group maps. -// This becomes necessary for sensors like Quad_iLGAD that combine two -// independent mapping regions separated by invalid gap rows. (Used to live as -// `combine_maps` in RemapAlgorithm, but arguably should not be part of that -// public API). Eventually, one could instead split RemapGenerate into hpp and -// cpp and make this one live in an unnamed namespace inside the cpp so that it -// cannot be exposed to the user through the public API (if we want to avoid -// that) -namespace detail { -/** - * @brief Combine TWO vertically ordered group remapping maps. - * - * Concatenates the maps in the order provided, inserting `gap_rows` - * rows of invalid entries (-1) between consecutive groups. - * - * The input maps must be ordered in the desired output order. - * Both maps must have the same number of columns. - * - * The returned effective ROI is the bounding ROI covering the - * effective ROIs of both input groups. It describes the physical - * source-pixel region and does not encode the artificial strixel - * gap rows in the output map. - * - * @param first Group map that comes first in output space. - * @param second Group map that comes second in output space. - * @param gap_rows Number of invalid strixel rows inserted between groups. - * - * @return Combined strixel-to-pixel map. - * - * @throws std::logic_error if group maps have different widths. - */ -defs::StrixelGroupToPixelMap -combine_group_maps(defs::StrixelGroupToPixelMap const &first, - defs::StrixelGroupToPixelMap const &second, - size_t gap_rows) { - - const ssize_t ncols = first.map.shape(1); - - // Make sure both maps have the same width. - if (second.map.shape(1) != ncols) { - throw std::logic_error("Cannot combine maps with different numbers " - "of columns"); - } - - // Check effective ROIs line up - if (first.effective_roi.xmin != second.effective_roi.xmin || - first.effective_roi.xmax != second.effective_roi.xmax) { - throw std::logic_error( - "Cannot combine group maps with different x extents"); - } - - // Calculate total number of output rows. - ssize_t total_rows = 0; - total_rows = first.map.shape(0) + second.map.shape(0) + - static_cast(gap_rows); - - // Allocate and initialize with -1. - // - // -1 represents an output strixel position that has - // no corresponding input pixel. - NDArray combined({total_rows, ncols}, -1); - - // Copy maps into the combined output. - auto copy_map = [&](auto const &source, ssize_t destination_row) { - const ssize_t nrows = source.map.shape(0); - - for (ssize_t row = 0; row < nrows; ++row) { - for (ssize_t col = 0; col < ncols; ++col) { - combined(destination_row + row, col) = source.map(row, col); - } - } - }; - - const ssize_t first_row = 0; - const ssize_t second_row = first.map.shape(0) + gap_rows; - - copy_map(first, first_row); - copy_map(second, second_row); - - // Bounding pixel ROI covered by the combined groups. - InclusiveROI effective_roi = first.effective_roi; - - effective_roi.xmin = - std::min(effective_roi.xmin, second.effective_roi.xmin); - effective_roi.xmax = - std::max(effective_roi.xmax, second.effective_roi.xmax); - effective_roi.ymin = - std::min(effective_roi.ymin, second.effective_roi.ymin); - effective_roi.ymax = - std::max(effective_roi.ymax, second.effective_roi.ymax); - - return {.map = std::move(combined), .effective_roi = effective_roi}; -} -} // namespace detail - -// ============================================================ -// Public generators -// ============================================================ - -/************************************ - * Single chip, multi-pitch, iLGAD - * - * Individual groups: - * - jungfrau_ilgad_singlechip_25um_strixel_map - * - jungfrau_ilgad_singlechip_15um_strixel_map - * - jungfrau_ilgad_singlechip_18um_strixel_map - * - * All in one vector: - * - jungfrau_ilgad_singlechip_multipitch_strixel_maps - ************************************/ - -/** - * @brief Generate a strixel-to-pixel remapping map for the 25 um strixel pitch - * region on a JUNGFRAU single-chip multi-pitch iLGAD sensor. - * - * The returned map converts pixels from the user-specified ASIC readout ROI - * into the corresponding strixel coordinate system. - * - * @param rx_roi - * ROI in the user/input ASIC coordinate system. - * - * @param placement - * Placement and orientation of the sensor on the module. - * - * @param bs - * Bonding shift applied before the configured sensor placement rotation. - * - * @return - * A strixel-to-pixel remapping map with flattened indices - * into the user-provided ROI. - */ -inline defs::StrixelGroupToPixelMap jungfrau_ilgad_singlechip_25um_strixel_map( - InclusiveROI rx_roi, defs::SensorModulePlacement placement, - defs::BondShift bs = {0, 0}) { - return algo::strixel_to_pixel_map(config::jungfrau::SingleChipMP_iLGAD_P25, - config::jungfrau::SingleChipMP_iLGAD_pix, - placement, rx_roi, bs); -} - -/** - * @brief Generate a strixel-to-pixel remapping map for the 15 um strixel pitch - * region on a JUNGFRAU single-chip multi-pitch iLGAD sensor. - * - * The returned map converts pixels from the user-specified ASIC readout ROI - * into the corresponding strixel coordinate system. - * - * @param rx_roi - * ROI in the user/input ASIC coordinate system. - * - * @param placement - * Placement and orientation of the sensor on the module. - * - * @param bs - * Bonding shift applied before the configured sensor placement rotation. - * - * @return - * A strixel-to-pixel remapping map with flattened indices - * into the user-provided ROI. - */ -inline defs::StrixelGroupToPixelMap jungfrau_ilgad_singlechip_15um_strixel_map( - InclusiveROI rx_roi, defs::SensorModulePlacement placement, - defs::BondShift bs = {0, 0}) { - return algo::strixel_to_pixel_map(config::jungfrau::SingleChipMP_iLGAD_P15, - config::jungfrau::SingleChipMP_iLGAD_pix, - placement, rx_roi, bs); -}; - -/** - * @brief Generate a strixel-to-pixel remapping map for the 18.75 um strixel - * pitch region on a JUNGFRAU single-chip multi-pitch iLGAD sensor. - * - * The returned map converts pixels from the user-specified ASIC readout ROI - * into the corresponding strixel coordinate system. - * - * @param rx_roi - * ROI in the user/input ASIC coordinate system. - * - * @param placement - * Placement and orientation of the sensor on the module. - * - * @param bs - * Bonding shift applied before the configured sensor placement rotation. - * - * @return - * A strixel-to-pixel remapping map with flattened indices - * into the user-provided ROI. - */ -inline defs::StrixelGroupToPixelMap jungfrau_ilgad_singlechip_18um_strixel_map( - InclusiveROI rx_roi, defs::SensorModulePlacement placement, - defs::BondShift bs = {0, 0}) { - return algo::strixel_to_pixel_map(config::jungfrau::SingleChipMP_iLGAD_P18, - config::jungfrau::SingleChipMP_iLGAD_pix, - placement, rx_roi, bs); -}; - -/** - * @brief Generate all strixel-to-pixel maps for a JUNGFRAU single-chip - * multi-pitch iLGAD sensor using an explicit sensor placement. - * - * This overload allows the caller to specify an arbitrary sensor placement - * instead of selecting one of the predefined chip placements. - * - * @param rx_roi - * ROI in the user/input ASIC coordinate system. - * - * @param placement - * Placement and orientation of the sensor on the module. - * - * @param bs - * Bonding shift applied before the configured sensor rotation. - * - * @return - * One remapping map per configured strixel group. - */ -inline auto jungfrau_ilgad_singlechip_multipitch_strixel_maps( - InclusiveROI rx_roi, defs::SensorModulePlacement placement, - defs::BondShift bs = {0, 0}) { - - return algo::strixel_to_pixel_maps(config::jungfrau::SingleChipMP_iLGAD, - placement, rx_roi, bs); -}; - -/************************************ - * Single chip, multi-pitch, TEW - * - * * Individual groups: - * - jungfrau_tew_singlechip_25um_strixel_map - * - jungfrau_tew_singlechip_15um_strixel_map - * - jungfrau_tew_singlechip_18um_strixel_map - * - * All in one vector: - * - jungfrau_tew_singlechip_multipitch_strixel_maps - ************************************/ - -/** - * @brief Generate a strixel-to-pixel map for the 25 um strixel pitch - * region on a JUNGFRAU single-chip multi-pitch TEW sensor. - * - * @param rx_roi - * ROI in the user/input ASIC coordinate system. - * @param placement - * Placement and orientation of the sensor on the module. - * @param bs - * Bonding shift applied before the configured sensor rotation. - * - * @return - * Strixel-to-pixel remapping map with indices into @p rx_roi. - */ -inline defs::StrixelGroupToPixelMap -jungfrau_tew_singlechip_25um_strixel_map(InclusiveROI rx_roi, - defs::SensorModulePlacement placement, - defs::BondShift bs = {0, 0}) { - return algo::strixel_to_pixel_map(config::jungfrau::SingleChipMP_TEW_P25, - config::jungfrau::SingleChipMP_TEW_pix, - placement, rx_roi, bs); -} - -/** - * @brief Generate a strixel-to-pixel map for the 15 um strixel pitch - * region on a JUNGFRAU single-chip multi-pitch TEW sensor. - */ -inline defs::StrixelGroupToPixelMap -jungfrau_tew_singlechip_15um_strixel_map(InclusiveROI rx_roi, - defs::SensorModulePlacement placement, - defs::BondShift bs = {0, 0}) { - return algo::strixel_to_pixel_map(config::jungfrau::SingleChipMP_TEW_P15, - config::jungfrau::SingleChipMP_TEW_pix, - placement, rx_roi, bs); -}; - -/** - * @brief Generate a strixel-to-pixel map for the 18.75 um strixel pitch - * region on a JUNGFRAU single-chip multi-pitch TEW sensor. - */ -inline defs::StrixelGroupToPixelMap -jungfrau_tew_singlechip_18um_strixel_map(InclusiveROI rx_roi, - defs::SensorModulePlacement placement, - defs::BondShift bs = {0, 0}) { - return algo::strixel_to_pixel_map(config::jungfrau::SingleChipMP_TEW_P18, - config::jungfrau::SingleChipMP_TEW_pix, - placement, rx_roi, bs); -}; - -/** - * @brief Generate all strixel-to-pixel maps for a JUNGFRAU single-chip - * multi-pitch TEW sensor using an explicit sensor placement. - * - * @param rx_roi - * ROI in the user/input ASIC coordinate system. - * - * @param placement - * Placement and orientation of the sensor on the module. - * - * @param bs - * Bonding shift applied before the configured sensor rotation. - * - * @return - * One remapping map per configured strixel group. - */ -inline auto jungfrau_tew_singlechip_multipitch_strixel_maps( - InclusiveROI rx_roi, defs::SensorModulePlacement placement, - defs::BondShift bs = {0, 0}) { - - return algo::strixel_to_pixel_maps(config::jungfrau::SingleChipMP_TEW, - placement, rx_roi, bs); -}; - -/************************************ - * Quad, 25 um, iLGAD - * - * Individual halves: - * - jungfrau_ilgad_quadbottom_25um_strixel_map - * - jungfrau_ilgad_quadtop_25um_strixel_map - * - * Vector with both halves: - * - jungfrau_ilgad_quad_25um_strixel_maps - * - * Complete, combined map of full sensor: - * - jungfrau_ilgad_quad_25um_strixel_map - * - * NOTE: In principle, we could hide or remove the individual halves and hide - * the vector version, only exposing the complete sensor map generator. - ************************************/ - -/** - * @brief Generate the remapping map for the bottom half of a JUNGFRAU - * 25 um iLGAD quad sensor. - * - * The returned map represents only the bottom strixel group of the quad - * sensor. It does not include the top half or the central strixel gap. - * - * @param rx_roi - * ROI in the user/input ASIC coordinate system. - * - * @param placement - * Placement and orientation of the quad sensor on the module. - * - * @return - * Strixel-to-pixel remapping map for the bottom sensor half. - */ -inline defs::StrixelGroupToPixelMap jungfrau_ilgad_quadbottom_25um_strixel_map( - InclusiveROI rx_roi, defs::SensorModulePlacement placement, - defs::BondShift bs = {0, 0}) { - return algo::strixel_to_pixel_map(config::jungfrau::Quad_iLGAD_bottomhalf, - config::jungfrau::Quad_iLGAD_pix, - placement, rx_roi, bs); -} - -/** - * @brief Generate the remapping map for the top half of a JUNGFRAU - * 25 um iLGAD quad sensor. - * - * The returned map represents only the top strixel group of the quad - * sensor. It does not include the bottom half or the central strixel gap. - * - * @param rx_roi - * ROI in the user/input ASIC coordinate system. - * - * @param placement - * Placement and orientation of the quad sensor on the module. - * - * @return - * Strixel-to-pixel remapping map for the top sensor half. - */ -inline defs::StrixelGroupToPixelMap -jungfrau_ilgad_quadtop_25um_strixel_map(InclusiveROI rx_roi, - defs::SensorModulePlacement placement, - defs::BondShift bs = {0, 0}) { - return algo::strixel_to_pixel_map(config::jungfrau::Quad_iLGAD_tophalf, - config::jungfrau::Quad_iLGAD_pix, - placement, rx_roi, bs); -} - -/** - * @brief Generate the individual strixel-to-pixel maps for a JUNGFRAU - * 25 um iLGAD quad sensor. - * - * The returned vector contains one map for each of the two sensor halves: - * - * - bottom half - * - top half - * - * The maps remain separate and do not contain the central strixel gap. - * Use jungfrau_ilgad_quad_25um_strixel_map() to obtain a single combined - * map including the configured gap. - * - * @param rx_roi - * ROI in the user/input ASIC coordinate system. - * - * @param placement - * Placement and orientation of the quad sensor on the module. - * - * @return - * Two strixel-group remapping maps, ordered according to the quad - * sensor configuration. - */ -inline auto -jungfrau_ilgad_quad_25um_strixel_maps(InclusiveROI rx_roi, - defs::SensorModulePlacement placement, - defs::BondShift bs = {0, 0}) { - - return algo::strixel_to_pixel_maps(config::jungfrau::Quad_iLGAD, placement, - rx_roi, bs); -} - -/** - * @brief Generate the combined strixel-to-pixel map for a JUNGFRAU - * 25 um iLGAD quad sensor. - * - * The two sensor halves with separate mapping rules are combined into one - * strixel coordinate system. The configured central gap between the two halves - * is represented by invalid entries in the corresponding gap rows of the - * returned map. - * - * @param rx_roi - * ROI in the user/input ASIC coordinate system. - * - * @param placement - * Placement and orientation of the quad sensor on the module. - * - * @param bs - * Bonding shift applied before the configured sensor rotation. - * - * @return - * A single combined strixel-to-pixel remapping map representing - * both sensor halves and the central strixel gap. - */ -inline defs::StrixelGroupToPixelMap -jungfrau_ilgad_quad_25um_strixel_map(InclusiveROI rx_roi, - defs::SensorModulePlacement placement, - defs::BondShift bs = {0, 0}) { - auto maps = jungfrau_ilgad_quad_25um_strixel_maps(rx_roi, placement, bs); - return detail::combine_group_maps( - maps[0], maps[1], config::jungfrau::Quad_iLGAD_strixel_gap_rows); -} -} // namespace aare::remap::generate diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt index 7f4db4c0..0e066695 100644 --- a/python/CMakeLists.txt +++ b/python/CMakeLists.txt @@ -47,6 +47,8 @@ set(PYTHON_FILES aare/RawFile.py aare/transform.py aare/ScanParameters.py + aare/strixelremap/__init__.py + aare/strixelremap/StrixelRemapFactories.py aare/utils.py) # Copy the python files to the build directory diff --git a/python/aare/RawFile.py b/python/aare/RawFile.py index 253fcc53..c769c063 100644 --- a/python/aare/RawFile.py +++ b/python/aare/RawFile.py @@ -4,10 +4,26 @@ import numpy as np from .ScanParameters import ScanParameters class RawFile(_aare.RawFile): - def __init__(self, fname, chunk_size = 1): + """ + Class to read Raw files produced by slsDetectorPackage + + Parameters: + fname (str): Path to the master file. + chunk_size (int, optional): Number of frames to read at a time. Defaults to 1. + strixeltransform (list, optional): List of strixel transform objects. Defaults to None. + """ + def __init__(self, fname, chunk_size = 1, strixeltransform : list = None): super().__init__(fname) self._chunk_size = chunk_size - + self._strixeltransform : list = strixeltransform + if self._strixeltransform is not None: + if(chunk_size != 1): + raise ValueError(f"RawFile with strixeltransform must have chunk_size 1, given {chunk_size}") + rois = super().master.rois + if(len(rois) != 1): + raise ValueError(f"RawFile with strixeltransform must have exactly one ROI, found {len(rois)}") # TODO: for now only support one ROI + + [transform.calculate_map(rois[0]) for transform in self._strixeltransform] def read(self) -> tuple: """Read the entire file. @@ -19,6 +35,41 @@ class RawFile(_aare.RawFile): self.seek(0) return self.read_n(self.total_frames) + def read_frame(self, frame_index: int | None = None ) -> tuple: + """Read one frame from the file and then advance the file pointer. + + .. note:: + + Uses the position of the file pointer :py:meth:`~RawFile.tell` to determine + which frame to read unless frame_index is specified. + + Args: + frame_index (int): If not None, seek to this frame before reading. + + Returns: + tuple: header, data + if strixeltransform is None + + tuple: header, list[list] + if strixeltransform is not None, where the list contains for each strixeltransform a list of transformed data for each strixel group. + + Raises: + RuntimeError: If the file is at the end. + """ + if frame_index is not None: + self.seek(frame_index) + + + header, data = super().read_frame() + if header.shape == (1,): + header = header[0] + + if self._strixeltransform: + res = [transform(data) for transform in self._strixeltransform] + return header, res + else: + return header, data + @property def scan_parameters(self): """Return the scan parameters. @@ -57,7 +108,11 @@ class RawFile(_aare.RawFile): def __next__(self): try: if self._chunk_size == 1: - return self.read_frame() + if self._strixeltransform is not None: + header, frame = self.read_frame() + return header, [transform(frame) for transform in self._strixeltransform] + else: + return self.read_frame() else: return self.read_n(self._chunk_size) diff --git a/python/aare/__init__.py b/python/aare/__init__.py index ecd111c9..9830919d 100644 --- a/python/aare/__init__.py +++ b/python/aare/__init__.py @@ -70,3 +70,5 @@ from ._aare import ( PixelHistogram_u32, PixelHistogram_u64, ) + +from . import strixelremap diff --git a/python/aare/strixelremap/StrixelRemapFactories.py b/python/aare/strixelremap/StrixelRemapFactories.py new file mode 100644 index 00000000..e403d696 --- /dev/null +++ b/python/aare/strixelremap/StrixelRemapFactories.py @@ -0,0 +1,41 @@ +from .._aare import strixelremap + + +def SensorConfig(sensor_geometry, group_configs): + """ + Helper function to create a sensor configuration from a sensor geometry and a list of group configurations + + Args: + sensor_geometry (SensorPixelGeometry): The sensor geometry + group_configs (list[GroupConfig]): The list of group configurations + """ + + if(len(group_configs) == 0): + raise ValueError("group_configs must contain at least one group configuration") + + N = len(group_configs) + + if N > 4: + raise ValueError("sensor configs with more than 4 groups are not bound in Python") + + sensor_config_cls = getattr(strixelremap, f"SensorConfig_{N}PixelGroups") + return sensor_config_cls(sensor_geometry, group_configs) + + +def StrixelPixelMap(sensor_config , placement : strixelremap.SensorModulePlacement, bond_shift : strixelremap.BondShift = strixelremap.BondShift(0, 0)): + """ + Helper function to create a StrixelPixelMap from a SensorConfig + + Args: + sensor_config (SensorConfig): The sensor configuration + placement (SensorModulePlacement): The placement of the sensor on the module + bond_shift (BondShift, optional): The bond shift to apply to the sensor. Defaults to BondShift(0, 0). + Returns: + StrixelPixelMap: The strixel pixel map object for the given sensor configuration and placement + """ + + N = len(sensor_config.group_configs) + + sensor_config_cls = getattr(strixelremap, f"StrixelPixelMap_{N}Groups_{N}Maps") + + return sensor_config_cls(sensor_config, placement, bond_shift) diff --git a/python/aare/strixelremap/__init__.py b/python/aare/strixelremap/__init__.py new file mode 100644 index 00000000..dbaeacf1 --- /dev/null +++ b/python/aare/strixelremap/__init__.py @@ -0,0 +1,6 @@ +from .StrixelRemapFactories import SensorConfig + +from .StrixelRemapFactories import StrixelPixelMap + +from .._aare.strixelremap import * + diff --git a/python/benchmarks/remap_benchmark.py b/python/benchmarks/remap_benchmark.py new file mode 100644 index 00000000..a7dd0067 --- /dev/null +++ b/python/benchmarks/remap_benchmark.py @@ -0,0 +1,36 @@ + + + + +import numpy as np +import pyperf + +from aare import strixelremap + +def numpy_take(input, order_map, output): + """Remap input array to output array using order_map with numpy.take""" + not_mapped = order_map == -1 + np.take(input, order_map, out=output) + output[not_mapped] = 0 + +def benchmark_remap(): + + strixelpixelmap = strixelremap.jungfrau_ilgad_singlechip_25um_strixel_map(user_roi = strixelremap.Chip1.placement_on_module, placement = strixelremap.Chip1) + + order_map = strixelpixelmap.map + + user_roi_height = strixelremap.Chip1.placement_on_module.height + user_roi_width = strixelremap.Chip1.placement_on_module.width + + data = np.random.randint(0, 2**16, size=(user_roi_height, user_roi_width)).astype(np.uint16) + + output = np.empty(order_map.shape, dtype=data.dtype) + + runner = pyperf.Runner() + + runner.bench_func("apply_remap", strixelremap.apply_remap, data, order_map, output) + + runner.bench_func("numpy_take", numpy_take, data, order_map, output) + +if __name__ == "__main__": + benchmark_remap() diff --git a/python/src/StrixelRemap/bind_InclusiveROI.hpp b/python/src/StrixelRemap/bind_InclusiveROI.hpp new file mode 100644 index 00000000..0217ed0a --- /dev/null +++ b/python/src/StrixelRemap/bind_InclusiveROI.hpp @@ -0,0 +1,114 @@ +#include + +#include "aare/StrixelPixelRemapping/InclusiveROI.hpp" + +namespace py = pybind11; + +void define_InclusiveROI(py::module &m) { + py::class_(m, "InclusiveROI") + .def(py::init(), py::arg("xmin"), py::arg("xmax"), + py::arg("ymin"), py::arg("ymax")) + .def_readwrite("xmin", &aare::InclusiveROI::xmin, + "minimum x coordinate (inclusive)") + .def_readwrite("xmax", &aare::InclusiveROI::xmax, + "maximum x coordinate (inclusive)") + .def_readwrite("ymin", &aare::InclusiveROI::ymin, + "minimum y coordinate (inclusive)") + .def_readwrite("ymax", &aare::InclusiveROI::ymax, + "maximum y coordinate (inclusive)") + + .def_property_readonly("width", &aare::InclusiveROI::width, + "width of the ROI") + + .def_property_readonly("height", &aare::InclusiveROI::height, + "height of the ROI") + + .def_property_readonly("size", &aare::InclusiveROI::size, + "number of pixels in the ROI") + + .def("is_empty", &aare::InclusiveROI::is_empty, + "check if the ROI is empty") + + .def( + "contains", + [](const aare::InclusiveROI &self, int x, int y) { + return self.contains(x, y); + }, + R"( + check if a point is contained in the ROI + + Parameters + ---------- + x : int + x coordinate of the point + y : int + y coordinate of the point + + Returns + ------- + bool + True if the point is contained in the ROI, False otherwise + )") + + .def( + "fits_in", + [](const aare::InclusiveROI &self, int ncols, int nrows) { + return self.fits_in(ncols, nrows); + }, + R"( + check if the ROI fits within a given number of columns and rows + Parameters + ---------- + ncols : int + number of columns + nrows : int + number of rows + Returns + ------- + bool + True if the ROI fits within the given dimensions, False otherwise + )") + + .def("__eq__", &aare::InclusiveROI::operator==, py::is_operator(), + "check if two InclusiveROI objects are equal") + + .def("__repr__", + [](const aare::InclusiveROI &self) { + return fmt::format( + "InclusiveROI(xmin={}, xmax={}, ymin={}, ymax={})", + self.xmin, self.xmax, self.ymin, self.ymax); + }) + + .def_static("emptyROI", &aare::InclusiveROI::emptyROI, + "create an empty InclusiveROI"); + + m.def("toInclusiveROI", &toInclusiveROI, py::arg("roi").noconvert(), + R"( + Convert a half-open ROI to an inclusive ROI + + Parameters + ---------- + roi : ROI + Half-open ROI to be converted + + Returns + ------- + InclusiveROI + Inclusive ROI with the same physical extent + )"); + + m.def("toHalfopenROI", &toHalfopenROI, py::arg("roi").noconvert(), + R"( + Convert an inclusive ROI to a half-open ROI + + Parameters + ---------- + roi : InclusiveROI + Inclusive ROI to be converted + + Returns + ------- + ROI + Half-open ROI with the same physical extent + )"); +} \ No newline at end of file diff --git a/python/src/StrixelRemap/bind_PredefinedVariables.hpp b/python/src/StrixelRemap/bind_PredefinedVariables.hpp new file mode 100644 index 00000000..347816a6 --- /dev/null +++ b/python/src/StrixelRemap/bind_PredefinedVariables.hpp @@ -0,0 +1,151 @@ +#include + +#include "aare/StrixelPixelRemapping/StrixelPixelMaps.hpp" +#include "aare/StrixelPixelRemapping/StrixelPixelRemapConfig.hpp" + +namespace py = pybind11; + +void define_predefinedConfigs(py::module &m) { + + // Predefined strixel geometries + m.attr("StrxP25") = aare::remap::config::jungfrau::StrxP25; + // Strixel geometry for 25 µm pitch strixels on iLGAD sensors (multiplicity + // = 3) + + m.attr("StrxP15") = aare::remap::config::jungfrau::StrxP15; + //"Strixel geometry for 15 µm pitch strixels on iLGAD sensors " + //"(multiplicity = 5)"; + m.attr("StrxP18") = aare::remap::config::jungfrau::StrxP18; + //"Strixel geometry for 18 µm pitch strixels on iLGAD sensors " + //"(multiplicity = 4)"; + m.attr("StrxP37") = aare::remap::config::jungfrau::StrxP37; + //"Strixel geometry for 37 µm pitch strixels on iLGAD sensors " + //"(multiplicity = 2)"; + + // Predefined sensor placements + m.attr("Chip1") = aare::remap::config::jungfrau::Chip1; + // Placement of the 2x2cm iLGAD sensor on the second chip (Chip1) of the + // Jungfrau module with no rotation applied. + + m.attr("Chip6") = aare::remap::config::jungfrau::Chip6; + //"Placement of the 2x2cm iLGAD sensor on the seventh chip (Chip6) of " + //"the Jungfrau module" + //"with a 180-degree rotation applied."; + m.attr("Quad") = aare::remap::config::jungfrau::Quad; + //"Placement of the 4x4cm iLGAD sensor on the quad " + //"(Chip1+Chip2+Chip5+Chip6) of the Jungfrau module" + //"with no rotation applied."; + + // Predefined sensor geometries + m.attr("SingleChipMP_iLGAD_pix") = + aare::remap::config::jungfrau::SingleChipMP_iLGAD_pix; + // "Pixel geometry of the 2x2 cm iLGAD sensor"; + m.attr("Quad_iLGAD_pix") = aare::remap::config::jungfrau::Quad_iLGAD_pix; + // "Pixel geometry of the 4x4 cm iLGAD sensor"; + m.attr("SingleChipMP_TEW_pix") = + aare::remap::config::jungfrau::SingleChipMP_TEW_pix; + // "Pixel geometry of the 2x2 cm TEW sensor"; + + // Predefined strixel groups + m.attr("SingleChipMP_iLGAD_P25") = + aare::remap::config::jungfrau::SingleChipMP_iLGAD_P25; + + // "Strixel group of 25 µm pitch strixels on the 2x2 cm iLGAD sensor"; + m.attr("SingleChipMP_iLGAD_P15") = + aare::remap::config::jungfrau::SingleChipMP_iLGAD_P15; + + // "Strixel group of 15 µm pitch strixels on the 2x2 cm iLGAD sensor"; + m.attr("SingleChipMP_iLGAD_P18") = + aare::remap::config::jungfrau::SingleChipMP_iLGAD_P18; + + // "Strixel group of 18.75 µm pitch strixels on the 2x2 cm iLGAD sensor"; + m.attr("SingleChipMP_TEW_P25") = + aare::remap::config::jungfrau::SingleChipMP_TEW_P25; + + // "Strixel group of 25 µm pitch strixels on the 2x2 cm TEW sensor"; + m.attr("SingleChipMP_TEW_P15") = + aare::remap::config::jungfrau::SingleChipMP_TEW_P15; + // "Strixel group of 15 µm pitch strixels on the 2x2 cm TEW sensor"; + m.attr("SingleChipMP_TEW_P18") = + aare::remap::config::jungfrau::SingleChipMP_TEW_P18; + // "Strixel group of 18.75 µm pitch strixels on the 2x2 cm TEW sensor"; + m.attr("Quad_iLGAD_bottomhalf") = + aare::remap::config::jungfrau::Quad_iLGAD_bottomhalf; + // "Strixel group of 25 µm pitch strixels located on the bottom half of " + // "4x4 cm iLGAD sensor"; + m.attr("Quad_iLGAD_tophalf") = + aare::remap::config::jungfrau::Quad_iLGAD_tophalf; + // "Strixel group of 25 µm pitch strixels located on the top half of 4x4 " + // "cm iLGAD sensor"; + + // Predefined sensor configurations + m.attr("SingleChipMP_iLGAD") = + aare::remap::config::jungfrau::SingleChipMP_iLGAD; + // "Sensor configuration of the 2x2 cm iLGAD sensor with all strixel + // groups"; + m.attr("SingleChipMP_TEW") = + aare::remap::config::jungfrau::SingleChipMP_TEW; + // "Sensor configuration of the 2x2 cm TEW sensor with all strixel groups"; + m.attr("Quad_iLGAD") = aare::remap::config::jungfrau::Quad_iLGAD; + // "Sensor configuration of the 4x4 cm iLGAD sensor with all strixel " + // "groups"; +} + +void define_predefinedStrixelPixelMaps(py::module &m) { + + py::class_>( + m, "Jungfrau_iLGAD_StrixelPixelMap") + .def(py::init(), + py::arg("module_placement").noconvert(), + py::arg("bond_shift").noconvert() = + aare::remap::defs::BondShift{0, 0}, + R"( + Construct a new Jungfrau_iLGAD_StrixelPixelMap object. + + Parameters + ---------- + module_placement : SensorModulePlacement + Placement and orientation of the sensor on the module. + bond_shift : BondShift, optional + Bonding shift applied before the configured sensor rotation. + Default is (0, 0). + )"); + + py::class_>( + m, "Jungfrau_TEW_StrixelPixelMap") + .def(py::init(), + py::arg("module_placement").noconvert(), + py::arg("bond_shift").noconvert() = + aare::remap::defs::BondShift{0, 0}, + R"( + Construct a new Jungfrau_TEW_StrixelPixelMap object. + + Parameters + ---------- + module_placement : SensorModulePlacement + Placement and orientation of the sensor on the module. + bond_shift : BondShift, optional + Bonding shift applied before the configured sensor rotation. + Default is (0, 0). + )"); + + py::class_>( + m, "Jungfrau_iLGAD_Quad_StrixelPixelMap") + .def(py::init(), + py::arg("bond_shift").noconvert() = + aare::remap::defs::BondShift{0, 0}, + R"( + Constructor for Jungfrau_iLGAD_Quad_StrixelPixelMap object. + + Parameters + ---------- + bond_shift : BondShift, optional + Bonding shift applied before the configured sensor rotation. + Default is (0, 0). + )"); +} diff --git a/python/src/StrixelRemap/bind_StrixelPixelMapDefs.hpp b/python/src/StrixelRemap/bind_StrixelPixelMapDefs.hpp new file mode 100644 index 00000000..3c33f28b --- /dev/null +++ b/python/src/StrixelRemap/bind_StrixelPixelMapDefs.hpp @@ -0,0 +1,182 @@ +#include + +#include "aare/StrixelPixelRemapping/StrixelPixelRemapDefs.hpp" +#include "aare/StrixelPixelRemapping/StrixelPixelRemapFormat.hpp" + +namespace py = pybind11; + +void define_PixelStrixelMapDefs(py::module &m) { + + py::enum_(m, "Rotation") + .value("Identity", aare::remap::defs::Rotation::Identity) + .value("Rotate180", aare::remap::defs::Rotation::Rotate180) + .export_values(); + + py::enum_(m, "ModuloOrdering") + .value("Forward", aare::remap::defs::ModuloOrdering::Forward) + .value("Reverse", aare::remap::defs::ModuloOrdering::Reverse) + .export_values(); + + py::class_(m, "Guardring") + .def(py::init(), py::arg("x"), py::arg("y")) + .def_readwrite("x", &aare::remap::defs::Guardring::x, + "ring width in pixels") + .def_readwrite("y", &aare::remap::defs::Guardring::y, + "ring height in pixels") + .def( + "__eq__", + [](const aare::remap::defs::Guardring &self, + const aare::remap::defs::Guardring &other) { + return self.x == other.x && self.y == other.y; + }, + py::is_operator()) + + .def("__repr__", [](const aare::remap::defs::Guardring &self) { + return fmt::format("Guardring{{x={}, y={}}}", self.x, self.y); + }); + + py::class_(m, "BondShift") + .def(py::init(), py::arg("x"), py::arg("y")) + .def_readwrite("x", &aare::remap::defs::BondShift::x, + "bond shift in x direction (pixels)") + .def_readwrite("y", &aare::remap::defs::BondShift::y, + "bond shift in y direction (pixels)") + .def("__repr__", [](const aare::remap::defs::BondShift &self) { + return fmt::format("BondShift{{x={}, y={}}}", self.x, self.y); + }); + + py::class_(m, "SensorPixelGeometry") + .def(py::init(), + py::arg("num_pix_x"), py::arg("num_pix_y"), + py::arg("guardring") = aare::remap::defs::Guardring{0, 0}) + .def_readwrite("num_pix_x", + &aare::remap::defs::SensorPixelGeometry::num_pix_x, + "number of pixels in x direction") + .def_readwrite("num_pix_y", + &aare::remap::defs::SensorPixelGeometry::num_pix_y, + "number of pixels in y direction") + .def_readwrite( + "guardring", &aare::remap::defs::SensorPixelGeometry::guardring, + "physical guardring around the sensor (default Guardring(0,0))") + + .def("__repr__", + [](const aare::remap::defs::SensorPixelGeometry &self) { + return fmt::format("SensorPixelGeometry{}", + aare::remap::format::to_string(self)); + }); + + py::class_(m, + "GroupStrixelGeometry") + .def(py::init(), py::arg("multiplicity"), + py::arg("pitch_um")) + .def_readwrite("multiplicity", + &aare::remap::defs::GroupStrixelGeometry::multiplicity, + "maximum number of pixels a strixel covers") + .def_readwrite("pitch_um", + &aare::remap::defs::GroupStrixelGeometry::pitch_um, + "effective minimal strixel pitch [µm]") + .def("__repr__", + [](const aare::remap::defs::GroupStrixelGeometry &self) { + return fmt::format("GroupStrixelGeometry{}", + aare::remap::format::to_string(self)); + }); + + py::class_(m, "GroupRouting") + .def(py::init(), + py::arg("mod_order") = aare::remap::defs::ModuloOrdering::Forward) + .def_readwrite( + "mod_order", &aare::remap::defs::GroupRouting::mod_order, + "modulo ordering of pixels within each strixel multiplicity group " + "default(ModuloOrdering::Forward)") + + .def("__repr__", [](const aare::remap::defs::GroupRouting &self) { + return fmt::format("GroupRouting{}", + aare::remap::format::to_string(self)); + }); + + py::class_(m, "GroupConfig") + .def(py::init(), + py::arg("strixel"), py::arg("routing"), + py::arg("placement_on_sensor")) + + .def(py::init([](const aare::remap::defs::GroupStrixelGeometry &strixel, + const aare::remap::defs::ModuloOrdering &mod_order, + const aare::InclusiveROI &placement_on_sensor) { + return aare::remap::defs::GroupConfig{ + strixel, {mod_order}, placement_on_sensor}; + }), + py::arg("strixel"), py::arg("routing"), + py::arg("placement_on_sensor")) + .def_readwrite("strixel", &aare::remap::defs::GroupConfig::strixel, + "strixel geometry of the group") + .def_readwrite("routing", &aare::remap::defs::GroupConfig::routing, + "pixel-to-strixel routing pattern") + .def_readwrite( + "placement_on_sensor", + &aare::remap::defs::GroupConfig::placement_on_sensor, + "placement of the strixel group on the sensor (sensor-local " + "coordinates)") + + .def("__repr__", [](const aare::remap::defs::GroupConfig &self) { + return fmt::format("GroupConfig{}", + aare::remap::format::to_string(self)); + }); + + py::class_( + m, "SensorModulePlacement") + .def(py::init(), + py::arg("placement_on_module"), py::arg("rotation")) + .def_readwrite( + "placement_on_module", + &aare::remap::defs::SensorModulePlacement::placement_on_module, + "sensor bounds in module coordinates") + .def_readwrite( + "rotation", &aare::remap::defs::SensorModulePlacement::rotation, + "physical orientation of the mounted sensor-ASIC assembly with " + "respect to the module reference frame") + + .def("__repr__", + [](const aare::remap::defs::SensorModulePlacement &self) { + return fmt::format("SensorModulePlacement{}", + aare::remap::format::to_string(self)); + }); + + py::class_( + m, "StrixelGroupToPixelMap") + .def(py::init<>()) + .def_readonly("effective_roi", + &aare::remap::defs::StrixelGroupToPixelMap::effective_roi, + "effective pixel ROI covered by this map (InclusiveROI)") + + .def_property_readonly( + "map", + [](const aare::remap::defs::StrixelGroupToPixelMap &self) + -> py::array { + return py::array_t( + self.map.shape(), self.map.data(), + py::cast(&self, py::return_value_policy::reference)); + }) + + .def("empty", &aare::remap::defs::StrixelGroupToPixelMap::empty, R"( + Check if the map is empty. + + Returns + ------- + bool + True if the map is empty, False otherwise. + )"); +} + +template void define_SensorConfig(py::module &m) { + const auto class_name = fmt::format("SensorConfig_{}PixelGroups", N); + py::class_>(m, class_name.c_str()) + .def(py::init>(), + py::arg("pixel"), py::arg("group_configs")) + .def_readwrite("pixel", &aare::remap::defs::SensorConfig::pixel, + "sensor pixel geometry (SensorPixelGeometry)") + .def_readwrite("group_configs", + &aare::remap::defs::SensorConfig::group_configs, + "[list] of strixel group configurations"); +} \ No newline at end of file diff --git a/python/src/StrixelRemap/bind_StrixelRemap.hpp b/python/src/StrixelRemap/bind_StrixelRemap.hpp new file mode 100644 index 00000000..e7de46d4 --- /dev/null +++ b/python/src/StrixelRemap/bind_StrixelRemap.hpp @@ -0,0 +1,214 @@ + +#include + +#include "aare/StrixelPixelRemapping/BaseStrixelPixelMap.hpp" + +namespace aare::remap::detail { + +struct StrixelPixelMapBindingAccess { + template + static void apply_group_remap(const StrixelPixelMap &map, + NDView input, NDView output, + const NDView order_map) { + map.apply_group_remap(input, output, order_map); + } +}; +} // namespace aare::remap::detail + +template +void define_StrixelPixelRemaps(py::module &m) { + + const auto class_name = + fmt::format("StrixelPixelMap_{}Groups_{}Maps", N, M); + py::class_>(m, class_name.c_str()) + .def(py::init &, + const aare::remap::defs::SensorModulePlacement &, + const aare::remap::defs::BondShift &>(), + py::arg("sensor_config"), py::arg("module_placement"), + py::arg("bond_shift") = aare::remap::defs::BondShift{0, 0}) + + .def("calculate_map", + &aare::remap::StrixelPixelMap::calculate_map, + py::arg("user_roi").noconvert(), + R"( + Calculate the strixel-to-pixel order maps for all strixel groups + based on the user-specified ROI. + + Parameters + ---------- + user_roi : InclusiveROI + User-specified ROI in the module's native coordinate system. + )") + + .def_property_readonly( + "group_maps", + [](const aare::remap::StrixelPixelMap &self) { + return self.get_group_maps(); + }, + R"( + Get the strixel-to-pixel order maps for all strixel groups. + + Returns + ------- + list of StrixelGroupToPixelMap + List of strixel-to-pixel order maps for each strixel group. + maps are empty if the groups are not covered by the user ROI. + )") + + .def( + "__call__", + [](aare::remap::StrixelPixelMap &self, + const aare::ROI &user_roi, + py::array_t + input) { + if (input.ndim() != 2) { + throw std::runtime_error("Input array must be 2D"); + } + + auto mapped_inputs = self(user_roi, make_view_2d(input)); + + py::list result_list; + for (auto mapped_input : mapped_inputs) { + auto *mapped_input_ptr = + new aare::NDArray(mapped_input); + + if (mapped_input_ptr->size() == 0) { + result_list.append(py::none()); + } else { + result_list.append(return_image_data(mapped_input_ptr)); + } + } + + return result_list; + }, + py::arg("user_roi").noconvert(), py::arg("input").noconvert(), + R"( + Apply the strixel-to-pixel remapping to an input array. + + Parameters + ---------- + user_roi : ROI + User-specified ROI in the module's native coordinate system. + input : NDView[uint16_t, 2] + Input array to be remapped. + + Returns + ------- + list of NDArray[uint16_t, 2] + Remapped arrays for each strixel group. + If a group is not covered by the user ROI, the corresponding array will be None. + )") + + .def( + "__call__", + [](const aare::remap::StrixelPixelMap &self, + py::array_t + input) { + if (input.ndim() != 2) { + throw std::runtime_error("Input array must be 2D"); + } + + auto mapped_inputs = self(make_view_2d(input)); + + py::list result_list; // TODO: can I reserve space for the list? + for (auto mapped_input : mapped_inputs) { + auto *mapped_input_ptr = + new aare::NDArray(mapped_input); + + if (mapped_input_ptr->size() == 0) { + result_list.append(py::none()); + } else { + result_list.append(return_image_data(mapped_input_ptr)); + } + } + + return result_list; + }, + py::arg("input").noconvert(), + R"( + Apply the strixel-to-pixel remapping to an input array. + This overload assumes that the user ROI has already been set and + the map calculated using `calculate_map()`. + + Parameters + ---------- + input : NDView[uint16_t, 2] + Input array to be remapped. + + Returns + ------- + list of NDArray[uint16_t, 2] + Remapped arrays for each strixel group. + If a group is not covered by the user ROI, the corresponding array will be None. + )") + + .def( + "__call__", + [](const aare::remap::StrixelPixelMap &self, + py::array_t input, + py::list &output) { + if (input.ndim() != 2) { + throw std::runtime_error("Input array must be 2D"); + } + + if (output.size() != M) { + throw std::runtime_error( + fmt::format("Output list size must be equal to the " + "number of strixel groups ({}), but got {}", + M, output.size())); + } + + const auto group_maps = self.get_group_maps(); + + const auto input_view = make_view_2d(input); + + for (size_t i = 0; i < group_maps.size(); ++i) { + if (group_maps[i].empty()) { + output[i] = py::none(); + } else { + + if (!py::isinstance>( + output[i])) { + throw std::runtime_error( + fmt::format("Output list element at index {} " + "is not a numpy array", + i)); + } + + auto out = py::array_t::ensure( + output[i]); + if (!out) { + throw std::runtime_error( + "Output entry must be a C-contiguous numpy " + "array"); + } + if (out.ndim() != 2) { + throw std::runtime_error("Output entry must be 2D"); + } + if (!out.writeable()) { + throw std::runtime_error( + "Output entry must be writable"); + } + + aare::remap::detail::StrixelPixelMapBindingAccess:: + apply_group_remap(self, input_view, + make_view_2d(out), + group_maps[i].map.view()); + } + } + }, + py::arg("input").noconvert(), py::arg("output").noconvert(), + R"( + Apply the strixel-to-pixel remapping to an input array. + This overload assumes that the user ROI has already been set and + the map calculated using `calculate_map()`. + + Parameters + ---------- + input : NDView[uint16_t, 2] + Input array to be remapped. + output : list of NDArray[uint16_t, 2] + Preallocated arrays to store the remapped results for each strixel group. + If a group is not covered by the user ROI, the corresponding output array will be None. + )"); +} diff --git a/python/src/bind_Defs.hpp b/python/src/bind_Defs.hpp index 2b25669b..ba12997e 100644 --- a/python/src/bind_Defs.hpp +++ b/python/src/bind_Defs.hpp @@ -37,6 +37,22 @@ void define_defs_bindings(py::module &m) { .def_readwrite("ymin", &ROI::ymin) .def_readwrite("ymax", &ROI::ymax) + .def("shape", + [](const ROI &self) { + return std::make_tuple(self.height(), self.width()); + }) + + .def( + "size", + [](const ROI &self) { return self.width() * self.height(); }, R"doc( + Calculate the size of the ROI. + + Returns + ------- + int + The total number of pixels in the ROI. + )doc") + .def( "slice", [](const ROI &self) { diff --git a/python/src/module.cpp b/python/src/module.cpp index 8e39d064..e4c9706e 100644 --- a/python/src/module.cpp +++ b/python/src/module.cpp @@ -23,6 +23,11 @@ #include "bind_RawFile.hpp" #include "bind_calibration.hpp" +#include "StrixelRemap/bind_InclusiveROI.hpp" +#include "StrixelRemap/bind_PredefinedVariables.hpp" +#include "StrixelRemap/bind_StrixelPixelMapDefs.hpp" +#include "StrixelRemap/bind_StrixelRemap.hpp" + // TODO! migrate the other names #include "ctb_raw_file.hpp" #include "file.hpp" @@ -63,6 +68,11 @@ double, 'f' for float) define_ClusterFileSink(m, "Cluster" #N "x" #M #TYPE_CODE); \ define_ClusterCollector(m, "Cluster" #N "x" #M #TYPE_CODE); +#define DEFINE_BINDINGS_SENSORCONFIG(N) define_SensorConfig(strixelremap); + +#define DEFINE_BINDINGS_STRIXELPIXELMAP(N, M) \ + define_StrixelPixelRemaps(strixelremap); + PYBIND11_MODULE(_aare, m) { auto experimental = m.def_submodule( "experimental", "Experimental APIs that may change without notice"); @@ -176,4 +186,22 @@ PYBIND11_MODULE(_aare, m) { define_eta(m, "d"); define_eta(m, "i"); define_eta(m, "i16"); + + auto strixelremap = + m.def_submodule("strixelremap", "Strixel remapping utilities."); + + define_InclusiveROI(strixelremap); + define_PixelStrixelMapDefs(strixelremap); + + DEFINE_BINDINGS_SENSORCONFIG(1); + DEFINE_BINDINGS_SENSORCONFIG(2); + DEFINE_BINDINGS_SENSORCONFIG(4); + DEFINE_BINDINGS_SENSORCONFIG(3); + DEFINE_BINDINGS_STRIXELPIXELMAP(1, 1); + DEFINE_BINDINGS_STRIXELPIXELMAP(2, 2); + DEFINE_BINDINGS_STRIXELPIXELMAP(4, 4); + DEFINE_BINDINGS_STRIXELPIXELMAP(3, 3); + DEFINE_BINDINGS_STRIXELPIXELMAP(2, 1); // quad iLGAD sensor + define_predefinedConfigs(strixelremap); + define_predefinedStrixelPixelMaps(strixelremap); } diff --git a/python/tests/test_RawFile.py b/python/tests/test_RawFile.py index 78636a9c..682db632 100644 --- a/python/tests/test_RawFile.py +++ b/python/tests/test_RawFile.py @@ -4,6 +4,9 @@ import json from aare import File, RawFile, RawSubFile, DetectorType, ROI, UDPPortPosition import numpy as np +from aare import strixelremap + +from test_helpers.RawFileHelpers import TemporaryJungfrauRawFiles @pytest.fixture def small_raw_file(tmp_path): @@ -384,3 +387,17 @@ def test_read_eiger_udp_port_disabled(test_data_path): assert len(rois) == 2 assert rois[0] == ROI(0, 512, 0, 512) assert rois[1] == ROI(1024, 1536, 0, 512) + +def test_RawFile_with_strixeltransform(): + """ list of transforms is passed to RawFile""" + transform = strixelremap.Jungfrau_iLGAD_StrixelPixelMap(module_placement = strixelremap.Chip1) + + my_raw_file = TemporaryJungfrauRawFiles() + + with RawFile(my_raw_file.master_path(), strixeltransform = [transform]) as f: + header, frames = f.read_frame() + assert len(frames) == 1 + assert len(frames[0]) == 3 + assert frames[0][0].shape == (165, 79) + assert frames[0][1].shape == (320, 47) + assert frames[0][2].shape == (476, 59) \ No newline at end of file diff --git a/python/tests/test_StrixelPixelRemapAPI.py b/python/tests/test_StrixelPixelRemapAPI.py new file mode 100644 index 00000000..3163ce0e --- /dev/null +++ b/python/tests/test_StrixelPixelRemapAPI.py @@ -0,0 +1,222 @@ +import pytest + +import numpy as np + +from aare import strixelremap +from aare import ROI + +def test_roimappings(): + """ Test exclusive to inclusive roi mapping and vice versa """ + + roi = ROI(0, 10, 0, 5) + + inclusive_roi = strixelremap.toInclusiveROI(roi) + + assert inclusive_roi.xmin == 0 + assert inclusive_roi.xmax == 9 + assert inclusive_roi.ymin == 0 + assert inclusive_roi.ymax == 4 + + exclusive_roi = strixelremap.toHalfopenROI(inclusive_roi) + + assert exclusive_roi.xmin == 0 + assert exclusive_roi.xmax == 10 + assert exclusive_roi.ymin == 0 + assert exclusive_roi.ymax == 5 + +def test_emptyROI(): + """ Test creation of an empty ROI """ + + empty_roi = strixelremap.InclusiveROI.emptyROI() + + assert empty_roi.xmin == 0 + assert empty_roi.xmax == -1 + assert empty_roi.ymin == 0 + assert empty_roi.ymax == -1 + + +def test_format(): + """ Test string representations """ + + inclusive_roi = strixelremap.InclusiveROI(0, 9, 0, 4) + + assert str(inclusive_roi) == "InclusiveROI(xmin=0, xmax=9, ymin=0, ymax=4)" + + jungfrau_pixel_geometry = strixelremap.SingleChipMP_TEW_pix + + assert str(jungfrau_pixel_geometry) == "SensorPixelGeometry{cols x rows: 256 x 256, guardring: {x = 0, y = 0}}" + + strixel_group = strixelremap.StrxP25 + + assert str(strixel_group) == "GroupStrixelGeometry{multiplicity: 3, pitch_um: 25}" + + jungfrau_group_config = strixelremap.SingleChipMP_TEW_P25 + + assert str(jungfrau_group_config) == "GroupConfig{strixel_group: {multiplicity: 3, pitch_um: 25}, routing: {Forward}, placement_on_sensor: {xmin=1, xmax=255, ymin=0, ymax=63}}" + + sensor_placement = strixelremap.Chip1 + + assert str(sensor_placement) == "SensorModulePlacement{placement_on_module: {xmin=256, xmax=511, ymin=0, ymax=255}, rotation: Identity}" + +def test_customSensorConfiguration(): + """ Test that a custom sensor configuration can be created and used to remap strixel pixels """ + + my_strixel_group = strixelremap.GroupStrixelGeometry(multiplicity=2, pitch_um=25.0) + + assert my_strixel_group.multiplicity == 2 + assert my_strixel_group.pitch_um == 25.0 + + my_sensor_geometry = strixelremap.SensorPixelGeometry(num_pix_x = 10, num_pix_y = 5) + + assert my_sensor_geometry.num_pix_x == 10 + assert my_sensor_geometry.num_pix_y == 5 + assert my_sensor_geometry.guardring == strixelremap.Guardring(0,0) + + my_group_config = strixelremap.GroupConfig(strixel = my_strixel_group, routing = strixelremap.ModuloOrdering.Forward,placement_on_sensor = strixelremap.InclusiveROI(0,9,0,4)) + + my_sensor_config = strixelremap.SensorConfig(sensor_geometry = my_sensor_geometry, group_configs = [my_group_config]) + + # rebase + user_roi = strixelremap.InclusiveROI(strixelremap.Chip1.placement_on_module.xmin + 0, strixelremap.Chip1.placement_on_module.xmin + 9, strixelremap.Chip1.placement_on_module.ymin + 0, strixelremap.Chip1.placement_on_module.ymin + 4) + + strixelpixelmap = strixelremap.StrixelPixelMap(sensor_config = my_sensor_config, placement = strixelremap.Chip1) + + strixelpixelmap.calculate_map(strixelremap.toHalfopenROI(user_roi)) + + group_maps = strixelpixelmap.group_maps + + assert len(group_maps) == 1 + + assert group_maps[0].effective_roi == my_group_config.placement_on_sensor + + map = group_maps[0].map + assert map.shape == (10 ,5) + + assert np.array_equal(map, np.array([[0, 2, 4, 6, 8], [1, 3, 5, 7, 9], [10, 12, 14, 16, 18], [11, 13, 15, 17, 19], [20, 22, 24, 26, 28], [21, 23, 25, 27, 29], [30, 32, 34, 36, 38], [31, 33, 35, 37, 39], [40, 42, 44, 46, 48], [41, 43, 45, 47, 49]])) + + input = np.arange(50).reshape((5,10)).astype(np.uint16) + + remapped_result = strixelpixelmap(input)[0] + + assert remapped_result.shape == (10,5) + + assert np.array_equal(remapped_result, np.array([[0, 2, 4, 6, 8], [1, 3, 5, 7, 9], [10, 12, 14, 16, 18], [11, 13, 15, 17, 19], [20, 22, 24, 26, 28], [21, 23, 25, 27, 29], [30, 32, 34, 36, 38], [31, 33, 35, 37, 39], [40, 42, 44, 46, 48], [41, 43, 45, 47, 49]])) + + # check output call operator with preallocated output array + output = np.empty(map.shape, dtype=input.dtype) + + strixelpixelmap(input, [output]) + + assert np.array_equal(output, np.array([[0, 2, 4, 6, 8], [1, 3, 5, 7, 9], [10, 12, 14, 16, 18], [11, 13, 15, 17, 19], [20, 22, 24, 26, 28], [21, 23, 25, 27, 29], [30, 32, 34, 36, 38], [31, 33, 35, 37, 39], [40, 42, 44, 46, 48], [41, 43, 45, 47, 49]])) + +def test_predefined_iLGAD_singlechip(): + """ Test predefined Junfrau iLGAD strixel pixel remap """ + + inclusive_user_roi = strixelremap.InclusiveROI(strixelremap.Chip1.placement_on_module.xmin + 11, strixelremap.Chip1.placement_on_module.xmin + 15, strixelremap.Chip1.placement_on_module.ymin + 10, strixelremap.Chip1.placement_on_module.ymin + 12) + + exclusive_user_roi = strixelremap.toHalfopenROI(inclusive_user_roi) + + strixelpixelmap = strixelremap.Jungfrau_iLGAD_StrixelPixelMap(module_placement = strixelremap.Chip1) + + strixelpixelmap.calculate_map(exclusive_user_roi) + + group_maps = strixelpixelmap.group_maps + + assert len(group_maps) == 3 + + assert group_maps[0].map.shape == (9, 2) + + assert group_maps[1].empty() == True + + assert group_maps[2].empty() == True + + group_map_0 = group_maps[0].map + + assert np.array_equal(group_map_0, np.array([[-1, 2], [0, 3], [1, 4], [-1, 7], [5,8], [6,9], [-1,12], [10,13], [11,14]])) + + input_data = np.array([[1,2,3,4,5],[1,2,3,4,5], [1,2,3,4,5]]).astype(np.uint16) + + output = strixelpixelmap(input_data) + + assert len(output) == 3 + + assert np.array_equal(output[0], np.array([[0, 3], [1,4], [2,5], [0, 3], [1,4], [2,5], [0, 3], [1,4], [2,5]])) + + assert output[1] == None + + assert output[2] == None + + +def test_predefined_iLGAD_quad_remap(): + """ Test predefined Junfrau iLGAD quad strixel pixel remap """ + + # TODO combine map with one empty + # only one map is not covered by ROI + inclusive_user_roi = strixelremap.InclusiveROI(strixelremap.Quad.placement_on_module.xmin + 11, strixelremap.Quad.placement_on_module.xmin + 15, strixelremap.Quad.placement_on_module.ymin + 9, strixelremap.Quad.placement_on_module.ymin + 11) + + exclusive_user_roi = strixelremap.toHalfopenROI(inclusive_user_roi) + + strixelpixelmap = strixelremap.Jungfrau_iLGAD_Quad_StrixelPixelMap() + + strixelpixelmap.calculate_map(exclusive_user_roi) + + group_maps = strixelpixelmap.group_maps + + assert len(group_maps) == 1 + + assert group_maps[0].map.shape == (9, 2) + + # both maps are covered by ROI + inclusive_user_roi = strixelremap.InclusiveROI(strixelremap.Quad.placement_on_module.xmin + 11, strixelremap.Quad.placement_on_module.xmin + 15, strixelremap.Quad.placement_on_module.ymin + 9, strixelremap.Quad.placement_on_module.ymin + 260) + + exclusive_user_roi = strixelremap.toHalfopenROI(inclusive_user_roi) + + strixelpixelmap = strixelremap.Jungfrau_iLGAD_Quad_StrixelPixelMap() + + strixelpixelmap.calculate_map(exclusive_user_roi) + + group_maps = strixelpixelmap.group_maps + + assert len(group_maps) == 1 + + # 3*246 rows top module + 3*4 rows bottom module + 12 gap rows = 762 rows, 2 columns + assert group_maps[0].map.shape == (762, 2) + + +def test_empty_map(): + """ Test empty map when ROI does not cover any strixel pixels """ + + exclusive_user_roi = ROI(0, 10, 0, 5) + + strixelpixelmap = strixelremap.Jungfrau_iLGAD_StrixelPixelMap(module_placement = strixelremap.Chip1) + + strixelpixelmap.calculate_map(exclusive_user_roi) + + group_maps = strixelpixelmap.group_maps + + assert len(group_maps) == 3 + + assert group_maps[0].empty() == True + + assert group_maps[1].empty() == True + + assert group_maps[2].empty() == True + + input_data = np.random.randint(0, 65535, size=exclusive_user_roi.shape(), dtype=np.uint16) + mapped_output = strixelpixelmap(input_data) + + assert len(mapped_output) == 3 + + assert mapped_output[0] == None + + assert mapped_output[1] == None + + assert mapped_output[2] == None + # check that the output call operator with preallocated output arrays works as expected + output_arrays = [np.random.randint(0, 65535, size=exclusive_user_roi.shape(), dtype=np.uint16) for _ in range(3)] + strixelpixelmap(input_data, output_arrays) + + assert output_arrays[0] == None + assert output_arrays[1] == None + assert output_arrays[2] == None + diff --git a/python/tests/test_helpers/RawFileHelpers.py b/python/tests/test_helpers/RawFileHelpers.py new file mode 100644 index 00000000..7309eff5 --- /dev/null +++ b/python/tests/test_helpers/RawFileHelpers.py @@ -0,0 +1,108 @@ +# SPDX-License-Identifier: MPL-2.0 + +import json +import shutil +import struct +import tempfile +import time +from pathlib import Path +from aare import ROI +import random + + +# TODO: maybe a fixture is better +def get_module_size_from_roi(module_idx : tuple, receiver_roi : ROI, pixels_per_module: tuple) -> tuple: + """Get the size of a module in pixels from the receiver ROI and the pixels per module. + + Args: + module_idx (tuple): The index of the module in the format (y, x). + receiver_roi (ROI): The receiver ROI. + pixels_per_module (tuple): The number of pixels per module in the x and y directions. + + Returns: + tuple: The size of the module in pixels in the x and y directions. + """ + module_x = module_idx[1] + module_y = module_idx[0] + + module_ROI = ROI( + module_x * pixels_per_module[1], (module_x + 1) * pixels_per_module[1], + module_y * pixels_per_module[0], (module_y + 1) * pixels_per_module[0] + ) + + # Calculate the size of the module in pixels + xmin = max(receiver_roi.xmin, module_ROI.xmin) + ymin = max(receiver_roi.ymin, module_ROI.ymin) + xmax = min(receiver_roi.xmax, module_ROI.xmax) + ymax = min(receiver_roi.ymax, module_ROI.ymax) + + + if xmin >= xmax or ymin >= ymax: + return (0, 0) + else: + module_size_x = xmax - xmin + module_size_y = ymax - ymin + return (module_size_y, module_size_x) + + +class TemporaryJungfrauRawFiles: + def __init__(self, modules: tuple = (2,1), pixels_per_module: tuple = (256,1024), receiver_roi : ROI = ROI(0, 1024, 0, 512)) -> None: + unique = time.monotonic_ns() + self._directory = Path(tempfile.gettempdir()) / f"aare-raw-{unique}" + self._directory.mkdir() + + image_size_in_bytes = receiver_roi.size() * 2 # 2 bytes per pixel + + metadata = { + "Version": 8.1, + "Detector Type": "Jungfrau", + "Timing Mode": "auto", + "Geometry": {"x": modules[1], "y": modules[0]}, + "Image Size": image_size_in_bytes, + "Pixels": {"x": pixels_per_module[1], "y": pixels_per_module[0]}, + "Max Frames Per File": 1, + "Total Frames": 2, + "Frames in File": 2, + "Frame Padding": 1, + "Frame Discard Policy": "nodiscard", + "UDP Ports Type" : ["bottom", "top"], + "UDP Ports Disabled": [], + "Receiver Rois": [{"xmin": receiver_roi.xmin, "xmax": receiver_roi.xmax-1, "ymin": receiver_roi.ymin, "ymax": receiver_roi.ymax-1}] # inclusive + } + self.master_path().write_text(json.dumps(metadata), encoding="utf-8") + + for module_x in range(metadata["Geometry"]["x"]): + for module_y in range(metadata["Geometry"]["y"]): + size = get_module_size_from_roi((module_y, module_x), receiver_roi, pixels_per_module) + if(size == (0, 0)): + continue + values = [random.randint(0, 65535) for _ in range(size[0] * size[1])] + pixels = struct.pack(f"<{len(values)}H", *values) + for file_index in range(metadata["Frames in File"]): + with self.data_path(module_x * metadata["Geometry"]["y"] + module_y, file_index).open("wb") as output: + output.write(self._detector_header_bytes(file_index)) + output.write(pixels) + + def cleanup(self) -> None: + shutil.rmtree(self._directory, ignore_errors=True) + + def __enter__(self) -> "TemporaryJungfrauRawFiles": + return self + + def __exit__(self, exc_type, exc, tb) -> None: + self.cleanup() + + def __del__(self) -> None: + self.cleanup() + + def master_path(self) -> Path: + return self._directory / "run_master_0.json" + + def data_path(self, module: int = 0, file: int = 0) -> Path: + return self._directory / f"run_d{module}_f{file}_0.raw" + + @staticmethod + def _detector_header_bytes(frame_number: int) -> bytes: + # TODO: Mirror include/aare/DetectorHeader.hpp exactly if the full + # binary layout is required by the test. + return struct.pack(" - -#include - -namespace aare::remap::algo { - -defs::StrixelGroupToPixelMap -strixel_to_pixel_map(defs::GroupConfig const &group_config, - defs::SensorPixelGeometry const &pixel, - defs::SensorModulePlacement const &placement, - InclusiveROI const &roi_user, defs::BondShift bond_shift) { - - const int multiplicity = group_config.strixel.multiplicity; - - // Defensive check to be sure misconfiguration is avoided - if (multiplicity <= 0) - throw std::logic_error("Strixel multiplicity must be positive"); - - // The group must contain an integer number of strixel columns. - const auto group_width = group_config.placement_on_sensor.width(); - - if (group_width % multiplicity != 0) - throw std::logic_error( - "Group ROI width must be divisible by strixel multiplicity"); - - // const int total_strixel_columns = group_width / multiplicity; - - // Determine the ordering of strixels within each multiplicity group. - std::vector mods(multiplicity); - std::iota(mods.begin(), mods.end(), 0); - - if (group_config.routing.mod_order == defs::ModuloOrdering::Reverse) - std::reverse(mods.begin(), mods.end()); - - // -- 1) Rebase the user ROI (rx_roi) into sensor-local coordinates - const InclusiveROI roi_user_local = - inclusiveroi::geom::rebaseROI(roi_user, placement.placement_on_module); - LOG(logDEBUG) - << "aare::remap::algo::strixel_to_pixel_map: Transformed user ROI: " - << roi_user_local << std::endl; - - LOG(logDEBUG) << "aare::remap::algo::strixel_to_pixel_map: Group ROI " - "before transformation (as in global config)" - << group_config.placement_on_sensor << '\n'; - - // -- 2) Apply the physical bond shift first, sensor rotation second. - const InclusiveROI roi_group = detail::update_pixel_group_placement( - group_config.placement_on_sensor, pixel, bond_shift, - placement.rotation); - - LOG(logDEBUG) << "aare::remap::algo::strixel_to_pixel_map: Group ROI after " - "transformation (as in local transformation) " - << roi_group << '\n'; - - // -- 3) Compute effective ROI = intersection( roi_user, roi_group ) - // Only pixels covered by both the user ROI and the transformed group - // contribute to this map. - const InclusiveROI effective_roi = - inclusiveroi::geom::intersect(roi_user_local, roi_group); - - // If ROIs don't intersect, return empty - if (effective_roi.xmax < effective_roi.xmin || - effective_roi.ymax < effective_roi.ymin) { - LOG(logWARNING) - << "User-supplied ROI does not intersect with configured " - "strixel ROI, returned map is empty!\n"; - return {{}, InclusiveROI::emptyROI()}; - } - - LOG(logDEBUG) << "aare::remap::algo::strixel_to_pixel_map: Result of " - "intersecting ROIs " - << effective_roi << '\n'; - - /****************************** - * Core of the algorithm - * - * Local lambda: - * Convert a sensor-local pixel coordinate into the corresponding - * local strixel coordinate. - * (Could be a separate function if preferred.) - ******************************/ - auto pixel_to_strixel = [&](int x, int y) { - const int dx = x - roi_group.xmin; - const int dy = y - roi_group.ymin; - - const int mod = dx % multiplicity; - const int col = dx / multiplicity; - const int row = dy * multiplicity + mods[mod]; - - return std::pair{row, col}; - }; - - //-- 4) Determine the range of strixel coordinates touched by the effective - // ROI. - // - // Since effective_roi is contained in roi_group: - // dx = x - roi_group.xmin >= 0 - // dy = y - roi_group.ymin >= 0 - // - // The strixel column is dx / multiplicity. - // Each pixel row maps onto a complete block of `multiplicity` - // strixel rows, regardless of the modulo ordering. - const int min_col = (effective_roi.xmin - roi_group.xmin) / multiplicity; - - const int max_col = (effective_roi.xmax - roi_group.xmin) / multiplicity; - - const int min_row = (effective_roi.ymin - roi_group.ymin) * multiplicity; - - // Catch the first row that is out of bounds (next multiplicity group) and - // calculate -1 - const int max_row = - (effective_roi.ymax - roi_group.ymin + 1) * multiplicity - 1; - - // Now from the found bounds of the strixel grid, we define the space to - // allocate for the order map - const int nrows = max_row - min_row + 1; - const int ncols = max_col - min_col + 1; - - // And allocate - aare::NDArray map({nrows, ncols}, -1); - - LOG(logDEBUG) - << "aare::remap::algo::strixel_to_pixel_map: Resulting strixel grid: (" - << map.shape(0) << ", " << map.shape(1) << ")" << '\n'; - - // -- 5) Populate the strixel-to-user-pixel map. - for (int y = effective_roi.ymin; y <= effective_roi.ymax; ++y) { - for (int x = effective_roi.xmin; x <= effective_roi.xmax; ++x) { - - auto [row, col] = pixel_to_strixel(x, y); - - const int map_col = col - min_col; - const int map_row = row - min_row; - - // index into !!!ORIGINAL USER ROI GRID!!! - const ssize_t user_pixel = - static_cast(y - roi_user_local.ymin) * - roi_user_local.width() + - (x - roi_user_local.xmin); - - map(map_row, map_col) = user_pixel; - } - } - - return {map, effective_roi}; -} - -} // namespace aare::remap::algo \ No newline at end of file diff --git a/src/StrixelPixelRemapping/StrixelPixelRemapAlgorithm.test.cpp b/src/StrixelPixelRemapping/StrixelPixelRemapAlgorithm.test.cpp index dfd9bd9e..87400ae4 100644 --- a/src/StrixelPixelRemapping/StrixelPixelRemapAlgorithm.test.cpp +++ b/src/StrixelPixelRemapping/StrixelPixelRemapAlgorithm.test.cpp @@ -1,4 +1,4 @@ -#include "aare/StrixelPixelRemapping/StrixelPixelRemapAlgorithm.hpp" +#include "aare/StrixelPixelRemapping/BaseStrixelPixelMap.hpp" #include // Test structure: @@ -103,7 +103,7 @@ TEST_CASE("update_pixel_group_placement: shift and rotation geometry", SECTION("Identity reproduces itself") { - auto updated = algo::detail::update_pixel_group_placement( + auto updated = ::detail::update_pixel_group_placement( group_roi, sensor, {0, 0}, defs::Rotation::Identity); CHECK(updated == group_roi); @@ -112,7 +112,7 @@ TEST_CASE("update_pixel_group_placement: shift and rotation geometry", SECTION("Bond shift") { const defs::BondShift shift{2, 3}; - auto updated = algo::detail::update_pixel_group_placement( + auto updated = detail::update_pixel_group_placement( group_roi, sensor, shift, defs::Rotation::Identity); // Original group: {5, 13, 8, 16} @@ -125,7 +125,7 @@ TEST_CASE("update_pixel_group_placement: shift and rotation geometry", SECTION("180 degree rotation") { - auto updated = algo::detail::update_pixel_group_placement( + auto updated = detail::update_pixel_group_placement( group_roi, sensor, {0, 0}, defs::Rotation::Rotate180); // Original group: {5, 13, 8, 16} @@ -137,7 +137,7 @@ TEST_CASE("update_pixel_group_placement: shift and rotation geometry", } SECTION("bond shift and rotation") { - const auto result = algo::detail::update_pixel_group_placement( + const auto result = detail::update_pixel_group_placement( group_roi, sensor, {2, 3}, defs::Rotation::Rotate180); // Original group: {5, 13, 8, 16} @@ -166,10 +166,13 @@ TEST_CASE("strixel_to_pixel_map: explicit mapping test with small ROI", const auto placement = test_placement(); // User ROI is in module coordinates, aligns with sensor - const InclusiveROI user_roi{50, 99, 50, 99}; + const ROI user_roi{50, 100, 50, 100}; - const auto result = - algo::strixel_to_pixel_map(group, sensor, placement, user_roi, {0, 0}); + StrixelPixelMap map(SensorConfig<1>{sensor, {group}}, placement, {0, 0}); + + map.calculate_map(user_roi); + + const auto result = map.get_group_maps()[0]; const InclusiveROI expected_roi{10, 15, 10, 11}; @@ -204,10 +207,13 @@ TEST_CASE("strixel_to_pixel_map: user ROI exactly aligned with group ROI", const auto placement = test_placement(); // User ROI is in module coordinates. - const InclusiveROI user_roi{60, 89, 60, 89}; + const ROI user_roi{60, 90, 60, 90}; - const auto result = - algo::strixel_to_pixel_map(group, sensor, placement, user_roi, {0, 0}); + StrixelPixelMap map(SensorConfig<1>{sensor, {group}}, placement, {0, 0}); + + map.calculate_map(user_roi); + + const auto result = map.get_group_maps()[0]; // ROI aligns exactly with group const InclusiveROI expected_roi = group.placement_on_sensor; @@ -241,10 +247,13 @@ TEST_CASE("strixel_to_pixel_map: user ROI larger than group ROI", const auto placement = test_placement(); // User ROI is in module coordinates. - const InclusiveROI user_roi{45, 104, 45, 104}; + const ROI user_roi{45, 105, 45, 105}; - const auto result = - algo::strixel_to_pixel_map(group, sensor, placement, user_roi, {0, 0}); + StrixelPixelMap map(SensorConfig<1>{sensor, {group}}, placement, {0, 0}); + + map.calculate_map(user_roi); + + const auto result = map.get_group_maps()[0]; const InclusiveROI expected_roi = group.placement_on_sensor; @@ -279,10 +288,13 @@ TEST_CASE("strixel_to_pixel_map: user ROI smaller than group ROI", const auto placement = test_placement(); // User ROI is in module coordinates. - const InclusiveROI user_roi{65, 70, 65, 66}; + const ROI user_roi{65, 71, 65, 67}; - const auto result = - algo::strixel_to_pixel_map(group, sensor, placement, user_roi, {0, 0}); + StrixelPixelMap map(SensorConfig<1>{sensor, {group}}, placement, {0, 0}); + + map.calculate_map(user_roi); + + const auto result = map.get_group_maps()[0]; // Now expected_roi is given by the smaller user ROI (in sensor-local // coordinates) @@ -349,10 +361,13 @@ TEST_CASE("strixel_to_pixel_map: user ROI partially overlaps", // User ROI is in module coordinates. // In sensor-local coordinates this is {5, 13, 5, 11}. - const InclusiveROI user_roi{55, 63, 55, 61}; + const ROI user_roi{55, 64, 55, 62}; - const auto result = - algo::strixel_to_pixel_map(group, sensor, placement, user_roi, {0, 0}); + StrixelPixelMap map(SensorConfig<1>{sensor, {group}}, placement, {0, 0}); + + map.calculate_map(user_roi); + + const auto result = map.get_group_maps()[0]; // Expected intersection: // @@ -392,14 +407,16 @@ TEST_CASE("strixel_to_pixel_map: user ROI does not intersect group ROI", const auto sensor = test_sensor(); const auto placement = test_placement(); - const InclusiveROI user_roi{10, 59, 10, 84}; + const ROI user_roi{10, 60, 10, 85}; - const auto result = - algo::strixel_to_pixel_map(group, sensor, placement, user_roi, {0, 0}); + StrixelPixelMap map(SensorConfig<1>{sensor, {group}}, placement, {0, 0}); + + map.calculate_map(user_roi); + + const auto result = map.get_group_map(0); CHECK(result.effective_roi.is_empty()); - CHECK(result.map.data() == - nullptr); // is there a better check for empty NDArray? + CHECK(result.empty() == true); } // ModuloOrdering @@ -409,14 +426,18 @@ TEST_CASE("strixel_to_pixel_map: modulo ordering", const auto sensor = test_sensor(); const auto placement = test_placement(); - const InclusiveROI user_roi{60, 62, 60, 62}; + const ROI user_roi{60, 63, 60, 63}; SECTION("Forward ordering") { const auto forward_group = large_test_group(defs::ModuloOrdering::Forward); - const auto result = algo::strixel_to_pixel_map( - forward_group, sensor, placement, user_roi, {0, 0}); + StrixelPixelMap map(SensorConfig<1>{sensor, {forward_group}}, placement, + {0, 0}); + + map.calculate_map(user_roi); + + const auto result = map.get_group_maps()[0]; CHECK(result.map.shape(0) == 9); CHECK(result.map.shape(1) == 1); @@ -443,8 +464,12 @@ TEST_CASE("strixel_to_pixel_map: modulo ordering", const auto reverse_group = large_test_group(defs::ModuloOrdering::Reverse); - const auto result = algo::strixel_to_pixel_map( - reverse_group, sensor, placement, user_roi, {0, 0}); + StrixelPixelMap map(SensorConfig<1>{sensor, {reverse_group}}, placement, + {0, 0}); + + map.calculate_map(user_roi); + + const auto result = map.get_group_maps()[0]; CHECK(result.map.shape(0) == 9); CHECK(result.map.shape(1) == 1); @@ -476,7 +501,7 @@ TEST_CASE( const auto placement = test_placement(); // user_roi = sensor_roi - const InclusiveROI user_roi = placement.placement_on_module; + const ROI user_roi = toHalfopenROI(placement.placement_on_module); // Rebase into sensor roi: // {50, 99, 50, 99} @@ -491,8 +516,11 @@ TEST_CASE( SECTION("bond shift integrates correctly") { const defs::BondShift shift{2, 3}; - const auto result = algo::strixel_to_pixel_map(group, sensor, placement, - user_roi, shift); + StrixelPixelMap map(SensorConfig<1>{sensor, {group}}, placement, shift); + + map.calculate_map(user_roi); + + const auto result = map.get_group_maps()[0]; // Original group: {5, 13, 8, 16} // After bond shift (+2, +3): @@ -520,8 +548,12 @@ TEST_CASE( const auto rotated_placement = test_placement(defs::Rotation::Rotate180); - const auto result = algo::strixel_to_pixel_map( - group, sensor, rotated_placement, user_roi, {0, 0}); + StrixelPixelMap map(SensorConfig<1>{sensor, {group}}, rotated_placement, + {0, 0}); + + map.calculate_map(user_roi); + + const auto result = map.get_group_maps()[0]; // Original group: {5, 13, 8, 16} // After rotation in 50x50 sensor: @@ -557,11 +589,11 @@ TEST_CASE("strixel_to_pixel_map: invalid multiplicity", const auto sensor = test_sensor(); const auto placement = test_placement(); - const InclusiveROI user_roi{10, 59, 10, 84}; + const ROI user_roi{10, 60, 10, 85}; - CHECK_THROWS_AS( - algo::strixel_to_pixel_map(group, sensor, placement, user_roi, {0, 0}), - std::logic_error); + StrixelPixelMap map(SensorConfig<1>{sensor, {group}}, placement, {0, 0}); + + CHECK_THROWS_AS(map.calculate_map(user_roi), std::logic_error); } // group width not divisible by multiplicity @@ -576,9 +608,9 @@ TEST_CASE("strixel_to_pixel_map: group width not divisible by multiplicity", const auto sensor = test_sensor(); const auto placement = test_placement(); - const InclusiveROI user_roi{10, 59, 10, 84}; + const ROI user_roi{10, 60, 10, 85}; - CHECK_THROWS_AS( - algo::strixel_to_pixel_map(group, sensor, placement, user_roi, {0, 0}), - std::logic_error); + StrixelPixelMap map(SensorConfig<1>{sensor, {group}}, placement, {0, 0}); + + CHECK_THROWS_AS(map.calculate_map(user_roi), std::logic_error); } \ No newline at end of file diff --git a/src/StrixelPixelRemapping/StrixelPixelRemapFormat.cpp b/src/StrixelPixelRemapping/StrixelPixelRemapFormat.cpp index d77f3daf..2dbb3568 100644 --- a/src/StrixelPixelRemapping/StrixelPixelRemapFormat.cpp +++ b/src/StrixelPixelRemapping/StrixelPixelRemapFormat.cpp @@ -3,44 +3,52 @@ #include namespace aare::remap::format { -// static inline std::string to_string(defs::Rotation r) { -// return (r == defs::Rotation::Identity ? "Identity" : "Rotate180"); -// } -// static inline std::string to_string(defs::SensorPixelGeometry const &g) { -// std::ostringstream os; - -// os << "SensorPixelGeometry\n" -// << " cols x rows: " << g.num_pix_x << " x " << g.num_pix_y << "\n" -// << " guardring :\n" -// << " x = " << g.guardring.x << "\n" -// << " y = " << g.guardring.y << "\n"; - -// return os.str(); -// } - -static inline std::string to_string(defs::GroupStrixelGeometry const &g) { - std::ostringstream os; - - os << "GroupStrixelGeometry\n" - << " multiplicity: " << g.multiplicity << "\n" - << " pitch : " << g.pitch_um << " um\n"; - - return os.str(); +std::string to_string(defs::Rotation r) { + return (r == defs::Rotation::Identity ? "Identity" : "Rotate180"); } -static inline std::string to_string(defs::GroupConfig const &c) { - std::ostringstream os; - - os << "GroupConfig\n" - << to_string(c.strixel) << "\n" - << " placement on sensor:\n" - << c.placement_on_sensor << "\n"; - - return os.str(); +std::string to_string(defs::ModuloOrdering mo) { + return (mo == defs::ModuloOrdering::Forward ? "Forward" : "Reverse"); } -inline std::ostream &operator<<(std::ostream &os, defs::GroupConfig const &c) { +std::string to_string(const defs::SensorPixelGeometry &g) { + + return fmt::format("{{cols x rows: {} x {}, guardring: " + "{{x = {}, y = {}}}}}", + g.num_pix_x, g.num_pix_y, g.guardring.x, g.guardring.y); +} + +std::string to_string(const defs::GroupStrixelGeometry &g) { + + return fmt::format("{{multiplicity: {}, pitch_um: {}}}", g.multiplicity, + g.pitch_um); +} + +std::string to_string(const defs::GroupRouting &r) { + return fmt::format("{{{}}}", to_string(r.mod_order)); +} + +std::string to_string(const InclusiveROI &roi) { + return fmt::format("{{xmin={}, xmax={}, ymin={}, ymax={}}}", roi.xmin, + roi.xmax, roi.ymin, roi.ymax); +} + +std::string to_string(const defs::GroupConfig &c) { + return fmt::format("{{strixel_group: {}, routing: {}, " + "placement_on_sensor: {}}}", + to_string(c.strixel), to_string(c.routing), + to_string(c.placement_on_sensor)); +} + +std::string to_string(const defs::SensorModulePlacement &p) { + return fmt::format("{{placement_on_module: {}, " + "rotation: {}}}", + to_string(p.placement_on_module), to_string(p.rotation)); +} + +std::ostream &operator<<(std::ostream &os, const defs::GroupConfig &c) { return os << to_string(c); } -} // namespace aare::remap::format \ No newline at end of file + +} // namespace aare::remap::format