diff --git a/qiskit_experiments/library/characterization/analysis/cr_hamiltonian_analysis.py b/qiskit_experiments/library/characterization/analysis/cr_hamiltonian_analysis.py index 23cd69063a..3be62756ae 100644 --- a/qiskit_experiments/library/characterization/analysis/cr_hamiltonian_analysis.py +++ b/qiskit_experiments/library/characterization/analysis/cr_hamiltonian_analysis.py @@ -22,11 +22,10 @@ import qiskit_experiments.data_processing as dp from qiskit_experiments.database_service.device_component import Qubit from qiskit_experiments.exceptions import AnalysisError -from qiskit_experiments.framework import AnalysisResultData +from qiskit_experiments.framework import AnalysisResultData, CompositeAnalysis, ExperimentData -# pylint: disable=line-too-long -class CrossResonanceHamiltonianAnalysis(curve.CurveAnalysis): +class TomographyElementAnalysis(curve.CurveAnalysis): r"""A class to analyze cross resonance Hamiltonian tomography experiment. # section: fit_model @@ -36,24 +35,22 @@ class CrossResonanceHamiltonianAnalysis(curve.CurveAnalysis): .. math:: \begin{align} - F_{x, c}(t) &= \frac{1}{\Omega_c^2} \left( - - p_{z, c} p_{x, c} + p_{z, c} p_{x, c} \cos(\Omega_c t') + - \Omega_c p_{y, c} \sin(\Omega_c t') \right) + b \tag{1} \\ - F_{y, c}(t) &= \frac{1}{\Omega_c^2} \left( - p_{z, c} p_{y, c} - p_{z, c} p_{y, c} \cos(\Omega_c t') - - \Omega_c p_{x, c} \sin(\Omega_c t') \right) + b \tag{2} \\ - F_{z, c}(t) &= \frac{1}{\Omega_c^2} \left( - p_{z, c}^2 + (p_{x, c}^2 + p_{y, c}^2) \cos(\Omega_c t') \right) + b \tag{3} + F_x(t) &= \frac{1}{\Omega^2} \left( + - p_z p_x + p_z p_x \cos(\Omega t') + + \Omega p_y \sin(\Omega t') \right) + b \tag{1} \\ + F_y(t) &= \frac{1}{\Omega^2} \left( + p_z p_y - p_x p_y \cos(\Omega t') - + \Omega p_x \sin(\Omega t') \right) + b \tag{2} \\ + F_z(t) &= \frac{1}{\Omega^2} \left( + p_z^2 + (p_x^2 + p_y^2) \cos(\Omega t') \right) + b \tag{3} \end{align} where :math:`t' = t + t_{\rm offset}` with :math:`t` is pulse duration to scan and :math:`t_{\rm offset}` is an extra fit parameter that may represent the edge effect. - The :math:`\Omega_c = \sqrt{p_{x, c}^2+p_{y, c}^2+p_{z, c}^2}` and - :math:`p_{x, c}, p_{y, c}, p_{z, c}, b` are also fit parameters. - The subscript :math:`c` represents the state of control qubit :math:`c \in \{0, 1\}`. - The fit functions :math:`F_{x, c}, F_{y, c}, F_{z, c}` approximate the Pauli expectation - values :math:`\langle \sigma_{x, c} (t) \rangle, \langle \sigma_{y, c} (t) \rangle, - \langle \sigma_{z, c} (t) \rangle` of the target qubit, respectively. + The :math:`\Omega = \sqrt{p_x^2+p_y^2+p_z^2}` and :math:`p_x, p_y, p_z, b` are fit parameters. + The fit functions :math:`F_x, F_y, F_z` approximate the Pauli expectation + values :math:`\langle \sigma_x (t) \rangle, \langle \sigma_y (t) \rangle, + \langle \sigma_z (t) \rangle` of the target qubit, respectively. Based on the fit result, cross resonance Hamiltonian coefficients can be written as @@ -89,33 +86,18 @@ class CrossResonanceHamiltonianAnalysis(curve.CurveAnalysis): .parametric_pulses.GaussianSquare` pulse envelope. bounds: [0, None] - defpar p_{x, 0}: - desc: Fit parameter of oscillations when control qubit state is 0. - init_guess: See fit model section. - bounds: None - - defpar p_{y, 0}: - desc: Fit parameter of oscillations when control qubit state is 0. + defpar p_x: + desc: Fit parameter of oscillations. init_guess: See fit model section. bounds: None - defpar p_{z, 0}: - desc: Fit parameter of oscillations when control qubit state is 0. + defpar p_y: + desc: Fit parameter of oscillations. init_guess: See fit model section. bounds: None - defpar p_{x, 1}: - desc: Fit parameter of oscillations when control qubit state is 1. - init_guess: See fit model section. - bounds: None - - defpar p_{y, 1}: - desc: Fit parameter of oscillations when control qubit state is 1. - init_guess: See fit model section. - bounds: None - - defpar p_{z, 1}: - desc: Fit parameter of oscillations when control qubit state is 1. + defpar p_z: + desc: Fit parameter of oscillations. init_guess: See fit model section. bounds: None @@ -126,70 +108,38 @@ class CrossResonanceHamiltonianAnalysis(curve.CurveAnalysis): bounds: None # section: see_also - qiskit_experiments.library.characterization.cr_hamiltonian.CrossResonanceHamiltonian + qiskit_experiments.library.characterization.CrossResonanceHamiltonian """ + __fixed_parameters__ = ["t_off"] __series__ = [ curve.SeriesDef( - name="x|c=0", - fit_func=lambda x, t_off, px0, px1, py0, py1, pz0, pz1, b: curve.fit_function.bloch_oscillation_x( - x + t_off, px=px0, py=py0, pz=pz0, baseline=b - ), - filter_kwargs={"control_state": 0, "meas_basis": "x"}, - plot_color="blue", - plot_symbol="o", - canvas=0, - ), - curve.SeriesDef( - name="y|c=0", - fit_func=lambda x, t_off, px0, px1, py0, py1, pz0, pz1, b: curve.fit_function.bloch_oscillation_y( - x + t_off, px=px0, py=py0, pz=pz0, baseline=b + name="cr_tomo_x", + fit_func=lambda x, t_off, px, py, pz, b: curve.fit_function.bloch_oscillation_x( + x + t_off, px=px, py=py, pz=pz, baseline=b ), - filter_kwargs={"control_state": 0, "meas_basis": "y"}, - plot_color="blue", - plot_symbol="o", - canvas=1, - ), - curve.SeriesDef( - name="z|c=0", - fit_func=lambda x, t_off, px0, px1, py0, py1, pz0, pz1, b: curve.fit_function.bloch_oscillation_z( - x + t_off, px=px0, py=py0, pz=pz0, baseline=b - ), - filter_kwargs={"control_state": 0, "meas_basis": "z"}, - plot_color="blue", - plot_symbol="o", - canvas=2, - ), - curve.SeriesDef( - name="x|c=1", - fit_func=lambda x, t_off, px0, px1, py0, py1, pz0, pz1, b: curve.fit_function.bloch_oscillation_x( - x + t_off, px=px1, py=py1, pz=pz1, baseline=b - ), - filter_kwargs={"control_state": 1, "meas_basis": "x"}, + filter_kwargs={"meas_basis": "x"}, plot_color="red", - plot_symbol="^", - canvas=0, + plot_symbol="o", ), curve.SeriesDef( - name="y|c=1", - fit_func=lambda x, t_off, px0, px1, py0, py1, pz0, pz1, b: curve.fit_function.bloch_oscillation_y( - x + t_off, px=px1, py=py1, pz=pz1, baseline=b + name="cr_tomo_y", + fit_func=lambda x, t_off, px, py, pz, b: curve.fit_function.bloch_oscillation_y( + x + t_off, px=px, py=py, pz=pz, baseline=b ), - filter_kwargs={"control_state": 1, "meas_basis": "y"}, - plot_color="red", + filter_kwargs={"meas_basis": "y"}, + plot_color="green", plot_symbol="^", - canvas=1, ), curve.SeriesDef( - name="z|c=1", - fit_func=lambda x, t_off, px0, px1, py0, py1, pz0, pz1, b: curve.fit_function.bloch_oscillation_z( - x + t_off, px=px1, py=py1, pz=pz1, baseline=b + name="cr_tomo_z", + fit_func=lambda x, t_off, px, py, pz, b: curve.fit_function.bloch_oscillation_z( + x + t_off, px=px, py=py, pz=pz, baseline=b ), - filter_kwargs={"control_state": 1, "meas_basis": "z"}, - plot_color="red", - plot_symbol="^", - canvas=2, + filter_kwargs={"meas_basis": "z"}, + plot_color="blue", + plot_symbol="x", ), ] @@ -201,113 +151,67 @@ def _default_options(cls): input_key="counts", data_actions=[dp.Probability("1"), dp.BasisExpectationValue()], ) - default_options.curve_plotter = "mpl_multiv_canvas" default_options.xlabel = "Flat top width" default_options.ylabel = ",," default_options.xval_unit = "s" - default_options.style = curve.visualization.PlotterStyle( - figsize=(8, 10), - legend_loc="lower right", - fit_report_rpos=(0.28, -0.10), - ) default_options.ylim = (-1, 1) + default_options.t_off = 0 return default_options - def _t_off_initial_guess(self) -> float: - """Return initial guess for time offset. - - This method assumes the :py:class:`~qiskit.pulse.library.parametric_pulses.GaussianSquare` - envelope with the Gaussian rising and falling edges with the parameter ``sigma``. - - This is intended to be overridden by a child class so that rest of the analysis class - logic can be reused for the fitting that assumes other pulse envelopes. - - Returns: - An initial guess for time offset parameter ``t_off`` in SI units. - - Raises: - AnalysisError: When the backend doesn't report the time resolution of waveforms. - """ - n_pulses = self._extra_metadata().get("n_cr_pulses", 1) - sigma = self._experiment_options().get("sigma", 0) - - # Convert sigma unit into SI - try: - prefactor = self._backend.configuration().dt - except AttributeError as ex: - raise AnalysisError("Backend configuration does not provide time resolution.") from ex - - return np.sqrt(2 * np.pi) * prefactor * sigma * n_pulses - def _generate_fit_guesses( self, user_opt: curve.FitOptions ) -> Union[curve.FitOptions, List[curve.FitOptions]]: """Compute the initial guesses. Args: - user_opt: Fit options filled with user provided guess and bounds. + user_opt: Fit options filled with user provided DICguess and bounds. Returns: List of fit options that are passed to the fitter function. """ - user_opt.bounds.set_if_empty(t_off=(0, np.inf), b=(-1, 1)) - - user_opt.p0.set_if_empty(t_off=self._t_off_initial_guess(), b=1e-9) - - guesses = defaultdict(list) - for control in (0, 1): - x_data = self._data(series_name=f"x|c={control}") - y_data = self._data(series_name=f"y|c={control}") - z_data = self._data(series_name=f"z|c={control}") - - omega_xyz = [] - for data in (x_data, y_data, z_data): - ymin, ymax = np.percentile(data.y, [10, 90]) - if ymax - ymin < 0.2: - # oscillation amplitude might be almost zero, - # then exclude from average because of lower SNR - continue - fft_freq = curve.guess.frequency(data.x, data.y) - omega_xyz.append(fft_freq) - if omega_xyz: - omega = 2 * np.pi * np.average(omega_xyz) - else: - omega = 1e-3 - - zmin, zmax = np.percentile(z_data.y, [10, 90]) - theta = np.arccos(np.sqrt((zmax - zmin) / 2)) - - # The FFT might be up to 1/2 bin off - df = 2 * np.pi / ((z_data.x[1] - z_data.x[0]) * len(z_data.x)) - for omega_shifted in [omega, omega - df / 2, omega + df / 2]: - for phi in np.linspace(-np.pi, np.pi, 5): - px = omega_shifted * np.cos(theta) * np.cos(phi) - py = omega_shifted * np.cos(theta) * np.sin(phi) - pz = omega_shifted * np.sin(theta) - guesses[control].append( - { - f"px{control}": px, - f"py{control}": py, - f"pz{control}": pz, - } - ) - if omega < df: - # empirical guess for low frequency case - guesses[control].append( - { - f"px{control}": omega, - f"py{control}": omega, - f"pz{control}": 0, - } - ) + user_opt.bounds.set_if_empty(b=(-1, 1)) + user_opt.p0.set_if_empty(b=1e-9) + + x_data = self._data(series_name=f"cr_tomo_x") + y_data = self._data(series_name=f"cr_tomo_y") + z_data = self._data(series_name=f"cr_tomo_z") + + omega_xyz = [] + for data in (x_data, y_data, z_data): + ymin, ymax = np.percentile(data.y, [10, 90]) + if ymax - ymin < 0.2: + # oscillation amplitude might be almost zero, + # then exclude from average because of lower SNR + continue + fft_freq = curve.guess.frequency(data.x, data.y) + omega_xyz.append(fft_freq) + if omega_xyz: + omega = 2 * np.pi * np.average(omega_xyz) + else: + omega = 1e-3 + + zmin, zmax = np.percentile(z_data.y, [10, 90]) + theta = np.arccos(np.sqrt((zmax - zmin) / 2)) + + # The FFT might be up to 1/2 bin off + df = 2 * np.pi / ((z_data.x[1] - z_data.x[0]) * len(z_data.x)) fit_options = [] - # combine all guesses in Cartesian product - for p0s, p1s in product(guesses[0], guesses[1]): - new_opt = user_opt.copy() - new_opt.p0.set_if_empty(**p0s, **p1s) - fit_options.append(new_opt) + for omega_shifted in [omega, omega - df / 2, omega + df / 2]: + for phi in np.linspace(-np.pi, np.pi, 5): + new_opt = user_opt.copy() + new_opt.p0.set_if_empty( + px=omega_shifted * np.cos(theta) * np.cos(phi), + py=omega_shifted * np.cos(theta) * np.sin(phi), + pz=omega_shifted * np.sin(theta), + ) + fit_options.append(new_opt) + if omega < df: + # empirical guess for low frequency case + lowf_guess = user_opt.copy() + lowf_guess.p0.set_if_empty(px=omega, py=omega, pz=0) + fit_options.append(lowf_guess) return fit_options @@ -322,28 +226,127 @@ def _evaluate_quality(self, fit_data: curve.FitData) -> Union[str, None]: return "bad" - def _extra_database_entry(self, fit_data: curve.FitData) -> List[AnalysisResultData]: - """Calculate Hamiltonian coefficients from fit values.""" - extra_entries = [] + + +# pylint: disable=line-too-long +class CrossResonanceHamiltonianAnalysis(CompositeAnalysis): + r"""A class to analyze cross resonance Hamiltonian tomography experiment. + + # section: fit_model + The following equations are used to approximate the dynamics of + the target qubit Bloch vector. + + .. math:: + + \begin{align} + F_x(t) &= \frac{1}{\Omega^2} \left( + - p_z p_x + p_z p_x \cos(\Omega t') + + \Omega p_y \sin(\Omega t') \right) + b \tag{1} \\ + F_y(t) &= \frac{1}{\Omega^2} \left( + p_z p_y - p_x p_y \cos(\Omega t') - + \Omega p_x \sin(\Omega t') \right) + b \tag{2} \\ + F_z(t) &= \frac{1}{\Omega^2} \left( + p_z^2 + (p_x^2 + p_y^2) \cos(\Omega t') \right) + b \tag{3} + \end{align} + + where :math:`t' = t + t_{\rm offset}` with :math:`t` is pulse duration to scan + and :math:`t_{\rm offset}` is an extra fit parameter that may represent the edge effect. + The :math:`\Omega = \sqrt{p_x^2+p_y^2+p_z^2}` and :math:`p_x, p_y, p_z, b` are fit parameters. + The fit functions :math:`F_x, F_y, F_z` approximate the Pauli expectation + values :math:`\langle \sigma_x (t) \rangle, \langle \sigma_y (t) \rangle, + \langle \sigma_z (t) \rangle` of the target qubit, respectively. + + Based on the fit result, cross resonance Hamiltonian coefficients can be written as + + .. math:: + + ZX &= \frac{p_{x, 0} - p_{x, 1}}{2} \\ + ZY &= \frac{p_{y, 0} - p_{y, 1}}{2} \\ + ZZ &= \frac{p_{z, 0} - p_{z, 1}}{2} \\ + IX &= \frac{p_{x, 0} + p_{x, 1}}{2} \\ + IY &= \frac{p_{y, 0} + p_{y, 1}}{2} \\ + IZ &= \frac{p_{z, 0} + p_{z, 1}}{2} + + In this analysis, the initial guess is generated by the following equations. + + .. math:: + + p_x &= \omega \cos(\theta) \cos(\phi) \\ + p_y &= \omega \cos(\theta) \sin(\phi) \\ + p_z &= \omega \sin(\theta) + + where :math:`\omega` is the mean oscillation frequency of eigenvalues, + :math:`\theta = \cos^{-1}\sqrt{\frac{\max F_z - \min F_z}{2}}` + and :math:`\phi \in [-\pi, \pi]`. + + # section: fit_parameters + + defpar t_{\rm off}: + desc: Offset to the pulse duration. For example, if pulse envelope is + a flat-topped Gaussian, two Gaussian edges may become an offset duration. + init_guess: Computed as :math:`N \sqrt{2 \pi} \sigma` where the :math:`N` is number of + pulses and :math:`\sigma` is Gaussian sigma of rising and falling edges. + Note that this implicitly assumes the :py:class:`~qiskit.pulse.library\ + .parametric_pulses.GaussianSquare` pulse envelope. + bounds: [0, None] + + defpar p_x: + desc: Fit parameter of oscillations. + init_guess: See fit model section. + bounds: None + + defpar p_y: + desc: Fit parameter of oscillations. + init_guess: See fit model section. + bounds: None + + defpar p_z: + desc: Fit parameter of oscillations. + init_guess: See fit model section. + bounds: None + + defpar b: + desc: Vertical offset of oscillations. This may indicate the state preparation and + measurement error. + init_guess: 0 + bounds: None + + # section: see_also + qiskit_experiments.library.characterization.CrossResonanceHamiltonian + qiskit_experiments.library.characterization.TomographyElementAnalysis + + """ + + def _run_analysis(self, experiment_data: ExperimentData): + + # wait for child experiments to complete + super()._run_analysis(experiment_data) + + if len(self.component_analysis()) != 2: + raise AnalysisError( + f"More than two analyses are found. {self.__class__.__name__} doesn't know " + "how to compute CR Hamiltonian coefficients with more than 2 curves." + ) + + analysis_results = [] for control in ("z", "i"): for target in ("x", "y", "z"): - p0_val = fit_data.fitval(f"p{target}0") - p1_val = fit_data.fitval(f"p{target}1") + fit0 = experiment_data.child_data(0).analysis_results(f"cr_tomo_p{target}0") + fit1 = experiment_data.child_data(1).analysis_results(f"cr_tomo_p{target}1") if control == "z": - coef_val = 0.5 * (p0_val - p1_val) / (2 * np.pi) + coef = 0.5 * (fit0.value - fit1.value) / (2 * np.pi) else: - coef_val = 0.5 * (p0_val + p1_val) / (2 * np.pi) - - extra_entries.append( - AnalysisResultData( - name=f"omega_{control}{target}", - value=coef_val, - chisq=fit_data.reduced_chisq, - device_components=[Qubit(q) for q in self._physical_qubits], - extra={"unit": "Hz"}, - ) + coef = 0.5 * (fit0.value + fit1.value) / (2 * np.pi) + + new_data = AnalysisResultData( + name=f"omega_{control}{target}", + value=coef, + quality="good" if fit0.quality == fit1.quality == "good" else "bad", + extra={"unit": "Hz"}, ) - return extra_entries + analysis_results.append(new_data) + + return analysis_results, [] diff --git a/qiskit_experiments/library/characterization/cr_hamiltonian.py b/qiskit_experiments/library/characterization/cr_hamiltonian.py index 8b99c26ff1..89bb305ed1 100644 --- a/qiskit_experiments/library/characterization/cr_hamiltonian.py +++ b/qiskit_experiments/library/characterization/cr_hamiltonian.py @@ -16,14 +16,150 @@ from typing import List, Tuple, Iterable, Dict, Optional import numpy as np +import collections +import itertools from qiskit import pulse, circuit, QuantumCircuit from qiskit.exceptions import QiskitError from qiskit.providers import Backend -from qiskit_experiments.framework import BaseExperiment, Options -from qiskit_experiments.library.characterization.analysis import CrossResonanceHamiltonianAnalysis +from qiskit.utils import optionals +from qiskit_experiments.framework import BaseExperiment, BatchExperiment, Options +from qiskit_experiments.curve_analysis import ParameterRepr +from .analysis.cr_hamiltonian_analysis import ( + TomographyElementAnalysis, + CrossResonanceHamiltonianAnalysis, +) -class CrossResonanceHamiltonian(BaseExperiment): + +class TomographyElement(BaseExperiment): + + def __init__( + self, + qubits: Tuple[int, int], + tomography_circuit: QuantumCircuit, + backend: Optional[Backend] = None, + ): + super().__init__(qubits=qubits, backend=backend, analysis=TomographyElementAnalysis()) + self.tomography_circuit = tomography_circuit + self.param_map_r = {param.name: param for param in tomography_circuit.parameters} + + @classmethod + def _default_experiment_options(cls) -> Options: + """Default experiment options. + + Experiment Options: + pulse_parameters (Dict[str, float]): Pulse parameters keyed on the + name of parameters attached to the ``tomography_circuit``. + durations (Sequence[int]): ###### + xval_offset (float): Initial guess of xvalue offset due to + rising and falling pulse edges. This should be provided by the + root experiment, since thie experiment is agnostic to the pulse shape. + dt (float): Time resoluton of the system. This parameter is + automatically set when backend is provided. + granularity (int): Constaints of pulse data chunk size. This parameter is + automatically set when backend is provided. + """ + options = super()._default_experiment_options() + options.durations = None + options.pulse_parameters = dict() + options.xval_offset = 0 + options.dt = 1 + options.granularity = 1 + options.cr_channel = 0 + + return options + + def _set_backend(self, backend): + """Extract dt and granularity from the backend.""" + super()._set_backend(backend) + configuration = backend.configuration() + + try: + dt_factor = configuration.dt + except AttributeError as ex: + raise AttributeError( + "Backend configuration does not provide system time resolution dt." + ) from ex + + try: + cr_channels = configuration.control(self.physical_qubits) + index = cr_channels[0].index + except AttributeError as ex: + raise AttributeError( + "Backend configuration does not provide control channel mapping." + ) from ex + + try: + granularity = configuration.timing_constraints["granularity"] + except (AttributeError, KeyError): + granularity = 1 + + # Update experiment options + self.set_experiment_options(dt=dt_factor, granularity=granularity, cr_channel=index) + + def set_experiment_options(self, **fields): + """Set the experiment options. + + Args: + fields: The fields to update the options + """ + super().set_experiment_options(**fields) + + # Set xval offset computed from the given pulse shapes + xval_offset = self.experiment_options.xval_offset + dt = self.experiment_options.dt + self.analysis.set_options(t_off=xval_offset * dt) + + def circuits(self) -> List[QuantumCircuit]: + opt = self.experiment_options + + tomo_circuits = [] + for meas_basis in ("x", "y", "z"): + tomo_circ = QuantumCircuit(2, 1) + + tomo_circ.compose( + other=self.tomography_circuit, + qubits=[0, 1], + inplace=True, + ) + + # measure + if meas_basis == "x": + tomo_circ.h(1) + elif meas_basis == "y": + tomo_circ.sdg(1) + tomo_circ.h(1) + tomo_circ.measure(1, 0) + + tomo_circ.metadata = { + "experiment_type": self.experiment_type, + "qubits": self.physical_qubits, + "meas_basis": meas_basis, + } + tomo_circuits.append(tomo_circ) + + pulse_shape = { + pobj: opt.pulse_parameters.get(pname, None) for pname, pobj in self.param_map_r.items() + } + pulse_shape[self.param_map_r["cr_channel"]] = opt.cr_channel + + experiment_circs = [] + for duration in opt.durations: + effective_duration = opt.granularity * int(duration / opt.granularity) + + params = pulse_shape.copy() + params[self.param_map_r["duration"]] = effective_duration + + for tomo_circ in tomo_circuits: + tomo_circ_t = tomo_circ.assign_parameters(params) + tomo_circ_t.metadata["xval"] = effective_duration * opt.dt # in units of sec + tomo_circ_t.metadata["pulse_shape"] = {p.name: v for p, v in params.items()} + experiment_circs.append(tomo_circ_t) + + return experiment_circs + + +class CrossResonanceHamiltonian(BatchExperiment): r"""Cross resonance Hamiltonian tomography experiment. # section: overview @@ -121,9 +257,19 @@ class CrossResonanceHamiltonian(BaseExperiment): .. ref_website:: Qiskit Textbook 6.7, https://qiskit.org/textbook/ch-quantum-hardware/hamiltonian-tomography.html """ - - # Number of CR pulses. The flat top duration per pulse is divided by this number. - __n_cr_pulses__ = 1 + __n_echos = 1 + + # Fully parametrize CR pulse. This is because parameters can be updated at anytime + # through experiment options, but CR schedule defined in the batch experiment + # is immediately passed to the component experiments at the class instantiation. + __parameters = collections.OrderedDict( + amp=circuit.Parameter("amp"), + amp_t=circuit.Parameter("amp_t"), + sigma=circuit.Parameter("sigma"), + risefall=circuit.Parameter("risefall"), + duration=circuit.Parameter("duration"), + cr_channel=circuit.Parameter("cr_channel"), + ) def __init__( self, @@ -147,13 +293,71 @@ def __init__( Raises: QiskitError: When ``qubits`` length is not 2. """ - super().__init__(qubits, analysis=CrossResonanceHamiltonianAnalysis(), backend=backend) - if len(qubits) != 2: raise QiskitError( "Length of qubits is not 2. Please provide index for control and target qubit." ) + cal_def = self._default_cr_schedule(*qubits) + + pulse_gate = circuit.Gate( + "cr_gate", + num_qubits=2, + params=cal_def.parameters, + ) + + cr_circuit = self._default_cr_sequence(pulse_gate) + + # Control state = 0 + cr_circuit0 = QuantumCircuit(2) + cr_circuit0.compose(cr_circuit, inplace=True) + cr_circuit0.add_calibration( + gate=pulse_gate, + qubits=qubits, + schedule=cal_def, + params=cal_def.parameters, + ) + exp0 = TomographyElement( + qubits=qubits, + tomography_circuit=cr_circuit0, + backend=backend, + ) + exp0.analysis.set_options( + result_parameters=[ + ParameterRepr("px", "cr_tomo_px0", "rad/s"), + ParameterRepr("py", "cr_tomo_py0", "rad/s"), + ParameterRepr("pz", "cr_tomo_pz0", "rad/s"), + ] + ) + + # Control state = 1 + cr_circuit1 = QuantumCircuit(2) + cr_circuit1.x(0) + cr_circuit1.compose(cr_circuit, inplace=True) + cr_circuit1.add_calibration( + gate=pulse_gate, + qubits=qubits, + schedule=cal_def, + params=cal_def.parameters, + ) + exp1 = TomographyElement( + qubits=qubits, + tomography_circuit=cr_circuit1, + backend=backend, + ) + exp1.analysis.set_options( + result_parameters=[ + ParameterRepr("px", "cr_tomo_px1", "rad/s"), + ParameterRepr("py", "cr_tomo_py1", "rad/s"), + ParameterRepr("pz", "cr_tomo_pz1", "rad/s"), + ] + ) + + super().__init__( + experiments=[exp0, exp1], + backend=backend, + ) + self.analysis = CrossResonanceHamiltonianAnalysis(analyses=[exp0.analysis, exp1.analysis]) self.set_experiment_options(flat_top_widths=flat_top_widths, **kwargs) @classmethod @@ -179,160 +383,102 @@ def _default_experiment_options(cls) -> Options: return options - def _build_cr_circuit( - self, - pulse_gate: circuit.Gate, - ) -> QuantumCircuit: - """Single tone cross resonance. + def set_experiment_options(self, **fields): + """Set the experiment options. Args: - pulse_gate: A pulse gate to represent a single cross resonance pulse. + fields: The fields to update the options + """ + super().set_experiment_options(**fields) + + # Override component experiment configurations + opt = self.experiment_options + + pulse_parameters = { + "amp": opt.amp, + "amp_t": opt.amp_t, + "sigma": opt.sigma, + "risefall": opt.risefall, + } + + # Entire CR pulse duration (in dt) + t_risefall = 2 * opt.sigma * opt.risefall + cr_durations = np.asarray(opt.flat_top_widths, dtype=float) / self.__n_echos + t_risefall + + # Effective length of Gaussian rising falling edges for fit guess (in dt). + edge_duration = np.sqrt(2 * np.pi) * opt.sigma * self.__n_echos + + for exp in self.component_experiment(): + # Copy pulse configurations to component experiments + exp.set_experiment_options( + durations=cr_durations, + pulse_parameters=pulse_parameters, + xval_offset=edge_duration, + ) + + def set_transpile_options(self, **fields): + """Set the transpiler options for :meth:`run` method. + + Args: + fields: The fields to update the options + """ + super().set_transpile_options(fields) + + for exp in self.component_experiment(): + exp.set_transpile_options(fields) + + @classmethod + def _default_cr_sequence(cls, pulse_gate: circuit.Gate) -> circuit.QuantumCircuit: + """Circuit level representation of cross resonance sequence. + + Args: + pulse_gate: Gate definition of the cross resonance. Returns: - A circuit definition for the cross resonance pulse to measure. + QuantumCircuit representation of cross resonance sequence. """ - cr_circuit = QuantumCircuit(2) + cr_circuit = circuit.QuantumCircuit(2) cr_circuit.append(pulse_gate, [0, 1]) return cr_circuit - def _build_cr_schedule( - self, - backend: Backend, - flat_top_width: float, - sigma: float, - ) -> pulse.ScheduleBlock: - """GaussianSquared cross resonance pulse. + @classmethod + def _default_cr_schedule(cls, control_index, target_index) -> pulse.Schedule: + """Pulse level representation of single cross resonance gate. Args: - backend: The target backend. - flat_top_width: Total length of flat top part of the pulse in units of dt. - sigma: Sigma of Gaussian edges in units of dt. + control_index: Index of control qubit. + target_index: Index of target qubit. Returns: - A schedule definition for the cross resonance pulse to measure. + Pulse schedule of cross resonance. """ - opt = self.experiment_options - - # Compute valid integer duration - cr_duration = round_pulse_duration( - backend=backend, duration=flat_top_width + 2 * sigma * opt.risefall - ) - - with pulse.build(backend, default_alignment="left", name="cr") as cross_resonance: + with pulse.build(default_alignment="left", name="cr") as cal_def: # add cross resonance tone pulse.play( pulse.GaussianSquare( - duration=cr_duration, - amp=opt.amp, - sigma=sigma, - width=flat_top_width, + duration=cls.__parameters["duration"], + amp=cls.__parameters["amp"], + sigma=cls.__parameters["sigma"], + risefall_sigma_ratio=cls.__parameters["risefall"], ), - pulse.control_channels(*self.physical_qubits)[0], + pulse.ControlChannel(cls.__parameters["cr_channel"]), + ) + pulse.play( + pulse.GaussianSquare( + duration=cls.__parameters["duration"], + amp=cls.__parameters["amp"], + sigma=cls.__parameters["sigma"], + risefall_sigma_ratio=cls.__parameters["risefall"], + ), + pulse.DriveChannel(target_index), ) - # add cancellation tone - if not np.isclose(opt.amp_t, 0.0): - pulse.play( - pulse.GaussianSquare( - duration=cr_duration, - amp=opt.amp_t, - sigma=sigma, - width=flat_top_width, - ), - pulse.drive_channel(self.physical_qubits[1]), - ) - else: - pulse.delay(cr_duration, pulse.drive_channel(self.physical_qubits[1])) # place holder for empty drive channels. this is necessary due to known pulse gate bug. - pulse.delay(cr_duration, pulse.drive_channel(self.physical_qubits[0])) - - return cross_resonance - - def circuits(self) -> List[QuantumCircuit]: - """Return a list of experiment circuits. - - Returns: - A list of :class:`QuantumCircuit`. - - Raises: - AttributeError: When the backend doesn't report the time resolution of waveforms. - """ - opt = self.experiment_options - - try: - dt_factor = self.backend.configuration().dt - except AttributeError as ex: - raise AttributeError("Backend configuration does not provide time resolution.") from ex + pulse.delay(cls.__parameters["duration"], pulse.DriveChannel(control_index)) - # Parametrized duration cannot be used because total duration is computed - # on the fly with granularity validation. This validation requires - # duration value that is not a parameter expression. - - # Note that this experiment scans flat top width rather than total duration. - expr_circs = list() - for flat_top_width in np.asarray(opt.flat_top_widths, dtype=float): - - cr_gate = circuit.Gate( - "cr_gate", - num_qubits=2, - params=[flat_top_width / self.__n_cr_pulses__], - ) - - for control_state in (0, 1): - for meas_basis in ("x", "y", "z"): - tomo_circ = QuantumCircuit(2, 1) - - # state prep - if control_state: - tomo_circ.x(0) - - # add cross resonance - tomo_circ.compose( - other=self._build_cr_circuit(cr_gate), - qubits=[0, 1], - inplace=True, - ) - - # measure - if meas_basis == "x": - tomo_circ.h(1) - elif meas_basis == "y": - tomo_circ.sdg(1) - tomo_circ.h(1) - tomo_circ.measure(1, 0) - - # add metadata - tomo_circ.metadata = { - "experiment_type": self.experiment_type, - "qubits": self.physical_qubits, - "xval": flat_top_width * dt_factor, # in units of sec - "control_state": control_state, - "meas_basis": meas_basis, - } - - # Create schedule and add it to the circuit. - # The flat top width and sigma are in units of dt - # width is divided by number of tones to keep total duration consistent - tomo_circ.add_calibration( - gate=cr_gate, - qubits=self.physical_qubits, - schedule=self._build_cr_schedule( - backend=self.backend, - flat_top_width=flat_top_width / self.__n_cr_pulses__, - sigma=opt.sigma, - ), - ) - - expr_circs.append(tomo_circ) - - return expr_circs - - def _additional_metadata(self) -> Dict[str, any]: - """Attach number of pulses to construct time offset initial guess in the fitter.""" - - return {"n_cr_pulses": self.__n_cr_pulses__} + return cal_def class EchoedCrossResonanceHamiltonian(CrossResonanceHamiltonian): @@ -365,21 +511,22 @@ class EchoedCrossResonanceHamiltonian(CrossResonanceHamiltonian): # section: reference .. ref_arxiv:: 1 2007.02925 + # see_also: + qiskit_experiments.library.characterization.CrossResonanceHamiltonian + """ - __n_cr_pulses__ = 2 + __n_echos = 2 - def _build_cr_circuit( - self, - pulse_gate: circuit.Gate, - ) -> QuantumCircuit: - """Build the echoed cross-resonance circuit out of two single cross-resonance tones. + @classmethod + def _default_cr_sequence(cls, pulse_gate: circuit.Gate) -> circuit.QuantumCircuit: + """Circuit level representation of cross resonance sequence. Args: - pulse_gate: A pulse gate to represent a single cross resonance pulse. + pulse_gate: Gate definition of the cross resonance. Returns: - A circuit definition for the cross resonance pulse to measure. + QuantumCircuit representation of cross resonance sequence. """ cr_circuit = QuantumCircuit(2) cr_circuit.append(pulse_gate, [0, 1]) @@ -389,21 +536,3 @@ def _build_cr_circuit( cr_circuit.rz(-np.pi, 1) return cr_circuit - - -def round_pulse_duration(backend: Backend, duration: float) -> int: - """Find the best pulse duration that meets timing constraints of the backend. - - Args: - backend: Target backend to play pulses. - duration: Duration of pulse to be formatted. - - Returns: - Valid integer pulse duration that meets timing constraints of the backend. - """ - # TODO this can be moved to some common utils - - timing_constraints = getattr(backend.configuration(), "timing_constraints", dict()) - granularity = int(timing_constraints.get("granularity", 1)) - - return granularity * int(duration / granularity) diff --git a/test/test_cross_resonance_hamiltonian.py b/test/test_cross_resonance_hamiltonian.py index b1819ce340..4e74454cd3 100644 --- a/test/test_cross_resonance_hamiltonian.py +++ b/test/test_cross_resonance_hamiltonian.py @@ -53,16 +53,23 @@ def __init__( b: Offset term. seed: Seed of random number generator used to generate count data. """ - self.fit_func_args = { - "t_off": t_off, - "px0": 2 * np.pi * (ix + zx), - "px1": 2 * np.pi * (ix - zx), - "py0": 2 * np.pi * (iy + zy), - "py1": 2 * np.pi * (iy - zy), - "pz0": 2 * np.pi * (iz + zz), - "pz1": 2 * np.pi * (iz - zz), - "b": b, - } + # composite experiment + self.fit_func_args = [ + { + "t_off": t_off, + "px": 2 * np.pi * (ix + zx), + "py": 2 * np.pi * (iy + zy), + "pz": 2 * np.pi * (iz + zz), + "b": b, + }, + { + "t_off": t_off, + "px": 2 * np.pi * (ix - zx), + "py": 2 * np.pi * (iy - zy), + "pz": 2 * np.pi * (iz - zz), + "b": b, + }, + ] self.seed = seed configuration = PulseBackendConfiguration( backend_name="fake_cr_hamiltonian", @@ -120,18 +127,18 @@ def run(self, run_input, **kwargs): shots = kwargs.get("shots", 1024) rng = np.random.default_rng(seed=self.seed) - series_defs = cr_hamiltonian_analysis.CrossResonanceHamiltonianAnalysis.__series__ - filter_kwargs_list = [sdef.filter_kwargs for sdef in series_defs] + series_defs = cr_hamiltonian_analysis.TomographyElementAnalysis.__series__ + filter_kwargs_list = [sdef.filter_kwargs["meas_basis"] for sdef in series_defs] for test_circ in run_input: - metadata = { - "control_state": test_circ.metadata["control_state"], - "meas_basis": test_circ.metadata["meas_basis"], - } - curve_ind = filter_kwargs_list.index(metadata) - xval = test_circ.metadata["xval"] - - expv = series_defs[curve_ind].fit_func(xval, **self.fit_func_args) + # composite experiment + component_index = test_circ.metadata["composite_index"][0] + component_metadata = test_circ.metadata["composite_metadata"][0] + + curve_ind = filter_kwargs_list.index(component_metadata["meas_basis"]) + xval = component_metadata["xval"] + + expv = series_defs[curve_ind].fit_func(xval, **self.fit_func_args[component_index]) popl = 0.5 * (1 - expv) counts = rng.multinomial(shots, [1 - popl, popl]) results.append( @@ -165,6 +172,7 @@ def test_circuit_generation(self): qubits=(0, 1), flat_top_widths=[1000], amp=0.1, + amp_t=0.05, sigma=64, risefall=2, ) @@ -182,7 +190,15 @@ def test_circuit_generation(self): ), pulse.ControlChannel(0), ) - pulse.delay(nearlest_16, pulse.DriveChannel(0)) + pulse.play( + pulse.GaussianSquare( + nearlest_16, + amp=0.05, + sigma=64, + width=1000, + ), + pulse.DriveChannel(0), + ) pulse.delay(nearlest_16, pulse.DriveChannel(1)) cr_gate = circuit.Gate("cr_gate", num_qubits=2, params=[1000]) @@ -254,10 +270,10 @@ def test_integration(self, ix, iy, iz, zx, zy, zz): # These values are computed from other analysis results in post hook. # Thus at least one of these values should be round-trip tested. res_ix = exp_data.analysis_results("omega_ix") - self.assertAlmostEqual(res_ix.value.n, ix, delta=2e4) self.assertRoundTripSerializable(res_ix.value, check_func=self.ufloat_equiv) self.assertEqual(res_ix.extra["unit"], "Hz") + self.assertAlmostEqual(exp_data.analysis_results("omega_ix").value.n, ix, delta=2e4) self.assertAlmostEqual(exp_data.analysis_results("omega_iy").value.n, iy, delta=2e4) self.assertAlmostEqual(exp_data.analysis_results("omega_iz").value.n, iz, delta=2e4) self.assertAlmostEqual(exp_data.analysis_results("omega_zx").value.n, zx, delta=2e4)