From 0bc5ee4fffddbe16cc7282adc8201ea34fe7eeac Mon Sep 17 00:00:00 2001 From: Marios Samiotis Date: Wed, 1 May 2024 14:53:22 +0200 Subject: [PATCH] Checkpoint before running a decent cryoscope --- pycqed/analysis_v2/cryoscope_v2_analysis.py | 488 ++++++++++- .../meta_instrument/HAL_Device.py | 70 +- .../qubit_objects/HAL_Transmon.py | 2 +- .../openql_experiments/multi_qubit_oql.py | 9 - pycqed/qce_utils/__init__.py | 0 pycqed/qce_utils/analysis_factory/__init__.py | 0 .../factory_transmon_arc_identifier.py | 246 ++++++ .../intrf_analysis_factory.py | 42 + .../plotting_functionality.py | 280 +++++++ .../qce_utils/control_interfaces/__init__.py | 0 .../connectivity_surface_code.py | 783 ++++++++++++++++++ .../intrf_channel_identifier.py | 297 +++++++ .../control_interfaces/intrf_connectivity.py | 145 ++++ .../intrf_connectivity_surface_code.py | 83 ++ pycqed/qce_utils/custom_exceptions.py | 245 ++++++ pycqed/qce_utils/definitions.py | 25 + .../qce_utils/measurement_module/__init__.py | 0 pycqed/qce_utils/module_description.md | 14 + 18 files changed, 2683 insertions(+), 46 deletions(-) create mode 100644 pycqed/qce_utils/__init__.py create mode 100644 pycqed/qce_utils/analysis_factory/__init__.py create mode 100644 pycqed/qce_utils/analysis_factory/factory_transmon_arc_identifier.py create mode 100644 pycqed/qce_utils/analysis_factory/intrf_analysis_factory.py create mode 100644 pycqed/qce_utils/analysis_factory/plotting_functionality.py create mode 100644 pycqed/qce_utils/control_interfaces/__init__.py create mode 100644 pycqed/qce_utils/control_interfaces/connectivity_surface_code.py create mode 100644 pycqed/qce_utils/control_interfaces/intrf_channel_identifier.py create mode 100644 pycqed/qce_utils/control_interfaces/intrf_connectivity.py create mode 100644 pycqed/qce_utils/control_interfaces/intrf_connectivity_surface_code.py create mode 100644 pycqed/qce_utils/custom_exceptions.py create mode 100644 pycqed/qce_utils/definitions.py create mode 100644 pycqed/qce_utils/measurement_module/__init__.py create mode 100644 pycqed/qce_utils/module_description.md diff --git a/pycqed/analysis_v2/cryoscope_v2_analysis.py b/pycqed/analysis_v2/cryoscope_v2_analysis.py index 32ae59be31..12a8e345a6 100644 --- a/pycqed/analysis_v2/cryoscope_v2_analysis.py +++ b/pycqed/analysis_v2/cryoscope_v2_analysis.py @@ -1,16 +1,20 @@ """ Created: 2020-07-15 -Author: Victor Negirneac """ - import matplotlib.pyplot as plt +import matplotlib.transforms as transforms from pycqed.analysis.analysis_toolbox import get_datafilepath_from_timestamp import pycqed.analysis_v2.cryoscope_v2_tools as cv2_tools from pycqed.analysis_v2 import measurement_analysis as ma2 from pycqed.analysis import fitting_models as fit_mods from pycqed.analysis.tools.plotting import (set_xlabel, set_ylabel) import pycqed.analysis_v2.base_analysis as ba +from pycqed.utilities.general import print_exception import pycqed.measurement.hdf5_data as hd5 +from pycqed.qce_utils.analysis_factory.factory_transmon_arc_identifier import ( + FluxArcIdentifier, + FluxArcIdentifierAnalysis, +) from collections import OrderedDict from uncertainties import ufloat from scipy import signal @@ -708,6 +712,20 @@ def prepare_plots(self): "xunit": "s", } +def filter_func(t, A, tau, B): + ''' + Filter function implemented + in the HDAWG IIR filter model. + ''' + return B*(1+A*np.exp(-t/tau)) + +def filter_func_high_pass(t, tau, t0): + ''' + Filter function implemented + in the HDAWG FIR high-pass filter model. + ''' + return np.exp(-(t-t0)/tau) + class multi_qubit_cryoscope_analysis(ba.BaseDataAnalysis): """ Simultaneous cryoscope analysis. @@ -715,6 +733,7 @@ class multi_qubit_cryoscope_analysis(ba.BaseDataAnalysis): def __init__(self, update_FIRs: bool=False, + update_IIRs: bool=False, t_start: str = None, t_stop: str = None, label: str = '', @@ -722,13 +741,17 @@ def __init__(self, extract_only: bool = False, auto=True, poly_params: dict = None, + derivative_window_length: float=5e-9, ): super().__init__(t_start=t_start, t_stop=t_stop, label=label, options_dict=options_dict, extract_only=extract_only) self.poly_params = poly_params + self.update_IIRs = update_IIRs self.update_FIRs = update_FIRs + self.derivative_window_length = derivative_window_length + assert not (update_FIRs and update_IIRs), 'Can only either update IIRs or FIRs' if auto: self.run_analysis() @@ -788,7 +811,7 @@ def process_data(self): for n, qubit in enumerate(self.Qubits): data_shape = a_obj.raw_data_dict['measured_values'][0][n*2:n*2+2] cryoscope_no_dist = ma2.Cryoscope_Analysis(t_start=self.timestamp, - ch_idx_cos=0, ch_idx_sin=1, derivative_window_length=8e-9, close_figs=True, + ch_idx_cos=0, ch_idx_sin=1, derivative_window_length=self.derivative_window_length, close_figs=True, ch_amp_key='Snapshot/instruments/flux_lm_{}/parameters/cfg_awg_channel_amplitude'.format(qubit), waveform_amp_key='Snapshot/instruments/flux_lm_{}/parameters/sq_amp'.format(qubit), ch_range_key='Snapshot/instruments/flux_lm_{}/parameters/cfg_awg_channel_range'.format(qubit), @@ -797,13 +820,13 @@ def process_data(self): extract_only=True) t = cryoscope_no_dist.ca.time a = cryoscope_no_dist.ca.get_amplitudes() - baseline_start = 50 - baseline_stop = 100 + baseline_start = -50 + baseline_stop = -30 norm = np.mean(a[baseline_start:baseline_stop]) a /= norm if np.std(a) < 1e-6: # UHF is switching channels cryoscope_no_dist = ma2.Cryoscope_Analysis(t_start=self.timestamp, - ch_idx_cos=1, ch_idx_sin=0, derivative_window_length=8e-9, close_figs=True, + ch_idx_cos=1, ch_idx_sin=0, derivative_window_length=self.derivative_window_length, close_figs=True, ch_amp_key='Snapshot/instruments/flux_lm_{}/parameters/cfg_awg_channel_amplitude'.format(qubit), waveform_amp_key='Snapshot/instruments/flux_lm_{}/parameters/sq_amp'.format(qubit), ch_range_key='Snapshot/instruments/flux_lm_{}/parameters/cfg_awg_channel_range'.format(qubit), @@ -813,14 +836,13 @@ def process_data(self): t = cryoscope_no_dist.ca.time a = cryoscope_no_dist.ca.get_amplitudes() baseline_start = 50 - baseline_stop = 100 + baseline_stop = -1 norm = np.mean(a[baseline_start:baseline_stop]) a /= norm - self.proc_data_dict['Traces'].append(a) self.proc_data_dict['time'] = t ################################### - # Run filter optimizer in parallel + # Run FIR filter optimizer ################################### if self.update_FIRs: # Parallel optimization crashes terminal @@ -837,7 +859,27 @@ def process_data(self): conv_filter = cv2_tools.convolve_FIRs([old_filter, new_filter]) conv_filter = cv2_tools.convert_FIR_for_HDAWG(conv_filter) self.proc_data_dict['conv_filters'][qubit] = conv_filter - + ################################### + # Fit exponential filter + ################################### + elif self.update_IIRs: + self.proc_data_dict['exponential_filter'] = {} + self.proc_data_dict['fit_params'] = {} + for i, q in enumerate(self.Qubits): + Times = self.proc_data_dict['time'] + Trace = self.proc_data_dict['Traces'][i] + # Look at signal after 50 ns + initial_idx = np.argmin(np.abs(Times-20e-9)) + Times = Times[initial_idx:] + Trace = Trace[initial_idx:] + # Fit exponential to trace + from scipy.optimize import curve_fit + p0 = [-.2, 15e-9, 1] + popt, pcov = curve_fit(filter_func, Times, Trace, p0=p0) + filtr = {'amp': popt[0], 'tau': popt[1]} + self.proc_data_dict['exponential_filter'][q] = filtr + self.proc_data_dict['fit_params'][q] = popt + def prepare_plots(self): for i, qubit in enumerate(self.Qubits): self.plot_dicts[f'Cryscope_trace_{qubit}'] = { @@ -847,9 +889,13 @@ def prepare_plots(self): 'qubit': qubit, 'timestamp': self.timestamp } + if self.update_IIRs: + self.plot_dicts[f'Cryscope_trace_{qubit}']['filter_pars'] = \ + self.proc_data_dict['fit_params'][qubit] def optimize_fir_software(y, baseline_start=100, - baseline_stop=None, taps=72, start_sample=0, stop_sample=200, cma_target=0.5): + baseline_stop=None, taps=72, start_sample=0, + stop_sample=200, cma_target=0.5): step_response = np.concatenate((np.array([0]), y)) baseline = np.mean(y[baseline_start:baseline_stop]) x0 = [1] + (taps - 1) * [0] @@ -857,24 +903,33 @@ def objective_function_fir(x): y = step_response yc = signal.lfilter(x, 1, y) return np.mean(np.abs(yc[1+start_sample:stop_sample] - baseline))/np.abs(baseline) - return cma.fmin2(objective_function_fir, x0, cma_target)[0] + return cma.fmin2(objective_function_fir, x0, cma_target, + options={'ftarget':1e-4, 'maxfevals': 2e5})[0] def plot_cryoscope_trace(trace, time, timestamp, qubit, + filter_pars=None, ax=None, **kw): - ax.plot(time, trace) - set_xlabel(ax, 'Time', 's') - set_ylabel(ax, 'Amplitude', 'a.u.') - ax.set_ylim(0.95,1.05) - ax.set_xlim(0, time[-1]) ax.axhline(1, color='grey', ls='-') ax.axhline(1.01, color='grey', ls='--') ax.axhline(0.99, color='grey', ls='--') ax.axhline(1.001, color='grey', ls=':') ax.axhline(0.999, color='grey', ls=':') - ax.set_title(timestamp+': Step responses Cryoscope '+qubit) + ax.plot(time, trace) + if filter_pars is not None: + ax.plot(time, filter_func(time, *filter_pars), label='IIR filter fit') + ax.legend(frameon=False) + set_xlabel(ax, 'Time', 's') + set_ylabel(ax, 'Amplitude', 'a.u.') + bottom, top = ax.get_ylim() + bottom = min(.95, bottom) + top = max(1.05, top) + ax.set_ylim(bottom,top) + ax.set_xlim(0, time[-1]) + ax.set_title(timestamp+': Step response Cryoscope '+qubit) + class Time_frequency_analysis(ba.BaseDataAnalysis): def __init__(self, @@ -1009,6 +1064,7 @@ def FFT_plotfn( fig.tight_layout() + class Flux_arc_analysis(ba.BaseDataAnalysis): def __init__(self, channel_amp:float, @@ -1101,4 +1157,398 @@ def Voltage_arc_plotfn( ax.set_ylabel('Detuning (MHz)') ax.set_xlabel('Output Voltage (V)') ax.set_title(f'{timestamp}\n{qubit} Voltage frequency arc') - fig.tight_layout() \ No newline at end of file + fig.tight_layout() + + +def voltage_arc_plotfn_detailed( + amplitudes, + detunings, + qubit, + timestamp, + ax, + **kw, + ): + identifier = FluxArcIdentifier( + _amplitude_array=amplitudes, + _detuning_array=detunings, + ) + fig = ax.get_figure() + fig, ax = FluxArcIdentifierAnalysis.plot_flux_arc_identifier( + identifier=identifier, + host_axes=(fig, ax), + ) + ax.set_title(f'{timestamp}\n{qubit} Detailed voltage frequency arc') + return fig, ax + + +class FluxArcSymmetryIntersectionAnalysis(ba.BaseDataAnalysis): + """ + Behaviour class, Analysis that handles intersection calculation of + equal positive and negative (AC) flux-pulse amplitudes while sweeping (DC) flux bias. + """ + + # region Class Constructor + def __init__( + self, + initial_bias: float, + t_start: str = None, + t_stop: str = None, + data_file_path: str = None, + label: str = "", + options_dict: dict = None, + ): + super().__init__( + t_start=t_start, + t_stop=t_stop, + label=label, + data_file_path=data_file_path, + options_dict=options_dict, + close_figs=True, + extract_only=False, + do_fitting=False, + ) + self.initial_bias: float = initial_bias # A + # endregion + + # region Interface Methods + def extract_data(self): + """ + This is a new style (sept 2019) data extraction. + This could at some point move to a higher level class. + """ + self.get_timestamps() + self.timestamp = self.timestamps[0] + + data_fp = get_datafilepath_from_timestamp(self.timestamp) + param_spec = {'data': ('Experimental Data/Data', 'dset'), + 'value_names': ('Experimental Data', 'attr:value_names')} + self.raw_data_dict = hd5.extract_pars_from_datafile(data_fp, param_spec) + # Parts added to be compatible with base analysis data requirements + self.raw_data_dict['timestamps'] = self.timestamps + self.raw_data_dict['folder'] = os.path.split(data_fp)[0] + + def process_data(self): + self.qubit_id = self.raw_data_dict['folder'].split('_')[-1] + self.flux_bias_array: np.ndarray = self.raw_data_dict['data'][:, 0] + self.positive_detuning_array: np.ndarray = self.raw_data_dict['data'][:, 1] + self.negative_detuning_array: np.ndarray = self.raw_data_dict['data'][:, 2] + + fit1 = np.polyfit(self.flux_bias_array, self.positive_detuning_array, 1) + fit2 = np.polyfit(self.flux_bias_array, self.negative_detuning_array, 1) + + # Extract the slope (m) and intercept (b) for both fits + m1, b1 = fit1 + m2, b2 = fit2 + + # Calculate the x- and y-coordinate of the intersection + self.x_intersect = (b2 - b1) / (m1 - m2) + self.y_intersect = m1 * self.x_intersect + b1 + self.suggested_bias_value = self.x_intersect + + def prepare_plots(self): + self.axs_dict = {} + fig, ax = plt.subplots(figsize=(6, 5), dpi=256) + self.axs_dict[f'flux_arc_intersection'] = ax + self.figs[f'flux_arc_intersection'] = fig + self.plot_dicts['flux_arc_intersection'] = { + 'plotfn': self.plot_flux_arc_intersection, + 'ax_id': 'flux_arc_intersection', + 'flux_bias_array': self.flux_bias_array, + 'positive_detuning_array': self.positive_detuning_array, + 'negative_detuning_array': self.negative_detuning_array, + 'initial_bias': self.initial_bias, + 'suggested_bias': self.suggested_bias_value, + 'timestamp': self.timestamps[0], + 'qubit_name': self.qubit_id, + } + + def run_post_extract(self): + self.prepare_plots() # specify default plots + self.plot(key_list='auto', axs_dict=self.axs_dict) # make the plots + if self.options_dict.get('save_figs', False): + self.save_figures( + close_figs=self.options_dict.get('close_figs', True), + tag_tstamp=self.options_dict.get('tag_tstamp', True)) + # endregion + + # region Static Class Methods + @staticmethod + def plot_flux_arc_intersection(flux_bias_array: np.ndarray, positive_detuning_array: np.ndarray, negative_detuning_array: np.ndarray, initial_bias: float, suggested_bias: float, timestamp: str, qubit_name: str, ax, **kw): + fit1 = np.polyfit(flux_bias_array, positive_detuning_array, 1) + poly1 = np.poly1d(fit1) + fit2 = np.polyfit(flux_bias_array, negative_detuning_array, 1) + poly2 = np.poly1d(fit2) + + # Extract the slope (m) and intercept (b) for both fits + m1, b1 = fit1 + m2, b2 = fit2 + + # Calculate the x- and y-coordinate of the intersection + x_intersect = suggested_bias + y_intersect = m1 * x_intersect + b1 + + # Figure and Axes + fig = ax.get_figure() + ax.plot( + flux_bias_array, + positive_detuning_array, + linestyle='none', + marker='o', + ) + ax.plot( + flux_bias_array, + negative_detuning_array, + linestyle='none', + marker='o', + ) + + min_x = min(x_intersect, min(flux_bias_array)) + max_x = max(x_intersect, max(flux_bias_array)) + span_x = abs(min_x - max_x) + high_resolution_x = np.linspace(min_x - 0.1 * span_x, max_x + 0.1 * span_x, 101) + y1_fit = poly1(high_resolution_x) + y2_fit = poly2(high_resolution_x) + + ax.plot(high_resolution_x, y1_fit, 'C0--') + ax.plot(high_resolution_x, y2_fit, 'C1--') + ax.plot(x_intersect, y_intersect, linestyle='none', marker='o', alpha=0.8, color='gray') + # Suggested value + ax.axvline(x_intersect, linestyle='--', marker='None', color='darkgrey') + + transform = transforms.blended_transform_factory(ax.transData, ax.transAxes) + ax.text(x_intersect, 0.9, f' {x_intersect * 1e3:0.4f} mA', ha='left', va='center', transform=transform) + ax.set_xlim(min(high_resolution_x), max(high_resolution_x)) + ax.set_xlabel('Flux bias [A]') + ax.set_ylabel('Frequency detuning [Hz]') + + ax.grid(True, alpha=0.5, linestyle='dashed') # Adds dashed gridlines + ax.set_axisbelow(True) # Puts grid on background + ax.set_title(f'{timestamp}\n{qubit_name} Frequency-arc vs DC-flux') + return fig, ax + # endregion + + +class Cryoscope_long_analysis(ba.BaseDataAnalysis): + def __init__(self, + update_IIR: bool = False, + update_IIR_high_pass: bool = False, + t_start: str = None, + t_stop: str = None, + label: str = '', + options_dict: dict = None, + extract_only: bool = False, + auto=True + ): + super().__init__(t_start=t_start, t_stop=t_stop, + label=label, + options_dict=options_dict, + extract_only=extract_only) + self.update_IIR = update_IIR + self.update_IIR_high_pass = update_IIR_high_pass + if auto: + self.run_analysis() + + def extract_data(self): + """ + This is a new style (sept 2019) data extraction. + This could at some point move to a higher level class. + """ + self.get_timestamps() + self.timestamp = self.timestamps[0] + + data_fp = get_datafilepath_from_timestamp(self.timestamp) + param_spec = {'data': ('Experimental Data/Data', 'dset'), + 'value_names': ('Experimental Data', 'attr:value_names')} + self.raw_data_dict = hd5.extract_pars_from_datafile( + data_fp, param_spec) + self.raw_data_dict['timestamps'] = self.timestamps + self.raw_data_dict['folder'] = os.path.split(data_fp)[0] + # Extrac poly coefficients of flux arc + self.qubit = self.raw_data_dict['folder'].split('_')[-2] + data_params = {'polycoeff': (f'Instrument settings/flux_lm_{self.qubit}', + 'attr:q_polycoeffs_freq_01_det'), + 'sq_amp': (f'Instrument settings/flux_lm_{self.qubit}', + 'attr:sq_amp'), + 'frequency': (f'Instrument settings/{self.qubit}', + 'attr:freq_qubit'), + 'freq_mod': (f'Instrument settings/{self.qubit}', + 'attr:mw_freq_mod')} + _dict = hd5.extract_pars_from_datafile(data_fp, data_params) + self.poly_coeffs = [ float(n) for n in \ + _dict['polycoeff'][1:-1].split(' ') if n != '' ] + self.frequency = eval(_dict['frequency']) + self.freq_mod = eval(_dict['freq_mod']) + self.sq_amp = eval(_dict['sq_amp']) + + def process_data(self): + # Sort time axis + Time = self.raw_data_dict['data'][:,0] + n_time = np.where(Time==Time[0])[0][1] + Time = Time[:n_time] + # Sort frequency axis + Frequencies = self.raw_data_dict['data'][:,1] + n_freq = len(np.unique(Frequencies)) + Frequencies = Frequencies[::n_time]+self.freq_mod + # Sort measurement results + Data = self.raw_data_dict['data'][:,2].reshape(n_freq, n_time) + # Fit landscape for center frequencies + from scipy.optimize import curve_fit + from scipy.special import erf + def skewed_gauss(x, x0, sigma, alpha, a, b): + ''' + Skewed gaussian fit function. + ''' + phi = np.exp(-0.5*((x-x0)/sigma)**2) + Phi = 0.5*( 1 + erf( alpha*((x-x0)/sigma) / np.sqrt(2) ) ) + return a*phi*Phi + b + # Fit skewed gaussian to every time step + Center_freqs = np.zeros(n_time) + Voltage = np.zeros(n_time) + for i in range(n_time): + _x = Frequencies + _y = Data[:,i] + p0 = [_x[np.argmax(_y)], np.std(Frequencies)/2, 0, np.max(_y), np.min(_y)] + popt, pcov = curve_fit(skewed_gauss, _x, _y, p0=p0) + # Get maximum of function + _xx = np.linspace(Frequencies[0], Frequencies[-1], 201) + _yy = skewed_gauss(_xx, *popt) + Center_freqs[i] = _xx[np.argmax(_yy)] + # Convert frequency into voltage output + detuning = self.frequency-Center_freqs[i] + flux_arc = np.poly1d(self.poly_coeffs) + # Calculate corresponding voltage corresponding to detuning + if self.sq_amp > 0: + # Choose positive voltage + Voltage[i] = max((flux_arc-detuning).roots) + else: + # Choose negative voltage + Voltage[i] = min((flux_arc-detuning).roots) + # Trace = Voltage/np.mean(Voltage[-6:]) + Trace = Voltage/np.mean(Voltage[-1:]) + # Fit exponential to trace + if self.update_IIR: + try: + p0 = [+.001, 400e-9, 1.0085] + popt, pcov = curve_fit(filter_func, Time[8:]*1e-9, Trace[8:], p0=p0) + except: + print_exception() + print('Fit failed. Trying new initial guess') + p0 = [-.01, 2e-6, 1.003] + # try: + popt, pcov = curve_fit(filter_func, Time[6:]*1e-9, Trace[6:], p0=p0) + print('Fit converged!') + # except: + # print_exception() + # popt=p0 + # print('Fit failed') + filtr = {'amp': popt[0], 'tau': popt[1]} + self.proc_data_dict['filter_pars'] = popt + self.proc_data_dict['exponential_filter'] = filtr + # Fit high pass to trace + if self.update_IIR_high_pass: + p0 = [1.8e-3, +2e-6] + popt, pcov = curve_fit(filter_func_high_pass, + Time[100:]*1e-9, Trace[100:], p0=p0) + filtr = {'tau': popt[0]} + self.proc_data_dict['filter_pars'] = p0#popt + self.proc_data_dict['high_pass_filter'] = filtr + # Save quantities for plot + self.proc_data_dict['Time'] = Time + self.proc_data_dict['Frequencies'] = Frequencies + self.proc_data_dict['Data'] = Data + self.proc_data_dict['Center_freqs'] = Center_freqs + self.proc_data_dict['Trace'] = Trace + + def prepare_plots(self): + self.axs_dict = {} + fig, ax = plt.subplots(figsize=(4,3), dpi=200) + # fig.patch.set_alpha(0) + self.axs_dict[f'Cryoscope_long'] = ax + self.figs[f'Cryoscope_long'] = fig + self.plot_dicts['Cryoscope_long'] = { + 'plotfn': Cryoscope_long_plotfn, + 'ax_id': 'Cryoscope_long', + 'Time': self.proc_data_dict['Time'], + 'Frequencies': self.proc_data_dict['Frequencies'], + 'Data': self.proc_data_dict['Data'], + 'Center_freqs': self.proc_data_dict['Center_freqs'], + 'Trace': self.proc_data_dict['Trace'], + 'qubit': self.qubit, + 'qubit_freq': self.frequency, + 'timestamp': self.timestamps[0], + 'filter_pars': self.proc_data_dict['filter_pars'] \ + if (self.update_IIR or self.update_IIR_high_pass) else None, + } + + def run_post_extract(self): + self.prepare_plots() # specify default plots + self.plot(key_list='auto', axs_dict=self.axs_dict) # make the plots + if self.options_dict.get('save_figs', False): + self.save_figures( + close_figs=self.options_dict.get('close_figs', True), + tag_tstamp=self.options_dict.get('tag_tstamp', True)) + +def Cryoscope_long_plotfn(Time, + Frequencies, + Data, + Center_freqs, + Trace, + timestamp, + qubit, + qubit_freq, + filter_pars=None, + ax=None, **kw): + fig = ax.get_figure() + # Spectroscopy plot + if Time[-1] > 2000: + _Time = Time/1e3 + else: + _Time = Time + ax.pcolormesh(_Time, Frequencies*1e-9, Data, shading='nearest') + ax.plot(_Time, Center_freqs*1e-9, 'w-', lw=1) + axt = ax.twinx() + _lim = ax.get_ylim() + _lim = (qubit_freq*1e-9-np.array(_lim))*1e3 + axt.set_ylim(_lim) + axt.set_ylabel('Detuning (MHz)') + # ax.set_xlabel('Time (ns)') + ax.set_ylabel('Frequency (GHz)') + ax.set_title('Spectroscopy of step response') + # Cryoscope trace plot + ax1 = fig.add_subplot(111) + pos = ax1.get_position() + ax1.set_position([pos.x0+1.2, pos.y0, pos.width, pos.height]) + ax1.axhline(1, color='grey', ls='-') + ax1.axhline(1.005, color='grey', ls='--') + ax1.axhline(0.995, color='grey', ls='--') + ax1.axhline(1.001, color='grey', ls=':') + ax1.axhline(0.999, color='grey', ls=':') + if filter_pars is not None: + _x = np.linspace(Time[0], Time[-1], 201) + _x_ = np.linspace(_Time[0], _Time[-1], 201) + if len(filter_pars) == 2: # High pass compenstaion filter + tau = filter_pars[0]*1e6 + ax1.plot(_x_, filter_func_high_pass(_x*1e-9,*filter_pars), 'C1--', + label=f'IIR fit ($\\tau={tau:.1f}\\mu$s)') + else: # low-pass compensation filter + tau = filter_pars[1]*1e9 + ax1.plot(_x_, filter_func(_x*1e-9,*filter_pars), 'C1--', + label=f'IIR fit ($\\tau={tau:.0f}$ns)') + ax1.legend(frameon=False) + ax1.plot(_Time, Trace) + bottom, top = ax1.get_ylim() + bottom = min(.99, bottom) + top = max(1.01, top) + ax1.set_ylim(bottom, top) + ax1.set_xlim(_Time[0], _Time[-1]) + # ax1.set_xlabel('Time (ns)') + ax1.set_ylabel('Normalized amplitude') + ax1.set_title('Reconstructed step response') + if Time[-1] > 2000: + ax.set_xlabel('Time ($\\mu$s)') + ax1.set_xlabel('Time ($\\mu$s)') + else: + ax.set_xlabel('Time (ns)') + ax1.set_xlabel('Time (ns)') + # Fig title + fig.suptitle(f'{timestamp}\n{qubit} long time-scale cryoscope', x=1.1, y=1.15) diff --git a/pycqed/instrument_drivers/meta_instrument/HAL_Device.py b/pycqed/instrument_drivers/meta_instrument/HAL_Device.py index adac254f0a..c54fb9cfe8 100644 --- a/pycqed/instrument_drivers/meta_instrument/HAL_Device.py +++ b/pycqed/instrument_drivers/meta_instrument/HAL_Device.py @@ -2560,65 +2560,69 @@ def measure_cryoscope( double_projections: bool = False, wait_time_flux: int = 0, update_FIRs: bool=False, + update_IIRs: bool=False, waveform_name: str = "square", max_delay=None, twoq_pair=[2, 0], disable_metadata: bool = False, init_buffer=0, + analyze: bool = True, prepare_for_timedomain: bool = True, ): """ Performs a cryoscope experiment to measure the shape of a flux pulse. + Args: qubits (list): a list of two target qubits + times (array): array of measurment times + label (str): used to label the experiment + waveform_name (str {"square", "custom_wf"}) : defines the name of the waveform used in the cryoscope. Valid values are either "square" or "custom_wf" + max_delay {float, "auto"} : determines the delay in the pulse sequence if set to "auto" this is automatically set to the largest pulse duration for the cryoscope. + prepare_for_timedomain (bool): calls self.prepare_for_timedomain on start """ + assert self.ro_acq_weight_type() == 'optimal' + assert not (update_FIRs and update_IIRs), 'Can only either update IIRs or FIRs' + if update_FIRs or update_IIRs: + assert analyze==True, 'Analsis has to run for filter update' if MC is None: MC = self.instr_MC.get_instr() if nested_MC is None: nested_MC = self.instr_nested_MC.get_instr() - for q in qubits: assert q in self.qubits() - Q_idxs = [self.find_instrument(q).cfg_qubit_nr() for q in qubits] - if prepare_for_timedomain: self.prepare_for_timedomain(qubits=qubits) - if max_delay is None: max_delay = 0 else: max_delay = np.max(times) + 40e-9 - Fl_lutmans = [self.find_instrument(q).instr_LutMan_Flux.get_instr() \ for q in qubits] - if waveform_name == "square": Sw_functions = [swf.FLsweep(lutman, lutman.sq_length, waveform_name="square") for lutman in Fl_lutmans] swfs = swf.multi_sweep_function(Sw_functions) - flux_cw = "fl_cw_06" - + flux_cw = "sf_square" elif waveform_name == "custom_wf": Sw_functions = [swf.FLsweep(lutman, lutman.custom_wf_length, waveform_name="custom_wf") for lutman in Fl_lutmans] swfs = swf.multi_sweep_function(Sw_functions) - flux_cw = "fl_cw_05" - + flux_cw = "sf_custom_wf" else: raise ValueError( 'waveform_name "{}" should be either ' @@ -2658,22 +2662,40 @@ def measure_cryoscope( ) MC.set_detector_function(d) label = 'Cryoscope_{}_amps'.format('_'.join(qubits)) - MC.run(label+self.msmt_suffix,disable_snapshot_metadata=disable_metadata) + MC.run(label,disable_snapshot_metadata=disable_metadata) # Run analysis - a = ma2.cv2.multi_qubit_cryoscope_analysis( - label='Cryoscope', - update_FIRs=update_FIRs) + if analyze: + a = ma2.cv2.multi_qubit_cryoscope_analysis( + label='Cryoscope', + update_IIRs=update_IIRs, + update_FIRs=update_FIRs) if update_FIRs: for qubit, fltr in a.proc_data_dict['conv_filters'].items(): lin_dist_kern = self.find_instrument(f'lin_dist_kern_{qubit}') filter_dict = {'params': {'weights': fltr}, 'model': 'FIR', 'real-time': True } lin_dist_kern.filter_model_04(filter_dict) - + elif update_IIRs: + for qubit, fltr in a.proc_data_dict['exponential_filter'].items(): + lin_dist_kern = self.find_instrument(f'lin_dist_kern_{qubit}') + filter_dict = {'params': fltr, + 'model': 'exponential', 'real-time': True } + if fltr['amp'] > 0: + print('Amplitude of filter is positive (overfitting).') + print('Filter not updated.') + return True + else: + # Check wich is the first empty exponential filter + for i in range(4): + _fltr = lin_dist_kern.get(f'filter_model_0{i}') + if _fltr == {}: + lin_dist_kern.set(f'filter_model_0{i}', filter_dict) + return True + else: + print(f'filter_model_0{i} used.') + print('All exponential filter tabs are full. Filter not updated.') return True - - def measure_cryoscope_vs_amp( self, q0: str, @@ -2894,6 +2916,20 @@ def measure_timing_1d_trace( MC.set_sweep_points(latencies) MC.run(mmt_label) + if latency_type == 'flux': + self.tim_flux_latency_0(0) + self.tim_flux_latency_1(0) + self.tim_flux_latency_2(0) + self.prepare_timing() + + if latency_type == 'mw': + self.tim_mw_latency_0(0) + self.tim_mw_latency_1(0) + self.tim_mw_latency_2(0) + self.tim_mw_latency_3(0) + self.tim_mw_latency_4(0) + self.prepare_timing() + a_obj = ma2.Basic1DAnalysis(label=mmt_label) return a_obj diff --git a/pycqed/instrument_drivers/meta_instrument/qubit_objects/HAL_Transmon.py b/pycqed/instrument_drivers/meta_instrument/qubit_objects/HAL_Transmon.py index 8e5df57717..c0b99871d4 100644 --- a/pycqed/instrument_drivers/meta_instrument/qubit_objects/HAL_Transmon.py +++ b/pycqed/instrument_drivers/meta_instrument/qubit_objects/HAL_Transmon.py @@ -3037,7 +3037,7 @@ def measure_flux_frequency_timedomain( fl_lutman.sq_amp(amplitude) out_voltage = fl_lutman.sq_amp()*\ fl_lutman.cfg_awg_channel_amplitude()*\ - fl_lutman.cfg_awg_channel_range()/2 + fl_lutman.cfg_awg_channel_range()/2 # +/- 2.5V, else, 5Vpp if prepare_for_timedomain: self.prepare_for_timedomain() fl_lutman.load_waveforms_onto_AWG_lookuptable() diff --git a/pycqed/measurement/openql_experiments/multi_qubit_oql.py b/pycqed/measurement/openql_experiments/multi_qubit_oql.py index 475aa77f8d..ad331883e3 100644 --- a/pycqed/measurement/openql_experiments/multi_qubit_oql.py +++ b/pycqed/measurement/openql_experiments/multi_qubit_oql.py @@ -608,15 +608,6 @@ def Cryoscope( p = OqlProgram("Cryoscope", platf_cfg) - # FIXME: the variables created here are effectively unused - if cc.upper() == 'CCL': - flux_target = twoq_pair - elif cc.upper() == 'QCC' or cc.upper() == 'CC': - cw_idx = int(flux_cw[-2:]) - flux_cw = 'sf_{}'.format(_def_lm_flux[cw_idx]['name'].lower()) - else: - raise ValueError('CC type not understood: {}'.format(cc)) - k = p.create_kernel("RamZ_X") k.prepz(qubit_idxs[0]) k.barrier([]) # alignment workaround diff --git a/pycqed/qce_utils/__init__.py b/pycqed/qce_utils/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pycqed/qce_utils/analysis_factory/__init__.py b/pycqed/qce_utils/analysis_factory/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pycqed/qce_utils/analysis_factory/factory_transmon_arc_identifier.py b/pycqed/qce_utils/analysis_factory/factory_transmon_arc_identifier.py new file mode 100644 index 0000000000..1d98abfcb6 --- /dev/null +++ b/pycqed/qce_utils/analysis_factory/factory_transmon_arc_identifier.py @@ -0,0 +1,246 @@ +# ------------------------------------------- +# Factory module for constructing transmon-flux-arc identifier analysis. +# ------------------------------------------- +from abc import ABC, abstractmethod +from dataclasses import dataclass, field +from typing import List, Tuple +import warnings +import numpy as np +import matplotlib.transforms as transforms +from scipy.optimize import minimize +from pycqed.qce_utils.custom_exceptions import InterfaceMethodException +from pycqed.qce_utils.analysis_factory.intrf_analysis_factory import IFactoryManager, FigureDetails +from pycqed.qce_utils.analysis_factory.plotting_functionality import ( + construct_subplot, + SubplotKeywordEnum, + LabelFormat, + AxesFormat, + IFigureAxesPair, +) + + +@dataclass(frozen=True) +class Vec2D: + """ + Data class, containing x- and y-coordinate vector. + """ + x: float + y: float + + # region Class Methods + def to_vector(self) -> np.ndarray: + return np.asarray([self.x, self.y]) + + def to_tuple(self) -> Tuple[float, float]: + return self.x, self.y + + @classmethod + def from_vector(cls, vector: np.ndarray) -> 'Vec2D': + return Vec2D( + x=vector[0], + y=vector[1], + ) + + def __add__(self, other): + if isinstance(other, Vec2D): + return Vec2D(x=self.x + other.x, y=self.y + other.y) + raise NotImplemented(f"Addition with anything other than {Vec2D} is not implemented.") + # endregion + + +class IFluxArcIdentifier(ABC): + """ + Interface class, describing properties and get-methods for flux-arc identifier. + """ + + @property + @abstractmethod + def polynomial(self) -> np.polyfit: + """:return: Internally fitted polynomial.""" + raise InterfaceMethodException + + @property + @abstractmethod + def origin(self) -> Vec2D: + """:return: (Flux) arc origin x-y 2D vector.""" + raise InterfaceMethodException + + @abstractmethod + def get_amplitudes_at(self, detuning: float) -> np.ndarray: + """ + Filters only real roots. + :param detuning: detuning (y-value) at which to find the corresponding amplitude roots (x-values). + :return: Amplitudes (x-values) corresponding to desired detuning (y-values). + """ + roots: np.ndarray = (self.polynomial - detuning).roots + return roots[np.isclose(roots.imag, 0)].real + + +@dataclass(frozen=True) +class FluxArcIdentifier(IFluxArcIdentifier): + """ + Data class, containing (AC) flux pulse amplitude vs (Ramsey) frequency detuning. + """ + _amplitude_array: np.ndarray = field(init=True) + _detuning_array: np.ndarray = field(init=True) + _polynomial: np.polyfit = field(init=False) + + # region Class Properties + @property + def amplitudes(self) -> np.ndarray: + return self._amplitude_array + + @property + def detunings(self) -> np.ndarray: + return self._detuning_array + + @property + def polynomial(self) -> np.polyfit: + """:return: Internally fitted polynomial.""" + return self._polynomial + + @property + def origin(self) -> Vec2D: + """:return: (Flux) arc origin x-y 2D vector.""" + _polynomial = self.polynomial + result = minimize(_polynomial, x0=0) + return Vec2D( + x=result.x[0], + y=result.fun, + ) + + # endregion + + # region Class Methods + def __post_init__(self): + object.__setattr__(self, '_polynomial', self._construct_poly_fit( + x=self.amplitudes, + y=self.detunings, + )) + + def get_amplitudes_at(self, detuning: float) -> np.ndarray: + """ + Filters only real roots. + :param detuning: detuning (y-value) at which to find the corresponding amplitude roots (x-values). + :return: Amplitudes (x-values) corresponding to desired detuning (y-values). + """ + roots: np.ndarray = (self.polynomial - detuning).roots + real_roots: np.ndarray = roots[np.isclose(roots.imag, 0)].real + if len(real_roots) == 0: + warnings.warn(**PolynomialRootNotFoundWarning.warning_format(detuning)) + return real_roots + # endregion + + # region Static Class Methods + @staticmethod + def _construct_poly_fit(x: np.ndarray, y: np.ndarray) -> np.poly1d: + """:return: Custom polynomial a*x^4 + b*x^3 + c*x^2 + d*x + 0.""" + # Construct the design matrix including x^4, x^3, x^2, and x^1. + x_stack = np.column_stack((x ** 4, x ** 3, x ** 2, x)) + # Perform the linear least squares fitting + coefficients, residuals, rank, s = np.linalg.lstsq(x_stack, y, rcond=None) + # coefficients are the coefficients for x^4, x^3, x^2, and x^1 term respectively + a, b, c, d = coefficients + return np.poly1d([a, b, c, d, 0]) + # endregion + + +class FluxArcIdentifierAnalysis(IFactoryManager[FluxArcIdentifier]): + + # region Class Methods + def analyse(self, response: FluxArcIdentifier) -> List[FigureDetails]: + """ + Constructs one or multiple (matplotlib) figures from characterization response. + :param response: Characterization response used to construct analysis figures. + :return: Array-like of analysis figures. + """ + fig, ax = self.plot_flux_arc_identifier( + identifier=response, + ) + + return [ + FigureDetails(figure_object=fig, identifier="voltage_to_detuning"), + ] + + # endregion + + # region Static Class Methods + @staticmethod + def format_coefficient(coef): + """Format coefficient into scientific notation with LaTeX exponent format.""" + return f"{coef:.2e}".replace('+0', '^{').replace('-0', '-') + '}' + + @staticmethod + def plot_flux_arc_identifier(identifier: FluxArcIdentifier, **kwargs) -> IFigureAxesPair: + """ + :param identifier: + :param kwargs: + :return: + """ + # Data allocation + nyquist_frequency: float = 1.3e9 # Based on AWG sampling rate of 2.4GHz + roots: np.ndarray = identifier.get_amplitudes_at(detuning=nyquist_frequency) + min_root: float = float(np.min(np.abs(roots))) + high_resolution_amplitudes: np.ndarray = np.linspace(-min_root, min_root, 101) + # Evaluate the fitted polynomial + fitted_polynomial = identifier.polynomial + y_fit = fitted_polynomial(high_resolution_amplitudes) + origin: Vec2D = identifier.origin + + kwargs[SubplotKeywordEnum.LABEL_FORMAT.value] = kwargs.get(SubplotKeywordEnum.LABEL_FORMAT.value, LabelFormat( + x_label='Output voltage [V]', + y_label='Detuning [Hz]', + )) + fig, ax = construct_subplot(**kwargs) + ax.plot( + identifier.amplitudes, + identifier.detunings, + linestyle='none', + marker='o', + ) + ax.plot( + high_resolution_amplitudes, + y_fit, + linestyle='--', + marker='none', + color='k', + ) + ax.axhline(origin.y, linestyle='--', color='lightgrey', zorder=-1) + ax.axvline(origin.x, linestyle='--', color='lightgrey', zorder=-1) + + # Display the polynomial equation in the plot + a, b, c, d, _ = fitted_polynomial.coeffs + formatter = FluxArcIdentifierAnalysis.format_coefficient + equation_text = f"$y = {formatter(a)}x^4 + {formatter(b)}x^3 + {formatter(c)}x^2 + {formatter(d)}x$" + ax.text(0.5, 0.95, equation_text, transform=ax.transAxes, ha='center', va='top') + + ylim = ax.get_ylim() + # Draw horizontal line to indicate asymmetry + desired_detuning: float = 500e6 + roots: np.ndarray = identifier.get_amplitudes_at(detuning=desired_detuning) + if roots.size > 0: + negative_root = float(roots[roots <= 0]) + negative_arc_x = negative_root + negative_arc_y = fitted_polynomial(negative_arc_x) + positive_arc_x = -negative_arc_x + positive_arc_y = fitted_polynomial(positive_arc_x) + # Draw comparison lines + color: str = 'green' + ax.hlines(y=negative_arc_y, xmin=min(high_resolution_amplitudes), xmax=origin.x, linestyle='--', color=color, zorder=-1) + ax.hlines(y=positive_arc_y, xmin=origin.x, xmax=positive_arc_x, linestyle='--', color=color, zorder=-1) + ax.vlines(x=negative_arc_x, ymin=ylim[0], ymax=negative_arc_y, linestyle='--', color=color, zorder=-1) + # Draw annotations + ax.annotate('', xy=(origin.x, positive_arc_y), xytext=(origin.x, negative_arc_y), arrowprops=dict(arrowstyle="<->", color=color)) + delta: float = abs(positive_arc_y - negative_arc_y) + arrow_y_position: float = min(positive_arc_y, negative_arc_y) + delta / 2 + text_y_position: float = max(positive_arc_y * 1.05, arrow_y_position) + ax.text(origin.x, text_y_position, f' $\Delta={delta * 1e-6:.2f}$ MHz', ha='left', va='center') + ax.text(negative_arc_x, negative_arc_y, f' {desired_detuning * 1e-6:.0f} MHz at {negative_arc_x:.2f} V', ha='left', va='bottom') + # Draw origin offset + transform = transforms.blended_transform_factory(ax.transAxes, ax.transData) + ax.text(0.98, origin.y, f'{origin.y * 1e-6:.3f} MHz', ha='right', va='bottom', transform=transform) + + ax.set_xlim(left=min(high_resolution_amplitudes), right=max(high_resolution_amplitudes)) + ax.set_ylim(ylim) + return fig, ax + # endregion diff --git a/pycqed/qce_utils/analysis_factory/intrf_analysis_factory.py b/pycqed/qce_utils/analysis_factory/intrf_analysis_factory.py new file mode 100644 index 0000000000..309d94f17c --- /dev/null +++ b/pycqed/qce_utils/analysis_factory/intrf_analysis_factory.py @@ -0,0 +1,42 @@ +# ------------------------------------------- +# Module containing interface for analysis factory components. +# ------------------------------------------- +from abc import ABC, abstractmethod, ABCMeta +from dataclasses import dataclass, field +from typing import TypeVar, Dict, Type, List, Generic, Union +import logging +from enum import Enum, unique +import matplotlib.pyplot as plt +from pycqed.qce_utils.custom_exceptions import ( + InterfaceMethodException, +) + + +# Set up basic configuration for logging +logging.basicConfig(level=logging.WARNING, format='%(levelname)s:%(message)s') + + +T = TypeVar('T', bound=Type) + + +@dataclass(frozen=True) +class FigureDetails: + figure_object: plt.Figure + identifier: str + + +class IFactoryManager(ABC, Generic[T], metaclass=ABCMeta): + """ + Interface class, describing methods for manager factories. + """ + + # region Class Methods + @abstractmethod + def analyse(self, response: T) -> List[FigureDetails]: + """ + Constructs one or multiple (matplotlib) figures from characterization response. + :param response: Characterization response used to construct analysis figures. + :return: Array-like of analysis figures. + """ + raise InterfaceMethodException + # endregion diff --git a/pycqed/qce_utils/analysis_factory/plotting_functionality.py b/pycqed/qce_utils/analysis_factory/plotting_functionality.py new file mode 100644 index 0000000000..5be8178b19 --- /dev/null +++ b/pycqed/qce_utils/analysis_factory/plotting_functionality.py @@ -0,0 +1,280 @@ +# ------------------------------------------- +# General plotting functionality. +# ------------------------------------------- +from abc import abstractmethod +from collections.abc import Iterable as ABCIterable +from typing import Callable, Tuple, Optional, Iterable, List, Union +import matplotlib.pyplot as plt +import numpy as np +from enum import Enum +from pycqed.qce_utils.custom_exceptions import InterfaceMethodException + +IFigureAxesPair = Tuple[plt.Figure, plt.Axes] +KEYWORD_LABEL_FORMAT = 'label_format' +KEYWORD_AXES_FORMAT = 'axes_format' +KEYWORD_HOST_AXES = 'host_axes' + + +class IAxesFormat: + """ + Interface for applying formatting changes to axis. + """ + # region Interface Methods + @abstractmethod + def apply_to_axes(self, axes: plt.Axes) -> plt.Axes: + """ + Applies axes formatting settings to axis. + :param axes: Axes to be formatted. + :return: Updated Axes. + """ + raise InterfaceMethodException + # endregion + + # region Static Class Methods + @staticmethod + @abstractmethod + def default() -> 'IAxesFormat': + """:return: Default formatting instance.""" + raise InterfaceMethodException + # endregion + + +class LabelFormat(IAxesFormat): + """ + Specifies callable formatting functions for both vector components. + """ + IFormatCall = Callable[[float], str] + _default_format: IFormatCall = lambda x: f'{round(x)}' + _default_label: str = 'Default Label [a.u.]' + _default_symbol: str = 'X' + + # region Class Properties + @property + def x_label(self) -> str: + """:return: Unit label for x-vector component.""" + return self._x_label + + @property + def y_label(self) -> str: + """:return: Unit label for y-vector component.""" + return self._y_label + + @property + def z_label(self) -> str: + """:return: Unit label for z-vector component.""" + return self._z_label + + @property + def x_format(self) -> IFormatCall: + """:return: Formatting function of x-vector component.""" + return self._x_format + + @property + def y_format(self) -> IFormatCall: + """:return: Formatting function of y-vector component.""" + return self._y_format + + @property + def z_format(self) -> IFormatCall: + """:return: Formatting function of z-vector component.""" + return self._z_format + + @property + def x_symbol(self) -> str: + """:return: Unit symbol for x-vector component.""" + return self._x_symbol + + @property + def y_symbol(self) -> str: + """:return: Unit symbol for y-vector component.""" + return self._y_symbol + + @property + def z_symbol(self) -> str: + """:return: Unit symbol for z-vector component.""" + return self._z_symbol + # endregion + + # region Class Constructor + def __init__( + self, + x_label: str = _default_label, + y_label: str = _default_label, + z_label: str = _default_label, + x_format: IFormatCall = _default_format, + y_format: IFormatCall = _default_format, + z_format: IFormatCall = _default_format, + x_symbol: str = _default_symbol, + y_symbol: str = _default_symbol, + z_symbol: str = _default_symbol + ): + self._x_label: str = x_label + self._y_label: str = y_label + self._z_label: str = z_label + self._x_format: LabelFormat.IFormatCall = x_format + self._y_format: LabelFormat.IFormatCall = y_format + self._z_format: LabelFormat.IFormatCall = z_format + self._x_symbol: str = x_symbol + self._y_symbol: str = y_symbol + self._z_symbol: str = z_symbol + # endregion + + # region Interface Methods + def apply_to_axes(self, axes: plt.Axes) -> plt.Axes: + """ + Applies label formatting settings to axis. + :param axes: Axes to be formatted. + :return: Updated Axes. + """ + axes.set_xlabel(self.x_label) + axes.set_ylabel(self.y_label) + if hasattr(axes, 'set_zlabel'): + axes.set_zlabel(self.z_label) + return axes + # endregion + + # region Static Class Methods + @staticmethod + def default() -> 'LabelFormat': + """:return: Default LabelFormat instance.""" + return LabelFormat() + # endregion + + +class AxesFormat(IAxesFormat): + """ + Specifies general axis formatting functions. + """ + + # region Interface Methods + def apply_to_axes(self, axes: plt.Axes) -> plt.Axes: + """ + Applies axes formatting settings to axis. + :param axes: Axes to be formatted. + :return: Updated Axes. + """ + axes.grid(True, alpha=0.5, linestyle='dashed') # Adds dashed gridlines + axes.set_axisbelow(True) # Puts grid on background + return axes + # endregion + + # region Static Class Methods + @staticmethod + def default() -> 'AxesFormat': + """:return: Default AxesFormat instance.""" + return AxesFormat() + # endregion + + +class EmptyAxesFormat(AxesFormat): + """ + Overwrites AxesFormat with 'null' functionality. + Basically leaving the axes unchanged. + """ + + # region Interface Methods + def apply_to_axes(self, axes: plt.Axes) -> plt.Axes: + """ + Applies axes formatting settings to axis. + :param axes: Axes to be formatted. + :return: Updated Axes. + """ + return axes + # endregion + + +class SubplotKeywordEnum(Enum): + """ + Constructs specific enumerator for construct_subplot() method accepted keyword arguments. + """ + LABEL_FORMAT = 'label_format' + AXES_FORMAT = 'axes_format' + HOST_AXES = 'host_axes' + PROJECTION = 'projection' + FIGURE_SIZE = 'figsize' + + +# TODO: Extend (or add) functionality to construct mosaic plots +def construct_subplot(*args, **kwargs) -> IFigureAxesPair: + """ + Extends plt.subplots() by optionally working from host_axes + and applying label- and axes formatting. + :param args: Positional arguments that are passed to plt.subplots() method. + :param kwargs: Key-word arguments that are passed to plt.subplots() method. + :keyword label_format: (Optional) Formatting settings for figure labels. + :keyword axes_format: (Optional) Formatting settings for figure axes. + :keyword host_axes: (Optional) figure-axes pair to which to write the plot instead. + If not supplied, create a new figure-axes pair. + :return: Tuple of plotted figure and axis. + """ + # Kwarg retrieval + label_format: IAxesFormat = kwargs.pop(SubplotKeywordEnum.LABEL_FORMAT.value, LabelFormat.default()) + axes_format: IAxesFormat = kwargs.pop(SubplotKeywordEnum.AXES_FORMAT.value, AxesFormat.default()) + host_axes: Tuple[plt.Figure, plt.Axes] = kwargs.pop(SubplotKeywordEnum.HOST_AXES.value, None) + projection: Optional[str] = kwargs.pop(SubplotKeywordEnum.PROJECTION.value, None) + + # Figure and axis + if host_axes is not None: + fig, ax0 = host_axes + else: + fig, ax0 = plt.subplots(*args, **kwargs) + + # region Dress Axes + axes: Iterable[plt.Axes] = [ax0] if not isinstance(ax0, ABCIterable) else ax0 + for _ax in axes: + _ax = label_format.apply_to_axes(axes=_ax) + _ax = axes_format.apply_to_axes(axes=_ax) + # endregion + + return fig, ax0 + + +def draw_object_summary(host: IFigureAxesPair, params: object, apply_tight_layout: bool = True) -> IFigureAxesPair: + """ + Adds text window with fit summary based on model parameter. + :param host: Tuple of figure and axis. + :param params: Any object (or model parameter container class) that implements .__str__() method. + :param apply_tight_layout: (Optional) Boolean, whether a tight layout call should be applied to figure. + :return: Tuple of plotted figure and axis. + """ + + def linebreaks_to_columns(source: List[str], column: int, column_spacing: int) -> str: + """ + Attempts to insert tab spacing between source elements to create the visual illusion of columns. + :param source: Array-like of string elements to be placed in column-like structure. + :param column: Integer number of (maximum) columns. + :param column_spacing: Integer column spacing in character units. + :return: Single string with tabs to create column-like behaviour. + """ + # Data allocation + source_count: int = len(source) + desired_count: int = -(source_count // -column) * column # 'Upside down' floor division. + pad_count: int = desired_count - source_count + padded_source: List[str] = source + [''] * pad_count + slice_idx: List[Tuple[int, int]] = [(i * column, (i + 1) * column) for i in range(desired_count // column)] + result: str = '' + for i, (lb, ub) in enumerate(slice_idx): + row_elems = padded_source[lb: ub] + linebreak: str = '' if i == len(slice_idx) - 1 else '\t\n' # Only linebreak if there is another line coming + result += ('\t'.join(row_elems) + linebreak).expandtabs(tabsize=column_spacing) + return result + + fig, ax0 = host + text_str: str = params.__str__() + fontsize: int = 10 + ax0.text( + x=1.05, + y=0.99, + s=text_str, + fontdict=dict(horizontalalignment='left'), + transform=ax0.transAxes, + fontsize=fontsize, + verticalalignment='top', + horizontalalignment='left', + bbox=dict(boxstyle='round', facecolor='#C5C5C5', alpha=0.5), + linespacing=1.6, + ) + if apply_tight_layout: + fig.tight_layout() + + return fig, ax0 diff --git a/pycqed/qce_utils/control_interfaces/__init__.py b/pycqed/qce_utils/control_interfaces/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pycqed/qce_utils/control_interfaces/connectivity_surface_code.py b/pycqed/qce_utils/control_interfaces/connectivity_surface_code.py new file mode 100644 index 0000000000..fecdd8beaa --- /dev/null +++ b/pycqed/qce_utils/control_interfaces/connectivity_surface_code.py @@ -0,0 +1,783 @@ +# ------------------------------------------- +# Module containing implementation of surface-code connectivity structure. +# ------------------------------------------- +from dataclasses import dataclass, field +import warnings +from typing import List, Union, Dict, Tuple +from enum import Enum, unique, auto +from pycqed.qce_utils.definitions import SingletonABCMeta +from pycqed.qce_utils.custom_exceptions import ElementNotIncludedException +from pycqed.qce_utils.control_interfaces.intrf_channel_identifier import ( + IChannelIdentifier, + IFeedlineID, + IQubitID, + IEdgeID, + FeedlineIDObj, + QubitIDObj, + EdgeIDObj, +) +from pycqed.qce_utils.control_interfaces.intrf_connectivity_surface_code import ( + ISurfaceCodeLayer, + IParityGroup, + ParityType, +) +from pycqed.qce_utils.control_interfaces.intrf_connectivity import ( + IDeviceLayer +) + + +@unique +class FrequencyGroup(Enum): + LOW = auto() + MID = auto() + HIGH = auto() + + +@dataclass(frozen=True) +class FrequencyGroupIdentifier: + """ + Data class, representing (qubit) frequency group identifier. + """ + _id: FrequencyGroup + + # region Class Properties + @property + def id(self) -> FrequencyGroup: + """:return: Self identifier.""" + return self._id + # endregion + + # region Class Methods + def is_equal_to(self, other: 'FrequencyGroupIdentifier') -> bool: + """:return: Boolean, whether other frequency group identifier is equal self.""" + return self.id == other.id + + def is_higher_than(self, other: 'FrequencyGroupIdentifier') -> bool: + """:return: Boolean, whether other frequency group identifier is 'lower' than self.""" + # Guard clause, if frequency groups are equal, return False + if self.is_equal_to(other): + return False + if self.id == FrequencyGroup.MID and other.id == FrequencyGroup.LOW: + return True + if self.id == FrequencyGroup.HIGH: + return True + return False + + def is_lower_than(self, other: 'FrequencyGroupIdentifier') -> bool: + """:return: Boolean, whether other frequency group identifier is 'higher' than self.""" + # Guard clause, if frequency groups are equal, return False + if self.is_equal_to(other): + return False + if self.is_higher_than(other): + return False + return True + # endregion + + +@dataclass(frozen=True) +class DirectionalEdgeIDObj(EdgeIDObj, IEdgeID): + """ + Data class, implementing IEdgeID interface. + Overwrites __hash__ and __eq__ to make qubit-to-qubit direction relevant. + """ + + # region Class Methods + def __hash__(self): + """ + Sorts individual qubit hashes such that the order is NOT maintained. + Making hash comparison independent of order. + """ + return hash((self.qubit_id0.__hash__(), self.qubit_id1.__hash__())) + + def __eq__(self, other): + if isinstance(other, DirectionalEdgeIDObj): + # Edge is equal if they share the same qubit identifiers, order does not matter + return other.__hash__() == self.__hash__() + if isinstance(other, EdgeIDObj): + warnings.warn(message=f"Comparing directional edge to non-directional edge returns False by default.") + return False + return False + # endregion + + +@dataclass(frozen=True) +class ParityGroup(IParityGroup): + """ + Data class, implementing IParityGroup interface. + """ + _parity_type: ParityType = field(init=True) + """X or Z type stabilizer.""" + _ancilla_qubit: IQubitID = field(init=True) + """Ancilla qubit.""" + _data_qubits: List[IQubitID] = field(init=True) + """Data qubits.""" + _edges: List[IEdgeID] = field(init=False) + """Edges between ancilla and data qubits.""" + + # region Interface Properties + @property + def parity_type(self) -> ParityType: + """:return: Parity type (X or Z type stabilizer).""" + return self._parity_type + + @property + def ancilla_id(self) -> IQubitID: + """:return: (Main) ancilla-qubit-ID from parity.""" + return self._ancilla_qubit + + @property + def data_ids(self) -> List[IQubitID]: + """:return: (All) data-qubit-ID's from parity.""" + return self._data_qubits + + @property + def edge_ids(self) -> List[IEdgeID]: + """:return: (All) edge-ID's between ancilla and data qubit-ID's.""" + return self._edges + # endregion + + # region Interface Methods + def contains(self, element: Union[IQubitID, IEdgeID]) -> bool: + """:return: Boolean, whether element is part of parity group or not.""" + if element in self.data_ids: + return True + if element in self.edge_ids: + return True + if element == self.ancilla_id: + return True + return False + # endregion + + # region Class Methods + def __post_init__(self): + edges: List[IEdgeID] = [ + EdgeIDObj( + qubit_id0=self.ancilla_id, + qubit_id1=data_qubit_id, + ) + for data_qubit_id in self.data_ids + ] + object.__setattr__(self, '_edges', edges) + # endregion + + +@dataclass(frozen=True) +class FluxDanceLayer: + """ + Data class, containing directional gates played during 'flux-dance' layer. + """ + _edge_ids: List[IEdgeID] + """Non-directional edges, part of flux-dance layer.""" + + # region Class Properties + @property + def qubit_ids(self) -> List[IQubitID]: + """:return: All qubit-ID's.""" + return list(set([qubit_id for edge in self.edge_ids for qubit_id in edge.qubit_ids])) + + @property + def edge_ids(self) -> List[IEdgeID]: + """:return: Array-like of directional edge identifiers, specific for this flux dance.""" + return self._edge_ids + # endregion + + # region Class Methods + def contains(self, element: Union[IQubitID, IEdgeID]) -> bool: + """:return: Boolean, whether element is part of flux-dance layer or not.""" + if element in self.qubit_ids: + return True + if element in self.edge_ids: + return True + return False + + def get_involved_edge(self, qubit_id: IQubitID) -> IEdgeID: + """:return: Edge in which qubit-ID is involved. If qubit-ID not part of self, raise error.""" + for edge in self.edge_ids: + if edge.contains(element=qubit_id): + return edge + raise ElementNotIncludedException(f'Element {qubit_id} is not part of self ({self}) and cannot be part of an edge.') + + def get_spectating_qubit_ids(self, device_layer: IDeviceLayer) -> List[IQubitID]: + """:return: Direct spectator (nearest neighbor) to qubit-ID's participating in flux-dance.""" + participating_qubit_ids: List[IQubitID] = self.qubit_ids + nearest_neighbor_ids: List[IQubitID] = [neighbor_id for qubit_id in participating_qubit_ids for neighbor_id in device_layer.get_neighbors(qubit_id, order=1)] + filtered_nearest_neighbor_ids: List[IQubitID] = list(set([qubit_id for qubit_id in nearest_neighbor_ids if qubit_id not in participating_qubit_ids])) + return filtered_nearest_neighbor_ids + + def requires_parking(self, qubit_id: IQubitID, device_layer: ISurfaceCodeLayer) -> bool: + """ + Determines whether qubit-ID is required to park based on participation in flux dance and frequency group. + :return: Boolean, whether qubit-ID requires some form of parking. + """ + spectating_qubit_ids: List[IQubitID] = self.get_spectating_qubit_ids(device_layer=device_layer) + # Guard clause, if qubit-ID does not spectate the flux-dance, no need for parking + if qubit_id not in spectating_qubit_ids: + return False + # Check if qubit-ID requires parking based on its frequency group ID and active two-qubit gates. + frequency_group: FrequencyGroupIdentifier = device_layer.get_frequency_group_identifier(element=qubit_id) + # Parking is required if any neighboring qubit from a higher frequency group is part of an edge. + neighboring_qubit_ids: List[IQubitID] = device_layer.get_neighbors(qubit=qubit_id, order=1) + involved_neighbors: List[IQubitID] = [qubit_id for qubit_id in neighboring_qubit_ids if self.contains(qubit_id)] + involved_frequency_groups: List[FrequencyGroupIdentifier] = [device_layer.get_frequency_group_identifier(element=qubit_id) for qubit_id in involved_neighbors] + return any([neighbor_frequency_group.is_higher_than(frequency_group) for neighbor_frequency_group in involved_frequency_groups]) + # endregion + + + +@dataclass(frozen=True) +class VirtualPhaseIdentifier(IChannelIdentifier): + """ + Data class, describing (code-word) identifier for virtual phase. + """ + _id: str + + # region Interface Properties + @property + def id(self) -> str: + """:returns: Reference Identifier.""" + return self._id + # endregion + + # region Interface Methods + def __hash__(self): + """:returns: Identifiable hash.""" + return self._id.__hash__() + + def __eq__(self, other): + """:returns: Boolean if other shares equal identifier, else InterfaceMethodException.""" + if isinstance(other, VirtualPhaseIdentifier): + return self.id.__eq__(other.id) + return False + # endregion + + +@dataclass(frozen=True) +class FluxOperationIdentifier(IChannelIdentifier): + """ + Data class, describing (code-word) identifier for flux operation. + """ + _id: str + + # region Interface Properties + @property + def id(self) -> str: + """:returns: Reference Identifier.""" + return self._id + # endregion + + # region Interface Methods + def __hash__(self): + """:returns: Identifiable hash.""" + return self._id.__hash__() + + def __eq__(self, other): + """:returns: Boolean if other shares equal identifier, else InterfaceMethodException.""" + if isinstance(other, FluxOperationIdentifier): + return self.id.__eq__(other.id) + return False + # endregion + + +class Surface17Layer(ISurfaceCodeLayer, metaclass=SingletonABCMeta): + """ + Singleton class, implementing ISurfaceCodeLayer interface to describe a surface-17 layout. + """ + _feedline_qubit_lookup: Dict[IFeedlineID, List[IQubitID]] = { + FeedlineIDObj('FL1'): [QubitIDObj('D9'), QubitIDObj('D8'), QubitIDObj('X4'), QubitIDObj('Z4'), QubitIDObj('Z2'), QubitIDObj('D6')], + FeedlineIDObj('FL2'): [QubitIDObj('D3'), QubitIDObj('D7'), QubitIDObj('D2'), QubitIDObj('X3'), QubitIDObj('Z1'), QubitIDObj('X2'), QubitIDObj('Z3'), QubitIDObj('D5'), QubitIDObj('D4')], + FeedlineIDObj('FL3'): [QubitIDObj('D1'), QubitIDObj('X1')], + } + _qubit_edges: List[IEdgeID] = [ + EdgeIDObj(QubitIDObj('D1'), QubitIDObj('Z1')), + EdgeIDObj(QubitIDObj('D1'), QubitIDObj('X1')), + EdgeIDObj(QubitIDObj('D2'), QubitIDObj('X1')), + EdgeIDObj(QubitIDObj('D2'), QubitIDObj('Z1')), + EdgeIDObj(QubitIDObj('D2'), QubitIDObj('X2')), + EdgeIDObj(QubitIDObj('D3'), QubitIDObj('X2')), + EdgeIDObj(QubitIDObj('D3'), QubitIDObj('Z2')), + EdgeIDObj(QubitIDObj('D4'), QubitIDObj('Z3')), + EdgeIDObj(QubitIDObj('D4'), QubitIDObj('X3')), + EdgeIDObj(QubitIDObj('D4'), QubitIDObj('Z1')), + EdgeIDObj(QubitIDObj('D5'), QubitIDObj('Z1')), + EdgeIDObj(QubitIDObj('D5'), QubitIDObj('X3')), + EdgeIDObj(QubitIDObj('D5'), QubitIDObj('Z4')), + EdgeIDObj(QubitIDObj('D5'), QubitIDObj('X2')), + EdgeIDObj(QubitIDObj('D6'), QubitIDObj('X2')), + EdgeIDObj(QubitIDObj('D6'), QubitIDObj('Z4')), + EdgeIDObj(QubitIDObj('D6'), QubitIDObj('Z2')), + EdgeIDObj(QubitIDObj('D7'), QubitIDObj('Z3')), + EdgeIDObj(QubitIDObj('D7'), QubitIDObj('X3')), + EdgeIDObj(QubitIDObj('D8'), QubitIDObj('X3')), + EdgeIDObj(QubitIDObj('D8'), QubitIDObj('X4')), + EdgeIDObj(QubitIDObj('D8'), QubitIDObj('Z4')), + EdgeIDObj(QubitIDObj('D9'), QubitIDObj('Z4')), + EdgeIDObj(QubitIDObj('D9'), QubitIDObj('X4')), + ] + _parity_group_x: List[IParityGroup] = [ + ParityGroup( + _parity_type=ParityType.STABILIZER_X, + _ancilla_qubit=QubitIDObj('X1'), + _data_qubits=[QubitIDObj('D1'), QubitIDObj('D2')] + ), + ParityGroup( + _parity_type=ParityType.STABILIZER_X, + _ancilla_qubit=QubitIDObj('X2'), + _data_qubits=[QubitIDObj('D2'), QubitIDObj('D3'), QubitIDObj('D5'), QubitIDObj('D6')] + ), + ParityGroup( + _parity_type=ParityType.STABILIZER_X, + _ancilla_qubit=QubitIDObj('X3'), + _data_qubits=[QubitIDObj('D4'), QubitIDObj('D5'), QubitIDObj('D7'), QubitIDObj('D8')] + ), + ParityGroup( + _parity_type=ParityType.STABILIZER_X, + _ancilla_qubit=QubitIDObj('X4'), + _data_qubits=[QubitIDObj('D8'), QubitIDObj('D9')] + ), + ] + _parity_group_z: List[IParityGroup] = [ + ParityGroup( + _parity_type=ParityType.STABILIZER_Z, + _ancilla_qubit=QubitIDObj('Z1'), + _data_qubits=[QubitIDObj('D1'), QubitIDObj('D2'), QubitIDObj('D4'), QubitIDObj('D5')] + ), + ParityGroup( + _parity_type=ParityType.STABILIZER_Z, + _ancilla_qubit=QubitIDObj('Z2'), + _data_qubits=[QubitIDObj('D3'), QubitIDObj('D6')] + ), + ParityGroup( + _parity_type=ParityType.STABILIZER_Z, + _ancilla_qubit=QubitIDObj('Z3'), + _data_qubits=[QubitIDObj('D4'), QubitIDObj('D7')] + ), + ParityGroup( + _parity_type=ParityType.STABILIZER_Z, + _ancilla_qubit=QubitIDObj('Z4'), + _data_qubits=[QubitIDObj('D5'), QubitIDObj('D6'), QubitIDObj('D8'), QubitIDObj('D9')] + ), + ] + _frequency_group_lookup: Dict[IQubitID, FrequencyGroupIdentifier] = { + QubitIDObj('D1'): FrequencyGroupIdentifier(_id=FrequencyGroup.LOW), + QubitIDObj('D2'): FrequencyGroupIdentifier(_id=FrequencyGroup.LOW), + QubitIDObj('D3'): FrequencyGroupIdentifier(_id=FrequencyGroup.LOW), + QubitIDObj('D4'): FrequencyGroupIdentifier(_id=FrequencyGroup.HIGH), + QubitIDObj('D5'): FrequencyGroupIdentifier(_id=FrequencyGroup.HIGH), + QubitIDObj('D6'): FrequencyGroupIdentifier(_id=FrequencyGroup.HIGH), + QubitIDObj('D7'): FrequencyGroupIdentifier(_id=FrequencyGroup.LOW), + QubitIDObj('D8'): FrequencyGroupIdentifier(_id=FrequencyGroup.LOW), + QubitIDObj('D9'): FrequencyGroupIdentifier(_id=FrequencyGroup.LOW), + QubitIDObj('Z1'): FrequencyGroupIdentifier(_id=FrequencyGroup.MID), + QubitIDObj('Z2'): FrequencyGroupIdentifier(_id=FrequencyGroup.MID), + QubitIDObj('Z3'): FrequencyGroupIdentifier(_id=FrequencyGroup.MID), + QubitIDObj('Z4'): FrequencyGroupIdentifier(_id=FrequencyGroup.MID), + QubitIDObj('X1'): FrequencyGroupIdentifier(_id=FrequencyGroup.MID), + QubitIDObj('X2'): FrequencyGroupIdentifier(_id=FrequencyGroup.MID), + QubitIDObj('X3'): FrequencyGroupIdentifier(_id=FrequencyGroup.MID), + QubitIDObj('X4'): FrequencyGroupIdentifier(_id=FrequencyGroup.MID), + } + + # region ISurfaceCodeLayer Interface Properties + @property + def parity_group_x(self) -> List[IParityGroup]: + """:return: (All) parity groups part of X-stabilizers.""" + return self._parity_group_x + + @property + def parity_group_z(self) -> List[IParityGroup]: + """:return: (All) parity groups part of Z-stabilizers.""" + return self._parity_group_z + # endregion + + # region Class Properties + @property + def feedline_ids(self) -> List[IFeedlineID]: + """:return: All feedline-ID's.""" + return list(self._feedline_qubit_lookup.keys()) + + @property + def qubit_ids(self) -> List[IQubitID]: + """:return: All qubit-ID's.""" + return [qubit_id for qubit_ids in self._feedline_qubit_lookup.values() for qubit_id in qubit_ids] + + @property + def edge_ids(self) -> List[IEdgeID]: + """:return: All edge-ID's.""" + return self._qubit_edges + # endregion + + # region ISurfaceCodeLayer Interface Methods + def get_parity_group(self, element: Union[IQubitID, IEdgeID]) -> IParityGroup: + """:return: Parity group of which element (edge- or qubit-ID) is part of.""" + # Assumes element is part of only a single parity group + for parity_group in self.parity_group_x + self.parity_group_z: + if parity_group.contains(element=element): + return parity_group + raise ElementNotIncludedException(f"Element: {element} is not included in any parity group.") + # endregion + + # region IDeviceLayer Interface Methods + def get_connected_qubits(self, feedline: IFeedlineID) -> List[IQubitID]: + """:return: Qubit-ID's connected to feedline-ID.""" + # Guard clause, if feedline not in lookup, raise exception + if feedline not in self._feedline_qubit_lookup: + raise ElementNotIncludedException(f"Element: {feedline} is not included in any feedline group.") + return self._feedline_qubit_lookup[feedline] + + def get_neighbors(self, qubit: IQubitID, order: int = 1) -> List[IQubitID]: + """ + Requires :param order: to be higher or equal to 1. + :return: qubit neighbors separated by order. (order=1, nearest neighbors). + """ + if order > 1: + raise NotImplementedError("Apologies, so far there has not been a use for. But feel free to implement.") + edges: List[IEdgeID] = self.get_edges(qubit=qubit) + result: List[IQubitID] = [] + for edge in edges: + result.append(edge.get_connected_qubit_id(element=qubit)) + return result + + def get_edges(self, qubit: IQubitID) -> List[IEdgeID]: + """:return: All qubit-to-qubit edges from qubit-ID.""" + result: List[IEdgeID] = [] + for edge in self.edge_ids: + if edge.contains(element=qubit): + result.append(edge) + return result + + def contains(self, element: Union[IFeedlineID, IQubitID, IEdgeID]) -> bool: + """:return: Boolean, whether element is part of device layer or not.""" + if element in self.feedline_ids: + return True + if element in self.qubit_ids: + return True + if element in self.edge_ids: + return True + return False + + def get_frequency_group_identifier(self, element: IQubitID) -> FrequencyGroupIdentifier: + """:return: Frequency group identifier based on qubit-ID.""" + return self._frequency_group_lookup[element] + # endregion + + +class Repetition9Layer(ISurfaceCodeLayer, metaclass=SingletonABCMeta): + """ + Singleton class, implementing ISurfaceCodeLayer interface to describe a repetition-9 layout. + """ + _parity_group_x: List[IParityGroup] = [ + ParityGroup( + _parity_type=ParityType.STABILIZER_X, + _ancilla_qubit=QubitIDObj('X1'), + _data_qubits=[QubitIDObj('D2'), QubitIDObj('D1')] + ), + ParityGroup( + _parity_type=ParityType.STABILIZER_X, + _ancilla_qubit=QubitIDObj('X2'), + _data_qubits=[QubitIDObj('D2'), QubitIDObj('D3')] + ), + ParityGroup( + _parity_type=ParityType.STABILIZER_X, + _ancilla_qubit=QubitIDObj('X3'), + _data_qubits=[QubitIDObj('D8'), QubitIDObj('D7')] + ), + ParityGroup( + _parity_type=ParityType.STABILIZER_X, + _ancilla_qubit=QubitIDObj('X4'), + _data_qubits=[QubitIDObj('D9'), QubitIDObj('D8')] + ), + ] + _parity_group_z: List[IParityGroup] = [ + ParityGroup( + _parity_type=ParityType.STABILIZER_Z, + _ancilla_qubit=QubitIDObj('Z1'), + _data_qubits=[QubitIDObj('D4'), QubitIDObj('D5')] + ), + ParityGroup( + _parity_type=ParityType.STABILIZER_Z, + _ancilla_qubit=QubitIDObj('Z2'), + _data_qubits=[QubitIDObj('D6'), QubitIDObj('D3')] + ), + ParityGroup( + _parity_type=ParityType.STABILIZER_Z, + _ancilla_qubit=QubitIDObj('Z3'), + _data_qubits=[QubitIDObj('D7'), QubitIDObj('D4')] + ), + ParityGroup( + _parity_type=ParityType.STABILIZER_Z, + _ancilla_qubit=QubitIDObj('Z4'), + _data_qubits=[QubitIDObj('D5'), QubitIDObj('D6')] + ), + ] + _virtual_phase_lookup: Dict[DirectionalEdgeIDObj, VirtualPhaseIdentifier] = { + DirectionalEdgeIDObj(QubitIDObj('D1'), QubitIDObj('Z1')): VirtualPhaseIdentifier('vcz_virtual_q_ph_corr_NE'), + DirectionalEdgeIDObj(QubitIDObj('Z1'), QubitIDObj('D1')): VirtualPhaseIdentifier('vcz_virtual_q_ph_corr_SW'), + DirectionalEdgeIDObj(QubitIDObj('D1'), QubitIDObj('X1')): VirtualPhaseIdentifier('vcz_virtual_q_ph_corr_SE'), + DirectionalEdgeIDObj(QubitIDObj('X1'), QubitIDObj('D1')): VirtualPhaseIdentifier('vcz_virtual_q_ph_corr_NW'), + DirectionalEdgeIDObj(QubitIDObj('D2'), QubitIDObj('X1')): VirtualPhaseIdentifier('vcz_virtual_q_ph_corr_SW'), + DirectionalEdgeIDObj(QubitIDObj('X1'), QubitIDObj('D2')): VirtualPhaseIdentifier('vcz_virtual_q_ph_corr_NE'), + DirectionalEdgeIDObj(QubitIDObj('D2'), QubitIDObj('Z1')): VirtualPhaseIdentifier('vcz_virtual_q_ph_corr_NW'), + DirectionalEdgeIDObj(QubitIDObj('Z1'), QubitIDObj('D2')): VirtualPhaseIdentifier('vcz_virtual_q_ph_corr_SE'), + DirectionalEdgeIDObj(QubitIDObj('D2'), QubitIDObj('X2')): VirtualPhaseIdentifier('vcz_virtual_q_ph_corr_NE'), + DirectionalEdgeIDObj(QubitIDObj('X2'), QubitIDObj('D2')): VirtualPhaseIdentifier('vcz_virtual_q_ph_corr_SW'), + DirectionalEdgeIDObj(QubitIDObj('D3'), QubitIDObj('X2')): VirtualPhaseIdentifier('vcz_virtual_q_ph_corr_NW'), + DirectionalEdgeIDObj(QubitIDObj('X2'), QubitIDObj('D3')): VirtualPhaseIdentifier('vcz_virtual_q_ph_corr_SE'), + DirectionalEdgeIDObj(QubitIDObj('D3'), QubitIDObj('Z2')): VirtualPhaseIdentifier('vcz_virtual_q_ph_corr_NE'), + DirectionalEdgeIDObj(QubitIDObj('Z2'), QubitIDObj('D3')): VirtualPhaseIdentifier('vcz_virtual_q_ph_corr_SW'), + DirectionalEdgeIDObj(QubitIDObj('D4'), QubitIDObj('Z3')): VirtualPhaseIdentifier('vcz_virtual_q_ph_corr_NW'), + DirectionalEdgeIDObj(QubitIDObj('Z3'), QubitIDObj('D4')): VirtualPhaseIdentifier('vcz_virtual_q_ph_corr_SE'), + DirectionalEdgeIDObj(QubitIDObj('D4'), QubitIDObj('X3')): VirtualPhaseIdentifier('vcz_virtual_q_ph_corr_NE'), + DirectionalEdgeIDObj(QubitIDObj('X3'), QubitIDObj('D4')): VirtualPhaseIdentifier('vcz_virtual_q_ph_corr_SW'), + DirectionalEdgeIDObj(QubitIDObj('D4'), QubitIDObj('Z1')): VirtualPhaseIdentifier('vcz_virtual_q_ph_corr_SE'), + DirectionalEdgeIDObj(QubitIDObj('Z1'), QubitIDObj('D4')): VirtualPhaseIdentifier('vcz_virtual_q_ph_corr_NW'), + DirectionalEdgeIDObj(QubitIDObj('D5'), QubitIDObj('Z1')): VirtualPhaseIdentifier('vcz_virtual_q_ph_corr_SW'), + DirectionalEdgeIDObj(QubitIDObj('Z1'), QubitIDObj('D5')): VirtualPhaseIdentifier('vcz_virtual_q_ph_corr_NE'), + DirectionalEdgeIDObj(QubitIDObj('D5'), QubitIDObj('X3')): VirtualPhaseIdentifier('vcz_virtual_q_ph_corr_NW'), + DirectionalEdgeIDObj(QubitIDObj('X3'), QubitIDObj('D5')): VirtualPhaseIdentifier('vcz_virtual_q_ph_corr_SE'), + DirectionalEdgeIDObj(QubitIDObj('D5'), QubitIDObj('Z4')): VirtualPhaseIdentifier('vcz_virtual_q_ph_corr_NE'), + DirectionalEdgeIDObj(QubitIDObj('Z4'), QubitIDObj('D5')): VirtualPhaseIdentifier('vcz_virtual_q_ph_corr_SW'), + DirectionalEdgeIDObj(QubitIDObj('D5'), QubitIDObj('X2')): VirtualPhaseIdentifier('vcz_virtual_q_ph_corr_SE'), + DirectionalEdgeIDObj(QubitIDObj('X2'), QubitIDObj('D5')): VirtualPhaseIdentifier('vcz_virtual_q_ph_corr_NW'), + DirectionalEdgeIDObj(QubitIDObj('D6'), QubitIDObj('X2')): VirtualPhaseIdentifier('vcz_virtual_q_ph_corr_SW'), + DirectionalEdgeIDObj(QubitIDObj('X2'), QubitIDObj('D6')): VirtualPhaseIdentifier('vcz_virtual_q_ph_corr_NE'), + DirectionalEdgeIDObj(QubitIDObj('D6'), QubitIDObj('Z4')): VirtualPhaseIdentifier('vcz_virtual_q_ph_corr_NW'), + DirectionalEdgeIDObj(QubitIDObj('Z4'), QubitIDObj('D6')): VirtualPhaseIdentifier('vcz_virtual_q_ph_corr_SE'), + DirectionalEdgeIDObj(QubitIDObj('D6'), QubitIDObj('Z2')): VirtualPhaseIdentifier('vcz_virtual_q_ph_corr_SE'), + DirectionalEdgeIDObj(QubitIDObj('Z2'), QubitIDObj('D6')): VirtualPhaseIdentifier('vcz_virtual_q_ph_corr_NW'), + DirectionalEdgeIDObj(QubitIDObj('D7'), QubitIDObj('Z3')): VirtualPhaseIdentifier('vcz_virtual_q_ph_corr_SW'), + DirectionalEdgeIDObj(QubitIDObj('Z3'), QubitIDObj('D7')): VirtualPhaseIdentifier('vcz_virtual_q_ph_corr_NE'), + DirectionalEdgeIDObj(QubitIDObj('D7'), QubitIDObj('X3')): VirtualPhaseIdentifier('vcz_virtual_q_ph_corr_SE'), + DirectionalEdgeIDObj(QubitIDObj('X3'), QubitIDObj('D7')): VirtualPhaseIdentifier('vcz_virtual_q_ph_corr_NW'), + DirectionalEdgeIDObj(QubitIDObj('D8'), QubitIDObj('X3')): VirtualPhaseIdentifier('vcz_virtual_q_ph_corr_SW'), + DirectionalEdgeIDObj(QubitIDObj('X3'), QubitIDObj('D8')): VirtualPhaseIdentifier('vcz_virtual_q_ph_corr_NE'), + DirectionalEdgeIDObj(QubitIDObj('D8'), QubitIDObj('X4')): VirtualPhaseIdentifier('vcz_virtual_q_ph_corr_NE'), + DirectionalEdgeIDObj(QubitIDObj('X4'), QubitIDObj('D8')): VirtualPhaseIdentifier('vcz_virtual_q_ph_corr_SW'), + DirectionalEdgeIDObj(QubitIDObj('D8'), QubitIDObj('Z4')): VirtualPhaseIdentifier('vcz_virtual_q_ph_corr_SE'), + DirectionalEdgeIDObj(QubitIDObj('Z4'), QubitIDObj('D8')): VirtualPhaseIdentifier('vcz_virtual_q_ph_corr_NW'), + DirectionalEdgeIDObj(QubitIDObj('D9'), QubitIDObj('Z4')): VirtualPhaseIdentifier('vcz_virtual_q_ph_corr_SW'), + DirectionalEdgeIDObj(QubitIDObj('Z4'), QubitIDObj('D9')): VirtualPhaseIdentifier('vcz_virtual_q_ph_corr_NE'), + DirectionalEdgeIDObj(QubitIDObj('D9'), QubitIDObj('X4')): VirtualPhaseIdentifier('vcz_virtual_q_ph_corr_NW'), + DirectionalEdgeIDObj(QubitIDObj('X4'), QubitIDObj('D9')): VirtualPhaseIdentifier('vcz_virtual_q_ph_corr_SE'), + } + _flux_dances: List[Tuple[FluxDanceLayer, FluxOperationIdentifier]] = [ + ( + FluxDanceLayer( + _edge_ids=[ + EdgeIDObj(QubitIDObj('X1'), QubitIDObj('D1')), + EdgeIDObj(QubitIDObj('Z1'), QubitIDObj('D4')), + EdgeIDObj(QubitIDObj('X3'), QubitIDObj('D7')), + EdgeIDObj(QubitIDObj('Z2'), QubitIDObj('D6')), + ] + ), + FluxOperationIdentifier(_id='repetition_code_1') + ), + ( + FluxDanceLayer( + _edge_ids=[ + EdgeIDObj(QubitIDObj('X1'), QubitIDObj('D2')), + EdgeIDObj(QubitIDObj('Z1'), QubitIDObj('D5')), + EdgeIDObj(QubitIDObj('X3'), QubitIDObj('D8')), + EdgeIDObj(QubitIDObj('Z2'), QubitIDObj('D3')), + ] + ), + FluxOperationIdentifier(_id='repetition_code_2') + ), + ( + FluxDanceLayer( + _edge_ids=[ + EdgeIDObj(QubitIDObj('Z3'), QubitIDObj('D7')), + EdgeIDObj(QubitIDObj('X4'), QubitIDObj('D8')), + EdgeIDObj(QubitIDObj('Z4'), QubitIDObj('D5')), + EdgeIDObj(QubitIDObj('X2'), QubitIDObj('D2')), + ] + ), + FluxOperationIdentifier(_id='repetition_code_3') + ), + ( + FluxDanceLayer( + _edge_ids=[ + EdgeIDObj(QubitIDObj('Z3'), QubitIDObj('D4')), + EdgeIDObj(QubitIDObj('X4'), QubitIDObj('D9')), + EdgeIDObj(QubitIDObj('Z4'), QubitIDObj('D6')), + EdgeIDObj(QubitIDObj('X2'), QubitIDObj('D3')), + ] + ), + FluxOperationIdentifier(_id='repetition_code_4') + ), + ] + + # region ISurfaceCodeLayer Interface Properties + @property + def parity_group_x(self) -> List[IParityGroup]: + """:return: (All) parity groups part of X-stabilizers.""" + return self._parity_group_x + + @property + def parity_group_z(self) -> List[IParityGroup]: + """:return: (All) parity groups part of Z-stabilizers.""" + return self._parity_group_z + # endregion + + # region Class Properties + @property + def feedline_ids(self) -> List[IFeedlineID]: + """:return: All feedline-ID's.""" + return Surface17Layer().feedline_ids + + @property + def qubit_ids(self) -> List[IQubitID]: + """:return: All qubit-ID's.""" + return Surface17Layer().qubit_ids + + @property + def edge_ids(self) -> List[IEdgeID]: + """:return: All edge-ID's.""" + return Surface17Layer().edge_ids + # endregion + + # region ISurfaceCodeLayer Interface Methods + def get_parity_group(self, element: Union[IQubitID, IEdgeID]) -> IParityGroup: + """:return: Parity group of which element (edge- or qubit-ID) is part of.""" + # Assumes element is part of only a single parity group + for parity_group in self.parity_group_x + self.parity_group_z: + if parity_group.contains(element=element): + return parity_group + raise ElementNotIncludedException(f"Element: {element} is not included in any parity group.") + # endregion + + # region IGateDanceLayer Interface Methods + def get_flux_dance_at_round(self, index: int) -> FluxDanceLayer: + """:return: Flux-dance object based on round index.""" + try: + flux_dance_layer: FluxDanceLayer = self._flux_dances[index] + return flux_dance_layer + except: + raise ElementNotIncludedException(f"Index: {index} is out of bounds for flux dance of length: {len(self._flux_dances)}.") + # endregion + + # region IDeviceLayer Interface Methods + def get_connected_qubits(self, feedline: IFeedlineID) -> List[IQubitID]: + """:return: Qubit-ID's connected to feedline-ID.""" + return Surface17Layer().get_connected_qubits(feedline=feedline) + + def get_neighbors(self, qubit: IQubitID, order: int = 1) -> List[IQubitID]: + """ + Requires :param order: to be higher or equal to 1. + :return: qubit neighbors separated by order. (order=1, nearest neighbors). + """ + return Surface17Layer().get_neighbors(qubit=qubit, order=order) + + def get_edges(self, qubit: IQubitID) -> List[IEdgeID]: + """:return: All qubit-to-qubit edges from qubit-ID.""" + return Surface17Layer().get_edges(qubit=qubit) + + def contains(self, element: Union[IFeedlineID, IQubitID, IEdgeID]) -> bool: + """:return: Boolean, whether element is part of device layer or not.""" + return Surface17Layer().contains(element=element) + # endregion + + # region Class Methods + def _get_flux_dance_layer(self, element: IEdgeID) -> FluxDanceLayer: + """:return: Flux-dance layer of which edge element is part of.""" + # Assumes element is part of only a single flux-dance layer + for flux_dance_layer, _ in self._flux_dances: + if flux_dance_layer.contains(element=element): + return flux_dance_layer + raise ElementNotIncludedException(f"Element: {element} is not included in any flux-dance layer.") + + def _get_flux_operation_identifier(self, element: IEdgeID) -> FluxOperationIdentifier: + """:return: Identifier describing flux-dance layer.""" + for flux_dance_layer, flux_operation_identifier in self._flux_dances: + if flux_dance_layer.contains(element=element): + return flux_operation_identifier + raise ElementNotIncludedException(f"Element: {element} is not included in any flux-dance layer.") + + + def get_flux_operation_identifier(self, qubit_id0: str, qubit_id1: str) -> str: + """:return: Identifier describing flux-dance layer.""" + edge: IEdgeID = EdgeIDObj( + qubit_id0=QubitIDObj(_id=qubit_id0), + qubit_id1=QubitIDObj(_id=qubit_id1), + ) + return self._get_flux_operation_identifier(element=edge).id + + def get_edge_flux_operation_identifier(self, ancilla_qubit: str) -> List[str]: + """:return: Identifier describing flux-dance layer.""" + qubit_id: IQubitID = QubitIDObj(_id=ancilla_qubit) + parity_group: IParityGroup = self.get_parity_group(element=qubit_id) + return [ + self._get_flux_operation_identifier( + element=edge_id, + ).id + for edge_id in parity_group.edge_ids + ] + + def _get_virtual_phase_identifier(self, directional_edge: DirectionalEdgeIDObj) -> VirtualPhaseIdentifier: + """:return: Identifier for virtual phase correction. Based on element and parity group.""" + return self._virtual_phase_lookup[directional_edge] + + def get_virtual_phase_identifier(self, from_qubit: str, to_qubit: str) -> VirtualPhaseIdentifier: + """:return: Identifier for virtual phase correction. Based on element and parity group.""" + directional_edge: DirectionalEdgeIDObj = DirectionalEdgeIDObj( + qubit_id0=QubitIDObj(_id=from_qubit), + qubit_id1=QubitIDObj(_id=to_qubit), + ) + return self._get_virtual_phase_identifier(directional_edge=directional_edge) + + def get_ancilla_virtual_phase_identifier(self, ancilla_qubit: str) -> str: + """:return: Arbitrary virtual phase from ancilla used in parity group.""" + qubit_id: IQubitID = QubitIDObj(_id=ancilla_qubit) + parity_group: IParityGroup = self.get_parity_group(element=qubit_id) + directional_edge: DirectionalEdgeIDObj = DirectionalEdgeIDObj( + qubit_id0=parity_group.ancilla_id, + qubit_id1=parity_group.data_ids[0], + ) + return self._get_virtual_phase_identifier(directional_edge=directional_edge).id + + def get_data_virtual_phase_identifiers(self, ancilla_qubit: str) -> List[str]: + """:return: Arbitrary virtual phase from ancilla used in parity group.""" + qubit_id: IQubitID = QubitIDObj(_id=ancilla_qubit) + parity_group: IParityGroup = self.get_parity_group(element=qubit_id) + return [ + self._get_virtual_phase_identifier( + directional_edge=DirectionalEdgeIDObj( + qubit_id0=data_id, + qubit_id1=parity_group.ancilla_id, + ) + ).id + for data_id in parity_group.data_ids + ] + + def get_parity_data_identifier(self, ancilla_qubit: str) -> List[str]: + """ + Iterates over provided ancilla qubit ID's. + Construct corresponding IQubitID's. + Obtain corresponding IParityGroup's. + Flatten list of (unique) data qubit ID's part of these parity groups. + :return: Array-like of (unique) data qubit ID's part of ancilla qubit parity groups. + """ + ancilla_qubit_id: IQubitID = QubitIDObj(ancilla_qubit) + parity_group: IParityGroup = self.get_parity_group(element=ancilla_qubit_id) + data_qubit_ids: List[IQubitID] = [qubit_id for qubit_id in parity_group.data_ids] + return [qubit_id.id for qubit_id in data_qubit_ids] + + def get_parity_data_identifiers(self, ancilla_qubits: List[str]) -> List[str]: + """ + Iterates over provided ancilla qubit ID's. + Construct corresponding IQubitID's. + Obtain corresponding IParityGroup's. + Flatten list of (unique) data qubit ID's part of these parity groups. + :return: Array-like of (unique) data qubit ID's part of ancilla qubit parity groups. + """ + return [unique_qubit_id for ancilla_qubit in ancilla_qubits for unique_qubit_id in set(self.get_parity_data_identifier(ancilla_qubit=ancilla_qubit))] + + def get_frequency_group_identifier(self, element: IQubitID) -> FrequencyGroupIdentifier: + """:return: Frequency group identifier based on qubit-ID.""" + return Surface17Layer().get_frequency_group_identifier(element=element) + # endregion + + +if __name__ == '__main__': + + flux_dance_0 = Repetition9Layer().get_flux_dance_at_round(0) + print(flux_dance_0.edge_ids) diff --git a/pycqed/qce_utils/control_interfaces/intrf_channel_identifier.py b/pycqed/qce_utils/control_interfaces/intrf_channel_identifier.py new file mode 100644 index 0000000000..bdf6ffd944 --- /dev/null +++ b/pycqed/qce_utils/control_interfaces/intrf_channel_identifier.py @@ -0,0 +1,297 @@ +# ------------------------------------------- +# Interface for unique channel references +# For example: +# Qubit identifier, Feedline identifier, Flux channel identifier, etc. +# ------------------------------------------- +from abc import ABCMeta, abstractmethod, ABC +from dataclasses import dataclass, field +from typing import List, Dict +from pycqed.qce_utils.custom_exceptions import InterfaceMethodException, IsolatedGroupException + +QID = str # Might become int in future +QName = str + + +class IChannelIdentifier(ABC): + """ + Interface class, describing unique identifier. + """ + + # region Interface Properties + @property + @abstractmethod + def id(self) -> str: + """:returns: Reference Identifier.""" + raise InterfaceMethodException + # endregion + + # region Interface Methods + @abstractmethod + def __hash__(self): + """:returns: Identifiable hash.""" + raise InterfaceMethodException + + @abstractmethod + def __eq__(self, other): + """:returns: Boolean if other shares equal identifier, else InterfaceMethodException.""" + raise InterfaceMethodException + # endregion + + +class IQubitID(IChannelIdentifier, metaclass=ABCMeta): + """ + Interface for qubit reference. + """ + + # region Interface Properties + @property + @abstractmethod + def name(self) -> QName: + """:returns: Reference name for qubit.""" + raise InterfaceMethodException + # endregion + + +class IFeedlineID(IChannelIdentifier, metaclass=ABCMeta): + """ + Interface for feedline reference. + """ + pass + + +class IEdgeID(IChannelIdentifier, metaclass=ABCMeta): + """ + Interface class, for qubit-to-qubit edge reference. + """ + + # region Interface Properties + @property + def qubit_ids(self) -> List[IQubitID]: + """:return: All qubit-ID's.""" + raise InterfaceMethodException + # endregion + + # region Interface Methods + @abstractmethod + def contains(self, element: IQubitID) -> bool: + """:return: Boolean, whether element is part of edge or not.""" + raise InterfaceMethodException + + @abstractmethod + def get_connected_qubit_id(self, element: IQubitID) -> IQubitID: + """:return: Qubit-ID, connected to the other side of this edge.""" + raise InterfaceMethodException + # endregion + + +class IQubitIDGroups(ABC): + """ + Interface class, describing groups of IQubitID's. + """ + + # region Interface Properties + @property + @abstractmethod + def groups(self) -> List[List[IQubitID]]: + """:return: Array-like of grouped (array) IQubitID's.""" + raise InterfaceMethodException + # endregion + + # region Interface Methods + @abstractmethod + def get_group(self, group_member: IQubitID) -> List[IQubitID]: + """ + Returns empty list if group_member not part of this lookup. + :return: Array-like of group members. Including provided group_member. + """ + raise InterfaceMethodException + # endregion + + +@dataclass(frozen=True) +class QubitIDObj(IQubitID): + """ + Contains qubit label ID. + """ + _id: QName + + # region Interface Properties + @property + def id(self) -> QID: + """:returns: Reference ID for qubit.""" + return self._id + + @property + def name(self) -> QName: + """:returns: Reference name for qubit.""" + return self.id + # endregion + + # region Class Methods + def __hash__(self): + """:returns: Identifiable hash.""" + return self.id.__hash__() + + def __eq__(self, other): + """:returns: Boolean if other shares equal identifier, else InterfaceMethodException.""" + if isinstance(other, IQubitID): + return self.id.__eq__(other.id) + # raise NotImplementedError('QubitIDObj equality check to anything other than IQubitID interface is not implemented.') + return False + + def __repr__(self): + return f'{self.id}' + # endregion + + +@dataclass(frozen=True) +class QubitIDGroups(IQubitIDGroups): + """ + Data class, implementing IQubitIDGroups interface. + """ + group_lookup: Dict[IQubitID, int] = field(default_factory=dict) + """Lookup dictionary where each IQubitID is matched to a specific (integer) group identifier.""" + + # region Interface Properties + @property + def groups(self) -> List[List[IQubitID]]: + """:return: Array-like of grouped (array) IQubitID's.""" + return list(self.group_id_to_members.values()) + # endregion + + # region Class Properties + @property + def group_id_to_members(self) -> Dict[int, List[IQubitID]]: + """:return: Intermediate lookup table from group-id to its members.""" + group_lookup: Dict[int, List[IQubitID]] = {} + for qubit_id, group_id in self.group_lookup.items(): + if group_id not in group_lookup: + group_lookup[group_id] = [qubit_id] + else: + group_lookup[group_id].append(qubit_id) + return group_lookup + # endregion + + # region Interface Methods + def get_group(self, group_member: IQubitID) -> List[IQubitID]: + """ + Returns empty list if group_member not part of this lookup. + :return: Array-like of group members. Including provided group_member. + """ + group_id_to_members: Dict[int, List[IQubitID]] = self.group_id_to_members + # Guard clause, if provided group member not in this lookup, return empty list. + if group_member not in self.group_lookup: + return [] + group_id: int = self.group_lookup[group_member] + return group_id_to_members[group_id] + # endregion + + # region Class Methods + def __post_init__(self): + # Verify group member uniqueness. + all_group_members: List[IQubitID] = [qubit_id for group in self.groups for qubit_id in group] + isolated_groups: bool = len(set(all_group_members)) == len(all_group_members) + if not isolated_groups: + raise IsolatedGroupException(f'Expects all group members to be part of a single group.') + + @classmethod + def from_groups(cls, groups: List[List[IQubitID]]) -> 'QubitIDGroups': + """:return: Class method constructor based on list of groups of QUbitID's.""" + group_lookup: Dict[IQubitID, int] = {} + for group_id, group in enumerate(groups): + for qubit_id in group: + if qubit_id in group_lookup: + raise IsolatedGroupException(f'{qubit_id} is already in another group. Requires each group member to be part of only one group.') + group_lookup[qubit_id] = group_id + return QubitIDGroups( + group_lookup=group_lookup, + ) + # endregion + + +@dataclass(frozen=True) +class FeedlineIDObj(IFeedlineID): + """ + Data class, implementing IFeedlineID interface. + """ + name: QID + + # region Interface Properties + @property + def id(self) -> QID: + """:returns: Reference ID for feedline.""" + return self.name + # endregion + + # region Class Methods + def __hash__(self): + return self.id.__hash__() + + def __eq__(self, other): + if isinstance(other, IFeedlineID): + return self.id.__eq__(other.id) + # raise NotImplementedError('FeedlineIDObj equality check to anything other than IFeedlineID interface is not implemented.') + return False + + def __repr__(self): + return f'{self.id}' + # endregion + + +@dataclass(frozen=True) +class EdgeIDObj(IEdgeID): + """ + Data class, implementing IEdgeID interface. + """ + qubit_id0: IQubitID + """Arbitrary edge qubit-ID.""" + qubit_id1: IQubitID + """Arbitrary edge qubit-ID.""" + + # region Interface Properties + @property + def id(self) -> QID: + """:returns: Reference ID for edge.""" + return f"{self.qubit_id0.id}-{self.qubit_id1.id}" + + @property + def qubit_ids(self) -> List[IQubitID]: + """:return: All qubit-ID's.""" + return [self.qubit_id0, self.qubit_id1] + # endregion + + # region Interface Methods + def contains(self, element: IQubitID) -> bool: + """:return: Boolean, whether element is part of edge or not.""" + if element in [self.qubit_id0, self.qubit_id1]: + return True + return False + + def get_connected_qubit_id(self, element: IQubitID) -> IQubitID: + """:return: Qubit-ID, connected to the other side of this edge.""" + if element == self.qubit_id0: + return self.qubit_id1 + if element == self.qubit_id1: + return self.qubit_id0 + # If element is not part of this edge + raise ValueError(f"Element: {element} is not part of this edge: {self}") + # endregion + + # region Class Methods + def __hash__(self): + """ + Sorts individual qubit hashes such that the order is NOT maintained. + Making hash comparison independent of order. + """ + return hash((min(self.qubit_id0.__hash__(), self.qubit_id1.__hash__()), max(self.qubit_id0.__hash__(), self.qubit_id1.__hash__()))) + + def __eq__(self, other): + if isinstance(other, IEdgeID): + # Edge is equal if they share the same qubit identifiers, order does not matter + return other.contains(self.qubit_id0) and other.contains(self.qubit_id1) + # raise NotImplementedError('EdgeIDObj equality check to anything other than IEdgeID interface is not implemented.') + return False + + def __repr__(self): + return f'{self.id}' + # endregion diff --git a/pycqed/qce_utils/control_interfaces/intrf_connectivity.py b/pycqed/qce_utils/control_interfaces/intrf_connectivity.py new file mode 100644 index 0000000000..8730ef06cf --- /dev/null +++ b/pycqed/qce_utils/control_interfaces/intrf_connectivity.py @@ -0,0 +1,145 @@ +# ------------------------------------------- +# Module containing interface for device connectivity structure. +# ------------------------------------------- +from abc import ABC, ABCMeta, abstractmethod +from multipledispatch import dispatch +from typing import List, Tuple, Union +from pycqed.qce_utils.custom_exceptions import InterfaceMethodException +from pycqed.qce_utils.control_interfaces.intrf_channel_identifier import ( + IFeedlineID, + IQubitID, + IEdgeID, +) + + +class IIdentifier(ABC): + """ + Interface class, describing equality identifier method. + """ + + # region Interface Methods + @abstractmethod + def __eq__(self, other): + """:return: Boolean, whether 'other' equals 'self'.""" + raise InterfaceMethodException + # endregion + + +class INode(IIdentifier, metaclass=ABCMeta): + """ + Interface class, describing the node in a connectivity layer. + """ + + # region Interface Properties + @property + @abstractmethod + def edges(self) -> List['IEdge']: + """:return: (N) Edges connected to this node.""" + raise InterfaceMethodException + # endregion + + +class IEdge(IIdentifier, metaclass=ABCMeta): + """ + Interface class, describing a connection between two nodes. + """ + + # region Interface Properties + @property + @abstractmethod + def nodes(self) -> Tuple[INode, INode]: + """:return: (2) Nodes connected by this edge.""" + raise InterfaceMethodException + # endregion + + +class IConnectivityLayer(ABC): + """ + Interface class, describing a connectivity (graph) layer containing nodes and edges. + Note that a connectivity layer can include 'separated' graphs + where not all nodes have a connection path to all other nodes. + """ + + # region Interface Properties + @property + @abstractmethod + def nodes(self) -> List[INode]: + """:return: Array-like of nodes.""" + raise InterfaceMethodException + + @property + @abstractmethod + def edges(self) -> List[IEdge]: + """:return: Array-like of edges.""" + raise InterfaceMethodException + # endregion + + # region Interface Methods + @dispatch(node=INode) + @abstractmethod + def get_connected_nodes(self, node: INode, order: int) -> List[INode]: + """ + :param node: (Root) node to base connectivity on. + If node has no edges, return an empty list. + :param order: Connectivity range. + Order <=0: empty list, 1: first order connectivity, 2: second order connectivity, etc. + :return: Array-like of nodes connected to 'node' within order of connection (excluding 'node' itself). + """ + raise InterfaceMethodException + + @dispatch(edge=IEdge) + @abstractmethod + def get_connected_nodes(self, edge: IEdge, order: int) -> List[INode]: + """ + :param edge: (Root) edge to base connectivity on. + :param order: Connectivity range. + Order <=0: empty list, 1: first order connectivity, 2: second order connectivity, etc. + :return: Array-like of nodes connected to 'edge' within order of connection. + """ + raise InterfaceMethodException + # endregion + + +class IConnectivityStack(ABC): + """ + Interface class, describing an array-like of connectivity layers. + """ + + # region Interface Properties + @property + @abstractmethod + def layers(self) -> List[IConnectivityLayer]: + """:return: Array-like of connectivity layers.""" + raise InterfaceMethodException + # endregion + + +class IDeviceLayer(ABC): + """ + Interface class, describing relation based connectivity. + """ + + # region Interface Methods + @abstractmethod + def get_connected_qubits(self, feedline: IFeedlineID) -> List[IQubitID]: + """:return: Qubit-ID's connected to feedline-ID.""" + raise InterfaceMethodException + + @abstractmethod + def get_neighbors(self, qubit: IQubitID, order: int = 1) -> List[IQubitID]: + """ + Requires :param order: to be higher or equal to 1. + :return: qubit neighbors separated by order. (order=1, nearest neighbors). + """ + raise InterfaceMethodException + + @abstractmethod + def get_edges(self, qubit: IQubitID) -> List[IEdgeID]: + """:return: All qubit-to-qubit edges from qubit-ID.""" + raise InterfaceMethodException + + @abstractmethod + def contains(self, element: Union[IFeedlineID, IQubitID, IEdgeID]) -> bool: + """:return: Boolean, whether element is part of device layer or not.""" + raise InterfaceMethodException + # endregion diff --git a/pycqed/qce_utils/control_interfaces/intrf_connectivity_surface_code.py b/pycqed/qce_utils/control_interfaces/intrf_connectivity_surface_code.py new file mode 100644 index 0000000000..e912546aa0 --- /dev/null +++ b/pycqed/qce_utils/control_interfaces/intrf_connectivity_surface_code.py @@ -0,0 +1,83 @@ +# ------------------------------------------- +# Module containing interface for surface-code connectivity structure. +# ------------------------------------------- +from abc import ABC, ABCMeta, abstractmethod +from typing import List, Union +from enum import Enum +from pycqed.qce_utils.custom_exceptions import InterfaceMethodException +from pycqed.qce_utils.control_interfaces.intrf_channel_identifier import ( + IQubitID, + IEdgeID, +) +from pycqed.qce_utils.control_interfaces.intrf_connectivity import IDeviceLayer + + +class ParityType(Enum): + STABILIZER_X = 0 + STABILIZER_Z = 1 + + +class IParityGroup(ABC): + """ + Interface class, describing qubit (nodes) and edges related to the parity group. + """ + + # region Interface Properties + @property + @abstractmethod + def parity_type(self) -> ParityType: + """:return: Parity type (X or Z type stabilizer).""" + raise InterfaceMethodException + + @property + @abstractmethod + def ancilla_id(self) -> IQubitID: + """:return: (Main) ancilla-qubit-ID from parity.""" + raise InterfaceMethodException + + @property + @abstractmethod + def data_ids(self) -> List[IQubitID]: + """:return: (All) data-qubit-ID's from parity.""" + raise InterfaceMethodException + + @property + @abstractmethod + def edge_ids(self) -> List[IEdgeID]: + """:return: (All) edge-ID's between ancilla and data qubit-ID's.""" + raise InterfaceMethodException + # endregion + + # region Interface Methods + @abstractmethod + def contains(self, element: Union[IQubitID, IEdgeID]) -> bool: + """:return: Boolean, whether element is part of parity group or not.""" + raise InterfaceMethodException + # endregion + + +class ISurfaceCodeLayer(IDeviceLayer, metaclass=ABCMeta): + """ + Interface class, describing surface-code relation based connectivity. + """ + + # region Interface Properties + @property + @abstractmethod + def parity_group_x(self) -> List[IParityGroup]: + """:return: (All) parity groups part of X-stabilizers.""" + raise InterfaceMethodException + + @property + @abstractmethod + def parity_group_z(self) -> List[IParityGroup]: + """:return: (All) parity groups part of Z-stabilizers.""" + raise InterfaceMethodException + # endregion + + # region Interface Methods + @abstractmethod + def get_parity_group(self, element: Union[IQubitID, IEdgeID]) -> IParityGroup: + """:return: Parity group of which element (edge- or qubit-ID) is part of.""" + raise InterfaceMethodException + # endregion diff --git a/pycqed/qce_utils/custom_exceptions.py b/pycqed/qce_utils/custom_exceptions.py new file mode 100644 index 0000000000..ebc5d3c4e0 --- /dev/null +++ b/pycqed/qce_utils/custom_exceptions.py @@ -0,0 +1,245 @@ +# ------------------------------------------- +# Customized exceptions for better maintainability +# ------------------------------------------- +import numpy as np + + +class InterfaceMethodException(Exception): + """ + Raised when the interface method is not implemented. + """ + + +class WeakRefException(Exception): + """ + Raised when weak visa-instance reference is being retrieved which is not available. + """ + + +class ModelParameterException(Exception): + """ + Raised when model-parameter class is being constructed using an inconsistent amount of parameters. + """ + + +class ModelParameterSubClassException(Exception): + """ + Raised when model-parameter does not sub class the expected model-parameter class. + """ + + +class KeyboardFinish(KeyboardInterrupt): + """ + Indicates that the user safely aborts/interrupts terminal process. + """ + + +class IdentifierException(Exception): + """ + Raised when (qubit) identifier is not correctly handled. + """ + + +class InvalidProminenceThresholdException(Exception): + """ + Raised when dynamic prominence threshold for peak detection is inconclusive. + """ + + +class EnumNotDefinedException(Exception): + """ + Raised when undefined enum is detected. + """ + + +class EvaluationException(Exception): + """ + Raised when optimizer parameters have not yet been evaluated. + """ + + +class OverloadSignatureNotDefinedException(Exception): + """ + Raised when overload signature for specific function is not defined or recognized. + Search-keys: overload, dispatch, multipledispatch, type casting. + """ + + +class ArrayShapeInconsistencyException(Exception): + """ + Raised when the shape of arrays are inconsistent or incompatible with each other. + """ + + # region Static Class Methods + @staticmethod + def format_arrays(x: np.ndarray, y: np.ndarray) -> 'ArrayShapeInconsistencyException': + return ArrayShapeInconsistencyException(f'Provided x-y arrays are do not have the same shape: {x.shape} != {y.shape}') + # endregion + + +class ArrayNotComplexException(Exception): + """ + Raised when not all array elements are complex. + """ + + +class StateEvaluationException(Exception): + """ + Raised when state vector evaluation (expression to real float) fails. + """ + + +class StateConditionEvaluationException(Exception): + """ + Raised when state vector condition evaluation fails. + """ + + +class WrapperException(Exception): + """ + Raised any form of exception is needed within wrapper implementation. + """ + + +class InvalidPointerException(Exception): + """ + Raised when file-pointer is invalid (path-to-file does not exist). + """ + + +class SerializationException(Exception): + """ + Raised when there is a problem serializing an object. + """ + + +class HDF5ItemTypeException(Exception): + """ + Raised when type from an item inside hdf5-file group is not recognized. + """ + + +class DataGenerationCompleteException(Exception): + """ + Raised when upper bound of data generation has been reached. + """ + + +class DataInconclusiveException(Exception): + """ + Raised when data is incomplete or inconclusive. + """ + + +class LinspaceBoundaryException(Exception): + """ + Raised when the boundary values of a linear space sampler are identical. + """ + + +class TransmonFrequencyRangeException(Exception): + """ + Raised when frequency falls outside the range of Transmon frequency. + """ + + # region Static Class Methods + @staticmethod + def format_arrays(qubit_max_frequency: float, target_frequency: float) -> 'TransmonFrequencyRangeException': + return TransmonFrequencyRangeException(f'Target frequency value {target_frequency*1e-9:2.f} [GHz] not within qubit frequency range: 0-{qubit_max_frequency*1e-9:2.f} [GHz].') + # endregion + + +class DimensionalityException(Exception): + """ + Raised when dataset dimensionality is unknown or does not match expected. + """ + + +class FactoryRequirementNotSatisfiedException(Exception): + """ + Raised when factory deployment requirement is not satisfied. + """ + + +class NoSamplesToEvaluateException(Exception): + """ + Raised when functionality depending on non-zero number of samples fails. + """ + + # region Static Class Methods + @staticmethod + def format_for_model_driven_agent() -> 'NoSamplesToEvaluateException': + return NoSamplesToEvaluateException(f"Agent can not perform sample evaluation with 0 samples. Ensure to execute 'self.next(state: CoordinateResponsePair)' with at least a single state before requesting model evaluation.") + # endregion + + +class HardwareModuleChannelException(Exception): + """ + Raised when module channel index is out of range. + """ + + +class OperationTypeException(Exception): + """ + Raised when operation type does not correspond to expected type. + """ + + +class RegexGroupException(Exception): + """ + Raised when regex match does not find intended group. + """ + + +class IsolatedGroupException(Exception): + """ + Raised when a list of grouped elements are not isolated. Members from one group are shared in another group. + """ + + +class PeakDetectionException(Exception): + """ + Raised when the number of detected peaks is not sufficient. + """ + + +class FactoryManagerKeyException(Exception): + """ + Raised when the key is not present in the factory-manager components. + """ + + # region Static Class Methods + @staticmethod + def format_log(key, dictionary) -> 'FactoryManagerKeyException': + return FactoryManagerKeyException(f'Provided key: {key} is not present in {dictionary}.') + # endregion + + +class RequestNotSupportedException(FactoryManagerKeyException): + """ + Raised when (measurement) execution request is not support or can not be handled. + """ + + +class IncompleteParameterizationException(Exception): + """ + Raised when operation is not completely parameterized. + """ + + +class ElementNotIncludedException(Exception): + """ + Raised when element (such as IQubitID, IEdgeID or IFeedlineID) is not included in the connectivity layer. + """ + + +class GenericTypeException(Exception): + """ + Raised when generic type is not found or supported. + """ + + # region Static Class Methods + @staticmethod + def format_log(generic_type: type) -> 'GenericTypeException': + return GenericTypeException(f'Generic type : {generic_type} is not supported.') + # endregion diff --git a/pycqed/qce_utils/definitions.py b/pycqed/qce_utils/definitions.py new file mode 100644 index 0000000000..2fa4aa7d74 --- /dev/null +++ b/pycqed/qce_utils/definitions.py @@ -0,0 +1,25 @@ +# ------------------------------------------- +# Project root pointer +# ------------------------------------------- +import os +from abc import ABCMeta +from pathlib import Path +ROOT_DIR = Path(os.path.dirname(os.path.abspath(__file__))).parent.parent.absolute() +CONFIG_DIR = os.path.join(ROOT_DIR, 'data', 'class_configs') +UNITDATA_DIR = os.path.join(ROOT_DIR, 'data', 'unittest_data') +TEMP_DIR = os.path.join(ROOT_DIR, 'data', 'temp') +UI_STYLE_QSS = os.path.join(ROOT_DIR, 'style.qss') +FRAME_DIR = os.path.join(TEMP_DIR, 'frames') + + +class Singleton(type): + _instances = {} + + def __call__(cls, *args, **kwargs): + if cls not in cls._instances: + cls._instances[cls] = super().__call__(*args, **kwargs) + return cls._instances[cls] + + +class SingletonABCMeta(ABCMeta, Singleton): + pass diff --git a/pycqed/qce_utils/measurement_module/__init__.py b/pycqed/qce_utils/measurement_module/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pycqed/qce_utils/module_description.md b/pycqed/qce_utils/module_description.md new file mode 100644 index 0000000000..8b982ee330 --- /dev/null +++ b/pycqed/qce_utils/module_description.md @@ -0,0 +1,14 @@ +Purpose QCE-Utils +=== +This sub-module is a direct port from the standalone QCoExtended repository. +Only well established functionality from the standalone repository is transferred to PycQED. + +- Custom exceptions. Good practice to have a library of custom exceptions, these help identify which exceptions are raised in what situations. The most used one is 'InterfaceMethodException' which is raised if an (ABC) interface abstractmethod is not implemented. + +Control Interfaces +=== +Contains: +- Channel identifier interfaces. These are identifiers for individual qubits, edges and feedlines. +- Connectivity interfaces. These describe building blocks like nodes and edges, but also larger structures like connectivity layers and stacks (multiple layers). Together they combine in the Device layer interface, exposing get methods for relationships between nodes and edges. +- Surface-code specific connectivity interfaces. These extend the connectivity interfaces by exposing surface-code specific terminology like parity groups and (qubit) frequency groups. +- Surface-code connectivity. This implements the above-mentioned interfaces to create a so called 'Surface-17' connectivity layer. This can be used throughout to obtain qubit-to-qubit relations by simply referring to their corresponding identifiers. An example of its use is during multi-qubit experiments which use inter-dependent flux trajectories (like 'flux-dance cycles'). \ No newline at end of file