Source code for c3.qiskit.c3_backend

import uuid
import time
import numpy as np
import logging

from qiskit import qobj
from qiskit import QuantumCircuit
from qiskit.exceptions import QiskitError
from qiskit.providers import BackendV1 as Backend
from qiskit.providers import Options
from qiskit.providers.models import QasmBackendConfiguration
from qiskit.result import Result
from qiskit.compiler import assemble
from qiskit.qobj.qasm_qobj import QasmQobjExperiment

from c3.experiment import Experiment

from .c3_exceptions import C3QiskitError
from .c3_job import C3Job
from .c3_backend_utils import get_init_ground_state, get_sequence, flip_labels

from typing import Any, Dict, List
from abc import ABC, abstractclassmethod, abstractmethod

logger = logging.getLogger(__name__)


[docs]class C3QasmSimulator(Backend, ABC): """An Abtract Base Class for C3 Qasm Simulators for Qiskit. This class CAN NOT be instantiated directly. Classes derived from this must compulsorily implement :: def __init__(self, configuration=None, provider=None, **fields): def _default_options(cls) -> None: def run_experiment(self, experiment: QasmQobjExperiment) -> Dict[str, Any]: Parameters ---------- Backend : qiskit.providers.BackendV1 The C3QasmSimulator is derived from BackendV1 ABC : abc.ABC Helper class for defining Abstract classes using ABCMeta """ @abstractclassmethod def _default_options(cls) -> None: raise NotImplementedError("This must be implemented in the derived class")
[docs] def set_device_config(self, config_file: str) -> None: """Set the path to the config for the device Parameters ---------- config_file : str path to hjson file storing the configuration for all device parameters for simulation """ self._device_config = config_file
[docs] def get_labels(self) -> List[str]: """Return state labels for the system Returns ------- List[str] A list of state labels in hex format :: labels = ['0x1', ...] """ labels = [ hex(i) for i in range( 0, pow( self._number_of_levels, self._number_of_qubits, ), ) ] return labels
[docs] def disable_flip_labels(self) -> None: """Disable flipping of labels State Labels are flipped before returning results to match Qiskit style qubit indexing convention This function allows disabling of the flip """ self._flip_labels = False
[docs] def run(self, qobj: qobj.Qobj, **backend_options) -> C3Job: """Parse and run a Qobj Parameters ---------- qobj : Qobj The Qobj payload for the experiment backend_options : dict backend options Returns ------- C3Job An instance of the C3Job (derived from JobV1) with the result Raises ------ QiskitError Support for Pulse Jobs is not implemented Notes ----- 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 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), } """ if isinstance(qobj, (QuantumCircuit, list)): qobj = assemble(qobj, self, **backend_options) qobj_options = qobj.config elif isinstance(qobj, qobj.PulseQobj): raise QiskitError("Pulse jobs are not accepted") else: qobj_options = qobj.config self._set_options(qobj_config=qobj_options, backend_options=backend_options) job_id = str(uuid.uuid4()) job = C3Job(self, job_id, self._run_job(job_id, qobj)) return job
def _run_job(self, job_id, qobj): """Run experiments in qobj Parameters ---------- job_id : str unique id for the job qobj : Qobj job description Returns ------- Result Result object """ self._validate(qobj) result_list = [] self._shots = qobj.config.shots self._memory = getattr(qobj.config, "memory", False) self._qobj_config = qobj.config start = time.time() for experiment in qobj.experiments: result_list.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)
[docs] @abstractmethod def run_experiment(self, experiment: QasmQobjExperiment) -> Dict[str, Any]: raise NotImplementedError("This must be implemented in the derived class")
def _validate(self, qobj): """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 C3QiskitError( "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: logger.warning( 'No classical registers in circuit "%s", ' "counts will be empty.", name, ) elif "measure" not in [op.name for op in experiment.instructions]: logger.warning( 'No measurements in circuit "%s", ' "classical register will remain all zeros.", name, ) def _validate_initial_statevector(self): """Raise an error when experiment tries to set initial statevector Raises ------ C3QiskitError Error for statevector initialisation not implemented """ if self._initial_statevector is not None: raise C3QiskitError( "Setting initial statevector is not implemented in this simulator" ) else: pass def _initialize_statevector(self): """Raise an error when experiment tries to set initial statevector Raises ------ C3QiskitError Error for statevector initialisation not implemented """ if self._initial_statevector is not None: raise C3QiskitError( "Setting initial statevector is not implemented in this simulator" ) else: pass def _set_options(self, qobj_config=None, backend_options=None): """Qiskit stock method to Set the backend options for all experiments in a qobj""" # Reset default options self._initial_statevector = self.options.get("initial_statevector") if "backend_options" in backend_options and backend_options["backend_options"]: backend_options = backend_options["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 ) 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 C3QiskitError( "initial statevector is not normalized: " + "norm {} != 1".format(norm) )
[docs]class C3QasmPerfectSimulator(C3QasmSimulator): """A C3-based perfect gates simulator for Qiskit Parameters ---------- C3QasmSimulator : c3.qiskit.c3_backend.C3QasmSimulator Inherits the C3QasmSimulator and implements a perfect gate simulator """ # TODO List correct set of basis gates MAX_QUBITS_MEMORY = 20 _configuration = { "backend_name": "c3_qasm_perfect_simulator", "backend_version": "0.1", "n_qubits": MAX_QUBITS_MEMORY, "url": "https://github.com/q-optimize/c3", "simulator": True, "local": True, "conditional": False, "open_pulse": False, "memory": False, "max_shots": 65536, "coupling_map": None, "description": "A c3 simulator for qasm experiments with perfect gates", "basis_gates": [ "cx", "cz", "iSwap", "id", "x", "y", "z", "rx", "ry", "rz", "rzx", ], "gates": [], } DEFAULT_OPTIONS = {"initial_statevector": None, "shots": 1024, "memory": False} def __init__(self, configuration=None, provider=None, **fields): super().__init__( configuration=( configuration or QasmBackendConfiguration.from_dict(self._configuration) ), provider=provider, **fields ) # Define attributes in __init__. self._local_random = np.random.RandomState() self._classical_memory = 0 self._classical_register = 0 self._statevector = 0 self._number_of_cmembits = 0 self._number_of_qubits = 0 self._shots = 0 self._memory = False self._initial_statevector = self.options.get("initial_statevector") self._qobj_config = None # TEMP self._sample_measure = False self._flip_labels = True @classmethod def _default_options(cls) -> Options: return Options(shots=1024, memory=False, initial_statevector=None)
[docs] def run_experiment(self, experiment: QasmQobjExperiment) -> Dict[str, Any]: """Run an experiment (circuit) and return a single experiment result Parameters ---------- experiment : QasmQobjExperiment experiment from qobj experiments list Returns ------- Dict[str, Any] 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 } Raises ------ C3QiskitError If an error occured """ start = time.time() # setup C3 Experiment exp = Experiment() exp.load_quick_setup(self._device_config) pmap = exp.pmap instructions = pmap.instructions # initialise parameters self._number_of_qubits = len(pmap.model.subsystems) if self._number_of_qubits != experiment.config.n_qubits: raise C3QiskitError("Number of qubits in Circuit & Device don't match") shots = self._shots # TODO (Check) Assume all qubits have same Hilbert dims self._number_of_levels = pmap.model.dims[0] # Validate the dimension of initial statevector if set self._validate_initial_statevector() # TODO set simulator seed, check qiskit python qasm simulator # qiskit-terra/qiskit/providers/basicaer/qasm_simulator.py seed_simulator = 2441129 # convert qasm instruction set to c3 sequence sequence = get_sequence(experiment.instructions) # unique operations gate_keys = list(set(sequence)) # validate gates for gate in gate_keys: if gate not in instructions.keys(): raise C3QiskitError( "Gate {gate} not found in Device Instruction Set: {instructions}".format( gate=gate, instructions=list(instructions.keys()) ) ) perfect_gates = exp.get_perfect_gates(gate_keys) # initialise state psi_init = get_init_ground_state(self._number_of_qubits, self._number_of_levels) psi_t = psi_init.numpy() pop_t = exp.populations(psi_t, False) # compute final state for gate in sequence: psi_t = np.matmul(perfect_gates[gate], psi_t) pops = exp.populations(psi_t, False) pop_t = np.append(pop_t, pops, axis=1) # generate shots style readout with no SPAM # TODO a more sophisticated readout/measurement routine shots_data = (np.round(pop_t.T[-1] * shots)).astype("int32") # generate state labels output_labels = self.get_labels() # create results dict counts = dict(zip(output_labels, shots_data)) # keep only non-zero states counts = dict(filter(lambda elem: elem[1] != 0, counts.items())) # flipping state labels to match qiskit style qubit indexing convention # default is to flip labels to qiskit style, use disable_flip_labels() if self._flip_labels: counts = flip_labels(counts) end = time.time() exp_result = { "name": experiment.header.name, "header": experiment.header.to_dict(), "shots": self._shots, "seed": seed_simulator, "status": "DONE", "success": True, "data": {"counts": counts}, "time_taken": (end - start), } return exp_result
[docs]class C3QasmPhysicsSimulator(C3QasmSimulator): # TODO Boilerplate code. This simulator is not yet implemented. """A C3-based perfect gates simulator for Qiskit Parameters ---------- C3QasmSimulator : c3.qiskit.c3_backend.C3QasmSimulator Inherits the C3QasmSimulator and implements a physics based simulator """ MAX_QUBITS_MEMORY = 10 _configuration = { "backend_name": "c3_qasm_physics_simulator", "backend_version": "0.1", "n_qubits": MAX_QUBITS_MEMORY, "url": "https://github.com/q-optimize/c3", "simulator": True, "local": True, "conditional": False, "open_pulse": False, "memory": False, "max_shots": 65536, "coupling_map": None, "description": "A physics based c3 simulator for qasm experiments", "basis_gates": [], # TODO add basis gates "gates": [], } DEFAULT_OPTIONS = {"initial_statevector": None, "shots": 1024, "memory": False} def __init__(self, configuration=None, provider=None, **fields): super().__init__( configuration=( configuration or QasmBackendConfiguration.from_dict(self._configuration) ), provider=provider, **fields ) # Define attributes in __init__. self._local_random = np.random.RandomState() self._classical_memory = 0 self._classical_register = 0 self._statevector = 0 self._number_of_cmembits = 0 self._number_of_qubits = 0 self._shots = 0 self._memory = False self._initial_statevector = self.options.get("initial_statevector") self._qobj_config = None # TEMP self._sample_measure = False @classmethod def _default_options(cls) -> Options: return Options(shots=1024, memory=False, initial_statevector=None)
[docs] def run_experiment(self, experiment: QasmQobjExperiment) -> Dict[str, Any]: """Run an experiment (circuit) and return a single experiment result Parameters ---------- experiment : QasmQobjExperiment experiment from qobj experiments list Returns ------- Dict[str, Any] 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 } Raises ------ C3QiskitError If an error occured """ start = time.time() # setup C3 Experiment exp = Experiment() exp.quick_setup(self._device_config) pmap = exp.pmap model = pmap.model # noqa # initialise parameters self._number_of_qubits = len(pmap.model.subsystems) if self._number_of_qubits != experiment.config.n_qubits: raise C3QiskitError("Number of qubits in Circuit & Device dont match") shots = self._shots # noqa # TODO (Check) Assume all qubits have same Hilbert dims self._number_of_levels = pmap.model.dims[0] # Validate the dimension of initial statevector if set self._validate_initial_statevector() # TODO set simulator seed, check qiskit python qasm simulator # qiskit-terra/qiskit/providers/basicaer/qasm_simulator.py seed_simulator = 2441129 # convert qasm instruction set to c3 sequence sequence = get_sequence(experiment.instructions) # noqa # TODO get_init_ground_state(), compute_propagators(), evaluate(), process() # generate shots style readout with no SPAM # TODO a sophisticated readout/measurement routine # TODO generate state labels using get_labels() # TODO create results dict and remove empty states counts = {} # type: ignore # flipping state labels to match qiskit style qubit indexing convention # default is to flip labels to qiskit style, use disable_flip_labels() if self._flip_labels: counts = flip_labels(counts) end = time.time() exp_result = { "name": experiment.header.name, "header": experiment.header.to_dict(), "shots": self._shots, "seed": seed_simulator, "status": "DONE", "success": True, "data": {"counts": counts}, "time_taken": (end - start), } return exp_result