Merge branch 'main' into dev/reduce
All checks were successful
Build on RHEL8 / build (push) Successful in 3m16s
Build on RHEL9 / build (push) Successful in 3m35s

This commit is contained in:
2025-09-08 15:39:27 +02:00
23 changed files with 658 additions and 55 deletions

View File

@@ -368,6 +368,7 @@ set(PUBLICHEADERS
set(SourceFiles set(SourceFiles
${CMAKE_CURRENT_SOURCE_DIR}/src/calibration.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/CtbRawFile.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/CtbRawFile.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/decode.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/decode.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/defs.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/defs.cpp
@@ -437,6 +438,7 @@ endif()
if(AARE_TESTS) if(AARE_TESTS)
set(TestSources set(TestSources
${CMAKE_CURRENT_SOURCE_DIR}/src/algorithm.test.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/algorithm.test.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/calibration.test.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/defs.test.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/defs.test.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/decode.test.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/decode.test.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/Dtype.test.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Dtype.test.cpp

View File

@@ -1,7 +1,22 @@
# Release notes # Release notes
### head ### 2025.8.22
Features:
- Apply calibration works in G0 if passes a 2D calibration and pedestal
- count pixels that switch
- calculate pedestal (also g0 version)
- NDArray::view() needs an lvalue to reduce issues with the view outliving the array
Bugfixes:
- Now using glibc 2.17 in conda builds (was using the host)
- Fixed shifted pixels in clusters close to the edge of a frame
### 2025.7.18
Features: Features:
@@ -15,7 +30,7 @@ Bugfixes:
- Removed unused file: ClusterFile.cpp - Removed unused file: ClusterFile.cpp
### 2025.05.22 ### 2025.5.22
Features: Features:

View File

@@ -1 +1 @@
2025.5.22 2025.8.22

View File

@@ -3,3 +3,14 @@ python:
- 3.12 - 3.12
- 3.13 - 3.13
c_compiler:
- gcc # [linux]
c_stdlib:
- sysroot # [linux]
cxx_compiler:
- gxx # [linux]
c_stdlib_version: # [linux]
- 2.17 # [linux]

View File

@@ -16,6 +16,8 @@ build:
requirements: requirements:
build: build:
- {{ compiler('c') }}
- {{ stdlib("c") }}
- {{ compiler('cxx') }} - {{ compiler('cxx') }}
- cmake - cmake
- ninja - ninja

View File

@@ -17,8 +17,24 @@ Functions for applying calibration to data.
# Apply calibration to raw data to convert from raw ADC values to keV # Apply calibration to raw data to convert from raw ADC values to keV
data = aare.apply_calibration(raw_data, pd=pedestal, cal=calibration) data = aare.apply_calibration(raw_data, pd=pedestal, cal=calibration)
# If you pass a 2D pedestal and calibration only G0 will be used for the conversion
# Pixels that switched to G1 or G2 will be set to 0
data = aare.apply_calibration(raw_data, pd=pedestal[0], cal=calibration[0])
.. py:currentmodule:: aare .. py:currentmodule:: aare
.. autofunction:: apply_calibration .. autofunction:: apply_calibration
.. autofunction:: load_calibration .. autofunction:: load_calibration
.. autofunction:: calculate_pedestal
.. autofunction:: calculate_pedestal_float
.. autofunction:: calculate_pedestal_g0
.. autofunction:: calculate_pedestal_g0_float
.. autofunction:: count_switching_pixels

View File

@@ -145,8 +145,8 @@ class ClusterFinder {
m_pedestal.mean(iy + ir, ix + ic)); m_pedestal.mean(iy + ir, ix + ic));
cluster.data[i] = cluster.data[i] =
tmp; // Watch for out of bounds access tmp; // Watch for out of bounds access
i++;
} }
i++;
} }
} }

View File

@@ -105,7 +105,7 @@ class Frame {
* @tparam T type of the pixels * @tparam T type of the pixels
* @return NDView<T, 2> * @return NDView<T, 2>
*/ */
template <typename T> NDView<T, 2> view() { template <typename T> NDView<T, 2> view() & {
std::array<ssize_t, 2> shape = {static_cast<ssize_t>(m_rows), std::array<ssize_t, 2> shape = {static_cast<ssize_t>(m_rows),
static_cast<ssize_t>(m_cols)}; static_cast<ssize_t>(m_cols)};
T *data = reinterpret_cast<T *>(m_data); T *data = reinterpret_cast<T *>(m_data);

View File

@@ -25,7 +25,7 @@ template <typename T, ssize_t Ndim = 2>
class NDArray : public ArrayExpr<NDArray<T, Ndim>, Ndim> { class NDArray : public ArrayExpr<NDArray<T, Ndim>, Ndim> {
std::array<ssize_t, Ndim> shape_; std::array<ssize_t, Ndim> shape_;
std::array<ssize_t, Ndim> strides_; std::array<ssize_t, Ndim> strides_;
size_t size_{}; size_t size_{}; //TODO! do we need to store size when we have shape?
T *data_; T *data_;
public: public:
@@ -33,7 +33,7 @@ class NDArray : public ArrayExpr<NDArray<T, Ndim>, Ndim> {
* @brief Default constructor. Will construct an empty NDArray. * @brief Default constructor. Will construct an empty NDArray.
* *
*/ */
NDArray() : shape_(), strides_(c_strides<Ndim>(shape_)), data_(nullptr){}; NDArray() : shape_(), strides_(c_strides<Ndim>(shape_)), data_(nullptr) {};
/** /**
* @brief Construct a new NDArray object with a given shape. * @brief Construct a new NDArray object with a given shape.
@@ -43,8 +43,7 @@ class NDArray : public ArrayExpr<NDArray<T, Ndim>, Ndim> {
*/ */
explicit NDArray(std::array<ssize_t, Ndim> shape) explicit NDArray(std::array<ssize_t, Ndim> shape)
: shape_(shape), strides_(c_strides<Ndim>(shape_)), : shape_(shape), strides_(c_strides<Ndim>(shape_)),
size_(std::accumulate(shape_.begin(), shape_.end(), 1, size_(num_elements(shape_)),
std::multiplies<>())),
data_(new T[size_]) {} data_(new T[size_]) {}
/** /**
@@ -79,6 +78,24 @@ class NDArray : public ArrayExpr<NDArray<T, Ndim>, Ndim> {
other.reset(); // TODO! is this necessary? other.reset(); // TODO! is this necessary?
} }
//Move constructor from an an array with Ndim + 1
template <ssize_t M, typename = std::enable_if_t<(M == Ndim + 1)>>
NDArray(NDArray<T, M> &&other)
: shape_(drop_first_dim(other.shape())),
strides_(c_strides<Ndim>(shape_)), size_(num_elements(shape_)),
data_(other.data()) {
// For now only allow move if the size matches, to avoid unreachable data
// if the use case arises we can remove this check
if(size() != other.size()) {
data_ = nullptr; // avoid double free, other will clean up the memory in it's destructor
throw std::runtime_error(LOCATION +
"Size mismatch in move constructor of NDArray<T, Ndim-1>");
}
other.reset();
}
// Copy constructor // Copy constructor
NDArray(const NDArray &other) NDArray(const NDArray &other)
: shape_(other.shape_), strides_(c_strides<Ndim>(shape_)), : shape_(other.shape_), strides_(c_strides<Ndim>(shape_)),
@@ -380,12 +397,6 @@ NDArray<T, Ndim> NDArray<T, Ndim>::operator*(const T &value) {
result *= value; result *= value;
return result; return result;
} }
// template <typename T, ssize_t Ndim> void NDArray<T, Ndim>::Print() {
// if (shape_[0] < 20 && shape_[1] < 20)
// Print_all();
// else
// Print_some();
// }
template <typename T, ssize_t Ndim> template <typename T, ssize_t Ndim>
std::ostream &operator<<(std::ostream &os, const NDArray<T, Ndim> &arr) { std::ostream &operator<<(std::ostream &os, const NDArray<T, Ndim> &arr) {
@@ -437,4 +448,23 @@ NDArray<T, Ndim> load(const std::string &pathname,
return img; return img;
} }
template <typename RT, typename NT, typename DT, ssize_t Ndim>
NDArray<RT, Ndim> safe_divide(const NDArray<NT, Ndim> &numerator,
const NDArray<DT, Ndim> &denominator) {
if (numerator.shape() != denominator.shape()) {
throw std::runtime_error(
"Shapes of numerator and denominator must match");
}
NDArray<RT, Ndim> result(numerator.shape());
for (ssize_t i = 0; i < numerator.size(); ++i) {
if (denominator[i] != 0) {
result[i] =
static_cast<RT>(numerator[i]) / static_cast<RT>(denominator[i]);
} else {
result[i] = RT{0}; // or handle division by zero as needed
}
}
return result;
}
} // namespace aare } // namespace aare

View File

@@ -26,6 +26,33 @@ Shape<Ndim> make_shape(const std::vector<size_t> &shape) {
return arr; return arr;
} }
/**
* @brief Helper function to drop the first dimension of a shape.
* This is useful when you want to create a 2D view from a 3D array.
* @param shape The shape to drop the first dimension from.
* @return A new shape with the first dimension dropped.
*/
template<size_t Ndim>
Shape<Ndim-1> drop_first_dim(const Shape<Ndim> &shape) {
static_assert(Ndim > 1, "Cannot drop first dimension from a 1D shape");
Shape<Ndim - 1> new_shape;
std::copy(shape.begin() + 1, shape.end(), new_shape.begin());
return new_shape;
}
/**
* @brief Helper function when constructing NDArray/NDView. Calculates the number
* of elements in the resulting array from a shape.
* @param shape The shape to calculate the number of elements for.
* @return The number of elements in and NDArray/NDView of that shape.
*/
template <size_t Ndim>
size_t num_elements(const Shape<Ndim> &shape) {
return std::accumulate(shape.begin(), shape.end(), 1,
std::multiplies<size_t>());
}
template <ssize_t Dim = 0, typename Strides> template <ssize_t Dim = 0, typename Strides>
ssize_t element_offset(const Strides & /*unused*/) { ssize_t element_offset(const Strides & /*unused*/) {
return 0; return 0;
@@ -73,10 +100,21 @@ class NDView : public ArrayExpr<NDView<T, Ndim>, Ndim> {
} }
template <typename... Ix> template <typename... Ix>
const std::enable_if_t<sizeof...(Ix) == Ndim, T &> operator()(Ix... index) const { std::enable_if_t<sizeof...(Ix) == 1 && (Ndim > 1), NDView<T, Ndim - 1>> operator()(Ix... index) {
// return a view of the next dimension
std::array<ssize_t, Ndim - 1> new_shape{};
std::copy_n(shape_.begin() + 1, Ndim - 1, new_shape.begin());
return NDView<T, Ndim - 1>(&buffer_[element_offset(strides_, index...)],
new_shape);
}
template <typename... Ix>
std::enable_if_t<sizeof...(Ix) == Ndim, const T &> operator()(Ix... index) const {
return buffer_[element_offset(strides_, index...)]; return buffer_[element_offset(strides_, index...)];
} }
ssize_t size() const { return static_cast<ssize_t>(size_); } ssize_t size() const { return static_cast<ssize_t>(size_); }
size_t total_bytes() const { return size_ * sizeof(T); } size_t total_bytes() const { return size_ * sizeof(T); }
std::array<ssize_t, Ndim> strides() const noexcept { return strides_; } std::array<ssize_t, Ndim> strides() const noexcept { return strides_; }
@@ -85,9 +123,19 @@ class NDView : public ArrayExpr<NDView<T, Ndim>, Ndim> {
T *end() { return buffer_ + size_; } T *end() { return buffer_ + size_; }
T const *begin() const { return buffer_; } T const *begin() const { return buffer_; }
T const *end() const { return buffer_ + size_; } T const *end() const { return buffer_ + size_; }
T &operator()(ssize_t i) { return buffer_[i]; }
/**
* @brief Access element at index i.
*/
T &operator[](ssize_t i) { return buffer_[i]; } T &operator[](ssize_t i) { return buffer_[i]; }
const T &operator()(ssize_t i) const { return buffer_[i]; }
/**
* @brief Access element at index i.
*/
const T &operator[](ssize_t i) const { return buffer_[i]; } const T &operator[](ssize_t i) const { return buffer_[i]; }
bool operator==(const NDView &other) const { bool operator==(const NDView &other) const {
@@ -157,6 +205,22 @@ class NDView : public ArrayExpr<NDView<T, Ndim>, Ndim> {
const T *data() const { return buffer_; } const T *data() const { return buffer_; }
void print_all() const; void print_all() const;
/**
* @brief Create a subview of a range of the first dimension.
* This is useful for splitting a batches of frames in parallel processing.
* @param first The first index of the subview (inclusive).
* @param last The last index of the subview (exclusive).
* @return A new NDView that is a subview of the current view.
* @throws std::runtime_error if the range is invalid.
*/
NDView sub_view(ssize_t first, ssize_t last) const {
if (first < 0 || last > shape_[0] || first >= last)
throw std::runtime_error(LOCATION + "Invalid sub_view range");
auto new_shape = shape_;
new_shape[0] = last - first;
return NDView(buffer_ + first * strides_[0], new_shape);
}
private: private:
T *buffer_{nullptr}; T *buffer_{nullptr};
std::array<ssize_t, Ndim> strides_{}; std::array<ssize_t, Ndim> strides_{};

View File

@@ -240,14 +240,14 @@ template <typename T> void VarClusterFinder<T>::first_pass() {
for (ssize_t i = 0; i < original_.size(); ++i) { for (ssize_t i = 0; i < original_.size(); ++i) {
if (use_noise_map) if (use_noise_map)
threshold_ = 5 * noiseMap(i); threshold_ = 5 * noiseMap[i];
binary_(i) = (original_(i) > threshold_); binary_[i] = (original_[i] > threshold_);
} }
for (int i = 0; i < shape_[0]; ++i) { for (int i = 0; i < shape_[0]; ++i) {
for (int j = 0; j < shape_[1]; ++j) { for (int j = 0; j < shape_[1]; ++j) {
// do we have someting to process? // do we have something to process?
if (binary_(i, j)) { if (binary_(i, j)) {
auto tmp = check_neighbours(i, j); auto tmp = check_neighbours(i, j);
if (tmp != 0) { if (tmp != 0) {

View File

@@ -1,6 +1,9 @@
#pragma once #pragma once
#include "aare/NDArray.hpp"
#include "aare/NDView.hpp"
#include "aare/defs.hpp" #include "aare/defs.hpp"
#include "aare/utils/par.hpp"
#include "aare/utils/task.hpp" #include "aare/utils/task.hpp"
#include <cstdint> #include <cstdint>
#include <future> #include <future>
@@ -61,26 +64,146 @@ void apply_calibration_impl(NDView<T, 3> res, NDView<uint16_t, 3> raw_data,
for (int frame_nr = start; frame_nr != stop; ++frame_nr) { for (int frame_nr = start; frame_nr != stop; ++frame_nr) {
for (int row = 0; row != raw_data.shape(1); ++row) { for (int row = 0; row != raw_data.shape(1); ++row) {
for (int col = 0; col != raw_data.shape(2); ++col) { for (int col = 0; col != raw_data.shape(2); ++col) {
auto [value, gain] = get_value_and_gain(raw_data(frame_nr, row, col)); auto [value, gain] =
get_value_and_gain(raw_data(frame_nr, row, col));
// Using multiplication does not seem to speed up the code here
// ADU/keV is the standard unit for the calibration which
// means rewriting the formula is not worth it.
res(frame_nr, row, col) = res(frame_nr, row, col) =
(value - ped(gain, row, col)) / cal(gain, row, col); //TODO! use multiplication (value - ped(gain, row, col)) / cal(gain, row, col);
} }
} }
} }
} }
template <class T> template <class T>
void apply_calibration_impl(NDView<T, 3> res, NDView<uint16_t, 3> raw_data,
NDView<T, 2> ped, NDView<T, 2> cal, int start,
int stop) {
for (int frame_nr = start; frame_nr != stop; ++frame_nr) {
for (int row = 0; row != raw_data.shape(1); ++row) {
for (int col = 0; col != raw_data.shape(2); ++col) {
auto [value, gain] =
get_value_and_gain(raw_data(frame_nr, row, col));
// Using multiplication does not seem to speed up the code here
// ADU/keV is the standard unit for the calibration which
// means rewriting the formula is not worth it.
// Set the value to 0 if the gain is not 0
if (gain == 0)
res(frame_nr, row, col) =
(value - ped(row, col)) / cal(row, col);
else
res(frame_nr, row, col) = 0;
}
}
}
}
template <class T, ssize_t Ndim = 3>
void apply_calibration(NDView<T, 3> res, NDView<uint16_t, 3> raw_data, void apply_calibration(NDView<T, 3> res, NDView<uint16_t, 3> raw_data,
NDView<T, 3> ped, NDView<T, 3> cal, NDView<T, Ndim> ped, NDView<T, Ndim> cal,
ssize_t n_threads = 4) { ssize_t n_threads = 4) {
std::vector<std::future<void>> futures; std::vector<std::future<void>> futures;
futures.reserve(n_threads); futures.reserve(n_threads);
auto limits = split_task(0, raw_data.shape(0), n_threads); auto limits = split_task(0, raw_data.shape(0), n_threads);
for (const auto &lim : limits) for (const auto &lim : limits)
futures.push_back(std::async(&apply_calibration_impl<T>, res, raw_data, ped, cal, futures.push_back(std::async(
lim.first, lim.second)); static_cast<void (*)(NDView<T, 3>, NDView<uint16_t, 3>,
NDView<T, Ndim>, NDView<T, Ndim>, int, int)>(
apply_calibration_impl),
res, raw_data, ped, cal, lim.first, lim.second));
for (auto &f : futures) for (auto &f : futures)
f.get(); f.get();
} }
template <bool only_gain0>
std::pair<NDArray<size_t, 3>, NDArray<size_t, 3>>
sum_and_count_per_gain(NDView<uint16_t, 3> raw_data) {
constexpr ssize_t num_gains = only_gain0 ? 1 : 3;
NDArray<size_t, 3> accumulator(
std::array<ssize_t, 3>{num_gains, raw_data.shape(1), raw_data.shape(2)},
0);
NDArray<size_t, 3> count(
std::array<ssize_t, 3>{num_gains, raw_data.shape(1), raw_data.shape(2)},
0);
for (int frame_nr = 0; frame_nr != raw_data.shape(0); ++frame_nr) {
for (int row = 0; row != raw_data.shape(1); ++row) {
for (int col = 0; col != raw_data.shape(2); ++col) {
auto [value, gain] =
get_value_and_gain(raw_data(frame_nr, row, col));
if (gain != 0 && only_gain0)
continue;
accumulator(gain, row, col) += value;
count(gain, row, col) += 1;
}
}
}
return {std::move(accumulator), std::move(count)};
}
template <typename T, bool only_gain0 = false>
NDArray<T, 3 - static_cast<ssize_t>(only_gain0)>
calculate_pedestal(NDView<uint16_t, 3> raw_data, ssize_t n_threads) {
constexpr ssize_t num_gains = only_gain0 ? 1 : 3;
std::vector<std::future<std::pair<NDArray<size_t, 3>, NDArray<size_t, 3>>>>
futures;
futures.reserve(n_threads);
auto subviews = make_subviews(raw_data, n_threads);
for (auto view : subviews) {
futures.push_back(std::async(
static_cast<std::pair<NDArray<size_t, 3>, NDArray<size_t, 3>> (*)(
NDView<uint16_t, 3>)>(&sum_and_count_per_gain<only_gain0>),
view));
}
Shape<3> shape{num_gains, raw_data.shape(1), raw_data.shape(2)};
NDArray<size_t, 3> accumulator(shape, 0);
NDArray<size_t, 3> count(shape, 0);
// Combine the results from the futures
for (auto &f : futures) {
auto [acc, cnt] = f.get();
accumulator += acc;
count += cnt;
}
// Will move to a NDArray<T, 3 - static_cast<ssize_t>(only_gain0)>
// if only_gain0 is true
return safe_divide<T>(accumulator, count);
}
/**
* @brief Count the number of switching pixels in the raw data.
* This function counts the number of pixels that switch between G1 and G2 gain.
* It returns an NDArray with the number of switching pixels per pixel.
* @param raw_data The NDView containing the raw data
* @return An NDArray with the number of switching pixels per pixel
*/
NDArray<int, 2> count_switching_pixels(NDView<uint16_t, 3> raw_data);
/**
* @brief Count the number of switching pixels in the raw data.
* This function counts the number of pixels that switch between G1 and G2 gain.
* It returns an NDArray with the number of switching pixels per pixel.
* @param raw_data The NDView containing the raw data
* @param n_threads The number of threads to use for parallel processing
* @return An NDArray with the number of switching pixels per pixel
*/
NDArray<int, 2> count_switching_pixels(NDView<uint16_t, 3> raw_data,
ssize_t n_threads);
template <typename T>
auto calculate_pedestal_g0(NDView<uint16_t, 3> raw_data, ssize_t n_threads) {
return calculate_pedestal<T, true>(raw_data, n_threads);
}
} // namespace aare } // namespace aare

View File

@@ -1,7 +1,10 @@
#pragma once
#include <thread> #include <thread>
#include <utility> #include <utility>
#include <vector> #include <vector>
#include "aare/utils/task.hpp"
namespace aare { namespace aare {
template <typename F> template <typename F>
@@ -15,4 +18,17 @@ void RunInParallel(F func, const std::vector<std::pair<int, int>> &tasks) {
thread.join(); thread.join();
} }
} }
template <typename T>
std::vector<NDView<T,3>> make_subviews(NDView<T, 3> &data, ssize_t n_threads) {
std::vector<NDView<T, 3>> subviews;
subviews.reserve(n_threads);
auto limits = split_task(0, data.shape(0), n_threads);
for (const auto &lim : limits) {
subviews.push_back(data.sub_view(lim.first, lim.second));
}
return subviews;
}
} // namespace aare } // namespace aare

View File

@@ -1,4 +1,4 @@
#pragma once
#include <utility> #include <utility>
#include <vector> #include <vector>

View File

@@ -46,14 +46,13 @@ def ClusterFinderMT(image_size, cluster_size = (3,3), dtype=np.int32, n_sigma=5,
return cls(image_size, n_sigma=n_sigma, capacity=capacity, n_threads=n_threads) return cls(image_size, n_sigma=n_sigma, capacity=capacity, n_threads=n_threads)
def ClusterCollector(clusterfindermt, dtype=np.int32):
def ClusterCollector(clusterfindermt, cluster_size = (3,3), dtype=np.int32):
""" """
Factory function to create a ClusterCollector object. Provides a cleaner syntax for Factory function to create a ClusterCollector object. Provides a cleaner syntax for
the templated ClusterCollector in C++. the templated ClusterCollector in C++.
""" """
cls = _get_class("ClusterCollector", cluster_size, dtype) cls = _get_class("ClusterCollector", clusterfindermt.cluster_size, dtype)
return cls(clusterfindermt) return cls(clusterfindermt)
def ClusterFileSink(clusterfindermt, cluster_file, dtype=np.int32): def ClusterFileSink(clusterfindermt, cluster_file, dtype=np.int32):

View File

@@ -32,6 +32,7 @@ from .utils import random_pixels, random_pixel, flat_list, add_colorbar
from .func import * from .func import *
from .calibration import * from .calibration import *
from ._aare import apply_calibration from ._aare import apply_calibration, count_switching_pixels
from ._aare import calculate_pedestal, calculate_pedestal_float, calculate_pedestal_g0, calculate_pedestal_g0_float
from ._aare import VarClusterFinder from ._aare import VarClusterFinder

View File

@@ -17,27 +17,137 @@ py::array_t<DataType> pybind_apply_calibration(
calibration, calibration,
int n_threads = 4) { int n_threads = 4) {
auto data_span = make_view_3d(data); auto data_span = make_view_3d(data); // data is always 3D
auto ped = make_view_3d(pedestal);
auto cal = make_view_3d(calibration);
/* No pointer is passed, so NumPy will allocate the buffer */ /* No pointer is passed, so NumPy will allocate the buffer */
auto result = py::array_t<DataType>(data_span.shape()); auto result = py::array_t<DataType>(data_span.shape());
auto res = make_view_3d(result); auto res = make_view_3d(result);
if (data.ndim() == 3 && pedestal.ndim() == 3 && calibration.ndim() == 3) {
aare::apply_calibration<DataType>(res, data_span, ped, cal, n_threads); auto ped = make_view_3d(pedestal);
auto cal = make_view_3d(calibration);
aare::apply_calibration<DataType, 3>(res, data_span, ped, cal,
n_threads);
} else if (data.ndim() == 3 && pedestal.ndim() == 2 &&
calibration.ndim() == 2) {
auto ped = make_view_2d(pedestal);
auto cal = make_view_2d(calibration);
aare::apply_calibration<DataType, 2>(res, data_span, ped, cal,
n_threads);
} else {
throw std::runtime_error(
"Invalid number of dimensions for data, pedestal or calibration");
}
return result; return result;
} }
py::array_t<int> pybind_count_switching_pixels(
py::array_t<uint16_t, py::array::c_style | py::array::forcecast> data,
ssize_t n_threads = 4) {
auto data_span = make_view_3d(data);
auto arr = new NDArray<int, 2>{};
*arr = aare::count_switching_pixels(data_span, n_threads);
return return_image_data(arr);
}
template <typename T>
py::array_t<T> pybind_calculate_pedestal(
py::array_t<uint16_t, py::array::c_style | py::array::forcecast> data,
ssize_t n_threads) {
auto data_span = make_view_3d(data);
auto arr = new NDArray<T, 3>{};
*arr = aare::calculate_pedestal<T, false>(data_span, n_threads);
return return_image_data(arr);
}
template <typename T>
py::array_t<T> pybind_calculate_pedestal_g0(
py::array_t<uint16_t, py::array::c_style | py::array::forcecast> data,
ssize_t n_threads) {
auto data_span = make_view_3d(data);
auto arr = new NDArray<T, 2>{};
*arr = aare::calculate_pedestal<T, true>(data_span, n_threads);
return return_image_data(arr);
}
void bind_calibration(py::module &m) { void bind_calibration(py::module &m) {
m.def("apply_calibration", &pybind_apply_calibration<double>,
py::arg("raw_data").noconvert(), py::kw_only(),
py::arg("pd").noconvert(), py::arg("cal").noconvert(),
py::arg("n_threads") = 4);
m.def("apply_calibration", &pybind_apply_calibration<float>, m.def("apply_calibration", &pybind_apply_calibration<float>,
py::arg("raw_data").noconvert(), py::kw_only(), py::arg("raw_data").noconvert(), py::kw_only(),
py::arg("pd").noconvert(), py::arg("cal").noconvert(), py::arg("pd").noconvert(), py::arg("cal").noconvert(),
py::arg("n_threads") = 4); py::arg("n_threads") = 4);
m.def("apply_calibration", &pybind_apply_calibration<double>, m.def("count_switching_pixels", &pybind_count_switching_pixels,
R"(
Count the number of time each pixel switches to G1 or G2.
Parameters
----------
raw_data : array_like
3D array of shape (frames, rows, cols) to count the switching pixels from.
n_threads : int
The number of threads to use for the calculation.
)",
py::arg("raw_data").noconvert(), py::kw_only(), py::arg("raw_data").noconvert(), py::kw_only(),
py::arg("pd").noconvert(), py::arg("cal").noconvert(),
py::arg("n_threads") = 4); py::arg("n_threads") = 4);
m.def("calculate_pedestal", &pybind_calculate_pedestal<double>,
R"(
Calculate the pedestal for all three gains and return the result as a 3D array of doubles.
Parameters
----------
raw_data : array_like
3D array of shape (frames, rows, cols) to calculate the pedestal from.
Needs to contain data for all three gains (G0, G1, G2).
n_threads : int
The number of threads to use for the calculation.
)",
py::arg("raw_data").noconvert(), py::arg("n_threads") = 4);
m.def("calculate_pedestal_float", &pybind_calculate_pedestal<float>,
R"(
Same as `calculate_pedestal` but returns a 3D array of floats.
Parameters
----------
raw_data : array_like
3D array of shape (frames, rows, cols) to calculate the pedestal from.
Needs to contain data for all three gains (G0, G1, G2).
n_threads : int
The number of threads to use for the calculation.
)",
py::arg("raw_data").noconvert(), py::arg("n_threads") = 4);
m.def("calculate_pedestal_g0", &pybind_calculate_pedestal_g0<double>,
R"(
Calculate the pedestal for G0 and return the result as a 2D array of doubles.
Pixels in G1 and G2 are ignored.
Parameters
----------
raw_data : array_like
3D array of shape (frames, rows, cols) to calculate the pedestal from.
n_threads : int
The number of threads to use for the calculation.
)",
py::arg("raw_data").noconvert(), py::arg("n_threads") = 4);
m.def("calculate_pedestal_g0_float", &pybind_calculate_pedestal_g0<float>,
R"(
Same as `calculate_pedestal_g0` but returns a 2D array of floats.
Parameters
----------
raw_data : array_like
3D array of shape (frames, rows, cols) to calculate the pedestal from.
n_threads : int
The number of threads to use for the calculation.
)",
py::arg("raw_data").noconvert(), py::arg("n_threads") = 4);
} }

View File

@@ -1,6 +1,7 @@
import pytest import pytest
import numpy as np import numpy as np
from aare import apply_calibration
import aare
def test_apply_calibration_small_data(): def test_apply_calibration_small_data():
# The raw data consists of 10 4x5 images # The raw data consists of 10 4x5 images
@@ -27,7 +28,7 @@ def test_apply_calibration_small_data():
data = apply_calibration(raw, pd = pedestal, cal = calibration) data = aare.apply_calibration(raw, pd = pedestal, cal = calibration)
# The formula that is applied is: # The formula that is applied is:
@@ -41,3 +42,94 @@ def test_apply_calibration_small_data():
assert data[2,2,2] == 0 assert data[2,2,2] == 0
assert data[0,1,1] == 0 assert data[0,1,1] == 0
assert data[1,3,0] == 0 assert data[1,3,0] == 0
@pytest.fixture
def raw_data_3x2x2():
raw = np.zeros((3, 2, 2), dtype=np.uint16)
raw[0, 0, 0] = 100
raw[1,0, 0] = 200
raw[2, 0, 0] = 300
raw[0, 0, 1] = (1<<14) + 100
raw[1, 0, 1] = (1<<14) + 200
raw[2, 0, 1] = (1<<14) + 300
raw[0, 1, 0] = (1<<14) + 37
raw[1, 1, 0] = 38
raw[2, 1, 0] = (3<<14) + 39
raw[0, 1, 1] = (3<<14) + 100
raw[1, 1, 1] = (3<<14) + 200
raw[2, 1, 1] = (3<<14) + 300
return raw
def test_calculate_pedestal(raw_data_3x2x2):
# Calculate the pedestal
pd = aare.calculate_pedestal(raw_data_3x2x2)
assert pd.shape == (3, 2, 2)
assert pd.dtype == np.float64
assert pd[0, 0, 0] == 200
assert pd[1, 0, 0] == 0
assert pd[2, 0, 0] == 0
assert pd[0, 0, 1] == 0
assert pd[1, 0, 1] == 200
assert pd[2, 0, 1] == 0
assert pd[0, 1, 0] == 38
assert pd[1, 1, 0] == 37
assert pd[2, 1, 0] == 39
assert pd[0, 1, 1] == 0
assert pd[1, 1, 1] == 0
assert pd[2, 1, 1] == 200
def test_calculate_pedestal_float(raw_data_3x2x2):
#results should be the same for float
pd2 = aare.calculate_pedestal_float(raw_data_3x2x2)
assert pd2.shape == (3, 2, 2)
assert pd2.dtype == np.float32
assert pd2[0, 0, 0] == 200
assert pd2[1, 0, 0] == 0
assert pd2[2, 0, 0] == 0
assert pd2[0, 0, 1] == 0
assert pd2[1, 0, 1] == 200
assert pd2[2, 0, 1] == 0
assert pd2[0, 1, 0] == 38
assert pd2[1, 1, 0] == 37
assert pd2[2, 1, 0] == 39
assert pd2[0, 1, 1] == 0
assert pd2[1, 1, 1] == 0
assert pd2[2, 1, 1] == 200
def test_calculate_pedestal_g0(raw_data_3x2x2):
pd = aare.calculate_pedestal_g0(raw_data_3x2x2)
assert pd.shape == (2, 2)
assert pd.dtype == np.float64
assert pd[0, 0] == 200
assert pd[1, 0] == 38
assert pd[0, 1] == 0
assert pd[1, 1] == 0
def test_calculate_pedestal_g0_float(raw_data_3x2x2):
pd = aare.calculate_pedestal_g0_float(raw_data_3x2x2)
assert pd.shape == (2, 2)
assert pd.dtype == np.float32
assert pd[0, 0] == 200
assert pd[1, 0] == 38
assert pd[0, 1] == 0
assert pd[1, 1] == 0
def test_count_switching_pixels(raw_data_3x2x2):
# Count the number of pixels that switched gain
count = aare.count_switching_pixels(raw_data_3x2x2)
assert count.shape == (2, 2)
assert count.sum() == 8
assert count[0, 0] == 0
assert count[1, 0] == 2
assert count[0, 1] == 3
assert count[1, 1] == 3

View File

@@ -57,6 +57,7 @@ class ClusterFinderMTWrapper
size_t m_sink_size() const { return this->m_sink.sizeGuess(); } size_t m_sink_size() const { return this->m_sink.sizeGuess(); }
}; };
TEST_CASE("multithreaded cluster finder", "[.with-data]") { TEST_CASE("multithreaded cluster finder", "[.with-data]") {
auto fpath = auto fpath =
test_data_path() / "raw/moench03/cu_half_speed_master_4.json"; test_data_path() / "raw/moench03/cu_half_speed_master_4.json";
@@ -81,7 +82,8 @@ TEST_CASE("multithreaded cluster finder", "[.with-data]") {
CHECK(cf.m_input_queues_are_empty() == true); CHECK(cf.m_input_queues_are_empty() == true);
for (size_t i = 0; i < n_frames_pd; ++i) { for (size_t i = 0; i < n_frames_pd; ++i) {
cf.find_clusters(file.read_frame().view<uint16_t>()); auto frame = file.read_frame();
cf.find_clusters(frame.view<uint16_t>());
} }
cf.stop(); cf.stop();

View File

@@ -25,13 +25,13 @@ TEST_CASE("Construct from an NDView") {
REQUIRE(image.data() != view.data()); REQUIRE(image.data() != view.data());
for (uint32_t i = 0; i < image.size(); ++i) { for (uint32_t i = 0; i < image.size(); ++i) {
REQUIRE(image(i) == view(i)); REQUIRE(image[i] == view[i]);
} }
// Changing the image doesn't change the view // Changing the image doesn't change the view
image = 43; image = 43;
for (uint32_t i = 0; i < image.size(); ++i) { for (uint32_t i = 0; i < image.size(); ++i) {
REQUIRE(image(i) != view(i)); REQUIRE(image[i] != view[i]);
} }
} }
@@ -428,3 +428,29 @@ TEST_CASE("Construct an NDArray from an std::array") {
REQUIRE(a(i) == b[i]); REQUIRE(a(i) == b[i]);
} }
} }
TEST_CASE("Move construct from an array with Ndim + 1") {
NDArray<int, 3> a({{1,2,2}}, 0);
a(0, 0, 0) = 1;
a(0, 0, 1) = 2;
a(0, 1, 0) = 3;
a(0, 1, 1) = 4;
NDArray<int, 2> b(std::move(a));
REQUIRE(b.shape() == Shape<2>{2,2});
REQUIRE(b.size() == 4);
REQUIRE(b(0, 0) == 1);
REQUIRE(b(0, 1) == 2);
REQUIRE(b(1, 0) == 3);
REQUIRE(b(1, 1) == 4);
}
TEST_CASE("Move construct from an array with Ndim + 1 throws on size mismatch") {
NDArray<int, 3> a({{2,2,2}}, 0);
REQUIRE_THROWS(NDArray<int, 2>(std::move(a)));
}

44
src/calibration.cpp Normal file
View File

@@ -0,0 +1,44 @@
#include "aare/calibration.hpp"
namespace aare {
NDArray<int, 2> count_switching_pixels(NDView<uint16_t, 3> raw_data) {
NDArray<int, 2> switched(
std::array<ssize_t, 2>{raw_data.shape(1), raw_data.shape(2)}, 0);
for (int frame_nr = 0; frame_nr != raw_data.shape(0); ++frame_nr) {
for (int row = 0; row != raw_data.shape(1); ++row) {
for (int col = 0; col != raw_data.shape(2); ++col) {
auto [value, gain] =
get_value_and_gain(raw_data(frame_nr, row, col));
if (gain != 0) {
switched(row, col) += 1;
}
}
}
}
return switched;
}
NDArray<int, 2> count_switching_pixels(NDView<uint16_t, 3> raw_data,
ssize_t n_threads) {
NDArray<int, 2> switched(
std::array<ssize_t, 2>{raw_data.shape(1), raw_data.shape(2)}, 0);
std::vector<std::future<NDArray<int, 2>>> futures;
futures.reserve(n_threads);
auto subviews = make_subviews(raw_data, n_threads);
for (auto view : subviews) {
futures.push_back(
std::async(static_cast<NDArray<int, 2> (*)(NDView<uint16_t, 3>)>(
&count_switching_pixels),
view));
}
for (auto &f : futures) {
switched += f.get();
}
return switched;
}
} // namespace aare

49
src/calibration.test.cpp Normal file
View File

@@ -0,0 +1,49 @@
/************************************************
* @file test-Cluster.cpp
* @short test case for generic Cluster, ClusterVector, and calculate_eta2
***********************************************/
#include "aare/calibration.hpp"
// #include "catch.hpp"
#include <array>
#include <catch2/catch_all.hpp>
#include <catch2/catch_test_macros.hpp>
using namespace aare;
TEST_CASE("Test Pedestal Generation", "[.calibration]") {
NDArray<uint16_t, 3> raw(std::array<ssize_t, 3>{3, 2, 2}, 0);
// gain 0
raw(0, 0, 0) = 100;
raw(1, 0, 0) = 200;
raw(2, 0, 0) = 300;
// gain 1
raw(0, 0, 1) = (1 << 14) + 100;
raw(1, 0, 1) = (1 << 14) + 200;
raw(2, 0, 1) = (1 << 14) + 300;
raw(0, 1, 0) = (1 << 14) + 37;
raw(1, 1, 0) = 38;
raw(2, 1, 0) = (3 << 14) + 39;
// gain 2
raw(0, 1, 1) = (3 << 14) + 100;
raw(1, 1, 1) = (3 << 14) + 200;
raw(2, 1, 1) = (3 << 14) + 300;
auto pedestal = calculate_pedestal<double>(raw.view(), 4);
REQUIRE(pedestal.size() == raw.size());
CHECK(pedestal(0, 0, 0) == 200);
CHECK(pedestal(1, 0, 0) == 0);
CHECK(pedestal(1, 0, 1) == 200);
auto pedestal_gain0 = calculate_pedestal_g0<double>(raw.view(), 4);
REQUIRE(pedestal_gain0.size() == 4);
CHECK(pedestal_gain0(0, 0) == 200);
CHECK(pedestal_gain0(1, 0) == 38);
}

View File

@@ -7,6 +7,7 @@ Script to update VERSION file with semantic versioning if provided as an argumen
import sys import sys
import os import os
import re import re
from datetime import datetime
from packaging.version import Version, InvalidVersion from packaging.version import Version, InvalidVersion
@@ -26,8 +27,8 @@ def get_version():
# Check at least one argument is passed # Check at least one argument is passed
if len(sys.argv) < 2: if len(sys.argv) < 2:
return "0.0.0" version = datetime.today().strftime('%Y.%-m.%-d')
else:
version = sys.argv[1] version = sys.argv[1]
try: try: