Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 144 additions & 12 deletions qiskit/assembler/assemble_circuits.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,36 @@
# that they have been altered from the originals.

"""Assemble function for converting a list of circuits into a qobj."""
from collections import defaultdict
from typing import Dict, List, Optional, Tuple

from qiskit.assembler.run_config import RunConfig
from qiskit.assembler.assemble_schedules import _assemble_instructions as _assemble_schedule
from qiskit.circuit import QuantumCircuit
from qiskit.qobj import (QasmQobj, QobjExperimentHeader,
QasmQobjInstruction, QasmQobjExperimentConfig, QasmQobjExperiment,
QasmQobjConfig)
QasmQobjConfig, QasmExperimentCalibrations, GateCalibration,
PulseQobjInstruction, PulseLibraryItem, converters, QobjHeader)
from qiskit.tools.parallel import parallel_map


def _assemble_circuit(circuit):
# header stuff
PulseLibrary = Dict[str, List[complex]]


def _assemble_circuit(
circuit: QuantumCircuit,
run_config: RunConfig
) -> Tuple[QasmQobjExperiment, Optional[PulseLibrary]]:
"""Assemble one circuit.

Args:
circuit: circuit to assemble
run_config: configuration of the runtime environment

Returns:
One experiment for the QasmQobj, and pulse library for pulse gates (which could be None)
"""
# header data
num_qubits = 0
memory_slots = 0
qubit_labels = []
Expand Down Expand Up @@ -47,9 +69,14 @@ def _assemble_circuit(circuit):
creg_sizes=creg_sizes,
name=circuit.name,
global_phase=circuit.global_phase)

# TODO: why do we need n_qubits and memory_slots in both the header and the config
config = QasmQobjExperimentConfig(n_qubits=num_qubits, memory_slots=memory_slots)

calibrations, pulse_library = _assemble_pulse_gates(circuit, run_config)
if calibrations:
config.calibrations = calibrations

# Convert conditionals from QASM-style (creg ?= int) to qobj-style
# (register_bit ?= 1), by assuming device has unlimited register slots
# (supported only for simulators). Map all measures to a register matching
Expand Down Expand Up @@ -105,21 +132,114 @@ def _assemble_circuit(circuit):
del instruction._condition

instructions.append(instruction)
return QasmQobjExperiment(instructions=instructions, header=header,
config=config)
return (QasmQobjExperiment(instructions=instructions, header=header, config=config),
pulse_library)


def _assemble_pulse_gates(
circuit: QuantumCircuit,
run_config: RunConfig
) -> Tuple[Optional[QasmExperimentCalibrations], Optional[PulseLibrary]]:
"""Assemble and return the circuit calibrations and associated pulse library, if there are any.
The calibrations themselves may reference the pulse library which is returned as a dict.

Args:
circuit: circuit which may have pulse calibrations
run_config: configuration of the runtime environment

Returns:
The calibrations and pulse library, if there are any
"""
if not circuit.calibrations:
return None, None
Comment thread
lcapelluto marked this conversation as resolved.
if not hasattr(run_config, 'parametric_pulses'):
run_config.parametric_pulses = []
calibrations = []
pulse_library = {}
Comment thread
lcapelluto marked this conversation as resolved.
for gate, cals in circuit.calibrations.items():
for (qubits, params), schedule in cals.items():
qobj_instructions, _ = _assemble_schedule(
schedule,
converters.InstructionToQobjConverter(PulseQobjInstruction),
run_config,
pulse_library)
calibrations.append(
GateCalibration(str(gate), list(qubits), list(params), qobj_instructions))
Comment thread
lcapelluto marked this conversation as resolved.
return QasmExperimentCalibrations(gates=calibrations), pulse_library


def _extract_common_calibrations(
experiments: List[QasmQobjExperiment]
) -> Tuple[List[QasmQobjExperiment], Optional[QasmExperimentCalibrations]]:
"""Given a list of ``QasmQobjExperiment``s, each of which may have calibrations in their
``config``, collect common calibrations into a global ``QasmExperimentCalibrations``
and delete them from their local experiments.

Args:
experiments: The list of Qasm experiments that are being assembled into one qobj

Returns:
The input experiments with modified calibrations, and common calibrations, if there
are any
"""
def index_calibrations() -> Dict[int, List[Tuple[int, GateCalibration]]]:
"""Map each calibration to all experiments that contain it."""
exp_indices = defaultdict(list)
for exp_idx, exp in enumerate(experiments):
for gate_cal in exp.config.calibrations.gates:
# They must be keyed on the hash or identical cals will be indexed separately
exp_indices[hash(gate_cal)].append((exp_idx, gate_cal))
return exp_indices

def collect_common_calibrations() -> List[GateCalibration]:
"""If a gate calibration appears in all experiments, collect it."""
common_calibrations = []
for _, exps_w_cal in exp_indices.items():
if len(exps_w_cal) == len(experiments):
_, gate_cal = exps_w_cal[0]
common_calibrations.append(gate_cal)
return common_calibrations

def remove_common_gate_calibrations(exps: List[QasmQobjExperiment]) -> None:
"""For calibrations that appear in all experiments, remove them from the individual
experiment's ``config.calibrations``."""
for _, exps_w_cal in exp_indices.items():
if len(exps_w_cal) == len(exps):
for exp_idx, gate_cal in exps_w_cal:
exps[exp_idx].config.calibrations.gates.remove(gate_cal)
Comment thread
taalexander marked this conversation as resolved.

if not (experiments and all(hasattr(exp.config, 'calibrations') for exp in experiments)):
# No common calibrations
return experiments, None

exp_indices = index_calibrations()
common_calibrations = collect_common_calibrations()
remove_common_gate_calibrations(experiments)
Comment thread
lcapelluto marked this conversation as resolved.

# Remove the ``calibrations`` attribute if it's now empty
for exp in experiments:
if not exp.config.calibrations.gates:
del exp.config.calibrations

return experiments, QasmExperimentCalibrations(gates=common_calibrations)


def assemble_circuits(circuits, run_config, qobj_id, qobj_header):
def assemble_circuits(
circuits: List[QuantumCircuit],
run_config: RunConfig,
qobj_id: int,
qobj_header: QobjHeader
) -> QasmQobj:
"""Assembles a list of circuits into a qobj that can be run on the backend.

Args:
circuits (list[QuantumCircuit]): circuit(s) to assemble
qobj_id (int): identifier for the generated qobj
qobj_header (QobjHeader): header to pass to the results
run_config (RunConfig): configuration of the runtime environment
circuits: circuit(s) to assemble
run_config: configuration of the runtime environment
qobj_id: identifier for the generated qobj
qobj_header: header to pass to the results

Returns:
QasmQobj: the qobj to be run on the backends
The qobj to be run on the backends
"""
qobj_config = QasmQobjConfig()
if run_config:
Expand All @@ -138,7 +258,19 @@ def assemble_circuits(circuits, run_config, qobj_id, qobj_header):
qobj_config.memory_slots = max(memory_slot_sizes)
qobj_config.n_qubits = max(qubit_sizes)

experiments = parallel_map(_assemble_circuit, circuits)
experiments_and_pulse_libs = parallel_map(_assemble_circuit, circuits, [run_config])
experiments = []
pulse_library = {}
for exp, lib in experiments_and_pulse_libs:
Comment thread
lcapelluto marked this conversation as resolved.
experiments.append(exp)
if lib:
pulse_library.update(lib)
if pulse_library:
qobj_config.pulse_library = [PulseLibraryItem(name=name, samples=samples)
for name, samples in pulse_library.items()]
experiments, calibrations = _extract_common_calibrations(experiments)
if calibrations and calibrations.gates:
qobj_config.calibrations = calibrations

return QasmQobj(qobj_id=qobj_id,
config=qobj_config,
Expand Down
21 changes: 8 additions & 13 deletions qiskit/compiler/assemble.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,6 @@ 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]
Expand All @@ -142,16 +141,8 @@ 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)
run_config = _parse_circuit_args(parameter_binds, backend, parametric_pulses,
**run_config_common_dict)

# If circuits are parameterized, bind parameters and remove from run_config
bound_experiments, run_config = _expand_parameters(circuits=experiments,
Expand Down Expand Up @@ -352,7 +343,7 @@ def _parse_pulse_args(backend, qubit_lo_freq, meas_lo_freq, qubit_lo_range,
return run_config


def _parse_circuit_args(parameter_binds, **run_config):
def _parse_circuit_args(parameter_binds, backend, parametric_pulses, **run_config):
"""Build a circuit RunConfig replacing unset arguments with defaults derived from the `backend`.
See `assemble` for more information on the required arguments.

Expand All @@ -361,9 +352,13 @@ def _parse_circuit_args(parameter_binds, **run_config):
and determines the runtime environment.
"""
parameter_binds = parameter_binds or []

# create run configuration and populate
run_config_dict = dict(parameter_binds=parameter_binds, **run_config)
if backend:
run_config_dict['parametric_pulses'] = getattr(backend.configuration(), 'parametric_pulses',
[])
if parametric_pulses:
run_config_dict['parametric_pulses'] = parametric_pulses
Comment thread
taalexander marked this conversation as resolved.
run_config = RunConfig(**{k: v for k, v in run_config_dict.items() if v is not None})

return run_config
Expand Down
4 changes: 4 additions & 0 deletions qiskit/qobj/qasm_qobj.py
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,10 @@ def __init__(self, name, qubits, params, instructions):
self.params = params
self.instructions = instructions

def __hash__(self):
return hash((self.name, tuple(self.qubits), tuple(self.params),
tuple(str(inst) for inst in self.instructions)))

def to_dict(self):
"""Return a dictionary format representation of the Gate Calibration.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,8 @@ features:
# 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

Previously, this functionality could only be capitalized on through complete Pulse
Schedules. With updates to the QASM Qobj and the assembler, jobs can now be sent as
circuit jobs, augmented with calibration libraries to communicate your custom definitions
to the backend.
104 changes: 103 additions & 1 deletion test/python/compiler/test_assembler.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

import numpy as np
import qiskit.pulse as pulse
from qiskit.circuit import Instruction, Parameter
from qiskit.circuit import Instruction, Gate, Parameter
from qiskit.circuit import QuantumRegister, ClassicalRegister, QuantumCircuit
from qiskit.compiler.assemble import assemble
from qiskit.exceptions import QiskitError
Expand All @@ -35,6 +35,17 @@
from qiskit.validation.jsonschema import SchemaValidationError


class RxGate(Gate):
"""Used to test custom gate assembly.

Useful for testing pulse gates with parameters, as well.
Note: Parallel maps (e.g., in assemble_circuits) pickle their input,
so circuit features have to be defined top level.
"""
def __init__(self, theta):
super().__init__('rxtheta', 1, [theta])


class TestCircuitAssembler(QiskitTestCase):
"""Tests for assembling circuits to qobj."""

Expand Down Expand Up @@ -353,6 +364,97 @@ def test_circuit_with_global_phase(self):
self.assertEqual(getattr(qobj.experiments[0].header, 'global_phase'),
.3 * np.pi)

def test_pulse_gates_single_circ(self):
Comment thread
lcapelluto marked this conversation as resolved.
"""Test that we can add calibrations to circuits."""
theta = Parameter('theta')
circ = QuantumCircuit(2)
circ.h(0)
circ.append(RxGate(3.14), [0])
circ.append(RxGate(theta), [1])
circ = circ.assign_parameters({theta: 3.14})

with pulse.build() as custom_h_schedule:
pulse.play(pulse.library.Drag(50, 0.15, 4, 2), pulse.DriveChannel(0))

with pulse.build() as x180:
pulse.play(pulse.library.Gaussian(50, 0.2, 5), pulse.DriveChannel(1))

circ.add_calibration('h', [0], custom_h_schedule)
circ.add_calibration(RxGate(3.14), [0], x180)
circ.add_calibration(RxGate(3.14), [1], x180)

qobj = assemble(circ, FakeOpenPulse2Q())
# Only one circuit, so everything is stored at the job level
cals = qobj.config.calibrations
lib = qobj.config.pulse_library
self.assertFalse(hasattr(qobj.experiments[0].config, 'calibrations'))
self.assertEqual([gate.name == 'rxtheta' for gate in cals.gates].count(True), 2)
self.assertEqual([gate.name == 'h' for gate in cals.gates].count(True), 1)
self.assertEqual(len(lib), 2)
self.assertTrue(all(len(item.samples) == 50 for item in lib))

def test_pulse_gates_with_parameteric_pulses(self):
"""Test that pulse gates are assembled efficiently for backends that enable
parametric pulses.
"""
with pulse.build() as custom_h_schedule:
pulse.play(pulse.library.Drag(50, 0.15, 4, 2), pulse.DriveChannel(0))

circ = QuantumCircuit(2)
circ.h(0)
circ.add_calibration('h', [0], custom_h_schedule)

backend = FakeOpenPulse2Q()
backend.configuration().parametric_pulses = ['drag']
qobj = assemble(circ, backend)
self.assertFalse(hasattr(qobj.config, 'pulse_library'))
self.assertTrue(hasattr(qobj.config, 'calibrations'))

def test_pulse_gates_multiple_circuits(self):
"""Test one circuit with cals and another without."""
with pulse.build() as dummy_sched:
pulse.play(pulse.library.Drag(50, 0.15, 4, 2), pulse.DriveChannel(0))

circ = QuantumCircuit(2)
circ.h(0)
circ.append(RxGate(3.14), [1])
circ.add_calibration('h', [0], dummy_sched)
circ.add_calibration(RxGate(3.14), [1], dummy_sched)

circ2 = QuantumCircuit(2)
circ2.h(0)

qobj = assemble([circ, circ2], FakeOpenPulse2Q())
self.assertEqual(len(qobj.config.pulse_library), 1)
self.assertEqual(len(qobj.experiments[0].config.calibrations.gates), 2)
self.assertFalse(hasattr(qobj.config, 'calibrations'))
self.assertFalse(hasattr(qobj.experiments[1].config, 'calibrations'))

def test_pulse_gates_common_cals(self):
"""Test that common calibrations are added at the top level."""
with pulse.build() as dummy_sched:
pulse.play(pulse.library.Drag(50, 0.15, 4, 2), pulse.DriveChannel(0))

circ = QuantumCircuit(2)
circ.h(0)
circ.append(RxGate(3.14), [1])
circ.add_calibration('h', [0], dummy_sched)
circ.add_calibration(RxGate(3.14), [1], dummy_sched)

circ2 = QuantumCircuit(2)
circ2.h(0)
circ2.add_calibration(RxGate(3.14), [1], dummy_sched)

qobj = assemble([circ, circ2], FakeOpenPulse2Q())
# Identical pulses are only added once
self.assertEqual(len(qobj.config.pulse_library), 1)
# Identical calibrations are only added once
self.assertEqual(qobj.config.calibrations.gates[0].name, 'rxtheta')
self.assertEqual(qobj.config.calibrations.gates[0].params, [3.14])
self.assertEqual(qobj.config.calibrations.gates[0].qubits, [1])
self.assertEqual(len(qobj.experiments[0].config.calibrations.gates), 1)
self.assertFalse(hasattr(qobj.experiments[1].config, 'calibrations'))


class TestPulseAssembler(QiskitTestCase):
"""Tests for assembling schedules to qobj."""
Expand Down