diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 0cf0a9a5cebf..cc4576fdd18f 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -18,7 +18,7 @@ import warnings import numbers import multiprocessing as mp -from collections import OrderedDict +from collections import OrderedDict, defaultdict import numpy as np from qiskit.exceptions import QiskitError from qiskit.util import is_main_process @@ -164,6 +164,7 @@ def __init__(self, *regs, name=None, global_phase=0): self._qubits = [] self._clbits = [] self._ancillas = [] + self._calibrations = defaultdict(dict) self.add_register(*regs) # Parameter table tracks instructions with variable parameters. @@ -186,6 +187,15 @@ def data(self): """ return QuantumCircuitData(self) + @property + def calibrations(self): + """Return calibration dictionary. + + The custom pulse definition of a given gate is of the form + {'gate_name': {(qubits, params): schedule}} + """ + return dict(self._calibrations) + @data.setter def data(self, data_input): """Sets the circuit data from a list of instructions and context. @@ -2253,6 +2263,23 @@ def cz(self, control_qubit, target_qubit, # pylint: disable=invalid-name return self.append(CZGate(label=label, ctrl_state=ctrl_state), [control_qubit, target_qubit], []) + def add_calibration(self, gate, qubits, schedule, params=None): + """Register a low-level, custom pulse definition for the given gate. + + Args: + gate (Union[Gate, str]): Gate information. + qubits (Union[int, Tuple[int]]): List of qubits to be measured. + schedule (Schedule): Schedule information. + params (Optional[List[Union[float, Parameter]]]): A list of parameters. + + Raises: + Exception: if the gate is of type string and params is None. + """ + if isinstance(gate, Gate): + self._calibrations[gate.name][(tuple(qubits), tuple(gate.params))] = schedule + else: + self._calibrations[gate][(tuple(qubits), tuple(params))] = schedule + def _circuit_from_qasm(qasm): # pylint: disable=cyclic-import diff --git a/qiskit/compiler/assemble.py b/qiskit/compiler/assemble.py index fd7bc3542e4f..d61249a6124b 100644 --- a/qiskit/compiler/assemble.py +++ b/qiskit/compiler/assemble.py @@ -131,6 +131,7 @@ def assemble(experiments: Union[QuantumCircuit, List[QuantumCircuit], Schedule, Raises: QiskitError: if the input cannot be interpreted as either circuits or schedules + NotImplementedError: if circuit.calibrations is not empty. """ start_time = time() experiments = experiments if isinstance(experiments, list) else [experiments] @@ -141,6 +142,15 @@ def assemble(experiments: Union[QuantumCircuit, List[QuantumCircuit], Schedule, # assemble either circuits or schedules if all(isinstance(exp, QuantumCircuit) for exp in experiments): + # calibrate circuits to schedules (if any) + for exp in experiments: + if len(exp.calibrations) != 0: + # TODO: Do something here to schedule the circuits + # Raise an error - NotImplementedError" or try to schedule by adding + # cals to inst_map and call schedule. + # if scheduled, raise a warning - "Let the user know whats happening" + raise NotImplementedError + run_config = _parse_circuit_args(parameter_binds, **run_config_common_dict) # If circuits are parameterized, bind parameters and remove from run_config diff --git a/qiskit/compiler/transpile.py b/qiskit/compiler/transpile.py index 4e4c535f4b1b..5a140811c105 100644 --- a/qiskit/compiler/transpile.py +++ b/qiskit/compiler/transpile.py @@ -182,6 +182,11 @@ def callback_func(**kwargs): _log_transpile_time(start_time, end_time) return circuits + for circuit in circuits: + if len(circuit.calibrations) != 0: + warnings.warn("Transpiling with calibrations are not supported currently.", + UserWarning) + if pass_manager is not None: _check_conflicting_argument(optimization_level=optimization_level, basis_gates=basis_gates, coupling_map=coupling_map, seed_transpiler=seed_transpiler, diff --git a/releasenotes/notes/pulse-gate-calibrations-78fd3fa5a5328761.yaml b/releasenotes/notes/pulse-gate-calibrations-78fd3fa5a5328761.yaml new file mode 100644 index 000000000000..e501057a1eb0 --- /dev/null +++ b/releasenotes/notes/pulse-gate-calibrations-78fd3fa5a5328761.yaml @@ -0,0 +1,36 @@ +--- +features: + - | + Circuits now support a new feature: Pulse gates. This feature enables users who are working + primarily with ``QuantumCircuits`` to override (for basis gates) or specify (for standard + and custom gates) a definition of a ``Gate`` operation in terms of time-ordered signals + across hardware channels. In other words, it allow users to provide a pulse-level "custom + gate calibration." + + The circuits are built exactly as before. For example:: + + from qiskit import pulse + from qiskit.circuit import QuantumCircuit, Gate + + class RxGate(Gate): + def __init__(self, theta): + super().__init__('rxtheta', 1, [theta]) + + circ = QuantumCircuit(1) + circ.h(0) + circ.append(RxGate(3.14), [1]) + + Then, the user must register the calibration with the gate via the circuit method + ``add_calibration``, which takes a schedule definition as well as the specific qubits + and parameters that it is defined for:: + + # Define the gate implementation as a schedule + with pulse.build() as custom_h_schedule: + pulse.play(pulse.library.Drag(...), pulse.DriveChannel(0)) + + with pulse.build() as q1_x180: + pulse.play(pulse.library.Gaussian(...), pulse.DriveChannel(1)) + + # Register the schedule to the gate + circ.add_calibration('h', [0], custom_h_schedule) # or gate.name string to register + circ.add_calibration(RxGate(3.14), [0], q1_x180) # Can accept gate diff --git a/test/python/circuit/test_circuit_properties.py b/test/python/circuit/test_circuit_properties.py index 080ddac9d681..005caff992c9 100644 --- a/test/python/circuit/test_circuit_properties.py +++ b/test/python/circuit/test_circuit_properties.py @@ -16,7 +16,8 @@ import unittest import numpy as np -from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit +from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit, pulse +from qiskit.circuit.library import RXGate, RYGate from qiskit.test import QiskitTestCase from qiskit.circuit.exceptions import CircuitError # pylint: disable=unused-import @@ -614,6 +615,42 @@ def test_num_qubits_multiple_register_circuit(self): circ = QuantumCircuit(q_reg1, q_reg2, q_reg3) self.assertEqual(circ.num_qubits, 18) + def test_calibrations_basis_gates(self): + """Check if the calibrations for basis gates provided are added correctly.""" + circ = QuantumCircuit(2) + + with pulse.build() as q0_x180: + pulse.play(pulse.library.Gaussian(20, 1.0, 3.0), pulse.DriveChannel(0)) + with pulse.build() as q1_y90: + pulse.play(pulse.library.Gaussian(20, -1.0, 3.0), pulse.DriveChannel(1)) + + # Add calibration + circ.add_calibration(RXGate(3.14), [0], q0_x180) + circ.add_calibration(RYGate(1.57), [1], q1_y90) + + self.assertEqual(set(circ.calibrations.keys()), {'rx', 'ry'}) + self.assertEqual(set(circ.calibrations['rx'].keys()), {((0,), (3.14,))}) + self.assertEqual(set(circ.calibrations['ry'].keys()), {((1,), (1.57,))}) + self.assertEqual(circ.calibrations['rx'][((0,), (3.14,))].instructions, + q0_x180.instructions) + self.assertEqual(circ.calibrations['ry'][((1,), (1.57,))].instructions, + q1_y90.instructions) + + def test_calibrations_custom_gates(self): + """Check if the calibrations for custom gates with params provided are added correctly.""" + circ = QuantumCircuit(3) + + with pulse.build() as q0_x180: + pulse.play(pulse.library.Gaussian(20, 1.0, 3.0), pulse.DriveChannel(0)) + + # Add calibrations with a custom gate 'rxt' + circ.add_calibration('rxt', [0], q0_x180, params=[1.57, 3.14, 4.71]) + + self.assertEqual(set(circ.calibrations.keys()), {'rxt'}) + self.assertEqual(set(circ.calibrations['rxt'].keys()), {((0,), (1.57, 3.14, 4.71))}) + self.assertEqual(circ.calibrations['rxt'][((0,), (1.57, 3.14, 4.71))].instructions, + q0_x180.instructions) + if __name__ == '__main__': unittest.main()