Merge branch 'main' into dev/build-time
Build on RHEL8 / build (push) Failing after 1m37s
Build on RHEL9 / build (push) Failing after 1m47s
Run tests using data on local RHEL8 / build (push) Failing after 2m33s

This commit is contained in:
Erik Fröjdh authored and GitHub committed 2026-10-26 14:41:23 +01:00
commit a4da73fed1
28 files changed
+2701 -187

No files matched your search

+12 -2
View File
@@ -4,8 +4,18 @@ import numpy as np
from .ScanParameters import ScanParameters
class RawFile(_aare.RawFile):
def __init__(self, fname, chunk_size = 1):
super().__init__(fname)
def __init__(self, fname, chunk_size = 1, gap_pixels = False):
"""Open a raw file for reading.
Args:
fname: Master file (.json or .raw).
chunk_size: Frames returned per iteration step.
gap_pixels: Insert gap pixels into every frame. True uses the
default GapPixels configuration, or pass a GapPixels. Frame
shapes then include the gaps. Only Jungfrau and Eiger are
supported.
"""
super().__init__(fname, gap_pixels)
self._chunk_size = chunk_size
+1
View File
@@ -25,6 +25,7 @@ from ._aare import Pedestal_d, Pedestal_f, ClusterFinder_Cluster3x3i, VarCluster
from ._aare import DetectorType, FrameDiscardPolicy, ReadoutMode
from ._aare import hitmap
from ._aare import ROI
from ._aare import GapPixels, ModuleGaps, insert_gap_pixels, gapped_shape
from ._aare import xy
from ._aare import corner
+2 -2
View File
@@ -135,7 +135,7 @@ class Matterhorn10Transform:
elif self.dynamic_range == 8:
return np.take(data.view(np.uint8), self.pixel_map).reshape(self.num_counters, Matterhorn10.nRows, Matterhorn10.nCols)
else: #dynamic range 4
return np.take(_aare.expand4to8bit(data.view(np.uint8)), self.pixel_map).reshape(self.num_counters, Matterhorn10.nRows, Matterhorn10.nCols)
return np.take(_aare.expand4to8bit(data.view(np.uint8).reshape(-1)), self.pixel_map).reshape(self.num_counters, Matterhorn10.nRows, Matterhorn10.nCols)
class Mythen302Transform:
"""
@@ -169,7 +169,7 @@ class Mythen302Transform:
image : np.ndarray
uint32 array of size 64, 3
"""
res = _aare.decode_my302(data, self.offset)
res = _aare.decode_my302(data.reshape(-1), self.offset)
res = res.reshape(
Mythen302Transform._n_channels, Mythen302Transform._n_counters
)
+174
View File
@@ -0,0 +1,174 @@
// SPDX-License-Identifier: MPL-2.0
#pragma once
#include "aare/GapPixels.hpp"
#include "aare/ROI.hpp"
#include "aare/defs.hpp"
#include "np_helper.hpp"
#include <fmt/format.h>
#include <optional>
#include <pybind11/numpy.h>
#include <pybind11/operators.h>
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
namespace py = pybind11;
using namespace ::aare;
namespace {
template <typename T>
py::array insert_gap_pixels_typed(const py::array &image, const ROI &roi,
const GapPixels &gaps) {
py::array_t<T, py::array::c_style | py::array::forcecast> typed(image);
auto source = make_const_view_2d(typed);
auto *result = new NDArray<T, 2>(insert_gap_pixels<T>(source, roi, gaps));
return return_image_data(result);
}
} // namespace
void define_gap_pixels_bindings(py::module &m) {
py::class_<ModuleGaps>(m, "ModuleGaps", R"(
Pixels inserted at each boundary between modules.
)")
.def(py::init<>())
.def(py::init<ssize_t, ssize_t>(), py::arg("x"), py::arg("y"))
.def_readwrite("x", &ModuleGaps::x)
.def_readwrite("y", &ModuleGaps::y)
.def(py::self == py::self)
.def(py::self != py::self)
.def("__repr__", [](const ModuleGaps &self) {
return fmt::format("ModuleGaps(x={}, y={})", self.x, self.y);
});
py::class_<GapPixels>(m, "GapPixels", R"(
Configuration for inserting gap pixels into Jungfrau and Eiger
images. Chip and module sizes are fixed: 256x256 pixel chips in
1024x512 pixel modules.
Parameters
----------
chip_gap : int
Pixels inserted at each chip boundary inside a module, at most
1024. Source column 255 expands to the right and column 256 to
the left.
module_gaps : ModuleGaps, optional
Pixels inserted at module boundaries, at most 1024 each. None
means no pixels are inserted between modules.
fill_value : float
Value written to gap pixels, converted to the pixel type. Finite
values wrap for integer images; NaN, infinity and magnitudes of
2**63 or more raise ValueError for integer images. Module gaps
always receive this value.
split_counts : bool
Split the counts of the double-size pixels at chip boundaries
between the pixel and its gap pixel. Integer remainders go to
one side at random. Requires a chip gap of 2.
seed : int, optional
Seed for the random assignment of remainders.
)")
.def(
py::init([](ssize_t chip_gap, std::optional<ModuleGaps> module_gaps,
double fill_value, bool split_counts,
std::optional<uint64_t> seed) {
GapPixels gaps;
gaps.chip_gap = chip_gap;
gaps.module_gaps = module_gaps;
gaps.fill_value = fill_value;
gaps.split_counts = split_counts;
gaps.seed = seed;
return gaps;
}),
py::arg("chip_gap") = 2, py::arg("module_gaps") = py::none(),
py::arg("fill_value") = 0.0, py::arg("split_counts") = false,
py::arg("seed") = py::none())
.def_readwrite("chip_gap", &GapPixels::chip_gap)
.def_readwrite("module_gaps", &GapPixels::module_gaps)
.def_readwrite("fill_value", &GapPixels::fill_value)
.def_readwrite("split_counts", &GapPixels::split_counts)
.def_readwrite("seed", &GapPixels::seed)
.def(py::self == py::self)
.def(py::self != py::self)
.def("__repr__", [](const GapPixels &self) {
return fmt::format(
"GapPixels(chip_gap={}, module_gaps={}, fill_value={}, "
"split_counts={}, seed={})",
self.chip_gap,
self.module_gaps
? fmt::format("ModuleGaps(x={}, y={})", self.module_gaps->x,
self.module_gaps->y)
: "None",
self.fill_value, self.split_counts ? "True" : "False",
self.seed ? fmt::format("{}", *self.seed) : "None");
});
m.def(
"gapped_shape",
[](const ROI &roi, const GapPixels &gaps) {
const auto shape = gapped_shape(roi, gaps);
return py::make_tuple(shape[0], shape[1]);
},
py::arg("roi"), py::arg("gaps") = GapPixels{}, R"(
Shape (rows, cols) of the gapped image of a detector coordinate ROI.
Parameters
----------
roi : ROI
Detector coordinates of the image.
gaps : GapPixels
Gap configuration.
)");
m.def(
"insert_gap_pixels",
[](const py::array &image, const GapPixels &gaps,
std::optional<ROI> roi) -> py::array {
if (image.ndim() != 2) {
throw py::value_error(
fmt::format("Expected a 2D image, got {}D", image.ndim()));
}
const ROI rect =
roi.value_or(ROI{0, image.shape(1), 0, image.shape(0)});
if (py::isinstance<py::array_t<uint8_t>>(image)) {
return insert_gap_pixels_typed<uint8_t>(image, rect, gaps);
}
if (py::isinstance<py::array_t<uint16_t>>(image)) {
return insert_gap_pixels_typed<uint16_t>(image, rect, gaps);
}
if (py::isinstance<py::array_t<uint32_t>>(image)) {
return insert_gap_pixels_typed<uint32_t>(image, rect, gaps);
}
if (py::isinstance<py::array_t<int32_t>>(image)) {
return insert_gap_pixels_typed<int32_t>(image, rect, gaps);
}
if (py::isinstance<py::array_t<float>>(image)) {
return insert_gap_pixels_typed<float>(image, rect, gaps);
}
if (py::isinstance<py::array_t<double>>(image)) {
return insert_gap_pixels_typed<double>(image, rect, gaps);
}
throw py::type_error(
"Unsupported dtype, expected uint8, uint16, uint32, int32, "
"float32 or float64");
},
py::arg("image"), py::arg("gaps") = GapPixels{},
py::arg("roi") = py::none(), R"(
Insert gap pixels into a 2D image.
Parameters
----------
image : numpy.ndarray
Ungapped image of ``roi``.
gaps : GapPixels
Gap configuration.
roi : ROI, optional
Detector coordinates of the image. Defaults to the full image
starting at pixel (0, 0).
Returns
-------
numpy.ndarray
New array with the same dtype as ``image``.
)");
}
+35 -8
View File
@@ -3,6 +3,7 @@
#include "aare/CtbRawFile.hpp"
#include "aare/File.hpp"
#include "aare/Frame.hpp"
#include "aare/GapPixels.hpp"
#include "aare/RawFile.hpp"
#include "aare/RawMasterFile.hpp"
#include "aare/RawSubFile.hpp"
@@ -13,6 +14,7 @@
#include <cstdint>
#include <filesystem>
#include <fmt/format.h>
#include <memory>
#include <pybind11/iostream.h>
#include <pybind11/numpy.h>
#include <pybind11/pybind11.h>
@@ -34,7 +36,33 @@ std::runtime_error raw_file_read_error(RawFile &file,
void define_raw_file_io_bindings(py::module &m) {
py::class_<RawFile>(m, "RawFile")
.def(py::init<const std::filesystem::path &>())
.def(py::init([](const std::filesystem::path &fname,
const GapPixels &gap_pixels) {
return std::make_unique<RawFile>(fname, "r", gap_pixels);
}),
py::arg("fname"), py::arg("gap_pixels"))
.def(py::init([](const std::filesystem::path &fname, bool gap_pixels) {
return std::make_unique<RawFile>(
fname, "r",
gap_pixels ? std::optional<GapPixels>(GapPixels{})
: std::nullopt);
}),
py::arg("fname"), py::arg("gap_pixels") = false, R"(
Open a raw file for reading.
Parameters
----------
fname : path
Master file (.json or .raw).
gap_pixels : bool or GapPixels
Insert gap pixels into every frame. True uses the default
GapPixels configuration. Frame shapes, rows() and cols()
then include the gaps. Only Jungfrau and Eiger are
supported.
)")
.def_property_readonly("gap_pixels", &RawFile::gap_pixels, R"(
Gap pixel configuration, or None when reading without gaps.
)")
.def("read_frame",
[](RawFile &self) {
if (self.n_modules_in_roi().size() > 1) {
@@ -118,8 +146,8 @@ void define_raw_file_io_bindings(py::module &m) {
std::vector<size_t> shape;
shape.reserve(2);
shape.push_back(self.roi_geometries(roi_index).pixels_y());
shape.push_back(self.roi_geometries(roi_index).pixels_x());
shape.push_back(self.rows(roi_index));
shape.push_back(self.cols(roi_index));
py::array image =
allocate_image_data(self.bytes_per_pixel(), shape);
@@ -177,8 +205,8 @@ void define_raw_file_io_bindings(py::module &m) {
for (size_t r = 0; r < number_of_ROIs; r++) {
std::vector<size_t> shape;
shape.reserve(2);
shape.push_back(self.roi_geometries(r).pixels_y());
shape.push_back(self.roi_geometries(r).pixels_x());
shape.push_back(self.rows(r));
shape.push_back(self.cols(r));
images[r] =
allocate_image_data(self.bytes_per_pixel(), shape);
@@ -229,9 +257,8 @@ void define_raw_file_io_bindings(py::module &m) {
if (n_frames == 0) {
throw raw_file_read_error(self, "No frames left in file");
}
std::vector<size_t> shape{
n_frames, self.roi_geometries(roi_index).pixels_y(),
self.roi_geometries(roi_index).pixels_x()};
std::vector<size_t> shape{n_frames, self.rows(roi_index),
self.cols(roi_index)};
// return headers from all subfiles
auto n_mod = self.n_modules_in_roi()[roi_index];
+1
View File
@@ -51,6 +51,7 @@ PYBIND11_MODULE(_aare, m) {
bind_eta_3x3(m);
define_defs_bindings(m);
define_gap_pixels_bindings(m);
bind_eta_types(m);
}
+130
View File
@@ -0,0 +1,130 @@
# SPDX-License-Identifier: MPL-2.0
import numpy as np
import pytest
from aare import GapPixels, ModuleGaps, ROI, gapped_shape, insert_gap_pixels
def reference(image, boundaries_x, boundaries_y, fill=0):
"""Insert gap columns and rows with numpy: boundaries map a source index to
the number of pixels inserted before it."""
cols = [index for index, count in boundaries_x for _ in range(count)]
rows = [index for index, count in boundaries_y for _ in range(count)]
result = np.insert(image, cols, fill, axis=1)
return np.insert(result, rows, fill, axis=0)
def test_configuration_defaults_and_equality():
gaps = GapPixels()
assert gaps.chip_gap == 2
assert gaps.module_gaps is None
assert gaps.fill_value == 0
assert gaps.split_counts is False
assert gaps.seed is None
assert gaps == GapPixels()
assert gaps != GapPixels(fill_value=1)
gaps = GapPixels(chip_gap=4, module_gaps=ModuleGaps(8, 36), fill_value=-1, split_counts=False, seed=5)
assert gaps.module_gaps == ModuleGaps(x=8, y=36)
assert gaps.seed == 5
assert "ModuleGaps(x=8, y=36)" in repr(gaps)
gaps.module_gaps = None
assert gaps.module_gaps is None
def test_gapped_shape():
assert gapped_shape(ROI(0, 1024, 0, 512)) == (514, 1030)
assert gapped_shape(ROI(0, 1024, 0, 1024)) == (1028, 1030)
assert gapped_shape(ROI(0, 1024, 0, 1024), GapPixels(module_gaps=ModuleGaps(8, 36))) == (1064, 1030)
assert gapped_shape(ROI(0, 512, 0, 512)) == (514, 514) # Eiger quad
assert gapped_shape(ROI(256, 512, 0, 256)) == (256, 256)
assert gapped_shape(ROI(255, 257, 0, 1)) == (1, 4)
@pytest.mark.parametrize("dtype", [np.uint8, np.uint16, np.uint32, np.int32, np.float32, np.float64])
def test_insert_gap_pixels_matches_numpy(dtype):
rng = np.random.default_rng(1)
image = rng.integers(0, 200, size=(512, 1024)).astype(dtype)
result = insert_gap_pixels(image, GapPixels(fill_value=3))
expected = reference(image, [(256, 2), (512, 2), (768, 2)], [(256, 2)], fill=3)
assert result.dtype == image.dtype
assert result.shape == (514, 1030)
assert (result == expected).all()
def test_insert_gap_pixels_without_module_gaps():
# two stacked modules: chip gaps at rows 256 and 768, none at the module
# boundary at row 512
image = np.arange(1024 * 1024, dtype=np.uint32).reshape(1024, 1024)
result = insert_gap_pixels(image)
expected = reference(image, [(256, 2), (512, 2), (768, 2)], [(256, 2), (768, 2)])
assert result.shape == (1028, 1030)
assert (result == expected).all()
def test_insert_gap_pixels_with_module_gaps():
image = np.arange(1024 * 1024, dtype=np.uint32).reshape(1024, 1024)
gaps = GapPixels(module_gaps=ModuleGaps(8, 36), fill_value=9)
result = insert_gap_pixels(image, gaps)
expected = reference(image, [(256, 2), (512, 2), (768, 2)], [(256, 2), (512, 36), (768, 2)], fill=9)
assert result.shape == (1064, 1030)
assert (result == expected).all()
def test_insert_gap_pixels_in_roi():
image = np.array([[11, 22]], dtype=np.uint16)
result = insert_gap_pixels(image, roi=ROI(255, 257, 10, 11))
assert result.tolist() == [[11, 0, 0, 22]]
# a gap on the ROI border is not inserted
image = np.arange(8, dtype=np.uint16).reshape(2, 4)
result = insert_gap_pixels(image, roi=ROI(256, 260, 0, 2))
assert (result == image).all()
def test_insert_gap_pixels_split_counts():
rng = np.random.default_rng(2)
image = rng.integers(0, 1000, size=(512, 1024)).astype(np.uint32)
gaps = GapPixels(split_counts=True, seed=3)
first = insert_gap_pixels(image, gaps)
second = insert_gap_pixels(image, gaps)
assert first.sum() == image.sum()
assert (first == second).all()
# edge pixel and its gap pixel hold floor and ceil of the half; row 255
# is excluded because it is split again vertically
pair = np.stack([first[:255, 255], first[:255, 256]])
assert (pair.sum(axis=0) == image[:255, 255]).all()
assert (pair.max(axis=0) == (image[:255, 255] + 1) // 2).all()
halves = insert_gap_pixels(np.full((2, 2), 3.0), gaps, roi=ROI(255, 257, 255, 257))
assert (halves == 0.75).all()
def test_insert_gap_pixels_rejects_bad_input():
image = np.zeros((400, 400), dtype=np.uint16)
with pytest.raises(ValueError):
insert_gap_pixels(image, roi=ROI(0, 401, 0, 400))
with pytest.raises(ValueError):
insert_gap_pixels(np.zeros((2, 2, 2), dtype=np.uint16))
with pytest.raises(TypeError):
insert_gap_pixels(np.zeros((2, 2), dtype=np.int64))
with pytest.raises(ValueError):
insert_gap_pixels(image, GapPixels(split_counts=True, chip_gap=4))
with pytest.raises(ValueError):
insert_gap_pixels(image, GapPixels(fill_value=float("nan")))
with pytest.raises(ValueError):
insert_gap_pixels(image, GapPixels(fill_value=1e20))
with pytest.raises(ValueError):
insert_gap_pixels(image, GapPixels(chip_gap=6148914691236517205))
with pytest.raises(ValueError):
insert_gap_pixels(image, GapPixels(module_gaps=ModuleGaps(8, 2000)))
with pytest.raises(ValueError):
insert_gap_pixels(image, roi=ROI(2**40, 2**40 + 400, 0, 400))
def test_insert_gap_pixels_nan_fill_for_float_images():
image = np.ones((2, 2), dtype=np.float32)
result = insert_gap_pixels(image, GapPixels(fill_value=float("nan")), roi=ROI(255, 257, 0, 2))
assert result.shape == (2, 4)
assert np.isnan(result[:, 1:3]).all()
assert (result[:, [0, 3]] == 1).all()
+158
View File
@@ -2,6 +2,7 @@
import pytest
import json
from aare import File, RawFile, RawSubFile, DetectorType, ROI, UDPPortPosition, xy
from aare import GapPixels, ModuleGaps, insert_gap_pixels
import numpy as np
@@ -28,6 +29,46 @@ def small_raw_file(tmp_path):
return master_path
MODULE_ROWS = 2
MODULE_COLS = 3
def write_module_layout(directory, layout_rows, layout_cols, frames=2,
module_rows=MODULE_ROWS, module_cols=MODULE_COLS,
detector_type="Jungfrau"):
"""Write a raw file series with modules in the given layout.
Modules are numbered as DetectorGeometry orders them, down each column
first, and every module holds distinct values so misplacement is visible.
Returns the master path and the expected image of frame 0.
"""
master_path = directory / "run_master_0.json"
master_path.write_text(json.dumps({
"Version": 7.2,
"Detector Type": detector_type,
"Timing Mode": "auto",
"Geometry": {"x": layout_cols, "y": layout_rows},
"Image Size in bytes": module_rows * module_cols * 2,
"Pixels": {"x": module_cols, "y": module_rows},
"Max Frames Per File": 1,
"Total Frames": frames,
"Frames in File": frames,
"Frame Padding": 1,
"Frame Discard Policy": "nodiscard",
}))
expected = np.zeros((module_rows * layout_rows, module_cols * layout_cols), dtype=np.uint16)
for module in range(layout_rows * layout_cols):
module_row, module_col = module % layout_rows, module // layout_rows
pixels = ((np.arange(module_rows * module_cols, dtype=np.uint32) + module * 10 + 1) % 65536).astype(np.uint16)
expected[module_row * module_rows:(module_row + 1) * module_rows,
module_col * module_cols:(module_col + 1) * module_cols] = pixels.reshape(module_rows, module_cols)
for index in range(frames):
data_path = directory / f"run_d{module}_f{index}_0.raw"
# 112 byte detector header followed by the module pixels
data_path.write_bytes(bytes(112) + pixels.tobytes())
return master_path, expected
@pytest.mark.parametrize("reader_type", [RawFile, File])
@pytest.mark.parametrize("legacy_master", [False, True])
@pytest.mark.parametrize("padding, policy, supported", [
@@ -453,3 +494,120 @@ 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)
@pytest.mark.parametrize("layout", [(1, 1), (2, 1), (1, 2), (2, 2), (3, 2)])
@pytest.mark.parametrize("reader_type", [RawFile, File])
def test_raw_file_assembles_module_layout(tmp_path, layout, reader_type):
layout_rows, layout_cols = layout
master_path, expected = write_module_layout(tmp_path, layout_rows, layout_cols)
with reader_type(master_path) as reader:
# RawFile binds rows/cols as methods (ROI overloads), File as properties
rows = reader.rows() if reader_type is RawFile else reader.rows
cols = reader.cols() if reader_type is RawFile else reader.cols
assert (rows, cols) == expected.shape
result = reader.read_frame()
image = result[1] if reader_type is RawFile else result
assert image.shape == expected.shape
assert (image == expected).all()
reader.seek(0)
result = reader.read_n(2)
images = result[1] if reader_type is RawFile else result
assert images.shape == (2,) + expected.shape
assert (images == expected).all()
@pytest.mark.parametrize("layout", [(1, 1), (2, 1), (1, 2)])
def test_raw_file_gap_pixels(tmp_path, layout):
layout_rows, layout_cols = layout
master_path, expected = write_module_layout(tmp_path, layout_rows, layout_cols,
module_rows=512, module_cols=1024)
gaps = GapPixels(fill_value=5)
with RawFile(master_path, gap_pixels=gaps) as reader:
assert reader.gap_pixels == gaps
# one chip boundary per module row, three per module column and no
# gap between modules
assert (reader.rows(), reader.cols()) == ((512 + 2) * layout_rows, (1024 + 6) * layout_cols)
header, image = reader.read_frame()
assert header.size == layout_rows * layout_cols
assert (image == insert_gap_pixels(expected, gaps)).all()
reader.seek(0)
_, images = reader.read_n(2)
assert images.shape == (2, reader.rows(), reader.cols())
assert (images[1] == image).all()
with RawFile(master_path) as reader:
assert reader.gap_pixels is None
assert (reader.rows(), reader.cols()) == expected.shape
with RawFile(master_path, gap_pixels=True) as reader:
assert reader.gap_pixels == GapPixels()
assert (reader.rows(), reader.cols()) == insert_gap_pixels(expected).shape
_, image = reader.read_frame()
assert (image == insert_gap_pixels(expected)).all()
module_gaps = GapPixels(module_gaps=ModuleGaps(8, 36))
with RawFile(master_path, gap_pixels=module_gaps) as reader:
assert (reader.rows(), reader.cols()) == insert_gap_pixels(expected, module_gaps).shape
_, image = reader.read_frame()
assert (image == insert_gap_pixels(expected, module_gaps)).all()
def test_raw_file_gap_pixels_split_counts(tmp_path):
# one Eiger module: four 256x512 ports in a 2x2 layout. Splitting is
# rejected on raw Jungfrau data. The plain read is the reference since
# Eiger ports are row flipped on reading.
master_path, _ = write_module_layout(tmp_path, 2, 2, module_rows=256, module_cols=512,
detector_type="Eiger")
gaps = GapPixels(split_counts=True, seed=11)
with RawFile(master_path) as reader:
_, plain = reader.read_frame()
with RawFile(master_path, gap_pixels=gaps) as reader:
_, image = reader.read_frame()
assert (image == insert_gap_pixels(plain, gaps)).all()
assert image.sum() == plain.sum()
def test_raw_file_gap_pixels_split_counts_rejected_for_jungfrau(tmp_path):
master_path, _ = write_module_layout(tmp_path, 1, 1, module_rows=512, module_cols=1024)
RawFile(master_path, gap_pixels=GapPixels())
with pytest.raises(ValueError):
RawFile(master_path, gap_pixels=GapPixels(split_counts=True))
def test_raw_file_gap_pixels_unsupported_detector(tmp_path):
master_path, _ = write_module_layout(tmp_path, 1, 1, detector_type="Moench")
RawFile(master_path)
with pytest.raises(ValueError):
RawFile(master_path, gap_pixels=True)
@pytest.mark.withdata
def test_raw_file_gap_pixels_recorded_jungfrau(test_data_path):
path = test_data_path / "raw/jungfrau/jungfrau_single_master_0.json"
gaps = GapPixels(fill_value=3)
with RawFile(path, gap_pixels=gaps) as gapped, RawFile(path) as plain:
assert (gapped.rows(), gapped.cols()) == (514, 1030)
_, image = gapped.read_frame()
_, reference = plain.read_frame()
assert image.shape == (514, 1030)
assert (image == insert_gap_pixels(reference, gaps)).all()
@pytest.mark.withdata
def test_raw_file_gap_pixels_recorded_rois(test_data_path):
path = test_data_path / "raw/ROITestData/MultipleROIs/run_master_0.json"
with RawFile(path, gap_pixels=True) as gapped, RawFile(path) as plain:
assert gapped.num_rois == 2
_, images = gapped.read_rois()
_, references = plain.read_rois()
rois = plain.master.rois
assert images[0].shape == (303, 103)
assert images[1].shape == (101, 103)
for image, reference, roi in zip(images, references, rois):
assert (image == insert_gap_pixels(reference, roi=roi)).all()
gapped.seek(0)
_, frames = gapped.read_n_with_roi(2, roi_index=1)
assert frames.shape == (2, 101, 103)
+31
View File
@@ -56,3 +56,34 @@ def test_matterhorn10_16bit_4counters(test_data_path):
expected_data = np.tile(np.arange(255, -1, -1,dtype=np.uint16), (4, 256, 1)) # TODO: endianess issue ?
assert np.all(frames == expected_data)
def test_matterhorn10_4bit_accepts_2d_frame():
"""CtbRawFile.read_frame returns a (1, n_bytes) array; the 4 bit path must accept it"""
n_bytes = (transform.Matterhorn10.nRows * transform.Matterhorn10.nCols * 4) // 8
data = (np.arange(n_bytes) % 256).astype(np.uint8).reshape(1, n_bytes)
t = transform.Matterhorn10Transform(dynamic_range=4, num_counters=1)
frames = t(data)
assert frames.shape == (1, 256, 256)
assert frames.dtype == np.uint8
flat = data.reshape(-1)
nibbles = np.stack([flat & 0x0F, flat >> 4], axis=-1).reshape(-1)
expected = np.take(nibbles, t.pixel_map).reshape(1, 256, 256)
assert np.array_equal(frames, expected)
def test_mythen302_accepts_2d_frame():
"""CtbRawFile.read_frame returns a (1, n_bytes) array; Mythen302Transform must accept it"""
n_bytes = 64 * 3 * 3 + 2 # two extra bytes for the 4 bit offset
rng = np.random.default_rng(0)
data = rng.integers(0, 256, size=(1, n_bytes), dtype=np.uint8)
t = transform.Mythen302Transform(offset=4)
res = t(data)
assert res.shape == (64, 3)
assert res.dtype == np.uint32
assert np.array_equal(res, t(data.reshape(-1)))