Skip to content

Commit

Permalink
add AerCircuit and a path to run simulation without QObj
Browse files Browse the repository at this point in the history
  • Loading branch information
hhorii committed Feb 7, 2023
1 parent 0955bee commit 17f8151
Show file tree
Hide file tree
Showing 24 changed files with 1,185 additions and 281 deletions.
1 change: 1 addition & 0 deletions qiskit_aer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
from qiskit_aer import pulse
from qiskit_aer import quantum_info
from qiskit_aer import noise
from qiskit_aer import circuit
from qiskit_aer import utils
from qiskit_aer.version import __version__

Expand Down
9 changes: 7 additions & 2 deletions qiskit_aer/backends/aer_simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@

from ..version import __version__
from .aerbackend import AerBackend, AerError
from .backend_utils import (cpp_execute, available_methods,
available_devices,
from .backend_utils import (cpp_execute, cpp_execute_direct,
available_methods, available_devices,
MAX_QUBITS_STATEVECTOR,
BASIS_GATES)
# pylint: disable=import-error, no-name-in-module
Expand Down Expand Up @@ -730,6 +730,11 @@ def configuration(self):
config.backend_name = self.name()
return config

def _execute_direct(self, circuits, noise_model, config):
"""Execute circuits on the backend.
"""
return cpp_execute_direct(self._controller, circuits, noise_model, config)

def _execute(self, qobj):
"""Execute a qobj on the backend.
Expand Down
235 changes: 161 additions & 74 deletions qiskit_aer/backends/aerbackend.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,12 @@
from qiskit.qobj import QasmQobj, PulseQobj
from qiskit.result import Result
from ..aererror import AerError
from ..jobs import AerJob, AerJobSet, split_qobj
from ..jobs import AerJob, AerJobSet, AerJobDirect, split_qobj
from ..noise.noise_model import NoiseModel, QuantumErrorLocation
from ..noise.errors.quantum_error import QuantumChannelInstruction
from .aer_compiler import compile_circuit
from .backend_utils import format_save_type, circuit_optypes
from ..circuit.aer_circuit import generate_aer_circuits

# Logger
logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -159,30 +160,54 @@ def run(self,
Raises:
ValueError: if run is not implemented
"""
if isinstance(circuits, (QasmQobj, PulseQobj)):
warnings.warn(
'Using a qobj for run() is deprecated as of qiskit-aer 0.9.0'
' and will be removed no sooner than 3 months from that release'
' date. Transpiled circuits should now be passed directly using'
' `backend.run(circuits, **run_options).',
DeprecationWarning, stacklevel=2)
if parameter_binds:
raise AerError("Parameter binds can't be used with an input qobj")
# A work around to support both qobj options and run options until
# qobj is deprecated is to copy all the set qobj.config fields into
# run_options that don't override existing fields. This means set
# run_options fields will take precidence over the value for those
# fields that are set via assemble.
if not run_options:
run_options = circuits.config.__dict__
if isinstance(circuits, (QuantumCircuit, Schedule, ScheduleBlock)):
circuits = [circuits]
if not isinstance(circuits, list):
raise AerError("bad input to run() function; " +
"circuits must be either circuits or schedules")

if all(isinstance(circ, QuantumCircuit) for circ in circuits):
if (('executor' in run_options and run_options['executor']) or
('executor' in self.options.__dict__ and self.options.__dict__['executor']) or
validate):
return self._run_qobj(circuits, validate, parameter_binds, **run_options)
else:
run_options = copy.copy(run_options)
for key, value in circuits.config.__dict__.items():
if key not in run_options and value is not None:
run_options[key] = value
qobj = self._assemble(circuits, **run_options)
# return self._run_qobj(circuits, validate, parameter_binds, **run_options)
return self._run_circuits(circuits, parameter_binds, **run_options)
elif all(isinstance(circ, (ScheduleBlock, Schedule)) for circ in circuits):
return self._run_qobj(circuits, validate, parameter_binds, **run_options)
else:
qobj = self._assemble(circuits, parameter_binds=parameter_binds, **run_options)
raise AerError("bad input to run() function;" +
"circuits must be either circuits or schedules")

def _run_circuits(self,
circuits,
parameter_binds,
**run_options):
"""Run circuits by generating native circuits.
"""
circuits, noise_model = self._compile(circuits, **run_options)
if parameter_binds:
run_options['parameterizations'] = self._convert_binds(circuits, parameter_binds)
aer_circuits, config = generate_aer_circuits(circuits, self.options, **run_options)

# Submit job
job_id = str(uuid.uuid4())
aer_job = AerJobDirect(self, job_id, self._run_direct, aer_circuits, noise_model, config)
aer_job.submit()

return aer_job

# pylint: disable=arguments-differ
def _run_qobj(self,
circuits,
validate=False,
parameter_binds=None,
**run_options):
"""Run circuits by assembling qobj.
"""

qobj = self._assemble(circuits, parameter_binds=parameter_binds, **run_options)

# Optional validation
if validate:
Expand Down Expand Up @@ -345,6 +370,62 @@ def _run(self, qobj, job_id='', format_result=True):
return self._format_results(output)
return output

def _run_direct(self, circuits, noise_model, config, job_id='', format_result=True):
"""Run a job"""
# Start timer
start = time.time()

# Take metadata from headers of experiments to work around JSON serialization error
metadata_list = []
metadata_index = 0
for circ in circuits:
if hasattr(circ._header, "metadata") and circ._header.metadata:
metadata_copy = circ._header.metadata.copy()
metadata_list.append(metadata_copy)
circ._header.metadata.clear()
if "id" in metadata_copy:
circ._header.metadata["id"] = metadata_copy["id"]
circ._header.metadata["metadata_index"] = metadata_index
metadata_index += 1

# Run simulation
output = self._execute_direct(circuits, noise_model, config)

# Validate output
if not isinstance(output, dict):
logger.error("%s: simulation failed.", self.name())
if output:
logger.error('Output: %s', output)
raise AerError(
"simulation terminated without returning valid output.")

# Format results
output["job_id"] = job_id
output["date"] = datetime.datetime.now().isoformat()
output["backend_name"] = self.name()
output["backend_version"] = self.configuration().backend_version

# Push metadata to experiment headers
for result in output["results"]:
if ("header" in result and
"metadata" in result["header"] and result["header"]["metadata"] and
"metadata_index" in result["header"]["metadata"]):
metadata_index = result["header"]["metadata"]["metadata_index"]
result["header"]["metadata"] = metadata_list[metadata_index]

# Add execution time
output["time_taken"] = time.time() - start

# Display warning if simulation failed
if not output.get("success", False):
msg = "Simulation failed"
if "status" in output:
msg += f" and returned the following error message:\n{output['status']}"
logger.warning(msg)
if format_result:
return self._format_results(output)
return output

@staticmethod
def _format_results(output):
"""Format C++ simulator output for constructing Result"""
Expand All @@ -358,50 +439,51 @@ def _format_results(output):
data[key] = format_save_type(val, save_types[key], save_subtypes[key])
return Result.from_dict(output)

def _compile(self, circuits, **run_options):
"""Compile circuits and noise model"""
if isinstance(circuits, (QuantumCircuit, Schedule, ScheduleBlock)):
circuits = [circuits]
optypes = [circuit_optypes(circ) for circ in circuits]

# Compile Qasm3 instructions
circuits, optypes = compile_circuit(
circuits,
basis_gates=self.configuration().basis_gates,
optypes=optypes)

# run option noise model
circuits, noise_model, run_options = self._assemble_noise_model(
circuits, optypes, **run_options)

return circuits, noise_model

def _assemble(self, circuits, parameter_binds=None, **run_options):
"""Assemble one or more Qobj for running on the simulator"""
if isinstance(circuits, (QasmQobj, PulseQobj)):
qobj = circuits
else:
# Generate optypes for circuit
# Generate opsets of instructions
if isinstance(circuits, (QuantumCircuit, Schedule, ScheduleBlock)):
circuits = [circuits]
optypes = [circuit_optypes(circ) for circ in circuits]

# Compile Qasm3 instructions
circuits, optypes = compile_circuit(
circuits,
basis_gates=self.configuration().basis_gates,
optypes=optypes)

# run option noise model
circuits, optypes, run_options = self._assemble_noise_model(
circuits, optypes, **run_options)

if parameter_binds:
# Handle parameter binding
parameterizations = self._convert_binds(circuits, parameter_binds)
qobj = None
for circuit in circuits:
assemble_bind = {param: 1 for param in circuit.parameters}
qobj_tmp = assemble(
[circuit],
backend=self,
parameter_binds=[assemble_bind],
parameterizations=parameterizations)
if qobj:
qobj.experiments.append(qobj_tmp.experiments[0])
else:
qobj = qobj_tmp
else:
qobj = assemble(circuits, backend=self)

# Add optypes to qobj
# We convert to strings to avoid pybinding of types
qobj.config.optypes = [
set(i.__name__ for i in optype) if optype else set()
for optype in optypes]
# compile and insert noise injection points
circuits, noise_model = self._compile(circuits, **run_options)

# If noise model exists, add it to the run options
if noise_model:
run_options['noise_model'] = noise_model

if parameter_binds:
# Handle parameter binding
parameterizations = self._convert_binds(circuits, parameter_binds)
qobj = None
for circuit in circuits:
assemble_bind = {param: 1 for param in circuit.parameters}
qobj_tmp = assemble(
[circuit],
backend=self,
parameter_binds=[assemble_bind],
parameterizations=parameterizations)
if qobj:
qobj.experiments.append(qobj_tmp.experiments[0])
else:
qobj = qobj_tmp
else:
qobj = assemble(circuits, backend=self)

# Add options
for key, val in self.options.__dict__.items():
Expand All @@ -416,11 +498,8 @@ def _assemble(self, circuits, parameter_binds=None, **run_options):

def _assemble_noise_model(self, circuits, optypes, **run_options):
"""Move quantum error instructions from circuits to noise model"""
if isinstance(circuits, (QuantumCircuit, Schedule, ScheduleBlock)):
run_circuits = [circuits]
else:
# Make a shallow copy so we can modify list elements if required
run_circuits = copy.copy(circuits)
# Make a shallow copy so we can modify list elements if required
run_circuits = copy.copy(circuits)

# Flag for if we need to make a deep copy of the noise model
# This avoids unnecessarily copying the noise model for circuits
Expand Down Expand Up @@ -482,12 +561,8 @@ def _assemble_noise_model(self, circuits, optypes, **run_options):
run_circuits[idx] = new_circ
optypes[idx].discard(QuantumChannelInstruction)

# If we modified the existing noise model, add it to the run options
if updated_noise:
run_options['noise_model'] = noise_model

# Return the possibly updated circuits and noise model
return run_circuits, optypes, run_options
return run_circuits, noise_model, run_options

def _get_executor(self, **run_options):
"""Get the executor"""
Expand All @@ -508,6 +583,18 @@ def _execute(self, qobj):
"""
pass

@abstractmethod
def _execute_direct(self, aer_circuits, noise_model, config):
"""Execute aer circuits on the backend.
Args:
aer_circuits (List of AerCircuit): simulator input.
Returns:
dict: return a dictionary of results.
"""
pass

def _validate(self, qobj):
"""Validate the qobj for the backend"""
pass
Expand Down
42 changes: 41 additions & 1 deletion qiskit_aer/backends/backend_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from qiskit.quantum_info import Clifford
from .compatibility import (
Statevector, DensityMatrix, StabilizerState, Operator, SuperOp)

from qiskit_aer.circuit.aer_operation import AerOp

# Available system memory
SYSTEM_MEMORY_GB = local_hardware_info()['memory']
Expand Down Expand Up @@ -127,6 +127,20 @@ def cpp_execute(controller, qobj):
return controller(qobj)


def cpp_execute_direct(controller, aer_circuits, noise_model, config):
"""Execute aer circuits on C++ controller wrapper"""

native_circuits = [aer_circuit.assemble_native() for aer_circuit in aer_circuits]

# Location where we put external libraries that will be
# loaded at runtime by the simulator extension
config['library_dir'] = LIBRARY_DIR

noise_model = noise_model.to_dict(serializable=True) if noise_model else {}

return controller.execute(native_circuits, noise_model, config)


def available_methods(controller, methods, devices):
"""Check available simulation methods by running a dummy circuit."""
# Test methods are available using the controller
Expand Down Expand Up @@ -185,6 +199,24 @@ def save_inst(num_qubits):
return qobj


def add_final_save_op(aer_circs, state):
"""Add final save state op to all experiments in a qobj."""

def save_inst(num_qubits):
"""Return n-qubit save statevector inst"""
return QasmQobjInstruction(
name=f"save_{state}",
qubits=list(range(num_qubits)),
label=f"{state}",
snapshot_type="single")

for aer_circ in aer_circs:
num_qubits = aer_circ.num_qubits
aer_circ.append(AerOp(save_inst(num_qubits)))

return aer_circs


def map_legacy_method_options(qobj):
"""Map legacy method names of qasm simulator to aer simulator options"""
method = getattr(qobj.config, "method", None)
Expand All @@ -193,6 +225,14 @@ def map_legacy_method_options(qobj):
return qobj


def map_legacy_method_config(config):
"""Map legacy method names of qasm simulator to aer simulator options"""
method = config["method"] if "method" in config else None
if method in LEGACY_METHOD_MAP:
config["method"], config["device"] = LEGACY_METHOD_MAP[method]
return config


def format_save_type(data, save_type, save_subtype):
"""Format raw simulator result data based on save type."""
init_fns = {
Expand Down
Loading

0 comments on commit 17f8151

Please sign in to comment.