diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 1733762..03d06c0 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -13,17 +13,15 @@ jobs: - uses: actions/setup-python@v5 - name: Install dependencies run: | - pip install -e . - pip install pydata-sphinx-theme - pip install sphinx sphinx_rtd_theme myst_parser sphinx_mdinclude + pip install -e .[doc] - name: Sphinx build run: | - sphinx-build docs/source _build + sphinx-build docs/source build - name: Deploy to GitHub Pages uses: peaceiris/actions-gh-pages@v3 if: ${{github.event_name == 'push' && github.ref == 'refs/heads/main'}} with: publish_branch: gh-pages github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: _build/ + publish_dir: build/ force_orphan: true diff --git a/.github/workflows/pre_commit.yml b/.github/workflows/pre_commit.yml new file mode 100644 index 0000000..cbb7a50 --- /dev/null +++ b/.github/workflows/pre_commit.yml @@ -0,0 +1,14 @@ +name: pre-commit + +on: + pull_request: + push: + branches: [main, development] + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + - uses: pre-commit/action@v3.0.1 diff --git a/.github/workflows/workflows.yml b/.github/workflows/tests.yml similarity index 51% rename from .github/workflows/workflows.yml rename to .github/workflows/tests.yml index 398a450..54940d9 100644 --- a/.github/workflows/workflows.yml +++ b/.github/workflows/tests.yml @@ -1,4 +1,4 @@ -name: standard tests +name: tests on: pull_request: @@ -6,18 +6,16 @@ on: branches: [main, development] jobs: - pre-commit: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - - uses: pre-commit/action@v3.0.1 - - installation: + tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 - name: Install dependencies run: | - pip install -e . + pip install -e .[test] + pip install pytest + + - name: Test with pytest + run: | + pytest -sv diff --git a/.gitignore b/.gitignore index 6a878e7..577e439 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,7 @@ #debugging and data files test.ipynb *.h5 -!/aidatlu/test/interpreted_data.h5 -!/aidatlu/test/raw_data_test.h5 +!aidatlu/test/fixtures/* .vscode # Byte-compiled / optimized / DLL files diff --git a/README.md b/README.md index 5d79186..986e37e 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,14 @@ # AIDA-TLU -[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) +[![tests](https://github.com/SiLab-Bonn/aidatlu/actions/workflows/tests.yml/badge.svg)](https://github.com/SiLab-Bonn/aidatlu/actions/workflows/tests.yml) +[![pre-commit](https://github.com/SiLab-Bonn/aidatlu/actions/workflows/pre-commit.yml/badge.svg)](https://github.com/SiLab-Bonn/aidatlu/actions/workflows/pre-commit.yml) +[![documentation](https://github.com/SiLab-Bonn/aidatlu/actions/workflows/documentation.yml/badge.svg)](https://github.com/SiLab-Bonn/aidatlu/actions/workflows/documentation.yml) Repository for controlling the AIDA-2020 Trigger Logic Unit (TLU) with Python using uHAL bindings from [IPbus](https://ipbus.web.cern.ch/). The Python control software is based on [EUDAQ2](https://github.com/eudaq/eudaq/tree/master/user/tlu). The software is a lightweight version written in Python with a focus on readability and user-friendliness. Most user cases can be set with a .yaml configuration file and started by executing a single Python script. For a more in-depth look at the hardware components please take a look at the official [AIDA-2020 TLU project](https://ohwr.org/project/fmc-mtlu). +Additionally, take a look at the [documentation](https://silab-bonn.github.io/aidatlu/) for this software. # Installation ## IPbus You need to install [IPbus](https://ipbus.web.cern.ch/doc/user/html/software/install/compile.html) and its Python bindings to the desired interpreter. @@ -77,5 +80,23 @@ For more commands take a look at the python script aidatlu.py. All configurations are done by the use of a yaml file (tlu_configuration.yaml). +# Tests +With pytest (https://docs.pytest.org/en/7.4.x/) the AIDA TLU control program can be tested. +There is also an implemented AIDA-TLU mock, to allow tests and software development without hardware, +which also allows software development and testing without a working IPbus installation. +The mock is used as a default. -Additionally, take a look at the [documentation](https://silab-bonn.github.io/aidatlu/). +```bash + pytest -sv +``` +To test with connected hardware set an environment variable ```HW=True````: + +```bash + HW=True pytest -sv +``` + +You can also set the variable ```HW=False```` to test the mock TLU: + +```bash + HW=False pytest -sv +``` diff --git a/VERSION b/VERSION deleted file mode 100644 index 9459d4b..0000000 --- a/VERSION +++ /dev/null @@ -1 +0,0 @@ -1.1 diff --git a/aidatlu/README.md b/aidatlu/README.md index f63f724..75a608a 100644 --- a/aidatlu/README.md +++ b/aidatlu/README.md @@ -29,7 +29,8 @@ These operators are used in conjunction with the input channels CH1-CH6 and inte For example "(CH1 & ~CH2) & (CH3 | CH4 | CH5 | CH6)" produces a valid trigger, when CH1 and not CH2 triggers and when one of CH3, CH4, CH5 or CH6 triggers. An input channel that is not explicitly set to 'veto' or 'enabled' is automatically set to 'do not care'. -Trigger polarity controls if the TLU should trigger on a rising (0) or falling (1) edge of an incoming trigger signal. +TLU can trigger on a rising or falling edge. Trigger polarity is set using a string or boolean, +'rising' corresponds to false (0) and 'falling' to true (1) Each trigger input signal can be delayed and stretched by a given number of clock cycles. This is set with a list containing the number of clock cycles for every different trigger input. diff --git a/aidatlu/__init__.py b/aidatlu/__init__.py index e69de29..5becc17 100644 --- a/aidatlu/__init__.py +++ b/aidatlu/__init__.py @@ -0,0 +1 @@ +__version__ = "1.0.0" diff --git a/aidatlu/hardware/ioexpander_controller.py b/aidatlu/hardware/ioexpander_controller.py index b495a23..6dfa11b 100644 --- a/aidatlu/hardware/ioexpander_controller.py +++ b/aidatlu/hardware/ioexpander_controller.py @@ -58,46 +58,6 @@ def init_output_expander(self) -> None: ### LED Control ### - def test_leds(self, single=True) -> None: - """Test the 11 LEDs - - Args: - single (bool, optional): Test all possible RGB combinations for all LEDs. Defaults to True. - """ - self.log.info("Testing LEDs colors") - if single: - for color in [ - [0, 1, 1], - [1, 0, 1], - [1, 1, 0], - [1, 0, 0], - [0, 1, 0], - [0, 0, 1], - [0, 0, 0], - ]: - for i in range(11): - if i + 1 == 5: - pass - else: - self._set_led(i + 1, color) - time.sleep(0.1) - self.all_off() - time.sleep(0.05) - for color in [[0, 0, 1], [0, 1, 1], [1, 0, 1]]: - self._set_led(5, color) - time.sleep(0.15) - self.all_off() - time.sleep(0.1) - - else: - for color in ["w", "r", "g", "b"]: - self.log.info("Testing LEDs color: %s" % color) - - self.all_on(color) - time.sleep(1) - self.all_off() - time.sleep(1) - def all_on(self, color: str = "w") -> None: """Set all LEDs to same color diff --git a/aidatlu/main/config_parser.py b/aidatlu/main/config_parser.py index d405e51..280092e 100644 --- a/aidatlu/main/config_parser.py +++ b/aidatlu/main/config_parser.py @@ -4,11 +4,10 @@ class TLUConfigure: - def __init__(self, TLU, io_control, config_path) -> None: + def __init__(self, TLU, config_path) -> None: self.log = logger.setup_main_logger(__class__.__name__) self.tlu = TLU - self.io_control = io_control with open(config_path, "r") as file: self.conf = yaml.full_load(file) @@ -18,15 +17,7 @@ def configure(self) -> None: self.conf_dut() self.conf_trigger_inputs() self.conf_trigger_logic() - self.tlu.io_controller.clock_lemo_output( - self.conf["clock_lemo"]["enable_clock_lemo_output"] - ) - [ - self.tlu.dac_controller.set_voltage( - i + 1, self.conf["pmt_control"]["pmt_%s" % (i + 1)] - ) - for i in range(len(self.conf["pmt_control"])) - ] + self.conf_auxillary() self.tlu.set_enable_record_data(1) self.log.success("TLU configured") @@ -95,7 +86,7 @@ def get_data_handling(self) -> tuple: tuple: two bools, save and interpret data. """ - return self.conf["save_data"], self.conf["save_data"] + return self.conf["save_data"] def get_stop_condition(self) -> tuple: """Information about tlu stop condition. @@ -131,6 +122,18 @@ def get_zmq_connection(self) -> str: """ return self.conf["zmq_connection"] + def conf_auxillary(self): + """Configures PMT power outputs and clock LEMO I/O""" + self.tlu.io_controller.clock_lemo_output( + self.conf["clock_lemo"]["enable_clock_lemo_output"] + ) + [ + self.tlu.dac_controller.set_voltage( + i + 1, self.conf["pmt_control"]["pmt_%s" % (i + 1)] + ) + for i in range(len(self.conf["pmt_control"])) + ] + def conf_dut(self) -> None: """Parse the configuration for the DUT interface to the AIDATLU.""" dut = [0, 0, 0, 0] @@ -181,7 +184,11 @@ def conf_dut(self) -> None: self.tlu.dut_logic.set_dut_mask_mode( dut_mode[0] | dut_mode[1] | dut_mode[2] | dut_mode[3] ) - + self.log.debug("Set DUT mask: %s" % (dut[0] | dut[1] | dut[2] | dut[3])) + self.log.debug( + "Set DUT mask mode: %s" + % (dut_mode[0] | dut_mode[1] | dut_mode[2] | dut_mode[3]) + ) # Special configs self.tlu.dut_logic.set_dut_mask_mode_modifier(0) self.tlu.dut_logic.set_dut_ignore_busy(0) @@ -190,9 +197,18 @@ def conf_dut(self) -> None: def conf_trigger_logic(self) -> None: """Configures the trigger logic. So the trigger polarity and the trigger pulse length and stretch.""" - self.tlu.trigger_logic.set_trigger_polarity( - self.conf["trigger_inputs"]["trigger_polarity"]["polarity"] - ) + if self.conf["trigger_inputs"]["trigger_polarity"]["polarity"] in [ + 0, + "0", + "rising", + ]: + self.tlu.trigger_logic.set_trigger_polarity(0) + elif self.conf["trigger_inputs"]["trigger_polarity"]["polarity"] in [ + 1, + "1", + "falling", + ]: + self.tlu.trigger_logic.set_trigger_polarity(1) self.tlu.trigger_logic.set_pulse_stretch_pack( self.conf["trigger_inputs"]["trigger_signal_shape"]["stretch"] @@ -234,9 +250,9 @@ def conf_trigger_inputs(self) -> None: if trigger_configuration is not None: for trigger_led in range(6): if "~CH%i" % (trigger_led + 1) in trigger_configuration: - self.io_control.switch_led(trigger_led + 6, "r") + self.tlu.io_controller.switch_led(trigger_led + 6, "r") elif "CH%i" % (trigger_led + 1) in trigger_configuration: - self.io_control.switch_led(trigger_led + 6, "g") + self.tlu.io_controller.switch_led(trigger_led + 6, "g") long_word = 0x0 # Goes through all possible trigger combinations and checks if the combination is valid with the trigger logic. diff --git a/aidatlu/main/tlu.py b/aidatlu/main/tlu.py index fb41890..93936cb 100644 --- a/aidatlu/main/tlu.py +++ b/aidatlu/main/tlu.py @@ -5,7 +5,6 @@ import numpy as np import tables as tb -import uhal import zmq from aidatlu import logger @@ -20,13 +19,14 @@ class AidaTLU: - def __init__(self, hw, config_path, clock_config_path) -> None: + def __init__(self, hw, config_path, clock_config_path, i2c=I2CCore) -> None: self.log = logger.setup_main_logger(__class__.__name__) - self.i2c = I2CCore(hw) + self.i2c = i2c(hw) self.i2c_hw = hw self.log.info("Initializing IPbus interface") self.i2c.init() + if self.i2c.modules["eeprom"]: self.log.info("Found device with ID %s" % hex(self.get_device_id())) @@ -39,7 +39,7 @@ def __init__(self, hw, config_path, clock_config_path) -> None: self.dut_logic = DUTLogic(self.i2c) self.reset_configuration() - self.config_parser = TLUConfigure(self, self.io_controller, config_path) + self.config_parser = TLUConfigure(self, config_path) self.log.success("TLU initialized") @@ -145,59 +145,6 @@ def get_run_active(self) -> bool: """ return bool(self.i2c.read_register("Shutter.RunActiveRW")) - def test_configuration(self) -> None: - """Configure DUT 1 to run in a default test configuration. - Runs in EUDET mode with internal generated triggers. - This is just for testing and bugfixing. - """ - self.log.info("Configure DUT 1 in EUDET test mode") - - test_stretch = [1, 1, 1, 1, 1, 1] - test_delay = [0, 0, 0, 0, 0, 0] - - self.io_controller.configure_hdmi(1, "0111") - self.io_controller.clock_hdmi_output(1, "off") - self.trigger_logic.set_pulse_stretch_pack(test_stretch) - self.trigger_logic.set_pulse_delay_pack(test_delay) - self.trigger_logic.set_trigger_mask(mask_high=0x00000000, mask_low=0x00000002) - self.trigger_logic.set_trigger_polarity(1) - self.dut_logic.set_dut_mask("0001") - self.dut_logic.set_dut_mask_mode("00000000") - self.trigger_logic.set_internal_trigger_frequency(500) - - def default_configuration(self) -> None: - """Default configuration. Configures DUT 1 to run in EUDET mode. - This is just for testing and bugfixing. - """ - test_stretch = [1, 1, 1, 1, 1, 1] - test_delay = [0, 0, 0, 0, 0, 0] - - self.io_controller.configure_hdmi(1, "0111") - self.io_controller.configure_hdmi(2, "0111") - self.io_controller.configure_hdmi(3, "0111") - self.io_controller.configure_hdmi(4, "0111") - self.io_controller.clock_hdmi_output(1, "off") - self.io_controller.clock_hdmi_output(2, "off") - self.io_controller.clock_hdmi_output(3, "off") - self.io_controller.clock_hdmi_output(4, "off") - self.io_controller.clock_lemo_output(False) - self.dac_controller.set_threshold(1, -0.04) - self.dac_controller.set_threshold(2, -0.04) - self.dac_controller.set_threshold(3, -0.04) - self.dac_controller.set_threshold(4, -0.04) - self.dac_controller.set_threshold(5, -0.2) - self.dac_controller.set_threshold(6, -0.2) - self.trigger_logic.set_pulse_stretch_pack(test_stretch) - self.trigger_logic.set_pulse_delay_pack(test_delay) - self.trigger_logic.set_trigger_mask(mask_high=0, mask_low=2) - self.trigger_logic.set_trigger_polarity(1) - self.dut_logic.set_dut_mask("0001") - self.dut_logic.set_dut_mask_mode("00000000") - self.dut_logic.set_dut_mask_mode_modifier(0) - self.dut_logic.set_dut_ignore_busy(0) - self.dut_logic.set_dut_ignore_shutter(0x1) - self.trigger_logic.set_internal_trigger_frequency(0) - def start_run(self) -> None: """Start run configurations""" self.reset_counters() @@ -220,7 +167,7 @@ def set_enable_record_data(self, value: int) -> None: self.i2c.write_register("Event_Formatter.Enable_Record_Data", value) def get_event_fifo_csr(self) -> int: - """Reads value from 'EventFifoCSR' + """Reads value from 'EventFifoCSR', corresponds to status flags of the FIFO. Returns: int: number of events @@ -229,6 +176,8 @@ def get_event_fifo_csr(self) -> int: def get_event_fifo_fill_level(self) -> int: """Reads value from 'EventFifoFillLevel' + Returns the number of words written in + the FIFO. The lowest 14-bits are the actual data. Returns: int: buffer level of the fifi @@ -260,8 +209,6 @@ def pull_fifo_event(self) -> list: """ event_numb = self.get_event_fifo_fill_level() if event_numb: - if event_numb * 6 == 0xFEA: - self.log.warning("FIFO is full") fifo_content = self.i2c_hw.getNode("eventBuffer.EventFifoData").readBlock( event_numb ) @@ -269,7 +216,12 @@ def pull_fifo_event(self) -> list: return np.array(fifo_content) pass - def get_scalar(self): + def get_scalar(self) -> list: + """reads current sc values from registers + + Returns: + list: all 6 trigger sc values + """ s0 = self.i2c.read_register("triggerInputs.ThrCount0R") s1 = self.i2c.read_register("triggerInputs.ThrCount1R") s2 = self.i2c.read_register("triggerInputs.ThrCount2R") @@ -304,6 +256,7 @@ def init_raw_data_table(self) -> None: config_table.append(self.conf_list) def handle_status(self) -> None: + """Status message handling in separate thread. Calculates run time and obtain trigger information and sent it out every second.""" t = threading.current_thread() while getattr(t, "do_run", True): time.sleep(0.5) @@ -338,7 +291,7 @@ def log_sent_status(self, time: int) -> None: self.total_trigger_number = self.trigger_logic.get_pre_veto_trigger() s0, s1, s2, s3, s4, s5 = self.get_scalar() - if self.zmq_address not in [None, "off"]: + if self.zmq_address: self.socket.send_string( str( [ @@ -372,7 +325,7 @@ def log_sent_status(self, time: int) -> None: self.log.debug("FIFO level 2: %s" % self.get_event_fifo_csr()) self.log.debug( "fifo csr: %s fifo fill level: %s" - % (self.get_event_fifo_csr(), self.get_event_fifo_csr()) + % (self.get_event_fifo_fill_level(), self.get_event_fifo_csr()) ) self.log.debug( "post: %s pre: %s" @@ -382,6 +335,14 @@ def log_sent_status(self, time: int) -> None: ) ) self.log.debug("time stamp: %s" % (self.get_timestamp())) + if ( + self.run_time < 10 + ): # Logs trigger configuration when logging level is debug for the first 10s + current_event = self.pull_fifo_event() + if np.size(current_event) > 1: + self.log_trigger_inputs(current_event[0:6]) + if self.get_event_fifo_csr() == 0x10: + self.log.warning("FIFO is full") def log_trigger_inputs(self, event_vector: list) -> None: """Logs which inputs triggered the event corresponding to the event vector. @@ -396,13 +357,13 @@ def log_trigger_inputs(self, event_vector: list) -> None: input_4 = (w0 >> 19) & 0x1 input_5 = (w0 >> 20) & 0x1 input_6 = (w0 >> 21) & 0x1 - self.log.info("Event triggered:") - self.log.info( + self.log.debug( "Input 1: %s, Input 2: %s, Input 3: %s, Input 4: %s, Input 5: %s, Input 6: %s" % (input_1, input_2, input_3, input_4, input_5, input_6) ) def setup_zmq(self) -> None: + """Setup the zmq connection, this connection receives status messages.""" self.context = zmq.Context() self.socket = self.context.socket(zmq.PUB) self.socket.bind(self.zmq_address) @@ -410,23 +371,43 @@ def setup_zmq(self) -> None: def run(self) -> None: """Start run of the TLU.""" + self.start_run_configuration() + self.run_active = True + t = threading.Thread(target=self.handle_status) + t.start() + while self.run_active: + try: + self.run_loop() + if self.stop_condition is True: + raise KeyboardInterrupt + except: + if KeyboardInterrupt: + self.run_active = False + else: + # If this happens: poss. Hitrate to high for FIFO and or data handling. + self.log.warning("Incomplete event handling...") + + self.stop_run() + t.do_run = False + self.stop_run_configuration() + + def start_run_configuration(self) -> None: + """Start of the run configurations, consists of timestamp resets, data preparations and zmq connections initialization.""" self.start_run() self.get_fw_version() self.get_device_id() - run_active = True # reset starting parameter self.start_time = self.get_timestamp() self.last_time = 0 self.last_triggers_freq = self.trigger_logic.get_post_veto_trigger() self.last_particle_freq = self.trigger_logic.get_pre_veto_trigger() - first_event = True self.stop_condition = False # prepare data handling and zmq connection - save_data, interpret_data_bool = self.config_parser.get_data_handling() + self.save_data = self.config_parser.get_data_handling() self.zmq_address = self.config_parser.get_zmq_connection() self.max_trigger, self.timeout = self.config_parser.get_stop_condition() - if save_data: + if self.save_data: self.path = self.config_parser.get_output_data_path() if self.path == None: self.path = "tlu_data/" @@ -442,59 +423,50 @@ def run(self) -> None: ) self.init_raw_data_table() - if self.zmq_address not in [None, "off"]: + if self.zmq_address: self.setup_zmq() - t = threading.Thread(target=self.handle_status) - t.start() - while run_active: - try: - time.sleep(0.000001) - current_event = self.pull_fifo_event() - try: - if save_data and np.size(current_event) > 1: - self.data_table.append(current_event) - if self.stop_condition == True: - raise KeyboardInterrupt - except: - if KeyboardInterrupt: - run_active = False - t.do_run = False - self.stop_run() - else: - # If this happens: poss. Hitrate to high for FIFO and or Data handling. - self.log.warning("Incomplete Event handling...") - - # This loop sents which inputs produced the trigger signal for the first event. - if ( - np.size(current_event) > 1 - ) and first_event: # TODO only first event? - self.log_trigger_inputs(current_event[0:6]) - first_event = False - - except KeyboardInterrupt: - run_active = False - t.do_run = False - self.stop_run() + def run_loop(self) -> None: + """A single instance of the run loop. In a TLU run this function needs to be called repeatedly. - # Cleanup of FIFO + Raises: + KeyboardInterrupt: The run loop can be interrupted when raising a KeyboardInterrupt. + """ try: - while np.size(current_event) > 1: - current_event = self.pull_fifo_event() + current_event = self.pull_fifo_event() + try: + if self.save_data and np.size(current_event) > 1: + self.data_table.append(current_event) + if self.stop_condition is True: + raise KeyboardInterrupt + except: + if KeyboardInterrupt: + self.run_active = False + else: + # If this happens: poss. Hitrate to high for FIFO and or Data handling. + self.log.warning("Incomplete Event handling...") + except KeyboardInterrupt: - self.log.warning("Interrupted FIFO cleanup") + self.run_active = False + + def stop_run_configuration(self) -> None: + """Cleans remaining FIFO data and closes data files and zmq connections after a run.""" + # Cleanup of FIFO + self.pull_fifo_event() - if self.zmq_address not in [None, "off"]: + if self.zmq_address: self.socket.close() - if save_data: + if self.save_data: self.h5_file.close() - if interpret_data_bool: interpret_data(self.raw_data_path, self.interpreted_data_path) + self.log.success("Run finished") if __name__ == "__main__": + import uhal + uhal.setLogLevelTo(uhal.LogLevel.NOTICE) manager = uhal.ConnectionManager("file://../misc/aida_tlu_connection.xml") hw = uhal.HwInterface(manager.getDevice("aida_tlu.controlhub")) @@ -505,5 +477,4 @@ def run(self) -> None: tlu = AidaTLU(hw, config_path, clock_path) tlu.configure() - tlu.run() diff --git a/aidatlu/test/interpreted_data.h5 b/aidatlu/test/fixtures/interpreted_data_test.h5 similarity index 99% rename from aidatlu/test/interpreted_data.h5 rename to aidatlu/test/fixtures/interpreted_data_test.h5 index f932b2a..32610b7 100644 Binary files a/aidatlu/test/interpreted_data.h5 and b/aidatlu/test/fixtures/interpreted_data_test.h5 differ diff --git a/aidatlu/test/raw_data_test.h5 b/aidatlu/test/fixtures/raw_data_test.h5 similarity index 100% rename from aidatlu/test/raw_data_test.h5 rename to aidatlu/test/fixtures/raw_data_test.h5 diff --git a/aidatlu/test/fixtures/tlu_test_configuration.yaml b/aidatlu/test/fixtures/tlu_test_configuration.yaml new file mode 100644 index 0000000..2bc3fc2 --- /dev/null +++ b/aidatlu/test/fixtures/tlu_test_configuration.yaml @@ -0,0 +1,75 @@ +################################################ +# # +# This configuration is only used during tests # +# Changing settings besides MOCK lead to fails # +# # +################################################ + + +# Generate TLU internal trigger with given rate in Hz +internal_trigger: + internal_trigger_rate: 100000 + +# Set operating mode of the DUT, supported are three operating modes 'aida', 'aidatrig' and 'eudet' +# Set unused DUT interfaces to off, false or None +dut_module: + dut_1: + mode: aida + dut_2: + mode: eudet + dut_3: + mode: aidatrig + dut_4: + mode: off + +trigger_inputs: + # Threshold voltages for the trigger inputs in V. + threshold: + threshold_1: -0.1 + threshold_2: -0.2 + threshold_3: -0.3 + threshold_4: -0.4 + threshold_5: -0.5 + threshold_6: -0.6 + + # Trigger Logic configuration accept a python expression for the trigger inputs. + # The logic is set by using the variables for the input channels 'CH1', 'CH2', 'CH3', 'CH4', 'CH5' and 'CH6' + # and the Python bitwise operators AND: '&', OR: '|', NOT: '~' and so on. Dont forget to use brackets... + trigger_inputs_logic: (CH1 & CH2) | CH3 + + # TLU can trigger on a rising or falling edge. Trigger polarity is set using a string or boolean, + # 'rising' corresponds to false and 'falling' to true + trigger_polarity: + polarity: falling + + # Stretches and delays each trigger input signal for an number of clock cycles (corresponds to 6.25ns steps), + # The stretch and delay of all inputs is given as a list, + # each entry corresponding to an individual trigger input. + trigger_signal_shape: + stretch: [2, 2, 3, 2, 2, 2] + delay: [0, 1, 0, 0, 3, 0] + +# Enable the LEMO clock output using a boolean. +clock_lemo: + enable_clock_lemo_output: True + +# Set the four PMT control voltages in V +pmt_control: + pmt_1: 0.8 + pmt_2: 0.8 + pmt_3: 0 + pmt_4: -0.2 + +# Save and generate interpreted data from the raw data set. Set to 'True' or 'False'. +# If no specific output path is provided, the data is saved in the default output data path (aidatlu/aidatlu/tlu_data). +save_data: True +output_data_path: 'test_output_data/' + +# zmq connection for status messages, leave it blank or set to off if not needed +zmq_connection: off #"tcp://:7500" + +# Optional stop conditions can also be added to the configuration. +# These can be by timeout in seconds or a maximum output trigger number. +# If needed just uncomment the required stop condition in the example below: +# max_trigger_number: 1000000 +timeout: 5 diff --git a/aidatlu/test/hardware_test.py b/aidatlu/test/hardware_test.py deleted file mode 100644 index 74b8fe4..0000000 --- a/aidatlu/test/hardware_test.py +++ /dev/null @@ -1,213 +0,0 @@ -import time - -import numpy as np -import uhal - -from aidatlu.hardware.clock_controller import ClockControl -from aidatlu.hardware.dac_controller import DacControl -from aidatlu.hardware.dut_controller import DUTLogic -from aidatlu.hardware.i2c import I2CCore -from aidatlu.hardware.ioexpander_controller import IOControl -from aidatlu.hardware.trigger_controller import TriggerLogic -from aidatlu.main.config_parser import TLUConfigure -from aidatlu.main.tlu import AidaTLU - - -class Test_IOCControl: - uhal.setLogLevelTo(uhal.LogLevel.NOTICE) - manager = uhal.ConnectionManager("file://../misc/aida_tlu_connection.xml") - hw = uhal.HwInterface(manager.getDevice("aida_tlu.controlhub")) - - i2c = I2CCore(hw) - i2c.init() - ioexpander = IOControl(i2c) - - def test_ioexpander_led(self) -> None: - self.ioexpander.all_off() - self.ioexpander.test_leds(single=True) - self.ioexpander.all_off() - time.sleep(1) - self.ioexpander.all_on() - time.sleep(2) - self.ioexpander.all_off() - - def test_configure_hdmi(self) -> None: - for i in range(4): - self.ioexpander.configure_hdmi(i + 1, "1111") - self.ioexpander.clock_hdmi_output(i + 1, "chip") - time.sleep(1) - self.ioexpander.configure_hdmi(i + 1, "0000") - self.ioexpander.clock_hdmi_output(i + 1, "off") - - def test_clock_lemo_output(self): - self.ioexpander.clock_lemo_output(True) - time.sleep(1) - self.ioexpander.clock_lemo_output(False) - - -class Test_DacControl: - uhal.setLogLevelTo(uhal.LogLevel.NOTICE) - manager = uhal.ConnectionManager("file://../misc/aida_tlu_connection.xml") - hw = uhal.HwInterface(manager.getDevice("aida_tlu.controlhub")) - - i2c = I2CCore(hw) - i2c.init() - dac_true = DacControl(i2c, True) - dac_false = DacControl(i2c, False) - - def test_set_threshold(self) -> None: - for i in range(7): - for volts in np.arange(-1.3, 1.3, 1.3): - self.dac_true.set_threshold(i + 1, volts) - time.sleep(0.2) - self.dac_true.set_threshold(i + 1, 0) - time.sleep(0.5) - for i in range(7): - for volts in np.arange(-1.3, 1.3, 1.3): - self.dac_false.set_threshold(i + 1, volts) - time.sleep(0.2) - self.dac_false.set_threshold(i + 1, 0) - - def test_set_voltage(self) -> None: - for i in range(4): - for volts in np.arange(0, 1, 0.5): - self.dac_true.set_voltage(i + 1, volts) - time.sleep(0.2) - self.dac_true.set_voltage(5, 0) - - -class Test_ClockControl: - uhal.setLogLevelTo(uhal.LogLevel.NOTICE) - manager = uhal.ConnectionManager("file://../misc/aida_tlu_connection.xml") - hw = uhal.HwInterface(manager.getDevice("aida_tlu.controlhub")) - i2c = I2CCore(hw) - i2c.init() - ioexpander = IOControl(i2c) - clock = ClockControl(i2c, ioexpander) - - def test_device_info(self) -> None: - self.clock.log.info("Device Version: %i" % self.clock.get_device_version()) - self.clock.log.info("Design ID: %s" % self.clock.check_design_id()) - - def test_write_clock_register(self): - self.clock.write_clock_conf("../misc/aida_tlu_clk_config.txt") - - -class Test_DUTLogic: - uhal.setLogLevelTo(uhal.LogLevel.NOTICE) - manager = uhal.ConnectionManager("file://../misc/aida_tlu_connection.xml") - hw = uhal.HwInterface(manager.getDevice("aida_tlu.controlhub")) - - i2c = I2CCore(hw) - i2c.init() - dut = DUTLogic(i2c) - - def test_set_dut_mask(self) -> None: - time.sleep(1) - self.dut.set_dut_mask("1010") - time.sleep(1) - self.dut.set_dut_mask("0000") - - def test_set_dut_mask_mode(self): - self.dut.set_dut_mask_mode("00000000") - time.sleep(1) - self.dut.set_dut_mask_mode("11111111") - time.sleep(1) - self.dut.set_dut_mask_mode("01010101") - - def test_set_dut_mask_modifier(self) -> None: - # TODO What input here? - self.dut.set_dut_mask_mode_modifier(1) - time.sleep(1) - self.dut.set_dut_mask_mode_modifier(0) - - def test_set_dut_ignore_busy(self): - self.dut.set_dut_ignore_busy("1111") - time.sleep(1) - self.dut.set_dut_ignore_busy("0000") - - def test_set_dut_ignore_busy(self) -> None: - self.dut.set_dut_ignore_shutter(0) - - -class Test_TriggerLogic: - uhal.setLogLevelTo(uhal.LogLevel.NOTICE) - manager = uhal.ConnectionManager("file://../misc/aida_tlu_connection.xml") - hw = uhal.HwInterface(manager.getDevice("aida_tlu.controlhub")) - - i2c = I2CCore(hw) - trigger = TriggerLogic(i2c) - - def test_set_internal_trigger_frequency(self) -> None: - self.trigger.set_internal_trigger_frequency(0) - self.trigger.set_internal_trigger_frequency(10000) - self.trigger.set_internal_trigger_frequency(0) - - def test_set_trigger_veto(self) -> None: - self.trigger.set_trigger_veto(True) - time.sleep(1) - self.trigger.set_trigger_veto(False) - - def test_set_trigger_polarity(self): - self.trigger.set_trigger_polarity(1) - time.sleep(1) - self.trigger.set_trigger_polarity(0) - - def test_set_trigger_mask(self): - self.trigger.set_trigger_mask(0b0, 0b1) - time.sleep(1) - self.trigger.set_trigger_mask(0b0, 0b0) - - def test_set_pulse_stretch_pack(self) -> None: - self.trigger.set_pulse_stretch_pack([1, 1, 1, 1, 1, 1]) - time.sleep(1) - self.trigger.set_pulse_stretch_pack([2, 2, 2, 2, 2, 2]) - - def test_set_pulse_delay_pack(self) -> None: - self.trigger.set_pulse_delay_pack([0, 0, 0, 0, 0, 0]) - time.sleep(1) - self.trigger.set_pulse_delay_pack([1, 1, 1, 1, 1, 1]) - - -def test_run(): - uhal.setLogLevelTo(uhal.LogLevel.NOTICE) - manager = uhal.ConnectionManager("file://.././misc/aida_tlu_connection.xml") - hw = uhal.HwInterface(manager.getDevice("aida_tlu.controlhub")) - - config_path = "tlu_test_configuration.yaml" - clock_path = "../misc/aida_tlu_clk_config.txt" - tlu = AidaTLU(hw, config_path, clock_path) - - tlu.configure() - tlu.run() - - -if __name__ == "__main__": - test_io = Test_IOCControl() - test_io.test_clock_lemo_output() - test_io.test_configure_hdmi() - test_io.test_ioexpander_led() - - test_dac = Test_DacControl() - test_dac.test_set_threshold() - test_dac.test_set_threshold() - - test_dut = Test_DUTLogic() - test_dut.test_set_dut_ignore_busy() - test_dut.test_set_dut_mask() - test_dut.test_set_dut_mask_mode() - test_dut.test_set_dut_mask_modifier() - - test_clock = Test_ClockControl() - test_clock.test_device_info() - test_clock.test_write_clock_register() - - test_trigger = Test_TriggerLogic() - test_trigger.test_set_internal_trigger_frequency() - test_trigger.test_set_pulse_delay_pack() - test_trigger.test_set_pulse_stretch_pack() - test_trigger.test_set_trigger_mask() - test_trigger.test_set_trigger_polarity() - test_trigger.test_set_trigger_veto() - - test_run = test_run() diff --git a/aidatlu/test/register_table.yaml b/aidatlu/test/register_table.yaml new file mode 100644 index 0000000..b445f46 --- /dev/null +++ b/aidatlu/test/register_table.yaml @@ -0,0 +1,204 @@ +DUTInterfaces: + address: 0x1000 + DUTMaskW: + address: 0x0 + permission: w + IgnoreDUTBusyW: + address: 0x1 + permission: w + IgnoreShutterVetoW: + address: 0x2 + permission: w + DUTInterfaceModeW: + address": 0x3 + permission": w + DUTInterfaceModeModifierW: + address: 0x4 + permission: w + DUTMaskR: + address: 0x8 + permission: r + IgnoreDUTBusyR: + address: 0x9 + permission: r + IgnoreShutterVetoR: + address: 0xA + permission: r + DUTInterfaceModeR: + address: 0xB + permission: r + DUTInterfaceModeModifierR: + address: 0xC + permission: r + +Shutter: + address: 0x2000 + ControlRW: + address: 0x0 + permission: rw + ShutterSelectRW: + address: 0x1 + permission: rw + InternalShutterPeriodRW: + address: 0x2 + permission: rw + ShutterOnTimeRW: + address: 0x3 + permission: rw + ShutterVetoOffTimeRW: + address: 0x4 + permission: rw + ShutterOffTimeRW: + address: 0x5 + permission: rw + RunActiveRW: + address: 0x6 + permission: rw + +i2c_master: + address: 0x3000 + i2c_pre_lo: + address: 0x0 + permission: rw + i2c_pre_hi: + address: 0x1 + permission: rw + i2c_ctrl: + address: 0x2 + permission: rw + i2c_rxtx: + address: 0x3 + permission: rw + i2c_cmdstatus: + address: 0x4 + permission: rw + +eventBuffer: + address: 0x4000 + EventFifoData: + address: 0x0 + permission: r + EventFifoFillLevel: + address: 0x1 + permission: r + EventFifoCSR: + address: 0x2 + permission: rw + EventFifoFillLevelFlags: + address: 0x3 + permission: r + +Event_Formatter: + address: 0x5000 + Enable_Record_Data: + address: 0x0 + permission: rw + ResetTimestampW: + address: 0x1 + permission: w + CurrentTimestampLR: + address: 0x2 + permission": r + CurrentTimestampHR: + address: 0x3 + permission: r + +triggerInputs: + address: 0x6000 + SerdesRstW: + address: 0x0 + permission: w + InvertEdgeW: + address: 0x1 + permission: w + SerdesRstR: + address: 0x8 + permission: r + ThrCount0R: + address: 0x9 + permission: r + ThrCount1R: + address: 0xa + permission: r + ThrCount2R: + address: 0xb + permission: r + ThrCount3R: + address: 0xc + permission: r + ThrCount4R: + address: 0xd + permission: r + ThrCount5R: + address: 0xe + permission: r + +triggerLogic: + address: 0x7000 + PostVetoTriggersR: + address: 0x10 + permission: r + PreVetoTriggersR: + address: 0x11 + permission: r + InternalTriggerIntervalW: + address: 0x2 + permission: w + InternalTriggerIntervalR: + address: 0x12 + permission: r + TriggerVetoW: + address: 0x4 + permission: w + TriggerVetoR: + address: 0x14 + permission: r + ExternalTriggerVetoR: + address: 0x15 + permission: r + PulseStretchW: + address: 0x6 + permission: w + PulseStretchR: + address: 0x16 + permission: r + PulseDelayW: + address: 0x7 + permission: w + PulseDelayR: + address: 0x17 + permission: r + TriggerHoldOffW: + address: 0x8 + permission: w + TriggerHoldOffR: + address: 0x18 + permission: r + AuxTriggerCountR: + address: 0x19 + permission: r + TriggerPattern_lowW: + address: 0xA + permission: w + TriggerPattern_lowR: + address: 0x1A + permission: r + TriggerPattern_highW: + address: 0xB + permission: w + TriggerPattern_highR: + address: 0x1B + permission: r + +logic_clocks: + address": 0x8000 + LogicClocksCSR: + address: 0x0 + permission: rw + LogicRst: + address: 0x1 + permission: w + +version: + address: 0x1 + permission: r diff --git a/aidatlu/test/software_test.py b/aidatlu/test/software_test.py deleted file mode 100644 index 1ad31d7..0000000 --- a/aidatlu/test/software_test.py +++ /dev/null @@ -1,60 +0,0 @@ -import numpy as np -import tables as tb -from aidatlu.main.data_parser import interpret_data -from aidatlu.main.config_parser import TLUConfigure - - -def test_data_parser(): - interpret_data("raw_data_test.h5", "interpreted_data_test.h5") - - -def test_interpreted_data(): - features = np.dtype( - [ - ("eventnumber", "u4"), - ("timestamp", "u4"), - ("overflow", "u4"), - ("eventtype", "u4"), - ("input1", "u4"), - ("input2", "u4"), - ("input3", "u4"), - ("input4", "u4"), - ("inpu5", "u4"), - ("input6", "u4"), - ("sc1", "u4"), - ("sc2", "u4"), - ("sc3", "u4"), - ("sc4", "u4"), - ("sc5", "u4"), - ("sc6", "u4"), - ] - ) - - interpreted_data_path = "interpreted_data.h5" - interpreted_test_data_path = "interpreted_data_test.h5" - - with tb.open_file(interpreted_data_path, "r") as file: - table = file.root.interpreted_data - interpreted_data = np.array(table[:], dtype=features) - - with tb.open_file(interpreted_test_data_path, "r") as file: - table = file.root.interpreted_data - interpreted_test_data = np.array(table[:], dtype=features) - - assert np.array_equal(interpreted_data, interpreted_test_data) - - -def test_load_config(): - config_path = "../tlu_configuration.yaml" - config_parser = TLUConfigure(TLU=None, io_control=None, config_path=config_path) - _ = config_parser.get_configuration_table() - _ = config_parser.get_data_handling() - _ = config_parser.get_output_data_path() - _ = config_parser.get_stop_condition() - _ = config_parser.get_zmq_connection() - - -if __name__ == "__main__": - test_data_parser() - test_interpreted_data() - test_load_config() diff --git a/aidatlu/test/test_configuration.py b/aidatlu/test/test_configuration.py new file mode 100644 index 0000000..d378338 --- /dev/null +++ b/aidatlu/test/test_configuration.py @@ -0,0 +1,190 @@ +from pathlib import Path +import os +import yaml +import pytest +from aidatlu.main.config_parser import TLUConfigure +from aidatlu.hardware.ioexpander_controller import IOControl +from aidatlu.main.tlu import AidaTLU +from aidatlu.hardware.i2c import I2CCore +from aidatlu.test.utils import MockI2C + +FILEPATH = Path(__file__).parent +CONFIG_FILE = FILEPATH / "fixtures" / "tlu_test_configuration.yaml" + +try: + MOCK = not os.environ["HW"] == "True" +except KeyError: + MOCK = True + +if MOCK: + HW = None + I2CMETHOD = MockI2C +else: + import uhal + + uhal.setLogLevelTo(uhal.LogLevel.NOTICE) + manager = uhal.ConnectionManager( + "file://" + str(FILEPATH / "../misc/aida_tlu_connection.xml") + ) + HW = uhal.HwInterface(manager.getDevice("aida_tlu.controlhub")) + I2CMETHOD = I2CCore + +TLU = AidaTLU( + HW, + CONFIG_FILE, + FILEPATH / "../misc/aida_tlu_clk_config.txt", + i2c=I2CMETHOD, +) + + +def test_config_parser(): + """Test parsing the configuration file""" + + config_parser = TLUConfigure( + TLU=TLU, + config_path=CONFIG_FILE, + ) + with open(CONFIG_FILE) as yaml_file: + test_config = yaml.safe_load(yaml_file) + assert isinstance(config_parser.get_configuration_table(), list) + assert test_config["save_data"] == config_parser.get_data_handling() + assert test_config["output_data_path"] == config_parser.get_output_data_path() + assert (None, test_config["timeout"]) == config_parser.get_stop_condition() + assert test_config["zmq_connection"] == config_parser.get_zmq_connection() + + +def test_dut_configuration(): + """Test configuration of the DUT interfaces""" + config_parser = TLUConfigure( + TLU=TLU, + config_path=CONFIG_FILE, + ) + config_parser.conf_dut() + + assert TLU.i2c.read_register("DUTInterfaces.DUTMaskR") == 0x7 + assert TLU.i2c.read_register("DUTInterfaces.DUTInterfaceModeR") == 0x13 + assert TLU.i2c.read_register("DUTInterfaces.DUTInterfaceModeModifierR") == 0 + assert TLU.i2c.read_register("DUTInterfaces.IgnoreDUTBusyR") == 0 + assert TLU.i2c.read_register("DUTInterfaces.IgnoreShutterVetoR") == 0x1 + + # HDMI I/O + # clock + assert ( + TLU.io_controller._get_ioexpander_output(io_exp=2, exp_id=2, cmd_byte=2) == 0x50 + ) + # SPARE, TRG, CONT + assert ( + TLU.io_controller._get_ioexpander_output(io_exp=2, exp_id=1, cmd_byte=2) == 0x77 + ) + assert ( + TLU.io_controller._get_ioexpander_output(io_exp=2, exp_id=1, cmd_byte=3) == 0x77 + ) + + +def test_trigger_logic_configuration(): + """Test configuration of the trigger logic""" + config_parser = TLUConfigure( + TLU=TLU, + config_path=CONFIG_FILE, + ) + config_parser.conf_trigger_logic() + if MOCK: + assert TLU.i2c.read_register("triggerInputs.InvertEdgeW") == 0x1 + assert TLU.i2c.read_register("triggerLogic.InternalTriggerIntervalW") == 0x640 + # Read register does not have the same value as the write register for the mock + TLU.i2c.write_register( + "triggerLogic.PulseStretchR", + TLU.i2c.read_register("triggerLogic.PulseStretchW"), + ) + TLU.i2c.write_register( + "triggerLogic.PulseDelayR", + TLU.i2c.read_register("triggerLogic.PulseDelayW"), + ) + + else: + # Rounding error in TLU when calculating trigger frequency + assert TLU.i2c.read_register("triggerLogic.InternalTriggerIntervalR") == 0x63E + + assert TLU.i2c.read_register("triggerLogic.PulseStretchR") == 0x4210C42 + assert TLU.i2c.read_register("triggerLogic.PulseDelayR") == 0x300020 + + # Test LEDs + assert ( + TLU.io_controller._get_ioexpander_output(io_exp=1, exp_id=1, cmd_byte=2) == 0xFF + ) + assert ( + TLU.io_controller._get_ioexpander_output(io_exp=1, exp_id=1, cmd_byte=3) == 0xFF + ) + assert ( + TLU.io_controller._get_ioexpander_output(io_exp=1, exp_id=2, cmd_byte=2) == 0x7F + ) + assert ( + TLU.io_controller._get_ioexpander_output(io_exp=1, exp_id=2, cmd_byte=3) == 0x58 + ) + + +def test_trigger_input_configuration(): + """Test configuration of the trigger inputs""" + config_parser = TLUConfigure( + TLU=TLU, + config_path=CONFIG_FILE, + ) + config_parser.conf_trigger_inputs() + + if MOCK: + # Write array concatenates array bitwise, this is not implemented in the mock + mem_addr = 0x18 + (0 & 0x7) + assert TLU.i2c.read(TLU.i2c.modules["dac_1"], mem_addr) == [0x6C, 0x4E] + assert TLU.i2c.read(TLU.i2c.modules["dac_2"], mem_addr) == [0x44, 0xEC] + mem_addr = 0x18 + (1 & 0x7) + assert TLU.i2c.read(TLU.i2c.modules["dac_1"], mem_addr) == [0x76, 0x26] + assert TLU.i2c.read(TLU.i2c.modules["dac_2"], mem_addr) == [0x4E, 0xC4] + + else: + mem_addr = 0x18 + (0 & 0x7) + assert TLU.i2c.read(TLU.i2c.modules["dac_1"], mem_addr) == 0xD8 + assert TLU.i2c.read(TLU.i2c.modules["dac_2"], mem_addr) == 0x89 + mem_addr = 0x18 + (1 & 0x7) + assert TLU.i2c.read(TLU.i2c.modules["dac_1"], mem_addr) == 0x31 + assert TLU.i2c.read(TLU.i2c.modules["dac_2"], mem_addr) == 0x31 + + assert TLU.i2c.read_register("triggerLogic.TriggerPattern_lowR") == 0xF8F8F8F8 + assert TLU.i2c.read_register("triggerLogic.TriggerPattern_highR") == 0xF8F8F8F8 + + +def test_conf_auxillary(): + """Test PMT power and LEMO clock I/O""" + config_parser = TLUConfigure( + TLU=TLU, + config_path=CONFIG_FILE, + ) + config_parser.conf_auxillary() + + if MOCK: + # Write array concatenates array bitwise, this is not implemented in the mock + mem_addr = 0x18 + (0 & 0x7) + assert TLU.i2c.read(TLU.i2c.modules["pwr_dac"], mem_addr) == [0, 0] + mem_addr = 0x18 + (1 & 0x7) + assert TLU.i2c.read(TLU.i2c.modules["pwr_dac"], mem_addr) == [0xCC, 0xCC] + mem_addr = 0x18 + (2 & 0x7) + assert TLU.i2c.read(TLU.i2c.modules["pwr_dac"], mem_addr) == [0, 0] + mem_addr = 0x18 + (3 & 0x7) + assert TLU.i2c.read(TLU.i2c.modules["pwr_dac"], mem_addr) == [0xCC, 0xCC] + + else: + mem_addr = 0x18 + (0 & 0x7) + assert TLU.i2c.read(TLU.i2c.modules["pwr_dac"], mem_addr) == 0 + mem_addr = 0x18 + (1 & 0x7) + assert TLU.i2c.read(TLU.i2c.modules["pwr_dac"], mem_addr) == 0x31 + mem_addr = 0x18 + (2 & 0x7) + assert TLU.i2c.read(TLU.i2c.modules["pwr_dac"], mem_addr) == 0x33 + mem_addr = 0x18 + (3 & 0x7) + assert TLU.i2c.read(TLU.i2c.modules["pwr_dac"], mem_addr) == 0x35 + + assert ( + TLU.io_controller._get_ioexpander_output(io_exp=2, exp_id=2, cmd_byte=3) == 0xB0 + ) + + +if __name__ == "__main__": + pytest.main() diff --git a/aidatlu/test/test_data.py b/aidatlu/test/test_data.py new file mode 100644 index 0000000..8ba189b --- /dev/null +++ b/aidatlu/test/test_data.py @@ -0,0 +1,33 @@ +from pathlib import Path +import numpy as np +import tables as tb +import pytest +from aidatlu.main.data_parser import interpret_data + +FILEPATH = Path(__file__).parent + + +def test_interpreted_data(): + """Test interpreting and parsing data""" + + interpret_data( + FILEPATH / "fixtures" / "raw_data_test.h5", FILEPATH / "interpreted_data.h5" + ) + + interpreted_data_path = FILEPATH / "interpreted_data.h5" + interpreted_test_data_path = FILEPATH / "fixtures" / "interpreted_data_test.h5" + + with tb.open_file(interpreted_data_path, "r") as file: + interpreted_data = file.root.interpreted_data[:] + config_table = file.root.conf[:] + + with tb.open_file(interpreted_test_data_path, "r") as file: + interpreted_test_data = file.root.interpreted_data[:] + config_table_test = file.root.conf[:] + + assert np.array_equal(interpreted_data, interpreted_test_data) + assert np.array_equal(config_table, config_table_test) + + +if __name__ == "__main__": + pytest.main() diff --git a/aidatlu/test/test_tlu.py b/aidatlu/test/test_tlu.py new file mode 100644 index 0000000..ecdf8db --- /dev/null +++ b/aidatlu/test/test_tlu.py @@ -0,0 +1,92 @@ +from pathlib import Path +import time +import os +import pytest +from aidatlu.main.tlu import AidaTLU +from aidatlu.hardware.i2c import I2CCore +from aidatlu.test.utils import MockI2C + +FILEPATH = Path(__file__).parent +CONFIG_FILE = FILEPATH / "fixtures" / "tlu_test_configuration.yaml" + +try: + MOCK = not os.environ["HW"] == "True" +except KeyError: + MOCK = True + +if MOCK: + HW = None + I2CMETHOD = MockI2C +else: + import uhal + + uhal.setLogLevelTo(uhal.LogLevel.NOTICE) + manager = uhal.ConnectionManager( + "file://" + str(FILEPATH / "../misc/aida_tlu_connection.xml") + ) + HW = uhal.HwInterface(manager.getDevice("aida_tlu.controlhub")) + I2CMETHOD = I2CCore + +TLU = AidaTLU( + HW, + CONFIG_FILE, + FILEPATH / "../misc/aida_tlu_clk_config.txt", + i2c=I2CMETHOD, +) + + +def test_check_ups(): + """Test read write TLU configurations""" + + if MOCK: + TLU.set_event_fifo_csr(0) + assert TLU.get_event_fifo_csr() == 0 + assert TLU.get_device_id() == 0xFFFFFFFFFFFF + assert TLU.get_fw_version() == -1 + assert TLU.get_run_active() == 0 + assert TLU.get_event_fifo_fill_level() == -1 + assert TLU.get_timestamp() == -0x100000001 + assert TLU.get_scalar() == (-1, -1, -1, -1, -1, -1) + else: + TLU.set_event_fifo_csr(0) + assert TLU.get_event_fifo_csr() == 3 + assert TLU.get_run_active() == 0 + assert TLU.get_event_fifo_fill_level() == 0 + + +def test_configuration(): + """Full test TLU configuration using test configuration file""" + + TLU.configure() + + +def test_run(): + """Full test TLU run using test configuration file""" + + if MOCK: + start_time = time.time() + + def _get_timestamp(self): + # Helper function returns timestamp + return (time.time() - start_time) / 25 * 1000000000 + + def _pull_fifo_event(self): + # Blank FIFO pull helper function + return 0 + + # Overwrite TLU methods needed for run loop + func_type = type(TLU.get_timestamp) + TLU.get_timestamp = func_type(_get_timestamp, TLU) + func_type = type(TLU.pull_fifo_event) + TLU.pull_fifo_event = func_type(_pull_fifo_event, TLU) + + TLU.configure() + TLU.run() + + else: + TLU.configure() + TLU.run() + + +if __name__ == "__main__": + pytest.main() diff --git a/aidatlu/test/tlu_test_configuration.yaml b/aidatlu/test/tlu_test_configuration.yaml deleted file mode 100644 index 71c9cae..0000000 --- a/aidatlu/test/tlu_test_configuration.yaml +++ /dev/null @@ -1,59 +0,0 @@ -################################################ -# -# This configuration is only used during tests -# -############################################### - - -internal_trigger: #Generate TLU internal trigger with given rate in Hz - internal_trigger_rate: 100000 - -dut_module: - dut_1: - mode: 'aida' # 'aida', 'aidatrig', 'eudet', 'any' - dut_2: - mode: 'off' # 'aida', 'aidatrig', 'eudet', 'any' - dut_3: - mode: 'off' # 'aida', 'aidatrig', 'eudet', 'any' - dut_4: - mode: 'off' # 'aida', 'aidatrig', 'eudet', 'any' - -trigger_inputs: #threshold voltages for the trigger inputs in V. - threshold: - threshold_1: -0.1 - threshold_2: -0.1 - threshold_3: -0.1 - threshold_4: -0.1 - threshold_5: -0.1 - threshold_6: -0.1 - - # Trigger Logic configuration accept a python expression for the trigger inputs. - # The logic is set by using the variables for the input channels 'CH1', 'CH2', 'CH3', 'CH4', 'CH5' and 'CH6' - # and the Python bitwise operators AND: '&', OR: '|', NOT: '~' and so on. Dont forget to use brackets... - trigger_inputs_logic: CH1 - - trigger_polarity: #TLU triggers on rising (0) or falling (1) edge - polarity: 1 - - trigger_signal_shape: #Stretches and delays each trigger input signal for an number of clock cycles, - stretch: [2, 2, 2, 2, 2, 2] - delay: [0, 0, 0, 0, 0, 0] - -clock_lemo: - enable_clock_lemo_output: True - -pmt_control: - #PMT control voltages in V - pmt_1: 0.8 - pmt_2: 0.8 - pmt_3: 0 - pmt_4: 0 - -#Save data and generate interpreted data from the raw data set. Set to 'True' or 'False'. -save_data: True -output_data_path: 'test_output_data/' - -#zmq connection leave it blank or set to 'off' if not needed -zmq_connection: 'off' #"tcp://:7500" - -timeout: 5 diff --git a/aidatlu/test/utils.py b/aidatlu/test/utils.py new file mode 100644 index 0000000..ca20174 --- /dev/null +++ b/aidatlu/test/utils.py @@ -0,0 +1,95 @@ +import yaml +from pathlib import Path +from aidatlu import logger +from aidatlu.hardware.i2c import I2CCore, i2c_addr + +FILEPATH = Path(__file__).parent + + +class MockI2C(I2CCore): + """Class mocking the I2C interface and replacing the hardware with register dictionaries for testing.""" + + def __init__(self, hw_int) -> None: # noqa + with open(FILEPATH / "register_table.yaml", "r") as yaml_file: + self.reg_table = yaml.safe_load(yaml_file) + self.i2c_device_table = { + 0x21: {}, + 0x68: {}, # Si5345 + 0x13: {}, # 1st AD5665R + 0x1F: {}, # 2nd AD5665R + 0x50: {}, # EEPROM + 0x74: {}, # 1st expander PCA9539PW + 0x75: {}, # 2nd expander PCA9539PW + 0x51: {}, # Address of EEPROM on powermodule, only available on new modules + 0x1C: {}, # AD5665R (DAC) on powermodule + 0x76: {}, # 1st expander PCA9539PW + 0x77: {}, # 2nd expander PCA9539PW, + 0x3A: {}, # Display + } + self.log = logger.setup_derived_logger("I2CCore") + self.modules = i2c_addr # Use I2C device name to address translation + + def init(self): + self.log.info("Initializing Mock I2C") + self.set_i2c_clock_prescale(0x30) + self.set_i2c_control(0x80) + + self.write(0x21, 0x01, 0x7F) + if self.read(0x21, 0x01) & 0x80 != 0x80: + self.log.warning( + "Enabling Enclustra I2C bus might have failed. This could prevent from talking to the I2C slaves \ + on the TLU." + ) + # Omitted dynamic search for connected I2C modules here and just continue with `init` + self.set_i2c_tx(0x0) + self.set_i2c_command(0x50) + + def write(self, device_addr: int, mem_addr: int, value: int) -> None: + """Mock I2C device memory write""" + self.i2c_device_table[device_addr][mem_addr] = value + + def write_array(self, device_addr: int, mem_addr: int, values: list) -> None: + """Mock I2C device memory array write""" + self.i2c_device_table[device_addr][mem_addr] = values + + def read(self, device_addr: int, mem_addr: int) -> int: + """Mock I2C memory read""" + try: + return self.i2c_device_table[device_addr][mem_addr] + except KeyError: + self.i2c_device_table[device_addr][mem_addr] = -1 + return self.i2c_device_table[device_addr][mem_addr] + + def write_register(self, register: str, value: int) -> None: + """Mock IPbus register write""" + reg_adressing = register.split(".") + if len(reg_adressing) == 2: + self.reg_table[reg_adressing[0]][reg_adressing[1]]["value"] = value + elif len(reg_adressing) == 1: + self.reg_table[register]["value"] = value + else: + raise ValueError("Invalid register addressing") + + def read_register(self, register: str) -> int: + """Mock IPbus register read""" + # Read register does not have the same value as the write register for the mock + register_read_write = register[:-1] + register[-1].replace("R", "W") + if register == "eventBuffer.EventFifoCSR": + register_read_write = register + reg_adressing = register.split(".") + reg_adressing_read_write = register_read_write.split(".") + try: + if len(reg_adressing) == 2: + return self.reg_table[reg_adressing_read_write[0]][ + reg_adressing_read_write[1] + ]["value"] + if len(reg_adressing) == 1: + return self.reg_table[reg_adressing_read_write[0]]["value"] + raise ValueError("Invalid register addressing") + except KeyError: + if len(reg_adressing) == 2: + self.reg_table[reg_adressing[0]][reg_adressing[1]]["value"] = -1 + return self.reg_table[reg_adressing[0]][reg_adressing[1]]["value"] + if len(reg_adressing) == 1: + self.reg_table[reg_adressing[0]]["value"] = -1 + return self.reg_table[reg_adressing[0]]["value"] diff --git a/aidatlu/tlu_configuration.yaml b/aidatlu/tlu_configuration.yaml index 4d10696..ba51327 100644 --- a/aidatlu/tlu_configuration.yaml +++ b/aidatlu/tlu_configuration.yaml @@ -1,17 +1,21 @@ -internal_trigger: #Generate TLU internal trigger with given rate in Hz - internal_trigger_rate: 10000 +# Generate TLU internal trigger with given rate in Hz +internal_trigger: + internal_trigger_rate: 0 +# Set operating mode of the DUT, supported are three operating modes 'aida', 'aidatrig' and 'eudet' +# Set unused DUT interfaces to off, false or None dut_module: dut_1: - mode: 'aida' # 'aida', 'aidatrig', 'eudet', 'any' + mode: aida dut_2: - mode: 'aida' # 'aida', 'aidatrig', 'eudet', 'any' + mode: aida dut_3: - mode: 'eudet' # 'aida', 'aidatrig', 'eudet', 'any' + mode: eudet dut_4: - mode: 'off' # 'aida', 'aidatrig', 'eudet', 'any' + mode: off -trigger_inputs: #threshold voltages for the trigger inputs in V. +trigger_inputs: + # Threshold voltages for the trigger inputs in V. threshold: threshold_1: -0.1 threshold_2: -0.1 @@ -25,25 +29,39 @@ trigger_inputs: #threshold voltages for the trigger inputs in V. # and the Python bitwise operators AND: '&', OR: '|', NOT: '~' and so on. Dont forget to use brackets... trigger_inputs_logic: CH2 & CH4 - trigger_polarity: #TLU triggers on rising (0) or falling (1) edge - polarity: 1 + # TLU can trigger on a rising or falling edge. Trigger polarity is set using a string or boolean, + # 'rising' corresponds to false (0) and 'falling' to true (1) + trigger_polarity: + polarity: falling - trigger_signal_shape: #Stretches and delays each trigger input signal for an number of clock cycles, +# Stretches and delays each trigger input signal for an number of clock cycles (corresponds to 6.25ns steps), +# The stretch and delay of all inputs is given as a list, +# each entry corresponding to an individual trigger input. + trigger_signal_shape: stretch: [2, 2, 2, 2, 2, 2] delay: [0, 0, 0, 0, 0, 0] +# Enable the LEMO clock output using a boolean. clock_lemo: - enable_clock_lemo_output: True + enable_clock_lemo_output: on + +# Set the four PMT control voltages in V pmt_control: - #PMT control voltages in V pmt_1: 0.8 pmt_2: 0.8 pmt_3: 0 pmt_4: 0 -#Save data and generate interpreted data from the raw data set. Set to 'True' or 'False'. +# Save and generate interpreted data from the raw data set. Set to 'True' or 'False'. +# If no specific output path is provided, the data is saved in the default output data path (aidatlu/aidatlu/tlu_data). save_data: True output_data_path: -#zmq connection leave it blank or set to 'off' if not needed -zmq_connection: 'off' #"tcp://127.0.0.1:6500" +# zmq connection for status messages, leave it blank or set to off if not needed +zmq_connection: off #"tcp://127.0.0.1:6500" + +# Optional stop conditions can also be added to the configuration. +# These can be a timeout in seconds or a maximum output trigger number. +# If needed just uncomment the required stop condition in the example below: +# max_trigger_number: 1000000 +# timeout: 60 diff --git a/docs/source/Documentation.rst b/docs/source/Documentation.rst index 77dbd2b..7ebf878 100644 --- a/docs/source/Documentation.rst +++ b/docs/source/Documentation.rst @@ -309,10 +309,7 @@ Tests ------ With pytest (https://docs.pytest.org/en/7.4.x/) the AIDA TLU control program can be tested. In the test directory different testing scripts can be found. -The easiest way to test the whole setup it to navigate to the directory and type pytest into the terminal. -This starts a series of testing functions that start and stop different aspects of the control software. The test setup helps to find bugs when further developing the TLU program and also to check for depreciated functions. -For now this testing needs a functioning connection to a AIDA TLU. The command: .. code-block:: console @@ -324,13 +321,18 @@ But also the individual log outputs can be displayed. .. code-block:: console - pytest -o log_cli=True + pytest -sv Tests can be run individually. - +There is also an implemented AIDA-TLU mock, to allow tests and software development without hardware. +This mock is used as a default. +To test with connected hardware set an environment variable ```HW=True````: .. code-block:: console - pytest software_test.py + HW=True pytest -sv + +The tests load the configuration file ```tlu_test_configuration.yaml```. +Individual settings in the test configuration file can not be changed. Log Level ------ diff --git a/docs/source/conf.py b/docs/source/conf.py index 7765bf9..8088fbc 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -7,13 +7,12 @@ # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information -with open("../../VERSION") as version_file: - version = version_file.read().strip() +from importlib.metadata import version as get_version project = "AIDA-TLU" copyright = "2023, SiLab, Institute of Physics, University of Bonn" author = "Rasmus Partzsch" -release = version +version = get_version("aidatlu") import os import sys diff --git a/docs/source/main_code.rst b/docs/source/main_code.rst index 3d375c4..46f79a1 100644 --- a/docs/source/main_code.rst +++ b/docs/source/main_code.rst @@ -8,14 +8,13 @@ Trigger Logic Unit .. autoclass:: aidatlu.main.tlu.AidaTLU :members: -Configuration Parser +Configuration parser #################### .. autoclass:: aidatlu.main.config_parser.TLUConfigure :members: -Data Parser +Data interpretation #################### -.. autoclass:: aidatlu.main.data_parser.DataParser - :members: +.. autofunction:: aidatlu.main.data_parser.interpret_data diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..2b2aea5 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,34 @@ +[build-system] +requires = ["setuptools >= 61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "aidatlu" +dynamic = ["version"] +license = { "text" = "AGPL-3.0" } +description = "Control software for AIDA-2020 TLU" +readme = {file = "README.md", content-type = "text/markdown"} +requires-python = ">=3.10" +authors = [ + {name = "Rasmus Partzsch", email="rasmus.partzsch@uni-bonn.de"}, + {name = "Christian Bespin", email="cbespin@uni-bonn.de"} +] +dependencies = [ + "numpy", + "tables", + "coloredlogs", + "pyzmq", + "tqdm" +] + +[project.urls] +"Documentation" = "https://silab-bonn.github.io/aidatlu/" +"Repository" = "https://github.com/SiLab-Bonn/aidatlu/" + +[project.optional-dependencies] +hw = ["online_monitor"] # uhal package needs to be installed manually! +test = ["pytest", "coverage"] +doc = ["sphinx", "myst_parser", "sphinx_mdinclude", "pydata-sphinx-theme"] + +[tool.setuptools.dynamic] +version = {attr = "aidatlu.__version__"} diff --git a/setup.py b/setup.py deleted file mode 100644 index 9116599..0000000 --- a/setup.py +++ /dev/null @@ -1,36 +0,0 @@ -from setuptools import find_packages, setup - -author = "Christian Bespin, Rasmus Partzsch" -author_email = "bespin@physik.uni-bonn.de, rasmus.partzsch@uni-bonn.de" - -# Requirements -install_requires = [ - "pytest", - "numpy", - "tables", - "coloredlogs", - "pyzmq", - "online_monitor", - "tqdm", -] - -with open("VERSION") as version_file: - version = version_file.read().strip() - -setup( - name="aidatlu", - version=version, - description="Control software for AIDA-2020 TLU", - url="https://github.com/Silab-Bonn/aidatlu", - license="License AGPL-3.0 license", - long_description="Repository for controlling the AIDA-2020 Trigger Logic Unit (TLU) with Python using uHAL bindings from IPbus.", - author=author, - maintainer=author, - author_email=author_email, - maintainer_email=author_email, - install_requires=install_requires, - python_requires=">=3.8", - packages=find_packages(), - include_package_data=True, - platforms="posix", -)