Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
154 changes: 142 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,112 @@ 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() -> 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(experiments):
for exp_idx, gate_cal in exps_w_cal:
experiments[exp_idx].config.calibrations.gates.remove(gate_cal)

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()
Comment thread
lcapelluto marked this conversation as resolved.
Outdated

# 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],
Comment thread
lcapelluto marked this conversation as resolved.
Outdated
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 +256,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
2 changes: 1 addition & 1 deletion qiskit/circuit/quantumcircuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -2278,7 +2278,7 @@ def add_calibration(self, gate, qubits, schedule, params=None):
if isinstance(gate, Gate):
self._calibrations[gate.name][(tuple(qubits), tuple(gate.params))] = schedule
else:
self._calibrations[gate][(tuple(qubits), tuple(params))] = schedule
self._calibrations[gate][(tuple(qubits), tuple(params or []))] = schedule


def _circuit_from_qasm(qasm):
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.
Loading