Skip to content

Commit a9888d5

Browse files
committed
Introduce a new attribute for IntermediateDevices that allows them to specify a minimum clock high time in order to allow asymmetric clock ticks when combined with gated clocks on a pseudoclock. minimum_clock_high_time defaults to half of the minimum time between IntermediateDevice instructions. It be backwards compatible with previous versions of labscript devices.
Fixes #51. I also discovered a bug where the check against the next all change time did not capture the last change time on the clock line because the stop time had not yet been added. This is now fixed. There were also some issues with various error messages. I've fixed those too and moved to use f strings so they're more readable. The whole `Pseudoclock.collect_change_times` method has also been reformatted in line with how `black` would format it (since I was working on it anyway). Some commented out code was also removed here too.
1 parent 763a0f1 commit a9888d5

File tree

1 file changed

+122
-61
lines changed

1 file changed

+122
-61
lines changed

labscript/labscript.py

+122-61
Original file line numberDiff line numberDiff line change
@@ -658,11 +658,22 @@ def __init__(self, name, parent_device, **kwargs):
658658
raise LabscriptError('Error instantiating device %s. The parent (%s) must be an instance of ClockLine.'%(name, parent_device_name))
659659
Device.__init__(self, name, parent_device, 'internal', **kwargs) # This 'internal' should perhaps be more descriptive?
660660

661+
@property
662+
def minimum_clock_high_time(self):
663+
if getattr(self, "clock_limit", None) is None:
664+
return 0
665+
666+
# Convert clock limit to minimum pulse period and then divide by 2 to
667+
# get minimum half period. This is the fastest assuming the minimum high
668+
# time corresponds to half the fastest clock pulse supported.
669+
return 1/self.clock_limit/2
670+
661671

662672
class ClockLine(Device):
663673
description = 'Generic ClockLine'
664674
allowed_children = [IntermediateDevice]
665675
_clock_limit = None
676+
_minimum_clock_high_time = 0
666677

667678
@set_passed_properties(property_names = {})
668679
def __init__(self, name, pseudoclock, connection, ramping_allowed = True, **kwargs):
@@ -675,6 +686,10 @@ def add_device(self, device):
675686
Device.add_device(self, device)
676687
if getattr(device, 'clock_limit', None) is not None and (self._clock_limit is None or device.clock_limit < self.clock_limit):
677688
self._clock_limit = device.clock_limit
689+
if getattr(device, 'minimum_clock_high_time', None) is not None:
690+
self._minimum_clock_high_time = max(
691+
device.minimum_clock_high_time, self._minimum_clock_high_time
692+
)
678693

679694
# define a property to make sure no children overwrite this value themselves
680695
# The calculation of maximum clock_limit should be done by the add_device method above
@@ -689,6 +704,10 @@ def clock_limit(self):
689704
return self.parent_device.clock_limit
690705
return self._clock_limit
691706

707+
@property
708+
def minimum_clock_high_time(self):
709+
return self._minimum_clock_high_time
710+
692711

693712
class Pseudoclock(Device):
694713
"""Parent class of all pseudoclocks.
@@ -757,113 +776,147 @@ def collect_change_times(self, all_outputs, outputs_by_clockline):
757776
change_times[clock_line].extend(output_change_times)
758777
all_change_times.extend(output_change_times)
759778
ramps_by_clockline[clock_line].extend(output.get_ramp_times())
760-
761-
# print 'initial_change_times for %s: %s'%(clock_line.name,change_times[clock_line])
762-
779+
763780
# Change to a set and back to get rid of duplicates:
764781
if not all_change_times:
765782
all_change_times.append(0)
766783
all_change_times.append(self.parent_device.stop_time)
767-
# include trigger times in change_times, so that pseudoclocks always have an instruction immediately following a wait:
784+
# include trigger times in change_times, so that pseudoclocks always
785+
# have an instruction immediately following a wait:
768786
all_change_times.extend(self.parent_device.trigger_times)
769-
770-
####################################################################################################
771-
# Find out whether any other clockline has a change time during a ramp on another clockline. #
772-
# If it does, we need to let the ramping clockline know it needs to break it's loop at that time #
773-
####################################################################################################
787+
788+
########################################################################
789+
# Find out whether any other clockline has a change time during a ramp #
790+
# on another clockline. If it does, we need to let the ramping #
791+
# clockline know it needs to break it's loop at that time #
792+
########################################################################
774793
# convert all_change_times to a numpy array
775794
all_change_times_numpy = array(all_change_times)
776-
795+
777796
# quantise the all change times to the pseudoclock clock resolution
778-
# all_change_times_numpy = (all_change_times_numpy/self.clock_resolution).round()*self.clock_resolution
779-
all_change_times_numpy = self.quantise_to_pseudoclock(all_change_times_numpy)
780-
797+
all_change_times_numpy = self.quantise_to_pseudoclock(
798+
all_change_times_numpy
799+
)
800+
781801
# Loop through each clockline
782-
# print ramps_by_clockline
783802
for clock_line, ramps in ramps_by_clockline.items():
784803
# for each clockline, loop through the ramps on that clockline
785804
for ramp_start_time, ramp_end_time in ramps:
786-
# for each ramp, check to see if there is a change time in all_change_times which intersects
787-
# with the ramp. If there is, add a change time into this clockline at that point
788-
indices = np.where((ramp_start_time < all_change_times_numpy) & (all_change_times_numpy < ramp_end_time))
805+
# for each ramp, check to see if there is a change time in
806+
# all_change_times which intersects with the ramp. If there is,
807+
# add a change time into this clockline at that point
808+
indices = np.where(
809+
(ramp_start_time < all_change_times_numpy) &
810+
(all_change_times_numpy < ramp_end_time)
811+
)
789812
for idx in indices[0]:
790813
change_times[clock_line].append(all_change_times_numpy[idx])
791-
814+
792815
# Get rid of duplicates:
793816
all_change_times = list(set(all_change_times_numpy))
794817
all_change_times.sort()
795-
818+
796819
# Check that the pseudoclock can handle updates this fast
797820
for i, t in enumerate(all_change_times[:-1]):
798821
dt = all_change_times[i+1] - t
799822
if dt < 1.0/self.clock_limit:
800-
raise LabscriptError('Commands have been issued to devices attached to %s at t= %s s and %s s. '%(self.name, str(t),str(all_change_times[i+1])) +
801-
'This Pseudoclock cannot support update delays shorter than %s sec.'%(str(1.0/self.clock_limit)))
823+
raise LabscriptError(
824+
"Commands have been issued to devices attached to "
825+
f"{self.name} at t={t} and t={all_change_times[i+1]}. "
826+
"This Pseudoclock cannot support update delays shorter "
827+
f"than {1.0/self.clock_limit} seconds."
828+
)
802829

803-
####################################################################################################
804-
# For each clockline, make sure we have a change time for triggers, stop_time, t=0 and #
805-
# check that no change tiems are too close together #
806-
####################################################################################################
830+
########################################################################
831+
# For each clockline, make sure we have a change time for triggers, #
832+
# stop_time, t=0 and check that no change times are too close together #
833+
########################################################################
807834
for clock_line, change_time_list in change_times.items():
808-
# include trigger times in change_times, so that pseudoclocks always have an instruction immediately following a wait:
835+
# include trigger times in change_times, so that pseudoclocks always
836+
# have an instruction immediately following a wait:
809837
change_time_list.extend(self.parent_device.trigger_times)
810-
838+
811839
# If the device has no children, we still need it to have a
812840
# single instruction. So we'll add 0 as a change time:
813841
if not change_time_list:
814842
change_time_list.append(0)
815-
843+
816844
# quantise the all change times to the pseudoclock clock resolution
817-
# change_time_list = (array(change_time_list)/self.clock_resolution).round()*self.clock_resolution
818845
change_time_list = self.quantise_to_pseudoclock(change_time_list)
819-
846+
820847
# Get rid of duplicates if trigger times were already in the list:
821848
change_time_list = list(set(change_time_list))
822849
change_time_list.sort()
823850

851+
# Also add the stop time as as change time. First check that it
852+
# isn't too close to the time of the last instruction:
853+
if not self.parent_device.stop_time in change_time_list:
854+
dt = self.parent_device.stop_time - change_time_list[-1]
855+
if abs(dt) < 1.0/clock_line.clock_limit:
856+
raise LabscriptError(
857+
"The stop time of the experiment is "
858+
f"t={self.parent_device.stop_time}, but the last "
859+
f"instruction for a device attached to {self.name} is "
860+
f"at t={change_time_list[-1]}. One or more connected "
861+
"devices cannot support update delays shorter than "
862+
f"{1.0/clock_line.clock_limit} seconds. Please set the "
863+
"stop_time a bit later."
864+
)
865+
866+
change_time_list.append(self.parent_device.stop_time)
867+
868+
# Sort change times so self.stop_time will be in the middle
869+
# somewhere if it is prior to the last actual instruction.
870+
# Whilst this means the user has set stop_time in error, not
871+
# catching the error here allows it to be caught later by the
872+
# specific device that has more instructions after
873+
# self.stop_time. Thus we provide the user with sligtly more
874+
# detailed error info.
875+
change_time_list.sort()
876+
824877
# index to keep track of in all_change_times
825878
j = 0
826879
# Check that no two instructions are too close together:
827880
for i, t in enumerate(change_time_list[:-1]):
828881
dt = change_time_list[i+1] - t
829882
if dt < 1.0/clock_line.clock_limit:
830-
raise LabscriptError('Commands have been issued to devices attached to clockline %s at t= %s s and %s s. '%(clock_line.name, str(t),str(change_time_list[i+1])) +
831-
'One or more connected devices on ClockLine %s cannot support update delays shorter than %s sec.'%(clock_line.name, str(1.0/clock_line.clock_limit)))
832-
883+
raise LabscriptError(
884+
"Commands have been issued to devices attached to "
885+
f"clockline {clock_line.name} at t={t} and "
886+
f"t={change_time_list[i+1]}. One or more connected "
887+
f"devices on ClockLine {clock_line.name} cannot "
888+
"support update delays shorter than "
889+
f"{1.0/clock_line.clock_limit} seconds"
890+
)
891+
833892
all_change_times_len = len(all_change_times)
834893
# increment j until we reach the current time
835894
while all_change_times[j] < t and j < all_change_times_len-1:
836895
j += 1
837896
# j should now index all_change_times at "t"
838-
# Check that the next all change_time is not too close (and thus would force this clock tick to be faster than the clock_limit)
897+
# Check that the next all change_time is not too close (and thus
898+
# would force this clock tick to be faster than the minimum
899+
# clock high time)
839900
dt = all_change_times[j+1] - t
840-
if dt < 1.0/clock_line.clock_limit:
841-
raise LabscriptError('Commands have been issued to devices attached to %s at t= %s s and %s s. '%(self.name, str(t),str(all_change_times[j+1])) +
842-
'One or more connected devices on ClockLine %s cannot support update delays shorter than %s sec.'%(clock_line.name, str(1.0/clock_line.clock_limit)))
843-
844-
# Also add the stop time as as change time. First check that it isn't too close to the time of the last instruction:
845-
if not self.parent_device.stop_time in change_time_list:
846-
dt = self.parent_device.stop_time - change_time_list[-1]
847-
if abs(dt) < 1.0/clock_line.clock_limit:
848-
raise LabscriptError('The stop time of the experiment is t= %s s, but the last instruction for a device attached to %s is at t= %s s. '%( str(self.stop_time), self.name, str(change_time_list[-1])) +
849-
'One or more connected devices cannot support update delays shorter than %s sec. Please set the stop_time a bit later.'%str(1.0/clock_line.clock_limit))
850-
851-
change_time_list.append(self.parent_device.stop_time)
852-
853-
# Sort change times so self.stop_time will be in the middle
854-
# somewhere if it is prior to the last actual instruction. Whilst
855-
# this means the user has set stop_time in error, not catching
856-
# the error here allows it to be caught later by the specific
857-
# device that has more instructions after self.stop_time. Thus
858-
# we provide the user with sligtly more detailed error info.
859-
change_time_list.sort()
860-
861-
# because we made the list into a set and back to a list, it is now a different object
862-
# so modifying it won't update the list in the dictionary.
863-
# So store the updated list in the dictionary
901+
if dt < (2 * clock_line.minimum_clock_high_time):
902+
raise LabscriptError(
903+
"Commands have been issued to devices attached to "
904+
f"{self.name} at t={t} and t={all_change_times[j+1]}. "
905+
"One or more connected devices on ClockLine "
906+
f"{clock_line.name} cannot support clock ticks with a "
907+
"digital high time shorter than "
908+
f"{clock_line.minimum_clock_high_time} which is more "
909+
"than half the available time between the event at "
910+
f"t={t} on ClockLine {clock_line.name} and the next "
911+
"event on another ClockLine."
912+
)
913+
914+
# because we made the list into a set and back to a list, it is now
915+
# a different object so modifying it won't update the list in the
916+
# dictionary. So store the updated list in the dictionary
864917
change_times[clock_line] = change_time_list
865918
return all_change_times, change_times
866-
919+
867920
def expand_change_times(self, all_change_times, change_times, outputs_by_clockline):
868921
"""For each time interval delimited by change_times, constructs
869922
an array of times at which the clock for this device needs to
@@ -2412,7 +2465,11 @@ def go_high(self):
24122465
self.add_instruction(0,1)
24132466
self._static_value = 1
24142467
else:
2415-
raise LabscriptError('%s %s has already been set to %s. It cannot also be set to %s.'%(self.description, self.name, self.instruction_to_string[self._static_value], self.instruction_to_string[value]))
2468+
raise LabscriptError(
2469+
f"{self.description} {self.name} has already been set to "
2470+
f"{self.instruction_to_string(self._static_value)}. It cannot "
2471+
"also be set to 1."
2472+
)
24162473

24172474
def go_low(self):
24182475
"""Command a static low output.
@@ -2424,7 +2481,11 @@ def go_low(self):
24242481
self.add_instruction(0,0)
24252482
self._static_value = 0
24262483
else:
2427-
raise LabscriptError('%s %s has already been set to %s. It cannot also be set to %s.'%(self.description, self.name, self.instruction_to_string[self._static_value], self.instruction_to_string[value]))
2484+
raise LabscriptError(
2485+
f"{self.description} {self.name} has already been set to "
2486+
f"{self.instruction_to_string(self._static_value)}. It cannot "
2487+
"also be set to 0."
2488+
)
24282489

24292490
def get_change_times(self):
24302491
"""Enforces no change times.

0 commit comments

Comments
 (0)