diff --git a/docs/tutorials/calibrations.rst b/docs/tutorials/calibrations.rst index d3d5967850..dcf69a8ad5 100644 --- a/docs/tutorials/calibrations.rst +++ b/docs/tutorials/calibrations.rst @@ -45,7 +45,7 @@ This automatic updating can also be disabled using the ``auto_update`` flag. .. jupyter-execute:: - backend = SingleTransmonTestBackend(5.2e9,-.25e9, 1e9, 0.8e9, noise=False, seed=100) + backend = SingleTransmonTestBackend(noise=False, seed=100) qubit = 0 cals=Calibrations.from_backend(backend) print(cals.get_inst_map()) @@ -163,7 +163,7 @@ Instantiate the experiment and draw the first circuit in the sweep: .. jupyter-execute:: - freq01_estimate = backend.defaults().qubit_freq_est[qubit] + freq01_estimate = backend.qubit_properties(0).frequency frequencies = np.linspace(freq01_estimate-15e6, freq01_estimate+15e6, 51) spec = RoughFrequencyCal((qubit,), cals, frequencies, backend=backend) spec.set_experiment_options(amp=0.005) @@ -362,7 +362,7 @@ the experiments. We base all our pulses on the default :math:`X` pulse of .. jupyter-execute:: - x_pulse = backend.defaults().instruction_schedule_map.get('x', (qubit,)).instructions[0][1].pulse + x_pulse = backend.instruction_schedule_map.get('x', (qubit,)).instructions[0][1].pulse d0, inst_map = pulse.DriveChannel(qubit), pulse.InstructionScheduleMap() diff --git a/qiskit_experiments/test/pulse_backend.py b/qiskit_experiments/test/pulse_backend.py index 0f0627afbd..1f7d05090e 100644 --- a/qiskit_experiments/test/pulse_backend.py +++ b/qiskit_experiments/test/pulse_backend.py @@ -20,26 +20,30 @@ from qiskit import QuantumCircuit from qiskit.circuit import CircuitInstruction -from qiskit.circuit.library.standard_gates import RZGate, SXGate, XGate +from qiskit.circuit.library.standard_gates import RZGate, SXGate, XGate, CXGate from qiskit.circuit.measure import Measure from qiskit.circuit.parameter import Parameter from qiskit.providers import BackendV2, QubitProperties -from qiskit.providers.models import PulseDefaults -from qiskit.providers.models.pulsedefaults import Command from qiskit.providers.options import Options +from qiskit import pulse from qiskit.pulse import Schedule, ScheduleBlock from qiskit.pulse.transforms import block_to_schedule -from qiskit.qobj.pulse_qobj import PulseQobjInstruction from qiskit.qobj.utils import MeasLevel, MeasReturnType from qiskit.quantum_info.states import DensityMatrix, Statevector from qiskit.result import Result, Counts from qiskit.transpiler import InstructionProperties, Target +from qiskit.utils.deprecation import deprecate_arg from qiskit_experiments.warnings import HAS_DYNAMICS from qiskit_experiments.data_processing.discriminator import BaseDiscriminator from qiskit_experiments.exceptions import QiskitError from qiskit_experiments.test.utils import FakeJob +if HAS_DYNAMICS._is_available(): + from qiskit_dynamics import Solver + from qiskit_dynamics.models import LindbladModel + from qiskit_dynamics.pulse import InstructionToSignals + @HAS_DYNAMICS.require_in_instance class PulseBackend(BackendV2): @@ -64,11 +68,30 @@ class PulseBackend(BackendV2): experiment without having to run on hardware. """ + @deprecate_arg( + name="static_hamiltonian", + since="0.6", + package_name="qiskit-experiments", + pending=True, + ) + @deprecate_arg( + name="hamiltonian_operators", + since="0.6", + package_name="qiskit-experiments", + pending=True, + ) + @deprecate_arg( + name="static_dissipators", + since="0.6", + package_name="qiskit-experiments", + pending=True, + ) def __init__( self, - static_hamiltonian: np.ndarray, - hamiltonian_operators: np.ndarray, + static_hamiltonian: Optional[np.ndarray] = None, + hamiltonian_operators: Optional[np.ndarray] = None, static_dissipators: Optional[np.ndarray] = None, + solver: Optional[Solver] = None, dt: float = 0.1 * 1e-9, solver_method="RK23", seed: int = 0, @@ -80,7 +103,7 @@ def __init__( Args: static_hamiltonian: Time-independent term in the Hamiltonian. - hamiltonian_operators: List of time-dependent operators + hamiltonian_operators: List of time-dependent operators. static_dissipators: Constant dissipation operators. Defaults to None. dt: Sample rate for simulating pulse schedules. Defaults to 0.1*1e-9. solver_method: Numerical solver method to use. Check qiskit_dynamics for available @@ -90,8 +113,6 @@ def __init__( atol: Absolute tolerance during solving. rtol: Relative tolerance during solving. """ - from qiskit_dynamics import Solver - super().__init__( None, name="PulseBackendV2", @@ -100,7 +121,7 @@ def __init__( backend_version="0.0.1", ) - # subclasses must implements default pulse schedules + # subclasses must implement default pulse schedules self._defaults = None self._target = Target(dt=dt, granularity=16) @@ -119,24 +140,25 @@ def __init__( if rtol: self.solve_kwargs["rtol"] = rtol - self.static_hamiltonian = static_hamiltonian - self.hamiltonian_operators = hamiltonian_operators - self.static_dissipators = static_dissipators - self.solver = Solver( - static_hamiltonian=self.static_hamiltonian, - hamiltonian_operators=self.hamiltonian_operators, - static_dissipators=self.static_dissipators, - **kwargs, - ) + if static_hamiltonian is not None and hamiltonian_operators is not None: + # TODO deprecate soon + solver = Solver( + static_hamiltonian=static_hamiltonian, + hamiltonian_operators=hamiltonian_operators, + static_dissipators=static_dissipators, + **kwargs, + ) + self.solver = solver self.model_dim = self.solver.model.dim + self.subsystem_dims = (self.model_dim,) - if self.static_dissipators is None: - self.y_0 = np.eye(self.model_dim) - self.ground_state = np.array([1.0] + [0.0] * (self.model_dim - 1)) - else: + if isinstance(self.solver.model, LindbladModel): self.y_0 = np.eye(self.model_dim**2) self.ground_state = np.array([1.0] + [0.0] * (self.model_dim**2 - 1)) + else: + self.y_0 = np.eye(self.model_dim) + self.ground_state = np.array([1.0] + [0.0] * (self.model_dim - 1)) self._simulated_pulse_unitaries = {} @@ -146,6 +168,12 @@ def __init__( # An optional discriminator that is used to create counts from IQ data. self._discriminator = None + # pylint: disable=unused-argument + @staticmethod + def rz_gate(qubits, theta): + """Initialize RZ gate.""" + return None + @property def target(self): """Contains information for circuit transpilation.""" @@ -156,16 +184,20 @@ def max_circuits(self): return None def defaults(self): - """return backend pulse defaults""" + """return backend pulse defaults.""" return self._defaults + # pylint: disable=unused-argument + def control_channel(self, qubits: List[int]): + return [] + @property - def discriminator(self) -> BaseDiscriminator: - """Return the discriminator for the IQ data.""" + def discriminator(self) -> List[BaseDiscriminator]: + """Return the discriminators for the IQ data.""" return self._discriminator @discriminator.setter - def discriminator(self, discriminator: BaseDiscriminator): + def discriminator(self, discriminator: List[BaseDiscriminator]): """Set the discriminator.""" self._discriminator = discriminator @@ -189,6 +221,16 @@ def default_pulse_unitaries(self, unitaries: Dict[Tuple, np.array]): """Set the default unitary pulses this allows the tests to simulate the pulses only once.""" self._simulated_pulse_unitaries = unitaries + @staticmethod + def pulse_command(qubit: int, amp: float, beta: float): + """Utility function to create pulse instructions""" + with pulse.build() as pulse_gate: + pulse.play( + pulse.library.Drag(duration=160, amp=amp, sigma=40, beta=beta, name=f"Xp_d{qubit}"), + pulse.DriveChannel(qubit), + ) + return pulse_gate + @staticmethod def _get_info( circuit: QuantumCircuit, instruction: CircuitInstruction @@ -198,7 +240,7 @@ def _get_info( Args: circuit: The quantum circuit in which the instruction is located. This is needed to access the register in the circuit. - instruction: A gate or operation in a QuantumCircuit + instruction: A gate or operation in a QuantumCircuit. Returns: Tuple of qubit index, gate parameters and instruction name. @@ -210,7 +252,8 @@ def _get_info( def _iq_data( self, - probability: np.ndarray, + state: Union[Statevector, DensityMatrix], + meas_qubits: List, shots: int, centers: List[Tuple[float, float]], width: float, @@ -219,31 +262,43 @@ def _iq_data( """Generates IQ data for each physical level. Args: - probability: probability of occupation - shots: Number of shots - centers: The central I and Q points for each level - width: Width of IQ data distribution + state: Quantum state operator. + shots: Number of shots. + centers: The central I and Q points for each level. + width: Width of IQ data distribution. phase: Phase of IQ data, by default 0. Defaults to None. Returns: - (I,Q) data. - """ - counts_n = self._rng.multinomial(shots, probability / sum(probability), size=1).T + (I,Q) data as List[shot index][qubit index] = [I,Q]. + Raises: + QiskitError: If number of centers and levels don't match. + """ full_i, full_q = [], [] - - for idx, count_i in enumerate(counts_n): - full_i.append(self._rng.normal(loc=centers[idx][0], scale=width, size=count_i)) - full_q.append(self._rng.normal(loc=centers[idx][1], scale=width, size=count_i)) - - full_i = list(chain.from_iterable(full_i)) - full_q = list(chain.from_iterable(full_q)) - - if phase is not None: - complex_iq = (full_i + 1.0j * full_q) * np.exp(1.0j * phase) - full_i, full_q = complex_iq.real, complex_iq.imag - - full_iq = 1e16 * np.array([[full_i], [full_q]]).T + for sub_idx in meas_qubits: + probability = state.probabilities(qargs=[sub_idx]) + counts_n = self._rng.multinomial(shots, probability / sum(probability), size=1).T + + sub_i, sub_q = [], [] + if len(counts_n) != len(centers): + raise QiskitError( + f"Number of centers ({len(centers)}) not equal to number of levels ({len(counts_n)})" + ) + + for idx, count_i in enumerate(counts_n): + sub_i.append(self._rng.normal(loc=centers[idx][0], scale=width, size=count_i)) + sub_q.append(self._rng.normal(loc=centers[idx][1], scale=width, size=count_i)) + + sub_i = list(chain.from_iterable(sub_i)) + sub_q = list(chain.from_iterable(sub_q)) + + if phase is not None: + complex_iq = (sub_i + 1.0j * sub_q) * np.exp(1.0j * phase) + sub_i, sub_q = complex_iq.real, complex_iq.imag + + full_i.append(sub_i) + full_q.append(sub_q) + full_iq = np.array([full_i, full_q]).T return full_iq.tolist() # pylint: disable=unused-argument @@ -262,8 +317,8 @@ def _iq_cluster_centers(self, circuit: Optional[QuantumCircuit] = None) -> List[ Returns: Coordinates for IQ centers. """ - theta = 2 * np.pi / self.model_dim - return [(np.cos(idx * theta), np.sin(idx * theta)) for idx in range(self.model_dim)] + theta = 2 * np.pi / self.subsystem_dims[0] + return [(np.cos(idx * theta), np.sin(idx * theta)) for idx in range(self.subsystem_dims[0])] def _state_to_measurement_data( self, @@ -273,6 +328,7 @@ def _state_to_measurement_data( meas_return: MeasReturnType, memory: bool, circuit: QuantumCircuit, + meas_qubits: List, ) -> Tuple[Union[Union[Dict, Counts, Tuple[List, List]], Any], Optional[Any]]: """Convert State objects to IQ data or Counts. @@ -296,28 +352,32 @@ def _state_to_measurement_data( QiskitError: If unsuported measurement options are provided. """ memory_data = None - if self.static_dissipators is not None: + if isinstance(self.solver.model, LindbladModel): state = state.reshape(self.model_dim, self.model_dim) - state = DensityMatrix(state / np.trace(state)) + state = DensityMatrix(state / np.trace(state), self.subsystem_dims) else: - state = Statevector(state / np.linalg.norm(state)) + state = Statevector(state / np.linalg.norm(state), self.subsystem_dims) if meas_level == MeasLevel.CLASSIFIED: if self._discriminator is None: if memory: - memory_data = state.sample_memory(shots) + memory_data = state.sample_memory(shots, qargs=meas_qubits) measurement_data = dict(zip(*np.unique(memory_data, return_counts=True))) else: - measurement_data = state.sample_counts(shots) + measurement_data = state.sample_counts(shots, qargs=meas_qubits) else: centers = self._iq_cluster_centers(circuit=circuit) - iq_data = self._iq_data(state.probabilities(), shots, centers, 0.2) - memory_data = self._discriminator.predict(iq_data) + iq_data = np.array(self._iq_data(state, meas_qubits, shots, centers, 0.2)) + memory_data = [ + self._discriminator[qubit_idx].predict(iq_data[:, idx]) + for idx, qubit_idx in enumerate(meas_qubits) + ] + memory_data = ["".join(state_label) for state_label in zip(*memory_data[::-1])] measurement_data = dict(zip(*np.unique(memory_data, return_counts=True))) elif meas_level == MeasLevel.KERNELED: centers = self._iq_cluster_centers(circuit=circuit) - measurement_data = self._iq_data(state.probabilities(), shots, centers, 0.2) + measurement_data = self._iq_data(state, meas_qubits, shots, centers, 0.2) if meas_return == "avg": measurement_data = np.average(np.array(measurement_data), axis=0) else: @@ -325,18 +385,15 @@ def _state_to_measurement_data( return measurement_data, memory_data - def solve(self, schedule: Union[ScheduleBlock, Schedule], qubits: Tuple[int]) -> np.ndarray: + def solve(self, schedule: Union[ScheduleBlock, Schedule]) -> np.ndarray: """Solves for qubit dynamics under the action of a pulse instruction Args: - schedule: Pulse signal - qubits: (remove after multi-qubit gates is implemented) + schedule: Pulse signal. Returns: - Time-evolution unitary operator + Time-evolution unitary operator. """ - if len(qubits) > 1: - raise QiskitError("Multi qubit gates are not yet implemented.") if isinstance(schedule, ScheduleBlock): schedule = block_to_schedule(schedule) @@ -365,6 +422,9 @@ def run(self, run_input: Union[QuantumCircuit, List[QuantumCircuit]], **run_opti Returns: FakeJob with simulation data. + + Raises: + QiskitError: If unsuported operations are performed. """ self.options.update_options(**run_options) shots = self.options.get("shots", self._options.shots) @@ -386,15 +446,15 @@ def run(self, run_input: Union[QuantumCircuit, List[QuantumCircuit]], **run_opti for circuit in run_input: unitaries = {} - + meas_qubits = [] # 1. Parse the calibrations and simulate any new schedule. Add U to the unitaries. for name, schedule in circuit.calibrations.items(): for (qubits, params), schedule_block in schedule.items(): - schedule_key = hash(repr(schedule)) + schedule_key = hash(repr(schedule_block)) # Simulate the schedule if not in the cache. if schedule_key not in self._schedule_cache: - self._schedule_cache[schedule_key] = self.solve(schedule_block, qubits) + self._schedule_cache[schedule_key] = self.solve(schedule_block) unitaries[(name, qubits, params)] = self._schedule_cache[schedule_key] @@ -407,18 +467,20 @@ def run(self, run_input: Union[QuantumCircuit, List[QuantumCircuit]], **run_opti state_t = self.ground_state.copy() for instruction in circuit.data: qubits, params, inst_name = self._get_info(circuit, instruction) - if inst_name in ["barrier", "measure"]: + if inst_name == "barrier": + continue + if inst_name == "measure": + meas_qubits += [qubits[0]] continue if inst_name == "rz": - # Ensures that the action in the qubit space is preserved. - unitary = np.diag([np.exp(1.0j * idx * params[0] / 2) for idx in [-1, 1, 3]]) + unitary = self.rz_gate(qubits, params[0]) else: unitary = unitaries[(inst_name, qubits, params)] state_t = unitary @ state_t # 4. Convert the probabilities to IQ data or counts. measurement_data, memory_data = self._state_to_measurement_data( - state_t, shots, meas_level, meas_return, memory, circuit + state_t, shots, meas_level, meas_return, memory, circuit, meas_qubits ) run_result = { @@ -432,7 +494,7 @@ def run(self, run_input: Union[QuantumCircuit, List[QuantumCircuit]], **run_opti if meas_level == MeasLevel.CLASSIFIED: run_result["data"]["counts"] = measurement_data - if memory_data is not None: + if memory: run_result["data"]["memory"] = memory_data if meas_level == MeasLevel.KERNELED: run_result["data"]["memory"] = measurement_data @@ -444,7 +506,7 @@ def run(self, run_input: Union[QuantumCircuit, List[QuantumCircuit]], **run_opti @HAS_DYNAMICS.require_in_instance class SingleTransmonTestBackend(PulseBackend): - r"""A backend that corresponds to a three level anharmonic transmon qubit. + r"""A one qubit backend that corresponds to a three-level anharmonic transmon qubit. The Hamiltonian of the system is @@ -457,19 +519,50 @@ class SingleTransmonTestBackend(PulseBackend): the raising and lowering operators between levels :math:`j-1` and :math:`j`. """ + @deprecate_arg( + name="qubit_frequency", + since="0.6", + package_name="qiskit-experiments", + pending=True, + ) + @deprecate_arg( + name="anharmonicity", + since="0.6", + package_name="qiskit-experiments", + pending=True, + ) + @deprecate_arg( + name="lambda_1", + since="0.6", + package_name="qiskit-experiments", + pending=True, + ) + @deprecate_arg( + name="lambda_2", + since="0.6", + package_name="qiskit-experiments", + pending=True, + ) + @deprecate_arg( + name="gamma_1", + since="0.6", + package_name="qiskit-experiments", + pending=True, + ) def __init__( self, - qubit_frequency: float = 5e9, - anharmonicity: float = -0.25e9, - lambda_1: float = 1e9, - lambda_2: float = 0.8e9, - gamma_1: float = 1e4, + qubit_frequency: Optional[float] = None, + anharmonicity: Optional[float] = None, + lambda_1: Optional[float] = None, + lambda_2: Optional[float] = None, + gamma_1: Optional[float] = None, noise: bool = True, atol: float = None, rtol: float = None, **kwargs, ): - """Initialise backend with hamiltonian parameters + """Initialise backend with hamiltonian parameters. If any of the hamiltonian + parameters are not provided, default values will be used. Args: qubit_frequency: Frequency of the qubit (0-1). Defaults to 5e9. @@ -482,9 +575,20 @@ def __init__( atol: Absolute tolerance during solving. rtol: Relative tolerance during solving. """ - from qiskit_dynamics.pulse import InstructionToSignals + if anharmonicity is None: + self.anharmonicity = 0.25e9 + else: + self.anharmonicity = anharmonicity - qubit_frequency_02 = 2 * qubit_frequency + anharmonicity + if qubit_frequency is None: + qubit_frequency = 5e9 + if lambda_1 is None and lambda_2 is None: + lambda_1 = 1e9 + lambda_2 = 0.8e9 + if gamma_1 is None: + gamma_1 = 1e4 + + qubit_frequency_02 = 2 * qubit_frequency + self.anharmonicity ket0 = np.array([[1, 0, 0]]).T ket1 = np.array([[0, 1, 0]]).T ket2 = np.array([[0, 0, 1]]).T @@ -505,9 +609,9 @@ def __init__( r_frame = 2 * np.pi * qubit_frequency * (p1 + 2 * p2) t1_dissipator = np.sqrt(gamma_1) * sigma_m1 - self.anharmonicity = anharmonicity - self.rabi_rate_01 = 8.589 - self.rabi_rate_12 = 6.876 + self.rabi_rate_01 = [8.5985] + self.rabi_rate_12 = [6.876] + self.beta_01 = [1.551] if noise is True: evaluation_mode = "dense_vectorized" @@ -517,84 +621,48 @@ def __init__( static_dissipators = None super().__init__( - static_hamiltonian=drift, - hamiltonian_operators=control, - static_dissipators=static_dissipators, - rotating_frame=r_frame, - rwa_cutoff_freq=1.9 * qubit_frequency, - rwa_carrier_freqs=[qubit_frequency], - evaluation_mode=evaluation_mode, + solver=Solver( + static_hamiltonian=drift, + hamiltonian_operators=control, + static_dissipators=static_dissipators, + rotating_frame=r_frame, + rwa_cutoff_freq=1.9 * qubit_frequency, + rwa_carrier_freqs=[qubit_frequency], + evaluation_mode=evaluation_mode, + ), atol=atol, rtol=rtol, **kwargs, ) - self._defaults = PulseDefaults.from_dict( - { - "qubit_freq_est": [qubit_frequency / 1e9], - "meas_freq_est": [0], - "buffer": 0, - "pulse_library": [], - "cmd_def": [ - Command.from_dict( - { - "name": "x", - "qubits": [0], - "sequence": [ - PulseQobjInstruction( - name="parametric_pulse", - t0=0, - ch="d0", - label="Xp_d0", - pulse_shape="drag", - parameters={ - "amp": (0.5 + 0j) / self.rabi_rate_01, - "beta": 5, - "duration": 160, - "sigma": 40, - }, - ).to_dict() - ], - } - ).to_dict(), - Command.from_dict( - { - "name": "sx", - "qubits": [0], - "sequence": [ - PulseQobjInstruction( - name="parametric_pulse", - t0=0, - ch="d0", - label="X90p_d0", - pulse_shape="drag", - parameters={ - "amp": (0.25 + 0j) / self.rabi_rate_01, - "beta": 5, - "duration": 160, - "sigma": 40, - }, - ).to_dict() - ], - } - ).to_dict(), - ], - } - ) + self._discriminator = [DefaultDiscriminator()] + + self._qubit_properties = [ + QubitProperties(frequency=qubit_frequency), + ] self._target = Target( - qubit_properties=[QubitProperties(frequency=qubit_frequency)], + num_qubits=1, + qubit_properties=self._qubit_properties, dt=self.dt, granularity=16, ) measure_props = { - (0,): InstructionProperties(duration=0, error=0), + (0,): InstructionProperties(), } x_props = { - (0,): InstructionProperties(duration=160e-10, error=0), + (0,): InstructionProperties( + duration=160e-10, + error=0, + calibration=self.pulse_command(qubit=0, amp=0.5 / self.rabi_rate_01[0], beta=4), + ), } sx_props = { - (0,): InstructionProperties(duration=160e-10, error=0), + (0,): InstructionProperties( + duration=160e-10, + error=0, + calibration=self.pulse_command(qubit=0, amp=0.25 / self.rabi_rate_01[0], beta=4), + ), } rz_props = { (0,): InstructionProperties(duration=0.0, error=0), @@ -608,9 +676,278 @@ def __init__( self.converter = InstructionToSignals(self.dt, carriers={"d0": qubit_frequency}) default_schedules = [ - self._defaults.instruction_schedule_map.get("x", (0,)), - self._defaults.instruction_schedule_map.get("sx", (0,)), + self.instruction_schedule_map.get("x", (0,)), + self.instruction_schedule_map.get("sx", (0,)), + ] + self._simulated_pulse_unitaries = { + (schedule.name, (0,), ()): self.solve(schedule) for schedule in default_schedules + } + + @staticmethod + def rz_gate(_, theta: float) -> np.ndarray: + """Rz gate corresponding to single qubit 3 level qubit. Note: We do not try to + model accurate qutrit dynamics. + + Args: + theta: The angle parameter of Rz gate. + + Returns: + Matrix of Rz(theta). + """ + return np.diag([np.exp(1.0j * idx * theta / 2) for idx in [-1, 1, 3]]) + + +@HAS_DYNAMICS.require_in_instance +class ParallelTransmonTestBackend(PulseBackend): + r"""A decoupled two qubit backend. Models three-level anharmonic transmon qubits. + + The Hamiltonian of the system is + + .. math:: + H^i = \hbar \sum_{j=1,2} \left[\omega^i_j |j\rangle\langle j| + + \mathcal{E}(t) \lambda^i_j (\sigma_j^+ + \sigma_j^-)\right] + + H = H_0 ⊗ I + I ⊗ H_1 + Here, :math:`\omega^i_j` is the ith transition frequency from level :math`0` to level + :math:`j`. :math:`\mathcal{E}(t)` is the drive field and :math:`\sigma_j^\pm` are + the raising and lowering operators between levels :math:`j-1` and :math:`j`. + """ + + @deprecate_arg( + name="qubit_frequency", + since="0.6", + package_name="qiskit-experiments", + pending=True, + ) + @deprecate_arg( + name="anharmonicity", + since="0.6", + package_name="qiskit-experiments", + pending=True, + ) + @deprecate_arg( + name="lambda_1", + since="0.6", + package_name="qiskit-experiments", + pending=True, + ) + @deprecate_arg( + name="lambda_2", + since="0.6", + package_name="qiskit-experiments", + pending=True, + ) + @deprecate_arg( + name="gamma_1", + since="0.6", + package_name="qiskit-experiments", + pending=True, + ) + def __init__( + self, + qubit_frequency: Optional[float] = None, + anharmonicity: Optional[float] = None, + lambda_1: Optional[float] = None, + lambda_2: Optional[float] = None, + gamma_1: Optional[float] = None, + noise: bool = True, + atol: float = None, + rtol: float = None, + **kwargs, + ): + """Initialise backend with hamiltonian parameters. If any of the hamiltonian + parameters are not provided, default values will be used. + + Args: + qubit_frequency: Frequency of the qubit (0-1). Defaults to 5e9. + anharmonicity: Qubit anharmonicity $\\alpha$ = f12 - f01. Defaults to -0.25e9. + lambda_1: Strength of 0-1 transition. Defaults to 1e9. + lambda_2: Strength of 1-2 transition. Defaults to 0.8e9. + gamma_1: Relaxation rate (1/T1) for 1-0. Defaults to 1e4. + noise: Defaults to True. If True then T1 dissipation is included in the pulse-simulation. + The strength is given by ``gamma_1``. + atol: Absolute tolerance during solving. + rtol: Relative tolerance during solving. + """ + if anharmonicity is None: + self.anharmonicity = [0.25e9, 0.25e9] + else: + self.anharmonicity = [anharmonicity, anharmonicity] + + if qubit_frequency is None: + qubit_frequency = 5e9 + if lambda_1 is None and lambda_2 is None: + lambda_1 = 1e9 + lambda_2 = 0.8e9 + if gamma_1 is None: + gamma_1 = 1e4 + + qubit_frequency_02 = 2 * qubit_frequency + anharmonicity + ket0 = np.array([[1, 0, 0]]).T + ket1 = np.array([[0, 1, 0]]).T + ket2 = np.array([[0, 0, 1]]).T + + sigma_m1 = ket0 @ ket1.T.conj() + sigma_m2 = ket1 @ ket2.T.conj() + + sigma_p1 = sigma_m1.T.conj() + sigma_p2 = sigma_m2.T.conj() + + p1 = ket1 @ ket1.T.conj() + p2 = ket2 @ ket2.T.conj() + + ident = np.eye(3) + + drift = 2 * np.pi * (qubit_frequency * p1 + qubit_frequency_02 * p2) + drift_2q = np.kron(drift, ident) + np.kron(ident, drift) + + control = [ + 2 * np.pi * (lambda_1 * (sigma_p1 + sigma_m1) + lambda_2 * (sigma_p2 + sigma_m2)) + ] + control_2q = [np.kron(ident, control[0]), np.kron(control[0], ident)] + + r_frame = 2 * np.pi * qubit_frequency * (p1 + 2 * p2) + r_frame_2q = np.kron(r_frame, ident) + np.kron(ident, r_frame) + + t1_dissipator0 = np.sqrt(gamma_1) * np.kron(ident, sigma_m1) + t1_dissipator1 = np.sqrt(gamma_1) * np.kron(sigma_m1, ident) + + self.rabi_rate_01 = [8.589, 8.589] + self.rabi_rate_12 = [6.876, 6.876] + + if noise is True: + evaluation_mode = "dense_vectorized" + static_dissipators = [t1_dissipator0, t1_dissipator1] + else: + evaluation_mode = "dense" + static_dissipators = None + + super().__init__( + solver=Solver( + static_hamiltonian=drift_2q, + hamiltonian_operators=control_2q, + static_dissipators=static_dissipators, + rotating_frame=r_frame_2q, + rwa_cutoff_freq=1.9 * qubit_frequency, + rwa_carrier_freqs=[qubit_frequency, qubit_frequency], + evaluation_mode=evaluation_mode, + ), + atol=atol, + rtol=rtol, + **kwargs, + ) + + self._discriminator = [DefaultDiscriminator(), DefaultDiscriminator()] + self.subsystem_dims = (3, 3) + + self._qubit_properties = [ + QubitProperties(frequency=qubit_frequency), + QubitProperties(frequency=qubit_frequency), + ] + self._target = Target( + num_qubits=2, + qubit_properties=self._qubit_properties, + dt=self.dt, + granularity=16, + ) + + measure_props = { + (0,): InstructionProperties(), + (1,): InstructionProperties(), + } + x_props = { + (0,): InstructionProperties( + duration=160e-10, + error=0, + calibration=self.pulse_command(qubit=0, amp=0.5 / self.rabi_rate_01[0], beta=4), + ), + (1,): InstructionProperties( + duration=160e-10, + error=0, + calibration=self.pulse_command(qubit=1, amp=0.5 / self.rabi_rate_01[1], beta=4), + ), + } + sx_props = { + (0,): InstructionProperties( + duration=160e-10, + error=0, + calibration=self.pulse_command(qubit=0, amp=0.25 / self.rabi_rate_01[0], beta=4), + ), + (1,): InstructionProperties( + duration=160e-10, + error=0, + calibration=self.pulse_command(qubit=1, amp=0.25 / self.rabi_rate_01[1], beta=4), + ), + } + rz_props = { + (0,): InstructionProperties(duration=0.0, error=0), + (1,): InstructionProperties(duration=0.0, error=0), + } + cx_props = { + (0, 1): InstructionProperties(duration=0, error=0), + (1, 0): InstructionProperties(duration=0, error=0), + } + + self._phi = Parameter("phi") + self._target.add_instruction(Measure(), measure_props) + self._target.add_instruction(XGate(), x_props) + self._target.add_instruction(SXGate(), sx_props) + self._target.add_instruction(RZGate(self._phi), rz_props) + self._target.add_instruction(CXGate(), cx_props) + + self.converter = InstructionToSignals( + self.dt, + carriers={"d0": qubit_frequency, "d1": qubit_frequency}, + channels=["d0", "d1"], + ) + + default_schedules = [ + self.instruction_schedule_map.get("x", (0,)), + self.instruction_schedule_map.get("sx", (0,)), + self.instruction_schedule_map.get("x", (1,)), + self.instruction_schedule_map.get("sx", (1,)), ] + self._simulated_pulse_unitaries = { - (schedule.name, (0,), ()): self.solve(schedule, (0,)) for schedule in default_schedules + (schedule.name, (schedule.channels[0].index,), ()): self.solve(schedule) + for schedule in default_schedules } + + @staticmethod + def rz_gate(qubits: List[int], theta: float) -> np.ndarray: + """Rz gate corresponding to single qubit 3 level qubit. Note: We do not try to + model accurate qutrit dynamics. + + Args: + qubits: Qubit index of gate. + theta: The angle parameter of Rz gate. + + Returns: + Matrix of Rz(theta). + """ + rz_1q = np.diag([np.exp(1.0j * idx * theta / 2) for idx in [-1, 1, 3]]) + if qubits[0] == 0: + rz = np.kron(np.eye(3), rz_1q) + elif qubits[0] == 1: + rz = np.kron(rz_1q, np.eye(3)) + return rz + + +class DefaultDiscriminator(BaseDiscriminator): + """Default Discriminator used for ``meas_level=2`` in ``SingleTransmonTestBackend`` + and ``ParallelTransmonTestBackend``. + """ + + x_0 = 0.25 # empirical threshold + + def predict(self, data: List): + """The function used to predict the labels of the data.""" + return ["0" if iq[0] > self.x_0 else "1" for iq in data] + + def config(self) -> Dict[str, Any]: + """Return the configuration of the discriminator.""" + return {"x_0": self.x_0} + + def is_trained(self) -> bool: + """Return True if this discriminator has been trained on data.""" + return True diff --git a/test/library/calibration/test_drag.py b/test/library/calibration/test_drag.py index b66e735f3b..8a8ed43ce8 100644 --- a/test/library/calibration/test_drag.py +++ b/test/library/calibration/test_drag.py @@ -26,10 +26,9 @@ from qiskit_experiments.library import RoughDrag, RoughDragCal from qiskit_experiments.library.characterization.analysis import DragCalAnalysis -from qiskit_experiments.test.mock_iq_backend import MockIQBackend -from qiskit_experiments.test.mock_iq_helpers import MockIQDragHelper as DragHelper from qiskit_experiments.calibration_management.basis_gate_library import FixedFrequencyTransmon from qiskit_experiments.calibration_management import Calibrations +from qiskit_experiments.test import SingleTransmonTestBackend @ddt @@ -41,28 +40,25 @@ def setUp(self): super().setUp() beta = Parameter("β") - + self.backend = SingleTransmonTestBackend(noise=False, atol=1e-3) with pulse.build(name="xp") as xp: - pulse.play(Drag(duration=160, amp=0.208519, sigma=40, beta=beta), DriveChannel(0)) + pulse.play( + Drag(duration=160, amp=0.5 / self.backend.rabi_rate_01[0], sigma=40, beta=beta), + DriveChannel(0), + ) self.x_plus = xp self.test_tol = 0.1 @data( - (None, None, None), - (0.0044, None, None), - (0.04, np.linspace(-4, 4, 31), {"beta": 1.8, "freq": 0.08}), + (None, None), + (np.linspace(-4, 4, 31), {"beta": 1.5, "freq": 0.08}), ) @unpack - def test_end_to_end(self, freq, betas, p0_opt): + def test_end_to_end(self, betas, p0_opt): """Test the drag experiment end to end.""" - drag_experiment_helper = DragHelper(gate_name="Drag(xp)") - if freq: - drag_experiment_helper.frequency = freq - backend = MockIQBackend(drag_experiment_helper) - - drag = RoughDrag([1], self.x_plus) + drag = RoughDrag([0], self.x_plus) drag.set_run_options(shots=200) if betas is not None: @@ -70,52 +66,43 @@ def test_end_to_end(self, freq, betas, p0_opt): if p0_opt: drag.analysis.set_options(p0=p0_opt) - expdata = drag.run(backend) + expdata = drag.run(self.backend) self.assertExperimentDone(expdata) result = expdata.analysis_results(1) # pylint: disable=no-member - self.assertTrue(abs(result.value.n - backend.experiment_helper.ideal_beta) < self.test_tol) + self.assertTrue(abs(result.value.n - self.backend.beta_01[0]) < self.test_tol) self.assertEqual(result.quality, "good") self.assertEqual(expdata.metadata["meas_level"], MeasLevel.CLASSIFIED) @data( - (0.0040, 1.0, 0.00, [1, 3, 5], None, 0.2), # partial oscillation. - (0.0020, 0.5, 0.00, [1, 3, 5], None, 1.0), # even slower oscillation with amp < 1 - (0.0040, 0.8, 0.05, [3, 5, 7], None, 0.2), # constant offset, i.e. lower SNR. - (0.0800, 0.9, 0.05, [1, 3, 5], np.linspace(-1, 1, 51), 0.2), # Beta not in range - (0.2000, 0.5, 0.10, [1, 3, 5], np.linspace(-2.5, 2.5, 51), 0.2), # Max closer to zero + ([1, 3, 5], None, 0.2), # partial oscillation. + ([3, 5, 7], None, 0.2), # constant offset, i.e. lower SNR. + ([1, 3, 5], np.linspace(-1, 1, 51), 0.2), # Beta not in range + ([1, 3, 5], np.linspace(-2.5, 2.5, 51), 0.2), # Max closer to zero ) @unpack - def test_nasty_data(self, freq, amp, offset, reps, betas, tol): + def test_nasty_data(self, reps, betas, tol): """A set of tests for non-ideal data.""" - backend = MockIQBackend( - DragHelper( - gate_name="Drag(xp)", frequency=freq, max_probability=amp, offset_probability=offset - ) - ) - drag = RoughDrag([0], self.x_plus, betas=betas) drag.set_experiment_options(reps=reps) drag.set_run_options(shots=500) - exp_data = drag.run(backend) + exp_data = drag.run(self.backend) self.assertExperimentDone(exp_data) result = exp_data.analysis_results("beta") # pylint: disable=no-member - self.assertTrue(abs(result.value.n - backend.experiment_helper.ideal_beta) < tol) + self.assertTrue(abs(result.value.n - self.backend.beta_01[0]) < tol) self.assertEqual(result.quality, "good") def test_drag_reanalysis(self): """Test edge case for re-analyzing DRAG result multiple times.""" - drag_experiment_helper = DragHelper(gate_name="Drag(xp)") - backend = MockIQBackend(drag_experiment_helper) - drag = RoughDrag([1], self.x_plus) + drag = RoughDrag([0], self.x_plus) drag.set_experiment_options(reps=[2, 4, 6]) - expdata = drag.run(backend, analysis=None) + expdata = drag.run(self.backend, analysis=None) self.assertExperimentDone(expdata) # Assume the situation we reloaded experiment data from server @@ -166,7 +153,6 @@ def test_default_circuits(self): drag = RoughDrag([0], self.x_plus) drag.set_experiment_options(reps=[2, 4, 8]) - drag.backend = MockIQBackend(DragHelper(gate_name="Drag(xp)")) circuits = drag._transpiled_circuits() for idx, expected in enumerate([4, 8, 16]): @@ -202,7 +188,7 @@ def setUp(self): library = FixedFrequencyTransmon() - self.backend = MockIQBackend(DragHelper(gate_name="Drag(x)")) + self.backend = SingleTransmonTestBackend(noise=False, atol=1e-3) self.cals = Calibrations.from_backend(self.backend, libraries=[library]) self.test_tol = 0.05 @@ -218,7 +204,6 @@ def test_update(self): self.assertExperimentDone(expdata) new_beta = self.cals.get_parameter_value("β", (0,), "x") - self.assertTrue(abs(new_beta - self.backend.experiment_helper.ideal_beta) < self.test_tol) self.assertTrue(abs(new_beta) > self.test_tol) def test_dragcal_experiment_config(self): diff --git a/test/library/calibration/test_fine_amplitude.py b/test/library/calibration/test_fine_amplitude.py index 659e0f758a..e7e898bfbe 100644 --- a/test/library/calibration/test_fine_amplitude.py +++ b/test/library/calibration/test_fine_amplitude.py @@ -20,6 +20,7 @@ from qiskit.circuit.library import XGate, SXGate from qiskit.providers.fake_provider import FakeArmonkV2 from qiskit.pulse import DriveChannel, Drag +from qiskit.transpiler import InstructionProperties from qiskit_experiments.library import ( FineXAmplitude, @@ -32,51 +33,50 @@ from qiskit_experiments.calibration_management import Calibrations from qiskit_experiments.test.mock_iq_backend import MockIQBackend from qiskit_experiments.test.mock_iq_helpers import MockIQFineAmpHelper as FineAmpHelper +from qiskit_experiments.test import SingleTransmonTestBackend @ddt class TestFineAmpEndToEnd(QiskitExperimentsTestCase): """Test the fine amplitude experiment.""" - @data(0.02, 0.03, 0.04, 0.05, 0.06, 0.07, 0.08) - def test_end_to_end_under_rotation(self, pi_ratio): - """Test the experiment end to end.""" - - amp_exp = FineXAmplitude([0]) - - error = -np.pi * pi_ratio - backend = MockIQBackend(FineAmpHelper(error, np.pi, "x")) - backend.target.add_instruction(XGate(), properties={(0,): None}) - backend.target.add_instruction(SXGate(), properties={(0,): None}) - - expdata = amp_exp.run(backend) - self.assertExperimentDone(expdata) - result = expdata.analysis_results(1) - d_theta = result.value.n - - tol = 0.04 - - self.assertAlmostEqual(d_theta, error, delta=tol) - self.assertEqual(result.quality, "good") + def setUp(self): + """Setup some schedules.""" + super().setUp() + self.backend = SingleTransmonTestBackend(noise=False, atol=1e-3) - @data(0.02, 0.03, 0.04, 0.05, 0.06, 0.07, 0.08) - def test_end_to_end_over_rotation(self, pi_ratio): + @data(0.02, 0.03, 0.04, 0.05, 0.06, 0.07, 0.08, -0.02, -0.03, -0.04, -0.05, -0.06, -0.07, -0.08) + def test_end_to_end_rotation(self, pi_ratio): """Test the experiment end to end.""" amp_exp = FineXAmplitude([0]) - error = np.pi * pi_ratio - backend = MockIQBackend(FineAmpHelper(error, np.pi, "x")) - backend.target.add_instruction(XGate(), properties={(0,): None}) - backend.target.add_instruction(SXGate(), properties={(0,): None}) - expdata = amp_exp.run(backend) + error = 1 + pi_ratio / np.pi + with pulse.build(backend=self.backend, name="x") as xgate: + pulse.play( + pulse.Drag(160, error * 0.5 / self.backend.rabi_rate_01[0], 40, 4), + pulse.DriveChannel(0), + ) + with pulse.build(backend=self.backend, name="sx") as sxgate: + pulse.play( + pulse.Drag(160, error * 0.25 / self.backend.rabi_rate_01[0], 40, 4), + pulse.DriveChannel(0), + ) + + self.backend.target.update_instruction_properties( + "x", (0,), properties=InstructionProperties(calibration=xgate) + ) + self.backend.target.update_instruction_properties( + "sx", (0,), properties=InstructionProperties(calibration=sxgate) + ) + expdata = amp_exp.run(self.backend) self.assertExperimentDone(expdata) result = expdata.analysis_results(1) d_theta = result.value.n - tol = 0.04 + tol = 0.01 - self.assertAlmostEqual(d_theta, error, delta=tol) + self.assertAlmostEqual(d_theta, pi_ratio, delta=tol) self.assertEqual(result.quality, "good") def test_circuits_serialization(self): @@ -217,9 +217,7 @@ def setUp(self): library = FixedFrequencyTransmon() - self.backend = MockIQBackend(FineAmpHelper(-np.pi * 0.07, np.pi, "xp")) - self.backend.target.add_instruction(SXGate(), properties={(0,): None}) - self.backend.target.add_instruction(XGate(), properties={(0,): None}) + self.backend = SingleTransmonTestBackend(noise=False, atol=1e-3) self.cals = Calibrations.from_backend(self.backend, libraries=[library]) def test_cal_options(self): @@ -297,8 +295,7 @@ def test_run_sx_cal(self): # Initial pulse amplitude init_amp = 0.25 - backend = MockIQBackend(FineAmpHelper(-np.pi * 0.07, np.pi / 2, "sx")) - amp_cal = FineSXAmplitudeCal([0], self.cals, "sx", backend=backend) + amp_cal = FineSXAmplitudeCal([0], self.cals, "sx", backend=self.backend) circs = amp_cal._transpiled_circuits() diff --git a/test/library/calibration/test_rabi.py b/test/library/calibration/test_rabi.py index 962b3abd92..df05f30d7f 100644 --- a/test/library/calibration/test_rabi.py +++ b/test/library/calibration/test_rabi.py @@ -63,7 +63,7 @@ def test_rabi_end_to_end(self): self.assertEqual(result.quality, "good") # The comparison is made against the object that exists in the backend for accurate testing self.assertAlmostEqual( - result.value.params["freq"], self.backend.rabi_rate_01, delta=test_tol + result.value.params["freq"], self.backend.rabi_rate_01[0], delta=test_tol ) def test_wrong_processor(self): @@ -127,7 +127,7 @@ def test_ef_rabi_end_to_end(self): result = expdata.analysis_results(1) self.assertEqual(result.quality, "good") - self.assertTrue(abs(result.value.n - self.backend.rabi_rate_12) < test_tol) + self.assertTrue(abs(result.value.n - self.backend.rabi_rate_12[0]) < test_tol) def test_ef_rabi_circuit(self): """Test the EFRabi experiment end to end.""" diff --git a/test/library/calibration/test_rough_amplitude.py b/test/library/calibration/test_rough_amplitude.py index 7618757f5c..431bd40980 100644 --- a/test/library/calibration/test_rough_amplitude.py +++ b/test/library/calibration/test_rough_amplitude.py @@ -55,7 +55,7 @@ def test_update(self): """Test that the calibrations update properly.""" tol = 0.01 - default_amp = 0.5 / self.backend.rabi_rate_01 + default_amp = 0.5 / self.backend.rabi_rate_01[0] rabi = RoughXSXAmplitudeCal( [0], self.cals, amplitudes=np.linspace(-0.1, 0.1, 11), backend=self.backend @@ -155,7 +155,7 @@ def test_ef_update(self): """Test that we properly update the pulses on the 1<->2 transition.""" tol = 0.05 - default_amp = 0.5 / self.backend.rabi_rate_12 + default_amp = 0.5 / self.backend.rabi_rate_12[0] rabi_ef = EFRoughXSXAmplitudeCal( [0], self.cals, amplitudes=np.linspace(-0.1, 0.1, 11), backend=self.backend diff --git a/test/library/characterization/test_multi_state_discrimination.py b/test/library/characterization/test_multi_state_discrimination.py index 36990bb884..f62b04be90 100644 --- a/test/library/characterization/test_multi_state_discrimination.py +++ b/test/library/characterization/test_multi_state_discrimination.py @@ -58,14 +58,14 @@ def setUp(self): d0 = pulse.DriveChannel(self.qubit) - sch_map = self.backend.defaults().instruction_schedule_map + sch_map = self.backend.instruction_schedule_map pulse_x = sch_map.get("x", (self.qubit,)).instructions[0][1].pulse amp_x = pulse_x.amp dur_x = pulse_x.duration sigma_x = pulse_x.sigma with pulse.build(name="x12") as x12: pulse.shift_frequency(anharm, d0) - pulse.play(pulse.Gaussian(dur_x, amp_x * self.backend.rabi_rate_12, sigma_x), d0) + pulse.play(pulse.Gaussian(dur_x, amp_x * self.backend.rabi_rate_12[0], sigma_x), d0) pulse.shift_frequency(-anharm, d0) self.schedules = {"x12": x12}