Dev/python bindings for strixels (#364)
Build on RHEL9 / build (push) Successful in 2m50s
Build on RHEL8 / build (push) Successful in 3m40s
Run tests using data on local RHEL8 / build (push) Successful in 4m47s

- added release notes 
- added documentation for python 
- added python bindings

---------

Co-authored-by: vhinger <viktoria.hinger@psi.ch>
Co-authored-by: Erik Fröjdh <erik.frojdh@psi.ch>
This commit is contained in:
2026-09-16 13:53:02 +02:00
committed by GitHub
co-authored by hinger_v Erik Fröjdh
parent 2027c8371f
commit c880d6a252
35 changed files with 2321 additions and 826 deletions
+2
View File
@@ -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
+58 -3
View File
@@ -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)
+2
View File
@@ -70,3 +70,5 @@ from ._aare import (
PixelHistogram_u32,
PixelHistogram_u64,
)
from . import strixelremap
@@ -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)
+6
View File
@@ -0,0 +1,6 @@
from .StrixelRemapFactories import SensorConfig
from .StrixelRemapFactories import StrixelPixelMap
from .._aare.strixelremap import *
+36
View File
@@ -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()
@@ -0,0 +1,114 @@
#include <pybind11/pybind11.h>
#include "aare/StrixelPixelRemapping/InclusiveROI.hpp"
namespace py = pybind11;
void define_InclusiveROI(py::module &m) {
py::class_<aare::InclusiveROI>(m, "InclusiveROI")
.def(py::init<int, int, int, int>(), 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
)");
}
@@ -0,0 +1,151 @@
#include <pybind11/pybind11.h>
#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_<aare::remap::Jungfrau_iLGAD_StrixelPixelMap,
aare::remap::StrixelPixelMap<3, 3>>(
m, "Jungfrau_iLGAD_StrixelPixelMap")
.def(py::init<const aare::remap::defs::SensorModulePlacement &,
const aare::remap::defs::BondShift &>(),
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_<aare::remap::Jungfrau_TEW_StrixelPixelMap,
aare::remap::StrixelPixelMap<3, 3>>(
m, "Jungfrau_TEW_StrixelPixelMap")
.def(py::init<const aare::remap::defs::SensorModulePlacement &,
const aare::remap::defs::BondShift &>(),
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_<aare::remap::Jungfrau_iLGAD_Quad_StrixelPixelMap,
aare::remap::StrixelPixelMap<2, 1>>(
m, "Jungfrau_iLGAD_Quad_StrixelPixelMap")
.def(py::init<const aare::remap::defs::BondShift &>(),
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).
)");
}
@@ -0,0 +1,182 @@
#include <pybind11/pybind11.h>
#include "aare/StrixelPixelRemapping/StrixelPixelRemapDefs.hpp"
#include "aare/StrixelPixelRemapping/StrixelPixelRemapFormat.hpp"
namespace py = pybind11;
void define_PixelStrixelMapDefs(py::module &m) {
py::enum_<aare::remap::defs::Rotation>(m, "Rotation")
.value("Identity", aare::remap::defs::Rotation::Identity)
.value("Rotate180", aare::remap::defs::Rotation::Rotate180)
.export_values();
py::enum_<aare::remap::defs::ModuloOrdering>(m, "ModuloOrdering")
.value("Forward", aare::remap::defs::ModuloOrdering::Forward)
.value("Reverse", aare::remap::defs::ModuloOrdering::Reverse)
.export_values();
py::class_<aare::remap::defs::Guardring>(m, "Guardring")
.def(py::init<int, int>(), 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_<aare::remap::defs::BondShift>(m, "BondShift")
.def(py::init<int, int>(), 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_<aare::remap::defs::SensorPixelGeometry>(m, "SensorPixelGeometry")
.def(py::init<int, int, aare::remap::defs::Guardring>(),
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_<aare::remap::defs::GroupStrixelGeometry>(m,
"GroupStrixelGeometry")
.def(py::init<int, double>(), 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_<aare::remap::defs::GroupRouting>(m, "GroupRouting")
.def(py::init<aare::remap::defs::ModuloOrdering>(),
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_<aare::remap::defs::GroupConfig>(m, "GroupConfig")
.def(py::init<aare::remap::defs::GroupStrixelGeometry,
aare::remap::defs::GroupRouting, aare::InclusiveROI>(),
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_<aare::remap::defs::SensorModulePlacement>(
m, "SensorModulePlacement")
.def(py::init<aare::InclusiveROI, aare::remap::defs::Rotation>(),
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_<aare::remap::defs::StrixelGroupToPixelMap>(
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<ssize_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 <std::size_t N> void define_SensorConfig(py::module &m) {
const auto class_name = fmt::format("SensorConfig_{}PixelGroups", N);
py::class_<aare::remap::defs::SensorConfig<N>>(m, class_name.c_str())
.def(py::init<aare::remap::defs::SensorPixelGeometry,
std::array<aare::remap::defs::GroupConfig, N>>(),
py::arg("pixel"), py::arg("group_configs"))
.def_readwrite("pixel", &aare::remap::defs::SensorConfig<N>::pixel,
"sensor pixel geometry (SensorPixelGeometry)")
.def_readwrite("group_configs",
&aare::remap::defs::SensorConfig<N>::group_configs,
"[list] of strixel group configurations");
}
@@ -0,0 +1,214 @@
#include <pybind11/pybind11.h>
#include "aare/StrixelPixelRemapping/BaseStrixelPixelMap.hpp"
namespace aare::remap::detail {
struct StrixelPixelMapBindingAccess {
template <std::size_t N, std::size_t M, typename T>
static void apply_group_remap(const StrixelPixelMap<N, M> &map,
NDView<T, 2> input, NDView<T, 2> output,
const NDView<const ssize_t, 2> order_map) {
map.apply_group_remap(input, output, order_map);
}
};
} // namespace aare::remap::detail
template <std::size_t N, std::size_t M = N, typename T = uint16_t>
void define_StrixelPixelRemaps(py::module &m) {
const auto class_name =
fmt::format("StrixelPixelMap_{}Groups_{}Maps", N, M);
py::class_<aare::remap::StrixelPixelMap<N, M>>(m, class_name.c_str())
.def(py::init<const aare::remap::defs::SensorConfig<N> &,
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<N, M>::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<N, M> &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<N, M> &self,
const aare::ROI &user_roi,
py::array_t<T, py::array::c_style | py::array::forcecast>
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<T, 2>(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<N, M> &self,
py::array_t<T, py::array::c_style | py::array::forcecast>
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<T, 2>(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<N, M> &self,
py::array_t<T, py::array::c_style | py::array::forcecast> 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<py::array_t<T, py::array::c_style>>(
output[i])) {
throw std::runtime_error(
fmt::format("Output list element at index {} "
"is not a numpy array",
i));
}
auto out = py::array_t<T, py::array::c_style>::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.
)");
}
+16
View File
@@ -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) {
+28
View File
@@ -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<T, N, M, U>(m, "Cluster" #N "x" #M #TYPE_CODE); \
define_ClusterCollector<T, N, M, U>(m, "Cluster" #N "x" #M #TYPE_CODE);
#define DEFINE_BINDINGS_SENSORCONFIG(N) define_SensorConfig<N>(strixelremap);
#define DEFINE_BINDINGS_STRIXELPIXELMAP(N, M) \
define_StrixelPixelRemaps<N, M>(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<double>(m, "d");
define_eta<int>(m, "i");
define_eta<int16_t>(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);
}
+17
View File
@@ -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)
+222
View File
@@ -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
+108
View File
@@ -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("<Q", frame_number) + bytes(range(104)) #in total detector header is 112 bytes, 8 bytes for frame number and 104 bytes for the rest of the header