mirror of
https://github.com/slsdetectorgroup/aare.git
synced 2026-09-25 17:02:11 +02:00
Dev/python bindings for strixels (#364)
- 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:
@@ -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
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1,6 @@
|
||||
from .StrixelRemapFactories import SensorConfig
|
||||
|
||||
from .StrixelRemapFactories import StrixelPixelMap
|
||||
|
||||
from .._aare.strixelremap import *
|
||||
|
||||
@@ -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.
|
||||
)");
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user