Skip to content

Commit e7636e0

Browse files
authored
Implementation of support for IonQ pauliexp gates (#7374)
The primary motivation for adding native Pauli‐exponential (pauliexp) support is performance - when we send pauliexp directly to IonQ, our compiler can optimize across a larger context (instead of first Trotterizing), which speeds up development and execution times. Allowing pauliexp to go through "as-is" reduces compilation overhead and yields shorter circuits. pauliexp isn’t yet documented publicly, but IonQ already supports it. The qiskit‐ionq package already includes helpers for Pauli exponentials ([Qiskit‐IonQ helpers](https://qiskit-community.github.io/qiskit-ionq/_modules/qiskit_ionq/helpers.html), so the hardware/runtime will accept it natively.
1 parent 662716b commit e7636e0

File tree

4 files changed

+166
-5
lines changed

4 files changed

+166
-5
lines changed

cirq-ionq/cirq_ionq/ionq_exceptions.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,12 @@ class IonQSerializerMixedGatesetsException(Exception):
5555

5656
def __init__(self, message):
5757
super().__init__(f"Message: '{message}'")
58+
59+
60+
class NotSupportedPauliexpParameters(Exception):
61+
"""An exception that may be thrown when trying to serialize a Cirq
62+
PauliStringPhasorGate to IonQ `pauliexp` gate with unsupported parameters.
63+
"""
64+
65+
def __init__(self, message):
66+
super().__init__(f"Message: '{message}'")

cirq-ionq/cirq_ionq/results.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,13 @@ def to_cirq_result(
267267
rand = cirq.value.parse_random_state(seed)
268268
measurements = {}
269269
values, weights = zip(*list(self.probabilities().items()))
270+
271+
# normalize weights to sum to 1 if within tolerance because
272+
# IonQ's pauliexp gates results are not extremely precise
273+
total = sum(weights)
274+
if np.isclose(total, 1.0, rtol=0, atol=1e-5):
275+
weights = tuple((w / total for w in weights))
276+
270277
indices = rand.choice(
271278
range(len(values)), p=weights, size=override_repetitions or self.repetitions()
272279
)

cirq-ionq/cirq_ionq/serializer.py

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,24 @@
1818

1919
import dataclasses
2020
import json
21+
import math
2122
from typing import Any, Callable, cast, Collection, Iterator, Sequence, TYPE_CHECKING
2223

2324
import numpy as np
2425

2526
import cirq
2627
from cirq.devices import line_qubit
27-
from cirq_ionq.ionq_exceptions import IonQSerializerMixedGatesetsException
28+
from cirq_ionq.ionq_exceptions import (
29+
IonQSerializerMixedGatesetsException,
30+
NotSupportedPauliexpParameters,
31+
)
2832
from cirq_ionq.ionq_native_gates import GPI2Gate, GPIGate, MSGate, ZZGate
2933

3034
if TYPE_CHECKING:
3135
import sympy
3236

37+
from cirq.ops.pauli_string_phasor import PauliStringPhasorGate
38+
3339
_NATIVE_GATES = cirq.Gateset(
3440
GPIGate, GPI2Gate, MSGate, ZZGate, cirq.MeasurementGate, unroll_circuit_op=False
3541
)
@@ -79,6 +85,7 @@ def __init__(self, atol: float = 1e-8):
7985
cirq.HPowGate: self._serialize_h_pow_gate,
8086
cirq.SwapPowGate: self._serialize_swap_gate,
8187
cirq.MeasurementGate: self._serialize_measurement_gate,
88+
cirq.ops.pauli_string_phasor.PauliStringPhasorGate: self._serialize_pauli_string_phasor_gate, # noqa: E501
8289
# These gates can't be used with any of the non-measurement gates above
8390
# Rather than validating this here, we rely on the IonQ API to report failure.
8491
GPIGate: self._serialize_gpi_gate,
@@ -201,8 +208,14 @@ def _num_qubits(self, circuit: cirq.AbstractCircuit) -> int:
201208
all_qubits = circuit.all_qubits()
202209
return cast(line_qubit.LineQubit, max(all_qubits)).x + 1
203210

204-
def _serialize_circuit(self, circuit: cirq.AbstractCircuit) -> list:
205-
return [self._serialize_op(op) for moment in circuit for op in moment]
211+
def _serialize_circuit(self, circuit: cirq.AbstractCircuit) -> list[dict]:
212+
return [
213+
serialized_op
214+
for moment in circuit
215+
for op in moment
216+
for serialized_op in [self._serialize_op(op)]
217+
if serialized_op != {}
218+
]
206219

207220
def _serialize_op(self, op: cirq.Operation) -> dict:
208221
if op.gate is None:
@@ -222,7 +235,9 @@ def _serialize_op(self, op: cirq.Operation) -> dict:
222235
for gate_mro_type in gate_type.mro():
223236
if gate_mro_type in self._dispatch:
224237
serialized_op = self._dispatch[gate_mro_type](gate, targets)
225-
if serialized_op:
238+
# serialized_op {} results when serializing a PauliStringPhasorGate
239+
# where the exponentiated term is identity or the evolution time is 0.
240+
if serialized_op == {} or serialized_op:
226241
return serialized_op
227242
raise ValueError(f'Gate {gate} acting on {targets} cannot be serialized by IonQ API.')
228243

@@ -277,6 +292,38 @@ def _serialize_h_pow_gate(self, gate: cirq.HPowGate, targets: Sequence[int]) ->
277292
return {'gate': 'h', 'targets': targets}
278293
return None
279294

295+
def _serialize_pauli_string_phasor_gate(
296+
self, gate: PauliStringPhasorGate, targets: Sequence[int]
297+
) -> dict[str, Any] | None:
298+
PAULIS = {0: "I", 1: "X", 2: "Y", 3: "Z"}
299+
# Cirq uses big-endian ordering while IonQ API uses little-endian ordering.
300+
big_endian_pauli_string = ''.join(
301+
[PAULIS[pindex] for pindex in gate.dense_pauli_string.pauli_mask]
302+
)
303+
little_endian_pauli_string = big_endian_pauli_string[::-1]
304+
pauli_string_coefficient = gate.dense_pauli_string.coefficient
305+
coefficients = [pauli_string_coefficient.real]
306+
307+
# I am ignoring here the global phase of:
308+
# i * pi * (gate.exponent_neg + gate.exponent_pos) / 2
309+
time = math.pi * (gate.exponent_neg - gate.exponent_pos) / 2
310+
if time < 0:
311+
raise NotSupportedPauliexpParameters(
312+
'IonQ `pauliexp` gates does not support negative evolution times. '
313+
f'Found in a PauliStringPhasorGate a negative evolution time {time}.'
314+
)
315+
if little_endian_pauli_string == "" or time == 0:
316+
seralized_gate = {}
317+
else:
318+
seralized_gate = {
319+
'gate': 'pauliexp',
320+
'terms': [little_endian_pauli_string],
321+
"coefficients": coefficients,
322+
'targets': targets,
323+
'time': time,
324+
}
325+
return seralized_gate
326+
280327
# These could potentially be using serialize functions on the gates themselves.
281328
def _serialize_gpi_gate(self, gate: GPIGate, targets: Sequence[int]) -> dict | None:
282329
return {'gate': 'gpi', 'target': targets[0], 'phase': gate.phase}

cirq-ionq/cirq_ionq/serializer_test.py

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,18 @@
1515
from __future__ import annotations
1616

1717
import json
18+
import math
1819

1920
import numpy as np
2021
import pytest
2122
import sympy
2223

2324
import cirq
2425
import cirq_ionq as ionq
25-
from cirq_ionq.ionq_exceptions import IonQSerializerMixedGatesetsException
26+
from cirq_ionq.ionq_exceptions import (
27+
IonQSerializerMixedGatesetsException,
28+
NotSupportedPauliexpParameters,
29+
)
2630

2731

2832
def test_serialize_single_circuit_empty_circuit_invalid():
@@ -527,6 +531,100 @@ def test_serialize_many_circuits_swap_gate():
527531
_ = serializer.serialize_many_circuits([circuit])
528532

529533

534+
def test_serialize_single_circuit_pauli_string_phasor_gate():
535+
q0, q1, q2 = cirq.LineQubit.range(3)
536+
serializer = ionq.Serializer()
537+
pauli_string = cirq.Z(q0) * cirq.I(q1) * cirq.Y(q2)
538+
exponent_neg = 0.25
539+
exponent_pos = -0.5
540+
circuit = cirq.Circuit(
541+
cirq.PauliStringPhasor(pauli_string, exponent_neg=exponent_neg, exponent_pos=exponent_pos)
542+
)
543+
result = serializer.serialize_single_circuit(circuit)
544+
545+
# compare time floating point values with a tolerance
546+
expected_time = math.pi * (exponent_neg - exponent_pos) / 2
547+
assert result.body['circuit'][0]['time'] == pytest.approx(expected_time, abs=1e-10)
548+
549+
result.body['circuit'][0].pop('time')
550+
assert result == ionq.SerializedProgram(
551+
body={
552+
'gateset': 'qis',
553+
'qubits': 3,
554+
'circuit': [
555+
{'gate': 'pauliexp', 'terms': ['YZ'], 'coefficients': [1.0], 'targets': [0, 2]}
556+
],
557+
},
558+
metadata={},
559+
settings={},
560+
)
561+
562+
563+
def test_serialize_many_circuits_pauli_string_phasor_gate():
564+
q0, q1, q2, q4 = cirq.LineQubit.range(4)
565+
serializer = ionq.Serializer()
566+
pauli_string = cirq.Z(q0) * cirq.I(q1) * cirq.Y(q2) * cirq.X(q4)
567+
exponent_neg = 0.25
568+
exponent_pos = -0.5
569+
circuit = cirq.Circuit(
570+
cirq.PauliStringPhasor(pauli_string, exponent_neg=exponent_neg, exponent_pos=exponent_pos)
571+
)
572+
result = serializer.serialize_many_circuits([circuit])
573+
574+
# compare time floating point values with a tolerance
575+
expected_time = math.pi * (exponent_neg - exponent_pos) / 2
576+
assert result.body['circuits'][0]['circuit'][0]['time'] == pytest.approx(
577+
expected_time, abs=1e-10
578+
)
579+
580+
result.body['circuits'][0]['circuit'][0].pop('time')
581+
assert result == ionq.SerializedProgram(
582+
body={
583+
'gateset': 'qis',
584+
'qubits': 4,
585+
'circuits': [
586+
{
587+
'circuit': [
588+
{
589+
'gate': 'pauliexp',
590+
'terms': ['XYZ'],
591+
'coefficients': [1.0],
592+
'targets': [0, 2, 3],
593+
}
594+
]
595+
}
596+
],
597+
},
598+
metadata={'measurements': '[{}]', 'qubit_numbers': '[4]'},
599+
settings={},
600+
)
601+
602+
603+
def test_serialize_negative_argument_pauli_string_phasor_gate_raises_exception():
604+
q0, q1, q2 = cirq.LineQubit.range(3)
605+
serializer = ionq.Serializer()
606+
pauli_string = cirq.Z(q0) * cirq.I(q1) * cirq.Y(q2)
607+
exponent_neg = -0.25
608+
exponent_pos = 0.5
609+
circuit = cirq.Circuit(
610+
cirq.PauliStringPhasor(pauli_string, exponent_neg=exponent_neg, exponent_pos=exponent_pos)
611+
)
612+
with pytest.raises(NotSupportedPauliexpParameters):
613+
serializer.serialize_single_circuit(circuit)
614+
615+
616+
def test_serialize_pauli_string_phasor_gate_only_id_gates_in_pauli_string():
617+
q0, q1, q2 = cirq.LineQubit.range(3)
618+
serializer = ionq.Serializer()
619+
pauli_string = +1 * cirq.I(q0) * cirq.I(q1) * cirq.I(q2)
620+
circuit = cirq.Circuit(
621+
cirq.PauliStringPhasor(pauli_string, exponent_neg=1, exponent_pos=0),
622+
cirq.measure((q0, q1, q2), key='result'),
623+
)
624+
result = serializer.serialize_single_circuit(circuit)
625+
assert result.body['circuit'] == []
626+
627+
530628
def test_serialize_single_circuit_measurement_gate():
531629
q0 = cirq.LineQubit(0)
532630
circuit = cirq.Circuit(cirq.measure(q0, key='tomyheart'))

0 commit comments

Comments
 (0)