diff --git a/cirq-ionq/cirq_ionq/serializer.py b/cirq-ionq/cirq_ionq/serializer.py index 56194dae241..11de3ea3e5b 100644 --- a/cirq-ionq/cirq_ionq/serializer.py +++ b/cirq-ionq/cirq_ionq/serializer.py @@ -18,15 +18,19 @@ 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.ops.pauli_string_phasor import PauliStringPhasorGate from cirq_ionq.ionq_exceptions import IonQSerializerMixedGatesetsException from cirq_ionq.ionq_native_gates import GPI2Gate, GPIGate, MSGate, ZZGate + if TYPE_CHECKING: import sympy @@ -79,6 +83,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, # 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, @@ -202,7 +207,13 @@ def _num_qubits(self, circuit: cirq.AbstractCircuit) -> int: 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] + 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: @@ -222,7 +233,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.') @@ -277,6 +290,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 | 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 + if pauli_string_coefficient.imag != 0: + raise ValueError( + 'IonQ pauliexp gates does not support complex evolution coefficients. ' + f'Found in a PauliStringPhasorGate a complex evolution coefficient {pauli_string_coefficient} for the associated DensePauliString.' + ) + 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 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, + } + # TODO: remove this print statement once the serializer is stable. + print(seralized_gate) + 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} diff --git a/cirq-ionq/cirq_ionq/serializer_test.py b/cirq-ionq/cirq_ionq/serializer_test.py index c50d0e2e0cc..a1cd59e50f8 100644 --- a/cirq-ionq/cirq_ionq/serializer_test.py +++ b/cirq-ionq/cirq_ionq/serializer_test.py @@ -15,7 +15,7 @@ from __future__ import annotations import json - +import math import numpy as np import pytest import sympy @@ -527,6 +527,116 @@ 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 = 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_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': 3, + 'circuits': [ + { + 'circuit': [ + { + 'gate': 'pauliexp', + 'terms': ['YZ'], + 'coefficients': [1.0], + 'targets': [0, 2], + } + ] + } + ], + }, + metadata={'measurements': '[{}]', 'qubit_numbers': '[3]'}, + settings={}, + ) + + +def test_serialize_single_circuit_negative_argument_pauli_string_phasor_gate(): + q0, q1, q2 = cirq.LineQubit.range(3) + serializer = ionq.Serializer() + pauli_string = -1 * 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 = -1 * 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_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'))