diff --git a/qiskit_aer/__init__.py b/qiskit_aer/__init__.py index 730eed4e52..ba26e5f3f2 100644 --- a/qiskit_aer/__init__.py +++ b/qiskit_aer/__init__.py @@ -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__ diff --git a/qiskit_aer/backends/aer_simulator.py b/qiskit_aer/backends/aer_simulator.py index f761ecbe3f..f2b5f822a7 100644 --- a/qiskit_aer/backends/aer_simulator.py +++ b/qiskit_aer/backends/aer_simulator.py @@ -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 @@ -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. diff --git a/qiskit_aer/backends/aerbackend.py b/qiskit_aer/backends/aerbackend.py index 9b838e74de..c4532d06e7 100644 --- a/qiskit_aer/backends/aerbackend.py +++ b/qiskit_aer/backends/aerbackend.py @@ -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__) @@ -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: @@ -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""" @@ -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(): @@ -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 @@ -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""" @@ -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 diff --git a/qiskit_aer/backends/backend_utils.py b/qiskit_aer/backends/backend_utils.py index dcbc6a50f9..609a08386a 100644 --- a/qiskit_aer/backends/backend_utils.py +++ b/qiskit_aer/backends/backend_utils.py @@ -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'] @@ -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 @@ -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) @@ -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 = { diff --git a/qiskit_aer/backends/pulse_simulator.py b/qiskit_aer/backends/pulse_simulator.py index 87221e002b..dc01464806 100644 --- a/qiskit_aer/backends/pulse_simulator.py +++ b/qiskit_aer/backends/pulse_simulator.py @@ -278,6 +278,12 @@ def _execute(self, qobj): qobj.config.qubit_freq_est = self.defaults().qubit_freq_est return pulse_controller(qobj) + def _execute_direct(self, circuits, noise_model, config): + """Execute circuits on the backend. + """ + raise AerError("not implemented yet") + # return cpp_execute_direct(self._controller, circuits, noise_model, config) + def set_option(self, key, value): """Set pulse simulation options and update backend.""" if key == 'meas_levels': diff --git a/qiskit_aer/backends/qasm_simulator.py b/qiskit_aer/backends/qasm_simulator.py index a0dec10856..a4a92843a0 100644 --- a/qiskit_aer/backends/qasm_simulator.py +++ b/qiskit_aer/backends/qasm_simulator.py @@ -26,7 +26,8 @@ from .backend_utils import (cpp_execute, available_methods, MAX_QUBITS_STATEVECTOR, LEGACY_METHOD_MAP, - map_legacy_method_options) + map_legacy_method_options, + map_legacy_method_config) # pylint: disable=import-error, no-name-in-module from .controller_wrappers import aer_controller_execute @@ -517,6 +518,12 @@ def _execute(self, qobj): qobj = map_legacy_method_options(qobj) return cpp_execute(self._controller, qobj) + def _execute_direct(self, circuits, noise_model, config): + """Execute circuits on the backend. + """ + config = map_legacy_method_config(config) + return cpp_execute_direct(self._controller, circuits, noise_model, config) + def set_option(self, key, value): if key == "custom_instructions": self._set_configuration_option(key, value) diff --git a/qiskit_aer/backends/statevector_simulator.py b/qiskit_aer/backends/statevector_simulator.py index 100cfb7b57..7067c32f9c 100644 --- a/qiskit_aer/backends/statevector_simulator.py +++ b/qiskit_aer/backends/statevector_simulator.py @@ -27,7 +27,11 @@ MAX_QUBITS_STATEVECTOR, LEGACY_METHOD_MAP, add_final_save_instruction, - map_legacy_method_options) + cpp_execute_direct, + map_legacy_method_options, + map_legacy_method_config, + add_final_save_op, + ) # pylint: disable=import-error, no-name-in-module from .controller_wrappers import aer_controller_execute @@ -261,12 +265,17 @@ def _execute(self, qobj): Returns: dict: return a dictionary of results. """ - # Make deepcopy so we don't modify the original qobj - qobj = copy.deepcopy(qobj) qobj = add_final_save_instruction(qobj, "statevector") qobj = map_legacy_method_options(qobj) return cpp_execute(self._controller, qobj) + def _execute_direct(self, circuits, noise_model, config): + """Execute circuits on the backend. + """ + circuits = add_final_save_op(circuits, "statevector") + config = map_legacy_method_config(config) + return cpp_execute_direct(self._controller, circuits, noise_model, config) + def _validate(self, qobj): """Semantic validations of the qobj which cannot be done via schemas. Some of these may later move to backend schemas. diff --git a/qiskit_aer/backends/unitary_simulator.py b/qiskit_aer/backends/unitary_simulator.py index 2db5880aa9..056a7348a5 100644 --- a/qiskit_aer/backends/unitary_simulator.py +++ b/qiskit_aer/backends/unitary_simulator.py @@ -24,11 +24,14 @@ from ..aererror import AerError from ..version import __version__ from .aerbackend import AerBackend -from .backend_utils import (cpp_execute, available_devices, +from .backend_utils import (cpp_execute, cpp_execute_direct, + available_devices, MAX_QUBITS_STATEVECTOR, LEGACY_METHOD_MAP, add_final_save_instruction, - map_legacy_method_options) + map_legacy_method_options, + add_final_save_op, + map_legacy_method_config) # pylint: disable=import-error, no-name-in-module from .controller_wrappers import aer_controller_execute @@ -260,12 +263,17 @@ def _execute(self, qobj): Returns: dict: return a dictionary of results. """ - # Make deepcopy so we don't modify the original qobj - qobj = copy.deepcopy(qobj) qobj = add_final_save_instruction(qobj, "unitary") qobj = map_legacy_method_options(qobj) return cpp_execute(self._controller, qobj) + def _execute_direct(self, circuits, noise_model, config): + """Execute circuits on the backend. + """ + circuits = add_final_save_op(circuits, "unitary") + config = map_legacy_method_config(config) + return cpp_execute_direct(self._controller, circuits, noise_model, config) + def _validate(self, qobj): """Semantic validations of the qobj which cannot be done via schemas. Some of these may later move to backend schemas. diff --git a/qiskit_aer/backends/wrappers/aer_circuit_binding.hpp b/qiskit_aer/backends/wrappers/aer_circuit_binding.hpp new file mode 100644 index 0000000000..5723d3eb31 --- /dev/null +++ b/qiskit_aer/backends/wrappers/aer_circuit_binding.hpp @@ -0,0 +1,99 @@ +/** + * This code is part of Qiskit. + * + * (C) Copyright IBM 2023. + * + * 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. + */ + +#ifndef _aer_circuit_binding_hpp_ +#define _aer_circuit_binding_hpp_ + +#include "misc/warnings.hpp" +DISABLE_WARNING_PUSH +#include +DISABLE_WARNING_POP +#if defined(_MSC_VER) + #undef snprintf +#endif + +#include + +#include "framework/matrix.hpp" +#include "framework/python_parser.hpp" +#include "framework/pybind_json.hpp" +#include "framework/pybind_casts.hpp" + +#include "framework/types.hpp" +#include "framework/results/pybind_result.hpp" + +#include "framework/circuit.hpp" + +namespace py = pybind11; +using namespace AER; + +template +void bind_aer_op_(MODULE m) { + py::class_ aer_op(m, "AerOp_"); + aer_op.def(py::init(), "constructor"); + aer_op.def("__repr__", [](const Operations::Op &op) { std::stringstream ss; ss << op; return ss.str(); }); + aer_op.def_readwrite("conditional", &Operations::Op::conditional); + aer_op.def_readwrite("conditional_reg", &Operations::Op::conditional_reg); +} + +template +void bind_aer_circuit_(MODULE m) { + py::class_ aer_circuit(m, "AerCircuit_"); + aer_circuit.def(py::init()); + aer_circuit.def("__repr__", [](const Circuit &circ) { + std::stringstream ss; + ss << "Circuit(" + << "qubit=" << circ.num_qubits + << ", num_memory=" << circ.num_memory + << ", num_registers=" << circ.num_registers; + + ss << ", ops={"; + for (auto i = 0; i < circ.ops.size(); ++i) + if (i == 0) + ss << circ.ops[i]; + else + ss << "," << circ.ops[i]; + + ss << "}" + << ", shots=" << circ.shots + << ", seed=" << circ.seed + << ", global_phase_angle=" << circ.global_phase_angle + ; + ss << ")"; + return ss.str(); + }); + aer_circuit.def_readwrite("num_qubits", &Circuit::num_qubits); + aer_circuit.def_readwrite("num_memory", &Circuit::num_memory); + aer_circuit.def_readwrite("ops", &Circuit::ops); + aer_circuit.def_readwrite("global_phase_angle", &Circuit::global_phase_angle); + aer_circuit.def_readwrite("header", &Circuit::header); + aer_circuit.def("set_header", [aer_circuit](Circuit &circ, + const py::handle& header) { + circ.header = header; + }); + + aer_circuit.def("append", [aer_circuit](Circuit &circ, const Operations::Op &op) { + circ.ops.push_back(op); + }); +} + +template +void bind_aer_circuit(MODULE m) { + + bind_aer_op_(m); + bind_aer_circuit_(m); + +} + +#endif \ No newline at end of file diff --git a/qiskit_aer/backends/wrappers/aer_controller_binding.hpp b/qiskit_aer/backends/wrappers/aer_controller_binding.hpp new file mode 100644 index 0000000000..3420935f8e --- /dev/null +++ b/qiskit_aer/backends/wrappers/aer_controller_binding.hpp @@ -0,0 +1,82 @@ +/** + * This code is part of Qiskit. + * + * (C) Copyright IBM 2023. + * + * 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. + */ + +#ifndef _aer_controller_binding_hpp_ +#define _aer_controller_binding_hpp_ + +#include "misc/warnings.hpp" +DISABLE_WARNING_PUSH +#include +DISABLE_WARNING_POP +#if defined(_MSC_VER) + #undef snprintf +#endif + +#include + +#include "framework/matrix.hpp" +#include "framework/python_parser.hpp" +#include "framework/pybind_casts.hpp" +#include "framework/types.hpp" +#include "framework/results/pybind_result.hpp" + +#include "controllers/aer_controller.hpp" + +#include "controllers/controller_execute.hpp" + +namespace py = pybind11; +using namespace AER; + +template +class ControllerExecutor { +public: + ControllerExecutor() = default; + py::object operator()(const py::handle &qobj) { +#ifdef TEST_JSON // Convert input qobj to json to test standalone data reading + return AerToPy::to_python(controller_execute(json_t(qobj))); +#else + return AerToPy::to_python(controller_execute(qobj)); +#endif + } + + py::object execute(std::vector &circuits, Noise::NoiseModel &noise_model, json_t& config) const { + return AerToPy::to_python(controller_execute(circuits, noise_model, config)); + } +}; + +template +void bind_aer_controller(MODULE m) { + py::class_ > aer_ctrl (m, "aer_controller_execute"); + aer_ctrl.def(py::init<>()); + aer_ctrl.def("__call__", &ControllerExecutor::operator()); + aer_ctrl.def("__reduce__", [aer_ctrl](const ControllerExecutor &self) { + return py::make_tuple(aer_ctrl, py::tuple()); + }); + aer_ctrl.def("execute", [aer_ctrl](ControllerExecutor &self, + std::vector &circuits, + py::object noise_model, + py::object config) { + + Noise::NoiseModel noise_model_native; + if (noise_model) + noise_model_native.load_from_json(noise_model); + + json_t config_json; + if (config) + config_json = config; + + return self.execute(circuits, noise_model_native, config_json); + }); +} +#endif diff --git a/qiskit_aer/backends/wrappers/aer_state_binding.hpp b/qiskit_aer/backends/wrappers/aer_state_binding.hpp new file mode 100644 index 0000000000..f9ee185ecc --- /dev/null +++ b/qiskit_aer/backends/wrappers/aer_state_binding.hpp @@ -0,0 +1,156 @@ +/** + * This code is part of Qiskit. + * + * (C) Copyright IBM 2023. + * + * 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. + */ + +#ifndef _aer_state_binding_hpp_ +#define _aer_state_binding_hpp_ + +#include "misc/warnings.hpp" +DISABLE_WARNING_PUSH +#include +DISABLE_WARNING_POP +#if defined(_MSC_VER) + #undef snprintf +#endif + +#include +#include + +#include "framework/matrix.hpp" +#include "framework/python_parser.hpp" +#include "framework/pybind_casts.hpp" +#include "framework/types.hpp" +#include "framework/results/pybind_result.hpp" + +#include "controllers/state_controller.hpp" + +namespace py = pybind11; +using namespace AER; + +template +void bind_aer_state(MODULE m) { + py::class_ aer_state(m, "AerStateWrapper"); + + aer_state.def(py::init<>(), "constructor"); + + aer_state.def("__repr__", [](const AerState &state) { + std::stringstream ss; + ss << "AerStateWrapper(" + << "initialized=" << state.is_initialized() + << ", num_of_qubits=" << state.num_of_qubits(); + ss << ")"; + return ss.str(); + }); + + aer_state.def("configure", &AerState::configure); + aer_state.def("allocate_qubits", &AerState::allocate_qubits); + aer_state.def("reallocate_qubits", &AerState::reallocate_qubits); + aer_state.def("set_random_seed", &AerState::set_random_seed); + aer_state.def("set_seed", &AerState::set_seed); + aer_state.def("clear", &AerState::clear); + aer_state.def("num_of_qubits", &AerState::num_of_qubits); + + aer_state.def("initialize", &AerState::initialize); + aer_state.def("initialize_statevector", [aer_state](AerState &state, + int num_of_qubits, + py::array_t> &values, + bool copy) { + std::complex* data_ptr = reinterpret_cast*>(values.mutable_data(0)); + state.configure("method", "statevector"); + state.initialize_statevector(num_of_qubits, data_ptr, copy); + return true; + }); + + aer_state.def("move_to_buffer", [aer_state](AerState &state) { + return state.move_to_vector().move_to_buffer(); + }); + + aer_state.def("move_to_ndarray", [aer_state](AerState &state) { + auto vec = state.move_to_vector(); + + std::complex* data_ptr = vec.data(); + auto ret = AerToPy::to_numpy(std::move(vec)); + return ret; + }); + + aer_state.def("flush", &AerState::flush_ops); + + aer_state.def("last_result", [aer_state](AerState &state) { + return AerToPy::to_python(state.last_result().to_json()); + }); + + + aer_state.def("apply_initialize", &AerState::apply_initialize); + aer_state.def("apply_global_phase", &AerState::apply_global_phase); + aer_state.def("apply_unitary", [aer_state](AerState &state, + const reg_t &qubits, + const py::array_t> &values) { + size_t mat_len = (1UL << qubits.size()); + auto ptr = values.unchecked<2>(); + cmatrix_t mat(mat_len, mat_len); + for (auto i = 0; i < mat_len; ++i) + for (auto j = 0; j < mat_len; ++j) + mat(i, j) = ptr(i, j); + state.apply_unitary(qubits, mat); + }); + + aer_state.def("apply_multiplexer", [aer_state](AerState &state, + const reg_t &control_qubits, + const reg_t &target_qubits, + const py::array_t> &values) { + size_t mat_len = (1UL << target_qubits.size()); + size_t mat_size = (1UL << control_qubits.size()); + auto ptr = values.unchecked<3>(); + std::vector mats; + for (auto i = 0; i < mat_size; ++i) { + cmatrix_t mat(mat_len, mat_len); + for (auto j = 0; j < mat_len; ++j) + for (auto k = 0; k < mat_len; ++k) + mat(j, k) = ptr(i, j, k); + mats.push_back(mat); + } + state.apply_multiplexer(control_qubits, target_qubits, mats); + }); + + aer_state.def("apply_diagonal", &AerState::apply_diagonal_matrix); + aer_state.def("apply_x", &AerState::apply_x); + aer_state.def("apply_cx", &AerState::apply_cx); + aer_state.def("apply_mcx", &AerState::apply_mcx); + aer_state.def("apply_y", &AerState::apply_y); + aer_state.def("apply_cy", &AerState::apply_cy); + aer_state.def("apply_mcy", &AerState::apply_mcy); + aer_state.def("apply_z", &AerState::apply_z); + aer_state.def("apply_cz", &AerState::apply_cz); + aer_state.def("apply_mcz", &AerState::apply_mcz); + aer_state.def("apply_mcphase", &AerState::apply_mcphase); + aer_state.def("apply_h", &AerState::apply_h); + aer_state.def("apply_u", &AerState::apply_u); + aer_state.def("apply_cu", &AerState::apply_cu); + aer_state.def("apply_mcu", &AerState::apply_mcu); + aer_state.def("apply_mcswap", &AerState::apply_mcswap); + aer_state.def("apply_measure", &AerState::apply_measure); + aer_state.def("apply_reset", &AerState::apply_reset); + aer_state.def("probability", &AerState::probability); + aer_state.def("probabilities", [aer_state](AerState &state, + const reg_t qubits) { + if (qubits.empty()) + return state.probabilities(); + else + return state.probabilities(qubits); + }, py::arg("qubits") = reg_t()); + aer_state.def("sample_memory", &AerState::sample_memory); + aer_state.def("sample_counts", &AerState::sample_counts); + +} + +#endif \ No newline at end of file diff --git a/qiskit_aer/backends/wrappers/bindings.cc b/qiskit_aer/backends/wrappers/bindings.cc index be54626d69..48b79ad36b 100644 --- a/qiskit_aer/backends/wrappers/bindings.cc +++ b/qiskit_aer/backends/wrappers/bindings.cc @@ -12,29 +12,15 @@ DISABLE_WARNING_POP #undef snprintf #endif -#include "framework/matrix.hpp" -#include "framework/python_parser.hpp" -#include "framework/pybind_casts.hpp" -#include "framework/types.hpp" -#include "framework/results/pybind_result.hpp" +#include "aer_controller_binding.hpp" +#include "aer_state_binding.hpp" +#include "aer_circuit_binding.hpp" -#include "controllers/aer_controller.hpp" -#include "controllers/controller_execute.hpp" +using namespace AER; -#include "controllers/state_controller.hpp" - -template -class ControllerExecutor { -public: - ControllerExecutor() = default; - py::object operator()(const py::handle &qobj) { -#ifdef TEST_JSON // Convert input qobj to json to test standalone data reading - return AerToPy::to_python(AER::controller_execute(json_t(qobj))); -#else - return AerToPy::to_python(AER::controller_execute(qobj)); -#endif - } -}; +Operations::Op create_aer_operation(const py::handle &qasmQobjInst) { + return Operations::input_to_op(qasmQobjInst); +} PYBIND11_MODULE(controller_wrappers, m) { @@ -42,124 +28,8 @@ PYBIND11_MODULE(controller_wrappers, m) { int prov; MPI_Init_thread(nullptr,nullptr,MPI_THREAD_MULTIPLE,&prov); #endif - - py::class_ > aer_ctrl (m, "aer_controller_execute"); - aer_ctrl.def(py::init<>()); - aer_ctrl.def("__call__", &ControllerExecutor::operator()); - aer_ctrl.def("__reduce__", [aer_ctrl](const ControllerExecutor &self) { - return py::make_tuple(aer_ctrl, py::tuple()); - }); - - py::class_ aer_state(m, "AerStateWrapper"); - - aer_state.def(py::init<>(), "constructor"); - - aer_state.def("__repr__", [](const AER::AerState &state) { - std::stringstream ss; - ss << "AerStateWrapper(" - << "initialized=" << state.is_initialized() - << ", num_of_qubits=" << state.num_of_qubits(); - ss << ")"; - return ss.str(); - }); - - aer_state.def("configure", &AER::AerState::configure); - aer_state.def("allocate_qubits", &AER::AerState::allocate_qubits); - aer_state.def("reallocate_qubits", &AER::AerState::reallocate_qubits); - aer_state.def("set_random_seed", &AER::AerState::set_random_seed); - aer_state.def("set_seed", &AER::AerState::set_seed); - aer_state.def("clear", &AER::AerState::clear); - aer_state.def("num_of_qubits", &AER::AerState::num_of_qubits); - - aer_state.def("initialize", &AER::AerState::initialize); - aer_state.def("initialize_statevector", [aer_state](AER::AerState &state, - int num_of_qubits, - py::array_t> &values, - bool copy) { - std::complex* data_ptr = reinterpret_cast*>(values.mutable_data(0)); - state.configure("method", "statevector"); - state.initialize_statevector(num_of_qubits, data_ptr, copy); - return true; - }); - - aer_state.def("move_to_buffer", [aer_state](AER::AerState &state) { - return state.move_to_vector().move_to_buffer(); - }); - - aer_state.def("move_to_ndarray", [aer_state](AER::AerState &state) { - auto vec = state.move_to_vector(); - - std::complex* data_ptr = vec.data(); - auto ret = AerToPy::to_numpy(std::move(vec)); - return ret; - }); - - aer_state.def("flush", &AER::AerState::flush_ops); - - aer_state.def("last_result", [aer_state](AER::AerState &state) { - return AerToPy::to_python(state.last_result().to_json()); - }); - - - aer_state.def("apply_initialize", &AER::AerState::apply_initialize); - aer_state.def("apply_global_phase", &AER::AerState::apply_global_phase); - aer_state.def("apply_unitary", [aer_state](AER::AerState &state, - const reg_t &qubits, - const py::array_t> &values) { - size_t mat_len = (1UL << qubits.size()); - auto ptr = values.unchecked<2>(); - AER::cmatrix_t mat(mat_len, mat_len); - for (auto i = 0; i < mat_len; ++i) - for (auto j = 0; j < mat_len; ++j) - mat(i, j) = ptr(i, j); - state.apply_unitary(qubits, mat); - }); - - aer_state.def("apply_multiplexer", [aer_state](AER::AerState &state, - const reg_t &control_qubits, - const reg_t &target_qubits, - const py::array_t> &values) { - size_t mat_len = (1UL << target_qubits.size()); - size_t mat_size = (1UL << control_qubits.size()); - auto ptr = values.unchecked<3>(); - std::vector mats; - for (auto i = 0; i < mat_size; ++i) { - AER::cmatrix_t mat(mat_len, mat_len); - for (auto j = 0; j < mat_len; ++j) - for (auto k = 0; k < mat_len; ++k) - mat(j, k) = ptr(i, j, k); - mats.push_back(mat); - } - state.apply_multiplexer(control_qubits, target_qubits, mats); - }); - - aer_state.def("apply_diagonal", &AER::AerState::apply_diagonal_matrix); - aer_state.def("apply_x", &AER::AerState::apply_x); - aer_state.def("apply_cx", &AER::AerState::apply_cx); - aer_state.def("apply_mcx", &AER::AerState::apply_mcx); - aer_state.def("apply_y", &AER::AerState::apply_y); - aer_state.def("apply_cy", &AER::AerState::apply_cy); - aer_state.def("apply_mcy", &AER::AerState::apply_mcy); - aer_state.def("apply_z", &AER::AerState::apply_z); - aer_state.def("apply_cz", &AER::AerState::apply_cz); - aer_state.def("apply_mcz", &AER::AerState::apply_mcz); - aer_state.def("apply_mcphase", &AER::AerState::apply_mcphase); - aer_state.def("apply_h", &AER::AerState::apply_h); - aer_state.def("apply_u", &AER::AerState::apply_u); - aer_state.def("apply_cu", &AER::AerState::apply_cu); - aer_state.def("apply_mcu", &AER::AerState::apply_mcu); - aer_state.def("apply_mcswap", &AER::AerState::apply_mcswap); - aer_state.def("apply_measure", &AER::AerState::apply_measure); - aer_state.def("apply_reset", &AER::AerState::apply_reset); - aer_state.def("probability", &AER::AerState::probability); - aer_state.def("probabilities", [aer_state](AER::AerState &state, - const reg_t qubits) { - if (qubits.empty()) - return state.probabilities(); - else - return state.probabilities(qubits); - }, py::arg("qubits") = reg_t()); - aer_state.def("sample_memory", &AER::AerState::sample_memory); - aer_state.def("sample_counts", &AER::AerState::sample_counts); - + m.def("create_aer_operation", &create_aer_operation); + bind_aer_controller(m); + bind_aer_state(m); + bind_aer_circuit(m); } diff --git a/qiskit_aer/circuit/__init__.py b/qiskit_aer/circuit/__init__.py new file mode 100644 index 0000000000..a6c27376ac --- /dev/null +++ b/qiskit_aer/circuit/__init__.py @@ -0,0 +1,35 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. + +"""Internal classes for Aer circuit""" + +""" +================================================ +Aer Circuit (:mod:`qiskit_aer.circuit`) +================================================ + +.. currentmodule:: qiskit_aer.circuit + +This module contains classes and functions to manage Aer circuits. + +Classes +======= + +The following are the classes used to represent internal circuits in Aer. + +.. autosummary:: + :toctree: ../stubs/ + + AerCircuit + AerOperation + +""" diff --git a/qiskit_aer/circuit/aer_circuit.py b/qiskit_aer/circuit/aer_circuit.py new file mode 100644 index 0000000000..93ed605635 --- /dev/null +++ b/qiskit_aer/circuit/aer_circuit.py @@ -0,0 +1,191 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017-2023. +# +# 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. +""" +Directly simulatable circuit in Aer. +""" +from typing import List, Dict, Optional +from copy import copy + +from qiskit.circuit import QuantumCircuit, Clbit +from qiskit.tools.parallel import parallel_map +from qiskit.providers.options import Options +from qiskit_aer.backends.controller_wrappers import AerCircuit_ +from .aer_operation import AerOp, BinaryFuncOp, generate_aer_operation +from ..noise.noise_model import NoiseModel +from qiskit.qobj import QobjExperimentHeader + + +class AerCircuit: + """A class of an internal ciruict of Aer + """ + def __init__( + self, + header: QobjExperimentHeader, + num_qubits: int, + num_memory: int, + global_phase: Optional[float] = None, + ): + self._header = header + self._num_qubits = num_qubits + self._num_memory = num_memory + self._global_phase = global_phase + self._is_conditional_experiment = False + self._aer_ops = [] + + @property + def num_qubits(self): + """a number of qubits""" + return self._num_qubits + + def append( + self, + aer_op: AerOp + ) -> None: + self._aer_ops.append(aer_op) + + def assemble_native( + self + ) -> AerCircuit_: + aer_circuit = AerCircuit_() + aer_circuit.num_qubits = self._num_qubits + aer_circuit.num_memory = self._num_memory + if self._global_phase is None: + aer_circuit.global_phase_angle = 0.0 + else: + aer_circuit.global_phase_angle = self._global_phase + aer_circuit.ops = [aer_op.assemble_native() for aer_op in self._aer_ops] + aer_circuit.set_header(self._header) + + return aer_circuit + + +def generate_aer_circuit( + circuit: QuantumCircuit, +) -> AerCircuit: + + num_qubits = 0 + memory_slots = 0 + max_conditional_idx = 0 + + qreg_sizes = [] + creg_sizes = [] + qubit_labels = [] + clbit_labels = [] + + 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( + getattr(inst.operation, "condition", None) for inst in circuit.data + ) + + header = QobjExperimentHeader( + qubit_labels=qubit_labels, + n_qubits=num_qubits, + qreg_sizes=qreg_sizes, + clbit_labels=clbit_labels, + memory_slots=memory_slots, + creg_sizes=creg_sizes, + name=circuit.name, + global_phase=float(circuit.global_phase), + ) + + if hasattr(circuit, "metadata"): + header.metadata = copy(circuit.metadata) + + qubit_indices = {qubit: idx for idx, qubit in enumerate(circuit.qubits)} + clbit_indices = {clbit: idx for idx, clbit in enumerate(circuit.clbits)} + + aer_circ = AerCircuit( + header, + num_qubits, + memory_slots, + circuit.global_phase) + + for inst in circuit.data: + aer_op = generate_aer_operation(inst, + qubit_indices, + clbit_indices, + is_conditional_experiment) + + # 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. + conditional_reg = -1 + if hasattr(inst.operation, "condition") and inst.operation.condition: + ctrl_reg, ctrl_val = inst.operation.condition + mask = 0 + val = 0 + if isinstance(ctrl_reg, Clbit): + mask = 1 << clbit_indices[ctrl_reg] + val = (ctrl_val & 1) << clbit_indices[ctrl_reg] + else: + for clbit in clbit_indices: + if clbit in ctrl_reg: + mask |= 1 << clbit_indices[clbit] + val |= ((ctrl_val >> list(ctrl_reg).index(clbit)) & 1) << clbit_indices[ + clbit + ] + conditional_reg_idx = memory_slots + max_conditional_idx + aer_circ.append(BinaryFuncOp(mask, "==", val, conditional_reg_idx)) + max_conditional_idx += 1 + aer_op.set_conditional(conditional_reg_idx) + + aer_circ.append(aer_op) + + return aer_circ + + +def generate_aer_circuits( + circuits: List[QuantumCircuit], + backend_options: Options, + **run_options +) -> List[AerCircuit]: + """generates a list of Qiskit circuits into circuits that Aer can directly run. + + Args: + circuits: circuit(s) to be converted + + Returns: + circuits to be run on the Aer backends + + Examples: + + .. code-block:: python + + from qiskit.circuit import QuantumCircuit + from qiskit_aer_backends import generate_aer_circuits + # Create a circuit to be simulated + qc = QuantumCircuit(2, 2) + qc.h(0) + qc.cx(0, 1) + qc.measure_all() + # Generate AerCircuit from the input circuit + aer_qc_list, config = generate_aer_circuits(circuits=[qc]) + """ + # generate aer circuits + aer_circs = parallel_map(generate_aer_circuit, circuits) + config = { + 'memory_slots': max([aer_circ._num_memory for aer_circ in aer_circs]), + 'n_qubits': max([aer_circ._num_qubits for aer_circ in aer_circs]), + **backend_options.__dict__, + **run_options + } + return aer_circs, config diff --git a/qiskit_aer/circuit/aer_operation.py b/qiskit_aer/circuit/aer_operation.py new file mode 100644 index 0000000000..aa03be7188 --- /dev/null +++ b/qiskit_aer/circuit/aer_operation.py @@ -0,0 +1,93 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017-2023. +# +# 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. +""" +Directly simulatable circuit operation in Aer. +""" +from abc import ABC +from typing import List, Dict, Optional, Tuple +import numpy as np + +from qiskit.circuit import QuantumCircuit, CircuitInstruction, Parameter, ParameterExpression +from qiskit.qobj import QasmQobjInstruction +from qiskit_aer.backends.controller_wrappers import AerOp_, create_aer_operation +from qiskit_aer.aererror import AerError + + +class AerOp: + """A class of an internal ciruict operation of Aer + """ + def __init__( + self, + qobj_inst: Optional[QasmQobjInstruction] + ): + self._conditional = False + self._conditional_reg = -1 + self._qobj_inst = qobj_inst + if hasattr(qobj_inst, 'params'): + for i in range(len(qobj_inst.params)): + if (isinstance(qobj_inst.params[i], ParameterExpression) and + len(qobj_inst.params[i].parameters) > 0): + qobj_inst.params[i] = 0.0 + + def set_conditional( + self, + conditional_reg: int + ) -> None: + self._conditional = conditional_reg >= 0 + self._conditional_reg = conditional_reg + + def assemble_native( + self + ) -> AerOp_: + op = create_aer_operation(self._qobj_inst) + op.conditional = self._conditional + if self._conditional: + op.conditional_reg = self._conditional_reg + return op + + +class BinaryFuncOp(AerOp): + """bfunc operation""" + def __init__( + self, + mask: str, + relation: str, + val: str, + conditional_reg_idx: int + ): + super().__init__( + QasmQobjInstruction(name="bfunc", + mask="0x%X" % mask, + relation="==", + val="0x%X" % val, + register=conditional_reg_idx, + ) + ) + + +def generate_aer_operation( + inst: CircuitInstruction, + qubit_indices: Dict[int, int], + clbit_indices: Dict[int, int], + register_clbits: bool +) -> AerOp: + + qobj_inst = inst.operation.assemble() + + if inst.qubits: + qobj_inst.qubits = [qubit_indices[qubit] for qubit in inst.qubits] + if inst.clbits: + qobj_inst.memory = [clbit_indices[clbit] for clbit in inst.clbits] + if inst.operation.name == "measure" and register_clbits: + qobj_inst.register = [clbit_indices[clbit] for clbit in inst.clbits] + + return AerOp(qobj_inst=qobj_inst) diff --git a/qiskit_aer/jobs/__init__.py b/qiskit_aer/jobs/__init__.py index e929458f40..5ee0a1ac4f 100644 --- a/qiskit_aer/jobs/__init__.py +++ b/qiskit_aer/jobs/__init__.py @@ -30,9 +30,11 @@ AerJob AerJobSet + AerJobDirect """ from .aerjob import AerJob from .aerjobset import AerJobSet +from .aerjobdirect import AerJobDirect from .utils import split_qobj diff --git a/qiskit_aer/jobs/aerjobdirect.py b/qiskit_aer/jobs/aerjobdirect.py new file mode 100644 index 0000000000..1d93522534 --- /dev/null +++ b/qiskit_aer/jobs/aerjobdirect.py @@ -0,0 +1,121 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2018, 2019. +# +# 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. + +# pylint: disable=arguments-differ + +"""This module implements the job class used for AerBackend objects.""" + +import logging + +from qiskit.providers import JobV1 as Job +from qiskit.providers import JobStatus, JobError +from .utils import DEFAULT_EXECUTOR, requires_submit + +LOGGER = logging.getLogger(__name__) + + +class AerJobDirect(Job): + """AerJobDirect class for Qiskit Aer Simulators.""" + + def __init__(self, backend, job_id, fn, aer_circs, noise_model, config): + """ Initializes the asynchronous job. + + Args: + backend(AerBackend): the backend used to run the job. + job_id(str): a unique id in the context of the backend used to run the job. + fn(function): a callable function to execute circuits on backend. + aer_circs (list of AerCircuit): circuits to execute + noise_model (noise_model): noise mode in simulation + config (dict): configuration of simulation + """ + super().__init__(backend, job_id) + self._fn = fn + self._aer_circs = aer_circs + self._noise_model = noise_model + self._config = config + self._executor = DEFAULT_EXECUTOR + 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("Aer job has already been submitted.") + self._future = self._executor.submit(self._fn, + self._aer_circs, + self._noise_model, + self._config, + self._job_id) + + @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): + """Attempt to cancel the job.""" + return self._future.cancel() + + @requires_submit + def status(self): + """Gets the status of the job by querying the Python's future + + Returns: + 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 executor(self): + """Return the executor for this job""" + return self._executor diff --git a/qiskit_aer/quantum_info/states/aer_state.py b/qiskit_aer/quantum_info/states/aer_state.py index 6c7f72cdb2..fc99d67fb1 100644 --- a/qiskit_aer/quantum_info/states/aer_state.py +++ b/qiskit_aer/quantum_info/states/aer_state.py @@ -14,7 +14,7 @@ """ from enum import Enum import numpy as np -from qiskit.providers.aer.backends.controller_wrappers import AerStateWrapper +from qiskit_aer.backends.controller_wrappers import AerStateWrapper from ...backends.aerbackend import AerError diff --git a/releasenotes/notes/execute_without_qobj-bb97fde493d243ff.yaml b/releasenotes/notes/execute_without_qobj-bb97fde493d243ff.yaml new file mode 100644 index 0000000000..9b410d16c3 --- /dev/null +++ b/releasenotes/notes/execute_without_qobj-bb97fde493d243ff.yaml @@ -0,0 +1,11 @@ +--- +features: + - | + `QuantumCircuit` is simulated without generating QObj and parameter-bind + is resolved without evaluation of parameter expressions. Also, a new style + of qubit representation of `QuantumCircuit` is supported, which is not + supported in QObj. +deprecations: + - | + `backend.run` does not support `QasmQobj` and `PulseQobj`, which was deprecated + in #1328 (Aug 31, 2021). diff --git a/src/controllers/controller_execute.hpp b/src/controllers/controller_execute.hpp index acb4d59d28..7cda837df3 100755 --- a/src/controllers/controller_execute.hpp +++ b/src/controllers/controller_execute.hpp @@ -17,7 +17,10 @@ #include #include "misc/hacks.hpp" + #include "framework/results/result.hpp" +#include "framework/python_parser.hpp" +#include "framework/matrix.hpp" //========================================================================= // Controller Execute interface @@ -40,6 +43,118 @@ Result controller_execute(const inputdata_t& qobj) { return controller.execute(qobj); } +template +Result controller_execute(std::vector& input_circs, AER::Noise::NoiseModel &noise_model, json_t& config) { + controller_t controller; + + bool truncate = true; + if (Parser::check_key("enable_truncation", config)) + Parser::get_value(truncate, "enable_truncation", config); + + if (noise_model.has_nonlocal_quantum_errors()) + truncate = false; + + const size_t num_circs = input_circs.size(); + + // Check if parameterized circuits + // It should be of the form + // [exp0_params, exp1_params, ...] + // where: + // expk_params = [((i, j), pars), ....] + // i is the instruction index in the experiment + // j is the param index in the instruction + // pars = [par0, par1, ...] is a list of different parameterizations + using pos_t = std::pair; + using exp_params_t = std::vector>>; + std::vector param_table; + Parser::get_value(param_table, "parameterizations", config); + + // Validate parameterizations for number of circuis + if (!param_table.empty() && param_table.size() != num_circs) { + throw std::invalid_argument( + R"(Invalid parameterized circuits: "parameterizations" length does not match number of circuits.)"); + } + + std::vector circs; + + try { + // Load circuits + for (size_t i=0; i= num_instr) { + throw std::invalid_argument(R"(Invalid parameterized qobj: instruction position out of range)"); + } + auto &op = param_circ.ops[instr_pos]; + if (param_pos >= op.params.size()) { + throw std::invalid_argument(R"(Invalid parameterized qobj: instruction param position out of range)"); + } + if (j >= params.second.size()) { + throw std::invalid_argument(R"(Invalid parameterized qobj: parameterization value out of range)"); + } + // Update the param + op.params[param_pos] = params.second[j]; + } + // Run truncation. + // TODO: Truncation should be performed and parameters should be resolved after it. + // However, parameters are associated with indices of instructions, which can be changed in truncation. + // Therefore, current implementation performs truncation for each parameter set. + if (truncate) { + param_circ.set_params(true); + param_circ.set_metadata(config, true); + } + circs.push_back(std::move(param_circ)); + } + } + } + } catch (std::exception &e) { + Result result; + + result.status = Result::Status::error; + result.message = std::string("Failed to load circuits: ") + e.what(); + return result; + } + + int_t seed = -1; + uint_t seed_shift = 0; + + bool has_simulator_seed = Parser::get_value(seed, "seed_simulator", config); + if (!has_simulator_seed) + seed = circs[0].seed; + + for (auto& circ: circs) { + circ.seed = seed + seed_shift; + seed_shift += 2113; + } + + // Fix for MacOS and OpenMP library double initialization crash. + // Issue: https://github.com/Qiskit/qiskit-aer/issues/1 + std::string path; + Parser::get_value(path, "library_dir", config); + Hacks::maybe_load_openmp(path); + controller.set_config(config); + return controller.execute(circs, noise_model, config); +} + } // end namespace AER #endif diff --git a/src/framework/circuit.hpp b/src/framework/circuit.hpp index 9fae5e774f..514325ffb4 100755 --- a/src/framework/circuit.hpp +++ b/src/framework/circuit.hpp @@ -100,6 +100,12 @@ class Circuit { // to end of circuit void set_params(bool truncation = false); + // Set metadata from a specified configuration. + // If `truncation = false`, some configurations related to qubits will be + // overriden because `set_params(false)` sets minimum qubits to simulate + // circuits. + void set_metadata(const json_t &config, bool truncation); + // Set the circuit rng seed to random value inline void set_random_seed() {seed = std::random_device()();} @@ -161,10 +167,8 @@ Circuit::Circuit(const inputdata_t &circ, const json_t &qobj_config, bool trunca } } - // Load metadata + // Set header Parser::get_value(header, "header", circ); - Parser::get_value(shots, "shots", config); - Parser::get_value(global_phase_angle, "global_phase", header); // Load instructions if (Parser::check_key("instructions", circ) == false) { @@ -181,7 +185,15 @@ Circuit::Circuit(const inputdata_t &circ, const json_t &qobj_config, bool trunca converted_ops.emplace_back(Operations::input_to_op(the_op)); } ops = std::move(converted_ops); + set_params(truncation); + set_metadata(config, truncation); +} + +void Circuit::set_metadata(const json_t &config, bool truncation) { + // Load metadata + Parser::get_value(shots, "shots", config); + Parser::get_value(global_phase_angle, "global_phase", header); // Check for specified memory slots uint_t memory_slots = 0; diff --git a/src/framework/pybind_json.hpp b/src/framework/pybind_json.hpp index de06755d73..6aa9dc89da 100755 --- a/src/framework/pybind_json.hpp +++ b/src/framework/pybind_json.hpp @@ -221,6 +221,7 @@ json_t JSON::iterable_to_json_list(const py::handle& obj){ void std::to_json(json_t &js, const py::handle &obj) { static py::object PyNoiseModel = py::module::import("qiskit_aer.noise.noise_model").attr("NoiseModel"); static py::object PyQasmQobj = py::module::import("qiskit.qobj.qasm_qobj").attr("QasmQobj"); + static py::object PyQasmQobjHeader = py::module::import("qiskit.qobj.common").attr("QobjExperimentHeader"); if (py::isinstance(obj)) { js = obj.cast(); } else if (py::isinstance(obj)) { @@ -245,6 +246,8 @@ void std::to_json(json_t &js, const py::handle &obj) { std::to_json(js, obj.attr("to_dict")()); } else if (py::isinstance(obj, PyQasmQobj)){ std::to_json(js, obj.attr("to_dict")()); + } else if (py::isinstance(obj, PyQasmQobjHeader)){ + std::to_json(js, obj.attr("to_dict")()); } else { auto type_str = std::string(py::str(obj.get_type())); if ( type_str == "" diff --git a/test/terra/backends/aer_simulator/test_conditional.py b/test/terra/backends/aer_simulator/test_conditional.py index c7ee731362..a541e58b7e 100644 --- a/test/terra/backends/aer_simulator/test_conditional.py +++ b/test/terra/backends/aer_simulator/test_conditional.py @@ -92,6 +92,8 @@ def test_conditional_gates_132bit(self, method, device): conditional_type='gate') targets = ref_conditionals.condtional_counts_nbit(132, cases, shots, hex_counts=False) + circuits = circuits[0:1] + targets = targets[0:1] result = backend.run(circuits, shots=shots).result() self.assertSuccess(result) self.compare_counts(result, circuits, targets, hex_counts=False, delta=0) diff --git a/test/terra/backends/test_parameterized_qobj.py b/test/terra/backends/test_parameterized_qobj.py index bf03ba5baa..6531736fab 100644 --- a/test/terra/backends/test_parameterized_qobj.py +++ b/test/terra/backends/test_parameterized_qobj.py @@ -67,57 +67,6 @@ def parameterized_qobj( parameterizations=params) return qobj - def test_parameterized_qobj_qasm_save_expval(self): - """Test parameterized qobj with Expectation Value snapshot and qasm simulator.""" - shots = 1000 - labels = save_expval_labels() * 3 - counts_targets = save_expval_counts(shots) * 3 - value_targets = save_expval_pre_meas_values() * 3 - - backend = AerSimulator() - qobj = self.parameterized_qobj(backend=backend, - shots=1000, - measure=True, - snapshot=True) - self.assertIn('parameterizations', qobj.to_dict()['config']) - with self.assertWarns(DeprecationWarning): - job = backend.run(qobj, **self.BACKEND_OPTS) - result = job.result() - success = getattr(result, 'success', False) - num_circs = len(result.to_dict()['results']) - self.assertTrue(success) - self.compare_counts(result, - range(num_circs), - counts_targets, - delta=0.1 * shots) - # Check snapshots - for j, target in enumerate(value_targets): - data = result.data(j) - for label in labels: - self.assertAlmostEqual( - data[label], target[label], delta=1e-7) - - def test_parameterized_qobj_statevector(self): - """Test parameterized qobj with Expectation Value snapshot and qasm simulator.""" - statevec_targets = save_expval_final_statevecs() * 3 - - backend = AerSimulator(method="statevector") - qobj = self.parameterized_qobj( - backend=backend, measure=False, snapshot=False, save_state=True, - ) - self.assertIn('parameterizations', qobj.to_dict()['config']) - with self.assertWarns(DeprecationWarning): - job = backend.run(qobj, **self.BACKEND_OPTS) - result = job.result() - success = getattr(result, 'success', False) - num_circs = len(result.to_dict()['results']) - self.assertTrue(success) - - for j in range(num_circs): - statevector = result.get_statevector(j) - np.testing.assert_array_almost_equal( - statevector, statevec_targets[j].data, decimal=7) - def test_run_path(self): """Test parameterized circuit path via backed.run()""" shots = 1000