Skip to content

Commit 8f0531d

Browse files
Merge pull request #35 from KrisThielemans/DetectionEfficiencies
add DetectionEfficiencies (aka "normalisation")
2 parents b49b6c8 + 970e612 commit 8f0531d

9 files changed

+497
-52
lines changed

cpp/petsird_analysis.cpp

+15
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ using petsird::hdf5::PETSIRDReader;
1515
# include "generated/binary/protocols.h"
1616
using petsird::binary::PETSIRDReader;
1717
#endif
18+
#include "petsird_helpers.h"
1819
#include <xtensor/xview.hpp>
1920
#include <xtensor/xio.hpp>
2021
#include <iostream>
@@ -45,6 +46,8 @@ main(int argc, char* argv[])
4546
<< header.scanner.scanner_geometry.replicated_modules[0].object.detecting_elements.size() << std::endl;
4647
std::cout << "Number of elements of first type in modules of first type: "
4748
<< header.scanner.scanner_geometry.replicated_modules[0].object.detecting_elements[0].transforms.size() << std::endl;
49+
std::cout << "Total number of 'crystals': " << petsird_helpers::get_num_det_els(header.scanner.scanner_geometry) << std::endl;
50+
4851
std::cout << "Number of TOF bins: " << header.scanner.NumberOfTOFBins() << std::endl;
4952
std::cout << "Number of energy bins: " << header.scanner.NumberOfEnergyBins() << std::endl;
5053

@@ -70,11 +73,23 @@ main(int argc, char* argv[])
7073
auto& event_time_block = std::get<petsird::EventTimeBlock>(time_block);
7174
last_time = event_time_block.start;
7275
num_prompts += event_time_block.prompt_events.size();
76+
std::cout << "===================== Events in time block from " << last_time << " ==============\n";
7377

7478
for (auto& event : event_time_block.prompt_events)
7579
{
7680
energy_1 += energy_mid_points[event.energy_indices[0]];
7781
energy_2 += energy_mid_points[event.energy_indices[1]];
82+
83+
std::cout << "CoincidenceEvent(detectorIds=[" << event.detector_ids[0] << ", " << event.detector_ids[1]
84+
<< "], tofIdx=" << event.tof_idx << ", energyIndices=[" << event.energy_indices[0] << ", "
85+
<< event.energy_indices[1] << "])\n";
86+
const auto module_and_elems
87+
= petsird_helpers::get_module_and_element(header.scanner.scanner_geometry, event.detector_ids);
88+
std::cout << " "
89+
<< "[ModuleAndElement(module=" << module_and_elems[0].module << ", "
90+
<< "el=" << module_and_elems[0].el << "), ModuleAndElement(module=" << module_and_elems[0].module << ", "
91+
<< "el=" << module_and_elems[0].el << ")]\n";
92+
std::cout << " efficiency:" << petsird_helpers::get_detection_efficiency(header.scanner, event) << "\n";
7893
}
7994
}
8095
}

cpp/petsird_generator.cpp

+112-21
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include <iostream>
99
#include <cmath>
1010
#include <random>
11+
#include <algorithm>
1112

1213
// (un)comment if you want HDF5 or binary output
1314
#define USE_HDF5
@@ -20,12 +21,18 @@ using petsird::hdf5::PETSIRDWriter;
2021
using petsird::binary::PETSIRDWriter;
2122
#endif
2223

24+
#include "petsird_helpers.h"
25+
2326
// these are constants for now
2427
constexpr uint32_t NUMBER_OF_ENERGY_BINS = 3;
2528
constexpr uint32_t NUMBER_OF_TOF_BINS = 300;
2629
constexpr float RADIUS = 400.F;
27-
constexpr std::array<float, 3> CRYSTAL_LENGTH{ 4.F, 4.F, 20.F };
28-
constexpr std::array<float, 3> NUM_CRYSTALS_PER_MODULE{ 5, 6, 2 };
30+
constexpr std::array<float, 3> CRYSTAL_LENGTH{ 20.F, 4.F, 4.F };
31+
constexpr std::array<float, 3> NUM_CRYSTALS_PER_MODULE{ 2, 4, 5 };
32+
constexpr uint32_t NUM_MODULES_ALONG_RING{ 20 };
33+
constexpr uint32_t NUM_MODULES_ALONG_AXIS{ 2 };
34+
constexpr float MODULE_AXIS_SPACING{ (NUM_CRYSTALS_PER_MODULE[2] + 4) * CRYSTAL_LENGTH[2] };
35+
2936
constexpr uint32_t NUMBER_OF_TIME_BLOCKS = 6;
3037
constexpr float COUNT_RATE = 500.F;
3138

@@ -62,8 +69,8 @@ get_detector_module()
6269
for (int rep2 = 0; rep2 < N2; ++rep2)
6370
{
6471
petsird::RigidTransformation transform{ { { 1.0, 0.0, 0.0, RADIUS + rep0 * CRYSTAL_LENGTH[0] },
65-
{ 0.0, 1.0, 0.0, rep1 * CRYSTAL_LENGTH[1] },
66-
{ 0.0, 0.0, 1.0, rep2 * CRYSTAL_LENGTH[2] } } };
72+
{ 0.0, 1.0, 0.0, (rep1 - N1 / 2) * CRYSTAL_LENGTH[1] },
73+
{ 0.0, 0.0, 1.0, (rep2 - N2 / 2) * CRYSTAL_LENGTH[2] } } };
6774
rep_volume.transforms.push_back(transform);
6875
rep_volume.ids.push_back(rep0 + N0 * (rep1 + N1 * rep2));
6976
}
@@ -85,25 +92,95 @@ get_scanner_geometry()
8592
rep_module.object = get_detector_module();
8693
int module_id = 0;
8794
std::vector<float> angles;
88-
for (int i = 0; i < 10; ++i)
95+
for (unsigned int i = 0; i < NUM_MODULES_ALONG_RING; ++i)
8996
{
90-
angles.push_back(static_cast<float>(2 * M_PI * i / 10));
97+
angles.push_back(static_cast<float>((2 * M_PI * i) / NUM_MODULES_ALONG_RING));
9198
}
9299
for (auto angle : angles)
93-
{
94-
petsird::RigidTransformation transform{ { { std::cos(angle), std::sin(angle), 0.F, 0.F },
95-
{ -std::sin(angle), std::cos(angle), 0.F, 0.F },
96-
{ 0.F, 0.F, 1.F, 0.F } } };
97-
rep_module.ids.push_back(module_id++);
98-
rep_module.transforms.push_back(transform);
99-
}
100+
for (unsigned ax_mod = 0; ax_mod < NUM_MODULES_ALONG_AXIS; ++ax_mod)
101+
{
102+
petsird::RigidTransformation transform{ { { std::cos(angle), std::sin(angle), 0.F, 0.F },
103+
{ -std::sin(angle), std::cos(angle), 0.F, 0.F },
104+
{ 0.F, 0.F, 1.F, MODULE_AXIS_SPACING * ax_mod } } };
105+
rep_module.ids.push_back(module_id++);
106+
rep_module.transforms.push_back(transform);
107+
}
100108
}
101109
petsird::ScannerGeometry scanner_geometry;
102110
scanner_geometry.replicated_modules.push_back(rep_module);
103111
scanner_geometry.ids.push_back(0);
104112
return scanner_geometry;
105113
}
106114

115+
petsird::DetectionEfficiencies
116+
get_detection_efficiencies(const petsird::ScannerInformation& scanner)
117+
{
118+
const auto num_det_els = petsird_helpers::get_num_det_els(scanner.scanner_geometry);
119+
petsird::DetectionEfficiencies detection_efficiencies;
120+
121+
detection_efficiencies.det_el_efficiencies = xt::ones<float>({ num_det_els, scanner.NumberOfEnergyBins() });
122+
123+
// only 1 type of module in the current scanner
124+
assert(scanner.scanner_geometry.replicated_modules.size() == 1);
125+
const auto& rep_module = scanner.scanner_geometry.replicated_modules[0];
126+
const auto num_modules = rep_module.transforms.size();
127+
128+
// We will only use rotational symmetries (no translation along the axis yet)
129+
// We also assume all module-pairs are in coincidence, except those with the same angle.
130+
// Writing a module number as (z-position, angle):
131+
// eff((z1,a1), (z2, a2)) == eff((z1,0), (z2, abs(a2-a1)))
132+
// or in linear indices
133+
// eff(z1 + NZ * a1, z2 + NZ * a2) == eff(z1, z2 + NZ * abs(a2 - a1))
134+
// (coincident) SGIDs need to start from 0, so ignoring self-coincident angles
135+
constexpr auto num_SGIDs = NUM_MODULES_ALONG_AXIS * NUM_MODULES_ALONG_AXIS * (NUM_MODULES_ALONG_RING - 1);
136+
// SGID = z1 + NZ * (z2 + NZ * abs(a2 - a1) - 1)
137+
constexpr auto NZ = NUM_MODULES_ALONG_AXIS;
138+
detection_efficiencies.module_pair_sgidlut = yardl::NDArray<int, 2>({ num_modules, num_modules });
139+
auto& module_pair_SGID_LUT = *detection_efficiencies.module_pair_sgidlut;
140+
for (unsigned int mod1 = 0; mod1 < num_modules; ++mod1)
141+
{
142+
for (unsigned int mod2 = 0; mod2 < num_modules; ++mod2)
143+
{
144+
const auto z1 = mod1 % NZ;
145+
const auto a1 = mod1 / NZ;
146+
const auto z2 = mod2 % NZ;
147+
const auto a2 = mod2 / NZ;
148+
if (a1 == a2)
149+
{
150+
module_pair_SGID_LUT(mod1, mod2) = -1;
151+
}
152+
else
153+
{
154+
module_pair_SGID_LUT(mod1, mod2) = z1 + NZ * (z2 + NZ * (std::abs(int(a2) - int(a1)) - 1));
155+
}
156+
}
157+
}
158+
// assert(module_pair_SGID_LUT).max() == num_SGIDs - 1);
159+
160+
// assign an empty vector first, and reserve correct size
161+
detection_efficiencies.module_pair_efficiencies_vector = petsird::ModulePairEfficienciesVector();
162+
detection_efficiencies.module_pair_efficiencies_vector->reserve(num_SGIDs);
163+
164+
assert(rep_module.object.detecting_elements.size() == 1);
165+
const auto& detecting_elements = rep_module.object.detecting_elements[0];
166+
const auto num_det_els_in_module = detecting_elements.transforms.size();
167+
for (unsigned int SGID = 0; SGID < num_SGIDs; ++SGID)
168+
{
169+
// extract first module_pair for this SGID. However, as this currently unused, it is commented out
170+
// const auto& module_pair = *std::find(module_pair_SGID_LUT.begin(), module_pair_SGID_LUT.end(), SGID);
171+
petsird::ModulePairEfficiencies module_pair_efficiencies;
172+
module_pair_efficiencies.values = yardl::NDArray<float, 4>(
173+
{ num_det_els_in_module, scanner.NumberOfEnergyBins(), num_det_els_in_module, scanner.NumberOfEnergyBins() });
174+
// give some (non-physical) value
175+
module_pair_efficiencies.values.fill(SGID);
176+
module_pair_efficiencies.sgid = SGID;
177+
detection_efficiencies.module_pair_efficiencies_vector->emplace_back(module_pair_efficiencies);
178+
assert(detection_efficiencies.module_pair_efficiencies_vector->size() == unsigned(SGID + 1));
179+
}
180+
181+
return detection_efficiencies;
182+
}
183+
107184
petsird::ScannerInformation
108185
get_scanner_info()
109186
{
@@ -133,6 +210,8 @@ get_scanner_info()
133210
scanner_info.event_time_block_duration = 1.F; // ms
134211
}
135212

213+
scanner_info.detection_efficiencies = get_detection_efficiencies(scanner_info);
214+
136215
return scanner_info;
137216
}
138217

@@ -154,12 +233,13 @@ get_header()
154233
}
155234

156235
// return pair of integers between 0 and max
157-
std::pair<int, int>
236+
std::array<unsigned, 2>
158237
get_random_pair(int max)
159238
{
160-
int a = rand() % max;
161-
int b = rand() % max;
162-
return std::make_pair(a, b);
239+
unsigned a = rand() % max;
240+
unsigned b = rand() % max;
241+
std::array<unsigned, 2> p{ a, b };
242+
return p;
163243
}
164244

165245
uint32_t
@@ -175,16 +255,27 @@ get_random_tof_value()
175255
}
176256

177257
std::vector<petsird::CoincidenceEvent>
178-
get_events(const petsird::Header&, std::size_t num_events)
258+
get_events(const petsird::Header& header, std::size_t num_events)
179259
{
180260
std::vector<petsird::CoincidenceEvent> events;
181261
events.reserve(num_events);
262+
const auto num_det_els = petsird_helpers::get_num_det_els(header.scanner.scanner_geometry);
182263
for (std::size_t i = 0; i < num_events; ++i)
183264
{
184-
const auto detectors = get_random_pair(1); // TODO header.scanner.NumberOfDetectors());
185265
petsird::CoincidenceEvent e;
186-
e.detector_ids[0] = detectors.first;
187-
e.detector_ids[1] = detectors.second;
266+
// Generate random detector_ids, where the corresponding modules are distinct
267+
while (true)
268+
{
269+
e.detector_ids = get_random_pair(num_det_els);
270+
const auto mod_and_els = petsird_helpers::get_module_and_element(header.scanner.scanner_geometry, e.detector_ids);
271+
if (!header.scanner.detection_efficiencies.module_pair_sgidlut /* there is no LUT */
272+
|| ((*header.scanner.detection_efficiencies.module_pair_sgidlut)(mod_and_els[0].module, mod_and_els[1].module)
273+
>= 0))
274+
{
275+
// in coincidence, we can get out of the loop
276+
break;
277+
}
278+
}
188279
e.energy_indices[0] = get_random_energy_value();
189280
e.energy_indices[1] = get_random_energy_value();
190281
e.tof_idx = get_random_tof_value();

cpp/petsird_helpers.h

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
Copyright (C) 2024 University College London
3+
4+
SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
#include "generated/types.h"
8+
9+
namespace petsird_helpers
10+
{
11+
12+
using namespace petsird;
13+
14+
inline std::size_t
15+
get_num_det_els(const ScannerGeometry& scanner_geometry)
16+
{
17+
std::size_t num_det_els = 0;
18+
for (const auto& rep_module : scanner_geometry.replicated_modules)
19+
{
20+
const auto& det_els = rep_module.object.detecting_elements;
21+
for (const auto& rep_volume : det_els)
22+
{
23+
num_det_els += rep_volume.transforms.size() * rep_module.transforms.size();
24+
}
25+
}
26+
return num_det_els;
27+
}
28+
29+
struct ModuleAndElement
30+
{
31+
int module;
32+
int el;
33+
};
34+
35+
template <class T>
36+
inline std::vector<ModuleAndElement>
37+
get_module_and_element(const ScannerGeometry& scanner_geometry, const T& scanner_det_ids)
38+
{
39+
assert(scanner_geometry.replicated_modules.size() == 1);
40+
const auto& rep_module = scanner_geometry.replicated_modules[0];
41+
assert(rep_module.object.detecting_elements.size() == 1);
42+
int num_modules = rep_module.transforms.size();
43+
44+
std::vector<ModuleAndElement> result;
45+
for (int det : scanner_det_ids)
46+
{
47+
result.push_back({ det % num_modules, det / num_modules });
48+
}
49+
return result;
50+
}
51+
52+
inline float
53+
get_detection_efficiency(const ScannerInformation& scanner, const CoincidenceEvent& event)
54+
{
55+
float eff = 1.0F;
56+
const auto& det_el_efficiencies = scanner.detection_efficiencies.det_el_efficiencies;
57+
if (!det_el_efficiencies)
58+
{
59+
eff *= ((*det_el_efficiencies)(event.detector_ids[0], event.energy_indices[0])
60+
* (*det_el_efficiencies)(event.detector_ids[1], event.energy_indices[1]));
61+
}
62+
const auto& module_pair_efficiencies_vector = scanner.detection_efficiencies.module_pair_efficiencies_vector;
63+
if (!module_pair_efficiencies_vector)
64+
{
65+
const auto& module_pair_SGID_LUT = scanner.detection_efficiencies.module_pair_sgidlut;
66+
assert(!module_pair_SGID_LUT);
67+
68+
const auto mod_and_els = get_module_and_element(scanner.scanner_geometry, event.detector_ids);
69+
assert(scanner.scanner_geometry.replicated_modules.size() == 1);
70+
const int SGID = (*module_pair_SGID_LUT)(mod_and_els[0].module, mod_and_els[1].module);
71+
assert(SGID >= 0);
72+
73+
const auto& module_pair_efficiencies = (*module_pair_efficiencies_vector)[SGID];
74+
assert(module_pair_efficiencies.sgid == static_cast<unsigned>(SGID));
75+
eff *= module_pair_efficiencies.values(mod_and_els[0].el, event.energy_indices[0], mod_and_els[1].el,
76+
event.energy_indices[1]);
77+
}
78+
return eff;
79+
}
80+
81+
} // namespace petsird_helpers

model/DetectionEfficiencies.yml

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Detection efficiencies for every detecting element and energy window
2+
# If size along energyBinIdx is 1, the effiencies are assumed to be the same for every energy bin.
3+
# Constraint: size(DetElEfficiencies, "energyBinIdx") == scannerInformation.numberOfEnergyBins or 1
4+
DetElEfficiencies: float[detElIdx, energyBinIdx]
5+
6+
# Efficiency for two detecting elements (det_els) in a pair of modules.
7+
# This is one component (often called "geometric") of the detection efficiency model.
8+
# If size along energyBinIdx is 1, the effiencies are assumed to be the same for every energy bin.
9+
ModulePairEfficiencies: !record
10+
fields:
11+
# Detection efficiency for a pair of detecting elements
12+
# detElIdx1 and detElIdx2 run from 0 to the number of det_els in each module
13+
values: float[detElIdx1, energyBinIdx1, detElIdx2, energyBinIdx2]
14+
# Symmetry Group Identifier (SGID)
15+
# This should be a number between 0 and numberOfSGIDs-1
16+
sgid: uint
17+
18+
19+
# Lookup table for SGIDs
20+
# For every module pair, give the SGID. If -1, the module-pair is not in coincidence.
21+
# Values run from -1 ... (numberOfSGIDs-1)
22+
ModulePairSGIDLUT: int[moduleIdx1, moduleIdx2]
23+
24+
ModulePairEfficienciesVector: ModulePairEfficiencies*
25+
26+
# Component-based information on detection efficiencies
27+
# This encodes a simple model for the detection efficiency of (true) coincidences
28+
# consisting of the product of the efficiency of the two detecting elements (det_els)
29+
# and a (geometric) component determined by their location in the two modules.
30+
# The former are stored in detElEfficiencies, and the latter in modulePairEfficienciesVector
31+
# (a list of ModulePairEfficiencies, each entry corresponding to a module pair).
32+
#
33+
# Most PET scanners have some kind of geometric symmetry, e.g. rotation over a full
34+
# module, or translation along the axis of the scanner. Module-pairs that are related
35+
# by such a symmetry often have the same geometric detection efficiencies. PETSIRD
36+
# calls this a "symmetry group" (SG). Each SG had an identifier (SGID). To save memory,
37+
# the modulePairEfficienciesVector needs to contain only one of element for each SGID.
38+
# The SGID for a module-pair can be found in modulePairSGIDLUT.
39+
#
40+
# Finding the total detection efficiency therefore follows these steps:
41+
# (the following pseudo-code ignores energy bins for simplicity)
42+
# 1. find modules for each det_el
43+
# 2. find det_el indices "inside" each module
44+
# 3. SGID = modulePairSGIDLUT[mod1, mod1]
45+
# 4. if (SGID < 0) return 0
46+
# 5. module_pair_efficiencies = modulePairEfficienciesVector[SGID]
47+
# 6. return detElEfficiencies[det_el1] * detElEfficiencies[det_el2] * module_pair_efficiencies[det_el_in_mod1, det_el_in_mod2]
48+
#
49+
# If either of the components is not present, its value is considered to be 1.
50+
DetectionEfficiencies: !record
51+
fields:
52+
# Detection efficiencies for every detecting element
53+
detElEfficiencies: DetElEfficiencies?
54+
# Lookup table for SGIDs.
55+
# Also indicates if coincidences between a module-pair are recorded.
56+
modulePairSGIDLUT: ModulePairSGIDLUT?
57+
# Vector of all modulePairEfficiencies (one for each SGID)
58+
# Constraint: size(modulePairEfficienciesVector) == max(modulePairSGIDLUT) + 1
59+
modulePairEfficienciesVector: ModulePairEfficienciesVector?

model/ScannerInformation.yml

+3
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ ScannerInformation: !record
5959
# Encode how the scanner handles multiple coincidences
6060
coincidencePolicy: CoincidencePolicy
6161

62+
# coincidence detection efficiencies
63+
detectionEfficiencies: DetectionEfficiencies
64+
6265
computedFields:
6366
numberOfTOFBins: size(tofBinEdges)-1
6467
numberOfEnergyBins: size(energyBinEdges)-1

0 commit comments

Comments
 (0)