Skip to content

Commit e187227

Browse files
committed
temporary vendor qiskit-ionq
1 parent f96ce75 commit e187227

File tree

3 files changed

+495
-6
lines changed

3 files changed

+495
-6
lines changed

azure-quantum/azure/quantum/qiskit/backends/ionq.py

Lines changed: 354 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,362 @@
1919
)
2020
from qiskit.providers import Options
2121

22-
from qiskit_ionq.helpers import (
23-
GATESET_MAP,
24-
qiskit_circ_to_ionq_circ,
22+
23+
#####################################################################
24+
#####################################################################
25+
#####################################################################
26+
# Vendored from qiskit_ionq
27+
28+
29+
# the qiskit gates that the IonQ backend can serialize to our IR
30+
# not the actual hardware basis gates for the system — we do our own transpilation pass.
31+
# also not an exact/complete list of the gates IonQ's backend takes
32+
# by name — please refer to IonQ docs for that.
33+
#
34+
# Some of these gates may be deprecated or removed in qiskit 1.0
35+
ionq_basis_gates = [
36+
"ccx",
37+
"ch",
38+
"cnot",
39+
"cp",
40+
"crx",
41+
"cry",
42+
"crz",
43+
"csx",
44+
"cx",
45+
"cy",
46+
"cz",
47+
"h",
48+
"i",
49+
"id",
50+
"mcp",
51+
"mcphase",
52+
"mct",
53+
"mcx",
54+
"measure",
55+
"p",
56+
"rx",
57+
"rxx",
58+
"ry",
59+
"ryy",
60+
"rz",
61+
"rzz",
62+
"s",
63+
"sdg",
64+
"swap",
65+
"sx",
66+
"sxdg",
67+
"t",
68+
"tdg",
69+
"toffoli",
70+
"x",
71+
"y",
72+
"z",
73+
"PauliEvolution",
74+
]
75+
76+
# https://ionq.com/docs/getting-started-with-native-gates
77+
ionq_native_basis_gates = [
78+
"gpi",
79+
"gpi2",
80+
"ms", # Pairwise MS gate
81+
"zz", # ZZ gate
82+
]
83+
84+
# Each language corresponds to a different set of basis gates.
85+
GATESET_MAP = {
86+
"qis": ionq_basis_gates,
87+
"native": ionq_native_basis_gates,
88+
}
89+
90+
ionq_api_aliases = { # todo fix alias bug
91+
"cp": "cz",
92+
"csx": "cv",
93+
"mcphase": "cz",
94+
"ccx": "cx", # just one C for all mcx
95+
"mcx": "cx", # just one C for all mcx
96+
"tdg": "ti",
97+
"p": "z",
98+
"PauliEvolution": "pauliexp",
99+
"rxx": "xx",
100+
"ryy": "yy",
101+
"rzz": "zz",
102+
"sdg": "si",
103+
"sx": "v",
104+
"sxdg": "vi",
105+
}
106+
107+
from qiskit.circuit import (
108+
controlledgate as q_cgates,
109+
QuantumCircuit,
25110
)
26111

112+
from typing import Literal, Any
113+
114+
from qiskit.exceptions import QiskitError
115+
116+
class IonQError(QiskitError):
117+
"""Base class for errors raised by an IonQProvider."""
118+
119+
def __str__(self) -> str:
120+
return f"{self.__class__.__name__}({self.message!r})"
121+
122+
def __repr__(self) -> str:
123+
return repr(str(self))
124+
125+
class JobError(QiskitError):
126+
"""Base class for errors raised by Jobs."""
127+
128+
pass
129+
130+
class IonQGateError(IonQError, JobError):
131+
"""Errors generated from invalid gate defs
132+
133+
Attributes:
134+
gate_name: The name of the gate which caused this error.
135+
"""
136+
137+
def __init__(self, gate_name: str, gateset: Literal["qis", "native"]):
138+
self.gate_name = gate_name
139+
self.gateset = gateset
140+
super().__init__(
141+
(
142+
f"gate '{gate_name}' is not supported on the '{gateset}' IonQ backends. "
143+
"Please use the qiskit.transpile method, manually rewrite to remove the gate, "
144+
"or change the gateset selection as appropriate."
145+
)
146+
)
147+
148+
def __repr__(self):
149+
return f"{self.__class__.__name__}(gate_name={self.gate_name!r}, gateset={self.gateset!r})"
150+
151+
class IonQMidCircuitMeasurementError(IonQError, JobError):
152+
"""Errors generated from attempting mid-circuit measurement, which is not supported.
153+
Measurement must come after all instructions.
154+
155+
Attributes:
156+
qubit_index: The qubit index to be measured mid-circuit
157+
"""
158+
159+
def __init__(self, qubit_index: int, gate_name: str):
160+
self.qubit_index = qubit_index
161+
self.gate_name = gate_name
162+
super().__init__(
163+
f"Attempting to put '{gate_name}' after a measurement on qubit {qubit_index}. "
164+
"Mid-circuit measurement is not supported."
165+
)
166+
167+
def __str__(self):
168+
kwargs = f"qubit_index={self.qubit_index!r}, gate_name={self.gate_name!r}"
169+
return f"{self.__class__.__name__}({kwargs})"
170+
171+
class IonQPauliExponentialError(IonQError):
172+
"""Errors generated from improper usage of Pauli exponentials."""
173+
174+
def paulis_commute(pauli_terms: list[str]) -> bool:
175+
"""Check if a list of Pauli terms commute.
176+
177+
Args:
178+
pauli_terms (list): A list of Pauli terms.
179+
180+
Returns:
181+
bool: Whether the Pauli terms commute.
182+
"""
183+
for i, term in enumerate(pauli_terms):
184+
for other_term in pauli_terms[i:]:
185+
assert len(term) == len(other_term)
186+
anticommutation_parity = 0
187+
for index, char in enumerate(term):
188+
other_char = other_term[index]
189+
if "I" not in (char, other_char):
190+
if char != other_char:
191+
anticommutation_parity += 1
192+
if anticommutation_parity % 2 == 1:
193+
return False
194+
return True
195+
196+
def qiskit_circ_to_ionq_circ(
197+
input_circuit: QuantumCircuit,
198+
gateset: Literal["qis", "native"] = "qis",
199+
ionq_compiler_synthesis: bool = False,
200+
):
201+
"""Build a circuit in IonQ's instruction format from qiskit instructions.
202+
203+
.. ATTENTION:: This function ignores the following compiler directives:
204+
* ``barrier``
205+
206+
Parameters:
207+
input_circuit (:class:`qiskit.circuit.QuantumCircuit`): A Qiskit quantum circuit.
208+
gateset (string): Set of gates to target. It can be QIS (required transpilation pass in
209+
IonQ backend, which is sent standard gates) or native (only IonQ native gates are
210+
allowed, in the future we may provide transpilation to these gates in Qiskit).
211+
ionq_compiler_synthesis (bool): Whether to opt-in to IonQ compiler's intelligent
212+
trotterization.
213+
214+
Raises:
215+
IonQGateError: If an unsupported instruction is supplied.
216+
IonQMidCircuitMeasurementError: If a mid-circuit measurement is detected.
217+
IonQPauliExponentialError: If non-commuting PauliExponentials are found without
218+
the appropriate flag.
219+
220+
Returns:
221+
list[dict]: A list of instructions in a converted dict format.
222+
int: The number of measurements.
223+
dict: The measurement map from qubit number to classical bit number.
224+
"""
225+
compiler_directives = ["barrier"]
226+
output_circuit = []
227+
num_meas = 0
228+
meas_map = [None] * len(input_circuit.clbits)
229+
for inst in input_circuit.data:
230+
instruction, qargs, cargs = inst.operation, inst.qubits, inst.clbits
231+
232+
# Don't process compiler directives.
233+
instruction_name = instruction.name
234+
if instruction_name in compiler_directives:
235+
continue
236+
237+
# Don't process measurement instructions.
238+
if instruction_name == "measure":
239+
meas_map[input_circuit.clbits.index(cargs[0])] = input_circuit.qubits.index(
240+
qargs[0]
241+
)
242+
num_meas += 1
243+
continue
244+
245+
# serialized identity gate is a no-op
246+
if instruction_name == "id":
247+
continue
248+
249+
# Raise out for instructions we don't support.
250+
if instruction_name not in GATESET_MAP[gateset]:
251+
raise IonQGateError(instruction_name, gateset)
252+
253+
# Process the instruction and convert.
254+
rotation: dict[str, Any] = {}
255+
if len(instruction.params) > 0:
256+
if gateset == "qis" or (
257+
len(instruction.params) == 1 and instruction_name != "zz"
258+
):
259+
# The float is here to cast Qiskit ParameterExpressions to numbers
260+
rotation = {
261+
("rotation" if gateset == "qis" else "phase"): float(
262+
instruction.params[0]
263+
)
264+
}
265+
if instruction_name == "PauliEvolution":
266+
# rename rotation to time
267+
rotation["time"] = rotation.pop("rotation")
268+
elif instruction_name in {"zz"}:
269+
rotation = {"angle": instruction.params[0]}
270+
else:
271+
rotation = {
272+
"phases": [float(t) for t in instruction.params[:2]],
273+
"angle": instruction.params[2],
274+
}
275+
276+
# Default conversion is simple, just gate & target(s).
277+
targets = [input_circuit.qubits.index(qargs[0])]
278+
if instruction_name in {"ms", "zz"}:
279+
targets.append(input_circuit.qubits.index(qargs[1]))
280+
281+
converted = (
282+
{"gate": instruction_name, "targets": targets}
283+
if instruction_name not in {"gpi", "gpi2"}
284+
else {
285+
"gate": instruction_name,
286+
"target": targets[0],
287+
}
288+
)
289+
290+
# re-alias certain names
291+
if instruction_name in ionq_api_aliases:
292+
instruction_name = ionq_api_aliases[instruction_name]
293+
converted["gate"] = instruction_name
294+
295+
# Make sure uncontrolled multi-targets use all qargs.
296+
if instruction.num_qubits > 1 and not hasattr(instruction, "num_ctrl_qubits"):
297+
converted["targets"] = [
298+
input_circuit.qubits.index(qargs[i])
299+
for i in range(instruction.num_qubits)
300+
]
301+
302+
# If this is a controlled gate, make sure to set control qubits.
303+
if isinstance(instruction, q_cgates.ControlledGate):
304+
gate = instruction_name[1:] # trim the leading c
305+
controls = [input_circuit.qubits.index(qargs[0])]
306+
targets = [input_circuit.qubits.index(qargs[1])]
307+
# If this is a multi-control, use more than one qubit.
308+
if instruction.num_ctrl_qubits > 1:
309+
controls = [
310+
input_circuit.qubits.index(qargs[i])
311+
for i in range(instruction.num_ctrl_qubits)
312+
]
313+
targets = [
314+
input_circuit.qubits.index(qargs[instruction.num_ctrl_qubits])
315+
]
316+
if gate == "swap":
317+
# If this is a cswap, we have two targets:
318+
targets = [
319+
input_circuit.qubits.index(qargs[-2]),
320+
input_circuit.qubits.index(qargs[-1]),
321+
]
322+
323+
# Update converted gate values.
324+
converted.update(
325+
{
326+
"gate": gate,
327+
"controls": controls,
328+
"targets": targets,
329+
}
330+
)
331+
332+
if instruction_name == "pauliexp":
333+
imag_coeff = any(coeff.imag for coeff in instruction.operator.coeffs)
334+
assert not imag_coeff, (
335+
"PauliEvolution gate must have real coefficients, "
336+
f"but got {imag_coeff}"
337+
)
338+
terms = [term[0] for term in instruction.operator.to_list()]
339+
if not ionq_compiler_synthesis and not paulis_commute(terms):
340+
raise IonQPauliExponentialError(
341+
f"You have included a PauliEvolutionGate with non-commuting terms: {terms}."
342+
"To decompose it with IonQ hardware-aware synthesis, resubmit with the "
343+
"IONQ_COMPILER_SYNTHESIS flag."
344+
)
345+
targets = [
346+
input_circuit.qubits.index(qargs[i])
347+
for i in range(instruction.num_qubits)
348+
]
349+
coefficients = [coeff.real for coeff in instruction.operator.coeffs]
350+
gate = {
351+
"gate": instruction_name,
352+
"targets": targets,
353+
"terms": terms,
354+
"coefficients": coefficients,
355+
}
356+
converted.update(gate)
357+
358+
# if there's a valid instruction after a measurement,
359+
if num_meas > 0:
360+
# see if any of the involved qubits have been measured,
361+
# and raise if so — no mid-circuit measurement!
362+
controls_and_targets = converted.get("targets", []) + converted.get(
363+
"controls", []
364+
)
365+
if any(i in meas_map for i in controls_and_targets):
366+
raise IonQMidCircuitMeasurementError(
367+
input_circuit.qubits.index(qargs[0]), instruction_name
368+
)
369+
370+
output_circuit.append({**converted, **rotation})
371+
372+
return output_circuit, num_meas, meas_map
373+
374+
#####################################################################
375+
#####################################################################
376+
#####################################################################
377+
27378
if TYPE_CHECKING:
28379
from azure.quantum.qiskit import AzureQuantumProvider
29380

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
qiskit-ionq>=0.5,<0.6
21
qsharp[qiskit]>=1.9.0,<2.0
32
Markdown>=3.4.1,<4.0
43
python-markdown-math>=0.8.0,<1.0

0 commit comments

Comments
 (0)