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 then method for composing two Clifford tableaux #4096

Merged
merged 23 commits into from
May 20, 2021
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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
ddc3ca7
Update cirq-core/cirq/qis/clifford_tableau.py
BichengYing May 17, 2021
ca17267
Update cirq-core/cirq/qis/clifford_tableau.py
BichengYing May 17, 2021
4acbcaf
Add more comments
BichengYing May 18, 2021
75207e5
Compute num_ys in advance
BichengYing May 18, 2021
de6c419
Fix the lint error
BichengYing May 18, 2021
9db74eb
Format file
BichengYing May 18, 2021
7c6eeac
Using the vectorization methods to compute the phases
BichengYing May 18, 2021
17f09f2
Update clifford_tableau.py
BichengYing May 18, 2021
bfa08ca
Update the comments
BichengYing May 18, 2021
464f912
Using @ and add more comments
BichengYing May 19, 2021
f044cc1
Merge branch 'master' into single_clifford
BichengYing May 20, 2021
b11caf7
Merge branch 'master' into single_clifford
CirqBot May 20, 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
53 changes: 49 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:
BichengYing marked this conversation as resolved.
Show resolved Hide resolved
"""Returns a 2n * 2n matrix of Clifford table."""
BichengYing marked this conversation as resolved.
Show resolved Hide resolved
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,48 @@ def _str_full_(self) -> str:

return string

def then(self, second: 'CliffordTableau') -> 'CliffordTableau':
balopat marked this conversation as resolved.
Show resolved Hide resolved
"""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
this CliffordTableau then applying the second one.
BichengYing marked this conversation as resolved.
Show resolved Hide resolved
"""
m1 = self.matrix().astype(int)
m2 = second.matrix().astype(int)
BichengYing marked this conversation as resolved.
Show resolved Hide resolved

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.
BichengYing marked this conversation as resolved.
Show resolved Hide resolved
balopat marked this conversation as resolved.
Show resolved Hide resolved
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]):
mpharrigan marked this conversation as resolved.
Show resolved Hide resolved
if v == 0:
continue
swap_phase += np.sum(m2[i, : self.n] * m2[i, self.n :]) # Y gate => iXZ
balopat marked this conversation as resolved.
Show resolved Hide resolved
swap_phase += 2 * np.sum(m2[i, : self.n] * prev_row_sum[self.n :]) # swap Z_i X_i
BichengYing marked this conversation as resolved.
Show resolved Hide resolved
BichengYing marked this conversation as resolved.
Show resolved Hide resolved
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add more explanatory comments to these lines? A reference would be great too. It is really not obvious to me that why we are adding observables (rows) together for example to count the Ys..

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I get the %4, counting 1j phases for each ZX and the 1j * 1j for -1 for XZ. But the way we combine the second tableaux is just hard to parse from the code itself, which I think should be a criteria for readable code, or at least we should have a ref, so that someone can understand it from there. The original Aaronson and Gottesman paper did not talk about combining the tableaux.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, add more sentences. But it is really hard for me to describe it precisely. The key idea is global phase is the only scalar so that we can always move that to the beginning. So we can safely compute the phase of each component whenever we like then add them at last. We have lots of components to generate phases (phase[k] + (swap_phase % 4) / 2) % 2 the first phase[k] is the phase we generate by composing the two tableaux without adjusting the order etc(note like origin table may have +/- phase for each stabilizer).

I don't know a good reference for it. @Strilanc Do you know any good references for it?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, I found this, where Theorem 36 describes a vectorized solution: https://arxiv.org/pdf/2009.03218.pdf, it was so small, I implemented it, but the code is not working yet, it fails on some of the random circuit tests, I probably made a mistake somewhere, at least I thoroughly reviewed your tests, and they do seem to be sound :)

       m1 = self.matrix().astype(int)
        m2 = second.matrix().astype(int)

        lmbda = np.zeros((2 * self.n, 2 * self.n))
        lmbda[:self.n, self.n:] = np.eye(self.n)
        p1 = np.diag(m1 @ lmbda @ m1.T)
        m2Lm2T = m2 @ lmbda @ m2.T
        p2 = np.diag(m2Lm2T)

        s1 = self.rs.astype(int)
        s2 = second.rs.astype(int)

        merged_m = np.mod(m1 @ m2, 2)
        p12 = np.mod(np.diag(merged_m @ lmbda @ merged_m.T), 2)
        assert np.allclose(p12, np.mod(p1 + m1 @ p2, 2)), f"expected: {p12}, got: {p1 + m1 @ p2}"

        signs = np.mod(s1 + p1 * (m1 @ p2) + m1 @ s2 + np.diag(
            m1 @ np.tril(np.outer(p2, p2.T) + m2Lm2T, -1) @ m1.T), 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 = signs

Copy link
Collaborator Author

@BichengYing BichengYing May 18, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for finding this reference! I spent some time figuring out how to implement it correctly. In coding, it is not as clean as the equations. The ugly part is they use i^a(-1)^bX^cZ^d format, where a,b,c,d is a boolean type. We need to convert to this format first, do the computation in that domain, then convert it back. In both conversions, counting the number of y gates is inevitable. Check this modification based on your code:

        m1 = self.matrix().astype(int)
        m2 = second.matrix().astype(int)

        num_ys1 = np.sum(m1[:, : self.n] * m1[:, self.n :], axis=1)
        num_ys2 = np.sum(m2[:, : self.n] * m2[:, self.n :], axis=1)

        p1 = np.mod(num_ys1, 2)
        p2 = np.mod(num_ys2, 2)

        s1 = self.rs.astype(int) + np.mod(num_ys1, 4) // 2
        s2 = second.rs.astype(int) + np.mod(num_ys2, 4) // 2

        lmbda = np.zeros((2 * self.n, 2 * self.n))
        lmbda[: self.n, self.n :] = np.eye(self.n)
        m2Lm2T = m2 @ lmbda @ m2.T

        m_12 = np.mod(m1.dot(m2), 2)
        p_12 = np.mod(p1 + m1.dot(p2), 2)
        s_12 = (
            s1
            + m1.dot(s2)
            + p1 * m1.dot(p2)
            + np.diag(m1 @ np.tril(np.outer(p2, p2.T) + m2Lm2T, -1) @ m1.T)
        )
        num_ys12 = np.sum(m_12[:, : self.n] * m_12[:, self.n :], axis=1)
        merged_phase = np.mod(p_12 + 2*s_12 - num_ys12, 4) // 2
        merged_m = m_12

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

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice! I'm impressed how you deeply understood all of this :)
You are right, the Aaronson & Gottesman representation's r is not the same as the s vector in the Gosset paper's representation. In order to calculate with the Gosset method, we need to "remove" the "signs" that come from the PauliStrings that have even number of Ys in them.

I'd be curious to see which implementation is faster. I started doing a bit of benchmarking by modifying the end of your then test:

    n_qubits = 400
    t_final, t_op, expected_t = _three_identical_table(n_qubits)
    seq_op = random_circuit(num_ops=1000, num_qubits=n_qubits)
    for i, (op, args) in enumerate(seq_op):
            t_op = cirq.CliffordTableau(n_qubits)
            op(t_op, *args)
            t_final = t_final.then(t_op)
            op(expected_t, *args)

    assert expected_t == t_final

And started to measure based on n_qubits.
On my machine

qubits new old
100 6.56 30.8
200 61.8 105.74
300 204.04 276.61
400 511.63 670

Which seems like the first version is slower at first blink, but it seems to be growing slower (with an exponent of ~2 instead of ~3.2)...but we need more datapoints. Both should be O(n^3), so these measurements are probably heavily influenced by vectorization and constant factors.

So - I like the Gosset version a bit more because we have a nice paper + proof describing it and it looks vectorized, so it should be faster - but we need to confirm it. I think speed does matter here, if anybody will use this for QEC, they will use large tableaux.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to close this off, the new method is slightly faster but not significantly - my previous data was messed up with manual datataking - this one was automated:

qubits new old
100 24.93 30.93
200 163.97 147.83
300 350.11 378.99
400 807.15 928.34
500 1679.39 1776.07
1000 18750.73 19171.23


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 __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 +247,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 +271,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
112 changes: 112 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,115 @@ 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