Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions qiskit_ionq/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,10 @@ class IonQBackendError(IonQError):
"""Errors generated from improper usage of IonQBackend objects."""


class IonQBackendNotSupportedError(IonQError):
"""The requested backend is not supported."""


class IonQJobError(IonQError, JobError):
"""Errors generated from improper usage of IonQJob objects."""

Expand Down Expand Up @@ -248,6 +252,7 @@ class IonQPauliExponentialError(IonQError):
"IonQClientError",
"IonQAPIError",
"IonQBackendError",
"IonQBackendNotSupportedError",
"IonQJobError",
"IonQGateError",
"IonQMidCircuitMeasurementError",
Expand Down
29 changes: 8 additions & 21 deletions qiskit_ionq/ionq_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
from qiskit.providers.models.backendstatus import BackendStatus
from qiskit.providers import Options

from . import exceptions, ionq_client, ionq_job, ionq_equivalence_library
from . import exceptions, ionq_client, ionq_job
from .helpers import GATESET_MAP, get_n_qubits

if TYPE_CHECKING:
Expand Down Expand Up @@ -140,8 +140,6 @@ class IonQBackend(Backend):
_client = None

def __init__(self, *args, **kwargs) -> None:
# Add IonQ equivalences
ionq_equivalence_library.add_equivalences()
super().__init__(*args, **kwargs)

@classmethod
Expand Down Expand Up @@ -342,19 +340,6 @@ def __ne__(self, other) -> bool:
class IonQSimulatorBackend(IonQBackend):
"""
IonQ Backend for running simulated jobs.


.. ATTENTION::

When noise_model ideal is specified, the maximum shot-count for a state vector sim is
always ``1``.

.. ATTENTION::

When noise_model ideal is specified, calling
:meth:`get_counts <qiskit_ionq.ionq_job.IonQJob.get_counts>`
on a job processed by this backend will return counts expressed as
probabilites, rather than a multiple of shots.
"""

@classmethod
Expand All @@ -372,18 +357,15 @@ def _default_options(cls) -> Options:
def run(self, circuit: QuantumCircuit, **kwargs) -> ionq_job.IonQJob:
"""Create and run a job on IonQ's Simulator Backend.

.. WARNING:

The maximum shot-count for a state vector sim is always ``1``.
As a result, the ``shots`` keyword argument in this method is ignored.

Args:
circuit (:class:`QuantumCircuit <qiskit.circuit.QuantumCircuit>`):
A Qiskit QuantumCircuit object.

Returns:
IonQJob: A reference to the job that was submitted.
"""
if "noise_model" not in kwargs:
kwargs["noise_model"] = self.options.noise_model
return super().run(circuit, **kwargs)

def calibration(self) -> None:
Expand All @@ -402,6 +384,7 @@ def __init__(
provider,
name: str = "simulator",
gateset: Literal["qis", "native"] = "qis",
noise_model="ideal",
):
"""Base class for interfacing with an IonQ backend"""
self._gateset = gateset
Expand Down Expand Up @@ -466,6 +449,10 @@ def __init__(
}
)
super().__init__(configuration=config, provider=provider)
# TODO: passing 'noise_model' to super().__init__ method is the
# proper method to handle this but it fails because Options has
# no field named data, perhaps this will be fixed in BackendV2
self._options.update_options(noise_model=noise_model)

def with_name(self, name, **kwargs) -> IonQSimulatorBackend:
"""Helper method that returns this backend with a more specific target system."""
Expand Down
95 changes: 89 additions & 6 deletions qiskit_ionq/ionq_equivalence_library.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@
from qiskit.circuit.equivalence_library import SessionEquivalenceLibrary
from qiskit.circuit import QuantumRegister, QuantumCircuit, Parameter
from qiskit.circuit.library import CXGate, RXGate, RZGate, UGate, XGate, CU3Gate
from .ionq_gates import GPIGate, GPI2Gate, MSGate

from qiskit_ionq.exceptions import IonQBackendNotSupportedError
from .ionq_gates import GPIGate, GPI2Gate, MSGate, ZZGate


def u_gate_equivalence() -> None:
Expand All @@ -57,16 +59,47 @@ def u_gate_equivalence() -> None:
)


def cx_gate_equivalence() -> None:
"""Add CX gate equivalence to the SessionEquivalenceLibrary."""
def cx_gate_equivalence_ms() -> None:
"""Add MS gate based CX gate equivalence to the SessionEquivalenceLibrary."""
q = QuantumRegister(2, "q")
cx_gate = QuantumCircuit(q)
cx_gate.append(GPI2Gate(1 / 4), [0])
cx_gate.append(MSGate(0, 0), [0, 1])
cx_gate.append(GPI2Gate(1 / 2), [0])
cx_gate.append(GPI2Gate(1 / 2), [1])
cx_gate.append(GPI2Gate(-1 / 4), [0])
SessionEquivalenceLibrary.add_equivalence(CXGate(), cx_gate)
if SessionEquivalenceLibrary.has_entry(CXGate()):
SessionEquivalenceLibrary.set_entry(CXGate(), [cx_gate])
else:
SessionEquivalenceLibrary.add_equivalence(CXGate(), cx_gate)


def cx_gate_equivalence_zz() -> None:
"""Add ZZ gate based CX gate equivalence to the SessionEquivalenceLibrary.
q_0: ────■────Sdag──────
│ZZ
q_1: H───■────Sdag────H─
"""
q = QuantumRegister(2, "q")
cx_gate = QuantumCircuit(q)
# H
cx_gate.append(GPI2Gate(0), [1])
cx_gate.append(GPIGate(-0.125), [1])
cx_gate.append(GPI2Gate(0.5), [1])
# ZZ
cx_gate.append(ZZGate(), [0, 1])
# Sdag
cx_gate.append(GPI2Gate(0.75), [0])
cx_gate.append(GPIGate(0.125), [0])
cx_gate.append(GPI2Gate(0.5), [0])
# H * Sdag
cx_gate.append(GPI2Gate(1.25), [1])
cx_gate.append(GPIGate(0.5), [1])
cx_gate.append(GPI2Gate(0.5), [1])
if SessionEquivalenceLibrary.has_entry(CXGate()):
SessionEquivalenceLibrary.set_entry(CXGate(), [cx_gate])
else:
SessionEquivalenceLibrary.add_equivalence(CXGate(), cx_gate)


# Below are the rules needed for Aer simulator to simulate circuits containing IonQ native gates
Expand Down Expand Up @@ -127,10 +160,60 @@ def ms_gate_equivalence() -> None:
)


def add_equivalences() -> None:
def zz_gate_equivalence() -> None:
"""Add ZZ gate equivalence to the SessionEquivalenceLibrary."""
q = QuantumRegister(2, "q")
zz_gate = QuantumCircuit(q)
zz_gate.h(1)
zz_gate.append(CXGate(), [0, 1])
zz_gate.s(0)
zz_gate.h(1)
zz_gate.s(1)
SessionEquivalenceLibrary.add_equivalence(ZZGate(), zz_gate)


def add_equivalences(backend_name, noise_model=None) -> None:
"""Add IonQ gate equivalences to the SessionEquivalenceLibrary."""
u_gate_equivalence()
cx_gate_equivalence()
if backend_name in (
"ionq_mock_backend",
"ionq_qpu",
"ionq_qpu.harmony",
"ionq_qpu.aria-1",
"ionq_qpu.aria-2",
):
cx_gate_equivalence_ms()
elif backend_name in (
"ionq_qpu.forte-1",
"ionq_qpu.forte-enterprise-1",
"ionq_qpu.forte-enterprise-2",
):
cx_gate_equivalence_zz()
elif backend_name == "ionq_simulator":
if noise_model is None or noise_model in [
"harmony",
"harmony-1",
"harmony-2",
"aria-1",
"aria-2",
"ideal",
"ideal-sampled",
]:
cx_gate_equivalence_ms()
elif noise_model in ["forte-1", "forte-enterprise-1", "forte-enterprise-2"]:
cx_gate_equivalence_zz()
else:
raise IonQBackendNotSupportedError(
f"The backend with name {backend_name} is not supported. "
"The following backends names are supported: simulator or ionq_simulator "
"(with noise models: ideal as default, ideal-sampled, aria-1, aria-2, forte-1, "
"forte-enterprise-1, forte-enterprise-2, and legacy harmony, harmony-1, harmony-2) "
"qpu.aria-1 or ionq_qpu.aria-1, qpu.aria-2 or ionq_qpu.aria-2, "
"qpu.forte-1 or ionq_qpu.forte-1, "
"qpu.forte-enterprise-1 or ionq_qpu.forte-enterprise-1, "
"qpu.forte-enterprise-2 or ionq_qpu.forte-enterprise-2."
)
gpi_gate_equivalence()
gpi2_gate_equivalence()
ms_gate_equivalence()
zz_gate_equivalence()
14 changes: 8 additions & 6 deletions qiskit_ionq/ionq_gates.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,19 +168,21 @@ class ZZGate(Gate):
\end{pmatrix}
"""

def __init__(self, theta: ParameterValueType, label: Optional[str] = None):
def __init__(
self, theta: Optional[ParameterValueType] = 0.25, label: Optional[str] = None
):
"""Create new ZZ gate."""
super().__init__("zz", 2, [theta], label=label)

def __array__(self, dtype=None) -> np.ndarray:
"""Return a numpy array for the ZZ gate."""
itheta2 = 1j * float(self.params[0]) * math.pi
i_theta_over_2 = 1j * float(self.params[0]) * math.pi
return np.array(
[
[np.exp(-itheta2), 0, 0, 0],
[0, np.exp(itheta2), 0, 0],
[0, 0, np.exp(itheta2), 0],
[0, 0, 0, np.exp(-itheta2)],
[np.exp(-i_theta_over_2), 0, 0, 0],
[0, np.exp(i_theta_over_2), 0, 0],
[0, 0, np.exp(i_theta_over_2), 0],
[0, 0, 0, np.exp(-i_theta_over_2)],
],
dtype=dtype,
)
2 changes: 2 additions & 0 deletions qiskit_ionq/ionq_optimizer_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
GPI2TwiceIsGPI,
CompactMoreThanThreeSingleQubitGates,
CommuteGPIsThroughMS,
CommuteGPITimesGPIThroughZZ,
)


Expand Down Expand Up @@ -163,5 +164,6 @@ def pass_manager(
custom_pass_manager.append(CancelGPIAdjoint())
custom_pass_manager.append(GPI2TwiceIsGPI())
custom_pass_manager.append(CommuteGPIsThroughMS())
custom_pass_manager.append(CommuteGPITimesGPIThroughZZ())
custom_pass_manager.append(CompactMoreThanThreeSingleQubitGates())
return custom_pass_manager
20 changes: 19 additions & 1 deletion qiskit_ionq/ionq_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@

from qiskit.providers.exceptions import QiskitBackendNotFoundError
from qiskit.providers.providerutils import filter_backends

from qiskit_ionq import ionq_equivalence_library
from .helpers import resolve_credentials

from . import ionq_backend
Expand Down Expand Up @@ -87,13 +89,29 @@ def get_backend(
more than one backend matches the filtering criteria.
"""
name = "ionq_" + name if not name.startswith("ionq_") else name

noise_model = None
if "noise_model" in kwargs:
noise_model = kwargs.pop("noise_model", None)

backends = self.backends(name, **kwargs)
if len(backends) > 1:
raise QiskitBackendNotFoundError("More than one backend matches criteria.")
if not backends:
raise QiskitBackendNotFoundError("No backend matches criteria.")

return backends[0].with_name(name, gateset=gateset)
if noise_model:
ionq_equivalence_library.add_equivalences(name, noise_model=noise_model)
else:
ionq_equivalence_library.add_equivalences(name)

if noise_model:
backend = backends[0].with_name(
name, gateset=gateset, noise_model=noise_model
)
else:
backend = backends[0].with_name(name, gateset=gateset)
return backend


class BackendService:
Expand Down
70 changes: 62 additions & 8 deletions qiskit_ionq/rewrite_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,15 +259,10 @@ def run(self, dag: DAGCircuit) -> DAGCircuit:
for qreg in dag.qregs.values():
sub_dag.add_qreg(qreg)

# map the ops to the qubits in the sub-DAG
ms_qubits = [next_node.qargs[0], next_node.qargs[1]]
gpis_qubit = [node.qargs[0]]
sub_dag.apply_operation_back(next_node.op, next_node.qargs)
sub_dag.apply_operation_back(node.op, node.qargs)

sub_dag.apply_operation_back(next_node.op, ms_qubits)
sub_dag.apply_operation_back(node.op, gpis_qubit)

wire_mapping = {qubit: qubit for qubit in ms_qubits}
wire_mapping[node.qargs[0]] = node.qargs[0]
wire_mapping = {qubit: qubit for qubit in next_node.qargs}

dag.substitute_node_with_dag(
next_node, sub_dag, wires=wire_mapping
Expand All @@ -279,3 +274,62 @@ def run(self, dag: DAGCircuit) -> DAGCircuit:
dag.remove_op_node(node)

return dag


class CommuteGPITimesGPIThroughZZ(TransformationPass):
"""GPI(theta) * GPI(theta') on either qubit commutes with ZZ"""

def run(self, dag: DAGCircuit) -> DAGCircuit:
nodes_to_remove = set()

for node in dag.topological_op_nodes():
if node in nodes_to_remove or node.op.name != "gpi":
continue
successors = [
succ for succ in dag.successors(node) if isinstance(succ, DAGOpNode)
]

gate_pattern_found = False
for next_node in successors:
if gate_pattern_found:
break
if next_node in nodes_to_remove or next_node.op.name != "gpi":
continue

if node.qargs[0] == next_node.qargs[0]:
next_node_successors = [
succ
for succ in dag.successors(next_node)
if isinstance(succ, DAGOpNode)
]
for next_next_node in next_node_successors:
if (
next_next_node.op.name == "zz"
and next_node.qargs[0] in next_next_node.qargs
):
sub_dag = DAGCircuit()
for qreg in dag.qregs.values():
sub_dag.add_qreg(qreg)

sub_dag.apply_operation_back(
next_next_node.op, next_next_node.qargs
)
sub_dag.apply_operation_back(node.op, node.qargs)
sub_dag.apply_operation_back(next_node.op, next_node.qargs)

wire_mapping = {
qubit: qubit for qubit in next_next_node.qargs
}

dag.substitute_node_with_dag(
next_next_node, sub_dag, wires=wire_mapping
)
nodes_to_remove.add(node)
nodes_to_remove.add(next_node)
gate_pattern_found = True
break

for node in nodes_to_remove:
dag.remove_op_node(node)

return dag
Loading