Skip to content

Commit d765ee2

Browse files
Merge pull request #23 from SiLab-Bonn/unit_tests
Unit tests for online analysis
2 parents a6d5546 + 9ea82e8 commit d765ee2

7 files changed

Lines changed: 154 additions & 4 deletions

pymosa/m26_configuration.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
run_number : # Base run number, will be automatically increased; if none is given, generate filename
33
output_folder : # Output folder for the telescope data; if none is given, the current working directory is used
44
m26_configuration_file : # Configuration file for Mimosa26 sensors, default: 'm26_config/m26_threshold_8.yaml'
5+
m26_jtag_configuration : True # Send Mimosa26 configuration via JTAG, default: True
6+
no_data_timeout : 30 # No data timeout after which the scan will be aborted, in seconds; if 0, the timeout is disabled
57
scan_timeout : 0 # Timeout after which the scan will be stopped, in seconds; if 0, the timeout is disabled; use Ctrl-C to stop run
68
max_triggers : 0 # Maximum number of triggers; if 0, there is no limit on the number of triggers; use Ctrl-C to stop run
79
send_data : 'tcp://127.0.0.1:8500' # TCP address to which the telescope data is send; to allow incoming connections on all interfaces use 0.0.0.0

pymosa/noise_occupancy_scan.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def take_data(self, update_rate=1):
3535
try:
3636
self.pbar.update(update_rate)
3737
except ValueError:
38-
pass
38+
pass
3939

4040
self.pbar.close()
4141

pymosa/online.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,12 @@ def is_frame_trailer1(word, plane): # Check if frame trailer1 word for the actu
129129
return (0x0000ffff & word) == (0xaa50 | plane)
130130

131131

132+
# Trigger words
133+
@njit
134+
def is_trigger_word(word): # Check if TLU word (trigger)
135+
return (0x80000000 & word) == 0x80000000
136+
137+
132138
@njit
133139
def histogram(raw_data, occ_hist, m26_frame_ids, m26_frame_length, m26_data_loss, m26_word_index, m26_timestamps, last_m26_timestamps, m26_n_words, m26_rows, m26_frame_status, last_completed_m26_frame_ids):
134140
''' Raw data to 2D occupancy histogram '''
@@ -230,8 +236,11 @@ def histogram(raw_data, occ_hist, m26_frame_ids, m26_frame_length, m26_data_loss
230236
break
231237
# Fill occupancy hist
232238
occ_hist[column + k, m26_rows[plane_id], plane_id] += 1
233-
else: # Raw data word is TLU/trigger word
239+
elif is_trigger_word(raw_data_word): # Raw data word is TLU/trigger word
234240
pass # Not needed here
241+
else: # Raw data contains unknown word, neither M26 nor TLU word
242+
for tmp_plane_index in range(6):
243+
m26_data_loss[tmp_plane_index] = True
235244

236245
return m26_frame_ids, m26_frame_length, m26_data_loss, m26_word_index, m26_timestamps, last_m26_timestamps, m26_n_words, m26_rows, m26_frame_status, last_completed_m26_frame_ids
237246

@@ -313,14 +322,14 @@ def worker(self, raw_data_queue, shared_array_base, lock, stop):
313322
# Per frame variables
314323
m26_frame_ids = np.zeros(shape=(6, ), dtype=np.int64) # The Mimosa26 frame ID of the actual frame
315324
m26_frame_length = np.zeros(shape=(6, ), dtype=np.uint32) # The number of "useful" data words for the actual frame
316-
m26_data_loss = np.ones((6, ), dtype=np.bool) # The data loss status for the actual frame
325+
m26_data_loss = np.ones((6, ), dtype=np.bool_) # The data loss status for the actual frame
317326
m26_word_index = np.zeros(shape=(6, ), dtype=np.uint32) # The word index per device of the actual frame
318327
m26_timestamps = np.zeros(shape=(6, ), dtype=np.int64) # The timestamp for each plane (in units of 40 MHz)
319328
last_m26_timestamps = np.zeros(shape=(6, ), dtype=np.int64)
320329
m26_n_words = np.zeros(shape=(6, ), dtype=np.uint32) # The number of words containing column / row info
321330
m26_rows = np.zeros(shape=(6, ), dtype=np.uint32) # The actual readout row (rolling shutter)
322331
m26_frame_status = np.zeros(shape=(6, ), dtype=np.uint32) # The status flags for the actual frames
323-
last_completed_m26_frame_ids = np.full(shape=6, dtype=np.int64, fill_value=-1) # The status if the frame is complete for the actual frame
332+
last_completed_m26_frame_ids = -1 * np.ones(shape=6, dtype=np.int64) # The status if the frame is complete for the actual frame
324333
while not stop.is_set():
325334
try:
326335
raw_data = raw_data_queue.get(timeout=self._queue_timeout)

pymosa/tests/anemone_raw_data.h5

8.67 MB
Binary file not shown.
1.44 MB
Binary file not shown.
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
#
2+
# ------------------------------------------------------------
3+
# Copyright (c) All rights reserved
4+
# SiLab, Institute of Physics, University of Bonn
5+
# ------------------------------------------------------------
6+
#
7+
8+
import logging
9+
import os
10+
import threading
11+
import time
12+
13+
from mock import patch
14+
15+
import pytest
16+
import tables as tb
17+
import numpy as np
18+
import matplotlib
19+
matplotlib.use('Agg') # noqa: E402 Allow headless plotting
20+
21+
import pymosa # noqa: E731 E402
22+
from pymosa import online as oa # noqa: E402
23+
from pymosa.tests import utils # noqa: E40
24+
25+
26+
@pytest.fixture()
27+
def data_folder():
28+
pymosa_path = os.path.dirname(pymosa.__file__)
29+
print(os.path.abspath(os.path.join(pymosa_path, 'tests')))
30+
return os.path.abspath(os.path.join(pymosa_path, 'tests'))
31+
32+
33+
@pytest.fixture()
34+
def ana_log_messages():
35+
ana_logger = logging.getLogger('OnlineAnalysis')
36+
_ana_log_handler = utils.MockLoggingHandler(level='DEBUG')
37+
ana_logger.addHandler(_ana_log_handler)
38+
ana_log_messages = _ana_log_handler.messages
39+
yield ana_log_messages
40+
ana_logger.removeHandler(_ana_log_handler) # cleanup
41+
42+
43+
@pytest.fixture()
44+
def occ_hist_oa():
45+
h = oa.OccupancyHistogramming()
46+
yield h
47+
h.close()
48+
del h
49+
50+
51+
def get_raw_data(raw_data_file):
52+
''' Yield data of one readout
53+
54+
Delay return if replay is too fast
55+
'''
56+
with tb.open_file(raw_data_file, mode="r") as in_file_h5:
57+
meta_data = in_file_h5.root.meta_data[:]
58+
raw_data = in_file_h5.root.raw_data
59+
n_readouts = meta_data.shape[0]
60+
61+
for i in range(n_readouts):
62+
# Raw data indeces of readout
63+
i_start = meta_data['index_start'][i]
64+
i_stop = meta_data['index_stop'][i]
65+
66+
yield raw_data[i_start:i_stop]
67+
68+
69+
def test_occupancy_histogramming(data_folder, occ_hist_oa):
70+
''' Test online occupancy histogramming '''
71+
72+
raw_data_file = os.path.join(data_folder, 'anemone_raw_data.h5')
73+
raw_data_file_result = os.path.join(data_folder, 'anemone_raw_data_interpreted_result.h5')
74+
for words in get_raw_data(raw_data_file):
75+
occ_hist_oa.add(words)
76+
77+
time.sleep(1.0)
78+
79+
occ_hist = occ_hist_oa.get()
80+
81+
with tb.open_file(raw_data_file_result) as in_file:
82+
for i in range(6):
83+
occ_hist_exptected = in_file.get_node(in_file.root, 'HistOcc_plane%d' % (i + 1))
84+
assert(np.array_equal(occ_hist_exptected[:, :], occ_hist[:, :, i]))
85+
86+
87+
# def test_occupancy_histogramming_errors(data_folder, occ_hist_oa, ana_log_messages):
88+
# # Check error message when requesting histogram before analysis finished
89+
90+
# def add_data():
91+
# for words in get_raw_data(raw_data_file):
92+
# for _ in range(100):
93+
# occ_hist_oa.add(words)
94+
95+
# raw_data_file = os.path.join(data_folder, 'anemone_raw_data.h5')
96+
# occ_hist_oa.reset()
97+
# thread = threading.Thread(target=add_data)
98+
# thread.start()
99+
# time.sleep(0.5) # wait for first data to be added to queue
100+
# occ_hist_oa.get(wait=False)
101+
# thread.join()
102+
# # Check that warning was given
103+
# assert('Getting histogram while analyzing data' in ana_log_messages['warning'])
104+
105+
106+
if __name__ == '__main__':
107+
import pytest
108+
pytest.main(['-s', __file__])

pymosa/tests/utils.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import logging
2+
3+
4+
class MockLoggingHandler(logging.Handler):
5+
"""Mock logging handler to check for expected logs.
6+
7+
Messages are available from an instance's ``messages`` dict, in order, indexed by
8+
a lowercase log level string (e.g., 'debug', 'info', etc.).
9+
10+
https://stackoverflow.com/questions/899067/how-should-i-verify-a-log-message-when-testing-python-code-under-nose
11+
"""
12+
13+
def __init__(self, *args, **kwargs):
14+
self.messages = {'debug': [], 'info': [], 'notice': [], 'success': [], 'warning': [], 'error': [],
15+
'critical': []}
16+
super(MockLoggingHandler, self).__init__(*args, **kwargs)
17+
18+
def emit(self, record):
19+
"Store a message from ``record`` in the instance's ``messages`` dict."
20+
try:
21+
self.messages[record.levelname.lower()].append(record.getMessage())
22+
except Exception:
23+
self.handleError(record)
24+
25+
def reset(self):
26+
self.acquire()
27+
try:
28+
for message_list in self.messages.values():
29+
message_list.clear()
30+
finally:
31+
self.release()

0 commit comments

Comments
 (0)