diff --git a/ADwinProII/blacs_workers.py b/ADwinProII/blacs_workers.py index 74dd07fb0f133a334f8bcb4c044a69b56698c4a1..c2db3c2ee7ed954cb08d42f6a2964c08110c8c38 100644 --- a/ADwinProII/blacs_workers.py +++ b/ADwinProII/blacs_workers.py @@ -29,7 +29,7 @@ class ADwinProIIWorker(Worker): def init(self): self.timing = None self.h5file = None - self.smart_cache = {"AOUT":None, "PIDs":None, "AIN":None} + self.smart_cache = {"AOUT":None, "PIDs":None, "PID_CONFIG":None, "AIN":None} self.smart_cache.update({DIO:None for DIO in self.DIO_ADwin_DataNo}) self.process_number_buffered = int(self.process_buffered[-1]) self.process_number_manual = int(self.process_manual[-1]) @@ -164,11 +164,19 @@ class ADwinProIIWorker(Worker): self.adw.SetData_Long(PIDs["n_cycles"], 4, 1, PIDs.shape[0]) self.adw.SetData_Long(PIDs["AOUT_channel"], 5, 1, PIDs.shape[0]) self.adw.SetData_Long(PIDs["PID_channel"], 6, 1, PIDs.shape[0]) - self.adw.SetData_Float(PIDs["PID_P"], 25, 1, PIDs.shape[0]) - self.adw.SetData_Float(PIDs["PID_I"], 26, 1, PIDs.shape[0]) - self.adw.SetData_Float(PIDs["PID_D"], 27, 1, PIDs.shape[0]) - self.adw.SetData_Long(PIDs["PID_min"], 28, 1, PIDs.shape[0]) - self.adw.SetData_Long(PIDs["PID_max"], 29, 1, PIDs.shape[0]) + self.adw.SetData_Long(PIDs["PID_start"], 30, 1, PIDs.shape[0]) + PID_config = group["ANALOG_OUT/PID_CONFIG"] + if fresh or not np.array_equal(PID_config[:],self.smart_cache["PID_CONFIG"]): + print("PID_CONFIG programmed.") + self.smart_cache["PID_CONFIG"] = PID_config[:] + n_PID = PID_config.shape[0] + self.adw.Set_Par(22,n_PID) + self.adw.SetData_Long(PID_config["PID_channel"], 24, 1, n_PID) + self.adw.SetData_Float(PID_config["PID_P"], 25, 1, n_PID) + self.adw.SetData_Float(PID_config["PID_I"], 26, 1, n_PID) + self.adw.SetData_Float(PID_config["PID_D"], 27, 1, n_PID) + self.adw.SetData_Long(PID_config["PID_min"], 28, 1, n_PID) + self.adw.SetData_Long(PID_config["PID_max"], 29, 1, n_PID) AIN = group["ANALOG_IN/TIMES"] if fresh or not np.array_equal(AIN[:],self.smart_cache["AIN"]): print("AIN programmed.") diff --git a/ADwinProII/labscript_devices.py b/ADwinProII/labscript_devices.py index d1bee26ac79993ac01ba804af3866b9d46de0644..f74a85b4056302be67bca20f1bfae9040378ece7 100644 --- a/ADwinProII/labscript_devices.py +++ b/ADwinProII/labscript_devices.py @@ -232,12 +232,14 @@ class ADwinProII(PseudoclockDevice): # Lists to collect instructions of analog channels analog_output = [] PID_channels = [] + PID_config = [] analog_input = [] for device in self.modules: if isinstance(device,ADwinAO8): analog_output.append(device.outputs) - PID_channels.append(device.PID_channels) + PID_channels.append(device.PID_table) + PID_config.append(device.PID_config) elif isinstance(device,ADwinDIO32): group.create_dataset("DIGITAL_OUT/"+device.name, data=device.digital_data) elif isinstance(device,ADwinAI8): @@ -252,14 +254,17 @@ class ADwinProII(PseudoclockDevice): PID_channels.append(np.full(1,last_values,dtype=PID_channels[0].dtype)) # Concatenate arrays PID_channels = np.concatenate(PID_channels) + PID_config = np.concatenate(PID_config) analog_output = np.concatenate(analog_output) analog_input = np.concatenate(analog_input) # Sort analog outputs and PID settings analog_output = np.sort(analog_output, axis=0, order="n_cycles") PID_channels = np.sort(PID_channels, axis=0, order="n_cycles") + PID_config = np.sort(PID_config, axis=0, order="PID_channel") # Save datasets AO_group.create_dataset('VALUES', compression=config.compression, data=analog_output) AO_group.create_dataset('PID_CHANNELS', compression=config.compression, data=PID_channels) + AO_group.create_dataset('PID_CONFIG', compression=config.compression, data=PID_config) AI_group.create_dataset('TIMES', data=analog_input) AI_datapoints = analog_input["stop_time"] - analog_input["start_time"] diff --git a/ADwinProII/labscript_devices_ADwin_modules.py b/ADwinProII/labscript_devices_ADwin_modules.py index 38725634bec2dd7d1871446f56c9652ca04a5ec0..48ff106d522e9bd4cec07a83a5a8b6340288c3dc 100644 --- a/ADwinProII/labscript_devices_ADwin_modules.py +++ b/ADwinProII/labscript_devices_ADwin_modules.py @@ -96,17 +96,13 @@ class ADwinAnalogOut(AnalogOut): ) self.PID = {} # instructions for PID settings - - def set_PID(self,t,PID_AnalogIn,P=0,I=0,D=0,limits=None): - """(De-)activate PID for analog output, or change settings + def init_PID(self,pid_no,P=0,I=0,D=0,limits=None): + """Set parameters for PID once at beginning of shot. Parameters ---------- - t : float - Time when to apply the PID settings - PID_AnalogIn : int or `AnalogIn` or None + pid_no : int or `AnalogIn` Channel of analog input for error siganl of PID feedback. - If `None` PID is deactivated. P : float Proportional parameter I : float @@ -116,27 +112,62 @@ class ADwinAnalogOut(AnalogOut): limits : tuple of float, optional Limits for output voltage, defaults to output limits """ + if hasattr(self,"pid_no"): + raise NotImplementedError("Only one set of PID parameters per channel is implemented.") if limits is None: # Use the Output limits if there are none specified here. limits = self.limits + if limits[0]<self.limits[0] or limits[1]>self.limits[1]: + raise LabscriptError(f"Limits of {self.name} with PID must not be larger than channel limits {self.limits}!") - if PID_AnalogIn is None: + if isinstance(pid_no,AnalogIn) and isinstance(pid_no.parent_device,_ADwinCard): + pid_no = int(pid_no.connection) + pid_no.parent_device.start_index + self.pid_no = pid_no + self.P = P + self.I = I + self.D = D + self.PID_min = limits[0] + self.PID_max = limits[1] + + + def set_PID(self,t,pid_no,set_output=0): + """(De-)activate PID for analog output, or change settings + + Parameters + ---------- + t : float + Time when to apply the PID settings + pid_no : int or `AnalogIn` or None + Channel of analog input for error siganl of PID feedback. + If `None` PID is deactivated. + set_value : float or "last" + When the PID is turned on, 'set_value' is the initially chosen output value, + 'last' means that the I value from the previous PID is taken. + When the PID is turned off, 'set_value' is programmed as the new output/target value, + 'last' means that the output from the PID loop is kept as 'set_target'. + """ + # Error check of bounds for set_output + if set_output!="last": + if set_output<self.PID_min or set_output>self.PID_max: + raise LabscriptError( + f"{self.name}: PID 'set_output={set_output}' must be within ({self.PID_min},{self.PID_max})" + ) + # TURN OFF PID + if pid_no is None: self.PID[t] = { "PID_channel": 0, - "P": P, - "I": I, - "D": D, - "limits": limits, + "start": set_output, } - elif (isinstance(PID_AnalogIn,AnalogIn) and isinstance(PID_AnalogIn.parent_device,_ADwinCard)) \ - or isinstance(PID_AnalogIn,int): + # If we don't keep the PID output, set the output to the set_value + if set_output!="last": + self.constant(t,set_output) + # TURN ON PID + elif (isinstance(pid_no,AnalogIn) and isinstance(pid_no.parent_device,_ADwinCard)) \ + or isinstance(pid_no,int): self.PID[t] = { - "PID_channel": PID_AnalogIn, - "P": P, - "I": I, - "D": D, - "limits": limits, + "PID_channel": pid_no, + "start": set_output, } # TODO: Do we need scale factors for setting a PID with integer? else: @@ -180,19 +211,17 @@ class ADwinAnalogOut(AnalogOut): def add_instruction(self,time,instruction,units=None): # Overwrite Output.add_instruction without limit check, becasue the value can be off-limits when this is the target value of the PID - # TODO / WARNING: THIS IS QUITE HACKY AND COULD LEAD TO OFF-LIMIT VALUES NOT NOTICED - # (the actual limits are also checked in the ADwin, so the actual output should be always within limits!) limits_temp = self.limits - self.limits = (-10,10) + if hasattr(self,"pid_no"): + self.limits = (-10,10) super().add_instruction(time,instruction,units) self.limits = limits_temp def expand_timeseries(self,all_times,flat_all_times_len): # Overwrite Output.add_instruction without limit check, becasue the value can be off-limits when this is the target value of the PID - # TODO / WARNING: THIS IS QUITE HACKY AND COULD LEAD TO OFF-LIMIT VALUES NOT NOTICED - # (the actual limits are also checked in the ADwin, so the actual output should be always within limits!) limits_temp = self.limits - self.limits = (-10,10) + if hasattr(self,"pid_no"): + self.limits = (-10,10) super().expand_timeseries(all_times,flat_all_times_len) self.limits = limits_temp @@ -310,6 +339,7 @@ class ADwinAO8(_ADwinCard): def __init__(self, name, parent_device, module_address, num_AO=8, **kwargs): self.num_AO = num_AO self.start_index = module_start_index[int(module_address)] + super().__init__(name, parent_device, module_address, **kwargs) @@ -329,11 +359,43 @@ class ADwinAO8(_ADwinCard): raise LabscriptError( f"The ramp sample rate ({rate_kHz:.0f}kHz) of {output.name} must not be faster than ADwin ({ADwin_rate_kHz:.0f}kHz)." ) + + # Check limits of output, but only when PID is NOT enabled (becasue then the target can be out of limits) + # Get all times when PID is not enabled + PID = output.PID.copy() + if 0 not in PID: + # Because 'np.digitize' determines the bins, we also have to make sure t=0 is included. + # If output.PID does not have the key t=0, then the PID is disabled in the beginning. + PID[0] = {"PID_channel":0,"start":0} + PID_times = np.array(list(PID.keys())) + PID_times.sort() + PID_off_times = [] + # For each output value, digitize gets the next highest time in PID_times. + # Using '-1' to get next lowest time. + for i_out,i_PID in enumerate(np.digitize(np.round(output.all_times,6), np.round(PID_times,6))-1): + t = PID_times[i_PID] + if PID[t]["PID_channel"]==0: + # When we turn the PID off but keep the last output, we make sure that + # - at least once in the end the output is (re)set, otherwise the get_final_values() in the Worker is wrong, + # - don't try to also set the target value at the same time, as this would overwrite the target in the ADwin with the PID output. + if PID[t]["start"]=="last": + if i_out+1==len(output.all_times): + raise LabscriptError(f"{output.name}: You must set the output at the end after turning off PID with 'last'.") + elif output.all_times[i_out] == t: + raise LabscriptError(f"{output.name}: Don't turn off PID with persitent value ('last') and also set new value.") + PID_off_times.append(i_out) + PID_off_outputs = output.raw_output[PID_off_times] + if np.any(PID_off_outputs < output.limits[0]) or np.any(PID_off_outputs > output.limits[1]): + error_times = output.all_times[PID_off_times][(PID_off_outputs < output.limits[0]) < (PID_off_outputs > output.limits[1])] + raise LabscriptError( + f"Limits of {output.name} (when PID is off) must be in {output.limits}, " + + f"you try to set ({PID_off_outputs.min()},{PID_off_outputs.max()}) " + + f"or turning off the PID with target value beyond limits at times {error_times}.") + # Check if the PID channel is allowed - if np.any(self.PID_channels["PID_channel"] > PIDNO): - max_PID_channel = self.PID_channels["PID_channel"].max() + if np.any(self.PID_table["PID_channel"] > PIDNO): raise LabscriptError(f"ADwin: Setting the PID channel to more than {PIDNO} is not possible!") - if np.any(self.PID_channels["PID_channel"] < 0): + if np.any(self.PID_table["PID_channel"] < 0): raise LabscriptError("ADwin: Setting the PID channel to less than 0 is not possible!") def generate_code(self,hdf5_file): @@ -342,25 +404,36 @@ class ADwinAO8(_ADwinCard): pseudoclock = clockline.parent_device output_dtypes = [("n_cycles",np.int32),("channel",np.int32),("value",np.int32)] - PID_dtypes = [ - ("n_cycles",np.int32),("AOUT_channel",np.int32),("PID_channel",np.int32), - ("PID_P",np.float64),("PID_I",np.float64),("PID_D",np.float64),("PID_min",np.int32),("PID_max",np.int32) + PID_config_dtypes = [ + ("PID_channel",np.int32),("PID_P",np.float64),("PID_I",np.float64),("PID_D",np.float64),("PID_min",np.int32),("PID_max",np.int32) + ] + PID_table_dtypes = [ + ("n_cycles",np.int32),("AOUT_channel",np.int32),("PID_channel",np.int32),("PID_start",np.int32) ] outputs = [] - PID_channels = [] + PID_table = [] + PID_config = [] for output in sorted(self.child_devices, key=lambda dev:int(dev.connection)): output.expand_output() # Get input channels for PID, collect changed for time table and store bare channels as dataset if output.PID: - PID_array = np.zeros(len(output.PID),dtype=PID_dtypes) + # Get PID parameters + if not hasattr(output,"pid_no"): + raise LabscriptError(f"For {self.name} you try to use a PID, but never set the parameters via {self.name}.init_PID().") + PID_config. append( + np.array([ + (output.pid_no,output.P,output.I,output.D,ADC(output.PID_min,self.resolution_bits,self.min_V,self.max_V),ADC(output.PID_max,self.resolution_bits,self.min_V,self.max_V)) + ], dtype=PID_config_dtypes) + ) + PID_array = np.zeros(len(output.PID),dtype=PID_table_dtypes) PID_times = np.array(list(output.PID.keys())) PID_times.sort() PID_array["n_cycles"] = np.round(PID_times * pseudoclock.clock_limit) # PID_array["PID_channel"] = list(output.PID_channel.values()) PID_array["AOUT_channel"] = int(output.connection) + self.start_index - PID_channels.append(PID_array) + PID_table.append(PID_array) # If a PID is enabled, the set values are not the actual voltage values of the # Output, but those measured at the input. If the input has a gain enabled, the # set values have the be scaled too, to have the PID stabilized to the right values. @@ -372,14 +445,12 @@ class ADwinAO8(_ADwinCard): output.raw_output[indices==i+1] *= output.PID[t]['PID_channel'].scale_factor elif isinstance(output.PID[t]['PID_channel'],int): PID_array["PID_channel"][i] = output.PID[t]['PID_channel'] - # If PID_channel[t]=None, there are zeros in the PID_array - - PID_array["PID_P"][i] = output.PID[t]['P'] - PID_array["PID_I"][i] = output.PID[t]['I'] - PID_array["PID_D"][i] = output.PID[t]['D'] - limits = output.PID[t]['limits'] - PID_array["PID_min"][i] = ADC(limits[0],self.resolution_bits,self.min_V,self.max_V) - PID_array["PID_max"][i] = ADC(limits[1],self.resolution_bits,self.min_V,self.max_V) + if output.PID[t]["start"]=="last": + # When we want to use the previous value during the shot, + # we use a value that's out of the 16 bit ADC range to identify. + PID_array["PID_start"][i] = 100_000 + else: + PID_array["PID_start"][i] = ADC(output.PID[t]['start'],self.resolution_bits,self.min_V,self.max_V) # The ADwin has 16 bit output resolution, so we quantize the Voltage to the right values quantized_output = ADC(output.raw_output,self.resolution_bits,self.min_V,self.max_V) out = np.empty(quantized_output.size,dtype=output_dtypes) @@ -389,7 +460,8 @@ class ADwinAO8(_ADwinCard): outputs.append(out) self.outputs = np.concatenate(outputs) if outputs else np.array([],dtype=output_dtypes) - self.PID_channels = np.concatenate(PID_channels) if PID_channels else np.array([],dtype=PID_dtypes) + self.PID_table = np.concatenate(PID_table) if PID_table else np.array([],dtype=PID_table_dtypes) + self.PID_config = np.concatenate(PID_config) if PID_config else np.array([],dtype=PID_config_dtypes)