mirror of
https://github.com/slsdetectorgroup/aare.git
synced 2026-10-29 09:01:27 +01:00
Merge branch 'main' into dev/build-time
This commit is contained in:
28 files changed
+2701
-187
No files matched your search
+12
-2
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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``.
|
||||
)");
|
||||
}
|
||||
@@ -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];
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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)))
|
||||
Reference in new issue
Block a user