Skip to content

Commit 29d99d3

Browse files
Create a transformer cancels the effect of Z-phases (#6837)
1 parent 0d9a6ee commit 29d99d3

File tree

4 files changed

+175
-2
lines changed

4 files changed

+175
-2
lines changed

cirq-core/cirq/experiments/z_phase_calibration.py

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
# limitations under the License.
1414

1515
"""Provides a method to do z-phase calibration for excitation-preserving gates."""
16-
from typing import Union, Optional, Sequence, Tuple, Dict, TYPE_CHECKING, Any
16+
from typing import Union, Optional, Sequence, Tuple, Dict, TYPE_CHECKING, Any, List
1717
import multiprocessing
1818
import multiprocessing.pool
1919

@@ -22,7 +22,8 @@
2222

2323
from cirq.experiments import xeb_fitting
2424
from cirq.experiments.two_qubit_xeb import parallel_xeb_workflow
25-
from cirq import ops
25+
from cirq.transformers import transformer_api
26+
from cirq import ops, circuits, protocols
2627

2728
if TYPE_CHECKING:
2829
import cirq
@@ -283,3 +284,84 @@ def plot_z_phase_calibration_result(
283284
ax.set_title('-'.join(str(q) for q in pair))
284285
ax.legend()
285286
return axes
287+
288+
289+
def _z_angles(old: ops.PhasedFSimGate, new: ops.PhasedFSimGate) -> Tuple[float, float, float]:
290+
"""Computes a set of possible 3 z-phases that result in the change in gamma, zeta, and chi."""
291+
# This procedure is the inverse of PhasedFSimGate.from_fsim_rz
292+
delta_gamma = new.gamma - old.gamma
293+
delta_zeta = new.zeta - old.zeta
294+
delta_chi = new.chi - old.chi
295+
return (-delta_gamma + delta_chi, -delta_gamma - delta_zeta, delta_zeta - delta_chi)
296+
297+
298+
@transformer_api.transformer
299+
class CalibrationTransformer:
300+
301+
def __init__(
302+
self,
303+
target: 'cirq.Gate',
304+
calibration_map: Dict[Tuple['cirq.Qid', 'cirq.Qid'], 'cirq.PhasedFSimGate'],
305+
):
306+
"""Create a CalibrationTransformer.
307+
308+
The transformer adds 3 ZPowGates around each calibrated gate to cancel the
309+
effect of z-phases.
310+
311+
Args:
312+
target: The target gate. Any gate matching this
313+
will be replaced based on the content of `calibration_map`.
314+
calibration_map:
315+
A map mapping qubit pairs to calibrated gates. This is the output of
316+
calling `calibrate_z_phases`.
317+
"""
318+
self.target = target
319+
if isinstance(target, ops.PhasedFSimGate):
320+
self.target_as_fsim = target
321+
elif (gate := ops.PhasedFSimGate.from_matrix(protocols.unitary(target))) is not None:
322+
self.target_as_fsim = gate
323+
else:
324+
raise ValueError(f"{target} is not equivalent to a PhasedFSimGate")
325+
self.calibration_map = calibration_map
326+
327+
def __call__(
328+
self,
329+
circuit: 'cirq.AbstractCircuit',
330+
*,
331+
context: Optional[transformer_api.TransformerContext] = None,
332+
) -> 'cirq.Circuit':
333+
"""Adds 3 ZPowGates around each calibrated gate to cancel the effect of Z phases.
334+
335+
Args:
336+
circuit: Circuit to transform.
337+
context: Optional transformer context (not used).
338+
339+
Returns:
340+
New circuit with the extra ZPowGates.
341+
"""
342+
new_moments: List[Union[List[cirq.Operation], 'cirq.Moment']] = []
343+
for moment in circuit:
344+
before = []
345+
after = []
346+
for op in moment:
347+
if op.gate != self.target:
348+
# not a target.
349+
continue
350+
assert len(op.qubits) == 2
351+
gate = self.calibration_map.get(op.qubits, None) or self.calibration_map.get(
352+
op.qubits[::-1], None
353+
)
354+
if gate is None:
355+
# no calibration available.
356+
continue
357+
angles = np.array(_z_angles(self.target_as_fsim, gate)) / np.pi
358+
angles = -angles # Take the negative to cancel the effect.
359+
before.append(ops.Z(op.qubits[0]) ** angles[0])
360+
before.append(ops.Z(op.qubits[1]) ** angles[1])
361+
after.append(ops.Z(op.qubits[0]) ** angles[2])
362+
if before:
363+
new_moments.append(before)
364+
new_moments.append(moment)
365+
if after:
366+
new_moments.append(after)
367+
return circuits.Circuit.from_moments(*new_moments)

cirq-core/cirq/experiments/z_phase_calibration_test.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
calibrate_z_phases,
2323
z_phase_calibration_workflow,
2424
plot_z_phase_calibration_result,
25+
CalibrationTransformer,
2526
)
2627
from cirq.experiments.xeb_fitting import XEBPhasedFSimCharacterizationOptions
2728

@@ -205,3 +206,33 @@ def test_plot_z_phase_calibration_result():
205206
np.testing.assert_allclose(axes[1].lines[0].get_xdata().astype(float), [1, 2, 3])
206207
np.testing.assert_allclose(axes[1].lines[0].get_ydata().astype(float), [0.6, 0.4, 0.1])
207208
np.testing.assert_allclose(axes[1].lines[1].get_ydata().astype(float), [0.7, 0.77, 0.8])
209+
210+
211+
@pytest.mark.parametrize('angles', 2 * np.pi * np.random.random((10, 10)))
212+
def test_transform_circuit(angles):
213+
theta, phi = angles[:2]
214+
old_zs = angles[2:6]
215+
new_zs = angles[6:]
216+
gate = cirq.PhasedFSimGate.from_fsim_rz(theta, phi, old_zs[:2], old_zs[2:])
217+
fsim = cirq.PhasedFSimGate.from_fsim_rz(theta, phi, new_zs[:2], new_zs[2:])
218+
c = cirq.Circuit(gate(cirq.q(0), cirq.q(1)))
219+
replacement_map = {(cirq.q(1), cirq.q(0)): fsim}
220+
221+
new_circuit = CalibrationTransformer(gate, replacement_map)(c)
222+
223+
# we replace the old gate with the `fsim` gate the result should be that the overall
224+
# unitary equals the unitary of the original (ideal) gate.
225+
circuit_with_replacement_gate = cirq.Circuit(
226+
op if op.gate != gate else fsim(*op.qubits) for op in new_circuit.all_operations()
227+
)
228+
np.testing.assert_allclose(cirq.unitary(circuit_with_replacement_gate), cirq.unitary(c))
229+
230+
231+
def test_transform_circuit_invalid_gate_raises():
232+
with pytest.raises(ValueError, match="is not equivalent to a PhasedFSimGate"):
233+
_ = CalibrationTransformer(cirq.XX, {})
234+
235+
236+
def test_transform_circuit_uncalibrated_gates_pass():
237+
c = cirq.Circuit(cirq.CZ(cirq.q(0), cirq.q(1)), cirq.measure(cirq.q(0)))
238+
assert c == CalibrationTransformer(cirq.CZ, {})(c)

cirq-core/cirq/ops/fsim_gate.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,45 @@ def from_fsim_rz(
347347
chi = (b0 - b1 - a0 + a1) / 2.0
348348
return PhasedFSimGate(theta, zeta, chi, gamma, phi)
349349

350+
@staticmethod
351+
def from_matrix(u: np.ndarray) -> Optional['PhasedFSimGate']:
352+
"""Contruct a PhasedFSimGate from unitary.
353+
354+
Args:
355+
u: A unitary matrix representing a PhasedFSimGate.
356+
357+
Returns:
358+
- Either PhasedFSimGate with the given unitary or None if
359+
the matrix is not unitary or if doesn't represent a PhasedFSimGate.
360+
"""
361+
362+
gamma = np.angle(u[1, 1] * u[2, 2] - u[1, 2] * u[2, 1]) / -2
363+
phi = -np.angle(u[3, 3]) - 2 * gamma
364+
phased_cos_theta_2 = u[1, 1] * u[2, 2]
365+
if phased_cos_theta_2 == 0:
366+
# The zeta phase is multiplied with cos(theta),
367+
# so if cos(theta) is zero then any value is possible.
368+
zeta = 0
369+
else:
370+
zeta = np.angle(u[2, 2] / u[1, 1]) / 2
371+
372+
phased_sin_theta_2 = u[1, 2] * u[2, 1]
373+
if phased_sin_theta_2 == 0:
374+
# The chi phase is multiplied with sin(theta),
375+
# so if sin(theta) is zero then any value is possible.
376+
chi = 0
377+
else:
378+
chi = np.angle(u[1, 2] / u[2, 1]) / 2
379+
380+
theta = np.angle(
381+
np.exp(1j * (gamma + zeta)) * u[1, 1] - np.exp(1j * (gamma - chi)) * u[1, 2]
382+
)
383+
384+
gate = PhasedFSimGate(theta=theta, phi=phi, chi=chi, zeta=zeta, gamma=gamma)
385+
if np.allclose(u, protocols.unitary(gate)):
386+
return gate
387+
return None
388+
350389
@property
351390
def rz_angles_before(self) -> Tuple['cirq.TParamVal', 'cirq.TParamVal']:
352391
"""Returns 2-tuple of phase angles applied to qubits before FSimGate."""

cirq-core/cirq/ops/fsim_gate_test.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -797,3 +797,24 @@ def test_phased_fsim_json_dict():
797797
assert cirq.PhasedFSimGate(
798798
theta=0.12, zeta=0.34, chi=0.56, gamma=0.78, phi=0.9
799799
)._json_dict_() == {'theta': 0.12, 'zeta': 0.34, 'chi': 0.56, 'gamma': 0.78, 'phi': 0.9}
800+
801+
802+
@pytest.mark.parametrize(
803+
'gate',
804+
[
805+
cirq.CZ,
806+
cirq.SQRT_ISWAP,
807+
cirq.SQRT_ISWAP_INV,
808+
cirq.ISWAP,
809+
cirq.ISWAP_INV,
810+
cirq.cphase(0.1),
811+
cirq.CZ**0.2,
812+
],
813+
)
814+
def test_phase_fsim_from_matrix(gate):
815+
u = cirq.unitary(gate)
816+
np.testing.assert_allclose(cirq.unitary(cirq.PhasedFSimGate.from_matrix(u)), u, atol=1e-8)
817+
818+
819+
def test_phase_fsim_from_matrix_not_fsim_returns_none():
820+
assert cirq.PhasedFSimGate.from_matrix(np.ones((4, 4))) is None

0 commit comments

Comments
 (0)