Skip to content

Commit 1751f74

Browse files
Merge pull request #70 from LIBRA-project/rem/class-structure-compass-measurements
Class structure for COMPASS measurements
2 parents bf88ee2 + 1c969ce commit 1751f74

File tree

9 files changed

+292
-7
lines changed

9 files changed

+292
-7
lines changed

libra_toolbox/neutron_detection/activation_foils/compass.py

Lines changed: 144 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import numpy as np
2+
from numpy.typing import NDArray
23
import os
34
from pathlib import Path
45
import pandas as pd
5-
from typing import Tuple, Dict
6+
from typing import Tuple, Dict, List, Union
67
import datetime
78
import uproot
9+
import glob
10+
11+
import warnings
812

913

1014
def get_channel(filename):
@@ -122,7 +126,9 @@ def get_start_stop_time(directory: str) -> Tuple[datetime.datetime, datetime.dat
122126
with open(info_file, "r") as file:
123127
lines = file.readlines()
124128
else:
125-
raise FileNotFoundError(f"Could not find run.info file in {directory}")
129+
raise FileNotFoundError(
130+
f"Could not find run.info file in parent directory {Path(directory).parent}"
131+
)
126132

127133
start_time, stop_time = None, None
128134
for line in lines:
@@ -141,16 +147,147 @@ def get_start_stop_time(directory: str) -> Tuple[datetime.datetime, datetime.dat
141147
return start_time, stop_time
142148

143149

144-
def get_live_time_from_root(filename, channel: int):
150+
def get_live_time_from_root(root_filename: str, channel: int) -> Tuple[float, float]:
145151
"""
146152
Gets live and real count time from Compass root file.
147-
Times are given in seconds.
148153
Live time is defined as the difference between the actual time that
149154
a count is occurring and the "dead time," in which the output of detector
150-
pulses is saturated such that additional signals cannot be processed.
151-
"""
155+
pulses is saturated such that additional signals cannot be processed."""
152156

153-
with uproot.open(filename) as root_file:
157+
with uproot.open(root_filename) as root_file:
154158
live_count_time = root_file[f"LiveTime_{channel}"].members["fMilliSec"] / 1000
155159
real_count_time = root_file[f"RealTime_{channel}"].members["fMilliSec"] / 1000
156160
return live_count_time, real_count_time
161+
162+
163+
class Detector:
164+
events: NDArray[Tuple[float, float]] # type: ignore # Array of (time in ps, energy) pairs
165+
channel_nb: int
166+
live_count_time: float
167+
real_count_time: float
168+
169+
def __init__(self, channel_nb) -> None:
170+
"""
171+
Initialize a Detector object.
172+
Args:
173+
channel_nb: channel number of the detector
174+
"""
175+
self.channel_nb = channel_nb
176+
self.events = np.empty((0, 2)) # Initialize as empty 2D array with 2 columns
177+
self.live_count_time = None
178+
self.real_count_time = None
179+
180+
def get_energy_hist(
181+
self, bins: Union[int, NDArray[np.float64], None] = None
182+
) -> Tuple[np.ndarray, np.ndarray]:
183+
"""
184+
Get the energy histogram of the detector events.
185+
Args:
186+
bins: number of bins, can be a numpy array, if None, it will be set to the
187+
maximum energy value in the events (one bin per energy value)
188+
Returns:
189+
Tuple of histogram values and bin edges
190+
"""
191+
192+
energy_values = self.events[:, 1].copy()
193+
time_values = self.events[:, 0].copy()
194+
195+
# sort data based on timestamp
196+
inds = np.argsort(time_values)
197+
time_values = time_values[inds]
198+
energy_values = energy_values[inds]
199+
200+
energy_values = np.nan_to_num(energy_values, nan=0)
201+
202+
if bins is None:
203+
bins = int(np.nanmax(energy_values))
204+
205+
return np.histogram(energy_values, bins=bins)
206+
207+
208+
class Measurement:
209+
start_time: datetime.datetime
210+
stop_time: datetime.datetime
211+
name: str
212+
detectors: List[Detector]
213+
214+
def __init__(self, name: str) -> None:
215+
"""
216+
Initialize a Measurement object.
217+
Args:
218+
name: name of the measurement
219+
"""
220+
self.start_time = None
221+
self.stop_time = None
222+
self.name = name
223+
self.detectors = []
224+
225+
@classmethod
226+
def from_directory(
227+
cls, source_dir: str, name: str, info_file_optional: bool = False
228+
) -> "Measurement":
229+
"""
230+
Create a Measurement object from a directory containing Compass data.
231+
Args:
232+
source_dir: directory containing Compass data
233+
name: name of the measurement
234+
info_file_optional: if True, the function will not raise an error
235+
if the run.info file is not found
236+
Returns:
237+
Measurement object
238+
"""
239+
measurement_object = cls(name=name)
240+
241+
# Get events
242+
time_values, energy_values = get_events(source_dir)
243+
244+
# Get start and stop time
245+
try:
246+
start_time, stop_time = get_start_stop_time(source_dir)
247+
measurement_object.start_time = start_time
248+
measurement_object.stop_time = stop_time
249+
except FileNotFoundError as e:
250+
if info_file_optional:
251+
warnings.warn(
252+
"run.info file not found. Assuming start and stop time are not needed."
253+
)
254+
else:
255+
raise FileNotFoundError(e)
256+
257+
# Create detectors
258+
detectors = [Detector(channel_nb=nb) for nb in time_values.keys()]
259+
260+
# Get live and real count times
261+
all_root_filenames = glob.glob(os.path.join(source_dir, "*.root"))
262+
if len(all_root_filenames) == 1:
263+
root_filename = all_root_filenames[0]
264+
else:
265+
root_filename = None
266+
print("No root file found, assuming all counts are live")
267+
268+
for detector in detectors:
269+
detector.events = np.column_stack(
270+
(time_values[detector.channel_nb], energy_values[detector.channel_nb])
271+
)
272+
273+
if root_filename:
274+
live_count_time, real_count_time = get_live_time_from_root(
275+
root_filename, detector.channel_nb
276+
)
277+
detector.live_count_time = live_count_time
278+
detector.real_count_time = real_count_time
279+
else:
280+
real_count_time = (stop_time - start_time).total_seconds()
281+
# Assume first and last event correspond to start and stop time of live counts
282+
# and convert from picoseconds to seconds
283+
ps_to_seconds = 1e-12
284+
live_count_time = (
285+
time_values[detector.channel_nb][-1]
286+
- time_values[detector.channel_nb][0]
287+
) * ps_to_seconds
288+
detector.live_count_time = live_count_time
289+
detector.real_count_time = real_count_time
290+
291+
measurement_object.detectors = detectors
292+
293+
return measurement_object
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
BOARD;CHANNEL;TIMETAG;ENERGY;ENERGYSHORT;FLAGS
2+
0;5;234859459;2;2;0x4000
3+
0;5;421999310;0;1;0x4000
4+
0;5;535148093;1237;810;0x4000
5+
0;5;1623550122;589;396;0x4000
6+
0;5;5997211248;375;251;0x4000
7+
0;5;6685836624;515;340;0x4000
8+
0;5;11116032249;568;380;0x4000
9+
0;5;11281099382;1;0;0x4000
10+
0;5;12783039350;5;0;0x4000
11+
0;5;18306299412;2;0;0x4000
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
0;5;234859459;2;2;0x4000
2+
0;5;421999310;0;1;0x4000
3+
0;5;535148093;1237;810;0x4000
4+
0;5;1623550122;589;396;0x4000
Binary file not shown.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
id=Co60_0_872uCi_19Marc2014_250319_run3
2+
time.start=2025/03/19 09:47:46.724-0400
3+
time.stop=2025/03/19 09:53:05.651-0400
4+
time.real=00:05:18
5+
board.0-14-292.readout.rate=51.149 kb/s
6+
board.0-14-292.4.rejections.singles=0.0
7+
board.0-14-292.4.rejections.pileup=0.0
8+
board.0-14-292.4.rejections.saturation=1301.87
9+
board.0-14-292.4.rejections.energy=0.0
10+
board.0-14-292.4.rejections.psd=0.0
11+
board.0-14-292.4.rejections.timedistribution=0.0
12+
board.0-14-292.4.throughput=2627.31
13+
board.0-14-292.4.icr=3059.24
14+
board.0-14-292.4.ocr=1293.91
15+
board.0-14-292.4.calibration.energy.c0=0.0
16+
board.0-14-292.4.calibration.energy.c1=1.0
17+
board.0-14-292.4.calibration.energy.c2=0.0
18+
board.0-14-292.4.calibration.energy.uom=keV
19+
board.0-14-292.5.rejections.singles=0.0
20+
board.0-14-292.5.rejections.pileup=0.0
21+
board.0-14-292.5.rejections.saturation=717.247
22+
board.0-14-292.5.rejections.energy=0.0
23+
board.0-14-292.5.rejections.psd=0.0
24+
board.0-14-292.5.rejections.timedistribution=0.0
25+
board.0-14-292.5.throughput=1694.65
26+
board.0-14-292.5.icr=1703.71
27+
board.0-14-292.5.ocr=984.476
28+
board.0-14-292.5.calibration.energy.c0=0.0
29+
board.0-14-292.5.calibration.energy.c1=1.0
30+
board.0-14-292.5.calibration.energy.c2=0.0
31+
board.0-14-292.5.calibration.energy.uom=keV
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
BOARD;CHANNEL;TIMETAG;ENERGY;ENERGYSHORT;FLAGS
2+
0;5;234859459;2;2;0x4000
3+
0;5;421999310;0;1;0x4000
4+
0;5;535148093;1237;810;0x4000
5+
0;5;1623550122;589;396;0x4000
6+
0;5;5997211248;375;251;0x4000
7+
0;5;6685836624;515;340;0x4000
8+
0;5;11116032249;568;380;0x4000
9+
0;5;11281099382;1;0;0x4000
10+
0;5;12783039350;5;0;0x4000
11+
0;5;18306299412;2;0;0x4000
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
0;5;234859459;2;2;0x4000
2+
0;5;421999310;0;1;0x4000
3+
0;5;535148093;1237;810;0x4000
4+
0;5;1623550122;589;396;0x4000
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
id=Co60_0_872uCi_19Marc2014_250319_run3
2+
time.start=2025/03/19 09:47:46.724-0400
3+
time.stop=2025/03/19 09:53:05.651-0400
4+
time.real=00:05:18
5+
board.0-14-292.readout.rate=51.149 kb/s
6+
board.0-14-292.4.rejections.singles=0.0
7+
board.0-14-292.4.rejections.pileup=0.0
8+
board.0-14-292.4.rejections.saturation=1301.87
9+
board.0-14-292.4.rejections.energy=0.0
10+
board.0-14-292.4.rejections.psd=0.0
11+
board.0-14-292.4.rejections.timedistribution=0.0
12+
board.0-14-292.4.throughput=2627.31
13+
board.0-14-292.4.icr=3059.24
14+
board.0-14-292.4.ocr=1293.91
15+
board.0-14-292.4.calibration.energy.c0=0.0
16+
board.0-14-292.4.calibration.energy.c1=1.0
17+
board.0-14-292.4.calibration.energy.c2=0.0
18+
board.0-14-292.4.calibration.energy.uom=keV
19+
board.0-14-292.5.rejections.singles=0.0
20+
board.0-14-292.5.rejections.pileup=0.0
21+
board.0-14-292.5.rejections.saturation=717.247
22+
board.0-14-292.5.rejections.energy=0.0
23+
board.0-14-292.5.rejections.psd=0.0
24+
board.0-14-292.5.rejections.timedistribution=0.0
25+
board.0-14-292.5.throughput=1694.65
26+
board.0-14-292.5.icr=1703.71
27+
board.0-14-292.5.ocr=984.476
28+
board.0-14-292.5.calibration.energy.c0=0.0
29+
board.0-14-292.5.calibration.energy.c1=1.0
30+
board.0-14-292.5.calibration.energy.c2=0.0
31+
board.0-14-292.5.calibration.energy.uom=keV

test/neutron_detection/test_compass.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,3 +275,59 @@ def test_get_live_time_from_root(root_filename, channel, live_time, real_time):
275275
)
276276
assert live_time_out == live_time
277277
assert real_time_out == real_time
278+
279+
280+
@pytest.mark.parametrize("no_root", [True, False])
281+
def test_measurement_object_from_directory(no_root):
282+
"""
283+
Test the Measurement object creation from a directory.
284+
"""
285+
if no_root:
286+
test_directory = (
287+
Path(__file__).parent
288+
/ "compass_test_data/complete_measurement_no_root/data"
289+
)
290+
else:
291+
test_directory = (
292+
Path(__file__).parent / "compass_test_data/complete_measurement/data"
293+
)
294+
295+
measurement = compass.Measurement.from_directory(test_directory, name="test")
296+
297+
assert len(measurement.detectors) == 1
298+
assert isinstance(measurement.detectors[0], compass.Detector)
299+
assert measurement.detectors[0].channel_nb == 1
300+
301+
assert measurement.detectors[0].events.shape[1] == 2
302+
303+
measurement.detectors[0].get_energy_hist(bins=None)
304+
305+
306+
@pytest.mark.parametrize(
307+
"bins",
308+
[
309+
10,
310+
20,
311+
50,
312+
100,
313+
None,
314+
np.arange(0, 10, 1),
315+
np.linspace(0, 10, num=100),
316+
],
317+
)
318+
def test_detector_get_energy_hist(bins):
319+
"""
320+
Test the get_energy_hist method of the Detector class.
321+
"""
322+
my_detector = compass.Detector(channel_nb=1)
323+
my_detector.events = np.array(
324+
[
325+
[1, 2],
326+
[3, 4],
327+
[5, 6],
328+
[7, 8],
329+
[9, 10],
330+
]
331+
)
332+
333+
my_detector.get_energy_hist(bins=bins)

0 commit comments

Comments
 (0)