diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..eaf346c --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,53 @@ +repos: +- repo: https://github.com/Lucas-C/pre-commit-hooks.git + rev: v1.5.4 + hooks: + - id: remove-crlf + files: (?!.*third_party)^.*$ | (?!.*book)^.*$ +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: check-added-large-files + args: + - --maxkb=20480 + - id: check-merge-conflict + - id: check-symlinks + - id: detect-private-key + files: (?!.*third_party)^.*$ | (?!.*book)^.*$ + - id: end-of-file-fixer + - id: trailing-whitespace + - id: requirements-txt-fixer + - id: sort-simple-yaml +- repo: https://github.com/pylint-dev/pylint + rev: v3.0.0a6 + hooks: + - id: pylint + args: + - --disable=all + - --load-plugins=docstring_checker + - --enable=doc-string-one-line,doc-string-end-with,doc-string-with-all-args,doc-string-triple-quotes,doc-string-missing,doc-string-indent-error,doc-string-with-returns,doc-string-with-raises +- repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks.git + rev: v2.10.0 + hooks: + - id: pretty-format-yaml + args: [--autofix, --indent, '4'] +- repo: https://github.com/hadialqattan/pycln + rev: v2.2.2 + hooks: + - id: pycln +- repo: https://github.com/pre-commit/mirrors-clang-format + rev: v15.0.7 + hooks: + - id: clang-format + args: [-style=file] + # Using this mirror lets us use mypyc-compiled black, which is about 2x faster +- repo: https://github.com/psf/black-pre-commit-mirror + rev: 23.12.0 + hooks: + - id: black +- repo: https://github.com/pycqa/isort + rev: 5.12.0 + hooks: + - id: isort + name: isort (python) + args: [--profile, black, --filter-files] diff --git a/qdao/base_circuit_wrapper.py b/qdao/base_circuit_wrapper.py index 36bd5bd..d714dfc 100644 --- a/qdao/base_circuit_wrapper.py +++ b/qdao/base_circuit_wrapper.py @@ -1,4 +1,6 @@ -from abc import ABC, abstractmethod, abstractproperty +from abc import ABC, abstractmethod +from typing import Any + import numpy as np @@ -7,22 +9,22 @@ class BaseCircWrapper(ABC): @property @abstractmethod - def num_qubits(self): + def num_qubits(self) -> Any: """Number of qubits of the quantum circuit""" @property @abstractmethod - def instructions(self): + def instructions(self) -> Any: """List of instructions in the quantum circuit""" @abstractmethod - def get_instr_qubits(self, instruction): + def get_instr_qubits(self, instruction) -> Any: """The qubit indices that current instruction act on""" @abstractmethod - def init_circ_from_sv(self, sv: np.ndarray): + def init_circ_from_sv(self, sv: np.ndarray) -> Any: """Set initial state vector of the quantum circuit""" @abstractmethod - def gen_sub_circ(self, instrs, num_local, num_primary): + def gen_sub_circ(self, instrs, num_local, num_primary) -> Any: """Generate a new subcircuit based on given list of instructions""" diff --git a/qdao/qiskit/annotated_operation.py b/qdao/qiskit/annotated_operation.py new file mode 100644 index 0000000..803baa6 --- /dev/null +++ b/qdao/qiskit/annotated_operation.py @@ -0,0 +1,240 @@ +# 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. + +"""Annotated Operations.""" + +from __future__ import annotations + +import dataclasses +from typing import List, Union + +from qiskit.circuit._utils import _compute_control_matrix, _ctrl_state_to_int +from qiskit.circuit.exceptions import CircuitError +from qiskit.circuit.operation import Operation + + +class Modifier: + """The base class that all modifiers of :class:`~.AnnotatedOperation` should + inherit from.""" + + pass + + +@dataclasses.dataclass +class InverseModifier(Modifier): + """Inverse modifier: specifies that the operation is inverted.""" + + pass + + +@dataclasses.dataclass +class ControlModifier(Modifier): + """Control modifier: specifies that the operation is controlled by ``num_ctrl_qubits`` + and has control state ``ctrl_state``.""" + + num_ctrl_qubits: int = 0 + ctrl_state: Union[int, str, None] = None + + def __init__( + self, num_ctrl_qubits: int = 0, ctrl_state: Union[int, str, None] = None + ): + self.num_ctrl_qubits = num_ctrl_qubits + self.ctrl_state = _ctrl_state_to_int(ctrl_state, num_ctrl_qubits) + + +@dataclasses.dataclass +class PowerModifier(Modifier): + """Power modifier: specifies that the operation is raised to the power ``power``.""" + + power: float + + +class AnnotatedOperation(Operation): + """Annotated operation.""" + + def __init__(self, base_op: Operation, modifiers: Union[Modifier, List[Modifier]]): + """ + Create a new AnnotatedOperation. + + An "annotated operation" allows to add a list of modifiers to the + "base" operation. For now, the only supported modifiers are of + types :class:`~.InverseModifier`, :class:`~.ControlModifier` and + :class:`~.PowerModifier`. + + An annotated operation can be viewed as an extension of + :class:`~.ControlledGate` (which also allows adding control to the + base operation). However, an important difference is that the + circuit definition of an annotated operation is not constructed when + the operation is declared, and instead happens during transpilation, + specifically during the :class:`~.HighLevelSynthesis` transpiler pass. + + An annotated operation can be also viewed as a "higher-level" + or "more abstract" object that can be added to a quantum circuit. + This enables writing transpiler optimization passes that make use of + this higher-level representation, for instance removing a gate + that is immediately followed by its inverse. + + Args: + base_op: base operation being modified + modifiers: ordered list of modifiers. Supported modifiers include + ``InverseModifier``, ``ControlModifier`` and ``PowerModifier``. + + Examples:: + + op1 = AnnotatedOperation(SGate(), [InverseModifier(), ControlModifier(2)]) + + op2_inner = AnnotatedGate(SGate(), InverseModifier()) + op2 = AnnotatedGate(op2_inner, ControlModifier(2)) + + Both op1 and op2 are semantically equivalent to an ``SGate()`` which is first + inverted and then controlled by 2 qubits. + """ + self.base_op = base_op + self.modifiers = modifiers if isinstance(modifiers, List) else [modifiers] + + @property + def name(self): + """Unique string identifier for operation type.""" + return "annotated" + + @property + def num_qubits(self): + """Number of qubits.""" + num_ctrl_qubits = 0 + for modifier in self.modifiers: + if isinstance(modifier, ControlModifier): + num_ctrl_qubits += modifier.num_ctrl_qubits + + return num_ctrl_qubits + self.base_op.num_qubits + + @property + def num_clbits(self): + """Number of classical bits.""" + return self.base_op.num_clbits + + def __eq__(self, other) -> bool: + """Checks if two AnnotatedOperations are equal.""" + return ( + isinstance(other, AnnotatedOperation) + and self.modifiers == other.modifiers + and self.base_op == other.base_op + ) + + def copy(self) -> "AnnotatedOperation": + """Return a copy of the :class:`~.AnnotatedOperation`.""" + return AnnotatedOperation(base_op=self.base_op, modifiers=self.modifiers.copy()) + + def to_matrix(self): + """Return a matrix representation (allowing to construct Operator).""" + from qiskit.quantum_info.operators import ( # pylint: disable=cyclic-import + Operator, + ) + + operator = Operator(self.base_op) + + for modifier in self.modifiers: + if isinstance(modifier, InverseModifier): + operator = operator.power(-1) + elif isinstance(modifier, ControlModifier): + operator = Operator( + _compute_control_matrix( + operator.data, modifier.num_ctrl_qubits, modifier.ctrl_state + ) + ) + elif isinstance(modifier, PowerModifier): + operator = operator.power(modifier.power) + else: + raise CircuitError(f"Unknown modifier {modifier}.") + return operator + + def control( + self, + num_ctrl_qubits: int = 1, + label: str | None = None, + ctrl_state: int | str | None = None, + annotated: bool = True, + ) -> AnnotatedOperation: + """ + Return the controlled version of itself. + + Implemented as an annotated operation, see :class:`.AnnotatedOperation`. + + Args: + num_ctrl_qubits: number of controls to add to gate (default: ``1``) + label: ignored (used for consistency with other control methods) + ctrl_state: The control state in decimal or as a bitstring + (e.g. ``'111'``). If ``None``, use ``2**num_ctrl_qubits-1``. + annotated: ignored (used for consistency with other control methods) + + Returns: + Controlled version of the given operation. + """ + # pylint: disable=unused-argument + extended_modifiers = self.modifiers.copy() + extended_modifiers.append( + ControlModifier(num_ctrl_qubits=num_ctrl_qubits, ctrl_state=ctrl_state) + ) + return AnnotatedOperation(self.base_op, extended_modifiers) + + def inverse(self, annotated: bool = True): + """ + Return the inverse version of itself. + + Implemented as an annotated operation, see :class:`.AnnotatedOperation`. + + Args: + annotated: ignored (used for consistency with other inverse methods) + + Returns: + Inverse version of the given operation. + """ + # pylint: disable=unused-argument + extended_modifiers = self.modifiers.copy() + extended_modifiers.append(InverseModifier()) + return AnnotatedOperation(self.base_op, extended_modifiers) + + +def _canonicalize_modifiers(modifiers): + """ + Returns the canonical representative of the modifier list. This is possible + since all the modifiers commute; also note that InverseModifier is a special + case of PowerModifier. The current solution is to compute the total number + of control qubits / control state and the total power. The InverseModifier + will be present if total power is negative, whereas the power modifier will + be present only with positive powers different from 1. + """ + power = 1 + num_ctrl_qubits = 0 + ctrl_state = 0 + + for modifier in modifiers: + if isinstance(modifier, InverseModifier): + power *= -1 + elif isinstance(modifier, ControlModifier): + num_ctrl_qubits += modifier.num_ctrl_qubits + ctrl_state = (ctrl_state << modifier.num_ctrl_qubits) | modifier.ctrl_state + elif isinstance(modifier, PowerModifier): + power *= modifier.power + else: + raise CircuitError(f"Unknown modifier {modifier}.") + + canonical_modifiers = [] + if power < 0: + canonical_modifiers.append(InverseModifier()) + power *= -1 + + if power != 1: + canonical_modifiers.append(PowerModifier(power)) + if num_ctrl_qubits > 0: + canonical_modifiers.append(ControlModifier(num_ctrl_qubits, ctrl_state)) + + return canonical_modifiers diff --git a/qdao/qiskit/circ_append.py b/qdao/qiskit/circ_append.py deleted file mode 100644 index adad723..0000000 --- a/qdao/qiskit/circ_append.py +++ /dev/null @@ -1,215 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017. -# -# 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=bad-docstring-quotes,invalid-name - -"""Quantum circuit object.""" - -import copy -import functools -import itertools -import multiprocessing as mp -import re -import string -import typing -from collections import OrderedDict, defaultdict, namedtuple -from typing import ( - Callable, - Dict, - Iterable, - List, - Mapping, - Optional, - Sequence, - Set, - Tuple, - Type, - TypeVar, - Union, -) - -from qiskit.circuit.classicalregister import ClassicalRegister, Clbit -from qiskit.circuit.exceptions import CircuitError -from qiskit.circuit.instruction import Instruction -from qiskit.circuit.instructionset import InstructionSet -from qiskit.circuit.operation import Operation -from qiskit.circuit.parameter import Parameter -from qiskit.circuit.quantumcircuitdata import CircuitInstruction -from qiskit.circuit.quantumregister import QuantumRegister, Qubit - -# The following types are not marked private to avoid leaking this "private/public" abstraction out -# into the documentation. They are not imported by circuit.__init__, nor are they meant to be. - -# Arbitrary type variables for marking up generics. -S = TypeVar("S") -T = TypeVar("T") - -# Types that can be coerced to a valid Qubit specifier in a circuit. -QubitSpecifier = Union[ - Qubit, - QuantumRegister, - int, - slice, - Sequence[Union[Qubit, int]], -] - -# Types that can be coerced to a valid Clbit specifier in a circuit. -ClbitSpecifier = Union[ - Clbit, - ClassicalRegister, - int, - slice, - Sequence[Union[Clbit, int]], -] - -# Generic type which is either :obj:`~Qubit` or :obj:`~Clbit`, used to specify types of functions -# which operate on either type of bit, but not both at the same time. -BitType = TypeVar("BitType", Qubit, Clbit) - -# Regex pattern to match valid OpenQASM identifiers -VALID_QASM2_IDENTIFIER = re.compile("[a-z][a-zA-Z_0-9]*") - - -def _append_init_sv(self, instruction, qargs=None, cargs=None): - """Append an instruction to the end of the circuit, modifying the circuit in place. - - .. warning:: - - This is an internal fast-path function, and it is the responsibility of the caller to - ensure that all the arguments are valid; there is no error checking here. In - particular, all the qubits and clbits must already exist in the circuit and there can be - no duplicates in the list. - - .. note:: - - This function may be used by callers other than :obj:`.QuantumCircuit` when the caller - is sure that all error-checking, broadcasting and scoping has already been performed, - and the only reference to the circuit the instructions are being appended to is within - that same function. In particular, it is not safe to call - :meth:`QuantumCircuit._append` on a circuit that is received by a function argument. - This is because :meth:`.QuantumCircuit._append` will not recognise the scoping - constructs of the control-flow builder interface. - - Args: - instruction: Operation instance to append - qargs: Qubits to attach the instruction to. - cargs: Clbits to attach the instruction to. - - Returns: - Operation: a handle to the instruction that was just added - - :meta public: - """ - old_style = not isinstance(instruction, CircuitInstruction) - if old_style: - instruction = CircuitInstruction(instruction, qargs, cargs) - self._data.append(instruction) - - # FIXME: No need to do so when appending initializing - # statevector instruction - # if isinstance(instruction.operation, Instruction): - # self._update_parameter_table(instruction) - - # mark as normal circuit if a new instruction is added - self.duration = None - self.unit = "dt" - return instruction.operation if old_style else instruction - - -def append_init_sv( - self, - instruction: Union[Operation, CircuitInstruction], - qargs: Optional[Sequence[QubitSpecifier]] = None, - cargs: Optional[Sequence[ClbitSpecifier]] = None, -) -> InstructionSet: - """Append one or more instructions to the end of the circuit, modifying the circuit in - place. - - The ``qargs`` and ``cargs`` will be expanded and broadcast according to the rules of the - given :class:`~.circuit.Instruction`, and any non-:class:`.Bit` specifiers (such as - integer indices) will be resolved into the relevant instances. - - If a :class:`.CircuitInstruction` is given, it will be unwrapped, verified in the context of - this circuit, and a new object will be appended to the circuit. In this case, you may not - pass ``qargs`` or ``cargs`` separately. - - Args: - instruction: :class:`~.circuit.Instruction` instance to append, or a - :class:`.CircuitInstruction` with all its context. - qargs: specifiers of the :class:`.Qubit`\\ s to attach instruction to. - cargs: specifiers of the :class:`.Clbit`\\ s to attach instruction to. - - Returns: - qiskit.circuit.InstructionSet: a handle to the :class:`.CircuitInstruction`\\ s that - were actually added to the circuit. - - Raises: - CircuitError: if the operation passed is not an instance of - :class:`~.circuit..Instruction`. - """ - if isinstance(instruction, CircuitInstruction): - operation = instruction.operation - qargs = instruction.qubits - cargs = instruction.clbits - else: - operation = instruction - # Convert input to instruction - if not isinstance(operation, Operation) and not hasattr( - operation, "to_instruction" - ): - if issubclass(operation, Operation): - raise CircuitError( - "Object is a subclass of Operation, please add () to " - "pass an instance of this object." - ) - - raise CircuitError( - "Object to append must be an Operation or have a to_instruction() method." - ) - if not isinstance(operation, Operation) and hasattr(operation, "to_instruction"): - operation = operation.to_instruction() - if not isinstance(operation, Operation): - raise CircuitError("object is not an Operation.") - - # FIXME: No use when initializing statevector - ## Make copy of parameterized gate instances - # if hasattr(operation, "params"): - # is_parameter = any(isinstance(param, Parameter) for param in operation.params) - # if is_parameter: - # operation = copy.deepcopy(operation) - - expanded_qargs = [self.qbit_argument_conversion(qarg) for qarg in qargs or []] - expanded_cargs = [self.cbit_argument_conversion(carg) for carg in cargs or []] - - if self._control_flow_scopes: - appender = self._control_flow_scopes[-1].append - requester = self._control_flow_scopes[-1].request_classical_resource - else: - appender = self._append_init_sv - requester = self._resolve_classical_resource - instructions = InstructionSet(resource_requester=requester) - if isinstance(operation, Instruction): - for qarg, carg in operation.broadcast_arguments(expanded_qargs, expanded_cargs): - self._check_dups(qarg) - instruction = CircuitInstruction(operation, qarg, carg) - appender(instruction) - instructions.add(instruction) - else: - # For Operations that are non-Instructions, we use the Instruction's default method - for qarg, carg in Instruction.broadcast_arguments( - operation, expanded_qargs, expanded_cargs - ): - self._check_dups(qarg) - instruction = CircuitInstruction(operation, qarg, carg) - appender(instruction) - instructions.add(instruction) - return instructions diff --git a/qdao/qiskit/circuit.py b/qdao/qiskit/circuit.py index 82ce7b3..a6253c6 100644 --- a/qdao/qiskit/circuit.py +++ b/qdao/qiskit/circuit.py @@ -5,9 +5,9 @@ from qdao.base_circuit_wrapper import BaseCircWrapper -from .initializer import initialize - -QuantumCircuit.initialize = initialize +# from .initializer import initialize +# +# QuantumCircuit.initialize = initialize class QiskitCircuitWrapper(BaseCircWrapper): @@ -49,13 +49,15 @@ def init_circ_from_sv(self, sv: np.ndarray): """ from qdao.simulator import QdaoSimObj + from .data_preparation.initializer import Initialize + if not isinstance(self._circ, QuantumCircuit): raise ValueError("Please set circ before initializing from sv!") nq = self._circ.num_qubits circ = QuantumCircuit(nq) # FIXME: To test the performance of qiskit, do not initialize - circ.initialize(sv, range(nq)) + circ.append(Initialize(sv), range(nq)) circ.compose(self._circ, inplace=True) return QdaoSimObj(sv, circ) diff --git a/qdao/qiskit/data_preparation/__init__.py b/qdao/qiskit/data_preparation/__init__.py new file mode 100644 index 0000000..290288c --- /dev/null +++ b/qdao/qiskit/data_preparation/__init__.py @@ -0,0 +1,49 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 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. + +""" +Data-encoding circuits +====================== + +In machine learning, pattern recognition and image processing, a **data-encoding circuit** +starts from an initial set of measured data and builds derived values (also known as +**features**) intended to be informative and non-redundant, facilitating the subsequent +learning and generalization steps, and in some cases leading to better human +interpretations. + +A feature map is related to **dimensionality reduction**; it involves reducing the amount of +resources required to describe a large set of data. When performing analysis of complex data, +one of the major problems stems from the number of variables involved. Analysis with a large +number of variables generally requires a large amount of memory and computation power, and may +even cause a classification algorithm to overfit to training samples and generalize poorly to new +samples. + +When the input data to an algorithm is too large to be processed and is suspected to be redundant +(for example, the same measurement is provided in both pounds and kilograms), then it can be +transformed into a reduced set of features, named a **feature vector**. + +The process of determining a subset of the initial features is called **feature selection**. +The selected features are expected to contain the relevant information from the input data, +so that the desired task can be performed by using the reduced representation instead +of the complete initial data. + +""" + +from .initializer import Initialize + +# FIXME(): for QDAO, we only need StatePreparation and Initialize +# from .pauli_feature_map import PauliFeatureMap +# from .z_feature_map import ZFeatureMap +# from .zz_feature_map import ZZFeatureMap +from .state_preparation import StatePreparation + +# __all__ = ["PauliFeatureMap", "ZFeatureMap", "ZZFeatureMap", "StatePreparation", "Initialize"] diff --git a/qdao/qiskit/data_preparation/initializer.py b/qdao/qiskit/data_preparation/initializer.py new file mode 100644 index 0000000..ccd1548 --- /dev/null +++ b/qdao/qiskit/data_preparation/initializer.py @@ -0,0 +1,100 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017. +# +# 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. + +""" +Initialize qubit registers to desired arbitrary state. +""" + +from __future__ import annotations + +import typing +from collections.abc import Sequence + +from qiskit.circuit.instruction import Instruction +from qiskit.circuit.quantumcircuit import QuantumCircuit +from qiskit.circuit.quantumregister import QuantumRegister + +from .state_preparation import StatePreparation + +if typing.TYPE_CHECKING: + from qiskit.quantum_info.states.statevector import Statevector + +_EPS = 1e-10 # global variable used to chop very small numbers to zero + + +class Initialize(Instruction): + """Complex amplitude initialization. + + Class that initializes some flexible collection of qubit registers, implemented by calling + the :class:`~.library.StatePreparation` class. + Note that ``Initialize`` is an :class:`~.circuit.Instruction` and not a :class:`.Gate` since it + contains a reset instruction, which is not unitary. + """ + + def __init__( + self, + params: Statevector | Sequence[complex] | str | int, + num_qubits: int | None = None, + normalize: bool = False, + ) -> None: + r""" + Args: + params: The state to initialize to, can be either of the following. + + * Statevector or vector of complex amplitudes to initialize to. + * Labels of basis states of the Pauli eigenstates Z, X, Y. See + :meth:`.Statevector.from_label`. Notice the order of the labels is reversed with + respect to the qubit index to be applied to. Example label '01' initializes the + qubit zero to :math:`|1\rangle` and the qubit one to :math:`|0\rangle`. + * An integer that is used as a bitmap indicating which qubits to initialize to + :math:`|1\rangle`. Example: setting params to 5 would initialize qubit 0 and qubit + 2 to :math:`|1\rangle` and qubit 1 to :math:`|0\rangle`. + + num_qubits: This parameter is only used if params is an int. Indicates the total + number of qubits in the `initialize` call. Example: `initialize` covers 5 qubits + and params is 3. This allows qubits 0 and 1 to be initialized to :math:`|1\rangle` + and the remaining 3 qubits to be initialized to :math:`|0\rangle`. + normalize: Whether to normalize an input array to a unit vector. + """ + self._stateprep = StatePreparation(params, num_qubits, normalize=normalize) + + super().__init__( + "initialize", self._stateprep.num_qubits, 0, self._stateprep.params + ) + + def _define(self): + q = QuantumRegister(self.num_qubits, "q") + initialize_circuit = QuantumCircuit(q, name="init_def") + initialize_circuit.reset(q) + initialize_circuit.append(self._stateprep, q) + self.definition = initialize_circuit + + def gates_to_uncompute(self) -> QuantumCircuit: + """Call to create a circuit with gates that take the desired vector to zero. + + Returns: + Circuit to take ``self.params`` vector to :math:`|{00\\ldots0}\\rangle` + """ + return self._stateprep._gates_to_uncompute() + + @property + def params(self): + """Return initialize params.""" + return self._stateprep.params + + @params.setter + def params(self, parameters: Statevector | Sequence[complex] | str | int) -> None: + """Set initialize params.""" + self._stateprep.params = parameters + + def broadcast_arguments(self, qargs, cargs): + return self._stateprep.broadcast_arguments(qargs, cargs) diff --git a/qdao/qiskit/data_preparation/pauli_feature_map.py b/qdao/qiskit/data_preparation/pauli_feature_map.py new file mode 100644 index 0000000..2b9c947 --- /dev/null +++ b/qdao/qiskit/data_preparation/pauli_feature_map.py @@ -0,0 +1,295 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 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. + +"""The Pauli expansion circuit module.""" + +from functools import reduce +from typing import Callable, List, Optional, Union + +import numpy as np +from qiskit.circuit import Parameter, ParameterVector, QuantumCircuit +from qiskit.circuit.library.standard_gates import HGate + +from ..n_local.n_local import NLocal + + +class PauliFeatureMap(NLocal): + r"""The Pauli Expansion circuit. + + The Pauli Expansion circuit is a data encoding circuit that transforms input data + :math:`\vec{x} \in \mathbb{R}^n`, where `n` is the ``feature_dimension``, as + + .. math:: + + U_{\Phi(\vec{x})}=\exp\left(i\sum_{S \in \mathcal{I}} + \phi_S(\vec{x})\prod_{i\in S} P_i\right). + + Here, :math:`S` is a set of qubit indices that describes the connections in the feature map, + :math:`\mathcal{I}` is a set containing all these index sets, and + :math:`P_i \in \{I, X, Y, Z\}`. Per default the data-mapping + :math:`\phi_S` is + + .. math:: + + \phi_S(\vec{x}) = \begin{cases} + x_i \text{ if } S = \{i\} \\ + \prod_{j \in S} (\pi - x_j) \text{ if } |S| > 1 + \end{cases}. + + The possible connections can be set using the ``entanglement`` and ``paulis`` arguments. + For example, for single-qubit :math:`Z` rotations and two-qubit :math:`YY` interactions + between all qubit pairs, we can set:: + + + feature_map = PauliFeatureMap(..., paulis=["Z", "YY"], entanglement="full") + + which will produce blocks of the form + + .. parsed-literal:: + + ┌───┐┌──────────────┐┌──────────┐ ┌───────────┐ + ┤ H ├┤ U1(2.0*x[0]) ├┤ RX(pi/2) ├──■───────────────────────────────────────■──┤ RX(-pi/2) ├ + ├───┤├──────────────┤├──────────┤┌─┴─┐┌─────────────────────────────────┐┌─┴─┐├───────────┤ + ┤ H ├┤ U1(2.0*x[1]) ├┤ RX(pi/2) ├┤ X ├┤ U1(2.0*(pi - x[0])*(pi - x[1])) ├┤ X ├┤ RX(-pi/2) ├ + └───┘└──────────────┘└──────────┘└───┘└─────────────────────────────────┘└───┘└───────────┘ + + The circuit contains ``reps`` repetitions of this transformation. + + Please refer to :class:`.ZFeatureMap` for the case of single-qubit Pauli-:math:`Z` rotations + and to :class:`.ZZFeatureMap` for the single- and two-qubit Pauli-:math:`Z` rotations. + + Examples: + + >>> prep = PauliFeatureMap(2, reps=1, paulis=['ZZ']) + >>> print(prep) + ┌───┐ + q_0: ┤ H ├──■───────────────────────────────────────■── + ├───┤┌─┴─┐┌─────────────────────────────────┐┌─┴─┐ + q_1: ┤ H ├┤ X ├┤ U1(2.0*(pi - x[0])*(pi - x[1])) ├┤ X ├ + └───┘└───┘└─────────────────────────────────┘└───┘ + + >>> prep = PauliFeatureMap(2, reps=1, paulis=['Z', 'XX']) + >>> print(prep) + ┌───┐┌──────────────┐┌───┐ ┌───┐ + q_0: ┤ H ├┤ U1(2.0*x[0]) ├┤ H ├──■───────────────────────────────────────■──┤ H ├ + ├───┤├──────────────┤├───┤┌─┴─┐┌─────────────────────────────────┐┌─┴─┐├───┤ + q_1: ┤ H ├┤ U1(2.0*x[1]) ├┤ H ├┤ X ├┤ U1(2.0*(pi - x[0])*(pi - x[1])) ├┤ X ├┤ H ├ + └───┘└──────────────┘└───┘└───┘└─────────────────────────────────┘└───┘└───┘ + + >>> prep = PauliFeatureMap(2, reps=1, paulis=['ZY']) + >>> print(prep) + ┌───┐┌──────────┐ ┌───────────┐ + q_0: ┤ H ├┤ RX(pi/2) ├──■───────────────────────────────────────■──┤ RX(-pi/2) ├ + ├───┤└──────────┘┌─┴─┐┌─────────────────────────────────┐┌─┴─┐└───────────┘ + q_1: ┤ H ├────────────┤ X ├┤ U1(2.0*(pi - x[0])*(pi - x[1])) ├┤ X ├───────────── + └───┘ └───┘└─────────────────────────────────┘└───┘ + + >>> from qiskit.circuit.library import EfficientSU2 + >>> prep = PauliFeatureMap(3, reps=3, paulis=['Z', 'YY', 'ZXZ']) + >>> wavefunction = EfficientSU2(3) + >>> classifier = prep.compose(wavefunction + >>> classifier.num_parameters + 27 + >>> classifier.count_ops() + OrderedDict([('cx', 39), ('rx', 36), ('u1', 21), ('h', 15), ('ry', 12), ('rz', 12)]) + + References: + + + + [1] Havlicek et al. Supervised learning with quantum enhanced feature spaces, + `Nature 567, 209-212 (2019) `__. + + """ + + def __init__( + self, + feature_dimension: Optional[int] = None, + reps: int = 2, + entanglement: Union[str, List[List[int]], Callable[[int], List[int]]] = "full", + alpha: float = 2.0, + paulis: Optional[List[str]] = None, + data_map_func: Optional[Callable[[np.ndarray], float]] = None, + parameter_prefix: str = "x", + insert_barriers: bool = False, + name: str = "PauliFeatureMap", + ) -> None: + """Create a new Pauli expansion circuit. + + Args: + feature_dimension: Number of qubits in the circuit. + reps: The number of repeated circuits. + entanglement: Specifies the entanglement structure. Refer to + :class:`~qiskit.circuit.library.NLocal` for detail. + alpha: The Pauli rotation factor, multiplicative to the pauli rotations + paulis: A list of strings for to-be-used paulis. If None are provided, ``['Z', 'ZZ']`` + will be used. + data_map_func: A mapping function for data x which can be supplied to override the + default mapping from :meth:`self_product`. + parameter_prefix: The prefix used if default parameters are generated. + insert_barriers: If True, barriers are inserted in between the evolution instructions + and hadamard layers. + + """ + + super().__init__( + num_qubits=feature_dimension, + reps=reps, + rotation_blocks=HGate(), + entanglement=entanglement, + parameter_prefix=parameter_prefix, + insert_barriers=insert_barriers, + skip_final_rotation_layer=True, + name=name, + ) + + self._data_map_func = data_map_func or self_product + self._paulis = paulis or ["Z", "ZZ"] + self._alpha = alpha + + def _parameter_generator( + self, rep: int, block: int, indices: List[int] + ) -> Optional[List[Parameter]]: + """If certain blocks should use certain parameters this method can be overridden.""" + params = [self.ordered_parameters[i] for i in indices] + return params + + @property + def num_parameters_settable(self): + """The number of distinct parameters.""" + return self.feature_dimension + + @property + def paulis(self) -> List[str]: + """The Pauli strings used in the entanglement of the qubits. + + Returns: + The Pauli strings as list. + """ + return self._paulis + + @paulis.setter + def paulis(self, paulis: List[str]) -> None: + """Set the pauli strings. + + Args: + paulis: The new pauli strings. + """ + self._invalidate() + self._paulis = paulis + + @property + def alpha(self) -> float: + """The Pauli rotation factor (alpha). + + Returns: + The Pauli rotation factor. + """ + return self._alpha + + @alpha.setter + def alpha(self, alpha: float) -> None: + """Set the Pauli rotation factor (alpha). + + Args: + alpha: Pauli rotation factor + """ + self._invalidate() + self._alpha = alpha + + @property + def entanglement_blocks(self): + return [self.pauli_block(pauli) for pauli in self._paulis] + + @entanglement_blocks.setter + def entanglement_blocks(self, entanglement_blocks): + self._entanglement_blocks = entanglement_blocks + + @property + def feature_dimension(self) -> int: + """Returns the feature dimension (which is equal to the number of qubits). + + Returns: + The feature dimension of this feature map. + """ + return self.num_qubits + + @feature_dimension.setter + def feature_dimension(self, feature_dimension: int) -> None: + """Set the feature dimension. + + Args: + feature_dimension: The new feature dimension. + """ + self.num_qubits = feature_dimension + + def _extract_data_for_rotation(self, pauli, x): + where_non_i = np.where(np.asarray(list(pauli[::-1])) != "I")[0] + x = np.asarray(x) + return x[where_non_i] + + def pauli_block(self, pauli_string): + """Get the Pauli block for the feature map circuit.""" + params = ParameterVector("_", length=len(pauli_string)) + time = self._data_map_func(np.asarray(params)) + return self.pauli_evolution(pauli_string, time) + + def pauli_evolution(self, pauli_string, time): + """Get the evolution block for the given pauli string.""" + # for some reason this is in reversed order + pauli_string = pauli_string[::-1] + + # trim the pauli string if identities are included + trimmed = [] + indices = [] + for i, pauli in enumerate(pauli_string): + if pauli != "I": + trimmed += [pauli] + indices += [i] + + evo = QuantumCircuit(len(pauli_string)) + + if len(trimmed) == 0: + return evo + + def basis_change(circuit, inverse=False): + for i, pauli in enumerate(pauli_string): + if pauli == "X": + circuit.h(i) + elif pauli == "Y": + circuit.rx(-np.pi / 2 if inverse else np.pi / 2, i) + + def cx_chain(circuit, inverse=False): + num_cx = len(indices) - 1 + for i in reversed(range(num_cx)) if inverse else range(num_cx): + circuit.cx(indices[i], indices[i + 1]) + + basis_change(evo) + cx_chain(evo) + evo.p(self.alpha * time, indices[-1]) + cx_chain(evo, inverse=True) + basis_change(evo, inverse=True) + return evo + + +def self_product(x: np.ndarray) -> float: + """ + Define a function map from R^n to R. + + Args: + x: data + + Returns: + float: the mapped value + """ + coeff = x[0] if len(x) == 1 else reduce(lambda m, n: m * n, np.pi - x) + return coeff diff --git a/qdao/qiskit/state_preparation.py b/qdao/qiskit/data_preparation/state_preparation.py similarity index 77% rename from qdao/qiskit/state_preparation.py rename to qdao/qiskit/data_preparation/state_preparation.py index f04477e..8d0c0f3 100644 --- a/qdao/qiskit/state_preparation.py +++ b/qdao/qiskit/data_preparation/state_preparation.py @@ -15,17 +15,19 @@ from typing import Optional, Union import numpy as np -from qiskit.circuit import QuantumCircuit, QuantumRegister, Qubit from qiskit.circuit.exceptions import CircuitError +from qiskit.circuit.gate import Gate from qiskit.circuit.library.standard_gates.h import HGate from qiskit.circuit.library.standard_gates.ry import RYGate from qiskit.circuit.library.standard_gates.rz import RZGate from qiskit.circuit.library.standard_gates.s import SdgGate, SGate from qiskit.circuit.library.standard_gates.x import CXGate, XGate +from qiskit.circuit.quantumcircuit import QuantumCircuit +from qiskit.circuit.quantumregister import QuantumRegister from qiskit.exceptions import QiskitError -from qiskit.quantum_info import Statevector - -from qdao.qiskit.gate import Gate +from qiskit.quantum_info.states.statevector import ( # pylint: disable=cyclic-import + Statevector, +) _EPS = 1e-10 # global variable used to chop very small numbers to zero @@ -43,6 +45,7 @@ def __init__( num_qubits: Optional[int] = None, inverse: bool = False, label: Optional[str] = None, + normalize: bool = False, ): r""" Args: @@ -63,6 +66,7 @@ def __init__( and the remaining 3 qubits to be initialized to :math:`|0\rangle`. inverse: if True, the inverse state is constructed. label: An optional label for the gate + normalize (bool): Whether to normalize an input array to a unit vector. Raises: QiskitError: ``num_qubits`` parameter used when ``params`` is not an integer @@ -98,8 +102,16 @@ def __init__( self._from_label = isinstance(params, str) self._from_int = isinstance(params, int) - num_qubits = self._get_num_qubits(num_qubits, params) + # FIXME(): QDAO requires initialization from sub-state-vector thus sum of amplitudes can be smaller than 1 + # if initialized from a vector, check that the parameters are normalized + # if not self._from_label and not self._from_int: + # norm = np.linalg.norm(params) + # if normalize: + # params = np.array(params, dtype=np.complex128) / norm + # elif not math.isclose(norm, 1.0, abs_tol=_EPS): + # raise QiskitError(f"Sum of amplitudes-squared is not 1, but {norm}.") + num_qubits = self._get_num_qubits(num_qubits, params) params = [params] if isinstance(params, int) else params super().__init__(self._name, num_qubits, params, label=self._label) @@ -201,15 +213,10 @@ def _get_num_qubits(self, num_qubits, params): "Desired statevector length not a positive power of 2." ) - # FIXME: We need partial initialization and we suppress this - ## Check if probabilities (amplitudes squared) sum to 1 - # if not math.isclose(sum(np.absolute(params) ** 2), 1.0, abs_tol=_EPS): - # raise QiskitError("Sum of amplitudes-squared does not equal one.") - num_qubits = int(num_qubits) return num_qubits - def inverse(self): + def inverse(self, annotated: bool = False): """Return inverted StatePreparation""" label = ( @@ -430,111 +437,3 @@ def _multiplex(self, target_gate, list_of_angles, last_cnot=True): circuit.append(CXGate(), [msb, lsb]) return circuit - - -def prepare_state(self, state, qubits=None, label=None): - r"""Prepare qubits in a specific state. - - This class implements a state preparing unitary. Unlike - :class:`qiskit.extensions.Initialize` it does not reset the qubits first. - - Args: - state (str or list or int or Statevector): - * Statevector: Statevector to initialize to. - * str: labels of basis states of the Pauli eigenstates Z, X, Y. See - :meth:`.Statevector.from_label`. Notice the order of the labels is reversed with respect - to the qubit index to be applied to. Example label '01' initializes the qubit zero to - :math:`|1\rangle` and the qubit one to :math:`|0\rangle`. - * list: vector of complex amplitudes to initialize to. - * int: an integer that is used as a bitmap indicating which qubits to initialize - to :math:`|1\rangle`. Example: setting params to 5 would initialize qubit 0 and qubit 2 - to :math:`|1\rangle` and qubit 1 to :math:`|0\rangle`. - - qubits (QuantumRegister or Qubit or int): - * QuantumRegister: A list of qubits to be initialized [Default: None]. - * Qubit: Single qubit to be initialized [Default: None]. - * int: Index of qubit to be initialized [Default: None]. - * list: Indexes of qubits to be initialized [Default: None]. - label (str): An optional label for the gate - - Returns: - qiskit.circuit.Instruction: a handle to the instruction that was just initialized - - Examples: - Prepare a qubit in the state :math:`(|0\rangle - |1\rangle) / \sqrt{2}`. - - .. jupyter-execute:: - - import numpy as np - from qiskit import QuantumCircuit - - circuit = QuantumCircuit(1) - circuit.prepare_state([1/np.sqrt(2), -1/np.sqrt(2)], 0) - circuit.draw() - - output: - - .. parsed-literal:: - - ┌─────────────────────────────────────┐ - q_0: ┤ State Preparation(0.70711,-0.70711) ├ - └─────────────────────────────────────┘ - - - Prepare from a string two qubits in the state :math:`|10\rangle`. - The order of the labels is reversed with respect to qubit index. - More information about labels for basis states are in - :meth:`.Statevector.from_label`. - - .. jupyter-execute:: - - import numpy as np - from qiskit import QuantumCircuit - - circuit = QuantumCircuit(2) - circuit.prepare_state('01', circuit.qubits) - circuit.draw() - - output: - - .. parsed-literal:: - - ┌─────────────────────────┐ - q_0: ┤0 ├ - │ State Preparation(0,1) │ - q_1: ┤1 ├ - └─────────────────────────┘ - - - Initialize two qubits from an array of complex amplitudes - .. jupyter-execute:: - - import numpy as np - from qiskit import QuantumCircuit - - circuit = QuantumCircuit(2) - circuit.prepare_state([0, 1/np.sqrt(2), -1.j/np.sqrt(2), 0], circuit.qubits) - circuit.draw() - - output: - - .. parsed-literal:: - - ┌───────────────────────────────────────────┐ - q_0: ┤0 ├ - │ State Preparation(0,0.70711,-0.70711j,0) │ - q_1: ┤1 ├ - └───────────────────────────────────────────┘ - """ - - if qubits is None: - qubits = self.qubits - elif isinstance(qubits, (int, np.integer, slice, Qubit)): - qubits = [qubits] - - num_qubits = len(qubits) if isinstance(state, int) else None - - return self.append(StatePreparation(state, num_qubits, label=label), qubits) - - -QuantumCircuit.prepare_state = prepare_state diff --git a/qdao/qiskit/data_preparation/z_feature_map.py b/qdao/qiskit/data_preparation/z_feature_map.py new file mode 100644 index 0000000..87cf1c7 --- /dev/null +++ b/qdao/qiskit/data_preparation/z_feature_map.py @@ -0,0 +1,105 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 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. + +"""Create a new first-order Pauli-Z expansion circuit.""" + +from typing import Callable, Optional + +import numpy as np + +from .pauli_feature_map import PauliFeatureMap + + +class ZFeatureMap(PauliFeatureMap): + """The first order Pauli Z-evolution circuit. + + On 3 qubits and with 2 repetitions the circuit is represented by: + + .. parsed-literal:: + + ┌───┐┌──────────────┐┌───┐┌──────────────┐ + ┤ H ├┤ U1(2.0*x[0]) ├┤ H ├┤ U1(2.0*x[0]) ├ + ├───┤├──────────────┤├───┤├──────────────┤ + ┤ H ├┤ U1(2.0*x[1]) ├┤ H ├┤ U1(2.0*x[1]) ├ + ├───┤├──────────────┤├───┤├──────────────┤ + ┤ H ├┤ U1(2.0*x[2]) ├┤ H ├┤ U1(2.0*x[2]) ├ + └───┘└──────────────┘└───┘└──────────────┘ + + This is a sub-class of :class:`~qiskit.circuit.library.PauliFeatureMap` where the Pauli + strings are fixed as `['Z']`. As a result the first order expansion will be a circuit without + entangling gates. + + Examples: + + >>> prep = ZFeatureMap(3, reps=3, insert_barriers=True) + >>> print(prep) + ┌───┐ ░ ┌──────────────┐ ░ ┌───┐ ░ ┌──────────────┐ ░ ┌───┐ ░ ┌──────────────┐ + q_0: ┤ H ├─░─┤ U1(2.0*x[0]) ├─░─┤ H ├─░─┤ U1(2.0*x[0]) ├─░─┤ H ├─░─┤ U1(2.0*x[0]) ├ + ├───┤ ░ ├──────────────┤ ░ ├───┤ ░ ├──────────────┤ ░ ├───┤ ░ ├──────────────┤ + q_1: ┤ H ├─░─┤ U1(2.0*x[1]) ├─░─┤ H ├─░─┤ U1(2.0*x[1]) ├─░─┤ H ├─░─┤ U1(2.0*x[1]) ├ + ├───┤ ░ ├──────────────┤ ░ ├───┤ ░ ├──────────────┤ ░ ├───┤ ░ ├──────────────┤ + q_2: ┤ H ├─░─┤ U1(2.0*x[2]) ├─░─┤ H ├─░─┤ U1(2.0*x[2]) ├─░─┤ H ├─░─┤ U1(2.0*x[2]) ├ + └───┘ ░ └──────────────┘ ░ └───┘ ░ └──────────────┘ ░ └───┘ ░ └──────────────┘ + + >>> data_map = lambda x: x[0]*x[0] + 1 # note: input is an array + >>> prep = ZFeatureMap(3, reps=1, data_map_func=data_map) + >>> print(prep) + ┌───┐┌───────────────────────┐ + q_0: ┤ H ├┤ U1(2.0*x[0]**2 + 2.0) ├ + ├───┤├───────────────────────┤ + q_1: ┤ H ├┤ U1(2.0*x[1]**2 + 2.0) ├ + ├───┤├───────────────────────┤ + q_2: ┤ H ├┤ U1(2.0*x[2]**2 + 2.0) ├ + └───┘└───────────────────────┘ + + >>> classifier = ZFeatureMap(3, reps=1) + RY(3, reps=1) + >>> print(classifier) + ┌───┐┌──────────────┐┌──────────┐ ┌──────────┐ + q_0: ┤ H ├┤ U1(2.0*x[0]) ├┤ RY(θ[0]) ├─■──■─┤ RY(θ[3]) ├──────────── + ├───┤├──────────────┤├──────────┤ │ │ └──────────┘┌──────────┐ + q_1: ┤ H ├┤ U1(2.0*x[1]) ├┤ RY(θ[1]) ├─■──┼──────■──────┤ RY(θ[4]) ├ + ├───┤├──────────────┤├──────────┤ │ │ ├──────────┤ + q_2: ┤ H ├┤ U1(2.0*x[2]) ├┤ RY(θ[2]) ├────■──────■──────┤ RY(θ[5]) ├ + └───┘└──────────────┘└──────────┘ └──────────┘ + + """ + + def __init__( + self, + feature_dimension: int, + reps: int = 2, + data_map_func: Optional[Callable[[np.ndarray], float]] = None, + parameter_prefix: str = "x", + insert_barriers: bool = False, + name: str = "ZFeatureMap", + ) -> None: + """Create a new first-order Pauli-Z expansion circuit. + + Args: + feature_dimension: The number of features + reps: The number of repeated circuits. Defaults to 2, has a minimum value of 1. + data_map_func: A mapping function for data x which can be supplied to override the + default mapping from :meth:`self_product`. + parameter_prefix: The prefix used if default parameters are generated. + insert_barriers: If True, barriers are inserted in between the evolution instructions + and hadamard layers. + + """ + super().__init__( + feature_dimension=feature_dimension, + paulis=["Z"], + reps=reps, + data_map_func=data_map_func, + parameter_prefix=parameter_prefix, + insert_barriers=insert_barriers, + name=name, + ) diff --git a/qdao/qiskit/data_preparation/zz_feature_map.py b/qdao/qiskit/data_preparation/zz_feature_map.py new file mode 100644 index 0000000..03978f9 --- /dev/null +++ b/qdao/qiskit/data_preparation/zz_feature_map.py @@ -0,0 +1,116 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 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. + +"""Second-order Pauli-Z expansion circuit.""" + +from typing import Callable, List, Optional, Union + +import numpy as np + +from .pauli_feature_map import PauliFeatureMap + + +class ZZFeatureMap(PauliFeatureMap): + r"""Second-order Pauli-Z evolution circuit. + + For 3 qubits and 1 repetition and linear entanglement the circuit is represented by: + + .. parsed-literal:: + + ┌───┐┌─────────────────┐ + ┤ H ├┤ U1(2.0*φ(x[0])) ├──■────────────────────────────■──────────────────────────────────── + ├───┤├─────────────────┤┌─┴─┐┌──────────────────────┐┌─┴─┐ + ┤ H ├┤ U1(2.0*φ(x[1])) ├┤ X ├┤ U1(2.0*φ(x[0],x[1])) ├┤ X ├──■────────────────────────────■── + ├───┤├─────────────────┤└───┘└──────────────────────┘└───┘┌─┴─┐┌──────────────────────┐┌─┴─┐ + ┤ H ├┤ U1(2.0*φ(x[2])) ├──────────────────────────────────┤ X ├┤ U1(2.0*φ(x[1],x[2])) ├┤ X ├ + └───┘└─────────────────┘ └───┘└──────────────────────┘└───┘ + + where :math:`\varphi` is a classical non-linear function, which defaults to :math:`\varphi(x) = x` + if and :math:`\varphi(x,y) = (\pi - x)(\pi - y)`. + + Examples: + + >>> from qiskit.circuit.library import ZZFeatureMap + >>> prep = ZZFeatureMap(2, reps=1) + >>> print(prep) + ┌───┐┌──────────────┐ + q_0: ┤ H ├┤ U1(2.0*x[0]) ├──■───────────────────────────────────────■── + ├───┤├──────────────┤┌─┴─┐┌─────────────────────────────────┐┌─┴─┐ + q_1: ┤ H ├┤ U1(2.0*x[1]) ├┤ X ├┤ U1(2.0*(pi - x[0])*(pi - x[1])) ├┤ X ├ + └───┘└──────────────┘└───┘└─────────────────────────────────┘└───┘ + + >>> from qiskit.circuit.library import EfficientSU2 + >>> classifier = ZZFeatureMap(3) + EfficientSU2(3) + >>> classifier.num_parameters + 15 + >>> classifier.parameters # 'x' for the data preparation, 'θ' for the SU2 parameters + ParameterView([ + ParameterVectorElement(x[0]), ParameterVectorElement(x[1]), + ParameterVectorElement(x[2]), ParameterVectorElement(θ[0]), + ParameterVectorElement(θ[1]), ParameterVectorElement(θ[2]), + ParameterVectorElement(θ[3]), ParameterVectorElement(θ[4]), + ParameterVectorElement(θ[5]), ParameterVectorElement(θ[6]), + ParameterVectorElement(θ[7]), ParameterVectorElement(θ[8]), + ParameterVectorElement(θ[9]), ParameterVectorElement(θ[10]), + ParameterVectorElement(θ[11]), ParameterVectorElement(θ[12]), + ParameterVectorElement(θ[13]), ParameterVectorElement(θ[14]), + ParameterVectorElement(θ[15]), ParameterVectorElement(θ[16]), + ParameterVectorElement(θ[17]), ParameterVectorElement(θ[18]), + ParameterVectorElement(θ[19]), ParameterVectorElement(θ[20]), + ParameterVectorElement(θ[21]), ParameterVectorElement(θ[22]), + ParameterVectorElement(θ[23]) + ]) + >>> classifier.count_ops() + OrderedDict([('ZZFeatureMap', 1), ('EfficientSU2', 1)]) + """ + + def __init__( + self, + feature_dimension: int, + reps: int = 2, + entanglement: Union[str, List[List[int]], Callable[[int], List[int]]] = "full", + data_map_func: Optional[Callable[[np.ndarray], float]] = None, + parameter_prefix: str = "x", + insert_barriers: bool = False, + name: str = "ZZFeatureMap", + ) -> None: + """Create a new second-order Pauli-Z expansion. + + Args: + feature_dimension: Number of features. + reps: The number of repeated circuits, has a min. value of 1. + entanglement: Specifies the entanglement structure. Refer to + :class:`~qiskit.circuit.library.NLocal` for detail. + data_map_func: A mapping function for data x. + parameter_prefix: The prefix used if default parameters are generated. + insert_barriers: If True, barriers are inserted in between the evolution instructions + and hadamard layers. + + Raises: + ValueError: If the feature dimension is smaller than 2. + """ + if feature_dimension < 2: + raise ValueError( + "The ZZFeatureMap contains 2-local interactions and cannot be " + f"defined for less than 2 qubits. You provided {feature_dimension}." + ) + + super().__init__( + feature_dimension=feature_dimension, + reps=reps, + entanglement=entanglement, + paulis=["Z", "ZZ"], + data_map_func=data_map_func, + parameter_prefix=parameter_prefix, + insert_barriers=insert_barriers, + name=name, + ) diff --git a/qdao/qiskit/gate.py b/qdao/qiskit/gate.py index 6852447..c4a3138 100644 --- a/qdao/qiskit/gate.py +++ b/qdao/qiskit/gate.py @@ -12,12 +12,15 @@ """Unitary gate.""" -from typing import List, Optional, Tuple, Union +from __future__ import annotations + +from typing import Iterable, Iterator import numpy as np from qiskit.circuit.exceptions import CircuitError from qiskit.circuit.parameterexpression import ParameterExpression +from .annotated_operation import AnnotatedOperation, ControlModifier from .instruction import Instruction @@ -25,7 +28,13 @@ class Gate(Instruction): """Unitary gate.""" def __init__( - self, name: str, num_qubits: int, params: List, label: Optional[str] = None + self, + name: str, + num_qubits: int, + params: list, + label: str | None = None, + duration=None, + unit="dt", ) -> None: """Create a new gate. @@ -36,7 +45,9 @@ def __init__( label: An optional label for the gate. """ self.definition = None - super().__init__(name, num_qubits, 0, params, label=label) + super().__init__( + name, num_qubits, 0, params, label=label, duration=duration, unit=unit + ) # Set higher priority than Numpy array and matrix classes __array_priority__ = 20 @@ -52,7 +63,6 @@ def to_matrix(self) -> np.ndarray: exception will be raised when this base class method is called. """ if hasattr(self, "__array__"): - # pylint: disable=no-member return self.__array__(dtype=complex) raise CircuitError(f"to_matrix not defined for this {type(self)}") @@ -63,34 +73,21 @@ def power(self, exponent: float): exponent (float): Gate^exponent Returns: - qiskit.circuit.library.UnitaryGate: To which `to_matrix` is self.to_matrix^exponent. + .library.UnitaryGate: To which `to_matrix` is self.to_matrix^exponent. Raises: CircuitError: If Gate is not unitary """ - from qiskit.circuit.library import ( - UnitaryGate, - ) # pylint: disable=cyclic-import - from qiskit.quantum_info.operators import ( - Operator, - ) # pylint: disable=cyclic-import - from scipy.linalg import schur - - # Should be diagonalized because it's a unitary. - decomposition, unitary = schur(Operator(self).data, output="complex") - # Raise the diagonal entries to the specified power - decomposition_power = [] - - decomposition_diagonal = decomposition.diagonal() - # assert off-diagonal are 0 - if not np.allclose(np.diag(decomposition_diagonal), decomposition): - raise CircuitError("The matrix is not diagonal") - - for element in decomposition_diagonal: - decomposition_power.append(pow(element, exponent)) - # Then reconstruct the resulting gate. - unitary_power = unitary @ np.diag(decomposition_power) @ unitary.conj().T - return UnitaryGate(unitary_power, label=f"{self.name}^{exponent}") + # pylint: disable=cyclic-import + from qiskit.circuit.library.generalized_gates.unitary import UnitaryGate + from qiskit.quantum_info.operators import Operator + + return UnitaryGate( + Operator(self).power(exponent), label=f"{self.name}^{exponent}" + ) + + def __pow__(self, exponent: float) -> "Gate": + return self.power(exponent) def _return_repeat(self, exponent: float) -> "Gate": return Gate( @@ -102,32 +99,45 @@ def _return_repeat(self, exponent: float) -> "Gate": def control( self, num_ctrl_qubits: int = 1, - label: Optional[str] = None, - ctrl_state: Optional[Union[int, str]] = None, + label: str | None = None, + ctrl_state: int | str | None = None, + annotated: bool = False, ): - """Return controlled version of gate. See :class:`.ControlledGate` for usage. + """ + Return the controlled version of itself. + + Implemented either as a controlled gate (ref. :class:`.ControlledGate`) + or as an annotated operation (ref. :class:`.AnnotatedOperation`). Args: - num_ctrl_qubits: number of controls to add to gate (default=1) - label: optional gate label - ctrl_state: The control state in decimal or as a bitstring - (e.g. '111'). If None, use 2**num_ctrl_qubits-1. + num_ctrl_qubits: number of controls to add to gate (default: ``1``) + label: optional gate label. Ignored if implemented as an annotated + operation. + ctrl_state: the control state in decimal or as a bitstring + (e.g. ``'111'``). If ``None``, use ``2**num_ctrl_qubits-1``. + annotated: indicates whether the controlled gate can be implemented + as an annotated gate. Returns: - qiskit.circuit.ControlledGate: Controlled version of gate. This default algorithm - uses num_ctrl_qubits-1 ancillae qubits so returns a gate of size - num_qubits + 2*num_ctrl_qubits - 1. + Controlled version of the given operation. Raises: QiskitError: unrecognized mode or invalid ctrl_state """ - # pylint: disable=cyclic-import - from .add_control import add_control + if not annotated: + # pylint: disable=cyclic-import + from .add_control import add_control - return add_control(self, num_ctrl_qubits, label, ctrl_state) + return add_control(self, num_ctrl_qubits, label, ctrl_state) + + else: + return AnnotatedOperation( + self, + ControlModifier(num_ctrl_qubits=num_ctrl_qubits, ctrl_state=ctrl_state), + ) @staticmethod - def _broadcast_single_argument(qarg: List) -> List: + def _broadcast_single_argument(qarg: list) -> Iterator[tuple[list, list]]: """Expands a single argument. For example: [q[0], q[1]] -> [q[0]], [q[1]] @@ -138,7 +148,7 @@ def _broadcast_single_argument(qarg: List) -> List: yield [arg0], [] @staticmethod - def _broadcast_2_arguments(qarg0: List, qarg1: List) -> List: + def _broadcast_2_arguments(qarg0: list, qarg1: list) -> Iterator[tuple[list, list]]: if len(qarg0) == len(qarg1): # [[q[0], q[1]], [r[0], r[1]]] -> [q[0], r[0]] # -> [q[1], r[1]] @@ -160,7 +170,7 @@ def _broadcast_2_arguments(qarg0: List, qarg1: List) -> List: ) @staticmethod - def _broadcast_3_or_more_args(qargs: List) -> List: + def _broadcast_3_or_more_args(qargs: list) -> Iterator[tuple[list, list]]: if all(len(qarg) == len(qargs[0]) for qarg in qargs): for arg in zip(*qargs): yield list(arg), [] @@ -169,7 +179,9 @@ def _broadcast_3_or_more_args(qargs: List) -> List: "Not sure how to combine these qubit arguments:\n %s\n" % qargs ) - def broadcast_arguments(self, qargs: List, cargs: List) -> Tuple[List, List]: + def broadcast_arguments( + self, qargs: list, cargs: list + ) -> Iterable[tuple[list, list]]: """Validation and handling of the arguments and its relationship. For example, ``cx([q[0],q[1]], q[2])`` means ``cx(q[0], q[2]); cx(q[1], q[2])``. This @@ -215,6 +227,10 @@ def broadcast_arguments(self, qargs: List, cargs: List) -> Tuple[List, List]: if any(not qarg for qarg in qargs): raise CircuitError("One or more of the arguments are empty") + if len(qargs) == 0: + return [ + ([], []), + ] if len(qargs) == 1: return Gate._broadcast_single_argument(qargs[0]) elif len(qargs) == 2: diff --git a/qdao/qiskit/initializer.py b/qdao/qiskit/initializer.py deleted file mode 100644 index e6cf00d..0000000 --- a/qdao/qiskit/initializer.py +++ /dev/null @@ -1,207 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017. -# -# 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. - -""" -Initialize qubit registers to desired arbitrary state. -""" -import numpy as np - -# FIXME: Used modified instruction -# from qiskit.circuit import Instruction -from qiskit.circuit import QuantumCircuit, QuantumRegister, Qubit - -from qdao.qiskit.circ_append import _append_init_sv, append_init_sv -from qdao.qiskit.instruction import Instruction -from qdao.qiskit.state_preparation import StatePreparation -from qdao.util import safe_import - -print_statistics = safe_import("qutils", "print_statistics") -time_it = safe_import("qutils", "time_it") - -_EPS = 1e-10 # global variable used to chop very small numbers to zero - -QuantumCircuit.append_init_sv = append_init_sv -QuantumCircuit._append_init_sv = _append_init_sv - - -class Initialize(Instruction): - """Complex amplitude initialization. - - Class that initializes some flexible collection of qubit registers, implemented by calling - the :class:`qiskit.extensions.StatePreparation` Class. - Note that Initialize is an Instruction and not a Gate since it contains a reset instruction, - which is not unitary. - """ - - def __init__(self, params, num_qubits=None): - r"""Create new initialize composite. - - Args: - params (str, list, int or Statevector): - * Statevector: Statevector to initialize to. - * list: vector of complex amplitudes to initialize to. - * string: labels of basis states of the Pauli eigenstates Z, X, Y. See - :meth:`.Statevector.from_label`. - Notice the order of the labels is reversed with respect to the qubit index to - be applied to. Example label '01' initializes the qubit zero to :math:`|1\rangle` - and the qubit one to :math:`|0\rangle`. - * int: an integer that is used as a bitmap indicating which qubits to initialize - to :math:`|1\rangle`. Example: setting params to 5 would initialize qubit 0 and qubit 2 - to :math:`|1\rangle` and qubit 1 to :math:`|0\rangle`. - - num_qubits (int): This parameter is only used if params is an int. Indicates the total - number of qubits in the `initialize` call. Example: `initialize` covers 5 qubits - and params is 3. This allows qubits 0 and 1 to be initialized to :math:`|1\rangle` - and the remaining 3 qubits to be initialized to :math:`|0\rangle`. - """ - self._stateprep = StatePreparation(params, num_qubits) - - super().__init__( - "initialize", self._stateprep.num_qubits, 0, self._stateprep.params - ) - - def _define(self): - q = QuantumRegister(self.num_qubits, "q") - initialize_circuit = QuantumCircuit(q, name="init_def") - initialize_circuit.reset(q) - initialize_circuit.append(self._stateprep, q) - self.definition = initialize_circuit - - def gates_to_uncompute(self): - """Call to create a circuit with gates that take the desired vector to zero. - - Returns: - QuantumCircuit: circuit to take self.params vector to :math:`|{00\\ldots0}\\rangle` - """ - return self._stateprep._gates_to_uncompute() - - @property - def params(self): - """Return initialize params.""" - return self._stateprep.params - - @params.setter - def params(self, parameters): - """Set initialize params.""" - self._stateprep.params = parameters - - def broadcast_arguments(self, qargs, cargs): - return self._stateprep.broadcast_arguments(qargs, cargs) - - -@time_it -def initialize(self, params, qubits=None): - r"""Initialize qubits in a specific state. - - Qubit initialization is done by first resetting the qubits to :math:`|0\rangle` - followed by calling :class:`qiskit.extensions.StatePreparation` - class to prepare the qubits in a specified state. - Both these steps are included in the - :class:`qiskit.extensions.Initialize` instruction. - - Args: - params (str or list or int): - * str: labels of basis states of the Pauli eigenstates Z, X, Y. See - :meth:`.Statevector.from_label`. Notice the order of the labels is reversed with respect - to the qubit index to be applied to. Example label '01' initializes the qubit zero to - :math:`|1\rangle` and the qubit one to :math:`|0\rangle`. - * list: vector of complex amplitudes to initialize to. - * int: an integer that is used as a bitmap indicating which qubits to initialize - to :math:`|1\rangle`. Example: setting params to 5 would initialize qubit 0 and qubit 2 - to :math:`|1\rangle` and qubit 1 to :math:`|0\rangle`. - - qubits (QuantumRegister or Qubit or int): - * QuantumRegister: A list of qubits to be initialized [Default: None]. - * Qubit: Single qubit to be initialized [Default: None]. - * int: Index of qubit to be initialized [Default: None]. - * list: Indexes of qubits to be initialized [Default: None]. - - Returns: - qiskit.circuit.Instruction: a handle to the instruction that was just initialized - - Examples: - Prepare a qubit in the state :math:`(|0\rangle - |1\rangle) / \sqrt{2}`. - - .. jupyter-execute:: - - import numpy as np - from qiskit import QuantumCircuit - - circuit = QuantumCircuit(1) - circuit.initialize([1/np.sqrt(2), -1/np.sqrt(2)], 0) - circuit.draw() - - output: - - .. parsed-literal:: - - ┌──────────────────────────────┐ - q_0: ┤ Initialize(0.70711,-0.70711) ├ - └──────────────────────────────┘ - - - Initialize from a string two qubits in the state :math:`|10\rangle`. - The order of the labels is reversed with respect to qubit index. - More information about labels for basis states are in - :meth:`.Statevector.from_label`. - - .. jupyter-execute:: - - import numpy as np - from qiskit import QuantumCircuit - - circuit = QuantumCircuit(2) - circuit.initialize('01', circuit.qubits) - circuit.draw() - - output: - - .. parsed-literal:: - - ┌──────────────────┐ - q_0: ┤0 ├ - │ Initialize(0,1) │ - q_1: ┤1 ├ - └──────────────────┘ - - Initialize two qubits from an array of complex amplitudes. - - .. jupyter-execute:: - - import numpy as np - from qiskit import QuantumCircuit - - circuit = QuantumCircuit(2) - circuit.initialize([0, 1/np.sqrt(2), -1.j/np.sqrt(2), 0], circuit.qubits) - circuit.draw() - - output: - - .. parsed-literal:: - - ┌────────────────────────────────────┐ - q_0: ┤0 ├ - │ Initialize(0,0.70711,-0.70711j,0) │ - q_1: ┤1 ├ - └────────────────────────────────────┘ - """ - if qubits is None: - qubits = self.qubits - elif isinstance(qubits, (int, np.integer, slice, Qubit)): - qubits = [qubits] - num_qubits = len(qubits) if isinstance(params, int) else None - - return self.append_init_sv(Initialize(params, num_qubits), qubits) - - -QuantumCircuit.initialize = initialize -QuantumCircuit.print_statistics = print_statistics diff --git a/qdao/qiskit/instruction.py b/qdao/qiskit/instruction.py index 295efef..ecf8cea 100644 --- a/qdao/qiskit/instruction.py +++ b/qdao/qiskit/instruction.py @@ -30,23 +30,23 @@ Instructions do not have any context about where they are in a circuit (which qubits/clbits). The circuit itself keeps this context. """ -import logging + +from __future__ import annotations + import copy -import time +import math from itertools import zip_longest -from typing import List +from typing import List, Type import numpy +from qiskit.circuit.annotated_operation import AnnotatedOperation, InverseModifier from qiskit.circuit.classicalregister import ClassicalRegister, Clbit from qiskit.circuit.exceptions import CircuitError from qiskit.circuit.operation import Operation from qiskit.circuit.parameter import ParameterExpression from qiskit.circuit.quantumregister import QuantumRegister -from qiskit.qasm2 import QASM2Error from qiskit.qobj.qasm_qobj import QasmQobjInstruction -from .tools import pi_check - _CUTOFF_PRECISION = 1e-10 @@ -98,11 +98,10 @@ def __init__( self._label = label # tuple (ClassicalRegister, int), tuple (Clbit, bool) or tuple (Clbit, int) # when the instruction has a conditional ("if") - self.condition = None + self._condition = None # list of instructions (and their contexts) that this instruction is composed of # empty definition means opaque or fundamental instruction self._definition = None - self._duration = duration self._unit = unit @@ -110,6 +109,66 @@ def __init__( params # must be at last (other properties may be required for validation) ) + @property + def base_class(self) -> Type[Instruction]: + """Get the base class of this instruction. This is guaranteed to be in the inheritance tree + of ``self``. + + The "base class" of an instruction is the lowest class in its inheritance tree that the + object should be considered entirely compatible with for _all_ circuit applications. This + typically means that the subclass is defined purely to offer some sort of programmer + convenience over the base class, and the base class is the "true" class for a behavioural + perspective. In particular, you should *not* override :attr:`base_class` if you are + defining a custom version of an instruction that will be implemented differently by + hardware, such as an alternative measurement strategy, or a version of a parametrised gate + with a particular set of parameters for the purposes of distinguishing it in a + :class:`.Target` from the full parametrised gate. + + This is often exactly equivalent to ``type(obj)``, except in the case of singleton instances + of standard-library instructions. These singleton instances are special subclasses of their + base class, and this property will return that base. For example:: + + >>> isinstance(XGate(), XGate) + True + >>> type(XGate()) is XGate + False + >>> XGate().base_class is XGate + True + + In general, you should not rely on the precise class of an instruction; within a given + circuit, it is expected that :attr:`Instruction.name` should be a more suitable + discriminator in most situations. + """ + return type(self) + + @property + def mutable(self) -> bool: + """Is this instance is a mutable unique instance or not. + + If this attribute is ``False`` the gate instance is a shared singleton + and is not mutable. + """ + return True + + def to_mutable(self): + """Return a mutable copy of this gate. + + This method will return a new mutable copy of this gate instance. + If a singleton instance is being used this will be a new unique + instance that can be mutated. If the instance is already mutable it + will be a deepcopy of that instance. + """ + return self.copy() + + @property + def condition(self): + """The classical condition on the instruction.""" + return self._condition + + @condition.setter + def condition(self, condition): + self._condition = condition + def __eq__(self, other): """Two instructions are the same if they have the same name, same dimensions, and same params. @@ -120,8 +179,9 @@ def __eq__(self, other): Returns: bool: are self and other equal. """ - if ( - type(self) is not type(other) + if ( # pylint: disable=too-many-boolean-expressions + not isinstance(other, Instruction) + or self.base_class is not other.base_class or self.name != other.name or self.num_qubits != other.num_qubits or self.num_clbits != other.num_clbits @@ -137,13 +197,15 @@ def __eq__(self, other): pass try: - if numpy.shape(self_param) == numpy.shape( - other_param + self_asarray = numpy.asarray(self_param) + other_asarray = numpy.asarray(other_param) + if numpy.shape(self_asarray) == numpy.shape( + other_asarray ) and numpy.allclose( self_param, other_param, atol=_CUTOFF_PRECISION, rtol=0 ): continue - except TypeError: + except (ValueError, TypeError): pass try: @@ -162,7 +224,7 @@ def __eq__(self, other): return True def __repr__(self) -> str: - """Generates a representation of the Intruction object instance + """Generates a representation of the Instruction object instance Returns: str: A representation of the Instruction instance with the name, number of qubits, classical bits and params( if any ) @@ -226,25 +288,13 @@ def params(self): @params.setter def params(self, parameters): - # FIXME: No need to - # append one-by-one when setting statevector - st = time.time() - # if isinstance(parameters, list): - # self._params = parameters - # return - # if not isinstance(parameters, numpy.ndarray): - # raise ValueError("For qdao, must initialize "\ - # "from a statevector (np.numpy.ndarray)") - ##self._params = list(parameters) - # self._params = parameters.tolist() + # FIXME(): remove validation for qdao performance self._params = parameters - logging.info("Time of setting params: {}".format(time.time() - st)) - # for single_param in parameters: - # if isinstance(single_param, ParameterExpression): - # self._params.append(single_param) - # else: - # self._params.append(self.validate_parameter(single_param)) + # if isinstance(single_param, ParameterExpression): + # self._params.append(single_param) + # else: + # self._params.append(self.validate_parameter(single_param)) def validate_parameter(self, parameter): """Instruction parameters has no validation or normalization.""" @@ -316,10 +366,10 @@ def assemble(self): """Assemble a QasmQobjInstruction""" instruction = QasmQobjInstruction(name=self.name) # Evaluate parameters - # if self.params: - # params = [x.evalf(x) if hasattr(x, "evalf") else x for x in self.params] - # FIXME: No need to do so for initializing statevector - instruction.params = self.params + if self.params: + # FIXME(): in QDAO no need to do so for performance issue + # params = [x.evalf(x) if hasattr(x, "evalf") else x for x in self.params] + instruction.params = self.params # Add placeholder for qarg and carg params if self.num_qubits: instruction.qubits = list(range(self.num_qubits)) @@ -365,7 +415,13 @@ def reverse_ops(self): qiskit.circuit.Instruction: a new instruction with sub-instructions reversed. """ - if not self._definition: + # A single `Instruction` cannot really determine whether it is a "composite" instruction or + # not; it depends on greater context whether it needs to be decomposed. The `_definition` + # not existing is flaky; all that means is that nobody has _yet_ asked for its definition; + # for efficiency, most gates define this on-the-fly. The checks here are a very very + # approximate check for an "atomic" instruction, that are mostly just this way for + # historical consistency. + if not self._definition or not self.mutable: return self.copy() reverse_inst = self.copy(name=self.name + "_reverse") @@ -377,22 +433,36 @@ def reverse_ops(self): reverse_inst.definition = reversed_definition return reverse_inst - def inverse(self): + def inverse(self, annotated: bool = False): """Invert this instruction. - If the instruction is composite (i.e. has a definition), - then its definition will be recursively inverted. + If `annotated` is `False`, the inverse instruction is implemented as + a fresh instruction with the recursively inverted definition. + + If `annotated` is `True`, the inverse instruction is implemented as + :class:`.AnnotatedOperation`, and corresponds to the given instruction + annotated with the "inverse modifier". Special instructions inheriting from Instruction can implement their own inverse (e.g. T and Tdg, Barrier, etc.) + In particular, they can choose how to handle the argument ``annotated`` + which may include ignoring it and always returning a concrete gate class + if the inverse is defined as a standard gate. + + Args: + annotated: if set to `True` the output inverse gate will be returned + as :class:`.AnnotatedOperation`. Returns: - qiskit.circuit.Instruction: a fresh instruction for the inverse + The inverse operation. Raises: CircuitError: if the instruction is not composite and an inverse has not been implemented for it. """ + if annotated: + return AnnotatedOperation(self, InverseModifier()) + if self.definition is None: raise CircuitError("inverse() not implemented for %s." % self.name) @@ -443,7 +513,7 @@ def c_if(self, classical, val): # Casting the conditional value as Boolean when # the classical condition is on a classical bit. val = bool(val) - self.condition = (classical, val) + self._condition = (classical, val) return self def copy(self, name=None): @@ -451,12 +521,11 @@ def copy(self, name=None): Copy of the instruction. Args: - name (str): name to be given to the copied circuit, - if None then the name stays the same. + name (str): name to be given to the copied circuit, if ``None`` then the name stays the same. Returns: - qiskit.circuit.Instruction: a copy of the current instruction, with the name - updated if it was provided + qiskit.circuit.Instruction: a copy of the current instruction, with the name updated if it + was provided """ cpy = self.__deepcopy__() @@ -464,38 +533,25 @@ def copy(self, name=None): cpy.name = name return cpy - def __deepcopy__(self, _memo=None): + def __deepcopy__(self, memo=None): cpy = copy.copy(self) cpy._params = copy.copy(self._params) if self._definition: - cpy._definition = copy.deepcopy(self._definition, _memo) + cpy._definition = copy.deepcopy(self._definition, memo) return cpy def _qasmif(self, string): """Print an if statement if needed.""" + from qiskit.qasm2 import QASM2ExportError # pylint: disable=cyclic-import + if self.condition is None: return string if not isinstance(self.condition[0], ClassicalRegister): - raise Qasm2Error( + raise QASM2ExportError( "OpenQASM 2 can only condition on registers, but got '{self.condition[0]}'" ) return "if(%s==%d) " % (self.condition[0].name, self.condition[1]) + string - def qasm(self): - """Return a default OpenQASM string for the instruction. - - Derived instructions may override this to print in a - different format (e.g. measure q[0] -> c[0];). - """ - name_param = self.name - if self.params: - name_param = "{}({})".format( - name_param, - ",".join([pi_check(i, ndigits=8, output="qasm") for i in self.params]), - ) - - return self._qasmif(name_param) - def broadcast_arguments(self, qargs, cargs): """ Validation of the arguments. @@ -516,6 +572,11 @@ def broadcast_arguments(self, qargs, cargs): f"The amount of qubit arguments {len(qargs)} does not match" f" the instruction expectation ({self.num_qubits})." ) + if len(cargs) != self.num_clbits: + raise CircuitError( + f"The amount of clbit arguments {len(cargs)} does not match" + f" the instruction expectation ({self.num_clbits})." + ) # [[q[0], q[1]], [c[0], c[1]]] -> [q[0], c[0]], [q[1], c[1]] flat_qargs = [qarg for sublist in qargs for qarg in sublist] @@ -571,12 +632,13 @@ def repeat(self, n): @property def condition_bits(self) -> List[Clbit]: """Get Clbits in condition.""" + from qiskit.circuit.controlflow import ( # pylint: disable=cyclic-import + condition_resources, + ) + if self.condition is None: return [] - if isinstance(self.condition[0], Clbit): - return [self.condition[0]] - else: # ClassicalRegister - return list(self.condition[0]) + return list(condition_resources(self.condition).clbits) @property def name(self): @@ -607,3 +669,13 @@ def num_clbits(self): def num_clbits(self, num_clbits): """Set num_clbits.""" self._num_clbits = num_clbits + + def _compare_parameters(self, other): + for x, y in zip(self.params, other.params): + try: + if not math.isclose(x, y, rel_tol=0, abs_tol=1e-10): + return False + except TypeError: + if x != y: + return False + return True diff --git a/qdao/qiskit/tools/__init__.py b/qdao/qiskit/tools/__init__.py deleted file mode 100644 index 3a2d4d5..0000000 --- a/qdao/qiskit/tools/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017, 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. - -""" -Helpful routines -""" -from .pi_check import pi_check diff --git a/qdao/qiskit/tools/pi_check.py b/qdao/qiskit/tools/pi_check.py deleted file mode 100644 index ba5fa06..0000000 --- a/qdao/qiskit/tools/pi_check.py +++ /dev/null @@ -1,182 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017, 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=too-many-return-statements - -"""Check if number close to values of PI -""" - -import numpy as np -from qiskit.circuit.parameterexpression import ParameterExpression -from qiskit.exceptions import QiskitError - -MAX_FRAC = 16 -N, D = np.meshgrid(np.arange(1, MAX_FRAC + 1), np.arange(1, MAX_FRAC + 1)) -FRAC_MESH = N / D * np.pi -RECIP_MESH = N / D / np.pi -POW_LIST = np.pi ** np.arange(2, 5) - - -def pi_check(inpt, eps=1e-6, output="text", ndigits=5): - """Computes if a number is close to an integer - fraction or multiple of PI and returns the - corresponding string. - - Args: - inpt (float): Number to check. - eps (float): EPS to check against. - output (str): Options are 'text' (default), - 'latex', 'mpl', and 'qasm'. - ndigits (int): Number of digits to print - if returning raw inpt. - - Returns: - str: string representation of output. - - Raises: - QiskitError: if output is not a valid option. - """ - if isinstance(inpt, ParameterExpression): - param_str = str(inpt) - from sympy import sympify - - expr = sympify(inpt._symbol_expr) - syms = expr.atoms() - for sym in syms: - if not sym.is_number: - continue - pi = pi_check(abs(float(sym)), eps=eps, output=output, ndigits=ndigits) - try: - _ = float(pi) - except (ValueError, TypeError): - from sympy import sstr - - sym_str = sstr(abs(sym), full_prec=False) - param_str = param_str.replace(sym_str, pi) - return param_str - elif isinstance(inpt, str): - return inpt - - def normalize(single_inpt): - if abs(single_inpt) < 1e-14: - return "0" - - if output == "text": - pi = "π" - elif output == "qasm": - pi = "pi" - elif output == "latex": - pi = "\\pi" - elif output == "mpl": - pi = "$\\pi$" - else: - raise QiskitError( - "pi_check parameter output should be text, latex, mpl, or qasm." - ) - - neg_str = "-" if single_inpt < 0 else "" - - # First check is for whole multiples of pi - val = single_inpt / np.pi - if abs(val) >= 1 - eps: - if abs(abs(val) - abs(round(val))) < eps: - val = int(abs(round(val))) - if abs(val) == 1: - str_out = f"{neg_str}{pi}" - else: - if output == "qasm": - str_out = f"{neg_str}{val}*{pi}" - else: - str_out = f"{neg_str}{val}{pi}" - return str_out - - # Second is a check for powers of pi - if abs(single_inpt) > np.pi: - power = np.where(abs(abs(single_inpt) - POW_LIST) < eps) - if power[0].shape[0]: - if output == "qasm": - str_out = "{:.{}g}".format(single_inpt, ndigits) - elif output == "latex": - str_out = f"{neg_str}{pi}^{power[0][0] + 2}" - elif output == "mpl": - str_out = f"{neg_str}{pi}$^{power[0][0] + 2}$" - else: - str_out = f"{neg_str}{pi}**{power[0][0] + 2}" - return str_out - - # Third is a check for a number larger than MAX_FRAC * pi, not a - # multiple or power of pi, since no fractions will exceed MAX_FRAC * pi - if abs(single_inpt) >= (MAX_FRAC * np.pi): - str_out = "{:.{}g}".format(single_inpt, ndigits) - return str_out - - # Fourth check is for fractions for 1*pi in the numer and any - # number in the denom. - val = np.pi / single_inpt - if abs(abs(val) - abs(round(val))) < eps: - val = int(abs(round(val))) - if output == "latex": - str_out = f"\\frac{{{neg_str}{pi}}}{{{val}}}" - else: - str_out = f"{neg_str}{pi}/{val}" - return str_out - - # Fifth check is for fractions where the numer > 1*pi and numer - # is up to MAX_FRAC*pi and denom is up to MAX_FRAC and all - # fractions are reduced. Ex. 15pi/16, 2pi/5, 15pi/2, 16pi/9. - frac = np.where(np.abs(abs(single_inpt) - FRAC_MESH) < eps) - if frac[0].shape[0]: - numer = int(frac[1][0]) + 1 - denom = int(frac[0][0]) + 1 - if output == "latex": - str_out = f"\\frac{{{neg_str}{numer}{pi}}}{{{denom}}}" - elif output == "qasm": - str_out = f"{neg_str}{numer}*{pi}/{denom}" - else: - str_out = f"{neg_str}{numer}{pi}/{denom}" - return str_out - - # Sixth check is for fractions where the numer > 1 and numer - # is up to MAX_FRAC and denom is up to MAX_FRAC*pi and all - # fractions are reduced. Ex. 15/16pi, 2/5pi, 15/2pi, 16/9pi - frac = np.where(np.abs(abs(single_inpt) - RECIP_MESH) < eps) - if frac[0].shape[0]: - numer = int(frac[1][0]) + 1 - denom = int(frac[0][0]) + 1 - if denom == 1 and output != "qasm": - denom = "" - if output == "latex": - str_out = f"\\frac{{{neg_str}{numer}}}{{{denom}{pi}}}" - elif output == "qasm": - str_out = f"{neg_str}{numer}/({denom}*{pi})" - else: - str_out = f"{neg_str}{numer}/{denom}{pi}" - return str_out - - # Nothing found - str_out = "{:.{}g}".format(single_inpt, ndigits) - return str_out - - complex_inpt = complex(inpt) - real, imag = map(normalize, [complex_inpt.real, complex_inpt.imag]) - - jstr = "\\jmath" if output == "latex" else "j" - if real == "0" and imag != "0": - str_out = imag + jstr - elif real != "0" and imag != "0": - op_str = "+" - # Remove + if imag negative except for latex fractions - if complex_inpt.imag < 0 and (output != "latex" or "\\frac" not in imag): - op_str = "" - str_out = f"{real}{op_str}{imag}{jstr}" - else: - str_out = real - return str_out diff --git a/tests/qdao/engine_test.py b/tests/qdao/engine_test.py index c517b46..3acae27 100644 --- a/tests/qdao/engine_test.py +++ b/tests/qdao/engine_test.py @@ -1,20 +1,19 @@ -import copy import os from time import time + import numpy as np +import pytest from qiskit import QuantumCircuit, qiskit - from qiskit.compiler import transpile +from qiskit.qasm2 import dumps from qiskit.quantum_info import Statevector -from qdao.circuit import BaselinePartitioner -from qdao.simulator import QdaoSimObj -from tests.qdao import QdaoBaseTest +from constants import * +from qdao.circuit import BaselinePartitioner from qdao.engine import Engine +from qdao.simulator import QdaoSimObj from qdao.util import retrieve_sv - -from constants import * -import pytest +from tests.qdao import QdaoBaseTest class TestEngine(QdaoBaseTest): @@ -22,54 +21,6 @@ def setup_method(self): if not os.path.exists("qcs"): os.system(f"git clone {QCS_URL} qcs") - @pytest.mark.skip( - reason="turn off automatic detection of this test since it is not necessary" - ) - def test_pre_postprocessing(self): - circ = self.get_qiskit_circ("random", num_qubits=8, depth=20, measure=False) - circ = transpile(circ, self._sv_sim) - - engine = Engine(circuit=circ, num_primary=6, num_local=2) - - sub_circs = engine._part.run(engine._circ) - - sv = engine._sim.run(QdaoSimObj(sub_circs[0].circ)) - engine._postprocess(sub_circs[0], 0, sv) - - obj = engine._preprocess(sub_circs[0], 0) - print(sub_circs[0].circ) - - assert np.array_equal(sv, obj.objs[0]) - - @pytest.mark.skip( - reason="turn off automatic detection of this test since it is not necessary" - ) - def test_run_step(self, nq): - NQ = int(nq) - NP = NQ - NL = NQ - 10 - - circ = self.get_qiskit_circ("random", num_qubits=NQ, depth=9, measure=False) - circ = transpile(circ, self._sv_sim) - circ_sv = copy.deepcopy(circ) - circ_sv.save_state() - st = time() - job = self._sv_sim.run(circ_sv) - sv0 = job.result().get_statevector() - print("Qiskit runs: {}".format(time() - st)) - - engine = Engine(circuit=circ, num_primary=NP, num_local=NL, is_parallel=True) - sub_circs = engine._part.run(circ) - engine._initialize() - simobj = engine._preprocess(sub_circs[0], 0) - st = time() - print("Start running simulation") - sv1 = engine._sim.run(simobj) - print("Qiskit runs: {}".format(time() - st)) - print("sub-circs num: {}".format(len(sub_circs))) - - assert sv0.equiv(sv1) - def run_qiskit_diff_test( self, circ: QuantumCircuit, @@ -156,48 +107,6 @@ def test_run_qiskit_any_qasm( device=device, ) - @pytest.mark.skip( - reason="turn off automatic detection of this test since it is not necessary" - ) - def test_run_qiskit_random_basic(self, nq, np, nl, mode): - NQ, NP, NL = self.get_qdao_params(nq, np, nl) - - D = NQ - 3 # depth - MAX_OP = 2 - - print("\n::::::::::::::::::Config::::::::::::::::::\n") - print("NQ::\t{}".format(NQ)) - print("NP::\t{}".format(NP)) - print("NL::\t{}".format(NL)) - print("D::\t{}".format(D)) - print("\n::::::::::::::::::Config::::::::::::::::::\n") - - from qdao.qiskit.utils import random_circuit - - circ_name = "_".join( - ["random", str(NQ), str(D), "max_operands", str(MAX_OP), "gen.qasm"] - ) - if not os.path.exists(QCS_BENCHMARKS_DIR + circ_name): - circ = random_circuit(NQ, D, max_operands=MAX_OP, measure=False) - circ = transpile(circ, self._sv_sim) - with open(QCS_BENCHMARKS_DIR + circ_name, "w") as f: - f.write(circ.qasm()) - else: - print( - "\n:::Reusing existing bench:::::{}::::::::\n".format( - QCS_BENCHMARKS_DIR + circ_name - ) - ) - circ = qiskit.circuit.QuantumCircuit.from_qasm_file( - QCS_BENCHMARKS_DIR + circ_name - ) - - circ = transpile(circ, self._sv_sim) - self.run_qiskit_diff_test(circ, NQ, NP, NL, mode) - - # @pytest.mark.skip( - # reason="turn off automatic detection of this test since it is not necessary" - # ) def test_run_qiskit_random(self, nq): NQ = int(nq) NP = NQ - 2 @@ -220,114 +129,6 @@ def test_run_qiskit_random(self, nq): print("Qiskit runs: {}".format(time() - st)) assert Statevector(sv).equiv(Statevector(sv_org)) - @pytest.mark.skip( - reason="turn off automatic detection of this test since it is not necessary" - ) - def test_run_quafu_single_random_no_init(self, nq): - NQ = int(nq) - - circ = self.get_qiskit_circ("random", num_qubits=NQ, depth=9, measure=False) - circ = transpile(circ, self._sv_sim) - from quafu.circuits.quantum_circuit import QuantumCircuit - - quafu_circ = QuantumCircuit(1) - quafu_circ.from_openqasm(circ.qasm()) - # quafu_circ = qasm_to_circuit(circ.qasm()) - - from quafu.simulators.simulator import simulate - - st = time() - sv_wo_init = simulate(quafu_circ, output="state_vector").get_statevector() - print("Quafu runs: {}".format(time() - st)) - - init_sv = np.zeros(1 << NQ, dtype=np.complex128) - init_sv[0] = 1 - sv_with_init = simulate( - quafu_circ, psi=init_sv, output="state_vector" - ).get_statevector() - - print(sv_wo_init) - print(sv_with_init) - assert Statevector(sv_wo_init).equiv(Statevector(sv_with_init)) - - @pytest.mark.skip( - reason="turn off automatic detection of this test since it is not necessary" - ) - def test_run_quafu_random_single(self, nq): - NQ = int(nq) - - circ = self.get_qiskit_circ("random", num_qubits=NQ, depth=9, measure=False) - circ = transpile(circ, self._sv_sim) - from quafu.circuits.quantum_circuit import QuantumCircuit - - quafu_circ = QuantumCircuit(1) - quafu_circ.from_openqasm(circ.qasm()) - # quafu_circ = qasm_to_circuit(circ.qasm()) - - from quafu.simulators.simulator import simulate - - st = time() - init_sv = np.zeros(1 << NQ, dtype=np.complex128) - init_sv[0] = 1 - sv_org = simulate( - quafu_circ, psi=init_sv, output="state_vector" - ).get_statevector() - print("Quafu runs: {}".format(time() - st)) - - @pytest.mark.skip( - reason="turn off automatic detection of this test since it is not necessary" - ) - def test_run_quafu_random_step_by_step(self, nq): - NQ = int(nq) - NP = NQ - 2 - NL = NQ - 4 - D = NQ - 3 # depth - - from qdao.qiskit.utils import random_circuit - - circ = random_circuit(NQ, D, max_operands=2, measure=False) - circ = transpile(circ, self._sv_sim) - - from quafu.circuits.quantum_circuit import QuantumCircuit - - quafu_circ = QuantumCircuit(1) - quafu_circ.from_openqasm(circ.qasm()) - # quafu_circ = qasm_to_circuit(circ.qasm()) - print("\nOriginal Circ") - quafu_circ.draw_circuit() - - engine = Engine( - circuit=quafu_circ, - num_primary=NP, - num_local=NL, - backend="quafu", - is_parallel=False, - ) - - sub_circs = engine._part.run(engine._circ) - engine._initialize() - - num_acc_ops = 0 - input_sv = np.zeros(1 << NQ, dtype=np.complex128) - input_sv[0] = 1 - for sub_circ in sub_circs: - for ichunk in range(engine._num_chunks): - simobj = engine._preprocess(sub_circ, ichunk) - sv = engine._sim.run(simobj) - engine._postprocess(sub_circ, ichunk, sv) - - sv_expected = retrieve_sv(NQ, num_local=NL) - print("\n:::::debugging sub-circ::::\n") - sub_circ.circ.draw_circuit() - self.debug_run_quafu_circ( - quafu_circ, - input_sv, - sv_expected, - (num_acc_ops, num_acc_ops + len(sub_circ.circ.gates)), - ) - num_acc_ops += len(sub_circ.circ.gates) - input_sv = sv_expected - def run_quafu_diff_test( self, circ: QuantumCircuit, @@ -351,8 +152,8 @@ def run_quafu_diff_test( from quafu.circuits.quantum_circuit import QuantumCircuit quafu_circ = QuantumCircuit(1) - quafu_circ.from_openqasm(circ.qasm()) - # quafu_circ = qasm_to_circuit(circ.qasm()) + quafu_circ.from_openqasm(dumps(circ)) + # quafu_circ = qasm_to_circuit(dumps(circ)) # print("\nOriginal Circ") # quafu_circ.draw_circuit() @@ -412,55 +213,6 @@ def get_qdao_params(self, nq, np, nl): return NQ, NP, NL - @pytest.mark.skip( - reason="turn off automatic detection of this test since it is not necessary" - ) - def test_run_quafu_random_basic(self, nq, np, nl, mode, parallel, diff): - """ - Basic test to run random circuits and - compare performance between - 1. Qdao on top of quafu - 2. Quafu - """ - NQ, NP, NL = self.get_qdao_params(nq, np, nl) - parallel = True if int(parallel) == 1 else False - diff = True if int(diff) == 1 else False - - D = NQ - 3 # depth - # D = 2 # depth - MAX_OP = 2 - - print("\n::::::::::::::::::Config::::::::::::::::::\n") - print("NQ::\t{}".format(NQ)) - print("NP::\t{}".format(NP)) - print("NL::\t{}".format(NL)) - print("D::\t{}".format(D)) - print("\n::::::::::::::::::Config::::::::::::::::::\n") - - from qdao.qiskit.utils import random_circuit - - circ_name = "_".join( - ["random", str(NQ), str(D), "max_operands", str(MAX_OP), "gen.qasm"] - ) - if not os.path.exists(QCS_BENCHMARKS_DIR + circ_name): - circ = random_circuit(NQ, D, max_operands=MAX_OP, measure=False) - circ = transpile(circ, self._sv_sim) - with open(QCS_BENCHMARKS_DIR + circ_name, "w") as f: - f.write(circ.qasm()) - else: - print( - "\n:::Reusing existing bench:::::{}::::::::\n".format( - QCS_BENCHMARKS_DIR + circ_name - ) - ) - circ = qiskit.circuit.QuantumCircuit.from_qasm_file( - QCS_BENCHMARKS_DIR + circ_name - ) - - self.run_quafu_diff_test( - circ, NQ, NP, NL, mode=mode, is_parallel=parallel, is_diff=diff - ) - def test_run_quafu_any_qasm(self, nq, np, nl, mode, qasm, parallel, diff): """ Basic test to run random circuits and @@ -490,36 +242,6 @@ def test_run_quafu_any_qasm(self, nq, np, nl, mode, qasm, parallel, diff): circ, NQ, NP, NL, mode=mode, is_parallel=parallel, is_diff=diff ) - @pytest.mark.skip( - reason="turn off automatic detection of this test since it is not necessary" - ) - def test_run_quafu_qasm_basic(self, bench, nq): - """ - Basic test to run random circuits and - compare performance between - 1. Qdao on top of quafu - 2. Quafu - """ - NQ = int(nq) - NP = NQ - 2 - NL = NQ - 10 - print("\n::::::::::::::::::Config::::::::::::::::::\n") - print("NQ::\t{}".format(NQ)) - print("NP::\t{}".format(NP)) - print("NL::\t{}".format(NL)) - print("bench::\t".format(bench)) - print("\n::::::::::::::::::Config::::::::::::::::::\n") - - qasm_path = QCS_BENCHMARKS_DIR + bench + "-" + str(NQ) + ".qasm" - if not os.path.exists(qasm_path): - raise FileNotFoundError("qasm does not exists: ".format(qasm_path)) - - circ = qiskit.circuit.QuantumCircuit.from_qasm_file(qasm_path) - self.run_quafu_diff_test(circ, NQ, NP, NL) - - # @pytest.mark.skip( - # reason="turn off automatic detection of this test since it is not necessary" - # ) def test_run_quafu_random_single_vs_qiskit_with_init(self, nq): NQ = int(nq) @@ -531,8 +253,8 @@ def test_run_quafu_random_single_vs_qiskit_with_init(self, nq): from quafu.circuits.quantum_circuit import QuantumCircuit quafu_circ = QuantumCircuit(1) - quafu_circ.from_openqasm(circ.qasm()) - # quafu_circ = qasm_to_circuit(circ.qasm()) + quafu_circ.from_openqasm(dumps(circ)) + # quafu_circ = qasm_to_circuit(dumps(circ)) from quafu.simulators.simulator import simulate @@ -561,40 +283,3 @@ def test_run_quafu_random_single_vs_qiskit_with_init(self, nq): # print(sv_qiskit.data) print("Qiskit runs: {}".format(time() - st)) - - @pytest.mark.skip( - reason="turn off automatic detection of this test since it is not necessary" - ) - def test_run_quafu_bench(self, bench): - qasm_path = QASMBENCH_LARGE_DIR + "/" + bench + "/" + bench + ".qasm" - quafu_circ = self.get_quafu_circ_from_qasm(qasm_path) - NQ = quafu_circ.num - NP = NQ - 2 - NL = NQ - 10 - - engine = Engine( - circuit=quafu_circ, - num_primary=NP, - num_local=NL, - backend="quafu", - is_parallel=True, - ) - engine.run() - sv = retrieve_sv(NQ, num_local=NL) - - init_sv = np.zeros(1 << NQ, dtype=np.complex128) - init_sv[0] = 1 - from quafu.simulators.simulator import simulate - - sv_org = simulate( - quafu_circ, psi=init_sv, output="state_vector" - ).get_statevector() - - assert Statevector(sv).equiv(Statevector(sv_org)) - - qiskit_circ = self.get_qiskit_circ("qasm", qasm_path=qasm_path) - qiskit_circ.remove_final_measurements() - qiskit_circ.save_state() - sv_qiskit = self._sv_sim.run(qiskit_circ).result().get_statevector().data - - assert Statevector(sv_qiskit).equiv(Statevector(sv_org))