Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add inverse method for Clifford tableau #4111

Merged
merged 23 commits into from
May 21, 2021
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
3bf0090
add to_phased_xz_gate function for SingleQubitCliffordGate
BichengYing Apr 25, 2021
72208c9
format using flynt instead
BichengYing Apr 25, 2021
0566724
Merge branch 'master' into clifford
BichengYing Apr 25, 2021
0ad7d86
Merge branch 'master' into clifford
BichengYing May 5, 2021
d680b29
Move `import DensePauliString` under CliffordTableau into runtime
BichengYing May 6, 2021
70283c4
Add merged_with method of two clifford tableau
BichengYing May 9, 2021
701f0df
Fix lint and coverage error
BichengYing May 9, 2021
e086c41
Varying the seed in the loop
BichengYing May 10, 2021
ae82bcd
Merge branch 'master' into single_clifford
BichengYing May 10, 2021
733c738
Address the comments and rename the function to `then`
BichengYing May 12, 2021
c7877e4
Ignore the pylint warning
BichengYing May 13, 2021
3e341c5
Minor update for more comments
BichengYing May 14, 2021
4d84529
Merge branch 'master' into single_clifford
BichengYing May 14, 2021
6a19c8b
Update cirq-core/cirq/qis/clifford_tableau.py
BichengYing May 15, 2021
356b1ed
Add dimension and type check and corresponding test.
BichengYing May 15, 2021
a4dd451
Merge branch 'single_clifford' into inv_tableau
BichengYing May 16, 2021
0d32604
Add `inverse` function for clifford tableau
BichengYing May 16, 2021
716f6db
revert phased xz commitment
BichengYing May 16, 2021
46260ba
Add more comments
BichengYing May 18, 2021
e9f4822
Revert "Add more comments"
BichengYing May 18, 2021
2d1e21c
Merge branch 'master' into inv_tableau
BichengYing May 21, 2021
372cf9f
Format
BichengYing May 21, 2021
b355f1a
Add more comments
BichengYing May 21, 2021
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
88 changes: 84 additions & 4 deletions cirq-core/cirq/qis/clifford_tableau.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@

import cirq
from cirq import protocols
from cirq.ops.dense_pauli_string import DensePauliString
from cirq.value import big_endian_int_to_digits


Expand Down Expand Up @@ -84,6 +83,10 @@ def rs(self, new_rs: np.array) -> None:
assert np.shape(new_rs) == (2 * self.n,)
self._rs[:-1] = np.array(new_rs).astype(bool)

def matrix(self) -> np.array:
"""Returns the 2n * 2n matrix representation of the Clifford tableau."""
return np.concatenate([self.xs, self.zs], axis=1)

def _json_dict_(self) -> Dict[str, Any]:
return protocols.obj_to_dict_helper(self, ['n', 'rs', 'xs', 'zs'])

Expand Down Expand Up @@ -176,6 +179,83 @@ def _str_full_(self) -> str:

return string

def then(self, second: 'CliffordTableau') -> 'CliffordTableau':
"""Returns a composed CliffordTableau of this tableau and the second tableau.

Then composed tableau is equal to (up to global phase) the composed
unitary operation of the two tableaux, i.e. equivalent to applying the unitary
operation of this CliffordTableau then applying the second one.

Args:
second: The second CliffordTableau to compose with.

Returns:
The composed CliffordTableau.

Raises:
TypeError: If the type of second is not CliffordTableau.
ValueError: If the number of qubits in the second tableau mismatch with
this tableau.
"""
if not isinstance(second, CliffordTableau):
raise TypeError("The type for second tableau must be the CliffordTableau type")
if self.n != second.n:
raise ValueError(
f"Mismatched number of qubits of two tableaux: {self.n} vs {second.n}."
)

# Convert the underlying data type from bool to int for easier numerical computation.
m1 = self.matrix().astype(int)
m2 = second.matrix().astype(int)

p1 = self.rs.astype(int)
p2 = second.rs.astype(int)

merged_m = np.mod(m1.dot(m2), 2)
phase = np.mod(p1 + m1.dot(p2), 2)

# we need more phase correction for expanding Y to XZ and swapping Z_iX_i order.
for k in range(2 * self.n):
swap_phase = 0 # value betwen 0 and 3 representing [1, i, -1, -i] respectively.
prev_row_sum = np.zeros([2 * self.n])
swap_phase += np.sum(m1[k, : self.n] * m1[k, self.n :]) # Y gate => iXZ
for i, v in enumerate(m1[k]):
if v == 0:
continue
swap_phase += np.sum(m2[i, : self.n] * m2[i, self.n :]) # Y gate => iXZ
swap_phase += 2 * np.sum(m2[i, : self.n] * prev_row_sum[self.n :]) # swap Z_i X_i
prev_row_sum += m2[i]
prev_row_sum = np.mod(prev_row_sum, 2)
swap_phase -= np.sum(prev_row_sum[: self.n] * prev_row_sum[self.n :]) # XZ => -iY
phase[k] = (phase[k] + (swap_phase % 4) / 2) % 2

merged_tableau = CliffordTableau(num_qubits=self.n)
merged_tableau.xs = merged_m[:, : self.n]
merged_tableau.zs = merged_m[:, self.n :]
merged_tableau.rs = phase
return merged_tableau

def inverse(self) -> 'CliffordTableau':
"""Returns the inverse Clifford tableau of this tableau."""
ret_table = CliffordTableau(num_qubits=self.n)
# It relies on the symplectic property of Clifford tableau.
# [A^T C^T [0 I [A B [0 I
# B^T D^T] I 0] C D] = I 0]
# So the inverse is [[D^T B^T], [C^T A^T]]
ret_table.xs[: self.n] = self.zs[self.n :].T
ret_table.zs[: self.n] = self.zs[: self.n].T
ret_table.xs[self.n :] = self.xs[self.n :].T
ret_table.zs[self.n :] = self.xs[: self.n].T

# Update phase
ret_table.rs = ret_table.then(self).rs
return ret_table

def __matmul__(self, second: 'CliffordTableau'):
if not isinstance(second, CliffordTableau):
return NotImplemented
return second.then(self)

def _rowsum(self, q1, q2):
"""Implements the "rowsum" routine defined by
Aaronson and Gottesman.
Expand All @@ -202,7 +282,7 @@ def g(x1, z1, x2, z2):
self._xs[q1, :] ^= self._xs[q2, :]
self._zs[q1, :] ^= self._zs[q2, :]

def _row_to_dense_pauli(self, i: int) -> DensePauliString:
def _row_to_dense_pauli(self, i: int) -> 'cirq.DensePauliString':
"""
Args:
i: index of the row in the tableau.
Expand All @@ -226,12 +306,12 @@ def _row_to_dense_pauli(self, i: int) -> DensePauliString:
pauli_mask += "I"
return cirq.DensePauliString(pauli_mask, coefficient=coefficient)

def stabilizers(self) -> List[DensePauliString]:
def stabilizers(self) -> List['cirq.DensePauliString']:
"""Returns the stabilizer generators of the state. These
are n operators {S_1,S_2,...,S_n} such that S_i |psi> = |psi>"""
return [self._row_to_dense_pauli(i) for i in range(self.n, 2 * self.n)]

def destabilizers(self) -> List[DensePauliString]:
def destabilizers(self) -> List['cirq.DensePauliString']:
"""Returns the destabilizer generators of the state. These
are n operators {S_1,S_2,...,S_n} such that along with the stabilizer
generators above generate the full Pauli group on n qubits."""
Expand Down
203 changes: 203 additions & 0 deletions cirq-core/cirq/qis/clifford_tableau_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,3 +322,206 @@ def test_deprecated_clifford_location():
from cirq.sim import CliffordTableau

CliffordTableau(num_qubits=1)


def _three_identical_table(num_qubits):
t1 = cirq.CliffordTableau(num_qubits)
t2 = cirq.CliffordTableau(num_qubits)
t3 = cirq.CliffordTableau(num_qubits)
return t1, t2, t3


def test_tableau_then():

t1, t2, expected_t = _three_identical_table(1)
assert expected_t == t1.then(t2)

t1, t2, expected_t = _three_identical_table(1)
_ = [_H(t, 0) for t in (t1, expected_t)]
_ = [_H(t, 0) for t in (t2, expected_t)]
assert expected_t == t1.then(t2)

t1, t2, expected_t = _three_identical_table(1)
_ = [_X(t, 0) for t in (t1, expected_t)]
_ = [_S(t, 0) for t in (t2, expected_t)]
assert expected_t == t1.then(t2)
assert expected_t != t2.then(t1)

t1, t2, expected_t = _three_identical_table(1)
_ = [_X(t, 0) for t in (t1, expected_t)]
_ = [_H(t, 0) for t in (t1, expected_t)]
_ = [_Z(t, 0) for t in (t1, expected_t)]
_ = [_S(t, 0) for t in (t2, expected_t)]
_ = [_H(t, 0) for t in (t2, expected_t)]
assert expected_t == t1.then(t2)
assert expected_t != t2.then(t1)

t1, t2, expected_t = _three_identical_table(2)
_ = [_H(t, 0) for t in (t1, expected_t)]
_ = [_H(t, 1) for t in (t1, expected_t)]
_ = [_H(t, 0) for t in (t2, expected_t)]
_ = [_H(t, 1) for t in (t2, expected_t)]
assert expected_t == t1.then(t2)

t1, t2, expected_t = _three_identical_table(2)
_ = [_H(t, 0) for t in (t1, expected_t)]
_ = [_CNOT(t, 0, 1) for t in (t1, expected_t)]
_ = [_S(t, 0) for t in (t2, expected_t)]
_ = [_X(t, 1) for t in (t2, expected_t)]
assert expected_t == t1.then(t2)
assert expected_t != t2.then(t1)

t1, t2, expected_t = _three_identical_table(2)
_ = [_H(t, 0) for t in (t1, expected_t)]
_ = [_CNOT(t, 0, 1) for t in (t1, expected_t)]
_ = [_S(t, 1) for t in (t2, expected_t)]
_ = [_CNOT(t, 1, 0) for t in (t2, expected_t)]
assert expected_t == t1.then(t2)
assert expected_t != t2.then(t1)

def random_circuit(num_ops, num_qubits, seed=12345):
prng = np.random.RandomState(seed)
candidate_op = [_H, _S, _X, _Z]
if num_qubits > 1:
candidate_op = [_H, _S, _X, _Z, _CNOT]

seq_op = []
for _ in range(num_ops):
op = prng.randint(len(candidate_op))
if op != 4:
args = (prng.randint(num_qubits),)
else:
args = prng.choice(num_qubits, 2, replace=False)
seq_op.append((candidate_op[op], args))
return seq_op

# Do small random circuits test 100 times.
for seed in range(100):
t1, t2, expected_t = _three_identical_table(8)
seq_op = random_circuit(num_ops=20, num_qubits=8, seed=seed)
for i, (op, args) in enumerate(seq_op):
if i < 7:
_ = [op(t, *args) for t in (t1, expected_t)]
else:
_ = [op(t, *args) for t in (t2, expected_t)]
assert expected_t == t1.then(t2)

# Since merging Clifford Tableau operation is O(n^3),
# running 100 qubits case is still fast.
t1, t2, expected_t = _three_identical_table(100)
seq_op = random_circuit(num_ops=1000, num_qubits=100)
for i, (op, args) in enumerate(seq_op):
if i < 350:
_ = [op(t, *args) for t in (t1, expected_t)]
else:
_ = [op(t, *args) for t in (t2, expected_t)]
assert expected_t == t1.then(t2)


def test_tableau_matmul():
t1, t2, expected_t = _three_identical_table(1)
_ = [_H(t, 0) for t in (t1, expected_t)]
_ = [_H(t, 0) for t in (t2, expected_t)]
assert expected_t == t2 @ t1

t1, t2, expected_t = _three_identical_table(1)
_ = [_X(t, 0) for t in (t1, expected_t)]
_ = [_S(t, 0) for t in (t2, expected_t)]
assert expected_t == t2 @ t1
assert expected_t != t1 @ t2

with pytest.raises(TypeError):
# pylint: disable=pointless-statement
t1 @ 21
# pylint: enable=pointless-statement


def test_tableau_then_with_bad_input():
t1 = cirq.CliffordTableau(1)
t2 = cirq.CliffordTableau(2)
with pytest.raises(ValueError, match="Mismatched number of qubits of two tableaux: 1 vs 2."):
t1.then(t2)

with pytest.raises(TypeError):
t1.then(cirq.X)


def test_inverse():
t = cirq.CliffordTableau(num_qubits=1)
assert t.inverse() == t

t = cirq.CliffordTableau(num_qubits=1)
_X(t, 0)
_S(t, 0)
expected_t = cirq.CliffordTableau(num_qubits=1)
_S(expected_t, 0) # the inverse of S gate is S*S*S.
_S(expected_t, 0)
_S(expected_t, 0)
_X(expected_t, 0)
assert t.inverse() == expected_t
assert t.then(t.inverse()) == cirq.CliffordTableau(num_qubits=1)
assert t.inverse().then(t) == cirq.CliffordTableau(num_qubits=1)

t = cirq.CliffordTableau(num_qubits=2)
_H(t, 0)
_H(t, 1)
# Because the ops are the same in either forward or backward way,
# t is self-inverse operator.
assert t.inverse() == t
assert t.then(t.inverse()) == cirq.CliffordTableau(num_qubits=2)
assert t.inverse().then(t) == cirq.CliffordTableau(num_qubits=2)

t = cirq.CliffordTableau(num_qubits=2)
_X(t, 0)
_CNOT(t, 0, 1)
expected_t = cirq.CliffordTableau(num_qubits=2)
_CNOT(t, 0, 1)
_X(t, 0)
assert t.inverse() == expected_t
assert t.then(t.inverse()) == cirq.CliffordTableau(num_qubits=2)
assert t.inverse().then(t) == cirq.CliffordTableau(num_qubits=2)

def random_circuit_and_its_inverse(num_ops, num_qubits, seed=12345):
prng = np.random.RandomState(seed)
candidate_op = [_H, _S, _X, _Z]
if num_qubits > 1:
candidate_op = [_H, _S, _X, _Z, _CNOT]

seq_op = []
inv_seq_ops = []
for _ in range(num_ops):
op = prng.randint(len(candidate_op))
if op != 4:
args = (prng.randint(num_qubits),)
else:
args = prng.choice(num_qubits, 2, replace=False)
seq_op.append((candidate_op[op], args))
if op == 1: # S gate
inv_seq_ops.extend([(_S, args), (_S, args), (_S, args)])
else:
inv_seq_ops.append((candidate_op[op], args))
return seq_op, inv_seq_ops[::-1]

# Do small random circuits test 100 times.
for seed in range(100):
t, expected_t, _ = _three_identical_table(7)
seq_op, inv_seq_ops = random_circuit_and_its_inverse(num_ops=50, num_qubits=7, seed=seed)
for op, args in seq_op:
op(t, *args)
for op, args in inv_seq_ops:
op(expected_t, *args)
assert t.inverse() == expected_t
assert t.then(t.inverse()) == cirq.CliffordTableau(num_qubits=7)
assert t.inverse().then(t) == cirq.CliffordTableau(num_qubits=7)

# Since inverse Clifford Tableau operation is O(n^3) (same order of composing two tableaux),
# running 100 qubits case is still fast.
t, expected_t, _ = _three_identical_table(100)
seq_op, inv_seq_ops = random_circuit_and_its_inverse(num_ops=1000, num_qubits=100)
for op, args in seq_op:
op(t, *args)
for op, args in inv_seq_ops:
op(expected_t, *args)
assert t.inverse() == expected_t
assert t.then(t.inverse()) == cirq.CliffordTableau(num_qubits=100)
assert t.inverse().then(t) == cirq.CliffordTableau(num_qubits=100)