From 5233d88abbb36429c9a03e5edffe50b41258f0de Mon Sep 17 00:00:00 2001 From: Haggai Landa Date: Mon, 25 Dec 2023 13:00:13 +0200 Subject: [PATCH] Added option to run pulse gates in spam estimation and some reformatting --- project_experiments/experiment_routines.py | 2 + .../library/bayesian/spam_1q_builder.py | 41 +++-- .../library/bayesian/spam_1q_estimator.py | 147 ++++++++++-------- .../library/bayesian/spam_1q_experiment.py | 140 +++++++++++++++-- project_experiments/run-experiment-1.py | 4 +- 5 files changed, 239 insertions(+), 95 deletions(-) diff --git a/project_experiments/experiment_routines.py b/project_experiments/experiment_routines.py index dbda7e9..e5fcabd 100644 --- a/project_experiments/experiment_routines.py +++ b/project_experiments/experiment_routines.py @@ -166,7 +166,9 @@ def run_experiment( n_draws=int(10e6), n_repeats=n_repeats, user_qubit_groups=all_qubits, + b_pulse_gates=False, ) + # b_pulse_gates can be set to True for a more accurate estimation in presence of gate error SPAM_exp = spam_model.build(backend) qubit_estimation_exp = [] diff --git a/project_experiments/library/bayesian/spam_1q_builder.py b/project_experiments/library/bayesian/spam_1q_builder.py index 55634bf..c620789 100644 --- a/project_experiments/library/bayesian/spam_1q_builder.py +++ b/project_experiments/library/bayesian/spam_1q_builder.py @@ -16,7 +16,6 @@ from qiskit_experiments.framework import ( BaseExperiment, ParallelExperiment, - BatchExperiment, ) from project_experiments.partition import partition_qubits @@ -52,6 +51,9 @@ class BayesianSPAMBuilder: transpile_options: Transpile options. distance: The graph distance parameter for parallelizing the qubits. A value of 1 indicates all qubits in parallel, a value of 2 indicates next-nearest-neighbors are parallel, etc. + s_add_suffix: An optional suffix string to add to the experiment results. + b_pulse_gates: Whether to use pulse gates. Important if gate errors are not small + enough, and in order to meaningfully estimate gate errors. """ def __init__( @@ -67,6 +69,8 @@ def __init__( transpile_options: Optional[dict] = None, user_qubit_groups: Optional[Sequence[Sequence[Sequence[int]]]] = None, distance=2, + s_add_suffix: Optional[str] = "", + b_pulse_gates=False, ): if gates is None: gates = BayesianSPAMEstimator.BAYESIAN_CPCMG_GATES @@ -87,35 +91,48 @@ def __init__( self._transpile_options = transpile_options self._user_qubit_groups = user_qubit_groups self._distance = distance + self._s_add_suffix = s_add_suffix + self._b_pulse_gates = b_pulse_gates - def build(self, backend: Backend) -> [BaseExperiment]: + def build(self, backend: Backend, model=None) -> [BaseExperiment]: """Build the batch of parallel experiments according to the qubit groups, constructed according to the requested `distance` parameter of the constructor. Args: backend: The backend whose connectivity is used to parallelize the experiments. + model: An optional BayesianSPAMEstimator to use - if None, one will be created + and initialized. Returns: The experiment for the device. """ - model = BayesianSPAMEstimator( - gates=self.gates, - parameters=self.parameters, - prior_intervals=self.prior_intervals, - n_draws=self.n_draws, - n_x90p_power=self.n_x90p_power, - n_repeats=self.n_repeats, - ) - model.prepare_Bayesian() + if model is None: + model = BayesianSPAMEstimator( + gates=self.gates, + parameters=self.parameters, + prior_intervals=self.prior_intervals, + n_draws=self.n_draws, + n_x90p_power=self.n_x90p_power, + n_repeats=self.n_repeats, + ) + model.prepare_Bayesian() qubit_groups = self._user_qubit_groups or partition_qubits( backend, self._distance ) + faulty_qubits = backend.properties().faulty_qubits() par_exps = [] for group in qubit_groups: exps = [] for qubit in group: - exp = BayesianSPAMExperiment(qubit=qubit[0], model=model) + if qubit[0] in faulty_qubits: + continue + exp = BayesianSPAMExperiment( + qubit=qubit[0], + model=model, + add_suffix=self._s_add_suffix, + b_pulse_gates=self._b_pulse_gates, + ) if self._experiment_options: exp.set_experiment_options(**self._experiment_options) if self._analysis_options: diff --git a/project_experiments/library/bayesian/spam_1q_estimator.py b/project_experiments/library/bayesian/spam_1q_estimator.py index 6b46064..ce1d2b7 100644 --- a/project_experiments/library/bayesian/spam_1q_estimator.py +++ b/project_experiments/library/bayesian/spam_1q_estimator.py @@ -38,6 +38,10 @@ class BayesianSPAMEstimator: "y90p", "y90m", "x90p^2", + "x90p^5", + "x90m^5", + "y90p^5", + "y90m^5", "x90p^4n", "x90p^(4n+1)", ] @@ -58,7 +62,7 @@ class BayesianSPAMEstimator: NUM_SUPPORTED_PARAMS = len(SUPPORTED_PARAMETERS) """The number of parameters that are supported for estimation (as listed in - SUPPORTED_PARAMETERS).""" + SUPPORTED_PARAMETERS).""" PARAM_BOUNDARIES = { "pi_x": (-1.0, 1.0), @@ -72,7 +76,7 @@ class BayesianSPAMEstimator: "theta_x90p": (-np.pi / 4, np.pi / 4), } """Boundaries of the intervals over which the prior (uniform) distribution of each parameter can - be defined.""" + be defined.""" BAYESIAN_QPCM_PARAMETERS = ["x_0", "y_0", "z_0", "pi_z", "pi_0"] """The five parameters for estimation of Quantum Preparation and Classical Measurement errors.""" @@ -85,8 +89,19 @@ class BayesianSPAMEstimator: [0.45, 0.6], ] """Five default parameter priors for estimation corresponding to the parameters defined in - BAYESIAN_QPCM_PARAMETERS. May not be suitable for all devices, if their manifested errors - are too large.""" + BAYESIAN_QPCM_PARAMETERS. May not be suitable for all devices, if their manifested errors + are too large.""" + + BAYESIAN_QPCMp5_PRIORS = [ + [-0.2, 0.2], + [-0.2, 0.2], + [0.82, 1.0], + [0.4, 0.55], + [0.45, 0.6], + ] + """Five default parameter priors for estimation corresponding to the parameters defined in + BAYESIAN_QPCM_PARAMETERS. May not be suitable for all devices, if their manifested errors + are too large.""" BAYESIAN_QPCMG_PARAMETERS = [ "x_0", @@ -98,7 +113,7 @@ class BayesianSPAMEstimator: "theta_x90p", ] """The seven parameters supported for estimation of Quantum Preparation / Classical Measurement - and Gate errors.""" + and Gate errors.""" BAYESIAN_QPCMG_PRIORS = [ [-0.1, 0.1], @@ -110,12 +125,12 @@ class BayesianSPAMEstimator: [-0.01, 0.01], ] """Seven default parameter priors for estimation corresponding to parameters defined in - BAYESIAN_QPCMG_PARAMETERS. May not be suitable for all devices, if their manifested errors - are too large.""" + BAYESIAN_QPCMG_PARAMETERS. May not be suitable for all devices, if their manifested errors + are too large.""" BAYESIAN_CPCMG_PARAMETERS = ["z_0", "pi_z", "pi_0", "epsilon_x90p", "theta_x90p"] """The five parameters supported for estimation of Classical Preparation / Classical Measurement - and Gate errors.""" + and Gate errors.""" BAYESIAN_CPCMG_PRIORS = [ [0.82, 1.0], @@ -125,16 +140,20 @@ class BayesianSPAMEstimator: [-0.02, 0.02], ] """Seven default parameter priors for estimation corresponding to parameters defined in - BAYESIAN_CPCMG_PARAMETERS. May not be suitable for all devices, if their manifested errors - are too large.""" + BAYESIAN_CPCMG_PARAMETERS. May not be suitable for all devices, if their manifested errors + are too large.""" BAYESIAN_DIRECT_GATES = ["id", "x", "x90p", "x90m", "y90p", "y90m"] """The six nonconcatenated gates used for estimation using a Bayesian estimation, without - gate errors.""" + gate errors.""" BAYESIAN_QPCM_GATES = ["id", "x", "x90p", "x90m", "y90p", "y90m"] """The six nonconcatenated gates used for estimation using a Bayesian estimation, without - gate errors.""" + gate errors.""" + + BAYESIAN_QPCMp5_GATES = ["id", "x90p^2", "x90p^5", "x90m^5", "y90p^5", "y90m^5"] + """The six nonconcatenated gates used for estimation using a Bayesian estimation, without + gate errors.""" BAYESIAN_CPCMG_GATES = ["id", "x90p^2", "x90p", "x90m", "x90p^4n", "x90p^(4n+1)"] """The six gates used for estimation using a Bayesian estimation, without gate errors.""" @@ -191,54 +210,6 @@ def __init__( self.n_repeats = n_repeats self.cube = None - def get_1q_circuits(self, qubit) -> List[QuantumCircuit]: - """Return a list of experiment circuits. - - Returns: - A list of :class:`QuantumCircuit`. - - Raises: - Exception: In case of unsupported gates requested. - """ - gates = self.gates - - circuits = [] - pi_2 = np.pi / 2 - for _, s_gate in enumerate(gates): - circ = QuantumCircuit(1, 1) - - if s_gate == "x": - circ.x(qubit) - elif s_gate[0:4] == "x90p": - if s_gate == "x90p": - n_len = 1 - elif s_gate == "x90p^2": - n_len = 2 - elif s_gate == "x90p^4n": - n_len = 4 * self.n_x90p_power - elif s_gate == "x90p^(4n+1)": - n_len = 4 * self.n_x90p_power + 1 - else: - raise Exception(f"Unknown/unsupported instruction {s_gate}.") - for _ in range(n_len): - circ.rx(pi_2, qubit) - elif s_gate == "x90m": - circ.rx(-pi_2, qubit) - elif s_gate == "y90p": - circ.ry(pi_2, qubit) - elif s_gate == "y90m": - circ.ry(pi_2, -qubit) - elif s_gate == "id": - pass - else: - raise Exception(f"Unknown/unsupported instruction {s_gate}.") - circ.measure(0, 0) - - circ.metadata = {"experiment_type": "Bayesian-spam-1q", "qubits": [qubit]} - circuits.append(circ) - - return circuits - def prepare_Bayesian(self): """Precomputes the Monte Carlo cube used for Bayesian estimation, enforcing constraints. With Python being an interpreted language and this computation being a bottle neck of the @@ -468,28 +439,28 @@ def prepare_Bayesian(self): + vals[ipiy] * vals[iy0] + vals[ipiz] * vals[iz0] ) - elif s_gate == "x90p" or s_gate == "x90p^(4n+1)": + elif s_gate == "x90p" or s_gate == "x90p^(4n+1)" or s_gate == "x90p^5": p = ( vals_pi0 + vals[ipix] * vals[ix0] - vals[ipiy] * vals[iz0] + vals[ipiz] * vals[iy0] ) - elif s_gate == "x90m": + elif s_gate == "x90m" or s_gate == "x90m^5": p = ( vals_pi0 + vals[ipix] * vals[ix0] + vals[ipiy] * vals[iz0] - vals[ipiz] * vals[iy0] ) - elif s_gate == "y90p": + elif s_gate == "y90p" or s_gate == "y90p^5": p = ( vals_pi0 + vals[ipix] * vals[iz0] + vals[ipiy] * vals[iy0] - vals[ipiz] * vals[ix0] ) - elif s_gate == "y90m": + elif s_gate == "y90m" or s_gate == "y90m^5": p = ( vals_pi0 - vals[ipix] * vals[iz0] @@ -639,3 +610,51 @@ def estimate_Bayesian( result["cube"] = cube result["P"] = P return result + + # def get_1q_circuits(self, qubit) -> List[QuantumCircuit]: + # """Return a list of experiment circuits. + # + # Returns: + # A list of :class:`QuantumCircuit`. + # + # Raises: + # Exception: In case of unsupported gates requested. + # """ + # gates = self.gates + # + # circuits = [] + # pi_2 = np.pi / 2 + # for _, s_gate in enumerate(gates): + # circ = QuantumCircuit(1, 1) + # + # if s_gate == "x": + # circ.x(qubit) + # elif s_gate[0:4] == "x90p": + # if s_gate == "x90p": + # n_len = 1 + # elif s_gate == "x90p^2": + # n_len = 2 + # elif s_gate == "x90p^4n": + # n_len = 4 * self.n_x90p_power + # elif s_gate == "x90p^(4n+1)": + # n_len = 4 * self.n_x90p_power + 1 + # else: + # raise Exception(f"Unknown/unsupported instruction {s_gate}.") + # for _ in range(n_len): + # circ.rx(pi_2, qubit) + # elif s_gate == "x90m": + # circ.rx(-pi_2, qubit) + # elif s_gate == "y90p": + # circ.ry(pi_2, qubit) + # elif s_gate == "y90m": + # circ.ry(pi_2, -qubit) + # elif s_gate == "id": + # pass + # else: + # raise Exception(f"Unknown/unsupported instruction {s_gate}.") + # circ.measure(0, 0) + # + # circ.metadata = {"experiment_type": "Bayesian-spam-1q", "qubits": [qubit]} + # circuits.append(circ) + # + # return circuits diff --git a/project_experiments/library/bayesian/spam_1q_experiment.py b/project_experiments/library/bayesian/spam_1q_experiment.py index fd27396..7fbb40e 100644 --- a/project_experiments/library/bayesian/spam_1q_experiment.py +++ b/project_experiments/library/bayesian/spam_1q_experiment.py @@ -26,18 +26,33 @@ class BayesianSPAMExperiment(BaseExperiment): """Bayesian estimation experiment for 1Q SPAM and gate errors.""" - def __init__(self, qubit: int, model, backend: Optional[Backend] = None): + def __init__( + self, + qubit: int, + model, + backend: Optional[Backend] = None, + add_suffix="", + b_pulse_gates=False, + ): """Create a new experiment. Args: qubit: Qubit index on which to run estimation. backend: Optional, the backend to run the experiment on. + add_suffix: An optional suffix string to add to the experiment results. + b_pulse_gates: Whether to use pulse gates. Important if gate errors are not small + enough, and in order to meaningfully estimate gate errors. """ - super().__init__([qubit], analysis=BayesianSPAMAnalysis(), backend=backend) + super().__init__( + [qubit], + analysis=BayesianSPAMAnalysis(add_suffix=add_suffix), + backend=backend, + ) self.set_experiment_options( n_x90p_power=model.n_x90p_power, gates=model.gates, n_repeats=model.n_repeats, + b_pulse_gates=b_pulse_gates, ) self.analysis.model = model @@ -46,6 +61,7 @@ def _default_experiment_options(cls) -> Options: options = super()._default_experiment_options() options.n_x90p_power = 0 options.n_repeats = 1 + options.b_pulse_gates = (False,) options.gates = [] return options @@ -68,13 +84,43 @@ def circuits(self) -> List[QuantumCircuit]: options = self.experiment_options gates = options.gates n_x90p_power = options.n_x90p_power + b_pulse_gates = options.b_pulse_gates circuits = [] pi_2 = np.pi / 2 n_repeats = options.n_repeats r_repeats = range(n_repeats) + + sy_gates = [] + sy_schedules = [] + if b_pulse_gates: + sx_schedule = self._backend.defaults().instruction_schedule_map.get( + "sx", self.physical_qubits[0] + ) + sx = ( + sx_schedule.filter(instruction_types=[pulse.Play]) + .instructions[0][1] + .pulse + ) + sy_angles = [-pi_2, pi_2, np.pi] + sy_names = ["sy", "sydg", "sx2dg"] + for i_sy_gate, sy_gate_name in enumerate(sy_names): + sy_gates.append(Gate(sy_gate_name, 1, [])) + with pulse.build() as sy_schedule: + pulse.play( + pulse.Drag( + duration=sx.duration, + amp=sx.amp, + sigma=sx.sigma, + beta=sx.beta, + angle=sx.angle + sy_angles[i_sy_gate], + name=sy_gate_name, + ), + pulse.DriveChannel(self.physical_qubits[0]), + ) + sy_schedules.append(sy_schedule) + for i_gate, s_gate in enumerate(gates): circ = QuantumCircuit(1, 1) - if s_gate == "x": circ.x(0) circ.barrier() @@ -83,6 +129,8 @@ def circuits(self) -> List[QuantumCircuit]: n_len = 1 elif s_gate == "x90p^2": n_len = 2 + elif s_gate == "x90p^5": + n_len = 5 elif s_gate == "x90p^4n": n_len = 4 * n_x90p_power elif s_gate == "x90p^(4n+1)": @@ -90,19 +138,50 @@ def circuits(self) -> List[QuantumCircuit]: else: raise Exception(f"Unknown/unsupported instruction {s_gate}.") for _ in range(n_len): - # circ.rx(pi_2, 0) circ.sx(0) circ.barrier() - elif s_gate == "x90m": - # circ.rx(-pi_2, 0) - circ.sxdg(0) - circ.barrier() - elif s_gate == "y90p": - circ.ry(pi_2, 0) - circ.barrier() - elif s_gate == "y90m": - circ.ry(-pi_2, 0) - circ.barrier() + elif s_gate == "x90m" or s_gate == "x90m^5": + n_len = 1 if s_gate == "x90m" else 5 + for _ in range(n_len): + if b_pulse_gates: + circ.append(sy_gates[2], [0]) + else: + circ.sxdg(0) + circ.barrier() + if b_pulse_gates: + circ.add_calibration( + gate=sy_gates[2], + qubits=self.physical_qubits, + schedule=sy_schedules[2], + ) + elif s_gate == "y90p" or s_gate == "y90p^5": + n_len = 1 if s_gate == "y90p" else 5 + for _ in range(n_len): + if b_pulse_gates: + circ.append(sy_gates[0], [0]) + else: + circ.ry(pi_2, 0) + circ.barrier() + if b_pulse_gates: + circ.add_calibration( + gate=sy_gates[0], + qubits=self.physical_qubits, + schedule=sy_schedules[0], + ) + elif s_gate == "y90m" or s_gate == "y90m^5": + n_len = 1 if s_gate == "y90m" else 5 + for _ in range(n_len): + if b_pulse_gates: + circ.append(sy_gates[1], [0]) + else: + circ.ry(-pi_2, 0) + circ.barrier() + if b_pulse_gates: + circ.add_calibration( + gate=sy_gates[1], + qubits=self.physical_qubits, + schedule=sy_schedules[1], + ) elif s_gate == "id": pass else: @@ -126,13 +205,36 @@ def circuits(self) -> List[QuantumCircuit]: return circuits + # def _transpiled_circuits(self) -> List[QuantumCircuit]: + # """Return a list of experiment circuits, transpiled.""" + # transpile_opts = copy.copy(self.transpile_options.__dict__) + # # transpile_opts["initial_layout"] = list(self.physical_qubits) + # # transpile_opts["optimization_level"] = 1 + # # Transpile level only for this exp. + # transpiled = transpile(self.circuits(), self.backend, **transpile_opts) + # + # fig = Figure() + # _ = FigureCanvasSVG(fig) + # ax = fig.subplots(1, 1, sharex=True) + # transpiled[0].draw("mpl", idle_wires=False, ax=ax) + # # if self.storage is not None: + # # self.storage["transpiled_circuit_figure"] = fig + # + # # For debugging: + # # for i_fig in range(6): + # # fig, ax = plt.subplots(1, 1, sharex=True) + # # transpiled[i_fig].draw("mpl", idle_wires=False, ax=ax) + # # plt.show() + # return transpiled + class BayesianSPAMAnalysis(BaseAnalysis): """Analysis of the Bayesian SPAM experiment.""" - def __init__(self): + def __init__(self, add_suffix=""): super().__init__() self.model = None + self.add_suffix = add_suffix def _run_analysis(self, experiment_data): # Fetch the probabilities for 00 and 11 @@ -173,10 +275,14 @@ def _run_analysis(self, experiment_data): var = vars_dict.get(key, 0) stddev = np.sqrt(var) analysis_results.append( - AnalysisResultData(name=key, value=ufloat(value, stddev)) + AnalysisResultData( + name=key + self.add_suffix, value=ufloat(value, stddev) + ) ) analysis_results.append( - AnalysisResultData(name="Var_P", value=ufloat(result.get("Var_P", 0), 0)) + AnalysisResultData( + name="Var_P" + self.add_suffix, value=ufloat(result.get("Var_P", 0), 0) + ) ) return analysis_results, [] diff --git a/project_experiments/run-experiment-1.py b/project_experiments/run-experiment-1.py index 777c38c..6804aeb 100644 --- a/project_experiments/run-experiment-1.py +++ b/project_experiments/run-experiment-1.py @@ -16,9 +16,9 @@ shots = 1 * 1024 # Number of shots per job -s_backend = "ibm_perth" +s_backend = "ibm_osaka" # Choose the relevant backend -edge_groups_name = "triples" +edge_groups_name = "rings.127.A" # E.g., "pairs" "triples" "rings.27" "rings.127.B" s_init_state = "gr" # The initial state: "+x" for a product of |+>'s, "gr" for a graph state