Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
2aab7fe
Adding the initial implementation for pauliexp gates support.
radumarg May 27, 2025
8848891
Bugfix.
radumarg May 27, 2025
758eefb
More fixes.
radumarg May 28, 2025
3439339
Fixing calculation of evolution time coefficient.
radumarg May 28, 2025
b5f9c96
Fixing comment.
radumarg May 28, 2025
26e3859
The serializer implementation is now complete. Started adding tests.
radumarg May 29, 2025
6f3387f
Fixing exponentiating identity terms. Moreunit tests.
radumarg Jun 4, 2025
24473ec
Bugfixes. Update tests.
radumarg Jun 11, 2025
915412c
Merge branch 'main' into pauli-exponential-support
radumarg Jun 11, 2025
01cc532
Format, lint fixes, type check fixes.
radumarg Jun 11, 2025
10957de
Merge branch 'pauli-exponential-support' of https://github.com/raduma…
radumarg Jun 11, 2025
7021ba0
Fix linting erorrs.
radumarg Jun 11, 2025
0885649
Fix format erorrs.
radumarg Jun 11, 2025
88f2309
Fix format erorrs.
radumarg Jun 11, 2025
2f03149
Fixing imports.
radumarg Jun 11, 2025
4917363
Fixing imports.
radumarg Jun 11, 2025
694fb3a
Merge branch 'main' into pauli-exponential-support
radumarg Jun 25, 2025
3cc813c
Merge branch 'main' into pauli-exponential-support
radumarg Jun 26, 2025
c3dbdd3
Rework after review: minor fixes.
radumarg Jun 26, 2025
cc7c0a3
Merge branch 'main' into pauli-exponential-support
radumarg Jun 27, 2025
de7a83e
Cleaning up the code. Fix linting and other format errors.
radumarg Jul 2, 2025
daa830f
Merge branch 'main' into pauli-exponential-support
radumarg Jul 2, 2025
b6af553
Fix linting error.
radumarg Jul 2, 2025
5541a28
Merge branch 'pauli-exponential-support' of https://github.com/raduma…
radumarg Jul 2, 2025
cbf0a3d
Fixing imports.
radumarg Jul 2, 2025
587015f
Again tring to fix imports.
radumarg Jul 2, 2025
3e045ac
Format import code.
radumarg Jul 2, 2025
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
9 changes: 9 additions & 0 deletions cirq-ionq/cirq_ionq/ionq_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,12 @@ class IonQSerializerMixedGatesetsException(Exception):

def __init__(self, message):
super().__init__(f"Message: '{message}'")


class NotSupportedPauliexpParameters(Exception):
"""An exception that may be thrown when trying to serialize a Cirq
PauliStringPhasorGate to IonQ `pauliexp` gate with unsupported parameters.
"""

def __init__(self, message):
super().__init__(f"Message: '{message}'")
7 changes: 7 additions & 0 deletions cirq-ionq/cirq_ionq/results.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,13 @@ def to_cirq_result(
rand = cirq.value.parse_random_state(seed)
measurements = {}
values, weights = zip(*list(self.probabilities().items()))

# normalize weights to sum to 1 if within tolerance because
# IonQ's pauliexp gates results are not extremely precise
total = sum(weights)
if np.isclose(total, 1.0, rtol=0, atol=1e-5):
weights = tuple((w / total for w in weights))

indices = rand.choice(
range(len(values)), p=weights, size=override_repetitions or self.repetitions()
)
Expand Down
55 changes: 51 additions & 4 deletions cirq-ionq/cirq_ionq/serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,24 @@

import dataclasses
import json
import math
from typing import Any, Callable, cast, Collection, Iterator, Sequence, TYPE_CHECKING

import numpy as np

import cirq
from cirq.devices import line_qubit
from cirq_ionq.ionq_exceptions import IonQSerializerMixedGatesetsException
from cirq_ionq.ionq_exceptions import (
IonQSerializerMixedGatesetsException,
NotSupportedPauliexpParameters,
)
from cirq_ionq.ionq_native_gates import GPI2Gate, GPIGate, MSGate, ZZGate

if TYPE_CHECKING:
import sympy

from cirq.ops.pauli_string_phasor import PauliStringPhasorGate

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

def _serialize_circuit(self, circuit: cirq.AbstractCircuit) -> list:
return [self._serialize_op(op) for moment in circuit for op in moment]
def _serialize_circuit(self, circuit: cirq.AbstractCircuit) -> list[dict]:
return [
serialized_op
for moment in circuit
for op in moment
for serialized_op in [self._serialize_op(op)]
if serialized_op != {}
]

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

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

def _serialize_pauli_string_phasor_gate(
self, gate: PauliStringPhasorGate, targets: Sequence[int]
) -> dict[str, Any] | None:
PAULIS = {0: "I", 1: "X", 2: "Y", 3: "Z"}
# Cirq uses big-endian ordering while IonQ API uses little-endian ordering.
big_endian_pauli_string = ''.join(
[PAULIS[pindex] for pindex in gate.dense_pauli_string.pauli_mask]
)
little_endian_pauli_string = big_endian_pauli_string[::-1]
pauli_string_coefficient = gate.dense_pauli_string.coefficient
coefficients = [pauli_string_coefficient.real]

# I am ignoring here the global phase of:
# i * pi * (gate.exponent_neg + gate.exponent_pos) / 2
time = math.pi * (gate.exponent_neg - gate.exponent_pos) / 2
if time < 0:
raise NotSupportedPauliexpParameters(
'IonQ `pauliexp` gates does not support negative evolution times. '
f'Found in a PauliStringPhasorGate a negative evolution time {time}.'
)
if little_endian_pauli_string == "" or time == 0:
seralized_gate = {}
else:
seralized_gate = {
'gate': 'pauliexp',
'terms': [little_endian_pauli_string],
"coefficients": coefficients,
'targets': targets,
'time': time,
}
return seralized_gate

# These could potentially be using serialize functions on the gates themselves.
def _serialize_gpi_gate(self, gate: GPIGate, targets: Sequence[int]) -> dict | None:
return {'gate': 'gpi', 'target': targets[0], 'phase': gate.phase}
Expand Down
100 changes: 99 additions & 1 deletion cirq-ionq/cirq_ionq/serializer_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,18 @@
from __future__ import annotations

import json
import math

import numpy as np
import pytest
import sympy

import cirq
import cirq_ionq as ionq
from cirq_ionq.ionq_exceptions import IonQSerializerMixedGatesetsException
from cirq_ionq.ionq_exceptions import (
IonQSerializerMixedGatesetsException,
NotSupportedPauliexpParameters,
)


def test_serialize_single_circuit_empty_circuit_invalid():
Expand Down Expand Up @@ -527,6 +531,100 @@ def test_serialize_many_circuits_swap_gate():
_ = serializer.serialize_many_circuits([circuit])


def test_serialize_single_circuit_pauli_string_phasor_gate():
q0, q1, q2 = cirq.LineQubit.range(3)
serializer = ionq.Serializer()
pauli_string = cirq.Z(q0) * cirq.I(q1) * cirq.Y(q2)
exponent_neg = 0.25
exponent_pos = -0.5
circuit = cirq.Circuit(
cirq.PauliStringPhasor(pauli_string, exponent_neg=exponent_neg, exponent_pos=exponent_pos)
)
result = serializer.serialize_single_circuit(circuit)

# compare time floating point values with a tolerance
expected_time = math.pi * (exponent_neg - exponent_pos) / 2
assert result.body['circuit'][0]['time'] == pytest.approx(expected_time, abs=1e-10)

result.body['circuit'][0].pop('time')
assert result == ionq.SerializedProgram(
body={
'gateset': 'qis',
'qubits': 3,
'circuit': [
{'gate': 'pauliexp', 'terms': ['YZ'], 'coefficients': [1.0], 'targets': [0, 2]}
],
},
metadata={},
settings={},
)


def test_serialize_many_circuits_pauli_string_phasor_gate():
q0, q1, q2, q4 = cirq.LineQubit.range(4)
serializer = ionq.Serializer()
pauli_string = cirq.Z(q0) * cirq.I(q1) * cirq.Y(q2) * cirq.X(q4)
exponent_neg = 0.25
exponent_pos = -0.5
circuit = cirq.Circuit(
cirq.PauliStringPhasor(pauli_string, exponent_neg=exponent_neg, exponent_pos=exponent_pos)
)
result = serializer.serialize_many_circuits([circuit])

# compare time floating point values with a tolerance
expected_time = math.pi * (exponent_neg - exponent_pos) / 2
assert result.body['circuits'][0]['circuit'][0]['time'] == pytest.approx(
expected_time, abs=1e-10
)

result.body['circuits'][0]['circuit'][0].pop('time')
assert result == ionq.SerializedProgram(
body={
'gateset': 'qis',
'qubits': 4,
'circuits': [
{
'circuit': [
{
'gate': 'pauliexp',
'terms': ['XYZ'],
'coefficients': [1.0],
'targets': [0, 2, 3],
}
]
}
],
},
metadata={'measurements': '[{}]', 'qubit_numbers': '[4]'},
settings={},
)


def test_serialize_negative_argument_pauli_string_phasor_gate_raises_exception():
q0, q1, q2 = cirq.LineQubit.range(3)
serializer = ionq.Serializer()
pauli_string = cirq.Z(q0) * cirq.I(q1) * cirq.Y(q2)
exponent_neg = -0.25
exponent_pos = 0.5
circuit = cirq.Circuit(
cirq.PauliStringPhasor(pauli_string, exponent_neg=exponent_neg, exponent_pos=exponent_pos)
)
with pytest.raises(NotSupportedPauliexpParameters):
serializer.serialize_single_circuit(circuit)


def test_serialize_pauli_string_phasor_gate_only_id_gates_in_pauli_string():
q0, q1, q2 = cirq.LineQubit.range(3)
serializer = ionq.Serializer()
pauli_string = +1 * cirq.I(q0) * cirq.I(q1) * cirq.I(q2)
circuit = cirq.Circuit(
cirq.PauliStringPhasor(pauli_string, exponent_neg=1, exponent_pos=0),
cirq.measure((q0, q1, q2), key='result'),
)
result = serializer.serialize_single_circuit(circuit)
assert result.body['circuit'] == []


def test_serialize_single_circuit_measurement_gate():
q0 = cirq.LineQubit(0)
circuit = cirq.Circuit(cirq.measure(q0, key='tomyheart'))
Expand Down