mirror of
https://github.com/slsdetectorgroup/aare.git
synced 2026-09-25 20:12:09 +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:
@@ -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