diff --git a/qiskit/compiler/transpile.py b/qiskit/compiler/transpile.py index 5a140811c105..c004b4953fc2 100644 --- a/qiskit/compiler/transpile.py +++ b/qiskit/compiler/transpile.py @@ -18,6 +18,7 @@ from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.providers import BaseBackend from qiskit.providers.models import BackendProperties +from qiskit.providers.v2.backend import Backend from qiskit.providers.models.backendproperties import Gate from qiskit.transpiler import Layout, CouplingMap, PropertySet, PassManager from qiskit.transpiler.basepasses import BasePass @@ -40,7 +41,7 @@ def transpile(circuits: Union[QuantumCircuit, List[QuantumCircuit]], - backend: Optional[BaseBackend] = None, + backend: Optional[Union[BaseBackend, Backend]] = None, basis_gates: Optional[List[str]] = None, coupling_map: Optional[Union[CouplingMap, List[List[int]]]] = None, backend_properties: Optional[BackendProperties] = None, @@ -246,8 +247,12 @@ def _check_circuits_coupling_map(circuits, transpile_args, backend): max_qubits = parsed_coupling_map.size() # If coupling_map is None, the limit might be in the backend (like in 1Q devices) - elif backend is not None and not backend.configuration().simulator: - max_qubits = backend.configuration().n_qubits + elif backend is not None: + if isinstance(backend, Backend): + max_qubits = backend.target.num_qubits + else: + if not backend.configuration().simulator: + max_qubits = backend.configuration().n_qubits if max_qubits is not None and (num_qubits > max_qubits): raise TranspilerError('Number of qubits ({}) '.format(num_qubits) + @@ -474,8 +479,11 @@ def _create_faulty_qubits_map(backend): def _parse_basis_gates(basis_gates, backend, circuits): # try getting basis_gates from user, else backend if basis_gates is None: - if getattr(backend, 'configuration', None): - basis_gates = getattr(backend.configuration(), 'basis_gates', None) + if isinstance(backend, Backend): + basis_gates = backend.target.basis_gates + else: + if getattr(backend, 'configuration', None): + basis_gates = getattr(backend.configuration(), 'basis_gates', None) # basis_gates could be None, or a list of basis, e.g. ['u3', 'cx'] if basis_gates is None or (isinstance(basis_gates, list) and all(isinstance(i, str) for i in basis_gates)): @@ -487,17 +495,20 @@ def _parse_basis_gates(basis_gates, backend, circuits): def _parse_coupling_map(coupling_map, backend, num_circuits): # try getting coupling_map from user, else backend if coupling_map is None: - if getattr(backend, 'configuration', None): - configuration = backend.configuration() - if hasattr(configuration, 'coupling_map') and configuration.coupling_map: - faulty_map = _create_faulty_qubits_map(backend) - if faulty_map: - coupling_map = CouplingMap() - for qubit1, qubit2 in configuration.coupling_map: - if faulty_map[qubit1] is not None and faulty_map[qubit2] is not None: - coupling_map.add_edge(faulty_map[qubit1], faulty_map[qubit2]) - else: - coupling_map = CouplingMap(configuration.coupling_map) + if isinstance(backend, Backend): + coupling_map = backend.target.coupling_map + else: + if getattr(backend, 'configuration', None): + configuration = backend.configuration() + if hasattr(configuration, 'coupling_map') and configuration.coupling_map: + faulty_map = _create_faulty_qubits_map(backend) + if faulty_map: + coupling_map = CouplingMap() + for qubit1, qubit2 in configuration.coupling_map: + if faulty_map[qubit1] is not None and faulty_map[qubit2] is not None: + coupling_map.add_edge(faulty_map[qubit1], faulty_map[qubit2]) + else: + coupling_map = CouplingMap(configuration.coupling_map) # coupling_map could be None, or a list of lists, e.g. [[0, 1], [2, 1]] if coupling_map is None or isinstance(coupling_map, CouplingMap): @@ -514,32 +525,36 @@ def _parse_coupling_map(coupling_map, backend, num_circuits): def _parse_backend_properties(backend_properties, backend, num_circuits): # try getting backend_properties from user, else backend if backend_properties is None: - if getattr(backend, 'properties', None): - backend_properties = backend.properties() - if backend_properties and \ - (backend_properties.faulty_qubits() or backend_properties.faulty_gates()): - faulty_qubits = sorted(backend_properties.faulty_qubits(), reverse=True) - faulty_edges = [gates.qubits for gates in backend_properties.faulty_gates()] - # remove faulty qubits in backend_properties.qubits - for faulty_qubit in faulty_qubits: - del backend_properties.qubits[faulty_qubit] - - gates = [] - for gate in backend_properties.gates: - # remove gates using faulty edges or with faulty qubits (and remap the - # gates in terms of faulty_qubits_map) - faulty_qubits_map = _create_faulty_qubits_map(backend) - if any([faulty_qubits_map[qubits] is not None for qubits in gate.qubits]) or \ - gate.qubits in faulty_edges: - continue - gate_dict = gate.to_dict() - replacement_gate = Gate.from_dict(gate_dict) - gate_dict['qubits'] = [faulty_qubits_map[qubit] for qubit in gate.qubits] - args = '_'.join([str(qubit) for qubit in gate_dict['qubits']]) - gate_dict['name'] = "%s%s" % (gate_dict['gate'], args) - gates.append(replacement_gate) - - backend_properties.gates = gates + if isinstance(backend, Backend): + backend_properties = backend.properties + else: + if getattr(backend, 'properties', None): + backend_properties = backend.properties() + if backend_properties and \ + (backend_properties.faulty_qubits() or backend_properties.faulty_gates()): + faulty_qubits = sorted(backend_properties.faulty_qubits(), reverse=True) + faulty_edges = [gates.qubits for gates in backend_properties.faulty_gates()] + # remove faulty qubits in backend_properties.qubits + for faulty_qubit in faulty_qubits: + del backend_properties.qubits[faulty_qubit] + + gates = [] + for gate in backend_properties.gates: + # remove gates using faulty edges or with faulty qubits (and remap the + # gates in terms of faulty_qubits_map) + faulty_qubits_map = _create_faulty_qubits_map(backend) + any_faulty_qubits = any( + [faulty_qubits_map[qubits] is not None for qubits in gate.qubits]) + if any_faulty_qubits or gate.qubits in faulty_edges: + continue + gate_dict = gate.to_dict() + replacement_gate = Gate.from_dict(gate_dict) + gate_dict['qubits'] = [faulty_qubits_map[qubit] for qubit in gate.qubits] + args = '_'.join([str(qubit) for qubit in gate_dict['qubits']]) + gate_dict['name'] = "%s%s" % (gate_dict['gate'], args) + gates.append(replacement_gate) + + backend_properties.gates = gates if not isinstance(backend_properties, list): backend_properties = [backend_properties] * num_circuits return backend_properties @@ -548,11 +563,18 @@ def _parse_backend_properties(backend_properties, backend, num_circuits): def _parse_backend_num_qubits(backend, num_circuits): if backend is None: return [None] * num_circuits - if not isinstance(backend, list): - return [backend.configuration().n_qubits] * num_circuits - backend_num_qubits = [] - for a_backend in backend: - backend_num_qubits.append(a_backend.configuration().n_qubits) + if isinstance(backend, Backend): + if not isinstance(backend, list): + return [backend.target.num_qubits] * num_circuits + backend_num_qubits = [] + for a_backend in backend: + backend_num_qubits.append(a_backend.target.num_qubits) + else: + if not isinstance(backend, list): + return [backend.configuration().n_qubits] * num_circuits + backend_num_qubits = [] + for a_backend in backend: + backend_num_qubits.append(a_backend.configuration().n_qubits) return backend_num_qubits @@ -633,11 +655,14 @@ def _parse_callback(callback, num_circuits): def _parse_faulty_qubits_map(backend, num_circuits): if backend is None: return [None] * num_circuits - if not isinstance(backend, list): - return [_create_faulty_qubits_map(backend)] * num_circuits - faulty_qubits_map = [] - for a_backend in backend: - faulty_qubits_map.append(_create_faulty_qubits_map(a_backend)) + elif isinstance(backend, Backend): + return [None] * num_circuits + else: + if not isinstance(backend, list): + return [_create_faulty_qubits_map(backend)] * num_circuits + faulty_qubits_map = [] + for a_backend in backend: + faulty_qubits_map.append(_create_faulty_qubits_map(a_backend)) return faulty_qubits_map diff --git a/qiskit/execute.py b/qiskit/execute.py index 2afd16e6286d..6df1f0feb902 100644 --- a/qiskit/execute.py +++ b/qiskit/execute.py @@ -23,6 +23,7 @@ from time import time from qiskit.compiler import transpile, assemble, schedule from qiskit.qobj.utils import MeasLevel, MeasReturnType +from qiskit.providers.v2.backend import Backend from qiskit.pulse import Schedule from qiskit.exceptions import QiskitError @@ -57,7 +58,7 @@ def execute(experiments, backend, experiments (QuantumCircuit or list[QuantumCircuit] or Schedule or list[Schedule]): Circuit(s) or pulse schedule(s) to execute - backend (BaseBackend): + backend (BaseBackend|Backend): Backend to execute circuits on. Transpiler options are automatically grabbed from backend.configuration() and backend.properties(). @@ -263,34 +264,45 @@ def execute(experiments, backend, meas_map=meas_map, method=scheduling_method) - # assembling the circuits into a qobj to be run on the backend - qobj = assemble(experiments, - qobj_id=qobj_id, - qobj_header=qobj_header, - shots=shots, - memory=memory, - max_credits=max_credits, - seed_simulator=seed_simulator, - default_qubit_los=default_qubit_los, - default_meas_los=default_meas_los, - schedule_los=schedule_los, - meas_level=meas_level, - meas_return=meas_return, - memory_slots=memory_slots, - memory_slot_size=memory_slot_size, - rep_time=rep_time, - rep_delay=rep_delay, - parameter_binds=parameter_binds, - backend=backend, - init_qubits=init_qubits, - **run_config) - - # executing the circuits on the backend and returning the job - start_time = time() - job = backend.run(qobj, **run_config) - end_time = time() - _log_submission_time(start_time, end_time) - return job + # v2 Providers Backend + if isinstance(backend, Backend): + start_time = time() + backend.set_options(shots=shots, **run_config) + job = backend.run(experiments) + end_time = time() + _log_submission_time(start_time, end_time) + return job + + # v1 Providers Backend + else: + # assembling the circuits into a qobj to be run on the backend + qobj = assemble(experiments, + qobj_id=qobj_id, + qobj_header=qobj_header, + shots=shots, + memory=memory, + max_credits=max_credits, + seed_simulator=seed_simulator, + default_qubit_los=default_qubit_los, + default_meas_los=default_meas_los, + schedule_los=schedule_los, + meas_level=meas_level, + meas_return=meas_return, + memory_slots=memory_slots, + memory_slot_size=memory_slot_size, + rep_time=rep_time, + rep_delay=rep_delay, + parameter_binds=parameter_binds, + backend=backend, + init_qubits=init_qubits, + **run_config) + + # executing the circuits on the backend and returning the job + start_time = time() + job = backend.run(qobj, **run_config) + end_time = time() + _log_submission_time(start_time, end_time) + return job def _check_conflicting_argument(**kargs): diff --git a/qiskit/providers/basicaer/basicaerjob.py b/qiskit/providers/basicaer/basicaerjob.py index 08db10b7eea2..d2666f10c3e1 100644 --- a/qiskit/providers/basicaer/basicaerjob.py +++ b/qiskit/providers/basicaer/basicaerjob.py @@ -12,125 +12,21 @@ """This module implements the job class used by Basic Aer Provider.""" -from concurrent import futures import sys -import functools -from qiskit.providers import BaseJob, JobStatus, JobError -from qiskit.qobj import validate_qobj_against_schema +from qiskit.providers.v2 import JobV1 +from qiskit.result.counts import Counts -def requires_submit(func): - """ - Decorator to ensure that a submit has been performed before - calling the method. +class BasicAerJob(JobV1): + """BasicAerJob class.""" - Args: - func (callable): test function to be decorated. + def __init__(self, job_id, backend, result_data, time_taken): + super().__init__(job_id, backend, time_taken=time_taken) + self.result_data = result_data - Returns: - callable: the decorated function. - """ - @functools.wraps(func) - def _wrapper(self, *args, **kwargs): - if self._future is None: - raise JobError("Job not submitted yet!. You have to .submit() first!") - return func(self, *args, **kwargs) - return _wrapper - - -class BasicAerJob(BaseJob): - """BasicAerJob class. - - Attributes: - _executor (futures.Executor): executor to handle asynchronous jobs - """ - - if sys.platform in ['darwin', 'win32']: - _executor = futures.ThreadPoolExecutor() - else: - _executor = futures.ProcessPoolExecutor() - - def __init__(self, backend, job_id, fn, qobj): - super().__init__(backend, job_id) - self._fn = fn - self._qobj = qobj - self._future = None - - def submit(self): - """Submit the job to the backend for execution. - - Raises: - QobjValidationError: if the JSON serialization of the Qobj passed - during construction does not validate against the Qobj schema. - - JobError: if trying to re-submit the job. - """ - if self._future is not None: - raise JobError("We have already submitted the job!") - - validate_qobj_against_schema(self._qobj) - self._future = self._executor.submit(self._fn, self._job_id, self._qobj) - - @requires_submit - def result(self, timeout=None): - # pylint: disable=arguments-differ - """Get job result. The behavior is the same as the underlying - concurrent Future objects, - - https://docs.python.org/3/library/concurrent.futures.html#future-objects - - Args: - timeout (float): number of seconds to wait for results. - - Returns: - qiskit.Result: Result object - - Raises: - concurrent.futures.TimeoutError: if timeout occurred. - concurrent.futures.CancelledError: if job cancelled before completed. - """ - return self._future.result(timeout=timeout) - - @requires_submit - def cancel(self): - return self._future.cancel() - - @requires_submit def status(self): - """Gets the status of the job by querying the Python's future - - Returns: - qiskit.providers.JobStatus: The current JobStatus - - Raises: - JobError: If the future is in unexpected state - concurrent.futures.TimeoutError: if timeout occurred. - """ - # The order is important here - if self._future.running(): - _status = JobStatus.RUNNING - elif self._future.cancelled(): - _status = JobStatus.CANCELLED - elif self._future.done(): - _status = JobStatus.DONE if self._future.exception() is None else JobStatus.ERROR - else: - # Note: There is an undocumented Future state: PENDING, that seems to show up when - # the job is enqueued, waiting for someone to pick it up. We need to deal with this - # state but there's no public API for it, so we are assuming that if the job is not - # in any of the previous states, is PENDING, ergo INITIALIZING for us. - _status = JobStatus.INITIALIZING - - return _status - - def backend(self): - """Return the instance of the backend used for this job.""" - return self._backend - - def qobj(self): - """Return the Qobj submitted for this job. + return 'COMPLETE' - Returns: - Qobj: the Qobj submitted for this job. - """ - return self._qobj + def wait_for_final_state(self): + return True diff --git a/qiskit/providers/basicaer/basicaerprovider.py b/qiskit/providers/basicaer/basicaerprovider.py index 3f410deff562..aab5240b1dc9 100644 --- a/qiskit/providers/basicaer/basicaerprovider.py +++ b/qiskit/providers/basicaer/basicaerprovider.py @@ -17,9 +17,9 @@ import logging from qiskit.exceptions import QiskitError -from qiskit.providers import BaseProvider +from qiskit.providers.v2 import ProviderV1 from qiskit.providers.exceptions import QiskitBackendNotFoundError -from qiskit.providers.providerutils import resolve_backend_name, filter_backends +from qiskit.providers.providerutils import filter_backends from .qasm_simulator import QasmSimulatorPy from .statevector_simulator import StatevectorSimulatorPy @@ -35,23 +35,39 @@ ] -class BasicAerProvider(BaseProvider): +def _resolve_backend_name(name, backends, deprecated, aliased): + available = [backend.name for backend in backends] + + resolved_name = deprecated.get(name, aliased.get(name, name)) + if isinstance(resolved_name, list): + resolved_name = next((b for b in resolved_name if b in available), "") + + if resolved_name not in available: + raise LookupError("backend '{}' not found.".format(name)) + + if name in deprecated: + logger.warning("Backend '%s' is deprecated. Use '%s'.", name, + resolved_name) + + return resolved_name + + +class BasicAerProvider(ProviderV1): """Provider for Basic Aer backends.""" def __init__(self, *args, **kwargs): - super().__init__(args, kwargs) + super().__init__() # Populate the list of Basic Aer backends. self._backends = self._verify_backends() def get_backend(self, name=None, **kwargs): backends = self._backends.values() - # Special handling of the `name` parameter, to support alias resolution # and deprecated names. if name: try: - resolved_name = resolve_backend_name( + resolved_name = _resolve_backend_name( name, backends, self._deprecated_backend_names(), {} @@ -71,13 +87,13 @@ def backends(self, name=None, filters=None, **kwargs): # and deprecated names. if name: try: - resolved_name = resolve_backend_name( + resolved_name = _resolve_backend_name( name, backends, self._deprecated_backend_names(), {} ) backends = [backend for backend in backends if - backend.name() == resolved_name] + backend.name == resolved_name] except LookupError: return [] @@ -110,9 +126,10 @@ def _verify_backends(self): for backend_cls in SIMULATORS: try: backend_instance = self._get_backend_instance(backend_cls) - backend_name = backend_instance.name() + backend_name = backend_instance.name ret[backend_name] = backend_instance except QiskitError as err: + print(err) # Ignore backends that could not be initialized. logger.info('Basic Aer backend %s is not available: %s', backend_cls, str(err)) @@ -131,7 +148,7 @@ def _get_backend_instance(self, backend_cls): """ # Verify that the backend can be instantiated. try: - backend_instance = backend_cls(provider=self) + backend_instance = backend_cls() except Exception as err: raise QiskitError('Backend %s could not be instantiated: %s' % (backend_cls, err)) diff --git a/qiskit/providers/basicaer/basicaertools.py b/qiskit/providers/basicaer/basicaertools.py index 04814aa12e8a..0c3c66b8834c 100644 --- a/qiskit/providers/basicaer/basicaertools.py +++ b/qiskit/providers/basicaer/basicaertools.py @@ -16,6 +16,8 @@ from string import ascii_uppercase, ascii_lowercase import numpy as np + +from qiskit.qobj import QasmQobjInstruction from qiskit.exceptions import QiskitError @@ -176,3 +178,74 @@ def _einsum_matmul_index_helper(gate_indices, number_of_qubits): # Combine indices into matrix multiplication string format # for numpy.einsum function return mat_left, mat_right, tens_in, tens_out + + +def assemble_circuit(circuit): + instructions = [] + num_qubits = 0 + memory_slots = 0 + qubit_labels = [] + clbit_labels = [] + + qreg_sizes = [] + creg_sizes = [] + for qreg in circuit.qregs: + qreg_sizes.append([qreg.name, qreg.size]) + for j in range(qreg.size): + qubit_labels.append([qreg.name, j]) + num_qubits += qreg.size + for creg in circuit.cregs: + creg_sizes.append([creg.name, creg.size]) + for j in range(creg.size): + clbit_labels.append([creg.name, j]) + memory_slots += creg.size + + is_conditional_experiment = any(op.condition for (op, qargs, cargs) in circuit.data) + max_conditional_idx = 0 + + for op_context in circuit.data: + instruction = op_context[0].assemble() + + # Add register attributes to the instruction + qargs = op_context[1] + cargs = op_context[2] + if qargs: + qubit_indices = [qubit_labels.index([qubit.register.name, qubit.index]) + for qubit in qargs] + instruction.qubits = qubit_indices + if cargs: + clbit_indices = [clbit_labels.index([clbit.register.name, clbit.index]) + for clbit in cargs] + instruction.memory = clbit_indices + # If the experiment has conditional instructions, assume every + # measurement result may be needed for a conditional gate. + if instruction.name == "measure" and is_conditional_experiment: + instruction.register = clbit_indices + + # To convert to a qobj-style conditional, insert a bfunc prior + # to the conditional instruction to map the creg ?= val condition + # onto a gating register bit. + if hasattr(instruction, '_condition'): + ctrl_reg, ctrl_val = instruction._condition + mask = 0 + val = 0 + for clbit in clbit_labels: + if clbit[0] == ctrl_reg.name: + mask |= (1 << clbit_labels.index(clbit)) + val |= (((ctrl_val >> clbit[1]) & 1) << clbit_labels.index(clbit)) + + conditional_reg_idx = memory_slots + max_conditional_idx + conversion_bfunc = QasmQobjInstruction(name='bfunc', + mask="0x%X" % mask, + relation='==', + val="0x%X" % val, + register=conditional_reg_idx) + instructions.append(conversion_bfunc) + instruction.conditional = conditional_reg_idx + max_conditional_idx += 1 + # Delete condition attribute now that we have replaced it with + # the conditional and bfuc + del instruction._condition + + instructions.append(instruction) + return instructions diff --git a/qiskit/providers/basicaer/qasm_simulator.py b/qiskit/providers/basicaer/qasm_simulator.py index 1a3c991adf23..33596a23f3e3 100644 --- a/qiskit/providers/basicaer/qasm_simulator.py +++ b/qiskit/providers/basicaer/qasm_simulator.py @@ -21,54 +21,71 @@ .. code-block:: python - QasmSimulatorPy().run(qobj) + QasmSimulatorPy().run(circuits) Where the input is a Qobj object and the output is a BasicAerJob object, which can later be queried for the Result object. The result will contain a 'memory' data field, which is a result of measurements for each shot. """ -import uuid -import time +import collections import logging - from math import log2 -from collections import Counter +import time +import uuid +import warnings + import numpy as np +from qiskit.assembler.disassemble import disassemble +from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.util import local_hardware_info -from qiskit.providers.models import QasmBackendConfiguration -from qiskit.result import Result -from qiskit.providers import BaseBackend from qiskit.providers.basicaer.basicaerjob import BasicAerJob +from qiskit.result.counts import Counts +from qiskit.providers.models import BackendStatus +from qiskit.providers.v2 import BackendV1 +from qiskit.providers.v2 import Options +from qiskit.providers.v2.target import TargetV1 +from qiskit.providers.v2.result_data import ResultData +from qiskit.providers.models.backendconfiguration import BackendConfiguration +from qiskit.qobj.qasm_qobj import QasmQobj +from qiskit.version import VERSION as __version__ +from qiskit.quantum_info import Statevector from .exceptions import BasicAerError from .basicaertools import single_gate_matrix from .basicaertools import cx_gate_matrix from .basicaertools import einsum_vecmul_index +from .basicaertools import assemble_circuit logger = logging.getLogger(__name__) +MAX_QUBITS_MEMORY = int(log2(local_hardware_info()['memory'] * (1024 ** 3) / 16)) -class QasmSimulatorPy(BaseBackend): - """Python implementation of a qasm simulator.""" - MAX_QUBITS_MEMORY = int(log2(local_hardware_info()['memory'] * (1024 ** 3) / 16)) - - DEFAULT_CONFIGURATION = { - 'backend_name': 'qasm_simulator', - 'backend_version': '2.0.0', - 'n_qubits': min(24, MAX_QUBITS_MEMORY), - 'url': 'https://github.com/Qiskit/qiskit-terra', - 'simulator': True, - 'local': True, - 'conditional': True, - 'open_pulse': False, - 'memory': True, - 'max_shots': 65536, - 'coupling_map': None, - 'description': 'A python simulator for qasm experiments', - 'basis_gates': ['u1', 'u2', 'u3', 'cx', 'id', 'unitary'], - 'gates': [ +class QasmSimulatorTarget(TargetV1): + @property + def num_qubits(self): + return min(24, MAX_QUBITS_MEMORY) + + @property + def conditional(self): + return True + + @property + def basis_gates(self): + return ['u1', 'u2', 'u3', 'cx', 'id', 'unitary'] + + @property + def supported_instructions(self): + return None + + @property + def coupling_map(self): + return None + + @property + def gates(self): + return [ { 'name': 'u1', 'parameters': ['lambda'], @@ -100,22 +117,38 @@ class QasmSimulatorPy(BaseBackend): 'qasm_def': 'unitary(matrix) q1, q2,...' } ] - } - DEFAULT_OPTIONS = { - "initial_statevector": None, - "chop_threshold": 1e-15 - } + +class QasmSimulatorPy(BackendV1): + """Python implementation of a qasm simulator.""" # Class level variable to return the final state at the end of simulation # This should be set to True for the statevector simulator SHOW_FINAL_STATE = False - def __init__(self, configuration=None, provider=None): - super().__init__(configuration=( - configuration or QasmBackendConfiguration.from_dict(self.DEFAULT_CONFIGURATION)), - provider=provider) - + @property + def local(self): + return True + + @property + def open_pulse(self): + return False + + @property + def memory(self): + return True + + def configuration(self): + warnings.warn("configuration() is deprecated") + return BackendConfiguration('Qasm simulator', 'v2', + self.target.num_qubits, + self.target.basis_gates, self.target.gates, + True, True, True, False, True, 8192, + self.target.coupling_map) + + def __init__(self, name='qasm_simulator', **fields): + super().__init__(name, **fields) + self._target = QasmSimulatorTarget() # Define attributes in __init__. self._local_random = np.random.RandomState() self._classical_memory = 0 @@ -125,11 +158,16 @@ def __init__(self, configuration=None, provider=None): self._number_of_qubits = 0 self._shots = 0 self._memory = False - self._initial_statevector = self.DEFAULT_OPTIONS["initial_statevector"] - self._chop_threshold = self.DEFAULT_OPTIONS["chop_threshold"] - self._qobj_config = None + self._initial_statevector = None + self._chop_threshold = self.options.get('chop_threshold') # TEMP - self._sample_measure = False + self._sample_measure = self.options.get('allow_sample_measuring') + + @classmethod + def _default_config(cls): + return Options(shots=1024, memory=False, + initial_statevector=None, chop_threshold=1e-15, + allow_sample_measuring=False) def _add_unitary(self, gate, qubits): """Apply an N-qubit unitary matrix. @@ -264,7 +302,7 @@ def _add_qasm_reset(self, qubit): def _validate_initial_statevector(self): """Validate an initial statevector""" # If initial statevector isn't set we don't need to validate - if self._initial_statevector is None: + if self.options.get('initial_statevector') is None: return # Check statevector is correct length for number of qubits length = len(self._initial_statevector) @@ -273,34 +311,18 @@ def _validate_initial_statevector(self): raise BasicAerError('initial statevector is incorrect length: ' + '{} != {}'.format(length, required_dim)) - def _set_options(self, qobj_config=None, backend_options=None): - """Set the backend options for all experiments in a qobj""" + def _set_options(self): + """Set the backend options for all experiments""" # Reset default options - self._initial_statevector = self.DEFAULT_OPTIONS["initial_statevector"] - self._chop_threshold = self.DEFAULT_OPTIONS["chop_threshold"] - if backend_options is None: - backend_options = {} - - # Check for custom initial statevector in backend_options first, - # then config second - if 'initial_statevector' in backend_options: - self._initial_statevector = np.array(backend_options['initial_statevector'], - dtype=complex) - elif hasattr(qobj_config, 'initial_statevector'): - self._initial_statevector = np.array(qobj_config.initial_statevector, - dtype=complex) + self._initial_statevector = self.options.get('initial_statevector') + self._chop_threshold = self.options.get('chop_threshold') + if self._initial_statevector is not None: # Check the initial statevector is normalized norm = np.linalg.norm(self._initial_statevector) if round(norm, 12) != 1: raise BasicAerError('initial statevector is not normalized: ' + 'norm {} != 1'.format(norm)) - # Check for custom chop threshold - # Replace with custom options - if 'chop_threshold' in backend_options: - self._chop_threshold = backend_options['chop_threshold'] - elif hasattr(qobj_config, 'chop_threshold'): - self._chop_threshold = qobj_config.chop_threshold def _initialize_statevector(self): """Set the initial statevector for simulation""" @@ -319,13 +341,13 @@ def _get_statevector(self): """Return the current statevector""" vec = np.reshape(self._statevector, 2 ** self._number_of_qubits) vec[abs(vec) < self._chop_threshold] = 0.0 - return vec + return Statevector(vec) def _validate_measure_sampling(self, experiment): """Determine if measure sampling is allowed for an experiment Args: - experiment (QobjExperiment): a qobj experiment. + experiment (QobjCircuit): a QuantumCircuit experiment. """ # If shots=1 we should disable measure sampling. # This is also required for statevector simulator to return the @@ -334,124 +356,75 @@ def _validate_measure_sampling(self, experiment): self._sample_measure = False return - # Check for config flag - if hasattr(experiment.config, 'allows_measure_sampling'): - self._sample_measure = experiment.config.allows_measure_sampling # If flag isn't found do a simple test to see if a circuit contains # no reset instructions, and no gates instructions after # the first measure. - else: - measure_flag = False - for instruction in experiment.instructions: - # If circuit contains reset operations we cannot sample - if instruction.name == "reset": + measure_flag = False + for instruction in experiment.data: + # If circuit contains reset operations we cannot sample + if instruction[0].name == "reset": + self._sample_measure = False + return + # If circuit contains a measure option then we can + # sample only if all following operations are measures + if measure_flag: + # If we find a non-measure instruction + # we cannot do measure sampling + if instruction[0].name not in ["measure", "barrier", "id", "u0"]: self._sample_measure = False return - # If circuit contains a measure option then we can - # sample only if all following operations are measures - if measure_flag: - # If we find a non-measure instruction - # we cannot do measure sampling - if instruction.name not in ["measure", "barrier", "id", "u0"]: - self._sample_measure = False - return - elif instruction.name == "measure": - measure_flag = True - # If we made it to the end of the circuit without returning - # measure sampling is allowed - self._sample_measure = True - - def run(self, qobj, backend_options=None): - """Run qobj asynchronously. - - Args: - qobj (Qobj): payload of the experiment - backend_options (dict): backend options - - Returns: - BasicAerJob: derived from BaseJob - - Additional Information: - backend_options: Is a dict of options for the backend. It may contain - * "initial_statevector": vector_like - - The "initial_statevector" option specifies a custom initial - initial statevector for the simulator to be used instead of the all - zero state. This size of this vector must be correct for the number - of qubits in all experiments in the qobj. - - Example:: - - backend_options = { - "initial_statevector": np.array([1, 0, 0, 1j]) / np.sqrt(2), - } - """ - self._set_options(qobj_config=qobj.config, - backend_options=backend_options) - job_id = str(uuid.uuid4()) - job = BasicAerJob(self, job_id, self._run_job, qobj) - job.submit() - return job + elif instruction[0].name == "measure": + measure_flag = True + # If we made it to the end of the circuit without returning + # measure sampling is allowed + self._sample_measure = True - def _run_job(self, job_id, qobj): - """Run experiments in qobj + def run(self, circuits): + """Run circuits Args: - job_id (str): unique id for the job. - qobj (Qobj): job description + circuits (list(QuantumCircuits): payload of the experiment Returns: - Result: Result object + List[Counts]: list """ - self._validate(qobj) + if isinstance(circuits, QuantumCircuit): + circuits = [circuits] + self._set_options() + job_id = str(uuid.uuid4()) result_list = [] - self._shots = qobj.config.shots - self._memory = getattr(qobj.config, 'memory', False) - self._qobj_config = qobj.config + self._shots = self.options.get('shots') + self._memory = self.options.get('memory', False) start = time.time() - for experiment in qobj.experiments: - result_list.append(self.run_experiment(experiment)) + result_data = [] + if isinstance(circuits, QasmQobj): + warnings.warn( + 'Passing in a Qobj object to run() is deprecated and support ' + 'for it will be removed in the future. Instead pass circuits ' + 'directly and use the backend to set run configuration ', + DeprecationWarning, stacklevel=2) + circuits, _, __ = disassemble(circuits) + + for experiment in circuits: + result_data.extend(self.run_experiment(experiment)) end = time.time() - result = {'backend_name': self.name(), - 'backend_version': self._configuration.backend_version, - 'qobj_id': qobj.qobj_id, - 'job_id': job_id, - 'results': result_list, - 'status': 'COMPLETED', - 'success': True, - 'time_taken': (end - start), - 'header': qobj.header.to_dict()} - - return Result.from_dict(result) + time_taken = end - start + return BasicAerJob(job_id, self, result_data, time_taken=time_taken) def run_experiment(self, experiment): """Run an experiment (circuit) and return a single experiment result. Args: - experiment (QobjExperiment): experiment from qobj experiments list + experiment (QuantumCircuit): A quantumcircuit to run Returns: - dict: A result dictionary which looks something like:: - - { - "name": name of this experiment (obtained from qobj.experiment header) - "seed": random seed used for simulation - "shots": number of shots used in the simulation - "data": - { - "counts": {'0x9: 5, ...}, - "memory": ['0x9', '0xF', '0x1D', ..., '0x9'] - }, - "status": status string for the simulation - "success": boolean - "time_taken": simulation time of this single experiment - } + Counts: A counts object with the result of the experiment Raises: BasicAerError: if an error occurred. """ start = time.time() - self._number_of_qubits = experiment.config.n_qubits - self._number_of_cmembits = experiment.config.memory_slots + self._number_of_qubits = experiment.num_qubits + self._number_of_cmembits = experiment.num_clbits self._statevector = 0 self._classical_memory = 0 self._classical_register = 0 @@ -459,10 +432,9 @@ def run_experiment(self, experiment): # Validate the dimension of initial statevector if set self._validate_initial_statevector() # Get the seed looking in circuit, qobj, and then random. - if hasattr(experiment.config, 'seed_simulator'): - seed_simulator = experiment.config.seed_simulator - elif hasattr(self._qobj_config, 'seed_simulator'): - seed_simulator = self._qobj_config.seed_simulator + seed_config = self.options.get('seed_simulator') + if seed_config: + seed_simulator = seed_config else: # For compatibility on Windows force dyte to be int32 # and set the maximum value to be (2 ** 31) - 1 @@ -488,7 +460,7 @@ def run_experiment(self, experiment): # Initialize classical memory to all 0 self._classical_memory = 0 self._classical_register = 0 - for operation in experiment.instructions: + for operation in assemble_circuit(experiment): conditional = getattr(operation, 'conditional', None) if isinstance(conditional, int): conditional_bit_set = (self._classical_register >> conditional) & 1 @@ -576,7 +548,7 @@ def run_experiment(self, experiment): self._classical_memory = \ (self._classical_memory & (~membit)) | (int(outcome) << cmembit) else: - backend = self.name() + backend = self.name err_msg = '{0} encountered unrecognized operation "{1}"' raise BasicAerError(err_msg.format(backend, operation.name)) @@ -591,41 +563,44 @@ def run_experiment(self, experiment): memory.append(hex(int(outcome, 2))) # Add data - data = {'counts': dict(Counter(memory))} - # Optionally add memory list - if self._memory: - data['memory'] = memory + results = [] + counts_raw = dict(collections.Counter(memory)) # Optionally add final statevector if self.SHOW_FINAL_STATE: - data['statevector'] = self._get_statevector() - # Remove empty counts and memory for statevector simulator - if not data['counts']: - data.pop('counts') - if 'memory' in data and not data['memory']: - data.pop('memory') + results.append(ResultData(experiment, 'statevector', self._get_statevector())) + return results + # Optionally add memory list + if self._memory: + results.append(ResultData(experiment, 'memory', memory)) end = time.time() - return {'name': experiment.header.name, - 'seed_simulator': seed_simulator, - 'shots': self._shots, - 'data': data, - 'status': 'DONE', - 'success': True, - 'time_taken': (end - start), - 'header': experiment.header.to_dict()} - - def _validate(self, qobj): + time_taken = end - start + counts = Counts(counts_raw, time_taken=time_taken) + results.append(ResultData(experiment, 'counts', counts, name=experiment.name, + shots=self._shots, seed_simulator=seed_simulator)) + return results + + def _validate(self, circuits): """Semantic validations of the qobj which cannot be done via schemas.""" - n_qubits = qobj.config.n_qubits - max_qubits = self.configuration().n_qubits - if n_qubits > max_qubits: - raise BasicAerError('Number of qubits {} '.format(n_qubits) + - 'is greater than maximum ({}) '.format(max_qubits) + - 'for "{}".'.format(self.name())) - for experiment in qobj.experiments: - name = experiment.header.name - if experiment.config.memory_slots == 0: + for circuit in circuits: + if circuit.num_qubits > self.num_qubits: + raise BasicAerError('Number of qubits {} '.format(circuit.num_qubits) + + 'is greater than maximum ({}) '.format(self.num_qubits) + + 'for "{}".'.format(self.name())) + for experiment in circuits: + name = experiment.name + if experiment.num_clbits == 0: logger.warning('No classical registers in circuit "%s", ' 'counts will be empty.', name) - elif 'measure' not in [op.name for op in experiment.instructions]: + elif 'measure' not in experiment.count_ops: logger.warning('No measurements in circuit "%s", ' 'classical register will remain all zeros.', name) + + def status(self): + warnings.warn("The status method for QasmSimulatorPy is deprecated " + "and will be removed in a future release.", + DeprecationWarning, stacklevel=2) + return BackendStatus(backend_name=self.name, + backend_version=__version__, + operational=True, + pending_jobs=0, + status_msg='') diff --git a/qiskit/providers/basicaer/statevector_simulator.py b/qiskit/providers/basicaer/statevector_simulator.py index e16681cd0430..3df110d4e650 100644 --- a/qiskit/providers/basicaer/statevector_simulator.py +++ b/qiskit/providers/basicaer/statevector_simulator.py @@ -36,65 +36,13 @@ class StatevectorSimulatorPy(QasmSimulatorPy): """Python statevector simulator.""" - MAX_QUBITS_MEMORY = int(log2(local_hardware_info()['memory'] * (1024 ** 3) / 16)) - - DEFAULT_CONFIGURATION = { - 'backend_name': 'statevector_simulator', - 'backend_version': '1.0.0', - 'n_qubits': min(24, MAX_QUBITS_MEMORY), - 'url': 'https://github.com/Qiskit/qiskit-terra', - 'simulator': True, - 'local': True, - 'conditional': True, - 'open_pulse': False, - 'memory': True, - 'max_shots': 65536, - 'coupling_map': None, - 'description': 'A Python statevector simulator for qobj files', - 'basis_gates': ['u1', 'u2', 'u3', 'cx', 'id', 'unitary'], - 'gates': [ - { - 'name': 'u1', - 'parameters': ['lambda'], - 'qasm_def': 'gate u1(lambda) q { U(0,0,lambda) q; }' - }, - { - 'name': 'u2', - 'parameters': ['phi', 'lambda'], - 'qasm_def': 'gate u2(phi,lambda) q { U(pi/2,phi,lambda) q; }' - }, - { - 'name': 'u3', - 'parameters': ['theta', 'phi', 'lambda'], - 'qasm_def': 'gate u3(theta,phi,lambda) q { U(theta,phi,lambda) q; }' - }, - { - 'name': 'cx', - 'parameters': ['c', 't'], - 'qasm_def': 'gate cx c,t { CX c,t; }' - }, - { - 'name': 'id', - 'parameters': ['a'], - 'qasm_def': 'gate id a { U(0,0,0) a; }' - }, - { - 'name': 'unitary', - 'parameters': ['matrix'], - 'qasm_def': 'unitary(matrix) q1, q2,...' - } - ] - } - # Override base class value to return the final state vector SHOW_FINAL_STATE = True - def __init__(self, configuration=None, provider=None): - super().__init__(configuration=( - configuration or QasmBackendConfiguration.from_dict(self.DEFAULT_CONFIGURATION)), - provider=provider) + def __init__(self, **fields): + super().__init__('statevector_simulator', **fields) - def run(self, qobj, backend_options=None): + def run(self, circuits): """Run qobj asynchronously. Args: @@ -126,7 +74,7 @@ def run(self, qobj, backend_options=None): "chop_threshold": 1e-15 } """ - return super().run(qobj, backend_options=backend_options) + return super().run(circuits) def _validate(self, qobj): """Semantic validations of the qobj which cannot be done via schemas. diff --git a/qiskit/providers/basicaer/unitary_simulator.py b/qiskit/providers/basicaer/unitary_simulator.py index 37cbddce5671..12674c5cc2e1 100644 --- a/qiskit/providers/basicaer/unitary_simulator.py +++ b/qiskit/providers/basicaer/unitary_simulator.py @@ -19,55 +19,65 @@ .. code-block:: python - UnitarySimulator().run(qobj) + UnitarySimulator().run(circuit) -Where the input is a Qobj object and the output is a BasicAerJob object, which can -later be queried for the Result object. The result will contain a 'unitary' -data field, which is a 2**n x 2**n complex numpy array representing the -circuit's unitary matrix. +Where the input is a QuantumCircuit object and the output a unitary matrix, +which is a 2**n x 2**n complex numpy array representing the circuit's unitary +matrix. """ + +import collections import logging -import uuid -import time from math import log2, sqrt +import time +import uuid + import numpy as np + +from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.util import local_hardware_info -from qiskit.providers.models import QasmBackendConfiguration -from qiskit.providers import BaseBackend +from qiskit.providers.v2.backend import BackendV1 +from qiskit.providers.v2.options import Options +from qiskit.providers.v2.target import Target +from qiskit.providers.v2.result_data import ResultData from qiskit.providers.basicaer.basicaerjob import BasicAerJob from qiskit.result import Result from .exceptions import BasicAerError from .basicaertools import single_gate_matrix from .basicaertools import cx_gate_matrix from .basicaertools import einsum_matmul_index +from .basicaertools import assemble_circuit logger = logging.getLogger(__name__) +MAX_QUBITS_MEMORY = int( + log2(sqrt(local_hardware_info()['memory'] * (1024 ** 3) / 16))) -# TODO add ["status"] = 'DONE', 'ERROR' especially for empty circuit error -# does not show up +class QasmUnitarySimulatorTarget(Target): + @property + def num_qubits(self): + return min(24, MAX_QUBITS_MEMORY) -class UnitarySimulatorPy(BaseBackend): - """Python implementation of a unitary simulator.""" + @property + def conditional(self): + return False + + @property + def coupling_map(self): + return None + + @property + def supported_instructions(self): + return None + + @property + def basis_gates(self): + return ['u1', 'u2', 'u3', 'cx', 'id', 'unitary'] - MAX_QUBITS_MEMORY = int(log2(sqrt(local_hardware_info()['memory'] * (1024 ** 3) / 16))) - - DEFAULT_CONFIGURATION = { - 'backend_name': 'unitary_simulator', - 'backend_version': '1.0.0', - 'n_qubits': min(24, MAX_QUBITS_MEMORY), - 'url': 'https://github.com/Qiskit/qiskit-terra', - 'simulator': True, - 'local': True, - 'conditional': False, - 'open_pulse': False, - 'memory': False, - 'max_shots': 65536, - 'coupling_map': None, - 'description': 'A python simulator for unitary matrix corresponding to a circuit', - 'basis_gates': ['u1', 'u2', 'u3', 'cx', 'id', 'unitary'], - 'gates': [ + @property + def gates(self): + return [ { 'name': 'u1', 'parameters': ['lambda'], @@ -99,23 +109,45 @@ class UnitarySimulatorPy(BaseBackend): 'qasm_def': 'unitary(matrix) q1, q2,...' } ] - } - DEFAULT_OPTIONS = { - "initial_unitary": None, - "chop_threshold": 1e-15 - } - - def __init__(self, configuration=None, provider=None): - super().__init__(configuration=( - configuration or QasmBackendConfiguration.from_dict(self.DEFAULT_CONFIGURATION)), - provider=provider) +# TODO add ["status"] = 'DONE', 'ERROR' especially for empty circuit error +# does not show up +class UnitarySimulatorPy(BackendV1): + """Python implementation of a unitary simulator.""" + @property + def local(self): + return True + + @property + def open_pulse(self): + return False + + @property + def memory(self): + return False + + def configuration(self): + warnings.warn("configuration() is deprecated") + return BackendConfiguration('unitary simulator', 'v2', + self.target.num_qubits, + self.target.basis_gates, self.target.gates, + True, True, True, False, True, 8192, + self.target.coupling_map) + + def __init__(self, name='unitary_simulator', **fields): + super().__init__(name, **fields) # Define attributes inside __init__. + self._target = QasmUnitarySimulatorTarget() self._unitary = None self._number_of_qubits = 0 - self._initial_unitary = None - self._chop_threshold = 1e-15 + self._initial_unitary = self.options.get('initial_unitary') + self._chop_threshold = self.options.get('chop_threshold') + + @classmethod + def _default_config(cls): + return Options(shots=1, + initial_unitary=None, chop_threshold=1e-15) def _add_unitary(self, gate, qubits): """Apply an N-qubit unitary matrix. @@ -148,22 +180,11 @@ def _validate_initial_unitary(self): raise BasicAerError('initial unitary is incorrect shape: ' + '{} != 2 ** {}'.format(shape, required_shape)) - def _set_options(self, qobj_config=None, backend_options=None): + def _set_options(self): """Set the backend options for all experiments in a qobj""" # Reset default options - self._initial_unitary = self.DEFAULT_OPTIONS["initial_unitary"] - self._chop_threshold = self.DEFAULT_OPTIONS["chop_threshold"] - if backend_options is None: - backend_options = {} - - # Check for custom initial statevector in backend_options first, - # then config second - if 'initial_unitary' in backend_options: - self._initial_unitary = np.array(backend_options['initial_unitary'], - dtype=complex) - elif hasattr(qobj_config, 'initial_unitary'): - self._initial_unitary = np.array(qobj_config.initial_unitary, - dtype=complex) + self._initial_unitary = self.options.get("initial_unitary") + self._chop_threshold = self.options.get("chop_threshold") if self._initial_unitary is not None: # Check the initial unitary is actually unitary shape = np.shape(self._initial_unitary) @@ -175,14 +196,6 @@ def _set_options(self, qobj_config=None, backend_options=None): norm = np.linalg.norm(u_dagger_u - iden) if round(norm, 10) != 0: raise BasicAerError("initial unitary is not unitary") - # Check the initial statevector is normalized - - # Check for custom chop threshold - # Replace with custom options - if 'chop_threshold' in backend_options: - self._chop_threshold = backend_options['chop_threshold'] - elif hasattr(qobj_config, 'chop_threshold'): - self._chop_threshold = qobj_config.chop_threshold def _initialize_unitary(self): """Set the initial unitary for simulation""" @@ -203,7 +216,7 @@ def _get_unitary(self): unitary[abs(unitary) < self._chop_threshold] = 0.0 return unitary - def run(self, qobj, backend_options=None): + def run(self, circuits): """Run qobj asynchronously. Args: @@ -211,67 +224,20 @@ def run(self, qobj, backend_options=None): backend_options (dict): backend options Returns: - BasicAerJob: derived from BaseJob - - Additional Information:: - - backend_options: Is a dict of options for the backend. It may contain - * "initial_unitary": matrix_like - * "chop_threshold": double - - The "initial_unitary" option specifies a custom initial unitary - matrix for the simulator to be used instead of the identity - matrix. This size of this matrix must be correct for the number - of qubits inall experiments in the qobj. - - The "chop_threshold" option specifies a truncation value for - setting small values to zero in the output unitary. The default - value is 1e-15. - - Example:: - - backend_options = { - "initial_unitary": np.array([[1, 0, 0, 0], - [0, 0, 0, 1], - [0, 0, 1, 0], - [0, 1, 0, 0]]) - "chop_threshold": 1e-15 - } + list: a list of unitary matrices """ - self._set_options(qobj_config=qobj.config, - backend_options=backend_options) + if isinstance(circuits, QuantumCircuit): + circuits = [circuits] + self._set_options() job_id = str(uuid.uuid4()) - job = BasicAerJob(self, job_id, self._run_job, qobj) - job.submit() - return job - - def _run_job(self, job_id, qobj): - """Run experiments in qobj. - - Args: - job_id (str): unique id for the job. - qobj (Qobj): job description - - Returns: - Result: Result object - """ - self._validate(qobj) - result_list = [] + self._validate(circuits) + result_data = [] start = time.time() - for experiment in qobj.experiments: - result_list.append(self.run_experiment(experiment)) + for experiment in circuits: + result_data.append(self.run_experiment(experiment)) end = time.time() - result = {'backend_name': self.name(), - 'backend_version': self._configuration.backend_version, - 'qobj_id': qobj.qobj_id, - 'job_id': job_id, - 'results': result_list, - 'status': 'COMPLETED', - 'success': True, - 'time_taken': (end - start), - 'header': qobj.header.to_dict()} - - return Result.from_dict(result) + time_taken = end - start + return BasicAerJob(job_id, self, result_data, time_taken=time_taken) def run_experiment(self, experiment): """Run an experiment (circuit) and return a single experiment result. @@ -280,7 +246,7 @@ def run_experiment(self, experiment): experiment (QobjExperiment): experiment from qobj experiments list Returns: - dict: A result dictionary which looks something like:: + ResultData: A result dictionary which looks something like:: { "name": name of this experiment (obtained from qobj.experiment header) @@ -301,13 +267,13 @@ def run_experiment(self, experiment): Note that the practical qubit limit is much lower than 24. """ start = time.time() - self._number_of_qubits = experiment.header.n_qubits + self._number_of_qubits = experiment.num_qubits # Validate the dimension of initial unitary if set self._validate_initial_unitary() self._initialize_unitary() - for operation in experiment.instructions: + for operation in assemble_circuit(experiment): if operation.name == 'unitary': qubits = operation.qubits gate = operation.params[0] @@ -334,41 +300,29 @@ def run_experiment(self, experiment): err_msg = '{0} encountered unrecognized operation "{1}"' raise BasicAerError(err_msg.format(backend, operation.name)) # Add final state to data - data = {'unitary': self._get_unitary()} + data = self._get_unitary() end = time.time() - return {'name': experiment.header.name, - 'shots': 1, - 'data': data, - 'status': 'DONE', - 'success': True, - 'time_taken': (end - start), - 'header': experiment.header.to_dict()} - - def _validate(self, qobj): - """Semantic validations of the qobj which cannot be done via schemas. + return ResultData(experiment, 'unitary', data) + + def _validate(self, circuits): + """Semantic validations of the circuit. Some of these may later move to backend schemas. 1. No shots 2. No measurements in the middle """ - n_qubits = qobj.config.n_qubits - max_qubits = self.configuration().n_qubits - if n_qubits > max_qubits: - raise BasicAerError('Number of qubits {} '.format(n_qubits) + - 'is greater than maximum ({}) '.format(max_qubits) + - 'for "{}".'.format(self.name())) - if hasattr(qobj.config, 'shots') and qobj.config.shots != 1: + shots = self.options.get('shots') + if shots and shots != 1: logger.info('"%s" only supports 1 shot. Setting shots=1.', - self.name()) - qobj.config.shots = 1 - for experiment in qobj.experiments: - name = experiment.header.name - if getattr(experiment.config, 'shots', 1) != 1: - logger.info('"%s" only supports 1 shot. ' - 'Setting shots=1 for circuit "%s".', - self.name(), name) - experiment.config.shots = 1 - for operation in experiment.instructions: - if operation.name in ['measure', 'reset']: + self.name) + self.set_options(shots=1) + for experiment in circuits: + if experiment.num_qubits > self._target.num_qubits: + raise BasicAerError('Number of qubits {} '.format(experiment.num_qubits) + + 'is greater than maximum ({}) '.format(self.num_qubits) + + 'for "{}".'.format(self.name)) + name = experiment.name + for operation in experiment.count_ops(): + if operation in ['measure', 'reset']: raise BasicAerError('Unsupported "%s" instruction "%s" ' + - 'in circuit "%s" ', self.name(), - operation.name, name) + 'in circuit "%s" ', self.name, + operation[0].name, name) diff --git a/qiskit/providers/v2/__init__.py b/qiskit/providers/v2/__init__.py new file mode 100644 index 000000000000..5901ec319d69 --- /dev/null +++ b/qiskit/providers/v2/__init__.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" +================================================ +Providers Interface (:mod:`qiskit.providers.v2`) +================================================ + +.. currentmodule:: qiskit.providers.v2 + +This module contains the classes used to build external providers for Terra. A +provider is anything that provides an external service to Terra. The typical +example of this is a Backend provider which provides +:class:`~qiskit.providers.v2.Backend` objects which can be used for executing +:class:`~qiskit.circuits.QuantumCircuit` and/or :class:`~qiskit.pulse.Schedule` +objects. This contains the abstract classes which are used to define the +interface between a provider and terra. + +Abstract Classes +================ + +Provider +-------- + +.. autosummary:: + :toctree: ../stubs/ + + Provider + ProviderV1 + +Backend +------- + +.. autosummary:: + :toctree: ../stubs/ + + Backend + BackendV1 + +Options +------- + +.. autosummary:: + :toctree: ../stubs/ + + Options + +Properties +---------- + +.. autosummary:: + :toctree: ../stubs/ + + Properties + PropertiesV1 + +Job +--- + +.. autosummary:: + :toctree: ../stubs/ + + Job + JobV1 + +ResultData +---------- + +.. autosummary:: + :toctree: ../stubs/ + + ResultData +""" + +from .provider import Provider +from .provider import ProviderV1 +from .backend import Backend +from .backend import BackendV1 +from .options import Options +from .properties import Properties +from .properties import PropertiesV1 +from .job import Job +from .job import JobV1 +from .result_data import ResultData diff --git a/qiskit/providers/v2/backend.py b/qiskit/providers/v2/backend.py new file mode 100644 index 000000000000..224ea1eaced4 --- /dev/null +++ b/qiskit/providers/v2/backend.py @@ -0,0 +1,168 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +from abc import ABC +from abc import abstractmethod + + +class Backend: + pass + + +class BackendV1(Backend, ABC): + """Abstract class for Backends + + This abstract class is to be used for all Backend objects created by a + provider. There are several classes of information contained in a Backend. + The first are the properties of class itself. These should be used to + defined the immutable characteristics of the backend. For a backend + that runs experiments this would be things like ``basis_gates`` that + do not change for the lifetime of the backend. The ``options`` + attribute of the backend is used to contain the dynamic properties of + the backend. The intent is that these will all be user configurable. It + should be used more for runtime properties that **configure** how the + backend is used. For example, something like a ``shots`` field for a + backend that runs experiments which would contain an int for how many + shots to execute. The ``properties`` attribute is optionally defined and + is used to return measured properties, or properties of a backend that may + change over time. The simplest example of this would be a version string, + which will change as a backend is updated, but also could be something like + noise parameters for backends that run experiments. + + For Backends that will run circuits you'll want to have a target defined + as well. This will provide necessary information to the transpiler so that + circuits will be transpiled so that they actually run on the backend. + """ + + version = 1 + + def __init__(self, name, **fields): + """Initialize a backend class + + Args: + name (str): The name of the backend + fields: kwargs for the values to use to override the default options. + Raises: + AttributeError: if input field not a valid options + """ + self.name = name + self._options = self._default_config() + if fields: + for field in fields: + if field not in self._options.data: + raise AttributeError( + "Options field %s is not valid for this backend" % field) + self._options.update_config(**fields) + + @classmethod + @abstractmethod + def _default_config(cls): + """Return the default options + + This method will return a :class:`qiskit.providers.v2.Options` + subclass object that will be used for the default options. These + should be the default parameters to use for the options of the + backend. + + Returns: + qiskit.providers.v2.Options: A options object with + default values set + """ + pass + + def set_options(self, **fields): + """Set the options fields for the backend + + This method is used to update the options of a backend. If + you need to change any of the options prior to running just + pass in the kwarg with the new value for the options. + + Args: + fields: The fields to update the options + + Raises: + AttributeError: If the field passed in is not part of the + options + """ + for field in fields: + if field not in self._options.data: + raise AttributeError( + "Options field %s is not valid for this " + "backend" % field) + self._options.update_config(**fields) + + @property + def properties(self): + """Return the backend's measure properties. + + The properties of a backend represent measure properties of a backend. + These are fields that are immutable for the property but may change + over time based on the state of the Backend. + + This is an optional property for the Backend and will return None if a + backend does not have properties. + """ + if hasattr(self, '_properties'): + return self._properties + else: + return None + + @property + def options(self): + """Return the options for the backend + + The options of a backend are the dynamic parameters defining + how the backend is used. These are used to control the :meth:`run` + method. + """ + return self._options + + @property + def target(self): + """Return the backend's target information + + The target of a backend represents the information for the compiler + to set this backend as the target device. These are fields that are + immutable for the Backend. + + This is an optional property for the Backend if it targets being a + compiler target. For classes of backends like circuit optimizers this + doesn't apply and doesn't have to be defined.and will return None if a + backend does not have a target. + """ + if hasattr(self, '_target'): + return self._target + else: + return None + + @abstractmethod + def run(self, run_input, **options): + """Run on the backend. + + This method that will return a :class:`~qiskit.providers.v2.Job` object + that run circuits. Depending on the backend this may be either an async + or sync call. It is the discretion of the provider to decide whether + running should block until the execution is finished or not. The Job + class can handle either situation. + + Args: + run_input: An individual or a list of of circuits or pulse + schedules to run on the backend. + options: Any kwarg options to pass to the backend for running the + config. If a key is also present in the options + attribute/object then the expectation is that the value + specified will be used instead of what's set in the options + object. + """ + pass diff --git a/qiskit/providers/v2/job.py b/qiskit/providers/v2/job.py new file mode 100644 index 000000000000..1de119a36755 --- /dev/null +++ b/qiskit/providers/v2/job.py @@ -0,0 +1,230 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +from abc import ABC +from abc import abstractmethod +import warnings + +import numpy as np + +from qiskit.circuit.quantumcircuit import QuantumCircuit +from qiskit.pulse.schedule import Schedule +from qiskit.exceptions import QiskitError +from qiskit.result.counts import Counts +from qiskit.result.result import Result +from qiskit.quantum_info.states.statevector import Statevector +from qiskit.providers.v2.result_data import ResultData + + +class Job: + pass + + +class JobV1(Job, ABC): + """A base class for representing an async job on a backend. + + This provides the base structure required for building an async or + sync job handler. The underlying concept around this is that this + object gets returned by :meth:`qiskit.providers.v2.Backend.run` and + depending on the nature of the backend will either be the record of + the execution with results, or in the case of async backends will + be a handle to the record of the async execution that will populate + the result data after the execution is complete. The ``result_data`` + instance attribute will be a list of + :class:`~qiskit.providers.v2.ResultData` objects. + (depending on the backend this would be a + :class:`~qiskit.circuit.QuantumCircuit`, :class:`~qiskit.result.Counts`, + :class:`~qiskit.quantum_info.Statevector`, etc. object), or in the case + there were multiple inputs a list of the data objects. name of the input circuit or schedule. + """ + + version = 1 + + job_id = None + metadata = None + + def __init__(self, job_id, backend, time_taken=None, **metadata): + """Initialize a new job object + + Args: + job_id (str): A unique identifier for the job + backend (qiskit.providers.v2.BaseBackend): The backend the job is + being run on + time_taken (float): Te duration of the job in seconds, should only + be set during init for sync jobs. + """ + + self.result_data = None + self.job_id = job_id + self.backend = backend + self.time_taken = None + self.metadata = metadata + + def _result_conversion(self, result, name=None): + if isinstance(result.data, Counts): + if 'seed_simulator' in result.metadata: + seed_simulator = result.metadata.pop( + 'seed_simulator') + else: + seed_simulator = None + header = result.metadata + header['name'] = name + result_dict = { + 'shots': result.metadata['shots'], + 'data': {'counts': result.data}, + 'success': True, + 'time_taken': result.data.time_taken, + 'header': header, + } + if seed_simulator: + result_dict['seed_simulator'] = seed_simulator + result_dict['seed'] = seed_simulator + elif isinstance(result.data, Statevector): + result_dict = { + 'data': {'statevector': result.data.data}, + 'status': 'DONE', + 'success': True, + 'shots': 1, + } + elif isinstance(result.data, np.ndarray): + if name: + header = {'name': name} + else: + header = {} + result_dict = { + 'data': {'unitary': result.data}, + 'status': 'DONE', + 'success': True, + 'shots': 1, + 'header': header + } + else: + raise Exception + return result_dict + + def result(self): + result_list = {} + self.wait_for_final_state() + warnings.warn("The result method is deprecated instead access the " + "result_data attribute to access the result from the " + "job", DeprecationWarning, stacklevel=2) + result_dict = { + 'backend_name': self.backend.name, + 'qobj_id': '', + 'backend_version': '', + 'success': True, + 'job_id': self.job_id, + } + if isinstance(self.result_data, list): + result_list = [] + for result in self.result_data: + result_list.append( + self._result_conversion(result, result.experiment.name)) + elif isinstance(self.result_data, ResultData): + result_list = [self._result_conversion(self.result_data)] + else: + raise TypeError( + "Result for job %s is not a circuit result and a backwards " + "compat Result object can't be constructed for " + "it" % self.job_id) + result_dict['results'] = result_list + return Result.from_dict(result_dict) + + @abstractmethod + def status(self): + """Return the job status as a string.""" + pass + + def cancel(self): + """An optional method to cancel a submitted job.""" + raise NotImplementedError + + @abstractmethod + def wait_for_final_state(): + """A blocking call that will return when the job is done or is no longer running.""" + pass + + def _get_experiment(self, key=None): + # Automatically return the first result if no key was provided. + if key is None: + if isinstance(self.result_data, + OrderedDict) and len(self.result_data) != 1: + raise QiskitError( + 'You have to select a circuit or schedule when there is more than ' + 'one available') + key = 0 + else: + if not isinstance(self.result_data, OrderedDict): + raise QiskitError("You can't specify a key if there is only " + "one result") + + # Key is a QuantumCircuit/Schedule or str: retrieve result by name. + if isinstance(key, (QuantumCircuit, Schedule)): + key = key.name + # Key is an integer: return result by index. + if isinstance(key, int): + return list(self.result_data.values())[key] + elif isinstance(key, str): + return self.result_data[key] + else: + raise TypeError('Invalid key type %s' % type(key)) + + def get_memory(self): + raise NotImplementedError + + def get_counts(self, experiment=None): + self.wait_for_final_state() + if isinstance(self.result_data, Counts): + return self.result_data + elif isinstance(self.result_data, OrderedDict): + exp_result = self._get_experiment(experiment) + if isinstance(exp_result, Counts): + return exp_result + else: + raise TypeError( + "Result for job %s is not a Counts object" % self.job_id) + else: + raise TypeError( + "Result for job %s is not a Counts object" % self.job_id) + + def get_statevector(self, experiment=None): + self.wait_for_final_state() + if isinstance(self.result_data, Statevector): + return self.result_data + elif isinstance(self.result_data, OrderedDict): + exp_result = self._get_experiment(experiment) + if isinstance(exp_result, Statevector): + return exp_result + else: + raise TypeError( + "Result for job %s is not a Statevector " + "object" % self.job_id) + else: + raise TypeError( + "Result for job %s is not a Statevector object" % self.job_id) + + def get_unitary(self, experiment=None): + self.wait_for_final_state() + if isinstance(self.result_data, np.array): + return self.result_data + elif isinstance(self.result_data, OrderedDict): + exp_result = self._get_experiment(experiment) + if isinstance(exp_result, np.ndarray): + return exp_result + else: + raise TypeError( + "Result for job %s is not a unitary array" % self.job_id) + else: + raise TypeError( + "Result for job %s is not a Statevector object" % self.job_id) diff --git a/qiskit/providers/v2/options.py b/qiskit/providers/v2/options.py new file mode 100644 index 000000000000..798a2fe72b81 --- /dev/null +++ b/qiskit/providers/v2/options.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + + +class Options: + """Base options object + + This class is the abstract class that all backend options are based + on. The properties of the class are intended to be all ddynamically + adjustable so that a user can reconfigure the backend on demand. If a + property is immutable to the user (eg something like number of qubits) + that should be a property of the backend class (itself instead of the + options. + """ + + def __init__(self, **kwargs): + self.data = kwargs if kwargs else {} + + def update_config(self, **fields): + self.data.update(fields) + + def get(self, field, default=None): + return self.data.get(field, default) diff --git a/qiskit/providers/v2/properties.py b/qiskit/providers/v2/properties.py new file mode 100644 index 000000000000..5bd9f57e6632 --- /dev/null +++ b/qiskit/providers/v2/properties.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +from abc import ABC +from abc import abstractmethod + + +class Properties: + pass + + +class PropertiesV1(Properties, ABC): + """Base properties object + + This class is the abstract class that backend properties are based on. + If a backend has properties defined these are static attributes that + define the characteristics of a backend. They are not a requried field + but if it is defined the intent is that they are static characteristics + of the backend for the lifetime of the backend object. + """ + version = 1 + + @property + @abstractmethod + def backend_version(self): + """The version string for a backend""" + pass + + @property + @abstractmethod + def qubits(self): + """The qubit properties of a backend""" + pass diff --git a/qiskit/providers/v2/provider.py b/qiskit/providers/v2/provider.py new file mode 100644 index 000000000000..00debdf4f257 --- /dev/null +++ b/qiskit/providers/v2/provider.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 2018. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Base class for a provider.""" + +from abc import ABC, abstractmethod + +from qiskit.providers.exceptions import QiskitBackendNotFoundError + + +class Provider: + pass + + +class ProviderV1(Provider, ABC): + """Base class for a Backend Provider.""" + + def get_backend(self, name=None, **kwargs): + """Return a single backend matching the specified filtering. + + Args: + name (str): name of the backend. + **kwargs: dict used for filtering. + + Returns: + BaseBackend: a backend matching the filtering. + + Raises: + QiskitBackendNotFoundError: if no backend could be found or + more than one backend matches the filtering criteria. + """ + backends = self.backends(name, **kwargs) + if len(backends) > 1: + raise QiskitBackendNotFoundError('More than one backend matches the criteria') + if not backends: + raise QiskitBackendNotFoundError('No backend matches the criteria') + + return backends[0] + + @abstractmethod + def backends(self, name=None, **kwargs): + """Return a list of backends matching the specified filtering. + + Args: + name (str): name of the backend. + **kwargs: dict used for filtering. + + Returns: + list[BaseBackend]: a list of Backends that match the filtering + criteria. + """ + pass + + def __eq__(self, other): + """Equality comparison. + + By default, it is assumed that two `Providers` from the same class are + equal. Subclassed providers can override this behavior. + """ + return type(self).__name__ == type(other).__name__ diff --git a/qiskit/providers/v2/result_data.py b/qiskit/providers/v2/result_data.py new file mode 100644 index 000000000000..9fb5d16a918d --- /dev/null +++ b/qiskit/providers/v2/result_data.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + + +class ResultData: + """The result data class is a container class for results from an experiment. + + ``ResultData`` objects contain the result data from an execution and the + associated metadata for it. There should be a single data type for the + result, if an experiment returns more than 1 result data type (for example, + counts and snapshots) a separate ``ResultData`` object should be returned + for each type. + """ + + version = 1 + + def __init__(self, experiment, data_type, data, **metadata): + """Create a new ResultData object + + Args: + experiment: The circuit or pulse schedule object that the result + data is from + data_type (str): The data type of the result. Can be used for + filtering. Any type is acceptable, but 4 value ``counts``, + ``statevector``, ``unitary``, and ``snapshot`` are special + cases in the :class:`~qiskit.providers.v2.Job` base class + because there are built-in methods to get those. + data: The data from the run + metadata: Any key value metadata to associate with the result data + """ + self.experiment = experiment + self.data = data + self.metadata = metadata diff --git a/qiskit/providers/v2/target.py b/qiskit/providers/v2/target.py new file mode 100644 index 000000000000..db4a293021f6 --- /dev/null +++ b/qiskit/providers/v2/target.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +from abc import ABC +from abc import abstractmethod + + +class Target: + pass + + +class TargetV1(ABC): + """Abstract class for Backend Target + + This abstract class is used to provide target information to the compiler + about a backend. These are immutable properties about the backend. The + properties defined here are what information may get consumed by terra, + however if it doesn't apply to a backend they can return ``None`` to + indicate this. The version field is specifically around the target format. + If in the future the fields in a target grow we should create a subclass + and that subclass should increase the version number. This will let the + transpiler with extra fields know beforehand which version the target + is (and ignore fields if they need a newer version). Additionally, in + the future this will be needed if/when a serialization format is added for + this. + """ + + version = 1 + + @property + @abstractmethod + def num_qubits(self): + """Return the number of qubits for the backend.""" + pass + + @property + @abstractmethod + def basis_gates(self): + """Return the list of basis gates for the backend.""" + pass + + @property + @abstractmethod + def supported_instructions(self): + """Return the list of supported non-gate instructions for the backend.""" + pass + + @property + @abstractmethod + def coupling_map(self): + """Return the qiskit.transpiler.CouplingMap object""" + pass + + @property + @abstractmethod + def conditional(self): + """Return bool whether the target can execute gates with classical conditions.""" + pass + + @property + @abstractmethod + def gates(self): + """Return a list of dictionaries describing the properties of each gate.""" + pass