"""Abstractions for code definitions."""
from __future__ import annotations
from dataclasses import dataclass
from functools import cached_property
import numpy as np
from zixy.container.coeffs import ComplexSign
from zixy.qubit import pauli
[docs]
class CodeDefinitionError(ValueError):
"""Raised when the definition of a stabilizer code is invalid."""
[docs]
@dataclass(frozen=True)
class StabilizerCode:
"""Definition of a stabilizer code.
Stores the following information
* n_physical_qubits (:math:`n`) - Number of physical qubits in a code block.
* n_logical_qubits (:math:`k`) - Number of logical qubits in a code block
* distance (:math:`d`) - The distance of the stabilizer code.
* generators - A set of :math:`(n-k)` commuting stabilizer generators.
* x_logicals - The logical :math:`X` operators of the stabilizer code.
* z_logicals - The logical :math:`Z` operators of the stabilizer code.
.. code-block:: python
from guppyft.code_def import StabilizerCode
STEANE_DEF = StabilizerCode.from_python_strings(
n_physical_qubits=7,
n_logical_qubits=1,
distance=3,
generators=["XXXXIII", "IXXIXXI", "IIXXIXX",
"ZZZZIII", "IZZIZZI", "IIZZIZZ"],
x_logicals=["XXXXXXX"],
z_logicals=["ZZZZZZZ"],
)
"""
n_physical_qubits: int
n_logical_qubits: int
distance: int
generators: pauli.SignTermSet
x_logicals: pauli.SignTerms
z_logicals: pauli.SignTerms
@cached_property
def y_logicals(self) -> pauli.SignTerms:
"""Return the :math:`Y` logical operators derived from :math:`X`
and :math:`Z` logicals."""
terms = pauli.ComplexSignTerms(self.n_physical_qubits)
for j in range(self.n_logical_qubits):
# ComplexSign(k) ~ i^k
# y_logicals[j] = i * (x_logicals[j] * z_logicals[j])
# y_term will always have a real (+/-)1 coefficient.
y_term = ComplexSign(1) * (self.x_logicals[j] * self.z_logicals[j]) # type: ignore[operator]
terms.append(y_term)
return terms.into(pauli.SignTerms)
[docs]
def __post_init__(self) -> None:
"""Validate the stabilizer-code definition."""
if len(self.generators) != self.n_physical_qubits - self.n_logical_qubits:
raise CodeDefinitionError(
"The number of stabilizer generators must equal n-k."
+ f" Got n={self.n_physical_qubits}, "
+ f"k={self.n_logical_qubits} with {len(self.generators)} generators."
)
if len(self.x_logicals) != self.n_logical_qubits:
raise CodeDefinitionError(
"Incorrect number of X logical operators: "
f"expected {self.n_logical_qubits}, "
f"got {len(self.x_logicals)}."
)
if len(self.z_logicals) != self.n_logical_qubits:
raise CodeDefinitionError(
"Incorrect number of Z logical operators: "
f"expected {self.n_logical_qubits}, "
f"got {len(self.z_logicals)}."
)
strings: pauli.Strings = self.generators.into(pauli.Strings)
all_stabilizer_generators_commute = np.all(strings.compatibility_matrix() == 1)
if not all_stabilizer_generators_commute:
raise CodeDefinitionError("All of the stabilizer generators must commute!")
[docs]
@staticmethod
def from_python_strings(
n_physical_qubits: int,
n_logical_qubits: int,
distance: int,
generators: list[str],
x_logicals: list[str],
z_logicals: list[str],
) -> StabilizerCode:
r"""Helper to create a :py:class:`StabilizerCode` from lists of Python strings.
The strings must be defined over the alphabet :math:`\{I, X, Y, Z\}` and
must be of length equal to the number of physical qubits.
A sign may be provided at the front. If a string is missing a sign,
it is assumed to be positive.
Args:
n_physical_qubits: The number of physical qubits in the code.
n_logical_qubits: The number of logical qubits in the code.
distance: The distance of the code.
generators: A list of stabilizer generators as Pauli strings.
x_logicals: A list of :math:`X` logical operators as Pauli strings.
z_logicals: A list of :math:`Z` logical operators as Pauli strings.
Returns:
A `StabilizerCode` instance representing the code.
"""
zixy_generators = pauli.SignTermSet.from_iterable(
(_str_to_zixy(s, n_physical_qubits) for s in generators),
n_physical_qubits,
)
zixy_x_logicals = pauli.SignTerms.from_iterable(
(_str_to_zixy(s, n_physical_qubits) for s in x_logicals),
n_physical_qubits,
)
zixy_z_logicals = pauli.SignTerms.from_iterable(
(_str_to_zixy(s, n_physical_qubits) for s in z_logicals),
n_physical_qubits,
)
return StabilizerCode(
n_physical_qubits=n_physical_qubits,
n_logical_qubits=n_logical_qubits,
distance=distance,
generators=zixy_generators,
x_logicals=zixy_x_logicals,
z_logicals=zixy_z_logicals,
)
def _str_to_zixy(s: str, n: int) -> pauli.SignTerm:
if s[0] not in "+-":
sign = "+"
else:
sign = s[0]
s = s[1:] # Drop the sign, since it is in a separate variable
if len(s) != n:
raise CodeDefinitionError(
f"All Pauli strings must be of length {n}. "
f"Got string '{s}' of length {len(s)}."
)
if not all(c in "IXYZ" for c in s):
raise CodeDefinitionError(
f"All Pauli strings must be defined over the alphabet {{I, X, Y, Z}}. "
f"Got string '{s}' with invalid characters."
)
pauli_str = "".join(f"{c}{i} " for i, c in enumerate(s))
return pauli.SignTerm.from_str(f"({sign}1, {pauli_str})", n)
def _identity_code(k: int) -> StabilizerCode:
"""Return a stabilizer code that encodes :math:`k` logical qubits into
:math:`k` physical qubits. This is the trivial code with distance 1.
Args:
k: The number of logical qubits to encode.
Returns:
A `StabilizerCode` instance representing the identity code.
"""
return StabilizerCode.from_python_strings(
n_physical_qubits=k,
n_logical_qubits=k,
distance=1,
generators=[],
x_logicals=["I" * i + "X" + "I" * (k - i - 1) for i in range(k)],
z_logicals=["I" * i + "Z" + "I" * (k - i - 1) for i in range(k)],
)