diff --git a/labscript_devices/PrawnBlaster/__init__.py b/labscript_devices/PrawnBlaster/__init__.py new file mode 100644 index 00000000..442af093 --- /dev/null +++ b/labscript_devices/PrawnBlaster/__init__.py @@ -0,0 +1,19 @@ +##################################################################### +# # +# /labscript_devices/PrawnBlaster/__init__.py # +# # +# Copyright 2021, Philip Starkey # +# # +# This file is part of labscript_devices, in the labscript suite # +# (see http://labscriptsuite.org), and is licensed under the # +# Simplified BSD License. See the license.txt file in the root of # +# the project for the full license. # +# # +##################################################################### +from labscript_devices import deprecated_import_alias + + +# For backwards compatibility with old experiment scripts: +PrawnBlaster = deprecated_import_alias( + "labscript_devices.PrawnBlaster.labscript_devices.PrawnBlaster" +) diff --git a/labscript_devices/PrawnBlaster/blacs_tabs.py b/labscript_devices/PrawnBlaster/blacs_tabs.py new file mode 100644 index 00000000..25ab73d0 --- /dev/null +++ b/labscript_devices/PrawnBlaster/blacs_tabs.py @@ -0,0 +1,127 @@ +##################################################################### +# # +# /labscript_devices/PrawnBlaster/blacs_tab.py # +# # +# Copyright 2021, Philip Starkey # +# # +# This file is part of labscript_devices, in the labscript suite # +# (see http://labscriptsuite.org), and is licensed under the # +# Simplified BSD License. See the license.txt file in the root of # +# the project for the full license. # +# # +##################################################################### +from blacs.device_base_class import ( + DeviceTab, + define_state, + MODE_BUFFERED, + MODE_MANUAL, + MODE_TRANSITION_TO_BUFFERED, + MODE_TRANSITION_TO_MANUAL, +) +import labscript_utils.properties + +from qtutils.qt import QtWidgets + + +class PrawnBlasterTab(DeviceTab): + def initialise_GUI(self): + self.connection_table_properties = ( + self.settings["connection_table"].find_by_name(self.device_name).properties + ) + + digital_outs = {} + for pin in self.connection_table_properties["out_pins"]: + digital_outs[f"GPIO {pin:02d}"] = {} + + # Create a single digital output + self.create_digital_outputs(digital_outs) + # Create widgets for output objects + _, _, do_widgets = self.auto_create_widgets() + # and auto place the widgets in the UI + self.auto_place_widgets(("Flags", do_widgets)) + + # Create status labels + self.status_label = QtWidgets.QLabel("Status: Unknown") + self.clock_status_label = QtWidgets.QLabel("Clock status: Unknown") + self.get_tab_layout().addWidget(self.status_label) + self.get_tab_layout().addWidget(self.clock_status_label) + + # Set the capabilities of this device + self.supports_smart_programming(True) + + # Create status monitor timout + self.statemachine_timeout_add(2000, self.status_monitor) + + def get_child_from_connection_table(self, parent_device_name, port): + # Pass down channel name search to the pseudoclocks (so we can find the + # clocklines) + if parent_device_name == self.device_name: + device = self.connection_table.find_by_name(self.device_name) + + for pseudoclock_name, pseudoclock in device.child_list.items(): + for child_name, child in pseudoclock.child_list.items(): + # store a reference to the internal clockline + if child.parent_port == port: + return DeviceTab.get_child_from_connection_table( + self, pseudoclock.name, port + ) + + return None + + def initialise_workers(self): + # Find the COM port to be used + com_port = str( + self.settings["connection_table"] + .find_by_name(self.device_name) + .BLACS_connection + ) + + worker_initialisation_kwargs = { + "com_port": com_port, + "num_pseudoclocks": self.connection_table_properties["num_pseudoclocks"], + "out_pins": self.connection_table_properties["out_pins"], + "in_pins": self.connection_table_properties["in_pins"], + } + self.create_worker( + "main_worker", + "labscript_devices.PrawnBlaster.blacs_workers.PrawnBlasterWorker", + worker_initialisation_kwargs, + ) + self.primary_worker = "main_worker" + + @define_state( + MODE_MANUAL + | MODE_BUFFERED + | MODE_TRANSITION_TO_BUFFERED + | MODE_TRANSITION_TO_MANUAL, + True, + ) + def status_monitor(self, notify_queue=None): + # When called with a queue, this function writes to the queue + # when the pulseblaster is waiting. This indicates the end of + # an experimental run. + status, clock_status, waits_pending = yield ( + self.queue_work(self.primary_worker, "check_status") + ) + + # Manual mode or aborted + done_condition = status == 0 or status == 5 + + # Update GUI status/clock status widgets + self.status_label.setText(f"Status: {status}") + self.clock_status_label.setText(f"Clock status: {clock_status}") + + if notify_queue is not None and done_condition and not waits_pending: + # Experiment is over. Tell the queue manager about it, then + # set the status checking timeout back to every 2 seconds + # with no queue. + notify_queue.put("done") + self.statemachine_timeout_remove(self.status_monitor) + self.statemachine_timeout_add(2000, self.status_monitor) + + @define_state(MODE_BUFFERED, True) + def start_run(self, notify_queue): + self.statemachine_timeout_remove(self.status_monitor) + yield (self.queue_work(self.primary_worker, "start_run")) + self.status_monitor() + self.statemachine_timeout_add(100, self.status_monitor, notify_queue) diff --git a/labscript_devices/PrawnBlaster/blacs_workers.py b/labscript_devices/PrawnBlaster/blacs_workers.py new file mode 100644 index 00000000..6c0bc957 --- /dev/null +++ b/labscript_devices/PrawnBlaster/blacs_workers.py @@ -0,0 +1,346 @@ +##################################################################### +# # +# /labscript_devices/PrawnBlaster/blacs_worker.py # +# # +# Copyright 2021, Philip Starkey # +# # +# This file is part of labscript_devices, in the labscript suite # +# (see http://labscriptsuite.org), and is licensed under the # +# Simplified BSD License. See the license.txt file in the root of # +# the project for the full license. # +# # +##################################################################### +import time +import labscript_utils.h5_lock +import h5py +from blacs.tab_base_classes import Worker +from labscript_utils.connections import _ensure_str +import labscript_utils.properties as properties + + +class PrawnBlasterWorker(Worker): + def init(self): + # fmt: off + global h5py; import labscript_utils.h5_lock, h5py + global serial; import serial + global time; import time + global re; import re + global numpy; import numpy + global zprocess; import zprocess + self.smart_cache = {} + self.cached_pll_params = {} + # fmt: on + + self.all_waits_finished = zprocess.Event("all_waits_finished", type="post") + self.wait_durations_analysed = zprocess.Event( + "wait_durations_analysed", type="post" + ) + self.wait_completed = zprocess.Event("wait_completed", type="post") + self.current_wait = 0 + self.wait_table = None + self.measured_waits = None + self.wait_timeout = None + self.h5_file = None + self.started = False + + self.prawnblaster = serial.Serial(self.com_port, 115200, timeout=1) + self.check_status() + + # configure number of pseudoclocks + self.prawnblaster.write(b"setnumpseudoclocks %d\r\n" % self.num_pseudoclocks) + assert self.prawnblaster.readline().decode() == "ok\r\n" + + # Configure pins + for i, (out_pin, in_pin) in enumerate(zip(self.out_pins, self.in_pins)): + self.prawnblaster.write(b"setoutpin %d %d\r\n" % (i, out_pin)) + assert self.prawnblaster.readline().decode() == "ok\r\n" + self.prawnblaster.write(b"setinpin %d %d\r\n" % (i, in_pin)) + assert self.prawnblaster.readline().decode() == "ok\r\n" + + def check_status(self): + if ( + self.started + and self.wait_table is not None + and self.current_wait < len(self.wait_table) + ): + # Try to read out wait. For now, we're only reading out waits from + # pseudoclock 0 since they should all be the same (requirement imposed by labscript) + self.prawnblaster.write(b"getwait %d %d\r\n" % (0, self.current_wait)) + response = self.prawnblaster.readline().decode() + if response != "wait not yet available\r\n": + # Parse the response from the PrawnBlaster + wait_remaining = int(response) + # Divide by two since the clock_resolution is for clock pulses, which + # have twice the clock_resolution of waits + # Technically, waits also only have a resolution of `clock_resolution` + # but the PrawnBlaster firmware accepts them in half of that so that + # they are easily converted to seconds via the clock frequency. + # Maybe this was a mistake, but it's done now. + clock_resolution = self.device_properties["clock_resolution"] / 2 + input_response_time = self.device_properties["input_response_time"] + timeout_length = round( + self.wait_table[self.current_wait]["timeout"] / clock_resolution + ) + + if wait_remaining == (2 ** 32 - 1): + # The wait hit the timeout - save the timeout duration as wait length + # and flag that this wait timedout + self.measured_waits[self.current_wait] = ( + timeout_length * clock_resolution + ) + self.wait_timeout[self.current_wait] = True + else: + # Calculate wait length + # This is a measurement of between the end of the last pulse and the + # retrigger signal. We obtain this by subtracting off the time it takes + # to detect the pulse in the ASM code once the trigger has hit the input + # pin (stored in input_response_time) + self.measured_waits[self.current_wait] = ( + (timeout_length - wait_remaining) * clock_resolution + ) - input_response_time + self.wait_timeout[self.current_wait] = False + + self.logger.info( + f"Wait {self.current_wait} finished. Length={self.measured_waits[self.current_wait]:.9f}s. Timed-out={self.wait_timeout[self.current_wait]}" + ) + + # Inform any interested parties that a wait has completed: + self.wait_completed.post( + self.h5_file, + data=_ensure_str(self.wait_table[self.current_wait]["label"]), + ) + + # increment the wait we are looking for! + self.current_wait += 1 + + # post message if all waits are done + if len(self.wait_table) == self.current_wait: + self.logger.info("All waits finished") + self.all_waits_finished.post(self.h5_file) + + # Determine if we are still waiting for wait information + waits_pending = False + if self.wait_table is not None: + if self.current_wait == len(self.wait_table): + waits_pending = False + else: + waits_pending = True + + run_status, clock_status = self.read_status() + return run_status, clock_status, waits_pending + + def read_status(self): + self.prawnblaster.write(b"status\r\n") + response = self.prawnblaster.readline().decode() + match = re.match(r"run-status:(\d) clock-status:(\d)(\r\n)?", response) + if match: + return int(match.group(1)), int(match.group(2)) + elif response: + raise Exception( + f"PrawnBlaster is confused: saying '{response}' instead of 'run-status: clock-status:'" + ) + else: + raise Exception( + f"PrawnBlaster is returning a invalid status '{response}'. Maybe it needs a reboot." + ) + + def program_manual(self, values): + for channel, value in values.items(): + pin = int(channel.split()[1]) + pseudoclock = self.out_pins.index(pin) + if value: + self.prawnblaster.write(b"go high %d\r\n" % pseudoclock) + else: + self.prawnblaster.write(b"go low %d\r\n" % pseudoclock) + + assert self.prawnblaster.readline().decode() == "ok\r\n" + + return values + + def transition_to_buffered(self, device_name, h5file, initial_values, fresh): + if fresh: + self.smart_cache = {} + + # fmt: off + self.h5_file = h5file # store reference to h5 file for wait monitor + self.current_wait = 0 # reset wait analysis + self.started = False # Prevent status check from detecting previous wait values + # betwen now and when we actually send the start signal + # fmt: on + + # Get data from HDF5 file + pulse_programs = [] + with h5py.File(h5file, "r") as hdf5_file: + group = hdf5_file[f"devices/{device_name}"] + for i in range(self.num_pseudoclocks): + pulse_programs.append(group[f"PULSE_PROGRAM_{i}"][:]) + self.smart_cache.setdefault(i, []) + self.device_properties = labscript_utils.properties.get( + hdf5_file, device_name, "device_properties" + ) + self.is_master_pseudoclock = self.device_properties["is_master_pseudoclock"] + + # waits + dataset = hdf5_file["waits"] + acquisition_device = dataset.attrs["wait_monitor_acquisition_device"] + timeout_device = dataset.attrs["wait_monitor_timeout_device"] + if ( + len(dataset) > 0 + and acquisition_device + == "%s_internal_wait_monitor_outputs" % device_name + and timeout_device == "%s_internal_wait_monitor_outputs" % device_name + ): + self.wait_table = dataset[:] + self.measured_waits = numpy.zeros(len(self.wait_table)) + self.wait_timeout = numpy.zeros(len(self.wait_table), dtype=bool) + else: + self.wait_table = ( + None # This device doesn't need to worry about looking at waits + ) + self.measured_waits = None + self.wait_timeout = None + + # Configure clock from device properties + clock_mode = 0 + if self.device_properties["external_clock_pin"] is not None: + if self.device_properties["external_clock_pin"] == 20: + clock_mode = 1 + elif self.device_properties["external_clock_pin"] == 22: + clock_mode = 2 + else: + raise RuntimeError( + f"Invalid external clock pin {self.device_properties['external_clock_pin']}. Pin must be 20, 22 or None." + ) + clock_frequency = self.device_properties["clock_frequency"] + + # Now set the clock details + self.prawnblaster.write(b"setclock %d %d\r\n" % (clock_mode, clock_frequency)) + response = self.prawnblaster.readline().decode() + assert response == "ok\r\n", f"PrawnBlaster said '{response}', expected 'ok'" + + # Program instructions + for pseudoclock, pulse_program in enumerate(pulse_programs): + for i, instruction in enumerate(pulse_program): + if i == len(self.smart_cache[pseudoclock]): + # Pad the smart cache out to be as long as the program: + self.smart_cache[pseudoclock].append(None) + + # Only program instructions that differ from what's in the smart cache: + if self.smart_cache[pseudoclock][i] != instruction: + self.prawnblaster.write( + b"set %d %d %d %d\r\n" + % ( + pseudoclock, + i, + instruction["half_period"], + instruction["reps"], + ) + ) + response = self.prawnblaster.readline().decode() + assert ( + response == "ok\r\n" + ), f"PrawnBlaster said '{response}', expected 'ok'" + self.smart_cache[pseudoclock][i] = instruction + + if not self.is_master_pseudoclock: + # Start the Prawnblaster and have it wait for a hardware trigger + self.wait_for_trigger() + + # All outputs end on 0 + final = {} + for pin in self.out_pins: + final[f"GPIO {pin:02d}"] = 0 + return final + + def start_run(self): + # Start in software: + self.logger.info("sending start") + self.prawnblaster.write(b"start\r\n") + response = self.prawnblaster.readline().decode() + assert response == "ok\r\n", f"PrawnBlaster said '{response}', expected 'ok'" + + # set started = True + self.started = True + + def wait_for_trigger(self): + # Set to wait for trigger: + self.logger.info("sending hwstart") + self.prawnblaster.write(b"hwstart\r\n") + response = self.prawnblaster.readline().decode() + assert response == "ok\r\n", f"PrawnBlaster said '{response}', expected 'ok'" + + running = False + while not running: + run_status, clock_status = self.read_status() + # If we are running, great, the PrawnBlaster is waiting for a trigger + if run_status == 2: + running = True + # if we are not in TRANSITION_TO_RUNNING, then something has gone wrong + # and we should raise an exception + elif run_status != 1: + raise RuntimeError( + f"Prawnblaster did not return an expected status. Status was {run_status}" + ) + time.sleep(0.01) + + # set started = True + self.started = True + + def transition_to_manual(self): + if self.wait_table is not None: + with h5py.File(self.h5_file, "a") as hdf5_file: + # Work out how long the waits were, save em, post an event saying so + dtypes = [ + ("label", "a256"), + ("time", float), + ("timeout", float), + ("duration", float), + ("timed_out", bool), + ] + data = numpy.empty(len(self.wait_table), dtype=dtypes) + data["label"] = self.wait_table["label"] + data["time"] = self.wait_table["time"] + data["timeout"] = self.wait_table["timeout"] + data["duration"] = self.measured_waits + data["timed_out"] = self.wait_timeout + + self.logger.info(str(data)) + + hdf5_file.create_dataset("/data/waits", data=data) + + self.wait_durations_analysed.post(self.h5_file) + + # If PrawnBlaster is master pseudoclock, then it will have it's status checked + # in the BLACS tab status check before any transition to manual is called. + # However, if it's not the master pseudoclock, we need to check here instead! + if not self.is_master_pseudoclock: + # Wait until shot completes + while True: + run_status, clock_status = self.read_status() + if run_status == 0: + break + if run_status in [3, 4, 5]: + raise RuntimeError( + f"Prawnblaster status returned run-status={run_status} during transition to manual" + ) + time.sleep(0.01) + + return True + + def shutdown(self): + self.prawnblaster.close() + + def abort_buffered(self): + if not self.is_master_pseudoclock: + # Only need to send abort signal if we have told the PrawnBlaster to wait + # for a hardware trigger. Otherwise it's just been programmed with + # instructions and there is nothing we need to do to abort. + self.prawnblaster.write(b"abort\r\n") + assert self.prawnblaster.readline().decode() == "ok\r\n" + # loop until abort complete + while self.read_status()[0] != 5: + time.sleep(0.5) + return True + + def abort_transition_to_buffered(self): + return self.abort_buffered() diff --git a/labscript_devices/PrawnBlaster/labscript_devices.py b/labscript_devices/PrawnBlaster/labscript_devices.py new file mode 100644 index 00000000..eb05b79c --- /dev/null +++ b/labscript_devices/PrawnBlaster/labscript_devices.py @@ -0,0 +1,376 @@ +##################################################################### +# # +# /labscript_devices/PrawnBlaster/labscript_devices.py # +# # +# Copyright 2021, Philip Starkey # +# # +# This file is part of labscript_devices, in the labscript suite # +# (see http://labscriptsuite.org), and is licensed under the # +# Simplified BSD License. See the license.txt file in the root of # +# the project for the full license. # +# # +##################################################################### + +import copy + +from labscript import ( + ClockLine, + IntermediateDevice, + LabscriptError, + PseudoclockDevice, + Pseudoclock, + WaitMonitor, + compiler, + config, + set_passed_properties, +) +import numpy as np + + +class _PrawnBlasterPseudoclock(Pseudoclock): + def __init__(self, i, *args, **kwargs): + super().__init__(*args, **kwargs) + self.i = i + + def add_device(self, device): + if isinstance(device, ClockLine): + # only allow one child + if self.child_devices: + raise LabscriptError( + f"Each pseudoclock of the PrawnBlaster {self.parent_device.name} only supports 1 clockline, which is automatically created. Please use the clockline located at {self.parent_device.name}.clockline[{self.i}]" + ) + Pseudoclock.add_device(self, device) + else: + raise LabscriptError( + f"You have connected {device.name} to {self.name} (a Pseudoclock of {self.parent_device.name}), but {self.name} only supports children that are ClockLines. Please connect your device to {self.parent_device.name}.clockline[{self.i}] instead." + ) + + +# +# Define dummy pseudoclock/clockline/intermediatedevice to trick wait monitor +# since everything is handled internally in this device +# +class _PrawnBlasterDummyPseudoclock(Pseudoclock): + def add_device(self, device): + if isinstance(device, _PrawnBlasterDummyClockLine): + if self.child_devices: + raise LabscriptError( + f"You are trying to access the special, dummy, PseudoClock of the PrawnBlaster {self.pseudoclock_device.name}. This is for internal use only." + ) + Pseudoclock.add_device(self, device) + else: + raise LabscriptError( + f"You are trying to access the special, dummy, PseudoClock of the PrawnBlaster {self.pseudoclock_device.name}. This is for internal use only." + ) + + # do nothing, this is a dummy class! + def generate_code(self, *args, **kwargs): + pass + + +class _PrawnBlasterDummyClockLine(ClockLine): + def add_device(self, device): + if isinstance(device, _PrawnBlasterDummyIntermediateDevice): + if self.child_devices: + raise LabscriptError( + f"You are trying to access the special, dummy, ClockLine of the PrawnBlaster {self.pseudoclock_device.name}. This is for internal use only." + ) + ClockLine.add_device(self, device) + else: + raise LabscriptError( + f"You are trying to access the special, dummy, ClockLine of the PrawnBlaster {self.pseudoclock_device.name}. This is for internal use only." + ) + + # do nothing, this is a dummy class! + def generate_code(self, *args, **kwargs): + pass + + +class _PrawnBlasterDummyIntermediateDevice(IntermediateDevice): + def add_device(self, device): + if isinstance(device, WaitMonitor): + IntermediateDevice.add_device(self, device) + else: + raise LabscriptError( + "You can only connect an instance of WaitMonitor to the device %s.internal_wait_monitor_outputs" + % (self.pseudoclock_device.name) + ) + + # do nothing, this is a dummy class! + def generate_code(self, *args, **kwargs): + pass + + +class PrawnBlaster(PseudoclockDevice): + description = "PrawnBlaster" + clock_limit = 1 / 100e-9 + clock_resolution = 20e-9 + # There appears to be ~50ns buffer on input and then we know there is 80ns between + # trigger detection and first output pulse + input_response_time = 50e-9 + trigger_delay = input_response_time + 80e-9 + # Overestimate that covers indefinite waits (which labscript does not yet support) + trigger_minimum_duration = 160e-9 + # There are 4 ASM instructions between end of pulse and being ready to detect + # a retrigger + wait_delay = 40e-9 + allowed_children = [_PrawnBlasterPseudoclock, _PrawnBlasterDummyPseudoclock] + max_instructions = 30000 + + @set_passed_properties( + property_names={ + "connection_table_properties": [ + "com_port", + "in_pins", + "out_pins", + "num_pseudoclocks", + ], + "device_properties": [ + "clock_frequency", + "external_clock_pin", + "clock_limit", + "clock_resolution", + "input_response_time", + "trigger_delay", + "trigger_minimum_duration", + "wait_delay", + "max_instructions", + ], + } + ) + def __init__( + self, + name, + trigger_device=None, + trigger_connection=None, + com_port="COM1", + num_pseudoclocks=1, + out_pins=None, + in_pins=None, + clock_frequency=100e6, + external_clock_pin=None, + use_wait_monitor=True, + ): + # Check number of pseudoclocks is within range + if num_pseudoclocks < 1 or num_pseudoclocks > 4: + raise LabscriptError( + f"The PrawnBlaster {name} only supports between 1 and 4 pseudoclocks" + ) + + # Update the specs based on the number of pseudoclocks + self.max_instructions = self.max_instructions // num_pseudoclocks + # Update the specs based on the clock frequency + if self.clock_resolution != 2 / clock_frequency: + factor = (2 / clock_frequency) / self.clock_resolution + self.clock_limit *= factor + self.clock_resolution *= factor + self.input_response_time *= factor + self.trigger_delay *= factor + self.trigger_minimum_duration *= factor + self.wait_delay *= factor + + # Instantiate the base class + PseudoclockDevice.__init__(self, name, trigger_device, trigger_connection) + self.num_pseudoclocks = num_pseudoclocks + # Wait monitor can only be used if this is the master pseudoclock + self.use_wait_monitor = use_wait_monitor and self.is_master_pseudoclock + + # Set the BLACS connections + self.BLACS_connection = com_port + + # Check in/out pins + if out_pins is None: + out_pins = [9, 11, 13, 15] + if in_pins is None: + in_pins = [0, 0, 0, 0] + if len(out_pins) < num_pseudoclocks: + raise LabscriptError( + f"The PrawnBlaster {self.name} is configured with {num_pseudoclocks} but only has pin numbers specified for {len(out_pins)}." + ) + else: + self.out_pins = out_pins[:num_pseudoclocks] + if len(in_pins) < num_pseudoclocks: + raise LabscriptError( + f"The PrawnBlaster {self.name} is configured with {num_pseudoclocks} but only has pin numbers specified for {len(in_pins)}." + ) + else: + self.in_pins = in_pins[:num_pseudoclocks] + + self._pseudoclocks = [] + self._clocklines = [] + for i in range(num_pseudoclocks): + self._pseudoclocks.append( + _PrawnBlasterPseudoclock( + i, + name=f"{name}_pseudoclock_{i}", + pseudoclock_device=self, + connection=f"pseudoclock {i}", + ) + ) + self._clocklines.append( + ClockLine( + name=f"{name}_clock_line_{i}", + pseudoclock=self._pseudoclocks[i], + connection=f"GPIO {self.out_pins[i]}", + ) + ) + + if self.use_wait_monitor: + # Create internal devices for connecting to a wait monitor + self.__wait_monitor_dummy_pseudoclock = _PrawnBlasterDummyPseudoclock( + "%s__dummy_wait_pseudoclock" % name, self, "_" + ) + self.__wait_monitor_dummy_clock_line = _PrawnBlasterDummyClockLine( + "%s__dummy_wait_clock_line" % name, + self.__wait_monitor_dummy_pseudoclock, + "_", + ) + self.__wait_monitor_intermediate_device = ( + _PrawnBlasterDummyIntermediateDevice( + "%s_internal_wait_monitor_outputs" % name, + self.__wait_monitor_dummy_clock_line, + ) + ) + + # Create the wait monitor + WaitMonitor( + "%s__wait_monitor" % name, + self.internal_wait_monitor_outputs, + "internal", + self.internal_wait_monitor_outputs, + "internal", + self.internal_wait_monitor_outputs, + "internal", + ) + + @property + def internal_wait_monitor_outputs(self): + return self.__wait_monitor_intermediate_device + + @property + def pseudoclocks(self): + return copy.copy(self._pseudoclocks) + + @property + def clocklines(self): + return copy.copy(self._clocklines) + + def add_device(self, device): + if len(self.child_devices) < ( + self.num_pseudoclocks + self.use_wait_monitor + ) and isinstance( + device, (_PrawnBlasterPseudoclock, _PrawnBlasterDummyPseudoclock) + ): + PseudoclockDevice.add_device(self, device) + elif isinstance(device, _PrawnBlasterPseudoclock): + raise LabscriptError( + f"The {self.description} {self.name} automatically creates the correct number of pseudoclocks." + + "Instead of instantiating your own Pseudoclock object, please use the internal" + + f" ones stored in {self.name}.pseudoclocks" + ) + else: + raise LabscriptError( + f"You have connected {device.name} (class {device.__class__}) to {self.name}, but {self.name} does not support children with that class." + ) + + def generate_code(self, hdf5_file): + PseudoclockDevice.generate_code(self, hdf5_file) + group = self.init_device_group(hdf5_file) + + current_wait_index = 0 + wait_table = sorted(compiler.wait_table) + + # For each pseudoclock + for i, pseudoclock in enumerate(self.pseudoclocks): + current_wait_index = 0 + + # Compress clock instructions with the same half_period + reduced_instructions = [] + for instruction in pseudoclock.clock: + if instruction == "WAIT": + # If we're using the internal wait monitor, set the timeout + if self.use_wait_monitor: + # Get the wait timeout value + wait_timeout = compiler.wait_table[ + wait_table[current_wait_index] + ][1] + current_wait_index += 1 + # The following half_period and reps indicates a wait instruction + reduced_instructions.append( + { + "half_period": round( + wait_timeout / (self.clock_resolution / 2) + ), + "reps": 0, + } + ) + continue + # Else, set an indefinite wait and wait for a trigger from something else. + else: + # Two waits in a row are an indefinite wait + reduced_instructions.append( + { + "half_period": 2 ** 32 - 1, + "reps": 0, + } + ) + reduced_instructions.append( + { + "half_period": 2 ** 32 - 1, + "reps": 0, + } + ) + + # Normal instruction + reps = instruction["reps"] + # half_period is in quantised units: + half_period = int(round(instruction["step"] / self.clock_resolution)) + if ( + # If there is a previous instruction + reduced_instructions + # And it's not a wait + and reduced_instructions[-1]["reps"] != 0 + # And the half_periods match + and reduced_instructions[-1]["half_period"] == half_period + # And the sum of the previous reps and current reps won't push it over the limit + and (reduced_instructions[-1]["reps"] + reps) < (2 ** 32 - 1) + ): + # Combine instructions! + reduced_instructions[-1]["reps"] += reps + else: + # New instruction + reduced_instructions.append( + {"half_period": half_period, "reps": reps} + ) + + # Only add this if there is room in the instruction table. The PrawnBlaster + # firmware has extre room at the end for an instruction that is always 0 + # and cannot be set over serial! + if len(reduced_instructions) != self.max_instructions: + # The following half_period and reps indicates a stop instruction: + reduced_instructions.append({"half_period": 0, "reps": 0}) + + # Check we have not exceeded the maximum number of supported instructions + # for this number of speudoclocks + if len(reduced_instructions) > self.max_instructions: + raise LabscriptError( + f"{self.description} {self.name}.clocklines[{i}] has too many instructions. It has {len(reduced_instructions)} and can only support {self.max_instructions}" + ) + + # Store these instructions to the h5 file: + dtypes = [("half_period", int), ("reps", int)] + pulse_program = np.zeros(len(reduced_instructions), dtype=dtypes) + for j, instruction in enumerate(reduced_instructions): + pulse_program[j]["half_period"] = instruction["half_period"] + pulse_program[j]["reps"] = instruction["reps"] + group.create_dataset( + f"PULSE_PROGRAM_{i}", compression=config.compression, data=pulse_program + ) + + # This is needed so the BLACS worker knows whether or not to be a wait monitor + self.set_property( + "is_master_pseudoclock", + self.is_master_pseudoclock, + location="device_properties", + ) + self.set_property("stop_time", self.stop_time, location="device_properties") diff --git a/labscript_devices/PrawnBlaster/register_classes.py b/labscript_devices/PrawnBlaster/register_classes.py new file mode 100644 index 00000000..f94be1ad --- /dev/null +++ b/labscript_devices/PrawnBlaster/register_classes.py @@ -0,0 +1,23 @@ +##################################################################### +# # +# /labscript_devices/PrawnBlaster/register_classes.py # +# # +# Copyright 2021, Philip Starkey # +# # +# This file is part of labscript_devices, in the labscript suite # +# (see http://labscriptsuite.org), and is licensed under the # +# Simplified BSD License. See the license.txt file in the root of # +# the project for the full license. # +# # +##################################################################### +import labscript_devices + +labscript_device_name = 'PrawnBlaster' +blacs_tab = 'labscript_devices.PrawnBlaster.blacs_tabs.PrawnBlasterTab' +parser = 'labscript_devices.PrawnBlaster.runviewer_parsers.PrawnBlasterParser' + +labscript_devices.register_classes( + labscript_device_name=labscript_device_name, + BLACS_tab=blacs_tab, + runviewer_parser=parser, +) diff --git a/labscript_devices/PrawnBlaster/runviewer_parsers.py b/labscript_devices/PrawnBlaster/runviewer_parsers.py new file mode 100644 index 00000000..42f9e552 --- /dev/null +++ b/labscript_devices/PrawnBlaster/runviewer_parsers.py @@ -0,0 +1,98 @@ +##################################################################### +# # +# /labscript_devices/PrawnBlaster/runviewer_parsers.py # +# # +# Copyright 2021, Philip Starkey # +# # +# This file is part of labscript_devices, in the labscript suite # +# (see http://labscriptsuite.org), and is licensed under the # +# Simplified BSD License. See the license.txt file in the root of # +# the project for the full license. # +# # +##################################################################### + +import labscript_utils.h5_lock # noqa: F401 +import h5py +import numpy as np + +import labscript_utils.properties as properties + + +class PrawnBlasterParser(object): + def __init__(self, path, device): + self.path = path + self.name = device.name + self.device = device + + def get_traces(self, add_trace, clock=None): + if clock is not None: + times, clock_value = clock[0], clock[1] + clock_indices = np.where((clock_value[1:] - clock_value[:-1]) == 1)[0] + 1 + # If initial clock value is 1, then this counts as a rising edge + # (clock should be 0 before experiment) but this is not picked up + # by the above code. So we insert it! + if clock_value[0] == 1: + clock_indices = np.insert(clock_indices, 0, 0) + clock_ticks = times[clock_indices] + + # get the pulse program + pulse_programs = [] + with h5py.File(self.path, "r") as f: + # Get the device properties + device_props = properties.get(f, self.name, "device_properties") + conn_props = properties.get(f, self.name, "connection_table_properties") + + self.clock_resolution = device_props["clock_resolution"] + self.trigger_delay = device_props["trigger_delay"] + self.wait_delay = device_props["wait_delay"] + + # Extract the pulse programs + num_pseudoclocks = conn_props["num_pseudoclocks"] + for i in range(num_pseudoclocks): + pulse_programs.append(f[f"devices/{self.name}/PULSE_PROGRAM_{i}"][:]) + + # Generate clocklines and triggers + clocklines_and_triggers = {} + for pulse_program in pulse_programs: + time = [] + states = [] + trigger_index = 0 + t = 0 if clock is None else clock_ticks[trigger_index] + self.trigger_delay + trigger_index += 1 + + clock_factor = self.clock_resolution / 2.0 + + last_instruction_was_wait = False + for row in pulse_program: + if row["reps"] == 0 and not last_instruction_was_wait: # WAIT + last_instruction_was_wait = True + if clock is not None: + t = clock_ticks[trigger_index] + self.trigger_delay + trigger_index += 1 + else: + t += self.wait_delay + elif last_instruction_was_wait: + # two waits in a row means an indefinite wait, so we just skip this + # instruction. + last_instruction_was_wait = False + continue + else: + last_instruction_was_wait = False + for i in range(row["reps"]): + for j in range(1, -1, -1): + time.append(t) + states.append(j) + t += row["half_period"] * clock_factor + + clock = (np.array(time), np.array(states)) + + for pseudoclock_name, pseudoclock in self.device.child_list.items(): + for clock_line_name, clock_line in pseudoclock.child_list.items(): + # Ignore the dummy internal wait monitor clockline + if clock_line.parent_port.startswith("GPIO"): + clocklines_and_triggers[clock_line_name] = clock + add_trace( + clock_line_name, clock, self.name, clock_line.parent_port + ) + + return clocklines_and_triggers