Skip to content

Commit 1bbbc9f

Browse files
authored
Multiple Qubits Clifford Gate (#4791)
Add initial Clifford Gate with multiple qubits. Compared with SingleQubitCliffordGate, it has fewer functionalities since we cannot enumerate all of them with PauliGates and several special single qubit properties like Bloch rotation no longer exist. Anyway, it provides several basic interactions: 1. It uses Clifford tableau as underlying data representation (different from the state representation). 2. It can be constructed from a tableau or list of operations (`_has_stabilizer_effect_` only). All Clifford gates can be built through \{S, H, CNOT\}, so we can construct any Clifford Gate from the list of operations. We just cannot pre-define it. 3. Decomposing into several basic operations. 4. Get unitary matrix through decomposing (we cannot do this in a reverse way from unitary to Clifford gate :( ). 5. Know how to interact with ActOnCliffordTableauArgs, i.e. it should be able to use with CliffordTableau simulator (Looks like we don't have that in cirq yet? @daxfohl will add that? see #4639 and #4748.). This PR is part of efforts for #3639. Context: this PR doesn't introduce any new algorithms but the key methods are already implemented in #4183 and #4096.
1 parent 59b72a1 commit 1bbbc9f

File tree

7 files changed

+638
-3
lines changed

7 files changed

+638
-3
lines changed

cirq-core/cirq/__init__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@
203203
CCNOT,
204204
CCNotPowGate,
205205
ClassicallyControlledOperation,
206+
CliffordGate,
206207
CNOT,
207208
CNotPowGate,
208209
ControlledGate,
@@ -706,7 +707,7 @@
706707
contrib,
707708
)
708709

709-
# deprecate cirq.ops.moment and related attributes
710+
# deprecate cirq.ops and related attributes
710711

711712
from cirq import _compat
712713

cirq-core/cirq/json_resolver_cache.py

+1
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ def _parallel_gate_op(gate, qubits):
6666
'CircuitOperation': cirq.CircuitOperation,
6767
'ClassicallyControlledOperation': cirq.ClassicallyControlledOperation,
6868
'ClassicalDataDictionaryStore': cirq.ClassicalDataDictionaryStore,
69+
'CliffordGate': cirq.CliffordGate,
6970
'CliffordState': cirq.CliffordState,
7071
'CliffordTableau': cirq.CliffordTableau,
7172
'CNotPowGate': cirq.CNotPowGate,

cirq-core/cirq/ops/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
)
2020

2121
from cirq.ops.clifford_gate import (
22+
CliffordGate,
2223
PauliTransform,
2324
SingleQubitCliffordGate,
2425
)

cirq-core/cirq/ops/clifford_gate.py

+309-2
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,44 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
from typing import Any, cast, Dict, NamedTuple, Optional, Sequence, Tuple, TYPE_CHECKING, Union
15+
from typing import (
16+
Any,
17+
cast,
18+
Dict,
19+
List,
20+
NamedTuple,
21+
Optional,
22+
Sequence,
23+
Tuple,
24+
TYPE_CHECKING,
25+
Union,
26+
)
1627

1728
import numpy as np
1829

1930
from cirq import protocols, value, linalg, qis
2031
from cirq._doc import document
21-
from cirq.ops import common_gates, gate_features, named_qubit, pauli_gates, phased_x_z_gate
32+
from cirq._import import LazyLoader
33+
from cirq.ops import (
34+
common_gates,
35+
gate_features,
36+
identity,
37+
named_qubit,
38+
raw_types,
39+
pauli_gates,
40+
phased_x_z_gate,
41+
)
2242
from cirq.ops.pauli_gates import Pauli
2343
from cirq.type_workarounds import NotImplementedType
2444

2545
if TYPE_CHECKING:
2646
import cirq
2747

48+
# Lazy imports to break circular dependencies.
49+
devices = LazyLoader("devices", globals(), "cirq.devices")
50+
sim = LazyLoader("sim", globals(), "cirq.sim")
51+
transformers = LazyLoader("transformers", globals(), "cirq.transformers")
52+
2853
PauliTransform = NamedTuple('PauliTransform', [('to', Pauli), ('flip', bool)])
2954
document(PauliTransform, """+X, -X, +Y, -Y, +Z, or -Z.""")
3055

@@ -571,3 +596,285 @@ def _circuit_diagram_info_(
571596
SingleQubitCliffordGate.Z: SingleQubitCliffordGate.Z_nsqrt,
572597
},
573598
}
599+
600+
601+
class CommonCliffordGateMetaClass(value.ABCMetaImplementAnyOneOf):
602+
"""A metaclass used to lazy initialize several common Clifford Gate as class attributes."""
603+
604+
@property
605+
def I(cls):
606+
if getattr(cls, '_I', None) is None:
607+
cls._I = cls._generate_clifford_from_known_gate(1, identity.I)
608+
return cls._I
609+
610+
@property
611+
def X(cls):
612+
if getattr(cls, '_X', None) is None:
613+
cls._Z = cls._generate_clifford_from_known_gate(1, pauli_gates.X)
614+
return cls._Z
615+
616+
@property
617+
def Y(cls):
618+
if getattr(cls, '_X', None) is None:
619+
cls._Z = cls._generate_clifford_from_known_gate(1, pauli_gates.Y)
620+
return cls._Z
621+
622+
@property
623+
def Z(cls):
624+
if getattr(cls, '_X', None) is None:
625+
cls._Z = cls._generate_clifford_from_known_gate(1, pauli_gates.Z)
626+
return cls._Z
627+
628+
@property
629+
def H(cls):
630+
if getattr(cls, '_H', None) is None:
631+
cls._H = cls._generate_clifford_from_known_gate(1, common_gates.H)
632+
return cls._H
633+
634+
@property
635+
def S(cls):
636+
if getattr(cls, '_S', None) is None:
637+
cls._S = cls._generate_clifford_from_known_gate(1, common_gates.S)
638+
return cls._S
639+
640+
@property
641+
def CNOT(cls):
642+
if getattr(cls, '_CNOT', None) is None:
643+
cls._CNOT = cls._generate_clifford_from_known_gate(2, common_gates.CNOT)
644+
return cls._CNOT
645+
646+
@property
647+
def CZ(cls):
648+
if getattr(cls, '_CZ', None) is None:
649+
cls._CZ = cls._generate_clifford_from_known_gate(2, common_gates.CZ)
650+
return cls._CZ
651+
652+
@property
653+
def SWAP(cls):
654+
if getattr(cls, '_SWAP', None) is None:
655+
cls._SWAP = cls._generate_clifford_from_known_gate(2, common_gates.SWAP)
656+
return cls._SWAP
657+
658+
659+
class CommonCliffordGates(metaclass=CommonCliffordGateMetaClass):
660+
661+
# We need to use the lazy initialization of these common gates since they need to use
662+
# cirq.sim, which can not be imported when
663+
@classmethod
664+
def _generate_clifford_from_known_gate(
665+
cls, num_qubits: int, gate: raw_types.Gate
666+
) -> 'CliffordGate':
667+
qubits = devices.LineQubit.range(num_qubits)
668+
t = qis.CliffordTableau(num_qubits=num_qubits)
669+
args = sim.ActOnCliffordTableauArgs(
670+
tableau=t, qubits=qubits, prng=np.random.RandomState(), log_of_measurement_results={}
671+
)
672+
673+
protocols.act_on(gate, args, qubits, allow_decompose=False)
674+
return CliffordGate.from_clifford_tableau(args.tableau)
675+
676+
@classmethod
677+
def from_clifford_tableau(cls, tableau: qis.CliffordTableau) -> 'CliffordGate':
678+
"""Create the CliffordGate instance from Clifford Tableau.
679+
680+
Args:
681+
tableau: A CliffordTableau to define the effect of Clifford Gate applying on
682+
the stabilizer state or Pauli group. The meaning of tableau here is
683+
To X Z sign
684+
from X [ X_x Z_x | r_x ]
685+
from Z [ X_z Z_z | r_z ]
686+
Each row in the Clifford tableau indicates how the transformation of original
687+
Pauli gates to the new gates after applying this Clifford Gate.
688+
689+
Returns:
690+
A CliffordGate instance, which has the transformation defined by
691+
the input tableau.
692+
693+
Raises:
694+
ValueError: When input tableau is wrong type or the tableau does not
695+
satisfy the symplectic property.
696+
"""
697+
if not isinstance(tableau, qis.CliffordTableau):
698+
raise ValueError('Input argument has to be a CliffordTableau instance.')
699+
if not tableau._validate():
700+
raise ValueError('It is not a valid Clifford tableau.')
701+
return CliffordGate(_clifford_tableau=tableau)
702+
703+
@classmethod
704+
def from_op_list(
705+
cls, operations: Sequence[raw_types.Operation], qubit_order: Sequence[raw_types.Qid]
706+
) -> 'CliffordGate':
707+
"""Construct a new Clifford gates from several known operations.
708+
709+
Args:
710+
operations: A list of cirq operations to construct the Clifford gate.
711+
The combination order is the first element in the list applies the transformation
712+
on the stabilizer state first.
713+
qubit_order: Determines how qubits are ordered when decomposite the operations.
714+
715+
Returns:
716+
A CliffordGate instance, which has the transformation on the stabilizer
717+
state equivalent to the composition of operations.
718+
719+
Raises:
720+
ValueError: When one or more operations do not have stabilizer effect.
721+
"""
722+
for op in operations:
723+
if op.gate and op.gate._has_stabilizer_effect_():
724+
continue
725+
raise ValueError(
726+
"Clifford Gate can only be constructed from the "
727+
"operations that has stabilizer effect."
728+
)
729+
730+
base_tableau = qis.CliffordTableau(len(qubit_order))
731+
args = sim.clifford.ActOnCliffordTableauArgs(
732+
tableau=base_tableau,
733+
qubits=qubit_order,
734+
prng=np.random.RandomState(0), # unused
735+
log_of_measurement_results={}, # unused
736+
)
737+
for op in operations:
738+
protocols.act_on(op, args, allow_decompose=True)
739+
740+
return CliffordGate.from_clifford_tableau(args.tableau)
741+
742+
@classmethod
743+
def _from_json_dict_(cls, n, rs, xs, zs, **kwargs):
744+
_clifford_tableau = qis.CliffordTableau._from_json_dict_(
745+
n,
746+
rs,
747+
xs,
748+
zs,
749+
)
750+
return cls(_clifford_tableau=_clifford_tableau)
751+
752+
753+
def _pad_tableau(
754+
clifford_tableau: qis.CliffordTableau, num_qubits_after_padding: int, axes: List[int]
755+
) -> qis.CliffordTableau:
756+
"""Roughly, this function copies self.tabluea into the "identity" matrix."""
757+
# Sanity check
758+
if len(set(axes)) != clifford_tableau.n:
759+
raise ValueError(
760+
"Input axes of padding should match with the number of qubits in the input tableau."
761+
)
762+
if clifford_tableau.n > num_qubits_after_padding:
763+
raise ValueError(
764+
"The number of qubits in the input tableau should not be larger than "
765+
"num_qubits_after_padding."
766+
)
767+
768+
padded_tableau = qis.CliffordTableau(num_qubits_after_padding)
769+
v_index = np.concatenate((np.asarray(axes), num_qubits_after_padding + np.asarray(axes)))
770+
771+
padded_tableau.xs[np.ix_(v_index, axes)] = clifford_tableau.xs
772+
padded_tableau.zs[np.ix_(v_index, axes)] = clifford_tableau.zs
773+
padded_tableau.rs[v_index] = clifford_tableau.rs
774+
return padded_tableau
775+
776+
777+
@value.value_equality
778+
class CliffordGate(raw_types.Gate, CommonCliffordGates):
779+
"""Clifford rotation for N-qubit."""
780+
781+
def __init__(
782+
self,
783+
*,
784+
_clifford_tableau: qis.CliffordTableau,
785+
) -> None:
786+
# We use the Clifford tableau to represent a Clifford gate.
787+
# It is crucial to note that the meaning of tableau here is different
788+
# from the one used to represent a Clifford state (Of course, they are related).
789+
# A) We have to use the full 2n * (2n + 1) matrix
790+
# B) The meaning of tableau here is
791+
# X Z sign
792+
# from X [ X_x Z_x | r_x ]
793+
# from Z [ X_z Z_z | r_z ]
794+
# Each row in the Clifford tableau means the transformation of original Pauli gates.
795+
# For example, take a 2 * (2+1) tableau as example:
796+
# X Z r
797+
# XI [ 1 0 | 1 0 | 0 ]
798+
# IX [ 0 0 | 1 1 | 0 ]
799+
# ZI [ 0 0 | 1 0 | 1 ]
800+
# IZ [ 1 0 | 1 1 | 0 ]
801+
# Take the third row as example: this means the ZI gate after the this gate,
802+
# more precisely the conjugate transformation of ZI by this gate, becomes -ZI.
803+
# (Note the real clifford tableau has to satify the Symplectic property.
804+
# here is just for illustration)
805+
self._clifford_tableau = _clifford_tableau.copy()
806+
807+
@property
808+
def clifford_tableau(self):
809+
return self._clifford_tableau
810+
811+
def _json_dict_(self) -> Dict[str, Any]:
812+
json_dict = self._clifford_tableau._json_dict_()
813+
return json_dict
814+
815+
def _value_equality_values_(self):
816+
return self.clifford_tableau
817+
818+
def _num_qubits_(self):
819+
return self.clifford_tableau.n
820+
821+
def _has_stabilizer_effect_(self) -> Optional[bool]:
822+
# By definition, Clifford Gate should always return True.
823+
return True
824+
825+
def __pow__(self, exponent) -> 'CliffordGate':
826+
if exponent == -1:
827+
return CliffordGate.from_clifford_tableau(self.clifford_tableau.inverse())
828+
if exponent > 0 and int(exponent) == exponent:
829+
base_tableau = self.clifford_tableau.copy()
830+
for _ in range(int(exponent) - 1):
831+
base_tableau = base_tableau.then(self.clifford_tableau)
832+
return CliffordGate.from_clifford_tableau(base_tableau)
833+
if exponent < 0 and int(exponent) == exponent:
834+
base_tableau = self.clifford_tableau.copy()
835+
for _ in range(int(-exponent) - 1):
836+
base_tableau = base_tableau.then(self.clifford_tableau)
837+
return CliffordGate.from_clifford_tableau(base_tableau.inverse())
838+
839+
return NotImplemented
840+
841+
def __repr__(self) -> str:
842+
return f"Clifford Gate with Tableau:\n {self.clifford_tableau._str_full_()}"
843+
844+
def _commutes_(self, other: Any, atol: float) -> Union[bool, NotImplementedType, None]:
845+
# Note even if we assume two gates define the tabluea based on the same qubit order,
846+
# the following approach cannot judge it:
847+
# self.clifford_tableau.then(other.clifford_tableau) == other.clifford_tableau.then(
848+
# self.clifford_tableau
849+
# )
850+
# For example: X.then(Z) and Z.then(X) both return same tableau
851+
# it is because Clifford tableau ignores the global phase information.
852+
return NotImplemented
853+
854+
def _decompose_(self, qubits: Sequence['cirq.Qid']) -> List[raw_types.Operation]:
855+
return transformers.analytical_decompositions.decompose_clifford_tableau_to_operations(
856+
list(qubits), self.clifford_tableau
857+
)
858+
859+
def _act_on_(self, args: 'cirq.ActOnArgs', qubits: Sequence['cirq.Qid']) -> bool:
860+
861+
# Note the computation complexity difference between _decompose_ and _act_on_.
862+
# Suppose this Gate has `m` qubits, args has `n` qubits, and the decomposition of
863+
# this operation into `k` operations:
864+
# 1. Direct act_on is O(n^3) -- two matrices multiplication
865+
# 2. Decomposition is O(m^3)+O(k*n^2) -- Decomposition complexity + k * One/two-qubits Ops
866+
# So when m << n, the decomposition is more efficient.
867+
if isinstance(args, sim.clifford.ActOnCliffordTableauArgs):
868+
axes = args.get_axes(qubits)
869+
# This padding is important and cannot be omitted.
870+
padded_tableau = _pad_tableau(self._clifford_tableau, len(args.qubits), axes)
871+
args._state = args.tableau.then(padded_tableau)
872+
return True
873+
874+
if isinstance(args, sim.clifford.ActOnStabilizerCHFormArgs):
875+
# Do we know how to apply CliffordTableau on ActOnStabilizerCHFormArgs?
876+
# It should be unlike because CliffordTableau ignores the global phase but CHForm
877+
# is aimed to fix that.
878+
return NotImplemented
879+
880+
return NotImplemented

0 commit comments

Comments
 (0)