diff --git a/MATLAB/+qc/awg_program.m b/MATLAB/+qc/awg_program.m index 91b06086c..62be0cf55 100644 --- a/MATLAB/+qc/awg_program.m +++ b/MATLAB/+qc/awg_program.m @@ -1,320 +1,392 @@ function [program, bool, msg] = awg_program(ctrl, varargin) - % pulse_template can also be a pulse name. In that case the pulse is - % automatically loaded. - - global plsdata - hws = plsdata.awg.hardwareSetup; - daq = plsdata.daq.inst; - - program = struct(); - msg = ''; - bool = false; - - default_args = struct(... - 'program_name', 'default_program', ... - 'pulse_template', 'default_pulse', ... - 'parameters_and_dicts', {plsdata.awg.defaultParametersAndDicts}, ... - 'channel_mapping', plsdata.awg.defaultChannelMapping, ... - 'window_mapping', plsdata.awg.defaultWindowMapping, ... - 'global_transformation', plsdata.awg.globalTransformation, ... - 'add_marker', {plsdata.awg.defaultAddMarker}, ... - 'force_update', false, ... - 'verbosity', 10 ... - ); - a = util.parse_varargin(varargin, default_args); - - % --- add --------------------------------------------------------------- - if strcmp(ctrl, 'add') - [~, bool, msg] = qc.awg_program('fresh', qc.change_field(a, 'verbosity', 0)); - if ~bool || a.force_update - plsdata.awg.currentProgam = ''; - - % Deleting old program should not be necessary. In practice however, - % updating an existing program seemed to crash Matlab sometimes. - % qc.awg_program('remove', qc.change_field(a, 'verbosity', 10)); - - a.pulse_template = pulse_to_python(a.pulse_template); - [a.pulse_template, a.channel_mapping] = add_marker_if_not_empty(a.pulse_template, a.add_marker, a.channel_mapping); - - program = qc.program_to_struct(a.program_name, a.pulse_template, a.parameters_and_dicts, a.channel_mapping, a.window_mapping, a.global_transformation); - plsdata.awg.registeredPrograms.(a.program_name) = program; - - % Save AWG amplitude at instantiation and upload time so that the - % amplitude at the sample can be reconstructed at a later time - if ~isfield(plsdata.awg.registeredPrograms.(a.program_name), 'amplitudes_at_upload') - % program not online yet - plsdata.awg.registeredPrograms.(a.program_name).amplitudes_at_upload = zeros(1, 4); - end - - for ii = int64(1:4) - % query actual amplitude from qupulse - plsdata.awg.registeredPrograms.(a.program_name).amplitudes_at_upload(ii) = plsdata.awg.inst.amplitude(ii); - end - - if a.verbosity > 9 - fprintf('Program ''%s'' is now being instantiated...', a.program_name); - tic; - end - instantiated_pulse = qc.instantiate_pulse(a.pulse_template, 'parameters', qc.join_params_and_dicts(program.parameters_and_dicts), 'channel_mapping', program.channel_mapping, 'window_mapping', program.window_mapping, 'global_transformation', program.global_transformation); - - if a.verbosity > 9 - fprintf('took %.0fs\n', toc); - fprintf('Program ''%s'' is now being uploaded...', a.program_name); - tic - end - util.py.call_with_interrupt_check(py.getattr(hws, 'register_program'), program.program_name, instantiated_pulse, pyargs('update', py.True)); - - if a.verbosity > 9 - fprintf('took %.0fs\n', toc); - end - - if bool && a.force_update - msg = ' since update forced'; - else - msg = ''; - end - msg = sprintf('Program ''%s'' added%s', a.program_name, msg); - - bool = true; - else - program = plsdata.awg.registeredPrograms.(a.program_name); - end - - % --- arm --------------------------------------------------------------- - elseif strcmp(ctrl, 'arm') - % Call directly before trigger comes, otherwise you might encounter a - % trigger timeout. Also, call after daq_operations('add')! - [~, bool, msg] = qc.awg_program('present', qc.change_field(a, 'verbosity', 0)); - if bool - % Wait for AWG to stop playing pulse, otherwise this might lead to a - % trigger timeout since the DAQ is not necessarily configured for the - % whole pulse time and can return data before the AWG stops playing - % the pulse. - if ~isempty(plsdata.awg.currentProgam) - waitingTime = min(max(plsdata.awg.registeredPrograms.(plsdata.awg.currentProgam).pulse_duration + plsdata.awg.registeredPrograms.(plsdata.awg.currentProgam).added_to_pulse_duration - (now() - plsdata.awg.triggerStartTime)*24*60*60, 0), plsdata.awg.maxPulseWait); - if waitingTime == plsdata.awg.maxPulseWait - warning('Maximum waiting time ''plsdata.awg.maxPulseWait'' = %g s reached.\nIncrease if you experience problems with the data acquistion.', plsdata.awg.maxPulseWait); - end - pause(waitingTime); - % fprintf('Waited for %.3fs for pulse to complete\n', waitingTime); - end - - % No longer needed since bug has been fixed - % qc.workaround_4chan_program_errors(a); - - hws.arm_program(a.program_name); - - plsdata.awg.currentProgam = a.program_name; - bool = true; - msg = sprintf('Program ''%s'' armed', a.program_name); - end - - % --- arm --------------------------------------------------------------- - elseif strcmp(ctrl, 'arm global') - if ischar(plsdata.awg.armGlobalProgram) - globalProgram = plsdata.awg.armGlobalProgram; - elseif iscell(plsdata.awg.armGlobalProgram) - globalProgram = plsdata.awg.armGlobalProgram{1}; - plsdata.awg.armGlobalProgram = circshift(plsdata.awg.armGlobalProgram, -1); - else - globalProgram = a.program_name; - warning('Not using global program since plsdata.awg.armGlobalProgram must contain a char or a cell.'); - end - - % Set scan axis labels here if the global program armament is called by - % a prefn in smrun. Only for charge scans - if startsWith(globalProgram, 'charge_4chan') - f = figure(a.fig_id); +% pulse_template can also be a pulse name. In that case the pulse is +% automatically loaded. + +global plsdata +hws = plsdata.awg.hardwareSetup; +daq = plsdata.daq.inst; + +%TODO: de-hardcode: handle different awgs +awg = 'hdawg'; +nAwg = numel(plsdata.awg.inst); +nAwgChan = 8 * nAwg; + +program = struct(); +msg = ''; +bool = false; + +default_args = struct(... + 'program_name', 'default_program', ... + 'pulse_template', 'default_pulse', ... + 'parameters_and_dicts', {plsdata.awg.defaultParametersAndDicts}, ... + 'channel_mapping', plsdata.awg.defaultChannelMapping, ... + 'window_mapping', plsdata.awg.defaultWindowMapping, ... + 'global_transformation', plsdata.awg.globalTransformation, ... + 'add_marker', {plsdata.awg.defaultAddMarker}, ... + 'force_update', false, ... + 'verbosity', 10 ... + ); +a = util.parse_varargin(varargin, default_args); + +% --- add --------------------------------------------------------------- +if strcmp(ctrl, 'add') + [~, bool, msg] = qc.awg_program('fresh', qc.change_field(a, 'verbosity', 0)); + if ~bool || a.force_update + plsdata.awg.currentProgam = ''; + + % Deleting old program should not be necessary. In practice however, + % updating an existing program seemed to crash Matlab sometimes. + % qc.awg_program('remove', qc.change_field(a, 'verbosity', 10)); + + a.pulse_template = pulse_to_python(a.pulse_template); + [a.pulse_template, a.channel_mapping] = add_marker_if_not_empty(a.pulse_template, a.add_marker, a.channel_mapping); + + program = qc.program_to_struct(... + a.program_name, ... + a.pulse_template, ... + a.parameters_and_dicts, ... + ... Resolve mappings from abstracted to hardware-known + qc.resolve_mappings(a.channel_mapping, plsdata.awg.defaultChannelMapping), ... + qc.resolve_mappings(a.window_mapping, plsdata.awg.defaultWindowMapping), ... + map_transformation_channels(a.global_transformation, plsdata.awg.globalTransformation)); + + plsdata.awg.registeredPrograms.(a.program_name) = program; + + % Save AWG amplitude at instantiation and upload time so that the + % amplitude at the sample can be reconstructed at a later time + if ~isfield(plsdata.awg.registeredPrograms.(a.program_name), 'amplitudes_at_upload') + % program not online yet + plsdata.awg.registeredPrograms.(a.program_name).amplitudes_at_upload = zeros(1, nAwgChan); + end + + %TODO: de-hardcode + if strcmp(awg, 'hdawg') && nAwg == 1 + plsdata.awg.registeredPrograms.(a.program_name).amplitudes_at_upload = cell2mat(cell(plsdata.awg.inst.channel_tuples{1}.amplitudes)); + elseif iscell(plsdata.awg.inst) + amps = arrayfun(@(awg_ii) cell2mat(cell(plsdata.awg.inst{awg_ii}.channel_tuples{1}.amplitudes)), [1:nAwg], 'UniformOutput', false); + plsdata.awg.registeredPrograms.(a.program_name).amplitudes_at_upload = cell2mat(amps); + else + for ii = int64(1:nAwgChan) + % query actual amplitude from qupulse + plsdata.awg.registeredPrograms.(a.program_name).amplitudes_at_upload(ii) = plsdata.awg.inst.amplitude(ii); + end + end + + if a.verbosity > 9 + fprintf('Program ''%s'' is now being instantiated...', a.program_name); + tic; + end + instantiated_pulse = qc.instantiate_pulse(... + a.pulse_template, ... + 'parameters', qc.join_params_and_dicts(program.parameters_and_dicts), ... + 'channel_mapping', program.channel_mapping, ... + 'window_mapping', program.window_mapping, ... + 'global_transformation', program.global_transformation ... + ); + + if instantiated_pulse == py.None + error("The created pulse has zero duration."); + end + + if a.verbosity > 9 + fprintf('took %.0fs\n', toc); + fprintf('Program ''%s'' is now being uploaded...', a.program_name); + tic + end + % Matlab crashes with this right now (12/20) 2020a, py37 TH + % util.py.call_with_interrupt_check(py.getattr(hws, 'register_program'), program.program_name, instantiated_pulse, pyargs('update', py.True)); + hws.register_program(program.program_name, instantiated_pulse, pyargs('update', py.True)); + + if a.verbosity > 9 + fprintf('took %.0fs\n', toc); + end + + if bool && a.force_update + msg = ' since update forced'; + else + msg = ''; + end + msg = sprintf('Program ''%s'' added%s', a.program_name, msg); + + bool = true; + else + program = plsdata.awg.registeredPrograms.(a.program_name); + end + + % --- arm --------------------------------------------------------------- +elseif strcmp(ctrl, 'arm') + % Call directly before trigger comes, otherwise you might encounter a + % trigger timeout. Also, call after daq_operations('add')! + [~, bool, msg] = qc.awg_program('present', qc.change_field(a, 'verbosity', 0)); + if bool + % Wait for AWG to stop playing pulse, otherwise this might lead to a + % trigger timeout since the DAQ is not necessarily configured for the + % whole pulse time and can return data before the AWG stops playing + % the pulse. + if ~isempty(plsdata.awg.currentProgam) + waitingTime = min(max(plsdata.awg.registeredPrograms.(plsdata.awg.currentProgam).pulse_duration + plsdata.awg.registeredPrograms.(plsdata.awg.currentProgam).added_to_pulse_duration - (now() - plsdata.awg.triggerStartTime)*24*60*60, 0), plsdata.awg.maxPulseWait); + if waitingTime == plsdata.awg.maxPulseWait + warning('Maximum waiting time ''plsdata.awg.maxPulseWait'' = %g s reached.\nIncrease if you experience problems with the data acquistion.', plsdata.awg.maxPulseWait); + end + pause(waitingTime); + % fprintf('Waited for %.3fs for pulse to complete\n', waitingTime); + end + + % No longer needed since bug has been fixed + % qc.workaround_4chan_program_errors(a); + + hws.arm_program(a.program_name); + + plsdata.awg.currentProgam = a.program_name; + bool = true; + msg = sprintf('Program ''%s'' armed', a.program_name); + end + + % --- arm --------------------------------------------------------------- +elseif strcmp(ctrl, 'arm global') + if ischar(plsdata.awg.armGlobalProgram) + globalProgram = plsdata.awg.armGlobalProgram; + elseif iscell(plsdata.awg.armGlobalProgram) + globalProgram = plsdata.awg.armGlobalProgram{1}; + plsdata.awg.armGlobalProgram = circshift(plsdata.awg.armGlobalProgram, -1); + else + globalProgram = a.program_name; + warning('Not using global program since plsdata.awg.armGlobalProgram must contain a char or a cell.'); + end + + % Set scan axis labels here if the global program armament is called by + % a prefn in smrun. Only for charge scans + if startsWith(globalProgram, 'charge_4chan') + f = figure(a.fig_id); + prog = globalProgram(1:strfind(globalProgram, '_d')-1); + + % always query the rf channels being swept so that they can be logged + % by a metafn. Doesn't allow for more than two gates + if contains(globalProgram, 'd12') + idx = [1 2]; + chans = {'RFQ1' 'RFQ2'}; + elseif contains(globalProgram, 'd23') + idx = [2 3]; + chans = {'RFQ2' 'RFQ3'}; + elseif contains(globalProgram, 'd34') + idx = [4 3]; + chans = {'RFQ4' 'RFQ3'}; + elseif contains(globalProgram, 'd14') + idx = [1 4]; + chans = {'RFQ1' 'RFQ4'}; + end + plsdata.awg.currentChannels = chans; + + % compare current AWG channel amplitudes to those at instantiation + % time + currentAmplitudes = plsdata.awg.currentAmplitudes; + uploadAmplitudes = plsdata.awg.registeredPrograms.(globalProgram).amplitudes_at_upload; + + updateRFchans = ~strcmp(globalProgram, a.program_name); + updateRFamps = ~all(currentAmplitudes == uploadAmplitudes); + + if updateRFchans || updateRFamps + + if updateRFamps + % Calculate amplitude at sample from the current amplitude, the + % amplitude at pulse instantiation time, and the pulse parameters + rng = [(plsdata.awg.registeredPrograms.(globalProgram).parameters_and_dicts{2}.([prog '___stop_x']) - ... + plsdata.awg.registeredPrograms.(globalProgram).parameters_and_dicts{2}.([prog '___start_x'])) ... + (plsdata.awg.registeredPrograms.(globalProgram).parameters_and_dicts{2}.([prog '___stop_y']) - ... + plsdata.awg.registeredPrograms.(globalProgram).parameters_and_dicts{2}.([prog '___start_y']))]; + amps = currentAmplitudes(idx) ./ uploadAmplitudes(idx) .* rng * 1e3 ./ 2; + end + for ax = f.Children(2:2:end)' + % Don't use xlabel(), ylabel() to stop matlab from updating the rest of the figure + if updateRFchans + ax.XLabel.String = replace_channel_in_string(ax.XLabel.String, chans{1}); + ax.YLabel.String = replace_channel_in_string(ax.YLabel.String, chans{2}); + end + if updateRFamps + ax.XLabel.String = replace_amplitude_in_string(ax.XLabel.String, amps(1)); + ax.YLabel.String = replace_amplitude_in_string(ax.YLabel.String, amps(2)); + ax.XTick = linspace(ax.XLim(1), ax.XLim(2), 3); + ax.YTick = linspace(ax.YLim(1), ax.YLim(2), 3); + ax.XTickLabel = sprintfc('%d', -1:1); + ax.YTickLabel = sprintfc('%d', -1:1); + end + end + end + end + % This code outputs the wrong pulses and isn't even faster + % - Then why is it still here? - TH + % registered_programs = util.py.py2mat(py.getattr(hws,'_registered_programs')); + % program = registered_programs.(globalProgram); + % awgs_to_upload_to = program{4}; + % dacs_to_arm = program{5}; + % for awgToUploadTo = awgs_to_upload_to + % awgToUploadTo{1}.arm(globalProgram); + % end + % for dacToArm = dacs_to_arm + % dacToArm{1}.arm_program(plsdata.awg.currentProgam); + % end + + qc.awg_program('arm', 'program_name', globalProgram, 'verbosity', a.verbosity, 'arm_global_for_workaround_4chan_program_errors', []); + + % --- remove ------------------------------------------------------------ +elseif strcmp(ctrl, 'remove') + % Arm the idle program so the program to be remove is not active by + % any chance (should not be needed - please test more thorougly whether it is needed) + plsdata.awg.inst.channel_tuples{1}.arm(py.None) + + [~, bool, msg] = qc.awg_program('present', qc.change_field(a, 'verbosity', 0)); + + if bool + bool = false; + + if isfield(plsdata.awg.registeredPrograms, a.program_name) + plsdata.awg.registeredPrograms = rmfield(plsdata.awg.registeredPrograms, a.program_name); + end + + try + hws.remove_program(a.program_name); + bool = true; + catch err + warning('The following error was encountered when running hardware_setup.remove_program.\nPlease debug AWG commands.\nThis might have to do with removing the current program.\n.Trying to recover by deleting operations.\n%s', err.getReport()); + qc.daq_operations('remove', 'program_name', a.program_name, 'verbosity', 10); + end + + msg = sprintf('Program ''%s'' removed', a.program_name); + end + + % --- clear all --------------------------------------------------------- +elseif strcmp(ctrl, 'clear all') % might take a long time + plsdata.awg.registeredPrograms = struct(); + program_names = fieldnames(util.py.py2mat(py.getattr(hws, '_registered_programs'))); + + bool = true; + for program_name = program_names.' + [~, boolNew] = qc.awg_program('remove', 'program_name', program_name{1}, 'verbosity', 10); + bool = bool & boolNew; + end + + if bool + msg = 'All programs cleared'; + else + msg = 'Error when trying to clear all progams'; + end + + % --- clear all fast ---------------------------------------------------- +elseif strcmp(ctrl, 'clear all fast') % fast but need to clear awg manually + plsdata.awg.hardwareSetup.clear_programs(); + py.getattr(daq, '_registered_programs').clear(); + plsdata.awg.registeredPrograms = struct(); + plsdata.awg.registeredPrograms.currentProgam = ''; + + % --- present ----------------------------------------------------------- +elseif strcmp(ctrl, 'present') % returns true if program is present + bool = py.list(hws.registered_programs.keys()).count(a.program_name) ~= 0; + if bool + msg = ''; + else + msg = 'not '; + end + msg = sprintf('Program ''%s'' %spresent', a.program_name, msg); + + % --- fresh ------------------------------------------------------------- +elseif strcmp(ctrl, 'fresh') % returns true if program is present and has not changed + [~, bool, msg] = qc.awg_program('present', qc.change_field(a, 'verbosity', 0)); + + if isfield(plsdata.awg.registeredPrograms, a.program_name) && bool + a.pulse_template = pulse_to_python(a.pulse_template); + [a.pulse_template, a.channel_mapping] = add_marker_if_not_empty(a.pulse_template, a.add_marker, a.channel_mapping); + + newProgram = qc.program_to_struct(... + a.program_name, ... + a.pulse_template, ... + a.parameters_and_dicts, ... + ... Resolve mappings from abstracted to hardware-known + qc.resolve_mappings(a.channel_mapping, plsdata.awg.defaultChannelMapping), ... + qc.resolve_mappings(a.window_mapping, plsdata.awg.defaultWindowMapping), ... + map_transformation_channels(a.global_transformation, plsdata.awg.defaultChannelMapping)); + + newProgram = qc.get_minimal_program(newProgram); + + awgProgram = plsdata.awg.registeredPrograms.(a.program_name); + awgProgram = qc.get_minimal_program(awgProgram); + + bool = isequal(newProgram, awgProgram); + + if bool + msg = ''; + else + msg = 'not '; + end + msg = sprintf('Program ''%s'' is %sup to date (fresh)', a.program_name, msg); + end + % if ~bool + % util.comparedata(newProgram, awgProgram); + % end + +end + +if a.verbosity > 9 + fprintf([msg '\n']); +end +end - % always query the rf channels being swept so that they can be logged - % by a metafn. - if startsWith(globalProgram, 'charge_4chan_d12') - idx = [1 2]; - chans = {'A' 'B'}; - elseif startsWith(globalProgram, 'charge_4chan_d23') - idx = [2 3]; - chans = {'B' 'C'}; - elseif startsWith(globalProgram, 'charge_4chan_d34') - idx = [4 3]; - chans = {'D' 'C'}; - elseif startsWith(globalProgram, 'charge_4chan_d14') - idx = [1 4]; - chans = {'A' 'D'}; - end - plsdata.awg.currentChannels = chans; - - % compare current AWG channel amplitudes to those at instantiation - % time - currentAmplitudes = plsdata.awg.currentAmplitudesHV; - uploadAmplitudes = plsdata.awg.registeredPrograms.(globalProgram).amplitudes_at_upload; - updateRFchans = ~strcmp(globalProgram, a.program_name); - updateRFamps = ~all(currentAmplitudes == uploadAmplitudes); - - if updateRFchans || updateRFamps - - if updateRFamps - % Calculate amplitude at sample from the current amplitude, the - % amplitude at pulse instantiation time, and the pulse parameters - rng = [(plsdata.awg.registeredPrograms.(globalProgram).parameters_and_dicts{2}.charge_4chan___stop_x - ... - plsdata.awg.registeredPrograms.(globalProgram).parameters_and_dicts{2}.charge_4chan___start_x) ... - (plsdata.awg.registeredPrograms.(globalProgram).parameters_and_dicts{2}.charge_4chan___stop_y - ... - plsdata.awg.registeredPrograms.(globalProgram).parameters_and_dicts{2}.charge_4chan___start_y)]; - amps = currentAmplitudes(idx)./uploadAmplitudes(idx).*rng*1e3; - end - for ax = f.Children(2:2:end)' - % Don't use xlabel(), ylabel() to stop matlab from updating the rest of the figure - if updateRFchans - ax.XLabel.String(3) = chans{1}; - ax.YLabel.String(3) = chans{2}; - end - if updateRFamps - ax.XLabel.String(6:9) = sprintf('%.1f', amps(1)); - ax.YLabel.String(6:9) = sprintf('%.1f', amps(2)); - ax.XTick = 0:10:100; - ax.YTick = 0:10:100; - ax.XTickLabel = sprintfc('%.1f', linspace(-amps(1)/2, amps(1)/2, 11)); - ax.YTickLabel = sprintfc('%.1f', linspace(-amps(2)/2, amps(2)/2, 11)); - end - end - end - end -% This code outputs the wrong pulses and isn't even faster -% - Then why is it still here? - TH -% registered_programs = util.py.py2mat(py.getattr(hws,'_registered_programs')); -% program = registered_programs.(globalProgram); -% awgs_to_upload_to = program{4}; -% dacs_to_arm = program{5}; -% for awgToUploadTo = awgs_to_upload_to -% awgToUploadTo{1}.arm(globalProgram); -% end -% for dacToArm = dacs_to_arm -% dacToArm{1}.arm_program(plsdata.awg.currentProgam); -% end - - qc.awg_program('arm', 'program_name', globalProgram, 'verbosity', a.verbosity, 'arm_global_for_workaround_4chan_program_errors', []); - - % --- remove ------------------------------------------------------------ - elseif strcmp(ctrl, 'remove') - % Arm the idle program so the program to be remove is not active by - % any chance (should not be needed - please test more thorougly whether it is needed) - plsdata.awg.inst.channel_pair_AB.arm(py.None); - plsdata.awg.inst.channel_pair_CD.arm(py.None); - - [~, bool, msg] = qc.awg_program('present', qc.change_field(a, 'verbosity', 0)); - - if bool - bool = false; - - if isfield(plsdata.awg.registeredPrograms, a.program_name) - plsdata.awg.registeredPrograms = rmfield(plsdata.awg.registeredPrograms, a.program_name); - end - - try - hws.remove_program(a.program_name); - bool = true; - catch err - warning('The following error was encountered when running hardware_setup.remove_program.\nPlease debug AWG commands.\nThis might have to do with removing the current program.\n.Trying to recover by deleting operations.\n%s', err.getReport()); - qc.daq_operations('remove', 'program_name', a.program_name, 'verbosity', 10); - end - - msg = sprintf('Program ''%s'' removed', a.program_name); - end - - % --- clear all --------------------------------------------------------- - elseif strcmp(ctrl, 'clear all') % might take a long time - plsdata.awg.registeredPrograms = struct(); - program_names = fieldnames(util.py.py2mat(py.getattr(hws, '_registered_programs'))); - - bool = true; - for program_name = program_names.' - [~, boolNew] = qc.awg_program('remove', 'program_name', program_name{1}, 'verbosity', 10); - bool = bool & boolNew; - end - - if bool - msg = 'All programs cleared'; - else - msg = 'Error when trying to clear all progams'; - end - - % --- clear all fast ---------------------------------------------------- - elseif strcmp(ctrl, 'clear all fast') % fast but need to clear awg manually - hws.registered_programs.clear(); - py.getattr(daq, '_registered_programs').clear(); - % --- present ----------------------------------------------------------- - elseif strcmp(ctrl, 'present') % returns true if program is present - bool = py.list(hws.registered_programs.keys()).count(a.program_name) ~= 0; - if bool - msg = ''; - else - msg = 'not '; - end - msg = sprintf('Program ''%s'' %spresent', a.program_name, msg); - - % --- fresh ------------------------------------------------------------- - elseif strcmp(ctrl, 'fresh') % returns true if program is present and has not changed - [~, bool, msg] = qc.awg_program('present', qc.change_field(a, 'verbosity', 0)); - - if isfield(plsdata.awg.registeredPrograms, a.program_name) && bool - a.pulse_template = pulse_to_python(a.pulse_template); - [a.pulse_template, a.channel_mapping] = add_marker_if_not_empty(a.pulse_template, a.add_marker, a.channel_mapping); - - newProgram = qc.program_to_struct(a.program_name, a.pulse_template, a.parameters_and_dicts, a.channel_mapping, a.window_mapping, a.global_transformation); - newProgram = qc.get_minimal_program(newProgram); - - awgProgram = plsdata.awg.registeredPrograms.(a.program_name); - awgProgram = qc.get_minimal_program(awgProgram); - - bool = isequal(newProgram, awgProgram); - - if bool - msg = ''; - else - msg = 'not '; - end - msg = sprintf('Program ''%s'' is %sup to date (fresh)', a.program_name, msg); - end - % if ~bool - % util.comparedata(newProgram, awgProgram); - % end - - end - - if a.verbosity > 9 - fprintf([msg '\n']); - end - - - - function pulse_template = pulse_to_python(pulse_template) - - if ischar(pulse_template) - pulse_template = qc.load_pulse(pulse_template); - end - - if isstruct(pulse_template) - pulse_template = qc.struct_to_pulse(pulse_template); - end - - + +if ischar(pulse_template) + pulse_template = qc.load_pulse(pulse_template); +end + +if isstruct(pulse_template) + pulse_template = qc.struct_to_pulse(pulse_template); +end +end + + function [pulse_template, channel_mapping] = add_marker_if_not_empty(pulse_template, add_marker, channel_mapping) - - if ~iscell(add_marker) - add_marker = {add_marker}; - end - - if ~isempty(add_marker) - marker_pulse = py.qctoolkit.pulses.PointPT({{0, 1},... - {py.getattr(pulse_template, 'duration'), 1}}, add_marker); - pulse_template = py.qctoolkit.pulses.AtomicMultiChannelPT(pulse_template, marker_pulse); - - for ii = 1:numel(add_marker) - channel_mapping.(args.add_marker{ii}) = add_marker{ii}; - end - end - - - \ No newline at end of file + +if ~iscell(add_marker) + add_marker = {add_marker}; +end + +if ~isempty(add_marker) + marker_values = py.dict(py.zip(add_marker, py.itertools.repeat(1))); + + pulse_template = py.qupulse.pulses.ParallelConstantChannelPT(pulse_template, marker_values); + + % This is the old evil code + % remove at some point S.H. +% marker_pulse = py.qctoolkit.pulses.PointPT({{0, 1},... +% {py.getattr(pulse_template, 'duration'), 1}}, add_marker); +% pulse_template = py.qctoolkit.pulses.AtomicMultiChannelPT(pulse_template, marker_pulse); + + for ii = 1:numel(add_marker) + channel_mapping.(add_marker{ii}) = add_marker{ii}; + end +end +end + +function mat_trafo = map_transformation_channels(mat_trafo, mapping) +if istable(mat_trafo) + for row = mat_trafo.Properties.RowNames' + mat_trafo.Properties.RowNames{row{1}} = mapping.(row{1}); + end + for col = mat_trafo.Properties.VariableNames + mat_trafo.Properties.VariableNames{col{1}} = mapping.(col{1}); + end +end +end + +function s = replace_channel_in_string(s, newval) +s1 = split(s, ' '); +s1{1} = newval; +s = join(s1, ' '); +end + +function s = replace_amplitude_in_string(s, newval) +s1 = split(s, '('); +s2 = split(s1{2}, ' '); +s2{1} = sprintf('%.1f', newval); +s = join([s1(1) join(s2, ' ')], '('); +end diff --git a/MATLAB/+qc/awg_program_volatile_params.m b/MATLAB/+qc/awg_program_volatile_params.m new file mode 100644 index 000000000..62be0cf55 --- /dev/null +++ b/MATLAB/+qc/awg_program_volatile_params.m @@ -0,0 +1,392 @@ +function [program, bool, msg] = awg_program(ctrl, varargin) +% pulse_template can also be a pulse name. In that case the pulse is +% automatically loaded. + +global plsdata +hws = plsdata.awg.hardwareSetup; +daq = plsdata.daq.inst; + +%TODO: de-hardcode: handle different awgs +awg = 'hdawg'; +nAwg = numel(plsdata.awg.inst); +nAwgChan = 8 * nAwg; + +program = struct(); +msg = ''; +bool = false; + +default_args = struct(... + 'program_name', 'default_program', ... + 'pulse_template', 'default_pulse', ... + 'parameters_and_dicts', {plsdata.awg.defaultParametersAndDicts}, ... + 'channel_mapping', plsdata.awg.defaultChannelMapping, ... + 'window_mapping', plsdata.awg.defaultWindowMapping, ... + 'global_transformation', plsdata.awg.globalTransformation, ... + 'add_marker', {plsdata.awg.defaultAddMarker}, ... + 'force_update', false, ... + 'verbosity', 10 ... + ); +a = util.parse_varargin(varargin, default_args); + +% --- add --------------------------------------------------------------- +if strcmp(ctrl, 'add') + [~, bool, msg] = qc.awg_program('fresh', qc.change_field(a, 'verbosity', 0)); + if ~bool || a.force_update + plsdata.awg.currentProgam = ''; + + % Deleting old program should not be necessary. In practice however, + % updating an existing program seemed to crash Matlab sometimes. + % qc.awg_program('remove', qc.change_field(a, 'verbosity', 10)); + + a.pulse_template = pulse_to_python(a.pulse_template); + [a.pulse_template, a.channel_mapping] = add_marker_if_not_empty(a.pulse_template, a.add_marker, a.channel_mapping); + + program = qc.program_to_struct(... + a.program_name, ... + a.pulse_template, ... + a.parameters_and_dicts, ... + ... Resolve mappings from abstracted to hardware-known + qc.resolve_mappings(a.channel_mapping, plsdata.awg.defaultChannelMapping), ... + qc.resolve_mappings(a.window_mapping, plsdata.awg.defaultWindowMapping), ... + map_transformation_channels(a.global_transformation, plsdata.awg.globalTransformation)); + + plsdata.awg.registeredPrograms.(a.program_name) = program; + + % Save AWG amplitude at instantiation and upload time so that the + % amplitude at the sample can be reconstructed at a later time + if ~isfield(plsdata.awg.registeredPrograms.(a.program_name), 'amplitudes_at_upload') + % program not online yet + plsdata.awg.registeredPrograms.(a.program_name).amplitudes_at_upload = zeros(1, nAwgChan); + end + + %TODO: de-hardcode + if strcmp(awg, 'hdawg') && nAwg == 1 + plsdata.awg.registeredPrograms.(a.program_name).amplitudes_at_upload = cell2mat(cell(plsdata.awg.inst.channel_tuples{1}.amplitudes)); + elseif iscell(plsdata.awg.inst) + amps = arrayfun(@(awg_ii) cell2mat(cell(plsdata.awg.inst{awg_ii}.channel_tuples{1}.amplitudes)), [1:nAwg], 'UniformOutput', false); + plsdata.awg.registeredPrograms.(a.program_name).amplitudes_at_upload = cell2mat(amps); + else + for ii = int64(1:nAwgChan) + % query actual amplitude from qupulse + plsdata.awg.registeredPrograms.(a.program_name).amplitudes_at_upload(ii) = plsdata.awg.inst.amplitude(ii); + end + end + + if a.verbosity > 9 + fprintf('Program ''%s'' is now being instantiated...', a.program_name); + tic; + end + instantiated_pulse = qc.instantiate_pulse(... + a.pulse_template, ... + 'parameters', qc.join_params_and_dicts(program.parameters_and_dicts), ... + 'channel_mapping', program.channel_mapping, ... + 'window_mapping', program.window_mapping, ... + 'global_transformation', program.global_transformation ... + ); + + if instantiated_pulse == py.None + error("The created pulse has zero duration."); + end + + if a.verbosity > 9 + fprintf('took %.0fs\n', toc); + fprintf('Program ''%s'' is now being uploaded...', a.program_name); + tic + end + % Matlab crashes with this right now (12/20) 2020a, py37 TH + % util.py.call_with_interrupt_check(py.getattr(hws, 'register_program'), program.program_name, instantiated_pulse, pyargs('update', py.True)); + hws.register_program(program.program_name, instantiated_pulse, pyargs('update', py.True)); + + if a.verbosity > 9 + fprintf('took %.0fs\n', toc); + end + + if bool && a.force_update + msg = ' since update forced'; + else + msg = ''; + end + msg = sprintf('Program ''%s'' added%s', a.program_name, msg); + + bool = true; + else + program = plsdata.awg.registeredPrograms.(a.program_name); + end + + % --- arm --------------------------------------------------------------- +elseif strcmp(ctrl, 'arm') + % Call directly before trigger comes, otherwise you might encounter a + % trigger timeout. Also, call after daq_operations('add')! + [~, bool, msg] = qc.awg_program('present', qc.change_field(a, 'verbosity', 0)); + if bool + % Wait for AWG to stop playing pulse, otherwise this might lead to a + % trigger timeout since the DAQ is not necessarily configured for the + % whole pulse time and can return data before the AWG stops playing + % the pulse. + if ~isempty(plsdata.awg.currentProgam) + waitingTime = min(max(plsdata.awg.registeredPrograms.(plsdata.awg.currentProgam).pulse_duration + plsdata.awg.registeredPrograms.(plsdata.awg.currentProgam).added_to_pulse_duration - (now() - plsdata.awg.triggerStartTime)*24*60*60, 0), plsdata.awg.maxPulseWait); + if waitingTime == plsdata.awg.maxPulseWait + warning('Maximum waiting time ''plsdata.awg.maxPulseWait'' = %g s reached.\nIncrease if you experience problems with the data acquistion.', plsdata.awg.maxPulseWait); + end + pause(waitingTime); + % fprintf('Waited for %.3fs for pulse to complete\n', waitingTime); + end + + % No longer needed since bug has been fixed + % qc.workaround_4chan_program_errors(a); + + hws.arm_program(a.program_name); + + plsdata.awg.currentProgam = a.program_name; + bool = true; + msg = sprintf('Program ''%s'' armed', a.program_name); + end + + % --- arm --------------------------------------------------------------- +elseif strcmp(ctrl, 'arm global') + if ischar(plsdata.awg.armGlobalProgram) + globalProgram = plsdata.awg.armGlobalProgram; + elseif iscell(plsdata.awg.armGlobalProgram) + globalProgram = plsdata.awg.armGlobalProgram{1}; + plsdata.awg.armGlobalProgram = circshift(plsdata.awg.armGlobalProgram, -1); + else + globalProgram = a.program_name; + warning('Not using global program since plsdata.awg.armGlobalProgram must contain a char or a cell.'); + end + + % Set scan axis labels here if the global program armament is called by + % a prefn in smrun. Only for charge scans + if startsWith(globalProgram, 'charge_4chan') + f = figure(a.fig_id); + prog = globalProgram(1:strfind(globalProgram, '_d')-1); + + % always query the rf channels being swept so that they can be logged + % by a metafn. Doesn't allow for more than two gates + if contains(globalProgram, 'd12') + idx = [1 2]; + chans = {'RFQ1' 'RFQ2'}; + elseif contains(globalProgram, 'd23') + idx = [2 3]; + chans = {'RFQ2' 'RFQ3'}; + elseif contains(globalProgram, 'd34') + idx = [4 3]; + chans = {'RFQ4' 'RFQ3'}; + elseif contains(globalProgram, 'd14') + idx = [1 4]; + chans = {'RFQ1' 'RFQ4'}; + end + plsdata.awg.currentChannels = chans; + + % compare current AWG channel amplitudes to those at instantiation + % time + currentAmplitudes = plsdata.awg.currentAmplitudes; + uploadAmplitudes = plsdata.awg.registeredPrograms.(globalProgram).amplitudes_at_upload; + + updateRFchans = ~strcmp(globalProgram, a.program_name); + updateRFamps = ~all(currentAmplitudes == uploadAmplitudes); + + if updateRFchans || updateRFamps + + if updateRFamps + % Calculate amplitude at sample from the current amplitude, the + % amplitude at pulse instantiation time, and the pulse parameters + rng = [(plsdata.awg.registeredPrograms.(globalProgram).parameters_and_dicts{2}.([prog '___stop_x']) - ... + plsdata.awg.registeredPrograms.(globalProgram).parameters_and_dicts{2}.([prog '___start_x'])) ... + (plsdata.awg.registeredPrograms.(globalProgram).parameters_and_dicts{2}.([prog '___stop_y']) - ... + plsdata.awg.registeredPrograms.(globalProgram).parameters_and_dicts{2}.([prog '___start_y']))]; + amps = currentAmplitudes(idx) ./ uploadAmplitudes(idx) .* rng * 1e3 ./ 2; + end + for ax = f.Children(2:2:end)' + % Don't use xlabel(), ylabel() to stop matlab from updating the rest of the figure + if updateRFchans + ax.XLabel.String = replace_channel_in_string(ax.XLabel.String, chans{1}); + ax.YLabel.String = replace_channel_in_string(ax.YLabel.String, chans{2}); + end + if updateRFamps + ax.XLabel.String = replace_amplitude_in_string(ax.XLabel.String, amps(1)); + ax.YLabel.String = replace_amplitude_in_string(ax.YLabel.String, amps(2)); + ax.XTick = linspace(ax.XLim(1), ax.XLim(2), 3); + ax.YTick = linspace(ax.YLim(1), ax.YLim(2), 3); + ax.XTickLabel = sprintfc('%d', -1:1); + ax.YTickLabel = sprintfc('%d', -1:1); + end + end + end + end + % This code outputs the wrong pulses and isn't even faster + % - Then why is it still here? - TH + % registered_programs = util.py.py2mat(py.getattr(hws,'_registered_programs')); + % program = registered_programs.(globalProgram); + % awgs_to_upload_to = program{4}; + % dacs_to_arm = program{5}; + % for awgToUploadTo = awgs_to_upload_to + % awgToUploadTo{1}.arm(globalProgram); + % end + % for dacToArm = dacs_to_arm + % dacToArm{1}.arm_program(plsdata.awg.currentProgam); + % end + + qc.awg_program('arm', 'program_name', globalProgram, 'verbosity', a.verbosity, 'arm_global_for_workaround_4chan_program_errors', []); + + % --- remove ------------------------------------------------------------ +elseif strcmp(ctrl, 'remove') + % Arm the idle program so the program to be remove is not active by + % any chance (should not be needed - please test more thorougly whether it is needed) + plsdata.awg.inst.channel_tuples{1}.arm(py.None) + + [~, bool, msg] = qc.awg_program('present', qc.change_field(a, 'verbosity', 0)); + + if bool + bool = false; + + if isfield(plsdata.awg.registeredPrograms, a.program_name) + plsdata.awg.registeredPrograms = rmfield(plsdata.awg.registeredPrograms, a.program_name); + end + + try + hws.remove_program(a.program_name); + bool = true; + catch err + warning('The following error was encountered when running hardware_setup.remove_program.\nPlease debug AWG commands.\nThis might have to do with removing the current program.\n.Trying to recover by deleting operations.\n%s', err.getReport()); + qc.daq_operations('remove', 'program_name', a.program_name, 'verbosity', 10); + end + + msg = sprintf('Program ''%s'' removed', a.program_name); + end + + % --- clear all --------------------------------------------------------- +elseif strcmp(ctrl, 'clear all') % might take a long time + plsdata.awg.registeredPrograms = struct(); + program_names = fieldnames(util.py.py2mat(py.getattr(hws, '_registered_programs'))); + + bool = true; + for program_name = program_names.' + [~, boolNew] = qc.awg_program('remove', 'program_name', program_name{1}, 'verbosity', 10); + bool = bool & boolNew; + end + + if bool + msg = 'All programs cleared'; + else + msg = 'Error when trying to clear all progams'; + end + + % --- clear all fast ---------------------------------------------------- +elseif strcmp(ctrl, 'clear all fast') % fast but need to clear awg manually + plsdata.awg.hardwareSetup.clear_programs(); + py.getattr(daq, '_registered_programs').clear(); + plsdata.awg.registeredPrograms = struct(); + plsdata.awg.registeredPrograms.currentProgam = ''; + + % --- present ----------------------------------------------------------- +elseif strcmp(ctrl, 'present') % returns true if program is present + bool = py.list(hws.registered_programs.keys()).count(a.program_name) ~= 0; + if bool + msg = ''; + else + msg = 'not '; + end + msg = sprintf('Program ''%s'' %spresent', a.program_name, msg); + + % --- fresh ------------------------------------------------------------- +elseif strcmp(ctrl, 'fresh') % returns true if program is present and has not changed + [~, bool, msg] = qc.awg_program('present', qc.change_field(a, 'verbosity', 0)); + + if isfield(plsdata.awg.registeredPrograms, a.program_name) && bool + a.pulse_template = pulse_to_python(a.pulse_template); + [a.pulse_template, a.channel_mapping] = add_marker_if_not_empty(a.pulse_template, a.add_marker, a.channel_mapping); + + newProgram = qc.program_to_struct(... + a.program_name, ... + a.pulse_template, ... + a.parameters_and_dicts, ... + ... Resolve mappings from abstracted to hardware-known + qc.resolve_mappings(a.channel_mapping, plsdata.awg.defaultChannelMapping), ... + qc.resolve_mappings(a.window_mapping, plsdata.awg.defaultWindowMapping), ... + map_transformation_channels(a.global_transformation, plsdata.awg.defaultChannelMapping)); + + newProgram = qc.get_minimal_program(newProgram); + + awgProgram = plsdata.awg.registeredPrograms.(a.program_name); + awgProgram = qc.get_minimal_program(awgProgram); + + bool = isequal(newProgram, awgProgram); + + if bool + msg = ''; + else + msg = 'not '; + end + msg = sprintf('Program ''%s'' is %sup to date (fresh)', a.program_name, msg); + end + % if ~bool + % util.comparedata(newProgram, awgProgram); + % end + +end + +if a.verbosity > 9 + fprintf([msg '\n']); +end +end + + + +function pulse_template = pulse_to_python(pulse_template) + +if ischar(pulse_template) + pulse_template = qc.load_pulse(pulse_template); +end + +if isstruct(pulse_template) + pulse_template = qc.struct_to_pulse(pulse_template); +end +end + + +function [pulse_template, channel_mapping] = add_marker_if_not_empty(pulse_template, add_marker, channel_mapping) + +if ~iscell(add_marker) + add_marker = {add_marker}; +end + +if ~isempty(add_marker) + marker_values = py.dict(py.zip(add_marker, py.itertools.repeat(1))); + + pulse_template = py.qupulse.pulses.ParallelConstantChannelPT(pulse_template, marker_values); + + % This is the old evil code + % remove at some point S.H. +% marker_pulse = py.qctoolkit.pulses.PointPT({{0, 1},... +% {py.getattr(pulse_template, 'duration'), 1}}, add_marker); +% pulse_template = py.qctoolkit.pulses.AtomicMultiChannelPT(pulse_template, marker_pulse); + + for ii = 1:numel(add_marker) + channel_mapping.(add_marker{ii}) = add_marker{ii}; + end +end +end + +function mat_trafo = map_transformation_channels(mat_trafo, mapping) +if istable(mat_trafo) + for row = mat_trafo.Properties.RowNames' + mat_trafo.Properties.RowNames{row{1}} = mapping.(row{1}); + end + for col = mat_trafo.Properties.VariableNames + mat_trafo.Properties.VariableNames{col{1}} = mapping.(col{1}); + end +end +end + +function s = replace_channel_in_string(s, newval) +s1 = split(s, ' '); +s1{1} = newval; +s = join(s1, ' '); +end + +function s = replace_amplitude_in_string(s, newval) +s1 = split(s, '('); +s2 = split(s1{2}, ' '); +s2{1} = sprintf('%.1f', newval); +s = join([s1(1) join(s2, ' ')], '('); +end diff --git a/MATLAB/+qc/cleanupfn_rf_switches.m b/MATLAB/+qc/cleanupfn_rf_switches.m new file mode 100644 index 000000000..6f10b9cee --- /dev/null +++ b/MATLAB/+qc/cleanupfn_rf_switches.m @@ -0,0 +1,11 @@ +function scan = cleanupfn_rf_switches(scan) + + if nargin < 1 + scan = []; + end + + evalin('caller', 'cleanupFnRfMsg = onCleanup(@()(fprintf(''Executing cleanup function: Turned RF Switches off\n'')));'); + evalin('caller', 'cleanupFnRf1 = onCleanup(@()(smset(''AWGSwitch1'', Inf)));'); + evalin('caller', 'cleanupFnRf2 = onCleanup(@()(smset(''AWGSwitch2'', Inf)));'); + +end \ No newline at end of file diff --git a/MATLAB/+qc/clear_old_scanlines.m b/MATLAB/+qc/clear_old_scanlines.m new file mode 100644 index 000000000..0049cc9b5 --- /dev/null +++ b/MATLAB/+qc/clear_old_scanlines.m @@ -0,0 +1,19 @@ +function clear_old_scanlines() + +global plsdata + + + +while true + try + plsdata.daq.inst.card.dropNextScanline(); + catch ME + if ME.ExceptionObject{1} == py.type(py.RuntimeError) + % all scanlines blocked + break + else + % unkown exception + rethrow(ME); + end + end +end \ No newline at end of file diff --git a/MATLAB/+qc/conf_seq.m b/MATLAB/+qc/conf_seq.m index b58afab43..7265d9a84 100644 --- a/MATLAB/+qc/conf_seq.m +++ b/MATLAB/+qc/conf_seq.m @@ -1,326 +1,370 @@ function scan = conf_seq(varargin) - % CONF_SEQ Create special-measure scans with inline qctoolkit pulses - % - % Only supports inline scans at the moment (could in principle arm a - % different program in each loop iteration using prefns but this is not - % implemented at the moment). - % - % Please only add aditional configfns directly before turning the AWG on - % since some other programs fetch information using configfn indices. - % - % This function gets only underscore arguments to be more consistend with - % qctoolkit. Other variables in this function are camel case. - % - % --- Outputs ------------------------------------------------------------- - % scan : special-measure scan - % - % --- Inputs -------------------------------------------------------------- - % varargin : name-value pairs or parameter struct. For a list of - % parameters see the struct defaultArgs below. - % - % ------------------------------------------------------------------------- - % (c) 2018/02 Pascal Cerfontaine (cerfontaine@physik.rwth-aachen.de) - - global plsdata - - alazarName = plsdata.daq.instSmName; - - % None of the arguments except pulse_template should contain any python - % objects to avoid erroneous saving when the scan is executed. - defaultArgs = struct(... - ... Pulses - 'program_name', 'default_program', ... - 'pulse_template', 'default_pulse', ... - 'parameters_and_dicts', {plsdata.awg.defaultParametersAndDicts}, ... - 'channel_mapping', plsdata.awg.defaultChannelMapping, ... - 'window_mapping', plsdata.awg.defaultWindowMapping, ... - 'add_marker', {plsdata.awg.defaultAddMarker}, ... - 'force_update', false, ... - ... - ... Pulse modification - 'pulse_modifier_args', struct(), ... % Additional arguments passed to the pulse_modifier_fn - 'pulse_modifier', false, ... % Automatically change the variable a (all input arguments) below, can be used to dynamically modify the pulse - 'pulse_modifier_fn', @tune.add_dbz_fid, ... % Can specify a custom function here which modifies the variable a (all input arguments) below - ... - ... Saving variables - 'save_custom_var_fn', @tune.get_global_opts,... % Can specify a function which returns data to be saved in the scan - 'save_custom_var_args', {{'dnp', 'tune_gui'}}, ... - 'save_metadata_fns', {{@sm_scans.triton_200.metafn_get_configchanvals} ... % Can specify functions to log metadata during each loop - {@sm_scans.triton_200.metafn_get_rf_channels}}, ... - 'save_metadata_fields', {{'configchanvals'} {'rfChannels'}}, ... % Fieldnames of the metadata struct saved by smrun - ... - ... Measurements - 'operations', {plsdata.daq.defaultOperations}, ... - ... - ... Other - 'nrep', 10, ... % Numer of repetition of pulse - 'fig_id', 2000, ... - 'fig_position', [], ... - 'disp_ops', ' default', ... % Refers to operations: List of indices of operations to show - 'disp_dim', [1 2], ... % dimension of display - 'delete_getchans', [1], ... % Refers to getchans: Indices of getchans (including those generated by procfns) to delete after the scan is complete - 'procfn_ops', {{}}, ... % Refers to operations: One entry for each virtual channel, each cell entry has four or five element: fn, args, dim, operation index, (optional) identifier - 'saveloop', 0, ... % save every nth loop - 'useCustomCleanupFn', false, ... % If this flag is true - 'customCleanupFn', [], ... % clean up anything else you would like cleaned up - 'useCustomConfigFn', false, ... % If this flag is true - 'customConfigFn', [], ... % add a custom config function which is executed directly before the AWG is turned on - 'arm_global', false, ... % If true, set the program to be armed via tunedata.global_opts.conf_seq.arm_program_name. - ... % If you use this, all programs need to be uploaded manually before the scan and need to - ... % have the same Alazar configuration. - 'rf_sources', [true true], ... % turn RF sources on and off automatically - 'buffer_strategy', {plsdata.daq.defaultBufferStrategy},... % call qc.set_alazar_buffer_strategy with these arguments before pulse - 'verbosity', 10 ... % 0: display nothing, 10: display all except when arming program, 11: display all - ); - a = util.parse_varargin(varargin, defaultArgs); - aOriginal = a; - - if a.pulse_modifier - try - a = feval(a.pulse_modifier_fn, a); % Add any proprietary function here - catch err - warning('Could not run pulse_modifier_fn successfully. Continuing as if pulse_modifier was false:\n%s', err.getReport()); - a = aOriginal; - end - end - - if ~ischar(a.pulse_template) && ~isstruct(a.pulse_template) - a.pulse_template = qc.pulse_to_struct(a.pulse_template); - end - - if numel(a.rf_sources) == 1 +% CONF_SEQ Create special-measure scans with inline qctoolkit pulses +% +% Only supports inline scans at the moment (could in principle arm a +% different program in each loop iteration using prefns but this is not +% implemented at the moment). +% +% Please only add aditional configfns directly before turning the AWG on +% since some other programs fetch information using configfn indices. +% +% This function gets only underscore arguments to be more consistend with +% qctoolkit. Other variables in this function are camel case. +% +% --- Outputs ------------------------------------------------------------- +% scan : special-measure scan +% +% --- Inputs -------------------------------------------------------------- +% varargin : name-value pairs or parameter struct. For a list of +% parameters see the struct defaultArgs below. +% +% ------------------------------------------------------------------------- +% (c) 2018/02 Pascal Cerfontaine (cerfontaine@physik.rwth-aachen.de) + +global plsdata + +alazarName = plsdata.daq.instSmName; + +% None of the arguments except pulse_template should contain any python +% objects to avoid erroneous saving when the scan is executed. +defaultArgs = struct(... + ... Pulses + 'program_name', 'default_program', ... + 'pulse_template', 'default_pulse', ... + 'parameters_and_dicts', {plsdata.awg.defaultParametersAndDicts}, ... + 'channel_mapping', plsdata.awg.defaultChannelMapping, ... + 'window_mapping', plsdata.awg.defaultWindowMapping, ... + 'add_marker', {plsdata.awg.defaultAddMarker}, ... + 'force_update', false, ... + ... + ... Pulse modification + 'pulse_modifier_args', struct(), ... % Additional arguments passed to the pulse_modifier_fn + 'pulse_modifier', false, ... % Automatically change the variable a (all input arguments) below, can be used to dynamically modify the pulse + 'pulse_modifier_fn', @tune.add_dbz_fid, ... % Can specify a custom function here which modifies the variable a (all input arguments) below + ... + ... Saving variables + 'save_custom_var_fn', @tune.get_global_opts,... % Can specify a function which returns data to be saved in the scan + 'save_custom_var_args', {{'dnp', 'tune_gui'}}, ... + 'save_metadata_fns', {{@sm_scans.triton_200.metafn_get_configchanvals ... % Can specify functions to log metadata during each loop + @sm_scans.triton_200.metafn_get_rf_channels}}, ... + 'save_metadata_fields', {{{'configchanvals'} {'rfChannels'}}}, ... % Fieldnames of the metadata struct saved by smrun + ... + ... Measurements + 'operations', {plsdata.daq.defaultOperations}, ... + ... + ... Other + 'nrep', 10, ... % Numer of repetition of pulse + 'fig_id', 2000, ... + 'fig_position', [], ... + 'disp_ops', 'default', ... % Refers to operations: List of indices of operations to show + 'disp_dim', [1 2], ... % dimension of display + 'delete_getchans', [1], ... % Refers to getchans: Indices of getchans (including those generated by procfns) to delete after the scan is complete + 'procfn_ops', {{}}, ... % Refers to operations: One entry for each virtual channel, each cell entry has four or five element: fn, args, dim, operation index, (optional) identifier + 'saveloop', 0, ... % save every nth loop + 'useCustomCleanupFn', false, ... % If this flag is true + 'customCleanupFn', [], ... % clean up anything else you would like cleaned up + 'useCustomConfigFn', false, ... % If this flag is true + 'customConfigFn', [], ... % add a custom config function which is executed directly before the AWG is turned on + 'arm_global', false, ... % If true, set the program to be armed via tunedata.global_opts.conf_seq.arm_program_name. + ... % If you use this, all programs need to be uploaded manually before the scan and need to + ... % have the same Alazar configuration. + 'rf_sources', [true true], ... % turn RF sources on and off automatically + 'defer_markers', {plsdata.awg.defaultDeferMarkers}, ... % defer markers to the pulse (ie let qupulse set them) + 'buffer_strategy', {plsdata.daq.defaultBufferStrategy},...% call qc.set_alazar_buffer_strategy with these arguments before pulse + 'verbosity', 10, ... % 0: display nothing, 10: display all except when arming program, 11: display all + 'dac_scan', []... + ); +a = util.parse_varargin(varargin, defaultArgs); +aOriginal = a; + +if a.pulse_modifier + try + a = feval(a.pulse_modifier_fn, a); % Add any proprietary function here + catch err + warning('Could not run pulse_modifier_fn successfully. Continuing as if pulse_modifier was false:\n%s', err.getReport()); + a = aOriginal; + end +end + +if ~ischar(a.pulse_template) && ~isstruct(a.pulse_template) + a.pulse_template = qc.pulse_to_struct(a.pulse_template); +end + +if numel(a.rf_sources) == 1 a.rf_sources = [a.rf_sources a.rf_sources]; - end - - scan = struct('configfn', [], 'cleanupfn', [], 'loops', struct('prefn', [], 'metafn', [])); - - % Save file and arguments with which scan was created (not stricly necessary) - try - if ischar(aOriginal.pulse_modifier_fn) - scan.data.pulse_modifier_fn = fileread(which(aOriginal.pulse_modifier_fn)); - else - scan.data.pulse_modifier_fn = fileread(which(func2str(aOriginal.pulse_modifier_fn))); - end - catch err - warning('Could not load pulse_modifier_fn for saving in scan for reproducibility:\n%s', err.getReport()); - end - scan.data.conf_seq_fn = fileread([mfilename('fullpath') '.m']); - scan.data.conf_seq_args = aOriginal; - - % Configure channels - scan.loops(1).getchan = {'ATSV', 'time'}; - scan.loops(1).setchan = {'count'}; - scan.loops(1).ramptime = []; - scan.loops(1).npoints = a.nrep; - scan.loops(1).rng = []; - - nGetChan = numel(scan.loops(1).getchan); - nOperations = numel(a.operations); - - % Turn AWG outputs off if scan stops (even if due to error) - scan.configfn(end+1).fn = @qc.cleanupfn_awg; - scan.configfn(end).args = {}; - - % Turn RF sources off if scan stops (even if due to error) - if any(a.rf_sources) - scan.configfn(end+1).fn = @qc.cleanupfn_rf_sources; - scan.configfn(end).args = {}; - end - - % Alazar buffer strategy. Can be used to mitigate buffer artifacts. - scan.configfn(end+1).fn = @smaconfigwrap; - scan.configfn(end).args = [{@qc.set_alazar_buffer_strategy}, a.buffer_strategy]; - - % Configure AWG - % * Calling qc.awg_program('add', ...) makes sure the pulse is uploaded - % again if any parameters changed. - % * If dictionaries were passed as strings, this will automatically - % reload the dictionaries and thus use any changes made in the - % dictionaries in the meantime. - % * The original parameters are saved in scan.data.awg_program. This - % includes the pulse_template in json format and all dictionary - % entries at the time when the scan was executed. - % * If a python pulse_template was passed, this will still save - % correctly since it was converted into a Matlab struct above. - scan.configfn(end+1).fn = @smaconfigwrap_save_data; - scan.configfn(end).args = {'awg_program', @qc.awg_program, 'add', a}; - - % Configure Alazar operations - % * alazar.update_settings = py.True is automatically set. This results - % in reconfiguration of the Alazar which takes a long time. Thus this - % should only be done before a scan is started (i.e. in a configfn). - % * qc.dac_operations('add', a) also resets the virtual channel in - % smdata.inst(sminstlookup(alazarName)).data.virtual_channel. - scan.configfn(end+1).fn = @smaconfigwrap_save_data; - scan.configfn(end).args = {'daq_operations', @qc.daq_operations, 'add', a}; - - % Configure Alazar virtual channel - % * Set datadim of instrument correctly - % * Save operation lengths in scan.data - scan.configfn(end+1).fn = @smaconfigwrap_save_data; - scan.configfn(end).args = {'daq_operations_length', @qc.daq_operations, 'set length', a}; - - % Extract operation data from first channel ('ATSV') - % * Add procfns to scan, one for each operation - % * The configfn qc.conf_seq_procfn sets args and dim of the first n - % procfns, where n is the number of operations. This ensures that start - % and stop always use the correct lengths even if they have changed due - % to changes in pulse dictionaries. qc.conf_seq_procfn assumes that the - % field scan.data.daq_operations_length has been set dynamically by a - % previous configfn. - nGetChan = numel(scan.loops(1).getchan); - for p = 1:numel(a.operations) - scan.loops(1).procfn(nGetChan + p).fn(1) = struct( ... - 'fn', @(x, startInd, stopInd)( x(startInd:stopInd) ), ... - 'args', {{nan, nan}}, ... - 'inchan', 1, ... - 'outchan', nGetChan + p ... - ); - scan.loops(1).procfn(nGetChan + p).dim = nan; - end - scan.configfn(end+1).fn = @qc.conf_seq_procfn; - scan.configfn(end).args = {}; - - if any(a.rf_sources) - % Turn RF switches on - scan.configfn(end+1).fn = @smaconfigwrap; - scan.configfn(end).args = {@smset, 'RF1_on', double(a.rf_sources(1))}; - scan.configfn(end+1).fn = @smaconfigwrap; - scan.configfn(end).args = {@smset, 'RF2_on', double(a.rf_sources(2))}; - scan.configfn(end+1).fn = @smaconfigwrap; - scan.configfn(end).args = {@pause, 0.05}; % So RF sources definitely on - - % Turn RF switches off - % -> already done by qc.cleanupfn_rf_sources called above - end - - % Add custom variables for documentation purposes - scan.configfn(end+1).fn = @smaconfigwrap_save_data; - scan.configfn(end).args = {'custom_var', a.save_custom_var_fn, a.save_custom_var_args}; - - % Add custom cleanup fn - if a.useCustomCleanupFn && ~isempty(a.customCleanupFn) - scan.configfn(end+1).fn = a.customCleanupFn; - scan.configfn(end).args = {}; - end - - % Add custom config fn - if a.useCustomConfigFn && ~isempty(a.customConfigFn) - scan.configfn(end+1).fn = a.customConfigFn; - scan.configfn(end).args = {}; - end - - % Delete unnecessary data - scan.cleanupfn(end+1).fn = @qc.cleanupfn_delete_getchans; - scan.cleanupfn(end).args = {a.delete_getchans}; - - % Allow time logging - % * Update dummy instrument with current time so can get the current time - % using a getchan - scan.loops(1).prefn(end+1).fn = @smaconfigwrap; - scan.loops(1).prefn(end).args = {@(chan)(smset('time', now()))}; - - % Allow logging metadata - for i = 1:length(a.save_metadata_fns) - scan.loops(1).metafn(end+1).fn = @smaconfigwrap_save_metadata; - scan.loops(1).metafn(end).args = {a.save_metadata_fields{i}, a.save_metadata_fns{i}}; - end - - % Turn AWG on - scan.configfn(end+1).fn = @smaconfigwrap; - scan.configfn(end).args = {@awgctrl, 'on'}; - - % Run AWG channel pair 1 - % * Arm the program - % * Trigger the Alazar - % * Will later also trigger the RF switches - % * Will run both channel pairs automatically if they are synced - % which they should be by default. - % * Should be the last prefn so no other channels changed when - % measurement starts (really necessary?) - scan.loops(1).prefn(end+1).fn = @smaconfigwrap; - if ~a.arm_global - scan.loops(1).prefn(end).args = {@qc.awg_program, 'arm', qc.change_field(a, 'verbosity', a.verbosity-1)}; - else - scan.loops(1).prefn(end).args = {@qc.awg_program, 'arm global', qc.change_field(a, 'verbosity', a.verbosity-1)}; - end - scan.loops(1).prefn(end+1).fn = @smaconfigwrap; - scan.loops(1).prefn(end).args = {@awgctrl, 'run', 1}; - - % Get AWG information (not needed at the moment) - % [analogNames, markerNames, channels] = qc.get_awg_channels(); - % [programNames, programs] = qc.get_awg_programs(); - - % Default display - if strcmp(a.disp_ops, 'default') - a.disp_ops = 1:min(4, nOperations); - end - - % Add user procfns - if isfield(scan.loops(1), 'procfn') - nProcFn = numel(scan.loops(1).procfn); - else - nProcFn = 0; - end - for opInd = 1:numel(a.procfn_ops) % count through operations - inchan = nGetChan + a.procfn_ops{opInd}{4}; - scan.loops(1).procfn(end+1).fn(1) = struct( ... - 'fn', a.procfn_ops{opInd}{1}, ... - 'args', {a.procfn_ops{opInd}{2}}, ... - 'inchan', inchan, ... - 'outchan', nProcFn + opInd ... - ); - scan.loops(1).procfn(end).dim = a.procfn_ops{opInd}{3}; - if numel(a.procfn_ops{opInd}) >= 5 - scan.loops(1).procfn(end).identifier = a.procfn_ops{opInd}{5}; - end - end - - % Configure display - scan.figure = a.fig_id; - if ~isempty(a.fig_position) - scan.figpos = a.fig_position; - end - scan.disp = []; - for l = 1:length(a.disp_ops) - for d = a.disp_dim - scan.disp(end+1).loop = 1; - scan.disp(end).channel = nGetChan + a.disp_ops(l); - scan.disp(end).dim = d; - - if a.disp_ops(l) <= nOperations - opInd = a.disp_ops(l); - else - opInd = a.procfn_ops{a.disp_ops(l)-nOperations}{4}; - end - - % added new condition "numel(opInd) == 1" to check for several - % inchans, later they should get a proper title (marcel) - if numel(opInd) == 1 && opInd <= numel(a.operations) - scan.disp(end).title = prepare_title(sprintf(['%s: '], a.operations{opInd}{:})); - elseif numel(opInd) == 1 && length(a.procfn_ops{opInd - nOperations}) > 4 - scan.disp(end).title = prepare_title(sprintf(['%s: '], a.procfn_ops{opInd - nOperations}{5})); - else - scan.disp(end).title = ''; - end - end - end - - if a.saveloop > 0 - scan.saveloop = [1, a.saveloop]; - end - -end - - - +end + +scan = struct('configfn', [], 'cleanupfn', [], 'loops', struct('prefn', [], 'metafn', [])); + +% Save file and arguments with which scan was created (not stricly necessary) +try + if ischar(aOriginal.pulse_modifier_fn) + scan.data.pulse_modifier_fn = fileread(which(aOriginal.pulse_modifier_fn)); + else + scan.data.pulse_modifier_fn = fileread(which(func2str(aOriginal.pulse_modifier_fn))); + end +catch err + warning('Could not load pulse_modifier_fn for saving in scan for reproducibility:\n%s', err.getReport()); +end +scan.data.conf_seq_fn = fileread([mfilename('fullpath') '.m']); +scan.data.conf_seq_args = aOriginal; + +% Configure channels +% Hack to mix DAC sweep with AWG scan. +if ~isempty(a.dac_scan) + scan.loops(1).getchan = {'ATSV', 'time'}; + scan.loops(1).setchan = {a.dac_scan.channel};% {'count'}; + scan.loops(1).ramptime = []; + scan.loops(1).npoints = a.dac_scan.npoints; %a.nrep; + scan.loops(1).rng = a.dac_scan.range;%[] + if ~isempty(a.dac_scan.trafo) + scan.loops(1).setchan = {a.dac_scan.channel a.dac_scan.trafo.channel}; + scan.loops(1).trafofn(2).fn = a.dac_scan.compst; + scan.loops(1).trafofn(2).args = a.dac_scan.trafo.args; + end +else + scan.loops(1).getchan = {'ATSV', 'time'}; + scan.loops(1).setchan = {'count'}; + scan.loops(1).ramptime = []; + scan.loops(1).npoints = a.nrep; + scan.loops(1).rng = []; +end + +nGetChan = numel(scan.loops(1).getchan); +nOperations = numel(a.operations); + +% Turn AWG outputs off if scan stops (even if due to error) +scan.configfn(end+1).fn = @qc.cleanupfn_awg; +scan.configfn(end).args = {}; + +% Turn RF sources off if scan stops (even if due to error) +if any(a.rf_sources) + scan.configfn(end+1).fn = @qc.cleanupfn_rf_sources; + scan.configfn(end).args = {}; +end + +% Alazar buffer strategy. Can be used to mitigate buffer artifacts. +scan.configfn(end+1).fn = @smaconfigwrap; +scan.configfn(end).args = [{@qc.set_alazar_buffer_strategy}, a.buffer_strategy]; + +% Drop previously recorded and not measured scanlines +scan.configfn(end+1).fn = @smaconfigwrap; +scan.configfn(end).args = [{@qc.clear_old_scanlines}, ]; + +% Configure AWG +% * Calling qc.awg_program('add', ...) makes sure the pulse is uploaded +% again if any parameters changed. +% * If dictionaries were passed as strings, this will automatically +% reload the dictionaries and thus use any changes made in the +% dictionaries in the meantime. +% * The original parameters are saved in scan.data.awg_program. This +% includes the pulse_template in json format and all dictionary +% entries at the time when the scan was executed. +% * If a python pulse_template was passed, this will still save +% correctly since it was converted into a Matlab struct above. +scan.configfn(end+1).fn = @smaconfigwrap_save_data; +scan.configfn(end).args = {'awg_program', @qc.awg_program, 'add', a}; + +% Configure Alazar operations +% * alazar.update_settings = py.True is automatically set. This results +% in reconfiguration of the Alazar which takes a long time. Thus this +% should only be done before a scan is started (i.e. in a configfn). +% * qc.dac_operations('add', a) also resets the virtual channel in +% smdata.inst(sminstlookup(alazarName)).data.virtual_channel. +scan.configfn(end+1).fn = @smaconfigwrap_save_data; +scan.configfn(end).args = {'daq_operations', @qc.daq_operations, 'add', a}; + +% Configure Alazar virtual channel +% * Set datadim of instrument correctly +% * Save operation lengths in scan.data +scan.configfn(end+1).fn = @smaconfigwrap_save_data; +scan.configfn(end).args = {'daq_operations_length', @qc.daq_operations, 'set length', a}; + +% Extract operation data from first channel ('ATSV') +% * Add procfns to scan, one for each operation +% * The configfn qc.conf_seq_procfn sets args and dim of the first n +% procfns, where n is the number of operations. This ensures that start +% and stop always use the correct lengths even if they have changed due +% to changes in pulse dictionaries. qc.conf_seq_procfn assumes that the +% field scan.data.daq_operations_length has been set dynamically by a +% previous configfn. +nGetChan = numel(scan.loops(1).getchan); +for p = 1:numel(a.operations) + scan.loops(1).procfn(nGetChan + p).fn(1) = struct( ... + 'fn', @(x, startInd, stopInd)( x(startInd:stopInd) ), ... + 'args', {{nan, nan}}, ... + 'inchan', 1, ... + 'outchan', nGetChan + p ... + ); + scan.loops(1).procfn(nGetChan + p).dim = nan; +end +scan.configfn(end+1).fn = @qc.conf_seq_procfn; +scan.configfn(end).args = {}; + +if any(a.rf_sources) + % Turn RF switches on + scan.configfn(end+1).fn = @smaconfigwrap; + scan.configfn(end).args = {@smset, 'RF1_on', double(a.rf_sources(1))}; + scan.configfn(end+1).fn = @smaconfigwrap; + scan.configfn(end).args = {@smset, 'RF2_on', double(a.rf_sources(2))}; + scan.configfn(end+1).fn = @smaconfigwrap; + scan.configfn(end).args = {@pause, 0.05}; % So RF sources definitely on + + % Turn RF switches off + % -> already done by qc.cleanupfn_rf_sources called above +end + +if ~isempty(a.defer_markers) + + if isa(a.defer_markers, 'bool') + % backwards compability + if a.defer_markers + defer_markers = {'AlazarTrig', 'AWGSwitch1', 'AWGSwitch2'}; + else + defer_markers = {}; + end + else + defer_markers = a.defer_markers; + end + + for marker = reshape(defer_markers, 1, []) + % Set marker to be read from pulse + scan.configfn(end+1).fn = @smaconfigwrap; + scan.configfn(end).args = {@smset, marker{1}, Inf}; + + % .. and restore previous state + scan.cleanupfn(end+1).fn = @smaconfigwrap; + scan.cleanupfn(end).args = {@smset, marker{1}, tune.smget_mat(marker{1})}; + end +end + +% Add custom variables for documentation purposes +scan.configfn(end+1).fn = @smaconfigwrap_save_data; +scan.configfn(end).args = {'custom_var', a.save_custom_var_fn, a.save_custom_var_args}; + +% Add custom cleanup fn +if a.useCustomCleanupFn && ~isempty(a.customCleanupFn) + scan.configfn(end+1).fn = a.customCleanupFn; + scan.configfn(end).args = {}; +end + +% Add custom config fn +if a.useCustomConfigFn && ~isempty(a.customConfigFn) + scan.configfn(end+1).fn = a.customConfigFn; + scan.configfn(end).args = {}; +end + +% Delete unnecessary data +scan.cleanupfn(end+1).fn = @qc.cleanupfn_delete_getchans; +scan.cleanupfn(end).args = {a.delete_getchans}; + +% Allow time logging +% * Update dummy instrument with current time so can get the current time +% using a getchan +scan.loops(1).prefn(end+1).fn = @smaconfigwrap; +scan.loops(1).prefn(end).args = {@(chan)(smset('time', now()))}; + +% Allow logging metadata +for i = 1:length(a.save_metadata_fns) + scan.loops(1).metafn(end+1).fn = @smaconfigwrap_save_metadata; + scan.loops(1).metafn(end).args = {a.save_metadata_fields{i}, a.save_metadata_fns{i}}; +end + +% Turn AWG on +scan.configfn(end+1).fn = @smaconfigwrap; +scan.configfn(end).args = {@awgctrl, 'on'}; + +% Run AWG channel pair 1 +% * Arm the program +% * Trigger the Alazar +% * Will later also trigger the RF switches +% * Will run both channel pairs automatically if they are synced +% which they should be by default. +% * Should be the last prefn so no other channels changed when +% measurement starts (really necessary?) +scan.loops(1).prefn(end+1).fn = @smaconfigwrap; +if ~a.arm_global + scan.loops(1).prefn(end).args = {@qc.awg_program, 'arm', qc.change_field(a, 'verbosity', a.verbosity-1)}; +else + scan.loops(1).prefn(end).args = {@qc.awg_program, 'arm global', qc.change_field(a, 'verbosity', a.verbosity-1)}; +end +scan.loops(1).prefn(end+1).fn = @smaconfigwrap; +scan.loops(1).prefn(end).args = {@awgctrl, 'run', 1}; + +% Get AWG information (not needed at the moment) +% [analogNames, markerNames, channels] = qc.get_awg_channels(); +% [programNames, programs] = qc.get_awg_programs(); + +% Default display +if strcmp(a.disp_ops, 'default') + a.disp_ops = 1:min(4, nOperations); +end + +% Add user procfns +if isfield(scan.loops(1), 'procfn') + nProcFn = numel(scan.loops(1).procfn); +else + nProcFn = 0; +end +for opInd = 1:numel(a.procfn_ops) % count through operations + inchan = nGetChan + a.procfn_ops{opInd}{4}; + scan.loops(1).procfn(end+1).fn(1) = struct( ... + 'fn', a.procfn_ops{opInd}{1}, ... + 'args', {a.procfn_ops{opInd}{2}}, ... + 'inchan', inchan, ... + 'outchan', nProcFn + opInd ... + ); + scan.loops(1).procfn(end).dim = a.procfn_ops{opInd}{3}; + if numel(a.procfn_ops{opInd}) >= 5 + scan.loops(1).procfn(end).identifier = a.procfn_ops{opInd}{5}; + end +end + +% Configure display +scan.figure = a.fig_id; +if ~isempty(a.fig_position) + scan.figpos = a.fig_position; +end +scan.disp = []; +for l = 1:length(a.disp_ops) + for d = a.disp_dim + scan.disp(end+1).loop = 1; + scan.disp(end).channel = nGetChan + a.disp_ops(l); + scan.disp(end).dim = d; + + if a.disp_ops(l) <= nOperations + opInd = a.disp_ops(l); + else + opInd = a.procfn_ops{a.disp_ops(l)-nOperations}{4}; + end + + % added new condition "numel(opInd) == 1" to check for several + % inchans, later they should get a proper title (marcel) + if numel(opInd) == 1 && opInd <= numel(a.operations) + scan.disp(end).title = prepare_title(sprintf(['%s: '], a.operations{opInd}{:})); + elseif numel(opInd) == 1 && length(a.procfn_ops{opInd - nOperations}) > 4 + scan.disp(end).title = prepare_title(sprintf(['%s: '], a.procfn_ops{opInd - nOperations}{5})); + else + scan.disp(end).title = ''; + end + end +end + +if a.saveloop > 0 + scan.saveloop = [1, a.saveloop]; +end + +end + + + function str = prepare_title(str) - - str = strrep(str, '_', ' '); - str = str(1:end-2); - - str = strrep(str, 'RepAverage', 'RSA'); - str = strrep(str, 'Downsample', 'DS'); - str = strrep(str, 'Qubit', 'Q'); - -end \ No newline at end of file + +str = strrep(str, '_', ' '); +str = str(1:end-2); + +str = strrep(str, 'RepAverage', 'RSA'); +str = strrep(str, 'Downsample', 'DS'); +str = strrep(str, 'Qubit', 'Q'); + +end diff --git a/MATLAB/+qc/conf_seq_qubus.m b/MATLAB/+qc/conf_seq_qubus.m new file mode 100644 index 000000000..de2499c7d --- /dev/null +++ b/MATLAB/+qc/conf_seq_qubus.m @@ -0,0 +1,380 @@ +function scan = conf_seq_qubus(varargin) +% CONF_SEQ Create special-measure scans with inline qctoolkit pulses +% +% Only supports inline scans at the moment (could in principle arm a +% different program in each loop iteration using prefns but this is not +% implemented at the moment). +% +% Please only add aditional configfns directly before turning the AWG on +% since some other programs fetch information using configfn indices. +% +% This function gets only underscore arguments to be more consistend with +% qctoolkit. Other variables in this function are camel case. +% +% --- Outputs ------------------------------------------------------------- +% scan : special-measure scan +% +% --- Inputs -------------------------------------------------------------- +% varargin : name-value pairs or parameter struct. For a list of +% parameters see the struct defaultArgs below. +% +% ------------------------------------------------------------------------- +% (c) 2018/02 Pascal Cerfontaine (cerfontaine@physik.rwth-aachen.de) + +global plsdata + +alazarName = plsdata.daq.instSmName; + +% None of the arguments except pulse_template should contain any python +% objects to avoid erroneous saving when the scan is executed. +defaultArgs = struct(... + ... Pulses + 'program_name', 'default_program', ... + 'pulse_template', 'default_pulse', ... + 'parameters_and_dicts', {plsdata.awg.defaultParametersAndDicts}, ... + 'channel_mapping', plsdata.awg.defaultChannelMapping, ... + 'window_mapping', plsdata.awg.defaultWindowMapping, ... + 'global_transformation', plsdata.awg.globalTransformation, ... + 'add_marker', {plsdata.awg.defaultAddMarker}, ... + 'force_update', false, ... + ... + ... Pulse modification + 'pulse_modifier_args', struct(), ... % Additional arguments passed to the pulse_modifier_fn + 'pulse_modifier', false, ... % Automatically change the variable a (all input arguments) below, can be used to dynamically modify the pulse + 'pulse_modifier_fn', @tune.add_dbz_fid, ... % Can specify a custom function here which modifies the variable a (all input arguments) below + ... + ... Saving variables + 'save_custom_var_fn', @tune.get_global_opts,... % Can specify a function which returns data to be saved in the scan + 'save_custom_var_args', {{'dnp', 'tune_gui'}}, ... + 'save_metadata_fns', {{@sm_scans.triton_200.metafn_get_configchanvals ... % Can specify functions to log metadata during each loop + @sm_scans.triton_200.metafn_get_rf_channels}}, ... + 'save_metadata_fields', {{{'configchanvals'} {'rfChannels'}}}, ... % Fieldnames of the metadata struct saved by smrun + ... + ... Measurements + 'operations', {plsdata.daq.defaultOperations}, ... + ... + ... Other + 'nrep', 10, ... % Numer of repetition of pulse + 'fig_id', 2000, ... + 'fig_position', [], ... + 'disp_ops', 'default', ... % Refers to operations: List of indices of operations to show + 'disp_dim', [1 2], ... % dimension of display + 'delete_getchans', [1], ... % Refers to getchans: Indices of getchans (including those generated by procfns) to delete after the scan is complete + 'procfn_ops', {{}}, ... % Refers to operations: One entry for each virtual channel, each cell entry has four or five element: fn, args, dim, operation index, (optional) identifier + 'saveloop', 0, ... % save every nth loop + 'useCustomCleanupFn', false, ... % If this flag is true + 'customCleanupFn', [], ... % clean up anything else you would like cleaned up + 'useCustomConfigFn', false, ... % If this flag is true + 'customConfigFn', [], ... % add a custom config function which is executed directly before the AWG is turned on + 'arm_global', false, ... % If true, set the program to be armed via tunedata.global_opts.conf_seq.arm_program_name. + ... % If you use this, all programs need to be uploaded manually before the scan and need to + ... % have the same Alazar configuration. + 'rf_sources', [true true], ... % turn RF sources on and off automatically + 'defer_markers', {plsdata.awg.defaultDeferMarkers}, ... % defer markers to the pulse (ie let qupulse set them) + 'buffer_strategy', {plsdata.daq.defaultBufferStrategy},...% call qc.set_alazar_buffer_strategy with these arguments before pulse + 'verbosity', 10, ... % 0: display nothing, 10: display all except when arming program, 11: display all + 'dac_scan', []... + ); +a = util.parse_varargin(varargin, defaultArgs); +aOriginal = a; + +if a.pulse_modifier + try + a = feval(a.pulse_modifier_fn, a); % Add any proprietary function here + catch err + warning('Could not run pulse_modifier_fn successfully. Continuing as if pulse_modifier was false:\n%s', err.getReport()); + a = aOriginal; + end +end + +if ~ischar(a.pulse_template) && ~isstruct(a.pulse_template) + a.pulse_template = qc.pulse_to_struct(a.pulse_template); +end + +if numel(a.rf_sources) == 1 + a.rf_sources = [a.rf_sources a.rf_sources]; +end + +scan = struct('configfn', [], 'cleanupfn', [], 'loops', struct('prefn', [], 'metafn', [])); + +% Save file and arguments with which scan was created (not stricly necessary) +try + if ischar(aOriginal.pulse_modifier_fn) + scan.data.pulse_modifier_fn = fileread(which(aOriginal.pulse_modifier_fn)); + else + scan.data.pulse_modifier_fn = fileread(which(func2str(aOriginal.pulse_modifier_fn))); + end +catch err + warning('Could not load pulse_modifier_fn for saving in scan for reproducibility:\n%s', err.getReport()); +end +scan.data.conf_seq_fn = fileread([mfilename('fullpath') '.m']); +scan.data.conf_seq_args = aOriginal; + +% Configure channels +% Hack to mix DAC sweep with AWG scan. +if ~isempty(a.dac_scan) + scan.loops(1).getchan = {'ATSV', 'time'}; + scan.loops(1).setchan = a.dac_scan.channel;% {'count'}; + scan.loops(1).ramptime = []; + scan.loops(1).npoints = a.dac_scan.npoints; %a.nrep; + scan.loops(1).rng = a.dac_scan.range;%[] + if ~isempty(a.dac_scan.trafo) + scan.loops(1).trafofn = a.dac_scan.trafo; + end +else + scan.loops(1).getchan = {'ATSV', 'time'}; + scan.loops(1).setchan = {'count'}; + scan.loops(1).ramptime = []; + scan.loops(1).npoints = a.nrep; + scan.loops(1).rng = []; +end + +nGetChan = numel(scan.loops(1).getchan); +nOperations = numel(a.operations); + +% Turn AWG outputs off if scan stops (even if due to error) +scan.configfn(end+1).fn = @qc.cleanupfn_awg; +scan.configfn(end).args = {}; + +% Turn RF sources off if scan stops (even if due to error) +if any(a.rf_sources) + scan.configfn(end+1).fn = @qc.cleanupfn_rf_sources; + scan.configfn(end).args = {}; +end + +% Alazar buffer strategy. Can be used to mitigate buffer artifacts. +scan.configfn(end+1).fn = @smaconfigwrap; +scan.configfn(end).args = [{@qc.set_alazar_buffer_strategy}, a.buffer_strategy]; + +% Drop previously recorded and not measured scanlines +scan.configfn(end+1).fn = @smaconfigwrap; +scan.configfn(end).args = [{@qc.clear_old_scanlines}, ]; + +% Configure AWG +% * Calling qc.awg_program('add', ...) makes sure the pulse is uploaded +% again if any parameters changed. +% * If dictionaries were passed as strings, this will automatically +% reload the dictionaries and thus use any changes made in the +% dictionaries in the meantime. +% * The original parameters are saved in scan.data.awg_program. This +% includes the pulse_template in json format and all dictionary +% entries at the time when the scan was executed. +% * If a python pulse_template was passed, this will still save +% correctly since it was converted into a Matlab struct above. +scan.configfn(end+1).fn = @smaconfigwrap_save_data; +scan.configfn(end).args = {'awg_program', @qc.awg_program, 'add', a}; + +% Configure Alazar operations +% * alazar.update_settings = py.True is automatically set. This results +% in reconfiguration of the Alazar which takes a long time. Thus this +% should only be done before a scan is started (i.e. in a configfn). +% * qc.dac_operations('add', a) also resets the virtual channel in +% smdata.inst(sminstlookup(alazarName)).data.virtual_channel. +scan.configfn(end+1).fn = @smaconfigwrap_save_data; +scan.configfn(end).args = {'daq_operations', @qc.daq_operations, 'add', a}; + +% Configure Alazar virtual channel +% * Set datadim of instrument correctly +% * Save operation lengths in scan.data +scan.configfn(end+1).fn = @smaconfigwrap_save_data; +scan.configfn(end).args = {'daq_operations_length', @qc.daq_operations, 'set length', a}; + +% Extract operation data from first channel ('ATSV') +% * Add procfns to scan, one for each operation +% * The configfn qc.conf_seq_procfn sets args and dim of the first n +% procfns, where n is the number of operations. This ensures that start +% and stop always use the correct lengths even if they have changed due +% to changes in pulse dictionaries. qc.conf_seq_procfn assumes that the +% field scan.data.daq_operations_length has been set dynamically by a +% previous configfn. +nGetChan = numel(scan.loops(1).getchan); +for p = 1:numel(a.operations) + scan.loops(1).procfn(nGetChan + p).fn(1) = struct( ... + 'fn', @(x, startInd, stopInd)( x(startInd:stopInd) ), ... + 'args', {{nan, nan}}, ... + 'inchan', 1, ... + 'outchan', nGetChan + p ... + ); + scan.loops(1).procfn(nGetChan + p).dim = nan; +end +scan.configfn(end+1).fn = @qc.conf_seq_procfn; +scan.configfn(end).args = {}; + +if any(a.rf_sources) + % Turn RF switches on + scan.configfn(end+1).fn = @smaconfigwrap; + scan.configfn(end).args = {@smset, 'RF1_on', double(a.rf_sources(1))}; + scan.configfn(end+1).fn = @smaconfigwrap; + scan.configfn(end).args = {@smset, 'RF2_on', double(a.rf_sources(2))}; + scan.configfn(end+1).fn = @smaconfigwrap; + scan.configfn(end).args = {@pause, 0.05}; % So RF sources definitely on + + % Turn RF switches off + % -> already done by qc.cleanupfn_rf_sources called above +end + +if ~isempty(a.defer_markers) + + if isa(a.defer_markers, 'bool') + % backwards compability + if a.defer_markers + defer_markers = {'AlazarTrig', 'AWGSwitch1', 'AWGSwitch2'}; + else + defer_markers = {}; + end + else + defer_markers = a.defer_markers; + end + + for marker = reshape(defer_markers, 1, []) + % Set marker to be read from pulse + scan.configfn(end+1).fn = @smaconfigwrap; + scan.configfn(end).args = {@smset, marker{1}, Inf}; + + % .. and restore previous state + scan.cleanupfn(end+1).fn = @smaconfigwrap; + scan.cleanupfn(end).args = {@smset, marker{1}, tune.smget_mat(marker{1})}; + end +end + +% Add custom variables for documentation purposes +scan.configfn(end+1).fn = @smaconfigwrap_save_data; +scan.configfn(end).args = {'custom_var', a.save_custom_var_fn, a.save_custom_var_args}; + +% Add custom cleanup fn +if a.useCustomCleanupFn && ~isempty(a.customCleanupFn) + scan.configfn(end+1).fn = a.customCleanupFn; + scan.configfn(end).args = {}; +end + +% Add custom config fn +if a.useCustomConfigFn && ~isempty(a.customConfigFn) + scan.configfn(end+1).fn = a.customConfigFn; + scan.configfn(end).args = {}; +end + +% Delete unnecessary data +scan.cleanupfn(end+1).fn = @qc.cleanupfn_delete_getchans; +scan.cleanupfn(end).args = {a.delete_getchans}; + +% Allow time logging +% * Update dummy instrument with current time so can get the current time +% using a getchan +scan.loops(1).prefn(end+1).fn = @smaconfigwrap; +scan.loops(1).prefn(end).args = {@(chan)(smset('time', now()))}; + +% Allow logging metadata +for i = 1:length(a.save_metadata_fns) + scan.loops(1).metafn(end+1).fn = @smaconfigwrap_save_metadata; + scan.loops(1).metafn(end).args = {a.save_metadata_fields{i}, a.save_metadata_fns{i}}; +end + +% Turn AWG on +scan.configfn(end+1).fn = @smaconfigwrap; +scan.configfn(end).args = {@awgctrl, 'on'}; + +% Run AWG channel pair 1 +% * Arm the program +% * Trigger the Alazar +% * Will later also trigger the RF switches +% * Will run both channel pairs automatically if they are synced +% which they should be by default. +% * Should be the last prefn so no other channels changed when +% measurement starts (really necessary?) +scan.loops(1).prefn(end+1).fn = @smaconfigwrap; +if ~a.arm_global + scan.loops(1).prefn(end).args = {@qc.awg_program, 'arm', qc.change_field(a, 'verbosity', a.verbosity-1)}; +else + scan.loops(1).prefn(end).args = {@qc.awg_program, 'arm global', qc.change_field(a, 'verbosity', a.verbosity-1)}; +end +scan.loops(1).prefn(end+1).fn = @smaconfigwrap; +scan.loops(1).prefn(end).args = {@awgctrl, 'run', 1}; + +% Get AWG information (not needed at the moment) +% [analogNames, markerNames, channels] = qc.get_awg_channels(); +% [programNames, programs] = qc.get_awg_programs(); + +% Default display +if strcmp(a.disp_ops, 'default') + a.disp_ops = 1:min(4, nOperations); +end + +% Add user procfns +if isfield(scan.loops(1), 'procfn') + nProcFn = numel(scan.loops(1).procfn); +else + nProcFn = 0; +end +for opInd = 1:numel(a.procfn_ops) % count through operations + inchan = nGetChan + a.procfn_ops{opInd}{4}; + scan.loops(1).procfn(end+1).fn(1) = struct( ... + 'fn', a.procfn_ops{opInd}{1}, ... + 'args', {a.procfn_ops{opInd}{2}}, ... + 'inchan', inchan, ... + 'outchan', nProcFn + opInd ... + ); + if ~isempty(a.procfn_ops{opInd}{3}) + scan.loops(1).procfn(end).dim = a.procfn_ops{opInd}{3}; + end + if numel(a.procfn_ops{opInd}) >= 5 + scan.loops(1).procfn(end).identifier = a.procfn_ops{opInd}{5}; + end +end + +% Configure display +scan.figure = a.fig_id; +if ~isempty(a.fig_position) + scan.figpos = a.fig_position; +end +scan.disp = []; +for l = 1:length(a.disp_ops) + for d = a.disp_dim(l) + scan.disp(end+1).loop = 1; + scan.disp(end).channel = nGetChan + a.disp_ops(l); + scan.disp(end).dim = d; + + if a.disp_ops(l) <= nOperations + opInd = a.disp_ops(l); + else + opInd = a.procfn_ops{a.disp_ops(l)-nOperations}{4}; + end + + % added new condition "numel(opInd) == 1" to check for several + % inchans, later they should get a proper title (marcel) + if numel(opInd) == 1 && opInd <= numel(a.operations) + if strcmp(a.operations{opInd}{1}, 'ChunkedAverage') + % the last element is the chuncksize exceeding unicode + % space resulting in meaningless Chinese character. + scan.disp(end).title = prepare_title(sprintf(['%s: '], a.operations{opInd}{1:end-1})); + else + scan.disp(end).title = prepare_title(sprintf(['%s: '], a.operations{opInd}{:})); + end + elseif numel(opInd) == 1 && length(a.procfn_ops{opInd - nOperations}) > 4 + scan.disp(end).title = prepare_title(sprintf(['%s: '], a.procfn_ops{opInd - nOperations}{5})); + else + scan.disp(end).title = ''; + end + end +end + +if a.saveloop > 0 + scan.saveloop = [1, a.saveloop]; +end + +end + + + +function str = prepare_title(str) + +str = strrep(str, '_', ' '); +str = str(1:end-2); + +str = strrep(str, 'RepAverage', 'RSA'); +str = strrep(str, 'Downsample', 'DS'); +str = strrep(str, 'Qubit', 'Q'); +str = strrep(str, 'Aux 1 Aux 2 Meas 1 Mask 1', 'SET Left'); +str = strrep(str, 'Aux 1 Aux 2 Meas 1 Mask 2', 'SET Right'); +str = strrep(str, 'Aux 1 Meas 1 Mask 1', 'SET Left - loading'); +str = strrep(str, 'Aux 1 Meas 2 Mask 1', 'SET Left - reading'); +end diff --git a/MATLAB/+qc/conf_seq_qubus_volatile_params.m b/MATLAB/+qc/conf_seq_qubus_volatile_params.m new file mode 100644 index 000000000..7a007537b --- /dev/null +++ b/MATLAB/+qc/conf_seq_qubus_volatile_params.m @@ -0,0 +1,372 @@ +function scan = conf_seq_qubus(varargin) +% CONF_SEQ Create special-measure scans with inline qctoolkit pulses +% +% Only supports inline scans at the moment (could in principle arm a +% different program in each loop iteration using prefns but this is not +% implemented at the moment). +% +% Please only add aditional configfns directly before turning the AWG on +% since some other programs fetch information using configfn indices. +% +% This function gets only underscore arguments to be more consistend with +% qctoolkit. Other variables in this function are camel case. +% +% --- Outputs ------------------------------------------------------------- +% scan : special-measure scan +% +% --- Inputs -------------------------------------------------------------- +% varargin : name-value pairs or parameter struct. For a list of +% parameters see the struct defaultArgs below. +% +% ------------------------------------------------------------------------- +% (c) 2018/02 Pascal Cerfontaine (cerfontaine@physik.rwth-aachen.de) + +global plsdata + +alazarName = plsdata.daq.instSmName; + +% None of the arguments except pulse_template should contain any python +% objects to avoid erroneous saving when the scan is executed. +defaultArgs = struct(... + ... Pulses + 'program_name', 'default_program', ... + 'pulse_template', 'default_pulse', ... + 'parameters_and_dicts', {plsdata.awg.defaultParametersAndDicts}, ... + 'channel_mapping', plsdata.awg.defaultChannelMapping, ... + 'window_mapping', plsdata.awg.defaultWindowMapping, ... + 'global_transformation', plsdata.awg.globalTransformation, ... + 'add_marker', {plsdata.awg.defaultAddMarker}, ... + 'force_update', false, ... + ... + ... Pulse modification + 'pulse_modifier_args', struct(), ... % Additional arguments passed to the pulse_modifier_fn + 'pulse_modifier', false, ... % Automatically change the variable a (all input arguments) below, can be used to dynamically modify the pulse + 'pulse_modifier_fn', @tune.add_dbz_fid, ... % Can specify a custom function here which modifies the variable a (all input arguments) below + ... + ... Saving variables + 'save_custom_var_fn', @tune.get_global_opts,... % Can specify a function which returns data to be saved in the scan + 'save_custom_var_args', {{'dnp', 'tune_gui'}}, ... + 'save_metadata_fns', {{@sm_scans.triton_200.metafn_get_configchanvals ... % Can specify functions to log metadata during each loop + @sm_scans.triton_200.metafn_get_rf_channels}}, ... + 'save_metadata_fields', {{{'configchanvals'} {'rfChannels'}}}, ... % Fieldnames of the metadata struct saved by smrun + ... + ... Measurements + 'operations', {plsdata.daq.defaultOperations}, ... + ... + ... Other + 'nrep', 10, ... % Numer of repetition of pulse + 'fig_id', 2000, ... + 'fig_position', [], ... + 'disp_ops', 'default', ... % Refers to operations: List of indices of operations to show + 'disp_dim', [1 2], ... % dimension of display + 'delete_getchans', [1], ... % Refers to getchans: Indices of getchans (including those generated by procfns) to delete after the scan is complete + 'procfn_ops', {{}}, ... % Refers to operations: One entry for each virtual channel, each cell entry has four or five element: fn, args, dim, operation index, (optional) identifier + 'saveloop', 0, ... % save every nth loop + 'useCustomCleanupFn', false, ... % If this flag is true + 'customCleanupFn', [], ... % clean up anything else you would like cleaned up + 'useCustomConfigFn', false, ... % If this flag is true + 'customConfigFn', [], ... % add a custom config function which is executed directly before the AWG is turned on + 'arm_global', false, ... % If true, set the program to be armed via tunedata.global_opts.conf_seq.arm_program_name. + ... % If you use this, all programs need to be uploaded manually before the scan and need to + ... % have the same Alazar configuration. + 'rf_sources', [true true], ... % turn RF sources on and off automatically + 'defer_markers', {plsdata.awg.defaultDeferMarkers}, ... % defer markers to the pulse (ie let qupulse set them) + 'buffer_strategy', {plsdata.daq.defaultBufferStrategy},...% call qc.set_alazar_buffer_strategy with these arguments before pulse + 'verbosity', 10, ... % 0: display nothing, 10: display all except when arming program, 11: display all + 'dac_scan', []... + ); +a = util.parse_varargin(varargin, defaultArgs); +aOriginal = a; + +if a.pulse_modifier + try + a = feval(a.pulse_modifier_fn, a); % Add any proprietary function here + catch err + warning('Could not run pulse_modifier_fn successfully. Continuing as if pulse_modifier was false:\n%s', err.getReport()); + a = aOriginal; + end +end + +if ~ischar(a.pulse_template) && ~isstruct(a.pulse_template) + a.pulse_template = qc.pulse_to_struct(a.pulse_template); +end + +if numel(a.rf_sources) == 1 + a.rf_sources = [a.rf_sources a.rf_sources]; +end + +scan = struct('configfn', [], 'cleanupfn', [], 'loops', struct('prefn', [], 'metafn', [])); + +% Save file and arguments with which scan was created (not stricly necessary) +try + if ischar(aOriginal.pulse_modifier_fn) + scan.data.pulse_modifier_fn = fileread(which(aOriginal.pulse_modifier_fn)); + else + scan.data.pulse_modifier_fn = fileread(which(func2str(aOriginal.pulse_modifier_fn))); + end +catch err + warning('Could not load pulse_modifier_fn for saving in scan for reproducibility:\n%s', err.getReport()); +end +scan.data.conf_seq_fn = fileread([mfilename('fullpath') '.m']); +scan.data.conf_seq_args = aOriginal; + +% Configure channels +% Hack to mix DAC sweep with AWG scan. +if ~isempty(a.dac_scan) + scan.loops(1).getchan = {'ATSV', 'time'}; + scan.loops(1).setchan = a.dac_scan.channel;% {'count'}; + scan.loops(1).ramptime = []; + scan.loops(1).npoints = a.dac_scan.npoints; %a.nrep; + scan.loops(1).rng = a.dac_scan.range;%[] + if ~isempty(a.dac_scan.trafo) + scan.loops(1).trafofn = a.dac_scan.trafo; + end +else + scan.loops(1).getchan = {'ATSV', 'time'}; + scan.loops(1).setchan = {'count'}; + scan.loops(1).ramptime = []; + scan.loops(1).npoints = a.nrep; + scan.loops(1).rng = []; +end + +nGetChan = numel(scan.loops(1).getchan); +nOperations = numel(a.operations); + +% Turn AWG outputs off if scan stops (even if due to error) +scan.configfn(end+1).fn = @qc.cleanupfn_awg; +scan.configfn(end).args = {}; + +% Turn RF sources off if scan stops (even if due to error) +if any(a.rf_sources) + scan.configfn(end+1).fn = @qc.cleanupfn_rf_sources; + scan.configfn(end).args = {}; +end + +% Alazar buffer strategy. Can be used to mitigate buffer artifacts. +scan.configfn(end+1).fn = @smaconfigwrap; +scan.configfn(end).args = [{@qc.set_alazar_buffer_strategy}, a.buffer_strategy]; + +% Drop previously recorded and not measured scanlines +scan.configfn(end+1).fn = @smaconfigwrap; +scan.configfn(end).args = [{@qc.clear_old_scanlines}, ]; + +% Configure AWG +% * Calling qc.awg_program('add', ...) makes sure the pulse is uploaded +% again if any parameters changed. +% * If dictionaries were passed as strings, this will automatically +% reload the dictionaries and thus use any changes made in the +% dictionaries in the meantime. +% * The original parameters are saved in scan.data.awg_program. This +% includes the pulse_template in json format and all dictionary +% entries at the time when the scan was executed. +% * If a python pulse_template was passed, this will still save +% correctly since it was converted into a Matlab struct above. +scan.configfn(end+1).fn = @smaconfigwrap_save_data; +scan.configfn(end).args = {'awg_program', @qc.awg_program, 'add', a}; + +% Configure Alazar operations +% * alazar.update_settings = py.True is automatically set. This results +% in reconfiguration of the Alazar which takes a long time. Thus this +% should only be done before a scan is started (i.e. in a configfn). +% * qc.dac_operations('add', a) also resets the virtual channel in +% smdata.inst(sminstlookup(alazarName)).data.virtual_channel. +scan.configfn(end+1).fn = @smaconfigwrap_save_data; +scan.configfn(end).args = {'daq_operations', @qc.daq_operations, 'add', a}; + +% Configure Alazar virtual channel +% * Set datadim of instrument correctly +% * Save operation lengths in scan.data +scan.configfn(end+1).fn = @smaconfigwrap_save_data; +scan.configfn(end).args = {'daq_operations_length', @qc.daq_operations, 'set length', a}; + +% Extract operation data from first channel ('ATSV') +% * Add procfns to scan, one for each operation +% * The configfn qc.conf_seq_procfn sets args and dim of the first n +% procfns, where n is the number of operations. This ensures that start +% and stop always use the correct lengths even if they have changed due +% to changes in pulse dictionaries. qc.conf_seq_procfn assumes that the +% field scan.data.daq_operations_length has been set dynamically by a +% previous configfn. +nGetChan = numel(scan.loops(1).getchan); +for p = 1:numel(a.operations) + scan.loops(1).procfn(nGetChan + p).fn(1) = struct( ... + 'fn', @(x, startInd, stopInd)( x(startInd:stopInd) ), ... + 'args', {{nan, nan}}, ... + 'inchan', 1, ... + 'outchan', nGetChan + p ... + ); + scan.loops(1).procfn(nGetChan + p).dim = nan; +end +scan.configfn(end+1).fn = @qc.conf_seq_procfn; +scan.configfn(end).args = {}; + +if any(a.rf_sources) + % Turn RF switches on + scan.configfn(end+1).fn = @smaconfigwrap; + scan.configfn(end).args = {@smset, 'RF1_on', double(a.rf_sources(1))}; + scan.configfn(end+1).fn = @smaconfigwrap; + scan.configfn(end).args = {@smset, 'RF2_on', double(a.rf_sources(2))}; + scan.configfn(end+1).fn = @smaconfigwrap; + scan.configfn(end).args = {@pause, 0.05}; % So RF sources definitely on + + % Turn RF switches off + % -> already done by qc.cleanupfn_rf_sources called above +end + +if ~isempty(a.defer_markers) + + if isa(a.defer_markers, 'bool') + % backwards compability + if a.defer_markers + defer_markers = {'AlazarTrig', 'AWGSwitch1', 'AWGSwitch2'}; + else + defer_markers = {}; + end + else + defer_markers = a.defer_markers; + end + + for marker = reshape(defer_markers, 1, []) + % Set marker to be read from pulse + scan.configfn(end+1).fn = @smaconfigwrap; + scan.configfn(end).args = {@smset, marker{1}, Inf}; + + % .. and restore previous state + scan.cleanupfn(end+1).fn = @smaconfigwrap; + scan.cleanupfn(end).args = {@smset, marker{1}, tune.smget_mat(marker{1})}; + end +end + +% Add custom variables for documentation purposes +scan.configfn(end+1).fn = @smaconfigwrap_save_data; +scan.configfn(end).args = {'custom_var', a.save_custom_var_fn, a.save_custom_var_args}; + +% Add custom cleanup fn +if a.useCustomCleanupFn && ~isempty(a.customCleanupFn) + scan.configfn(end+1).fn = a.customCleanupFn; + scan.configfn(end).args = {}; +end + +% Add custom config fn +if a.useCustomConfigFn && ~isempty(a.customConfigFn) + scan.configfn(end+1).fn = a.customConfigFn; + scan.configfn(end).args = {}; +end + +% Delete unnecessary data +scan.cleanupfn(end+1).fn = @qc.cleanupfn_delete_getchans; +scan.cleanupfn(end).args = {a.delete_getchans}; + +% Allow time logging +% * Update dummy instrument with current time so can get the current time +% using a getchan +scan.loops(1).prefn(end+1).fn = @smaconfigwrap; +scan.loops(1).prefn(end).args = {@(chan)(smset('time', now()))}; + +% Allow logging metadata +for i = 1:length(a.save_metadata_fns) + scan.loops(1).metafn(end+1).fn = @smaconfigwrap_save_metadata; + scan.loops(1).metafn(end).args = {a.save_metadata_fields{i}, a.save_metadata_fns{i}}; +end + +% Turn AWG on +scan.configfn(end+1).fn = @smaconfigwrap; +scan.configfn(end).args = {@awgctrl, 'on'}; + +% Run AWG channel pair 1 +% * Arm the program +% * Trigger the Alazar +% * Will later also trigger the RF switches +% * Will run both channel pairs automatically if they are synced +% which they should be by default. +% * Should be the last prefn so no other channels changed when +% measurement starts (really necessary?) +scan.loops(1).prefn(end+1).fn = @smaconfigwrap; +if ~a.arm_global + scan.loops(1).prefn(end).args = {@qc.awg_program, 'arm', qc.change_field(a, 'verbosity', a.verbosity-1)}; +else + scan.loops(1).prefn(end).args = {@qc.awg_program, 'arm global', qc.change_field(a, 'verbosity', a.verbosity-1)}; +end +scan.loops(1).prefn(end+1).fn = @smaconfigwrap; +scan.loops(1).prefn(end).args = {@awgctrl, 'run', 1}; + +% Get AWG information (not needed at the moment) +% [analogNames, markerNames, channels] = qc.get_awg_channels(); +% [programNames, programs] = qc.get_awg_programs(); + +% Default display +if strcmp(a.disp_ops, 'default') + a.disp_ops = 1:min(4, nOperations); +end + +% Add user procfns +if isfield(scan.loops(1), 'procfn') + nProcFn = numel(scan.loops(1).procfn); +else + nProcFn = 0; +end +for opInd = 1:numel(a.procfn_ops) % count through operations + inchan = nGetChan + a.procfn_ops{opInd}{4}; + scan.loops(1).procfn(end+1).fn(1) = struct( ... + 'fn', a.procfn_ops{opInd}{1}, ... + 'args', {a.procfn_ops{opInd}{2}}, ... + 'inchan', inchan, ... + 'outchan', nProcFn + opInd ... + ); + if ~isempty(a.procfn_ops{opInd}{3}) + scan.loops(1).procfn(end).dim = a.procfn_ops{opInd}{3}; + end + if numel(a.procfn_ops{opInd}) >= 5 + scan.loops(1).procfn(end).identifier = a.procfn_ops{opInd}{5}; + end +end + +% Configure display +scan.figure = a.fig_id; +if ~isempty(a.fig_position) + scan.figpos = a.fig_position; +end +scan.disp = []; +for l = 1:length(a.disp_ops) + for d = a.disp_dim(l) + scan.disp(end+1).loop = 1; + scan.disp(end).channel = nGetChan + a.disp_ops(l); + scan.disp(end).dim = d; + + if a.disp_ops(l) <= nOperations + opInd = a.disp_ops(l); + else + opInd = a.procfn_ops{a.disp_ops(l)-nOperations}{4}; + end + + % added new condition "numel(opInd) == 1" to check for several + % inchans, later they should get a proper title (marcel) + if numel(opInd) == 1 && opInd <= numel(a.operations) + scan.disp(end).title = prepare_title(sprintf(['%s: '], a.operations{opInd}{:})); + elseif numel(opInd) == 1 && length(a.procfn_ops{opInd - nOperations}) > 4 + scan.disp(end).title = prepare_title(sprintf(['%s: '], a.procfn_ops{opInd - nOperations}{5})); + else + scan.disp(end).title = ''; + end + end +end + +if a.saveloop > 0 + scan.saveloop = [1, a.saveloop]; +end + +end + + + +function str = prepare_title(str) + +str = strrep(str, '_', ' '); +str = str(1:end-2); + +str = strrep(str, 'RepAverage', 'RSA'); +str = strrep(str, 'Downsample', 'DS'); +str = strrep(str, 'Qubit', 'Q'); +str = strrep(str, 'Aux 1 Aux 2 Meas 1 Mask 1', 'SET Left'); +str = strrep(str, 'Aux 1 Aux 2 Meas 1 Mask 2', 'SET Right'); +end diff --git a/MATLAB/+qc/daq_operations.m b/MATLAB/+qc/daq_operations.m index cb1bc51b5..d595eb901 100644 --- a/MATLAB/+qc/daq_operations.m +++ b/MATLAB/+qc/daq_operations.m @@ -20,6 +20,8 @@ % --- add --------------------------------------------------------------- if strcmp(ctrl, 'add') % output is operations % Call before qc.awg_program('arm')! + + smdata.inst(instIndex).data.virtual_channel = struct( ... 'operations', {a.operations} ... @@ -57,10 +59,10 @@ elseif strcmp(ctrl, 'get length') % output is length % Operations need to have been added beforehand mask_maker = py.getattr(daq, '_make_mask'); - masks = util.py.py2mat(py.getattr(daq, '_registered_programs')); - masks = util.py.py2mat(masks.(a.program_name)); - operations = masks.operations; - masks = util.py.py2mat(masks.masks(mask_maker)); + programs = util.py.py2mat(py.getattr(daq, '_registered_programs')); + program = util.py.py2mat(programs.(a.program_name)); + operations = program.operations; + masks = util.py.py2mat(program.masks(mask_maker)); maskIdsFromOperations = cellfun(@(x)(char(x.maskID)), util.py.py2mat(operations), 'UniformOutput', false); @@ -81,6 +83,13 @@ error('daq_operations assumes that all masks should have the same length if using ComputeRepAverageDefinition.'); end output(k) = n(1); + elseif isa(operations{k}, 'py.atsaverage._atsaverage_release.ComputeChunkedAverageDefinition') + chunk_size = operations{k}.chunkSize; + window_lengths = double(py.numpy.max(py.numpy.asarray(masks{maskIndex}.length, py.numpy.dtype('u8')))); + max_chunks_per_window = ceil(window_lengths / chunk_size); + n_windows = size(masks{maskIndex}.length); + + output(k) = max_chunks_per_window * n_windows; else error('Operation ''%s'' not yet implemented', class(operations{k})); end diff --git a/MATLAB/+qc/instantiate_pulse.m b/MATLAB/+qc/instantiate_pulse.m index 9e91c397c..94f4505be 100644 --- a/MATLAB/+qc/instantiate_pulse.m +++ b/MATLAB/+qc/instantiate_pulse.m @@ -1,44 +1,48 @@ function instantiated_pulse = instantiate_pulse(pulse, varargin) - % Plug in parameters - - if qc.is_instantiated_pulse(pulse) - instantiated_pulse = pulse; - - else - default_args = struct(... - 'parameters', py.None, ... - 'channel_mapping', py.None, ... - 'window_mapping' , py.None, ... - 'global_transformation', [], ... - 'to_single_waveform', py.set() ... - ); - - args = util.parse_varargin(varargin, default_args); - - args.channel_mapping = replace_empty_with_pynone(args.channel_mapping); - args.window_mapping = replace_empty_with_pynone(args.window_mapping); - args.global_transformation = qc.to_transformation(args.global_transformation); - - kwargs = pyargs( ... - 'parameters' , args.parameters, ... - 'channel_mapping', args.channel_mapping, ... - 'measurement_mapping' , args.window_mapping, ... - 'global_transformation', args.global_transformation, ... - 'to_single_waveform', args.to_single_waveform ... - ); - - instantiated_pulse = util.py.call_with_interrupt_check(py.getattr(pulse, 'create_program'), kwargs); - end -end +% Plug in parameters +if qc.is_instantiated_pulse(pulse) + instantiated_pulse = pulse; + +else + default_args = struct(... + 'parameters', py.None, ... + 'channel_mapping', py.None, ... + 'window_mapping' , py.None, ... + 'global_transformation', [], ... + 'to_single_waveform', py.set(),... + 'volatile', py.set()... + ); + + args = util.parse_varargin(varargin, default_args); + + args.channel_mapping = replace_empty_with_pynone(args.channel_mapping); + args.window_mapping = replace_empty_with_pynone(args.window_mapping); + args.global_transformation = qc.to_transformation(args.global_transformation); + + kwargs = pyargs( ... + 'parameters' , args.parameters, ... + 'channel_mapping', args.channel_mapping, ... + 'measurement_mapping' , args.window_mapping, ... + 'global_transformation', args.global_transformation, ... + 'volatile', args.volatile,... + 'to_single_waveform', args.to_single_waveform ... + ); + + % Matlab crashes with this right now (12/20) 2020a, py37 TH + % instantiated_pulse = util.py.call_with_interrupt_check(py.getattr(pulse, 'create_program'), kwargs); + instantiated_pulse = pulse.create_program(kwargs); + +end +end function mappingStruct = replace_empty_with_pynone(mappingStruct) - - for fn = fieldnames(mappingStruct)' - if isempty(mappingStruct.(fn{1})) - mappingStruct.(fn{1}) = py.None; - end - end - + +for fn = fieldnames(mappingStruct)' + if isempty(mappingStruct.(fn{1})) + mappingStruct.(fn{1}) = py.None; + end +end + end diff --git a/MATLAB/+qc/load_pulse.m b/MATLAB/+qc/load_pulse.m index 4c651bb92..e4c009258 100644 --- a/MATLAB/+qc/load_pulse.m +++ b/MATLAB/+qc/load_pulse.m @@ -1,4 +1,6 @@ function pulse = load_pulse(pulse_name) global plsdata + % todo: add a method to sync with filesystem + plsdata.qc.pulse_storage.clear() pulse = plsdata.qc.pulse_storage{pulse_name}; \ No newline at end of file diff --git a/MATLAB/+qc/operations_to_python.m b/MATLAB/+qc/operations_to_python.m index 5790f0b5d..a1e7f25e9 100644 --- a/MATLAB/+qc/operations_to_python.m +++ b/MATLAB/+qc/operations_to_python.m @@ -21,6 +21,10 @@ pyOp = py.atsaverage.operations.RepAverage(args{:}); case 'RepeatedDownsample' pyOp = py.atsaverage.operations.RepeatedDownsample(args{:}); + case 'ChunkedAverage' + assert(numel(args) == 3); + args{3} = py.int(args{3}); + pyOp = py.atsaverage.operations.ChunkedAverage(args{:}); otherwise error('Operation %s not recognized', operations{k}{1}); end diff --git a/MATLAB/+qc/plot_pulse_4chan.m b/MATLAB/+qc/plot_pulse_4chan.m index 61680fb99..d83566813 100644 --- a/MATLAB/+qc/plot_pulse_4chan.m +++ b/MATLAB/+qc/plot_pulse_4chan.m @@ -5,76 +5,76 @@ % (c) 2018/06 Pascal Cerfontaine (cerfontaine@physik.rwth-aachen.de) defaultArgs = struct(... - 'charge_diagram_data_structs', {{}}, ... Should contain 2 structs in a cell array with fields x, y - ... and data, where data{1} contains the charge diagram data - 'plot_charge_diagram', true, ... - 'lead_points_cell', {{}}, ... Should contain a cell with a lead_points entry for each qubit - 'special_points_cell', {{}}, ... Should contain a cell with a special_points entry for each qubit - 'channels', {{'W', 'X', 'Y', 'Z'}}, ... - 'measurements', {{'A', 'A', 'B', 'B'}}, ... - 'markerChannels', {{'M1', '', 'M2', ''}} ... - ); + 'charge_diagram_data_structs', {{}}, ... Should contain 2 structs in a cell array with fields x, y + ... and data, where data{1} contains the charge diagram data + 'plot_charge_diagram', true, ... + 'lead_points_cell', {{}}, ... Should contain a cell with a lead_points entry for each qubit + 'special_points_cell', {{}}, ... Should contain a cell with a special_points entry for each qubit + 'channels', {{'W', 'X', 'Y', 'Z'}}, ... + 'measurements', {{'A', 'A', 'B', 'B'}}, ... + 'markerChannels', {{'M1', '', 'M2', ''}} ... + ); args = util.parse_varargin(varargin, defaultArgs); - - - for chrgInd = 1:2 - k = chrgInd + double(chrgInd==2); - q = 4 - k; - - if numel(args.charge_diagram_data_structs) >= chrgInd - args.charge_diagram_data = args.charge_diagram_data_structs{chrgInd}; - args.charge_diagram_data = {args.charge_diagram_data.x, args.charge_diagram_data.y, args.charge_diagram_data.data{1}}; - else - args.charge_diagram_data = {}; - end - - if numel(args.lead_points_cell) >= chrgInd - args.lead_points = args.lead_points_cell{chrgInd}; - else - args.lead_points = {}; - end - - if numel(args.special_points_cell) >= chrgInd - args.special_points = args.special_points_cell{chrgInd}; - else - args.special_points = {}; - end - - args.charge_diagram = args.channels(k:k+1); - if args.plot_charge_diagram - args.subplots = [220+k 220+k+1]; - else - args.subplots = [210+chrgInd]; - end - args.clear_fig = k==1; - [t, channels, measurements, instantiatedPulse] = qc.plot_pulse(pulse, args); - xlabel(args.channels(k)); - ylabel(args.channels(k+1)); - - if args.plot_charge_diagram - subplot(args.subplots(1)); - end - set(findall(gca, 'DisplayName', sprintf('Chan: %s', args.channels{q})), 'Visible', 'off'); - set(findall(gca, 'DisplayName', sprintf('Chan: %s', args.channels{q+1})), 'Visible', 'off'); - set(findall(gca, 'DisplayName', sprintf('Chan: %s', args.markerChannels{q})), 'Visible', 'off'); - set(findall(gca, 'DisplayName', sprintf('Chan: %s', args.markerChannels{q+1})), 'Visible', 'off'); - set(findall(gca, 'DisplayName', sprintf('Meas: %s', args.measurements{q})), 'Visible', 'off'); - set(findall(gca, 'DisplayName', sprintf('Meas: %s', args.measurements{q+1})), 'Visible', 'off'); - - [hLeg, hObj] = legend(gca); - for l = 1:numel(hLeg.String) - if strcmp(hLeg.String{l}, sprintf('Chan: %s', args.channels{q})) || ... - strcmp(hLeg.String{l}, sprintf('Chan: %s', args.channels{q+1})) || ... - strcmp(hLeg.String{l}, sprintf('Chan: %s', args.markerChannels{q})) || ... - strcmp(hLeg.String{l}, sprintf('Chan: %s', args.markerChannels{q+1})) || ... - strcmp(hLeg.String{l}, sprintf('Meas: %s', args.measurements{q})) || ... - strcmp(hLeg.String{l}, sprintf('Meas: %s', args.measurements{q+1})) - hLeg.String{l} = ''; - end - findobj(hObj, 'type', 'line'); - set(hObj, 'lineWidth', 2); - end - - - + + +for chrgInd = 1:2 + k = chrgInd + double(chrgInd==2); + q = 4 - k; + + if numel(args.charge_diagram_data_structs) >= chrgInd + args.charge_diagram_data = args.charge_diagram_data_structs{chrgInd}; + args.charge_diagram_data = {args.charge_diagram_data.x, args.charge_diagram_data.y, args.charge_diagram_data.data{1}}; + else + args.charge_diagram_data = {}; + end + + if numel(args.lead_points_cell) >= chrgInd + args.lead_points = args.lead_points_cell{chrgInd}; + else + args.lead_points = {}; + end + + if numel(args.special_points_cell) >= chrgInd + args.special_points = args.special_points_cell{chrgInd}; + else + args.special_points = {}; + end + + args.charge_diagram = args.channels(k:k+1); + if args.plot_charge_diagram + args.subplots = [220+k 220+k+1]; + else + args.subplots = [210+chrgInd]; + end + args.clear_fig = k==1; + [t, channels, measurements, instantiatedPulse] = qc.plot_pulse(pulse, args); + xlabel(args.channels(k)); + ylabel(args.channels(k+1)); + + if args.plot_charge_diagram + subplot(args.subplots(1)); + end + set(findall(gca, 'DisplayName', sprintf('Chan: %s', args.channels{q})), 'Visible', 'off'); + set(findall(gca, 'DisplayName', sprintf('Chan: %s', args.channels{q+1})), 'Visible', 'off'); + set(findall(gca, 'DisplayName', sprintf('Chan: %s', args.markerChannels{q})), 'Visible', 'off'); + set(findall(gca, 'DisplayName', sprintf('Chan: %s', args.markerChannels{q+1})), 'Visible', 'off'); + set(findall(gca, 'DisplayName', sprintf('Meas: %s', args.measurements{q})), 'Visible', 'off'); + set(findall(gca, 'DisplayName', sprintf('Meas: %s', args.measurements{q+1})), 'Visible', 'off'); + + [hLeg, hObj] = legend(gca); + for l = 1:numel(hLeg.String) + if strcmp(hLeg.String{l}, sprintf('Chan: %s', args.channels{q})) || ... + strcmp(hLeg.String{l}, sprintf('Chan: %s', args.channels{q+1})) || ... + strcmp(hLeg.String{l}, sprintf('Chan: %s', args.markerChannels{q})) || ... + strcmp(hLeg.String{l}, sprintf('Chan: %s', args.markerChannels{q+1})) || ... + strcmp(hLeg.String{l}, sprintf('Meas: %s', args.measurements{q})) || ... + strcmp(hLeg.String{l}, sprintf('Meas: %s', args.measurements{q+1})) + hLeg.String{l} = ''; + end + findobj(hObj, 'type', 'line'); + set(hObj, 'lineWidth', 2); + end + + + end \ No newline at end of file diff --git a/MATLAB/+qc/pulse_to_struct.m b/MATLAB/+qc/pulse_to_struct.m index 6d952fd61..9711027a6 100644 --- a/MATLAB/+qc/pulse_to_struct.m +++ b/MATLAB/+qc/pulse_to_struct.m @@ -1,9 +1,11 @@ function pulseStruct = pulse_to_struct(pulseTemplate) backend = py.qctoolkit.serialization.DictBackend(); - serializer = py.qctoolkit.serialization.Serializer(backend); - - serializer.serialize(pulseTemplate); + storage = py.qctoolkit.serialization.PulseStorage(backend); + + % THIS IS WRONG! + storage.overwrite('main',pulseTemplate) + pulseStruct = util.py.py2mat(backend.storage); if ~isfield(pulseStruct, 'main') diff --git a/MATLAB/+qc/resolve_mappings.m b/MATLAB/+qc/resolve_mappings.m new file mode 100644 index 000000000..9cf6c22e0 --- /dev/null +++ b/MATLAB/+qc/resolve_mappings.m @@ -0,0 +1,52 @@ +function resolved = resolve_mappings(varargin) +% Takes a variable number of parameter mappings (structs) and resolves them +% sequentially. Eg. if the first maps A to B and the second B to C, returns +% a mapping A to C. Goes left-to-right (first to last). +% +% >> qc.resolve_mappings(struct('a', 'b'), struct('b', 'c', 'x', 'y'), ... +% struct('c', 'd'), struct('x', 'z', 'asdf', 'jkl'), ... +% struct('z', 'a', 'bla', 'foo')) +% +% Warning: Field clash. Value to the right supercedes. +% > In qc.resolve_mappings (line 14) +% +% ans = +% +% struct with fields: +% +% a: 'd' +% x: 'a' +% asdf: 'jkl' +% bla: 'foo' +% ========================================================================= +resolved = varargin{1}; +varargin = varargin(2:end); + +while ~isempty(varargin) + visited_fields = {}; + for f = fieldnames(resolved)' + field = f{1}; + value = resolved.(field); + if isfield(varargin{1}, field) + warning('Field clash. Value to the right supercedes.') + resolved.(field) = varargin{1}.(field); + visited_fields = [visited_fields field]; + elseif isfield(varargin{1}, value) + resolved.(field) = varargin{1}.(value); + visited_fields = [visited_fields value]; + end + end + + % Add unchanged new fields from varargin{1} + for f = fieldnames(varargin{1})' + field = f{1}; + value = varargin{1}.(field); + if ~ismember(visited_fields, field) + resolved.(field) = value; + end + end + + varargin = varargin(2:end); +end + +end \ No newline at end of file diff --git a/MATLAB/+qc/setup_alazar_measurements.m b/MATLAB/+qc/setup_alazar_measurements.m index 1642a72b9..1b18e0c91 100644 --- a/MATLAB/+qc/setup_alazar_measurements.m +++ b/MATLAB/+qc/setup_alazar_measurements.m @@ -47,8 +47,8 @@ nQubits = args.nQubits; nMeasPerQubit = args.nMeasPerQubit; - py.setattr(hws, '_measurement_map', py.dict); - py.setattr(daq, '_mask_prototypes', py.dict); + py.getattr(hws, '_measurement_map').clear(); + py.getattr(daq, '_mask_prototypes').clear(); warning('Removing measurement_map and measurement_map might break stuff if previously set. Needs testing.'); for q = 1:nQubits @@ -81,12 +81,12 @@ % Q1 A1 qubitIndex, measIndex, hwChannel, auxFlag1, secondQubitIndex, secondHwChannel, auxFlag2 add_meas_and_mask(1, m, 2, false, 1, 0 , true); - % Q1 A2 qubitIndex, measIndex, hwChannel, auxFlag1, secondQubitIndex, secondHwChannel, auxFlag2 + % Q1 A2 qubitIndex, measIndex, hwChannel, auxFlag1, secondQubitIndex, secondHwChannel, auxFlag2 add_meas_and_mask(1, m, 2, false, 2, 1 , true); % Q2 A1 qubitIndex, measIndex, hwChannel, auxFlag1, secondQubitIndex, secondHwChannel, auxFlag2 add_meas_and_mask(2, m, 3, false, 1, 0 , true); - % Q2 A2 qubitIndex, measIndex, hwChannel, auxFlag1, secondQubitIndex, secondHwChannel, auxFlag2 + % Q2 A2 qubitIndex, measIndex, hwChannel, auxFlag1, secondQubitIndex, secondHwChannel, auxFlag2 add_meas_and_mask(2, m, 3, false, 2, 1 , true); end end diff --git a/MATLAB/+qc/struct_to_pulse.m b/MATLAB/+qc/struct_to_pulse.m index 9214f766a..aac737f56 100644 --- a/MATLAB/+qc/struct_to_pulse.m +++ b/MATLAB/+qc/struct_to_pulse.m @@ -1,15 +1,18 @@ function pulseTemplate = struct_to_pulse(pulseStruct) - + backend = py.qctoolkit.serialization.DictBackend(); - serializer = py.qctoolkit.serialization.Serializer(backend); + pulse_storage = py.qctoolkit.serialization.PulseStorage(backend); + % THIS IS WRONG!!! if startsWith(pulseStruct.main, '{') pulseName = 'main'; else pulseName = pulseStruct.main; - end + end + % feed into backend backend.storage.update(pulseStruct) - pulseTemplate = serializer.deserialize(pulseName); + + pulseTemplate = pulse_storage.get(pulseName); % plsStruct = util.py.py2mat(backend.storage) \ No newline at end of file diff --git a/alazar.patch b/alazar.patch new file mode 100644 index 000000000..178297bf9 --- /dev/null +++ b/alazar.patch @@ -0,0 +1,36 @@ +diff --cc qupulse/hardware/dacs/alazar.py +index abba0a3,9f4f984..0000000 +--- a/qupulse/hardware/dacs/alazar.py ++++ b/qupulse/hardware/dacs/alazar.py +@@@ -1,9 -1,5 +1,9 @@@ +- from typing import Dict, Any, Optional, Tuple, Sequence + -from typing import Dict, Any, Optional, Tuple, List, Iterable, Callable +++from typing import Dict, Any, Optional, Tuple, List, Iterable, Callable, Sequence + from collections import defaultdict + +import logging + +import math + +import functools + +import abc + + import numpy as np + +@@@ -222,12 -157,16 +278,11 @@@ class AlazarCard(DAC) + elif config.totalRecordSize < total_record_size: + raise ValueError('specified total record size is smaller than needed {} < {}'.format(config.totalRecordSize, + total_record_size)) + - + old_aimed_buffer_size = config.aimedBufferSize + - + - # work around for measurments not working with one buffer + - if config.totalRecordSize < 5*config.aimedBufferSize: + - config.aimedBufferSize = config.totalRecordSize // 5 + +- config.aimedBufferSize, config.totalRecordSize = self.buffer_strategy.calculate_acquisition_properties(config.masks, self.__card.minimum_record_size) +- config.apply(self.__card, True) ++ self.__card.applyConfiguration(config, True) + + - # "Hide" work around from the user + + # Keep user value + config.aimedBufferSize = old_aimed_buffer_size + + self.update_settings = False diff --git a/qupulse/_program/_loop.py b/qupulse/_program/_loop.py index 92d3f7eb2..06513beec 100644 --- a/qupulse/_program/_loop.py +++ b/qupulse/_program/_loop.py @@ -208,10 +208,6 @@ def _get_repr(self, first_prefix, other_prefixes) -> Generator[str, None, None]: yield from cast(Loop, elem)._get_repr(other_prefixes + ' ->', other_prefixes + ' ') def __repr__(self) -> str: - is_circular = is_tree_circular(self) - if is_circular: - return '{}: Circ {}'.format(id(self), is_circular) - str_len = 0 repr_list = [] for sub_repr in self._get_repr('', ''): diff --git a/qupulse/_program/seqc.py b/qupulse/_program/seqc.py index 50dd29a7c..2924268b4 100644 --- a/qupulse/_program/seqc.py +++ b/qupulse/_program/seqc.py @@ -106,7 +106,7 @@ def markers_ch1(self): @property def markers_ch2(self): - return np.bitwise_and(self.marker_data, 0b1100) + return np.right_shift(np.bitwise_and(self.marker_data, 0b1100), 2) @classmethod def from_sampled(cls, ch1: Optional[np.ndarray], ch2: Optional[np.ndarray], diff --git a/qupulse/expressions.py b/qupulse/expressions.py index bbfad3eed..a5ef6b79b 100644 --- a/qupulse/expressions.py +++ b/qupulse/expressions.py @@ -109,7 +109,12 @@ def evaluate_in_scope(self, scope: Mapping) -> Union[Number, numpy.ndarray]: Returns: """ - raise NotImplementedError("") + parsed_kwargs = self._parse_evaluate_numeric_arguments(scope) + + result, self._expression_lambda = evaluate_lambdified(self.underlying_expression, self.variables, + parsed_kwargs, lambdified=self._expression_lambda) + + return self._parse_evaluate_numeric_result(result, scope) def evaluate_numeric(self, **kwargs) -> Union[Number, numpy.ndarray]: return self.evaluate_in_scope(kwargs) diff --git a/qupulse/hardware/awgs/zihdawg.py b/qupulse/hardware/awgs/zihdawg.py index 6d6972a90..b811814c4 100644 --- a/qupulse/hardware/awgs/zihdawg.py +++ b/qupulse/hardware/awgs/zihdawg.py @@ -10,7 +10,7 @@ import hashlib import argparse import re -from abc import abstractmethod +from abc import abstractmethod, ABC try: # zhinst fires a DeprecationWarning from its own code in some versions... @@ -59,6 +59,7 @@ def _amplitude_scales(api_session, serial: str): for ch in range(8) ) + def _sigout_double(api_session, prop: str, serial: str, channel: int, value: float = None) -> float: """Query channel offset voltage and optionally set it.""" node_path = f'/{serial}/sigouts/{channel-1:d}/{prop}' @@ -67,12 +68,15 @@ def _sigout_double(api_session, prop: str, serial: str, channel: int, value: flo api_session.sync() # Global sync: Ensure settings have taken effect on the device. return api_session.getDouble(node_path) + def _sigout_range(api_session, serial: str, channel: int, voltage: float = None) -> float: return _sigout_double(api_session, 'range', serial, channel, voltage) + def _sigout_offset(api_session, serial: str, channel: int, voltage: float = None) -> float: return _sigout_double(api_session, 'offset', serial, channel, voltage) + def _sigout_on(api_session, serial: str, channel: int, value: bool = None) -> bool: """Query channel signal output status (enabled/disabled) and optionally set it. Corresponds to front LED.""" node_path = f'/{serial}/sigouts/{channel-1:d}/on' @@ -292,7 +296,7 @@ def _is_mds_master(self) -> Optional[bool]: idx = 0 while True: try: - devices = self.api_session.getString(f'/ZI/MDS/GROUPS/{idx}/DEVICES').split(',') + devices = self.api_session.getString(f'/ZI/MDS/GROUPS/{idx}/DEVICES').split(',') except RuntimeError: break @@ -394,7 +398,7 @@ def _initialize_awg_module(self): self._awg_module.set('awgModule/device', self.master_device.serial) self._awg_module.set('awgModule/index', self.awg_group_index) self._awg_module.execute() - self._elf_manager = ELFManager(self._awg_module) + self._elf_manager = ELFManager.DEFAULT_CLS(self._awg_module) self._upload_generator = () @property @@ -501,6 +505,7 @@ def upload(self, name: str, def _start_compile_and_upload(self): self._uploaded_seqc_source = None self._upload_generator = self._elf_manager.compile_and_upload(self._required_seqc_source) + logger.debug(f"_start_compile_and_upload: %r", next(self._upload_generator, "Finished")) def _wait_for_compile_and_upload(self): for state in self._upload_generator: @@ -551,15 +556,18 @@ def arm(self, name: Optional[str]) -> None: Currently hardware triggering is not implemented. The HDAWGProgramManager needs to emit code that calls `waitDigTrigger` to do that. """ - if self.num_channels > 8: + if self.master_device._is_mds_master(): + # This is required in MDS mode because program select does not work properly yet so we use single program + # mode if name is None: - self._required_seqc_source = "" + self._required_seqc_source = "sync();" else: self._required_seqc_source = self._program_manager.to_seqc_program(name) self._start_compile_and_upload() if self._required_seqc_source != self._uploaded_seqc_source: self._wait_for_compile_and_upload() + assert self._required_seqc_source == self._uploaded_seqc_source self.user_register(self._program_manager.Constants.TRIGGER_REGISTER, 0) @@ -581,10 +589,8 @@ def arm(self, name: Optional[str]) -> None: self.user_register(self._program_manager.Constants.PROG_SEL_REGISTER, self._program_manager.name_to_index(name) | int(self._program_manager.Constants.NO_RESET_MASK, 2)) - # this was a workaround for problems in the past and I totally forgot why it was here - # for ch_pair in self.master.channel_tuples: - # ch_pair._wait_for_compile_and_upload() - self.enable(True) + if name is not None: + self.enable(True) def run_current_program(self) -> None: """Run armed program.""" @@ -802,7 +808,9 @@ def offsets(self) -> Tuple[float, ...]: return tuple(map(self.master_device.offset, self._channels())) -class ELFManager: +class ELFManager(ABC): + DEFAULT_CLS = None + class AWGModule: def __init__(self, awg_module: zhinst_core.AwgModule): """Provide an easily mockable interface to the zhinst AwgModule object""" @@ -838,6 +846,14 @@ def compiler_source_file(self) -> str: def compiler_source_file(self, source_file: str): self._module.set('compiler/sourcefile', source_file) + @property + def compiler_source_string(self) -> str: + return self._module.getString('compiler/sourcestring') + + @compiler_source_string.setter + def compiler_source_string(self, source_string: str): + self._module.set('compiler/sourcestring', source_string) + @property def compiler_upload(self) -> bool: """auto upload after compiling""" @@ -912,6 +928,87 @@ def _source_hash(source_string: str) -> str: # use utf-16 because str is UTF16 on most relevant machines (Windows) return hashlib.sha512(bytes(source_string, 'utf-16')).hexdigest() + @abstractmethod + def compile_and_upload(self, source_string: str) -> Generator[str, str, None]: + """The function returns a generator that yields the current state of the progress. The generator is empty iff + the upload is complete. An exception is raised if there is an error. + + To abort send 'abort' to the generator. (not implemented :P) + + Example: + >>> my_source = 'playWave("my_wave");' + >>> for state in elf_manager.compile_and_upload(my_source): + ... print('Current state:', state) + ... time.sleep(1) + + Args: + source_string: Source code to compile + + Returns: + Generator object that needs to be consumed + """ + + +class SimpleELFManager(ELFManager): + def __init__(self, awg_module: zhinst.ziPython.AwgModule): + """This class organizes compiling and uploading of compiled programs. The source code file is named based on the + code hash to cache compilation results. This requires that the waveform names are unique. + + The compilation and upload itself are done asynchronously by zhinst.ziPython. To avoid spawning a useless + thread for updating the status the method :py:meth:`~ELFManager.compile_and_upload` returns a generator which + talks to the undelying library when needed.""" + super().__init__(awg_module) + + def compile_and_upload(self, source_string: str) -> Generator[str, str, None]: + self.awg_module.compiler_upload = True + self.awg_module.compiler_source_string = source_string + + while True: + status, msg = self.awg_module.compiler_status + if status == - 1: + yield 'compiling' + elif status == 0: + break + elif status == 1: + raise HDAWGCompilationException(msg) + elif status == 2: + logger.warning("Compiler warings: %s", msg) + break + else: + raise RuntimeError("Unexpected status", status, msg) + + while True: + status_int, progress = self.awg_module.elf_status + if progress == 1.0: + break + elif status_int == 1: + HDAWGUploadException(self.awg_module.compiler_status) + else: + yield 'uploading @ %d%%' % (100*progress) + + +ELFManager.DEFAULT_CLS = SimpleELFManager + + +class CachingELFManager(ELFManager): + def __init__(self, awg_module: zhinst.ziPython.AwgModule): + """FAILS TO UPLOAD THE CORRECT ELF FOR SOME REASON + TODO: Investigat + + This class organizes compiling and uploading of compiled programs. The source code file is named based on the + code hash to cache compilation results. This requires that the waveform names are unique. + + The compilation and upload itself are done asynchronously by zhinst.ziPython. To avoid spawning a useless + thread for updating the status the method :py:meth:`~ELFManager.compile_and_upload` returns a generator which + talks to the undelying library when needed.""" + super().__init__(awg_module) + + # automatically upload after successful compilation + self.awg_module.compiler_upload = True + + self._compile_job = None # type: Optional[Union[str, Tuple[str, int, str]]] + self._upload_job = None # type: Optional[Union[Tuple[str, float], Tuple[str, int]]] + def _update_compile_job_status(self): """Store current compile status in self._compile_job.""" compiler_start = self.awg_module.compiler_start @@ -920,8 +1017,7 @@ def _update_compile_job_status(self): elif isinstance(self._compile_job, str): if compiler_start: - # compilation is running - pass + logger.debug("Compiler is running.") else: compiler_status, status_string = self.awg_module.compiler_status diff --git a/qupulse/hardware/dacs/alazar.py b/qupulse/hardware/dacs/alazar.py index 7398e2115..8588dc26e 100644 --- a/qupulse/hardware/dacs/alazar.py +++ b/qupulse/hardware/dacs/alazar.py @@ -1,5 +1,4 @@ -import dataclasses -from typing import Dict, Any, Optional, Tuple, List, Iterable, Callable, Sequence +from typing import Dict, Any, Optional, Tuple, List, Iterable, Callable, Sequence, Mapping, FrozenSet from collections import defaultdict import copy import warnings @@ -7,6 +6,7 @@ import functools import abc import logging +import types import numpy as np @@ -16,50 +16,23 @@ from qupulse.utils.types import TimeType from qupulse.hardware.dacs.dac_base import DAC from qupulse.hardware.util import traced -from qupulse.utils.performance import time_windows_to_samples - -logger = logging.getLogger(__name__) -def _windows_to_samples(begins: np.ndarray, lengths: np.ndarray, - sample_rate: TimeType) -> Tuple[np.ndarray, np.ndarray]: - return time_windows_to_samples(begins, lengths, float(sample_rate)) +logger = logging.getLogger(__name__) -@dataclasses.dataclass -class AcquisitionProgram: - _sample_rate: Optional[TimeType] = dataclasses.field(default=None) - _masks: dict = dataclasses.field(default_factory=dict) +class AlazarProgram: + def __init__(self): + self._sample_factor = None + self._masks = {} + self.operations = [] + self._total_length = None + self._auto_rearm_count = 1 + self._buffer_strategy = None @property - def sample_rate(self) -> Optional[TimeType]: - return self._sample_rate - - def set_measurement_mask(self, mask_name: str, sample_rate: TimeType, - begins: np.ndarray, lengths: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: - """Raise error if sample factor has changed""" - if self._sample_rate is None: - self._sample_rate = sample_rate - elif sample_rate != self.sample_rate: - raise RuntimeError('class AcquisitionProgram has already masks with differing sample rate.') - - assert begins.dtype == float and lengths.dtype == float - - begins, lengths = self._masks[mask_name] = _windows_to_samples(begins, lengths, sample_rate) - - return begins, lengths - - def clear_masks(self): - self._masks.clear() - self._sample_rate = None - - -@dataclasses.dataclass -class AlazarProgram(AcquisitionProgram): - operations: Sequence = dataclasses.field(default_factory=list) - _total_length: Optional[int] = dataclasses.field(default=None) - _auto_rearm_count: int = dataclasses.field(default=1) - buffer_strategy: Optional = dataclasses.field(default=None) + def mask_names(self) -> FrozenSet[str]: + return frozenset(self._masks.keys()) def masks(self, mask_maker: Callable[[str, np.ndarray, np.ndarray], Mask]) -> List[Mask]: return [mask_maker(mask_name, *data) for mask_name, data in self._masks.items()] @@ -94,10 +67,78 @@ def auto_rearm_count(self, value: int): raise ValueError("Trigger count has to be in the interval [0, 2**64-1]") self._auto_rearm_count = trigger_count + def clear_masks(self): + self._masks.clear() + + @property + def sample_factor(self) -> Optional[TimeType]: + return self._sample_factor + + def _mask_to_samples(self, begins, lengths) -> Tuple[np.ndarray, np.ndarray]: + assert self._sample_factor is not None + assert begins.dtype == np.float and lengths.dtype == np.float + + sample_factor = self._sample_factor + + begins = np.rint(begins * float(sample_factor)).astype(dtype=np.uint64) + lengths = np.floor_divide(lengths * float(sample_factor.numerator), float(sample_factor.denominator)).astype( + dtype=np.uint64) + + sorting_indices = np.argsort(begins) + begins = begins[sorting_indices] + lengths = lengths[sorting_indices] + + begins.flags.writeable = False + lengths.flags.writeable = False + return begins, lengths + + def set_measurement_mask(self, mask_name: str, sample_factor: TimeType, + begins: np.ndarray, lengths: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: + """Raise error if sample factor has changed""" + if self._sample_factor is None: + self._sample_factor = sample_factor + + elif sample_factor != self._sample_factor: + raise RuntimeError('class AlazarProgram has already masks with differing sample factor') + + self._masks[mask_name] = self._mask_to_samples(begins, lengths) + + return begins, lengths + + def update_measurement_masks(self, sample_factor: TimeType, + masks: Mapping[str, Tuple[np.ndarray, np.ndarray]]) -> bool: + """ + + Args: + sample_factor: + masks: + + Returns: + True if the measurement masks changed + """ + self._sample_factor = sample_factor + + update_required = False + old_masks = self._masks.copy() + self._masks.clear() + for mask_name, (begins, lengths) in masks.items(): + begins, lengths = self._mask_to_samples(begins, lengths) + if not update_required: + # check if the masks changed + if not np.array_equal(old_masks.pop(mask_name, None), (begins, lengths)): + update_required = True + self._masks[mask_name] = (begins, lengths) + + if len(old_masks) != 0: + # there are removed masks + update_required = True + + return update_required + def iter(self, mask_maker): yield self.masks(mask_maker) yield self.operations - yield self.total_length + yield self._total_length def gcd_set(data): @@ -186,11 +227,14 @@ def calculate_acquisition_properties(self, buffer_length_divisor: int) -> Tuple[int, int]: gcd = None for mask in masks: + if len(mask.begin) < 2: + continue c_gcd = gcd_set(np.unique(np.diff(mask.begin.as_ndarray()))) if gcd is None: gcd = c_gcd else: gcd = math.gcd(gcd, c_gcd) + gcd = gcd or 1 buffer_size = max((gcd // buffer_length_divisor) * buffer_length_divisor, buffer_length_divisor) mtl = self.minimum_total_length(masks) @@ -283,8 +327,11 @@ def _make_mask(self, mask_id: str, begins, lengths) -> Mask: if mask_type not in ('auto', 'cross_buffer', None): warnings.warn("Currently only CrossBufferMask is implemented.") - if np.any(begins[:-1]+lengths[:-1] > begins[1:]): - raise ValueError('Found overlapping windows in begins') + if np.any(begins[:-1] + lengths[:-1] > begins[1:]): + warnings.warn("Found overlapping measurement windows. Ignore warning to enable automatic shrinking.", + category=WindowOverlapWarning) + begins, lengths = _shrink_overlapping_windows(begins, lengths) + mask = CrossBufferMask() mask.identifier = mask_id @@ -294,22 +341,25 @@ def _make_mask(self, mask_id: str, begins, lengths) -> Mask: return mask def set_measurement_mask(self, program_name, mask_name, begins, lengths) -> Tuple[np.ndarray, np.ndarray]: - sample_rate = TimeType.from_fraction(int(self.default_config.captureClockConfiguration.numeric_sample_rate(self.card.model)), 10**9) - return self._registered_programs[program_name].set_measurement_mask(mask_name, sample_rate, begins, lengths) + sample_factor = TimeType.from_fraction(int(self.default_config.captureClockConfiguration.numeric_sample_rate(self.card.model)), 10**9) + if program_name is self.__armed_program: + self.update_settings = True + return self._registered_programs[program_name].set_measurement_mask(mask_name, sample_factor, begins, lengths) def register_measurement_windows(self, program_name: str, windows: Dict[str, Tuple[np.ndarray, np.ndarray]]) -> None: program = self._registered_programs[program_name] - sample_rate = TimeType.from_fraction(int(self.default_config.captureClockConfiguration.numeric_sample_rate(self.card.model)), + sample_factor = TimeType.from_fraction(int(self.default_config.captureClockConfiguration.numeric_sample_rate(self.card.model)), 10 ** 9) - program.clear_masks() - - for mask_name, (begins, lengths) in windows.items(): - program.set_measurement_mask(mask_name, sample_rate, begins, lengths) + if program.update_measurement_masks(sample_factor, windows) and program is self.__armed_program: + self.update_settings = True def register_operations(self, program_name: str, operations) -> None: - self._registered_programs[program_name].operations = operations + program = self._registered_programs[program_name] + if program is self.__armed_program and program.operations != operations: + self.update_settings = True + program.operations = operations def arm_program(self, program_name: str) -> None: logger.debug("Arming program %s on %r", program_name, self.__card) @@ -323,10 +373,10 @@ def arm_program(self, program_name: str) -> None: config.masks, config.operations, total_record_size = self._registered_programs[program_name].iter( self._make_mask) - sample_rate_in_hz = config.captureClockConfiguration.numeric_sample_rate(self.card.model) + sample_rate = config.captureClockConfiguration.numeric_sample_rate(self.card.model) # sample rate in GHz - sample_rate = TimeType.from_fraction(sample_rate_in_hz, 10 ** 9) + sample_factor = TimeType.from_fraction(sample_rate, 10 ** 9) if not config.operations: raise RuntimeError("No operations: Arming program without operations is an error as there will " @@ -335,21 +385,26 @@ def arm_program(self, program_name: str) -> None: elif not config.masks: raise RuntimeError("No masks although there are operations in program: %r" % program_name) - elif self._registered_programs[program_name].sample_rate != sample_rate: + elif self._registered_programs[program_name].sample_factor != sample_factor: raise RuntimeError("Masks were registered with a different sample rate {}!={}".format( - self._registered_programs[program_name].sample_rate, sample_rate)) + self._registered_programs[program_name].sample_factor, sample_factor)) + + if total_record_size is None: + buffer_size, total_record_size = self.buffer_strategy.calculate_acquisition_properties( + config.masks, + self.record_size_factor) + config.aimedBufferSize = buffer_size + + else: + warnings.warn("Hardcoded record size for program", DeprecationWarning) assert total_record_size > 0 - # extend the total record size to be a multiple of record_size_factor - record_size_factor = self.record_size_factor - total_record_size = (((total_record_size - 1) // record_size_factor) + 1) * record_size_factor + if config.totalRecordSize: + warnings.warn("Total record size of config is ignored", DeprecationWarning) + + config.totalRecordSize = total_record_size - if config.totalRecordSize == 0: - config.totalRecordSize = total_record_size - elif config.totalRecordSize < total_record_size: - raise ValueError('specified total record size is smaller than needed {} < {}'.format(config.totalRecordSize, - total_record_size)) self.__card.applyConfiguration(config, True) self._current_config = config @@ -374,8 +429,8 @@ def clear(self) -> None: self.__armed_program = None @property - def mask_prototypes(self) -> Dict[str, Tuple[int, str]]: - return self._mask_prototypes + def mask_prototypes(self) -> Mapping[str, Tuple[int, str]]: + return types.MappingProxyType(self._mask_prototypes) def register_mask_for_channel(self, mask_id: str, hw_channel: int, mask_type='auto') -> None: """ @@ -389,7 +444,11 @@ def register_mask_for_channel(self, mask_id: str, hw_channel: int, mask_type='au raise ValueError('{} is not a valid hw channel'.format(hw_channel)) if mask_type not in ('auto', 'cross_buffer', None): raise NotImplementedError('Currently only can do cross buffer mask') - self._mask_prototypes[mask_id] = (hw_channel, mask_type) + val = (hw_channel, mask_type) + if self._mask_prototypes.get(mask_id, None) != val: + self._mask_prototypes[mask_id] = val + if self.__armed_program is not None and mask_id in self._registered_programs[self.__armed_program].mask_names: + self.update_settings = True def measure_program(self, channels: Iterable[str]) -> Dict[str, np.ndarray]: """ @@ -420,3 +479,23 @@ def get_input_range(operation_id: str): data[op_name] = scanline_data.operationResults[op_name].getAsVoltage(input_range) return data + + +class WindowOverlapWarning(RuntimeWarning): + pass + + +def _shrink_overlapping_windows(begins, lengths): + ends = begins + lengths + + overlaps = np.zeros_like(ends) + np.maximum(ends[:-1] - begins[1:], 0, out=overlaps[1:]) + + if np.any(overlaps >= lengths): + raise ValueError("Overlap is bigger than measurement window") + + begins = begins + overlaps + lengths = lengths - overlaps + + assert not np.any((begins+lengths)[:-1] > begins[1:]) + return begins, lengths diff --git a/qupulse/pulses/arithmetic_pulse_template.py b/qupulse/pulses/arithmetic_pulse_template.py index 7a2735ee2..ac8dced37 100644 --- a/qupulse/pulses/arithmetic_pulse_template.py +++ b/qupulse/pulses/arithmetic_pulse_template.py @@ -2,7 +2,6 @@ from typing import Any, Dict, List, Set, Optional, Union, Mapping, FrozenSet, cast, Callable from numbers import Real import warnings -import operator import sympy @@ -19,17 +18,15 @@ IdentityTransformation -def _apply_operation_to_channel_dict(lhs: Mapping[ChannelID, Any], - rhs: Mapping[ChannelID, Any], - operator_both: Optional[Callable[[Any, Any], Any]], - rhs_only: Optional[Callable[[Any], Any]] - ) -> Dict[ChannelID, Any]: +def _apply_operation_to_channel_dict(operator: str, + lhs: Mapping[ChannelID, Any], + rhs: Mapping[ChannelID, Any]) -> Dict[ChannelID, Any]: result = dict(lhs) for channel, rhs_value in rhs.items(): if channel in result: - result[channel] = operator_both(result[channel], rhs_value) + result[channel] = ArithmeticWaveform.operator_map[operator](result[channel], rhs_value) else: - result[channel] = rhs_only(rhs_value) + result[channel] = ArithmeticWaveform.rhs_only_map[operator](rhs_value) return result @@ -69,7 +66,7 @@ def __init__(self, "(it may be unequal only for fringe cases)" % (lhs.duration, rhs.duration), category=UnequalDurationWarningInArithmeticPT) - if not silent_atomic and not (lhs._is_atomic() and rhs._is_atomic()): + if not silent_atomic and (not isinstance(lhs, AtomicPulseTemplate) or not isinstance(rhs, AtomicPulseTemplate)): warnings.warn("ArithmeticAtomicPulseTemplate treats all operands as if they are atomic. " "You can silence this warning by passing `silent_atomic=True` or by ignoring this category.", category=ImplicitAtomicityInArithmeticPT) @@ -109,30 +106,14 @@ def duration(self) -> ExpressionScalar: """Duration of the lhs operand if it is larger zero. Else duration of the rhs.""" return ExpressionScalar(sympy.Max(self.lhs.duration, self.rhs.duration)) - def _apply_operation(self, lhs: Mapping[str, Any], rhs: Mapping[str, Any]) -> Dict[str, Any]: - operator_both = ArithmeticWaveform.operator_map[self._arithmetic_operator] - rhs_only = ArithmeticWaveform.rhs_only_map[self._arithmetic_operator] - return _apply_operation_to_channel_dict(lhs, rhs, - operator_both=operator_both, - rhs_only=rhs_only) - @property def integral(self) -> Dict[ChannelID, ExpressionScalar]: - # this is a guard for possible future changes - assert self._arithmetic_operator in ('+', '-'), \ - f"Integral not correctly implemented for '{self._arithmetic_operator}'" - return self._apply_operation(self.lhs.integral, self.rhs.integral) + return _apply_operation_to_channel_dict(self._arithmetic_operator, self.lhs.integral, self.rhs.integral) def _as_expression(self) -> Dict[ChannelID, ExpressionScalar]: - return self._apply_operation(self.lhs._as_expression(), self.rhs._as_expression()) - - @property - def initial_values(self) -> Dict[ChannelID, ExpressionScalar]: - return self._apply_operation(self.lhs.initial_values, self.rhs.initial_values) - - @property - def final_values(self) -> Dict[ChannelID, ExpressionScalar]: - return self._apply_operation(self.lhs.final_values, self.rhs.final_values) + return _apply_operation_to_channel_dict(self._arithmetic_operator, + self.lhs._as_expression(), + self.rhs._as_expression()) def build_waveform(self, parameters: Dict[str, Real], @@ -192,37 +173,25 @@ def deserialize(cls, serializer: Optional[Serializer] = None, **kwargs) -> 'Arit class ArithmeticPulseTemplate(PulseTemplate): + """""" + def __init__(self, lhs: Union[PulseTemplate, ExpressionLike, Mapping[ChannelID, ExpressionLike]], arithmetic_operator: str, rhs: Union[PulseTemplate, ExpressionLike, Mapping[ChannelID, ExpressionLike]], *, - identifier: Optional[str] = None): - """Implements the arithmetics between an aribrary pulse template and scalar values. The values can be the same - for all channels, channel specific or only for a subset of the inner pulse templates defined channels. - The expression may be time dependent if the pulse template is atomic. - - A channel dependent scalar is represented by a mapping of ChannelID -> Expression. - - The allowed operations are: - scalar + pulse_template - scalar - pulse_template - scalar * pulse_template - pulse_template + scalar - pulse_template - scalar - pulse_template * scalar - pulse_template / scalar + identifier: Optional[str] = None, + registry: Optional[PulseRegistryType] = None): + """ Args: lhs: Left hand side operand arithmetic_operator: String representation of the operator rhs: Right hand side operand - identifier: Identifier used for serialization + identifier: Raises: - TypeError: If both or none of the operands are pulse templates or if there is a time dependent expression - and a composite pulse template. - ValueError: If the scalar is a mapping and contains channels that are not defined on the pulse template. + TypeError if both or none of the operands are pulse templates """ PulseTemplate.__init__(self, identifier=identifier) @@ -250,18 +219,11 @@ def __init__(self, self._lhs = lhs self._rhs = rhs - self._pulse_template: PulseTemplate = pulse_template + self._pulse_template = pulse_template self._scalar = scalar self._arithmetic_operator = arithmetic_operator - - if not self._pulse_template._is_atomic() and _is_time_dependent(self._scalar): - raise TypeError("A time dependent ArithmeticPulseTemplate scalar operand currently requires an atomic " - "pulse template as the other operand.", self) - - if self._pulse_template._is_atomic(): - # this is a hack so we can use the AtomicPulseTemplate.integral default implementation - self._AS_EXPRESSION_TIME = AtomicPulseTemplate._AS_EXPRESSION_TIME + self._register(registry=registry) @staticmethod def _parse_operand(operand: Union[ExpressionLike, Mapping[ChannelID, ExpressionLike]], @@ -306,30 +268,17 @@ def _get_scalar_value(self, Returns: The evaluation of the scalar operand for all relevant channels """ - def _evaluate(value: ExpressionScalar): - return value._evaluate_to_time_dependent(parameters) - if isinstance(self._scalar, ExpressionScalar): - scalar_value = _evaluate(self._scalar) + scalar_value = self._scalar.evaluate_in_scope(parameters) return {channel_mapping[channel]: scalar_value for channel in self._pulse_template.defined_channels if channel_mapping[channel]} else: - return {channel_mapping[channel]: _evaluate(value) + return {channel_mapping[channel]: value.evaluate_in_scope(parameters) for channel, value in self._scalar.items() if channel_mapping[channel]} - def _as_expression(self): - atomic = cast(AtomicPulseTemplate, self._pulse_template) - as_expression = atomic._as_expression() - scalar = self._scalar_as_dict() - for ch, value in scalar.items(): - if 't' in value.variables: - scalar[ch] = value.evaluate_symbolic({'t': self._AS_EXPRESSION_TIME}) - - return self._apply_operation_to_channel_dict(as_expression, scalar) - @property def lhs(self): return self._lhs @@ -438,71 +387,47 @@ def defined_channels(self): def duration(self) -> ExpressionScalar: return self._pulse_template.duration - def _scalar_as_dict(self) -> Dict[ChannelID, ExpressionScalar]: - if isinstance(self._scalar, ExpressionScalar): - return {channel: self._scalar - for channel in self.defined_channels} - else: - return dict(self._scalar) - @property def integral(self) -> Dict[ChannelID, ExpressionScalar]: - if _is_time_dependent(self._scalar): - # use the superclass implementation that relies on _as_expression - return AtomicPulseTemplate.integral.fget(self) - integral = {channel: value.sympified_expression for channel, value in self._pulse_template.integral.items()} - scalar = self._scalar_as_dict() - if self._arithmetic_operator in ('+', '-'): - for ch, value in scalar.items(): - scalar[ch] = value * self.duration.sympified_expression - - return self._apply_operation_to_channel_dict(integral, scalar) - - def _apply_operation_to_channel_dict(self, - pt_values: Dict[ChannelID, ExpressionScalar], - scalar_values: Dict[ChannelID, ExpressionScalar]): - operator_map = { - '+': operator.add, - '-': operator.sub, - '/': operator.truediv, - '*': operator.mul - } - - rhs_only_map = { - '+': operator.pos, - '-': operator.neg, - '*': lambda x: x, - '/': lambda x: 1 / x - } - - if self._pulse_template is self.lhs: - lhs, rhs = pt_values, scalar_values + if isinstance(self._scalar, ExpressionScalar): + scalar = {channel: self._scalar.sympified_expression + for channel in self.defined_channels} else: - lhs, rhs = scalar_values, pt_values - # cannot divide by pulse templates - operator_map.pop('/') - rhs_only_map.pop('/') + scalar = {channel: value.sympified_expression + for channel, value in self._scalar.items()} - operator_both = operator_map.get(self._arithmetic_operator, None) - rhs_only = rhs_only_map.get(self._arithmetic_operator, None) + if self._arithmetic_operator == '+': + for channel, value in scalar.items(): + integral[channel] = integral[channel] + (value * self.duration.sympified_expression) - return _apply_operation_to_channel_dict(lhs, rhs, operator_both=operator_both, rhs_only=rhs_only) + elif self._arithmetic_operator == '*': + for channel, value in scalar.items(): + integral[channel] = integral[channel] * value - @property - def initial_values(self) -> Dict[ChannelID, ExpressionScalar]: - return self._apply_operation_to_channel_dict( - self._pulse_template.initial_values, - self._scalar_as_dict() - ) + elif self._arithmetic_operator == '/': + assert self._pulse_template is self.lhs + for channel, value in scalar.items(): + integral[channel] = integral[channel] / value - @property - def final_values(self) -> Dict[ChannelID, ExpressionScalar]: - return self._apply_operation_to_channel_dict( - self._pulse_template.final_values, - self._scalar_as_dict() - ) + else: + assert self._arithmetic_operator == '-' + if self._pulse_template is self.rhs: + # we need to negate all existing values + for channel, inner_value in integral.items(): + if channel in scalar: + integral[channel] = scalar[channel] * self.duration.sympified_expression - inner_value + else: + integral[channel] = -inner_value + + else: + for channel, value in scalar.items(): + integral[channel] = integral[channel] - value * self.duration.sympified_expression + + for channel, value in integral.items(): + integral[channel] = ExpressionScalar(value) + return integral @property def measurement_names(self) -> Set[str]: @@ -511,11 +436,9 @@ def measurement_names(self) -> Set[str]: @cached_property def _scalar_operand_parameters(self) -> FrozenSet[str]: if isinstance(self._scalar, dict): - return frozenset(variable - for value in self._scalar.values() - for variable in value.variables) - {'t'} + return frozenset(*(value.variables for value in self._scalar.values())) else: - return frozenset(self._scalar.variables) - {'t'} + return frozenset(self._scalar.variables) @property def parameter_names(self) -> Set[str]: @@ -533,9 +456,6 @@ def get_measurement_windows(self, measurement_mapping=measurement_mapping)) return measurements - def _is_atomic(self): - return self._pulse_template._is_atomic() - def try_operation(lhs: Union[PulseTemplate, ExpressionLike, Mapping[ChannelID, ExpressionLike]], op: str, @@ -568,13 +488,6 @@ def try_operation(lhs: Union[PulseTemplate, ExpressionLike, Mapping[ChannelID, E return NotImplemented -def _is_time_dependent(scalar: Union[ExpressionScalar, Dict[str, ExpressionScalar]]) -> bool: - if isinstance(scalar, dict): - return any('t' in value.variables for value in scalar.values()) - else: - return 't' in scalar.variables - - class UnequalDurationWarningInArithmeticPT(RuntimeWarning): """Signals that an ArithmeticAtomicPulseTemplate was constructed from operands with unequal duration. This is a separate class to allow easy silencing.""" diff --git a/qupulse/pulses/function_pulse_template.py b/qupulse/pulses/function_pulse_template.py index fa02feaff..9fbe4cc0a 100644 --- a/qupulse/pulses/function_pulse_template.py +++ b/qupulse/pulses/function_pulse_template.py @@ -99,11 +99,15 @@ def build_waveform(self, if channel is None: return None + duration = self.__duration_expression.evaluate_with_exact_rationals(parameters) + if duration == 0: + return None + if 't' in parameters: parameters = {k: v for k, v in parameters.items() if k != 't'} expression = self.__expression.evaluate_symbolic(substitutions=parameters) - duration = self.__duration_expression.evaluate_with_exact_rationals(parameters) + return FunctionWaveform.from_expression(expression=expression, duration=duration, diff --git a/qupulse/pulses/time_reversal_pulse_template.py b/qupulse/pulses/time_reversal_pulse_template.py index a4758d1a7..36f4dd049 100644 --- a/qupulse/pulses/time_reversal_pulse_template.py +++ b/qupulse/pulses/time_reversal_pulse_template.py @@ -49,9 +49,9 @@ def integral(self) -> Dict[ChannelID, ExpressionScalar]: def _internal_create_program(self, *, parent_loop: Loop, **kwargs) -> None: inner_loop = Loop() self._inner._internal_create_program(parent_loop=inner_loop, **kwargs) - inner_loop.reverse_inplace() - - parent_loop.append_child(inner_loop) + if inner_loop != Loop(): + inner_loop.reverse_inplace() + parent_loop.append_child(inner_loop) def build_waveform(self, *args, **kwargs) -> Optional[Waveform]: diff --git a/qupulse/serialization.py b/qupulse/serialization.py index 3825057ed..36c243809 100644 --- a/qupulse/serialization.py +++ b/qupulse/serialization.py @@ -1024,7 +1024,11 @@ def filter_serializables(self, obj_dict) -> Any: if get_default_pulse_registry() is self.storage: registry = dict() - return deserialization_callback(identifier=obj_identifier, registry=registry, **obj_dict) + try: + return deserialization_callback(identifier=obj_identifier, registry=registry, **obj_dict) + except (RuntimeError, TypeError) as err: + raise ValueError(f"Unable to deserialize {type_identifier} from {obj_dict}") from err + return obj_dict diff --git a/tests/_program/seqc_tests.py b/tests/_program/seqc_tests.py index 6b2cfb0db..dbc62a218 100644 --- a/tests/_program/seqc_tests.py +++ b/tests/_program/seqc_tests.py @@ -1064,4 +1064,232 @@ def test_full_run_with_dynamic_rate_reduction(self): wait(IDLE_WAIT_CYCLES); } }""" - self.assertEqual(expected_program, seqc_program) \ No newline at end of file + self.assertEqual(expected_program, seqc_program) + + def test_shuttle_pulse(self): + from qupulse.pulses import PointPT, RepetitionPT, ParallelChannelPT, MappingPT, ForLoopPT, AtomicMultiChannelPT, FunctionPT, TimeReversalPT + import sympy + + read_pls = PointPT([ + ('t_read_0', 'V_read_0'), + ('t_read_1', 'V_read_1'), + ('t_read_2', 'V_read_2'), + ('t_read_3', 'V_read_3'), + ], tuple('ABCDEFGHIJKLMNOP'), measurements=[('read', 0, 't_read_3')]) + + arbitrary_load = PointPT([ + ('t_load_0', 'V_load_0 * bit_flag + V_empty_0 * (1 - bit_flag)'), + ('t_load_1', 'V_load_1 * bit_flag + V_empty_1 * (1 - bit_flag)'), + ('t_load_2', 'V_load_2 * bit_flag + V_empty_2 * (1 - bit_flag)'), + ('t_load_3', 'V_load_3 * bit_flag + V_empty_3 * (1 - bit_flag)'), + ], tuple('ABCDEFGHIJKLMNOP'), measurements=[('load', 0, 't_load_3')]) + + load_bit = MappingPT(arbitrary_load, parameter_mapping={'bit_flag': 'pattern[bit]'}) + + reduce_amp = PointPT([ + (0, 'V_reduce_amp_0'), + ('t_reduce_amp', 'V_reduce_amp_1', 'linear'), + ('t_amp_holdon', 'V_reduce_amp_1', 'hold'), + ('t_recover_amp', 'V_reduce_amp_2', 'linear'), + ], tuple('ABCDEFGHIJKLMNOP'), measurements=[('reduce_amp', 0, 't_recover_amp')]) + + # define sinewaves by FunctionPT + + sample_rate, f, n_segments = sympy.symbols('sample_rate, f, n_segments') + n_oct = sympy.symbols('n_oct') + segment_time = n_segments / sample_rate # in ns + + shuttle_period = sympy.ceiling(1 / f / segment_time) * segment_time # in ns + shuttle_oct = shuttle_period / 8 + actual_frequency = 1 / shuttle_period + + # Make a shuttle pulse including 4 clavier gates + 2 individual gates on each side. + arbitrary_shuttle = AtomicMultiChannelPT( + FunctionPT(f'amp_A * cos(2*pi*{actual_frequency}*t + phi[0]) + offset[0]', duration_expression='duration', + channel='A'), + FunctionPT(f'amp_B * cos(2*pi*{actual_frequency}*t + phi[1]) + offset[1]', duration_expression='duration', + channel='B'), + FunctionPT(f'amp_C * cos(2*pi*{actual_frequency}*t + phi[2]) + offset[2]', duration_expression='duration', + channel='C'), + FunctionPT(f'amp_D * cos(2*pi*{actual_frequency}*t + phi[3]) + offset[3]', duration_expression='duration', + channel='D'), + + # >> Add T gates for both ends with consistent channel names ('F': TLB2 <- S4, 'K': TRB2 <- S4) + FunctionPT(f'amp_F * cos(2*pi*{actual_frequency}*t + phi[5]) + offset[5]', duration_expression='duration', + channel='F'), + FunctionPT(f'amp_K * cos(2*pi*{actual_frequency}*t + phi[10]) + offset[10]', duration_expression='duration', + channel='K'), + measurements=[('shuttle', 0, 'duration')] + ) + + shuttle_in = MappingPT(arbitrary_shuttle, + parameter_mapping={'duration': shuttle_period}, + measurement_mapping={'shuttle': 'shuttle_in'}) + + shuttle_out = MappingPT(TimeReversalPT(arbitrary_shuttle), + parameter_mapping={'duration': shuttle_period}, + measurement_mapping={'shuttle': 'shuttle_out'}) + + shuttle_fract_in = MappingPT(arbitrary_shuttle, + parameter_mapping={'duration': shuttle_oct * n_oct}, + measurement_mapping={'shuttle': 'shuttle_in'} + ) + + shuttle_fract_out = shuttle_fract_in.with_time_reversal() + + flush_out = MappingPT(RepetitionPT(TimeReversalPT(arbitrary_shuttle), '2 * len(pattern)'), + parameter_mapping={'duration': shuttle_period}, + measurement_mapping={'shuttle': 'flush_out'}, + parameter_constraints=['len(pattern) > 0']) + + wobble_shuttle = MappingPT(arbitrary_shuttle @ arbitrary_shuttle.with_time_reversal(), + parameter_mapping={'duration': shuttle_period}, + measurement_mapping={'shuttle': 'shuttle_in'}) + + # Plug load, shuttle in and read together => PL1 --> S+n --> DL1 + channels_onhold = load_bit.defined_channels - shuttle_in.defined_channels + bit_in = load_bit @ ParallelChannelPT(shuttle_in, + overwritten_channels={ch: f"offset[{ord(ch) - ord('A')}]" for ch in + channels_onhold}) + pattern_in = ForLoopPT(bit_in, 'bit', 'len(pattern)') + + # Plug fractional shuttle in, reduce amplitude and fractional out together + channels_onhold = pattern_in.defined_channels - shuttle_fract_in.defined_channels + fract_in_n_reduce_amp = ParallelChannelPT( + shuttle_fract_in, + overwritten_channels={ch: f"offset[{ord(ch) - ord('A')}]" for ch in channels_onhold} + ) @ reduce_amp @ ParallelChannelPT( + shuttle_fract_out, + overwritten_channels={ch: f"offset[{ord(ch) - ord('A')}]" for ch in channels_onhold}) + + # plug read and shuttle out together => S(-n' per read) --> DL1 + channels_onhold = reduce_amp.defined_channels - shuttle_in.defined_channels + period_out = ParallelChannelPT(shuttle_out, overwritten_channels={ch: f"offset[{ord(ch) - ord('A')}]" for ch in + channels_onhold}) @ read_pls + + # + channels_onhold = read_pls.defined_channels - shuttle_in.defined_channels + wobble = ParallelChannelPT(RepetitionPT(wobble_shuttle, 3), + overwritten_channels={ch: f"offset[{ord(ch) - ord('A')}]" for ch in channels_onhold}) + + # repeated read and shuttle out => [S(-n' per read) --> DL1] x (n_in + n_extra) + tot_period_out = RepetitionPT(period_out, 'len(pattern) + n_period_extra') + + # make a flush pulse according to the last read. + channels_onhold = read_pls.defined_channels - flush_out.defined_channels + flush = ParallelChannelPT(flush_out, + overwritten_channels={ch: f"flush[{ord(ch) - ord('A')}]" for ch in channels_onhold}) + + pattern_in_n_out = pattern_in @ fract_in_n_reduce_amp @ tot_period_out + + default_params = {} + + for n in ('load', 'read', 'empty'): + for ii in range(4): + default_params[f't_{n}_{ii}'] = ii * 25e6 + + default_params = {**default_params, + 'amp_A': .14 * 2, + 'amp_B': .14, + 'amp_C': .14 * 2, + 'amp_D': .14, + 'amp_F': .14, + 'amp_K': .14, + } + + default_params = {**default_params, + # A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P + # S1, S2, S3, S4, TRP, TRB2, RB2, TRB1, TLB1, TLP, TLB2, LB2, LB1, RB1, EMP, CLK + + 'V_load_0': [-.3, 0., .3, 0., -.2, 0., -.1, -.4, 0., 0., 0, -.3, 0., 0., 0., 0.], + 'V_load_1': [-.3, 0., .3, 0., -.2, 0., -.1, -.4, 0., .5, 0, -.3, 0., 0., 0., 0.], + 'V_load_2': [-.3, 0., .3, 0., -.2, 0., -.1, -.4, -.5, .5, 0, -.3, 0., 0., 0., 0.], + 'V_load_3': [-.3, 0., .3, 0., -.2, 0., -.1, -.4, -.5, .5, 0, -.3, 0., 0., 0., 0.], + + 'V_read_0': [-.3, 0., .3, 0., -.2, 0., -.1, -.4, -.5, .5, 0, -.3, 0., 0., 0., 0.], + 'V_read_1': [-.3, 0., .3, 0., -.2, 0., -.1, -.4, -.5, .5, 0, -.3, 0., 0., 0., 0.], + 'V_read_2': [-.3, 0., .3, 0., -.2, 0., -.1, -.4, 0., .5, 0, -.3, 0., 0., 0., 0.], + 'V_read_3': [-.3, 0., .3, 0., -.2, 0., -.1, -.4, 0., 0., 0, -.3, 0., 0., 0., 0.], + + 'V_empty_0': [-.3, 0., .3, 0., -.2, 0., 0, -.4, 0., 0., 0, -.3, 0., 0., 0., 0.], + 'V_empty_1': [-.3, 0., .3, 0., -.2, 0., 0, -.4, 0., 0., 0, -.3, 0., 0., 0., 0.], + 'V_empty_2': [-.3, 0., .3, 0., -.2, 0., 0, -.4, -.5, 0., 0, -.3, 0., 0., 0., 0.], + 'V_empty_3': [-.3, 0., .3, 0., -.2, 0., 0, -.4, -.5, 0., 0, -.3, 0., 0., 0., 0.], + + 'offset': [0., 0., 0., 0., -.2, 0., -.1, -.2, 0., 0., 0, -.3, 0., 0., 0., 0.], + 'flush': [0., 0., 0., 0., -.2, 0., -.1, -.2, 0., 0., 0, -.3, 0., 0., 0., 0.], + + 'f': 100e-9, + + 'pattern': [1, 0, 1, 0, 1, 1], + 'n_oct': 1, + 'n_period_extra': 3, + # number of period (extra reading wrt shuttle in. Default = 3 -> 3 additiional reading) + + 't_reduce_amp': 30e6, + 't_amp_holdon': 60e6, + 't_recover_amp': 90e6, + + 'V_reduce_amp_0': [-.3, 0., .3, 0., -.2, 0., -.1, -.4, 0., 0., 0, -.3, 0., 0., 0., 0.], + 'V_reduce_amp_1': [-.3, -.1, .3, .1, -.2, 0., -.1, -.4, 0., 0., 0, -.3, 0., 0., 0., 0.], + 'V_reduce_amp_2': [-.3, 0., .3, 0., -.2, 0., -.1, -.4, 0., 0., 0, -.3, 0., 0., 0., 0.], + + 'phi': [-3.1416, -4.7124, -6.2832, -7.8540, -1.5708, -7.8540, -1.5708, -1.5708, -1.5708, + -1.5708, -1.5708, -1.5708], + 'n_segments': 192, + 'sample_rate': 0.1 / 2 ** 5, + } + + program = pattern_in_n_out.create_program(parameters=default_params) + + manager = HDAWGProgramManager() + + manager.add_program('test', program, + channels=('A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'), + markers=(None,) * 16, + amplitudes=(4.,)*8, + offsets=(0.,)*8, + voltage_transformations=(None,)*8, + sample_rate=default_params['sample_rate']) + seqc_program = manager.to_seqc_program() + + return seqc_program + + @unittest.skipIf(sys.version_info.minor < 6, "This test requires dict to be ordered.") + def test_DigTrigger(self): + defined_channels = frozenset(['A', 'B', 'C']) + + unique_n = 1000 + unique_duration = 32 + + unique_wfs = get_unique_wfs(n=unique_n, duration=unique_duration, defined_channels=defined_channels) + same_wf = DummyWaveform(duration=48, sample_output=np.ones(48), defined_channels=defined_channels) + + channels = ('A', 'B') + markers = ('C', None, 'A', None) + amplitudes = (1., 1.) + offsets = (0., 0.) + volatage_transformations = (lambda x: x, lambda x: x) + sample_rate = 1 + + triggerIn = 8 + DigTriggerIndex = 1 + + root = complex_program_as_loop(unique_wfs, wf_same=same_wf) + seqc_nodes = complex_program_as_seqc(unique_wfs, wf_same=same_wf) + + manager = HDAWGProgramManager() + compiler_settings = manager.DEFAULT_COMPILER_SETTINGS + compiler_settings['trigger_wait_code'] = f'waitDigTrigger({DigTriggerIndex});' + manager.add_program('test', root, channels, markers, amplitudes, offsets, volatage_transformations, sample_rate) + + # 0: Program selection + # 1: Trigger + + seqc_program = manager.to_seqc_program() + + return seqc_program + + + + diff --git a/tests/hardware/alazar_tests.py b/tests/hardware/alazar_tests.py index c6e7d032d..df2dcefc7 100644 --- a/tests/hardware/alazar_tests.py +++ b/tests/hardware/alazar_tests.py @@ -4,7 +4,7 @@ import numpy as np from ..hardware import * -from qupulse.hardware.dacs.alazar import AlazarCard, AlazarProgram +from qupulse.hardware.dacs.alazar import AlazarCard, AlazarProgram, _shrink_overlapping_windows from qupulse.utils.types import TimeType @@ -148,6 +148,13 @@ def test_register_measurement_windows(self): # pi ist genau 3 np.testing.assert_equal(result_lengths if isinstance(result_lengths, np.ndarray) else result_lengths.as_ndarray(), 3) + def test_shrink_overlapping_windows(self): + np.testing.assert_equal( + (np.array([1, 4, 8]), np.array([3, 4, 4])), + _shrink_overlapping_windows(np.array([1, 4, 7]), + np.array([3, 4, 5])) + ) + def test_register_operations(self): card = AlazarCard(None)