diff --git a/src/mqt/qecc/circuit_synthesis/faults.py b/src/mqt/qecc/circuit_synthesis/faults.py index 8da7d68e..32a220c0 100644 --- a/src/mqt/qecc/circuit_synthesis/faults.py +++ b/src/mqt/qecc/circuit_synthesis/faults.py @@ -28,14 +28,27 @@ class PureFaultSet: """Represents a collection of pure faults (X-type or Z-type) in a quantum circuit.""" - def __init__(self, num_qubits: int) -> None: + def __init__(self, num_qubits: int, kind: str = "X") -> None: """Initialize a PureFaultSet object. Args: num_qubits: The number of qubits in the circuit. + kind: The type of faults that this PureFaultSet represents ('X' or 'Z'). """ self.num_qubits = num_qubits self.faults = np.zeros((0, num_qubits), dtype=np.int8) # Pure faults as binary vectors + self.kind = kind + + @property + def kind(self) -> str: + """Return the type of faults in the set ('X' or 'Z').""" + return self._kind + + @kind.setter + def kind(self, value: str) -> None: + """Set the type of faults in the set ('X' or 'Z').""" + assert value.upper() in {"X", "Z"}, "Kind must be either 'X' or 'Z'." + self._kind = value.upper() def add_fault(self, fault: npt.NDArray[np.int8]) -> None: """Add a fault to the fault set. @@ -72,10 +85,14 @@ def combine(self, other: PureFaultSet, inplace: bool = False) -> PureFaultSet: raise ValueError(msg) combined_faults = np.vstack([self.faults, other.faults]) + if self.kind != other.kind: + msg = "Fault sets must have the same kind to combine." + raise ValueError(msg) + if inplace: self.faults = combined_faults return self - return PureFaultSet.from_fault_array(combined_faults) + return PureFaultSet.from_fault_array(combined_faults, kind=self.kind) def to_array(self) -> npt.NDArray[np.int8]: """Convert the fault set to a numpy array. @@ -86,7 +103,7 @@ def to_array(self) -> npt.NDArray[np.int8]: return self.faults @classmethod - def from_fault_array(cls, array: npt.NDArray[np.int8]) -> PureFaultSet: + def from_fault_array(cls, array: npt.NDArray[np.int8], kind: str = "X") -> PureFaultSet: """Create a PureFaultSet from a numpy array of faults. Returns: @@ -95,7 +112,7 @@ def from_fault_array(cls, array: npt.NDArray[np.int8]) -> PureFaultSet: if array.ndim != 2: msg = "Input array must be 2-dimensional." raise ValueError(msg) - fault_set = cls(array.shape[1]) + fault_set = cls(array.shape[1], kind=kind) fault_set.faults = np.unique(array, axis=0) return fault_set @@ -124,7 +141,9 @@ def from_cnot_circuit(cls, circ: CNOTCircuit, kind: str = "X", reduce: bool = Fa qubit_faults[ctrl].append(new_fault) # Create the fault set - fs = cls.from_fault_array(np.array([fault for faults in qubit_faults for fault in faults], dtype=np.int8)) + fs = cls.from_fault_array( + np.array([fault for faults in qubit_faults for fault in faults], dtype=np.int8), kind=kind + ) if not reduce: return fs @@ -242,7 +261,7 @@ def __eq__(self, other: object) -> bool: """ if not isinstance(other, PureFaultSet): return False - return self.num_qubits == other.num_qubits and self.to_set() == other.to_set() + return self.num_qubits == other.num_qubits and self.to_set() == other.to_set() and self.kind == other.kind def __hash__(self) -> int: """Return a hash of the PureFaultSet. @@ -258,7 +277,7 @@ def copy(self) -> PureFaultSet: Returns: A new PureFaultSet object with the same faults and number of qubits. """ - new_set = PureFaultSet(self.num_qubits) + new_set = PureFaultSet(self.num_qubits, kind=self.kind) new_set.faults = np.copy(self.faults) return new_set @@ -352,7 +371,7 @@ def filter_faults(self, pred: Callable[[npt.NDArray[np.int8]], bool], inplace: b self.faults = filtered return self - return PureFaultSet.from_fault_array(filtered) + return PureFaultSet.from_fault_array(filtered, kind=self.kind) def permute_qubits(self, permutation: npt.NDArray[np.int8] | list[int], inplace: bool = True) -> PureFaultSet: """Permute the qubits in the fault set according to a given permutation. @@ -373,7 +392,38 @@ def permute_qubits(self, permutation: npt.NDArray[np.int8] | list[int], inplace: self.faults = permuted_faults return self - return PureFaultSet.from_fault_array(permuted_faults) + return PureFaultSet.from_fault_array(permuted_faults, kind=self.kind) + + def apply_cnot(self, control: int, target: int, inplace: bool = True) -> PureFaultSet: + """Apply a CNOT gate to the faults in the set, based on the type of faults (X or Z). + + Args: + control: The index of the control qubit. + target: The index of the target qubit. + inplace: If True, modifies the current fault set. If False, returns a new PureFaultSet with updated faults. + + Returns: + A new PureFaultSet with updated faults if inplace is False. + """ + if not (0 <= control < self.num_qubits) or not (0 <= target < self.num_qubits): + msg = f"Control and target indices must be between 0 and {self.num_qubits - 1}." + raise ValueError(msg) + # Dev Note: We do not allow negative indices so that we can easily check if control and target are different + if control == target: + msg = "Control and target qubits must be different." + raise ValueError(msg) + + updated_faults = np.copy(self.faults) + if self.kind == "X": + updated_faults[:, target] ^= updated_faults[:, control] + else: # self.kind == "Z" + updated_faults[:, control] ^= updated_faults[:, target] + + if inplace: + self.faults = updated_faults + return self + + return PureFaultSet.from_fault_array(updated_faults, kind=self.kind) def coset_leader(fault: npt.NDArray[np.int8], generators: npt.NDArray[np.int8]) -> npt.NDArray[np.int8]: @@ -482,3 +532,363 @@ def t_distinct(fs1: PureFaultSet, fs2: PureFaultSet, t: int, stabs: npt.NDArray[ return False # if no solution was found, the fault sets are t-distinct return True + + +class XZFaultList: + """Represents an ordered list of coupled pure faults (X-type and Z-type) in a quantum circuit.""" + + def __init__(self, num_qubits: int) -> None: + """Initialise a XZFaultList object. + + Args: + num_qubits (int): The number of qubits in the circuit + """ + self.num_qubits = num_qubits + self.faults = { + "X": np.zeros((0, num_qubits), dtype=np.int8), + "Z": np.zeros((0, num_qubits), dtype=np.int8), + } + + def add_fault(self, faults: tuple[npt.NDArray[np.int8] | None, npt.NDArray[np.int8] | None]) -> None: + """Add a single fault pair (X error, Z error) to the fault list. + + Args: + faults: A tuple of (x_fault, z_fault) where each is a 1D numpy array. + Each array must have length num_qubits. + One of the faults may be set to None, which is treated as an all-zero fault. + + Raises: + ValueError: If fault arrays don't have the correct length. + ValueError: If both faults are None + """ + assert len(faults) == 2, "Faults should be a tuple of x_fault and z_fault" + + x_fault, z_fault = faults + if x_fault is None and z_fault is None: + msg = "At least one fault must be provided." + raise ValueError(msg) + + if x_fault is None: + z_fault = np.asarray(z_fault, dtype=np.int8) + x_fault = np.zeros(self.num_qubits, dtype=np.int8) + elif z_fault is None: + x_fault = np.asarray(x_fault, dtype=np.int8) + z_fault = np.zeros(self.num_qubits, dtype=np.int8) + else: + x_fault = np.asarray(x_fault, dtype=np.int8) + z_fault = np.asarray(z_fault, dtype=np.int8) + + if x_fault.shape[0] != self.num_qubits or z_fault.shape[0] != self.num_qubits: + msg = f"Faults must have length {self.num_qubits}." + raise ValueError(msg) + + self.faults["X"] = np.vstack([self.faults["X"], x_fault]) + self.faults["Z"] = np.vstack([self.faults["Z"], z_fault]) + + def add_faults(self, faults: tuple[npt.NDArray[np.int8] | None, npt.NDArray[np.int8] | None]) -> None: + """Add multiple fault pairs to the fault list. + + Args: + faults: A tuple of (x_faults, z_faults) where each is a 2D numpy array. + Each array should have num_qubits columns. + One of the faults may be set to None, which is treated as an all-zero fault. + + Raises: + ValueError: If fault arrays don't have the correct shape. + ValueError: If both fault arrays are None + """ + x_faults, z_faults = faults + if x_faults is None and z_faults is None: + msg = "At least one fault array must be provided." + raise ValueError(msg) + + if x_faults is None: + z_faults = np.asarray(z_faults, dtype=np.int8) + x_faults = np.zeros_like(z_faults, dtype=np.int8) + elif z_faults is None: + x_faults = np.asarray(x_faults, dtype=np.int8) + z_faults = np.zeros_like(x_faults, dtype=np.int8) + else: + x_faults = np.asarray(x_faults, dtype=np.int8) + z_faults = np.asarray(z_faults, dtype=np.int8) + + if x_faults.shape[1] != self.num_qubits or z_faults.shape[1] != self.num_qubits: + msg = f"Faults must have {self.num_qubits} columns." + raise ValueError(msg) + + self.faults["X"] = np.vstack([self.faults["X"], x_faults]) + self.faults["Z"] = np.vstack([self.faults["Z"], z_faults]) + + def copy(self) -> XZFaultList: + """Create a copy of the XZFaultList. + + Returns: + A new XZFaultList object with copied fault arrays. + """ + new_list = XZFaultList(self.num_qubits) + new_list.faults["X"] = np.copy(self.faults["X"]) + new_list.faults["Z"] = np.copy(self.faults["Z"]) + return new_list + + def __iter__(self) -> Iterator[tuple[npt.NDArray[np.int8], npt.NDArray[np.int8]]]: + """Iterate over fault pairs in the list. + + Yields: + Tuples of (x_fault, z_fault) for each row in the fault arrays. + """ + for i in range(len(self.faults["X"])): + yield (self.faults["X"][i], self.faults["Z"][i]) + + def apply_cnot(self, control: int, target: int, inplace: bool = True) -> XZFaultList: + """Apply a CNOT gate to the faults in the list. + + For X-type faults: target qubit is affected by control qubit (target ^= control). + For Z-type faults: control qubit is affected by target qubit (control ^= target). + + Args: + control: The index of the control qubit. + target: The index of the target qubit. + inplace: If True, modifies the current fault list. If False, returns a new XZFaultList. + + Returns: + A new XZFaultList with updated faults if inplace is False, otherwise self. + + Raises: + ValueError: If control or target indices are out of range or equal. + """ + self.ensure_apply_valid_input(control, target) + + if inplace: + # Apply CNOT directly to self.faults + x_faults, z_faults = self.faults["X"], self.faults["Z"] + ret = self + else: + # Create a new XZFaultList with copied faults + new_list = XZFaultList(self.num_qubits) + new_list.faults["X"] = np.copy(self.faults["X"]) + new_list.faults["Z"] = np.copy(self.faults["Z"]) + + x_faults, z_faults = new_list.faults["X"], new_list.faults["Z"] + ret = new_list + + # Apply CNOT + x_faults[:, target] ^= x_faults[:, control] + z_faults[:, control] ^= z_faults[:, target] + + return ret + + def apply_hadamard(self, qubit: int, inplace: bool = True) -> XZFaultList: + """Apply a Hadamard gate to the faults in the list. + + A Hadamard gate swaps X and Z errors on the specified qubit. + + Args: + qubit: The index of the qubit. + inplace: If True, modifies the current fault list. If False, returns a new XZFaultList. + + Returns: + A new XZFaultList with updated faults if inplace is False, otherwise self. + + Raises: + ValueError: If qubit index is out of range. + """ + self.ensure_apply_valid_input(qubit) + + if inplace: + # Atomic swap using tuple assignment; use copies on RHS to avoid overlap + self.faults["X"][:, qubit], self.faults["Z"][:, qubit] = ( + self.faults["Z"][:, qubit].copy(), + self.faults["X"][:, qubit].copy(), + ) + return self + + # Create a new XZFaultList with copied and swapped faults + new_list = XZFaultList(self.num_qubits) + new_list.faults["X"] = np.copy(self.faults["X"]) + new_list.faults["Z"] = np.copy(self.faults["Z"]) + + # Atomic swap on the copies + new_list.faults["X"][:, qubit], new_list.faults["Z"][:, qubit] = ( + new_list.faults["Z"][:, qubit].copy(), + new_list.faults["X"][:, qubit].copy(), + ) + + return new_list + + def apply_reset(self, qubit: int, inplace: bool = True) -> XZFaultList: + """Apply a reset operation to the faults in the list. + + A reset removes any accumulated X and Z errors on the specified qubit. + + Args: + qubit: The index of the qubit. + inplace: If True, modifies the current fault list. If False, returns a new XZFaultList. + + Returns: + A new XZFaultList with updated faults if inplace is False, otherwise self. + + Raises: + ValueError: If qubit index is out of range. + """ + self.ensure_apply_valid_input(qubit) + + if inplace: + self.faults["X"][:, qubit] = 0 + self.faults["Z"][:, qubit] = 0 + return self + + new_list = XZFaultList(self.num_qubits) + new_list.faults["X"] = np.copy(self.faults["X"]) + new_list.faults["Z"] = np.copy(self.faults["Z"]) + new_list.faults["X"][:, qubit] = 0 + new_list.faults["Z"][:, qubit] = 0 + return new_list + + def apply_ccz(self, control1: int, control2: int, control3: int, inplace: bool = True) -> XZFaultList: + """Apply a CCZ gate to the faults in the list. + + The propagation model is adversarial: any pair of X faults on two controls + will induce a Z fault on the third control. + We can do this also because the given circuit is assumed to be fault tolerant. + + Note: CCZ is symmetrical, thus there is no "target" per se + + Args: + control1: The first control qubit. + control2: The second control qubit. + control3: The third control qubit. + inplace: If True, modifies the current fault list. If False, returns a new XZFaultList. + + Returns: + A new XZFaultList with updated faults if inplace is False, otherwise self. + + Raises: + ValueError: If any control index is out of range. + ValueError: If any control qubits are not distinct. + """ + # Z faults just get propagated through + # Only X faults are problematic + + # By right, the state of the qubits matter, which is why you can't simply propagate pauli gates through a CCZ gate. + + # Adversarial Fault Propagation for CCZ: + # We do a simple logic, that every pair of X faults leads, in the worst case, to a Z fault on the other control. So we can just add all pairs of X faults as Z faults. + # Z_i ^= (X_j & X_k) for all distinct i, j, k in {control1, control2, control3} + + self.ensure_apply_valid_input(control1, control2, control3) + + if inplace: + x_faults, z_faults = self.faults["X"], self.faults["Z"] + ret = self + else: + new_list = XZFaultList(self.num_qubits) + new_list.faults["X"] = np.copy(self.faults["X"]) + new_list.faults["Z"] = np.copy(self.faults["Z"]) + x_faults, z_faults = new_list.faults["X"], new_list.faults["Z"] + ret = new_list + + z_faults[:, control1] ^= x_faults[:, control2] & x_faults[:, control3] + z_faults[:, control2] ^= x_faults[:, control1] & x_faults[:, control3] + z_faults[:, control3] ^= x_faults[:, control1] & x_faults[:, control2] + + return ret + + def apply_ccx(self, control1: int, control2: int, target: int, inplace: bool = True) -> XZFaultList: + """Apply a CCX (Toffoli) gate to the faults in the list, by applying a H_target x CCZ x H_target. + + Args: + control1: The first control qubit. + control2: The second control qubit. + target: The target qubit. + inplace: If True, modifies the current fault list. If False, returns a new XZFaultList. + + Returns: + A new XZFaultList with updated faults if inplace is False, otherwise self. + + Raises: + ValueError: If any qubit index is out of range. + ValueError: If qubits are not distinct. + """ + self.ensure_apply_valid_input(control1, control2, target) + + fault_list = self.apply_hadamard(target, inplace=inplace) + fault_list.apply_ccz(control1, control2, target) + fault_list.apply_hadamard(target) + + return fault_list + + def ensure_apply_valid_input(self, *qubits: int) -> bool: + """Ensures that the input into apply_* functions are valid. + + Returns: + bool: True if everything is okay + + Raises: + ValueError: If any qubit index is out of range. + ValueError: If qubits are not distinct. + """ + n_q = len(qubits) + if any(not 0 <= q < self.num_qubits for q in qubits): + msg = f"Qubit {'indices' if n_q > 1 else 'index'} must be between 0 and {self.num_qubits - 1}." + raise ValueError(msg) + if n_q > 1 and len(set(qubits)) != n_q: + msg = "All qubits must be different." + raise ValueError(msg) + + return True + + def reduce_to_coset_leaders( + self, generators: tuple[npt.NDArray[np.int8] | None, npt.NDArray[np.int8] | None], inplace: bool = True + ) -> XZFaultList: + """Reduce fault list to coset leaders using provided generators. + + Applies coset leader reduction to X and Z type faults independently using the + corresponding generators. This is useful for reducing error syndromes to their + canonical representatives in quantum error correction. + + Args: + generators (Tuple[npt.NDArray[np.int8] | None, npt.NDArray[np.int8] | None]): + Tuple of (x_generators, z_generators). Each should be a 2D numpy array with + shape (num_generators, num_qubits) or None to skip reduction for that error type. + inplace (bool, optional): If True, modify this fault list in place. + If False, return a copy with reductions applied. Defaults to True. + + Returns: + XZFaultList: The reduced fault list (self if inplace=True, otherwise a copy). + + Raises: + ValueError: If any generator array has incorrect dimensions (must be 2D with num_qubits columns). + AssertionError: If generators tuple length is not 2. + """ + # Setting the corresponding generator to None means no reduction is done + + assert len(generators) == 2, "Generators should be a tuple of x_generators and z_generators" + + # use qecc_faults.coset_leader(single_fault, generators) for x and z + ret = self if inplace else self.copy() + + for error_type, g_ in zip(ret.faults, generators, strict=False): + # Ensure generators are numpy arrays (may be empty) + g = None if g_ is None else np.asarray(g_, dtype=np.int8) + + # Check sizes + if g is not None and (g.ndim != 2 or g.shape[1] != self.num_qubits): + msg = f"Generators must be a 2D array with {self.num_qubits} columns." + raise ValueError(msg) + + if ret.faults[error_type].shape[0] > 0 and g is not None and g.size > 0: + for i in range(ret.faults[error_type].shape[0]): + ret.faults[error_type][i] = np.asarray(coset_leader(ret.faults[error_type][i], g), dtype=np.int8) + + return ret + + def __repr__(self) -> str: + """Return a string representation of the XZFaultList.""" + repr_ = [ + object.__repr__(self) + f" num_qubits: {self.num_qubits}", + "X:", + repr(self.faults["X"]), + "Z:", + repr(self.faults["Z"]), + ] + return "\n".join(repr_) diff --git a/tests/circuit_synthesis/test_faults.py b/tests/circuit_synthesis/test_faults.py index a4f220d6..1729b18e 100644 --- a/tests/circuit_synthesis/test_faults.py +++ b/tests/circuit_synthesis/test_faults.py @@ -13,7 +13,7 @@ import pytest from mqt.qecc.circuit_synthesis.circuits import CNOTCircuit -from mqt.qecc.circuit_synthesis.faults import PureFaultSet, coset_leader, stabilizer_equivalent, t_distinct +from mqt.qecc.circuit_synthesis.faults import PureFaultSet, XZFaultList, coset_leader, stabilizer_equivalent, t_distinct @pytest.fixture @@ -64,16 +64,42 @@ def test_add_fault_invalid_length(): def test_combine_fault_sets(): """Test combining two fault sets.""" - fault_set_1 = PureFaultSet(num_qubits=3) + fault_set_1 = PureFaultSet(num_qubits=3, kind="Z") fault_set_1.add_fault(np.array([1, 0, 1], dtype=np.int8)) - fault_set_2 = PureFaultSet(num_qubits=3) + fault_set_2 = PureFaultSet(num_qubits=3, kind="Z") fault_set_2.add_fault(np.array([0, 1, 0], dtype=np.int8)) # Combine the fault sets combined_fault_set = fault_set_1.combine(fault_set_2) expected = np.array([[1, 0, 1], [0, 1, 0]], dtype=np.int8) assert combined_fault_set.to_set() == set(map(tuple, expected)), "Fault sets were not combined correctly." + assert combined_fault_set.kind == "Z", "Fault kind was not preserved when combining fault sets." + + +def test_combine_fault_sets_different_kind(): + """Test combining two fault sets with different kinds.""" + fault_set_1 = PureFaultSet(num_qubits=3, kind="X") + fault_set_1.add_fault(np.array([1, 0, 1], dtype=np.int8)) + + fault_set_2 = PureFaultSet(num_qubits=3, kind="Z") + fault_set_2.add_fault(np.array([0, 1, 0], dtype=np.int8)) + + with pytest.raises(ValueError, match=r"Fault sets must have the same kind to combine."): + _ = fault_set_1.combine(fault_set_2) + + +def test_combine_fault_sets_inplace_false_propagates_kind(): + """Test that non-inplace combining preserves the left fault set kind.""" + fault_set_1 = PureFaultSet(num_qubits=3, kind="Z") + fault_set_1.add_fault(np.array([1, 0, 1], dtype=np.int8)) + + fault_set_2 = PureFaultSet(num_qubits=3, kind="Z") + fault_set_2.add_fault(np.array([0, 1, 0], dtype=np.int8)) + + combined_fault_set = fault_set_1.combine(fault_set_2, inplace=False) + assert combined_fault_set.kind == "Z", "Fault kind was not propagated for inplace=False combine." + assert fault_set_1.kind == "Z", "Original fault set kind should remain unchanged." def test_combine_fault_sets_invalid(): @@ -98,6 +124,14 @@ def test_from_fault_array(): assert set(map(tuple, result)) == set(map(tuple, faults)), "Fault set was not created correctly from array." +def test_from_fault_array_invalid_dimension(): + """Test creating a PureFaultSet from an array with invalid dimensions.""" + faults = np.array([1, 0, 1], dtype=np.int8) # 1D array instead of 2D + + with pytest.raises(ValueError, match=r"Input array must be 2-dimensional."): + PureFaultSet.from_fault_array(faults) + + @pytest.mark.parametrize( ("stabs_fixture", "initial_faults", "expected_faults"), [ @@ -619,21 +653,668 @@ def test_not_t_distinct_four_qubits(): def test_permute_qubits_basic(): """Test basic permutation of faults.""" faults = np.array([[1, 1, 0], [0, 1, 1]], dtype=np.int8) - fault_set = PureFaultSet.from_fault_array(faults) + fault_set = PureFaultSet.from_fault_array(faults, kind="Z") permutation = [2, 0, 1] permuted_fault_set = fault_set.permute_qubits(permutation, inplace=False) assert np.array_equal(permuted_fault_set.faults, faults[:, permutation]), "Faults were not permuted correctly" - assert fault_set == PureFaultSet.from_fault_array(faults), "Original fault set should remain unchanged" + assert fault_set == PureFaultSet.from_fault_array(faults, kind="Z"), "Original fault set should remain unchanged" + assert permuted_fault_set.kind == "Z", "Fault kind should be preserved after permutation" + assert fault_set.kind == "Z", "Original fault kind should be preserved after permutation" def test_permute_qubits_inplace(): """Test inplace permutation of fault set.""" faults = np.array([[1, 1, 0], [0, 0, 1]], dtype=np.int8) - fault_set = PureFaultSet.from_fault_array(faults) + fault_set = PureFaultSet.from_fault_array(faults, kind="Z") permutation = [2, 0, 1] fault_set.permute_qubits(permutation, inplace=True) - assert fault_set != PureFaultSet.from_fault_array(faults), "Faults were not permuted correctly in place" + assert fault_set != PureFaultSet.from_fault_array(faults, kind="Z"), "Faults were not permuted correctly in place" + + +def test_invalid_fault_kind(): + """Test that an invalid kind raises an assertion error.""" + with pytest.raises(AssertionError, match=r"Kind must be either 'X' or 'Z'."): + pfs = PureFaultSet(5, kind="Y") + + with pytest.raises(AssertionError, match=r"Kind must be either 'X' or 'Z'."): + pfs = PureFaultSet.from_fault_array(np.array([[1, 0, 1]], dtype=np.int8), kind="Y") + + pfs = PureFaultSet(5) + with pytest.raises(AssertionError, match=r"Kind must be either 'X' or 'Z'."): + pfs.kind = "Y" + + +def test_apply_cnot_x(): + """Test applying a CNOT gate to the fault set.""" + faults1 = np.array([[1, 0, 0]], dtype=np.int8) + fault_set1 = PureFaultSet.from_fault_array(faults1, kind="X") + + # Apply CNOT with control=0 and target=1 + fault_set1.apply_cnot(control=0, target=1) + + expected_faults1 = np.array([[1, 1, 0]], dtype=np.int8) + assert np.array_equal(fault_set1.to_array(), expected_faults1), ( + "CNOT gate was not applied correctly to the fault set" + ) + + faults2 = np.array([[0, 1, 0]], dtype=np.int8) + fault_set2 = PureFaultSet.from_fault_array(faults2, kind="X") + + # Apply CNOT with control=0 and target=1 + fault_set2.apply_cnot(control=0, target=1) + + expected_faults2 = np.array([[0, 1, 0]], dtype=np.int8) + assert np.array_equal(fault_set2.to_array(), expected_faults2), ( + "CNOT gate was not applied correctly to the fault set" + ) + + +def test_apply_cnot_z(): + """Test applying a CNOT gate to the fault set.""" + faults1 = np.array([[1, 0, 0]], dtype=np.int8) + fault_set1 = PureFaultSet.from_fault_array(faults1, kind="Z") + + # Apply CNOT with control=0 and target=1 + fault_set1.apply_cnot(control=0, target=1) + + expected_faults1 = np.array([[1, 0, 0]], dtype=np.int8) + assert np.array_equal(fault_set1.to_array(), expected_faults1), ( + "CNOT gate was not applied correctly to the fault set" + ) + + faults2 = np.array([[0, 1, 0]], dtype=np.int8) + fault_set2 = PureFaultSet.from_fault_array(faults2, kind="Z") + + # Apply CNOT with control=0 and target=1 + fault_set2.apply_cnot(control=0, target=1) + + expected_faults2 = np.array([[1, 1, 0]], dtype=np.int8) + assert np.array_equal(fault_set2.to_array(), expected_faults2), ( + "CNOT gate was not applied correctly to the fault set" + ) + + +def test_apply_cnot_invalid_qubits(): + """Test that applying a CNOT gate with invalid qubit indices raises an error.""" + faults = np.array([[1, 0, 0]], dtype=np.int8) + fault_set = PureFaultSet.from_fault_array(faults) + + with pytest.raises(ValueError, match=r"Control and target qubits must be different."): + fault_set.apply_cnot(control=0, target=0) + + with pytest.raises(ValueError, match=r"Control and target indices must be between 0 and 2."): + fault_set.apply_cnot(control=3, target=1) + + with pytest.raises(ValueError, match=r"Control and target indices must be between 0 and 2."): + fault_set.apply_cnot(control=-1, target=1) + + +def test_apply_cnot_not_inplace(): + """Test that applying a CNOT gate does not modify the original fault set when inplace=False.""" + faults = np.array([[1, 0, 0]], dtype=np.int8) + fault_set = PureFaultSet.from_fault_array(faults) + + # Apply CNOT with control=0 and target=1 without modifying the original fault set + new_fault_set = fault_set.apply_cnot(control=0, target=1, inplace=False) + + expected_new_faults = np.array([[1, 1, 0]], dtype=np.int8) + assert np.array_equal(new_fault_set.to_array(), expected_new_faults), ( + "CNOT gate was not applied correctly to the new fault set" + ) + assert np.array_equal(fault_set.to_array(), faults), "Original fault set should remain unchanged" + + +def test_pure_fault_set_copy(): + """Test that PureFaultSet.copy() returns an independent copy.""" + faults = np.array([[1, 0, 0], [0, 1, 1]], dtype=np.int8) + fault_set = PureFaultSet.from_fault_array(faults, kind="Z") + + copied_fault_set = fault_set.copy() + + assert copied_fault_set is not fault_set + assert np.array_equal(copied_fault_set.to_array(), fault_set.to_array()) + assert copied_fault_set.kind == fault_set.kind + + copied_fault_set.apply_cnot(control=0, target=1) + + assert not np.array_equal(copied_fault_set.to_array(), fault_set.to_array()) + assert np.array_equal(fault_set.to_array(), np.unique(faults, axis=0)) + + +"""XZFaultList Tests""" + + +@pytest.fixture +def fault_list() -> XZFaultList: + """Fixture to create a sample XZFaultList for testing.""" + faults = XZFaultList(num_qubits=3) + faults.add_fault((np.array([1, 0, 1], dtype=np.int8), np.array([0, 1, 0], dtype=np.int8))) + faults.add_fault((np.array([0, 1, 0], dtype=np.int8), np.array([1, 0, 1], dtype=np.int8))) + return faults + + +def test_initialization_creates_empty_fault_arrays() -> None: + """Verify initialization creates empty X and Z fault arrays for given qubit count.""" + faults = XZFaultList(num_qubits=4) + + assert faults.num_qubits == 4 + assert np.array_equal(faults.faults["X"], np.zeros((0, 4), dtype=np.int8)) + assert np.array_equal(faults.faults["Z"], np.zeros((0, 4), dtype=np.int8)) + + +def test_add_fault_appends_x_and_z_rows() -> None: + """Ensure adding a single XZ fault appends rows to X and Z arrays.""" + faults = XZFaultList(num_qubits=3) + + faults.add_fault((np.array([1, 0, 1], dtype=np.int8), np.array([0, 1, 0], dtype=np.int8))) + + assert np.array_equal(faults.faults["X"], np.array([[1, 0, 1]], dtype=np.int8)) + assert np.array_equal(faults.faults["Z"], np.array([[0, 1, 0]], dtype=np.int8)) + + +def test_add_fault_rejects_wrong_length() -> None: + """Adding a fault with incorrect length raises a ValueError.""" + faults = XZFaultList(num_qubits=3) + + with pytest.raises(ValueError, match=r"Faults must have length 3."): + faults.add_fault((np.array([1, 0], dtype=np.int8), np.array([0, 1, 0], dtype=np.int8))) + + +def test_add_fault_replaces_none_with_zeros() -> None: + """None X or Z entries are replaced by zero rows when adding a fault.""" + faults = XZFaultList(num_qubits=3) + + faults.add_fault((None, np.array([0, 1, 0], dtype=np.int8))) + faults.add_fault((np.array([1, 0, 1], dtype=np.int8), None)) + + assert np.array_equal(faults.faults["X"], np.array([[0, 0, 0], [1, 0, 1]], dtype=np.int8)) + assert np.array_equal(faults.faults["Z"], np.array([[0, 1, 0], [0, 0, 0]], dtype=np.int8)) + + +def test_add_fault_rejects_both_none() -> None: + """Adding a fault with both X and Z equal to None raises a ValueError.""" + faults = XZFaultList(num_qubits=3) + + with pytest.raises(ValueError, match=r"At least one fault must be provided."): + faults.add_fault((None, None)) + + +def test_add_faults_appends_multiple_rows() -> None: + """Adding multiple faults appends corresponding rows to X and Z arrays.""" + faults = XZFaultList(num_qubits=3) + + faults.add_faults(( + np.array([[1, 0, 0], [0, 1, 1]], dtype=np.int8), + np.array([[0, 1, 0], [1, 0, 1]], dtype=np.int8), + )) + + assert np.array_equal(faults.faults["X"], np.array([[1, 0, 0], [0, 1, 1]], dtype=np.int8)) + assert np.array_equal(faults.faults["Z"], np.array([[0, 1, 0], [1, 0, 1]], dtype=np.int8)) + + +def test_add_faults_rejects_wrong_column_count() -> None: + """Adding fault arrays with incorrect column counts raises a ValueError.""" + faults = XZFaultList(num_qubits=3) + + with pytest.raises(ValueError, match=r"Faults must have 3 columns."): + faults.add_faults(( + np.array([[1, 0]], dtype=np.int8), + np.array([[0, 1]], dtype=np.int8), + )) + + +def test_add_faults_replaces_none_with_zeros() -> None: + """None arrays passed to add_faults are replaced by zero arrays of proper shape.""" + faults = XZFaultList(num_qubits=3) + + faults.add_faults(( + None, + np.array([[0, 1, 0], [1, 0, 1]], dtype=np.int8), + )) + faults.add_faults(( + np.array([[1, 0, 1]], dtype=np.int8), + None, + )) + + assert np.array_equal( + faults.faults["X"], + np.array([[0, 0, 0], [0, 0, 0], [1, 0, 1]], dtype=np.int8), + ) + assert np.array_equal( + faults.faults["Z"], + np.array([[0, 1, 0], [1, 0, 1], [0, 0, 0]], dtype=np.int8), + ) + + +def test_add_faults_rejects_both_none() -> None: + """add_faults raises ValueError when both X and Z arrays are None.""" + faults = XZFaultList(num_qubits=3) + + with pytest.raises(ValueError, match=r"At least one fault array must be provided."): + faults.add_faults((None, None)) + + +def test_copy_returns_independent_fault_list(fault_list: XZFaultList) -> None: + """Copying an XZFaultList returns an independent deep copy.""" + copied = fault_list.copy() + + copied.faults["X"][0, 0] = 0 + copied.faults["Z"][1, 2] = 0 + + assert np.array_equal(fault_list.faults["X"], np.array([[1, 0, 1], [0, 1, 0]], dtype=np.int8)) + assert np.array_equal(fault_list.faults["Z"], np.array([[0, 1, 0], [1, 0, 1]], dtype=np.int8)) + + +def test_iter_yields_fault_pairs_in_order(fault_list: XZFaultList) -> None: + """Iteration yields X,Z fault pairs in the same insertion order.""" + pairs = list(fault_list) + + assert len(pairs) == 2 + assert np.array_equal(pairs[0][0], np.array([1, 0, 1], dtype=np.int8)) + assert np.array_equal(pairs[0][1], np.array([0, 1, 0], dtype=np.int8)) + assert np.array_equal(pairs[1][0], np.array([0, 1, 0], dtype=np.int8)) + assert np.array_equal(pairs[1][1], np.array([1, 0, 1], dtype=np.int8)) + + +def test_apply_cnot_updates_x_and_z_faults(fault_list: XZFaultList) -> None: + """Applying a CNOT updates both X and Z arrays according to circuit action.""" + updated = fault_list.apply_cnot(control=0, target=1, inplace=False) + + expected_x = np.array([[1, 1, 1], [0, 1, 0]], dtype=np.int8) + expected_z = np.array([[1, 1, 0], [1, 0, 1]], dtype=np.int8) + + assert np.array_equal(updated.faults["X"], expected_x) + assert np.array_equal(updated.faults["Z"], expected_z) + assert np.array_equal(fault_list.faults["X"], np.array([[1, 0, 1], [0, 1, 0]], dtype=np.int8)) + assert np.array_equal(fault_list.faults["Z"], np.array([[0, 1, 0], [1, 0, 1]], dtype=np.int8)) + + +def test_apply_cnot_inplace_modifies_current_fault_list(fault_list: XZFaultList) -> None: + """Inplace CNOT application modifies the original fault list and returns it.""" + result = fault_list.apply_cnot(control=1, target=2, inplace=True) + + expected_x = np.array([[1, 0, 1], [0, 1, 1]], dtype=np.int8) + expected_z = np.array([[0, 1, 0], [1, 1, 1]], dtype=np.int8) + + assert result is fault_list + assert np.array_equal(fault_list.faults["X"], expected_x) + assert np.array_equal(fault_list.faults["Z"], expected_z) + + +def test_apply_cnot_rejects_invalid_qubits(fault_list: XZFaultList) -> None: + """Invalid or identical qubit indices for CNOT raise ValueError.""" + with pytest.raises(ValueError, match=r"All qubits must be different."): + fault_list.apply_cnot(control=1, target=1) + + with pytest.raises(ValueError, match=r"Qubit indices must be between 0 and 2."): + fault_list.apply_cnot(control=3, target=1) + + with pytest.raises(ValueError, match=r"Qubit indices must be between 0 and 2."): + fault_list.apply_cnot(control=-1, target=1) + + +def test_apply_hadamard_swaps_x_and_z_on_target_qubit(fault_list: XZFaultList) -> None: + """Hadamard on a qubit swaps X and Z faults on that qubit position.""" + updated = fault_list.apply_hadamard(qubit=1, inplace=False) + + expected_x = np.array([[1, 1, 1], [0, 0, 0]], dtype=np.int8) + expected_z = np.array([[0, 0, 0], [1, 1, 1]], dtype=np.int8) + + assert np.array_equal(updated.faults["X"], expected_x) + assert np.array_equal(updated.faults["Z"], expected_z) + assert np.array_equal(fault_list.faults["X"], np.array([[1, 0, 1], [0, 1, 0]], dtype=np.int8)) + assert np.array_equal(fault_list.faults["Z"], np.array([[0, 1, 0], [1, 0, 1]], dtype=np.int8)) + + +def test_apply_hadamard_inplace_modifies_current_fault_list(fault_list: XZFaultList) -> None: + """Inplace Hadamard modifies the fault list and returns the same object.""" + result = fault_list.apply_hadamard(qubit=0, inplace=True) + + expected_x = np.array([[0, 0, 1], [1, 1, 0]], dtype=np.int8) + expected_z = np.array([[1, 1, 0], [0, 0, 1]], dtype=np.int8) + + assert result is fault_list + assert np.array_equal(fault_list.faults["X"], expected_x) + assert np.array_equal(fault_list.faults["Z"], expected_z) + + +def test_apply_hadamard_rejects_invalid_qubit(fault_list: XZFaultList) -> None: + """Applying Hadamard with an out-of-range qubit index raises ValueError.""" + with pytest.raises(ValueError, match=r"Qubit index must be between 0 and 2."): + fault_list.apply_hadamard(qubit=3) + + +def test_apply_reset_rejects_invalid_qubit(fault_list: XZFaultList) -> None: + """Applying reset with an out-of-range qubit index raises ValueError.""" + with pytest.raises(ValueError, match=r"Qubit index must be between 0 and 2."): + fault_list.apply_reset(qubit=3) + + +def test_apply_ccz_updates_z_faults_non_inplace() -> None: + """Applying CCZ updates Z faults accordingly when not inplace.""" + faults = XZFaultList(num_qubits=3) + faults.add_fault((np.array([1, 1, 1], dtype=np.int8), np.array([0, 0, 0], dtype=np.int8))) + + updated = faults.apply_ccz(control1=0, control2=1, control3=2, inplace=False) + + expected_x = np.array([[1, 1, 1]], dtype=np.int8) + expected_z = np.array([[1, 1, 1]], dtype=np.int8) + + assert np.array_equal(updated.faults["X"], expected_x) + assert np.array_equal(updated.faults["Z"], expected_z) + assert np.array_equal(faults.faults["X"], np.array([[1, 1, 1]], dtype=np.int8)) + assert np.array_equal(faults.faults["Z"], np.array([[0, 0, 0]], dtype=np.int8)) + + +def test_apply_ccz_inplace_modifies_current_fault_list() -> None: + """Inplace CCZ modifies the fault list and returns it.""" + faults = XZFaultList(num_qubits=3) + faults.add_fault((np.array([1, 1, 1], dtype=np.int8), np.array([0, 0, 0], dtype=np.int8))) + + result = faults.apply_ccz(control1=0, control2=1, control3=2, inplace=True) + + expected_x = np.array([[1, 1, 1]], dtype=np.int8) + expected_z = np.array([[1, 1, 1]], dtype=np.int8) + + assert result is faults + assert np.array_equal(faults.faults["X"], expected_x) + assert np.array_equal(faults.faults["Z"], expected_z) + + +def test_apply_ccz_rejects_invalid_controls() -> None: + """CCZ rejects invalid or non-distinct control indices with ValueError.""" + faults = XZFaultList(num_qubits=3) + + with pytest.raises(ValueError, match=r"All qubits must be different."): + faults.apply_ccz(control1=0, control2=0, control3=2) + + with pytest.raises(ValueError, match=r"Qubit indices must be between 0 and 2."): + faults.apply_ccz(control1=0, control2=1, control3=3) + + +def test_apply_ccx_rejects_invalid_qubits() -> None: + """CCX (Toffoli) rejects invalid or non-distinct qubit indices with ValueError.""" + faults = XZFaultList(num_qubits=3) + + with pytest.raises(ValueError, match=r"All qubits must be different."): + faults.apply_ccx(control1=0, control2=1, target=1) + + with pytest.raises(ValueError, match=r"Qubit indices must be between 0 and 2."): + faults.apply_ccx(control1=0, control2=1, target=3) + + +def test_apply_ccz_unit_tests() -> None: + """Unit tests: verify CCZ truth table mapping from input X to output X and Z.""" + # its always 0,1,2 for the controls + # input x, output x, output z + unit_tests = [ + [(0, 0, 0), (0, 0, 0), (0, 0, 0)], + [(0, 0, 1), (0, 0, 1), (0, 0, 0)], + [(0, 1, 0), (0, 1, 0), (0, 0, 0)], + [(0, 1, 1), (0, 1, 1), (1, 0, 0)], + [(1, 0, 0), (1, 0, 0), (0, 0, 0)], + [(1, 0, 1), (1, 0, 1), (0, 1, 0)], + [(1, 1, 0), (1, 1, 0), (0, 0, 1)], + [(1, 1, 1), (1, 1, 1), (1, 1, 1)], + ] + + for input_x, expected_x, expected_z in unit_tests: + faults = XZFaultList(num_qubits=3) + faults.add_fault((np.array(input_x, dtype=np.int8), np.array([0, 0, 0], dtype=np.int8))) + + updated = faults.apply_ccz(control1=0, control2=1, control3=2, inplace=False) + + assert np.array_equal(updated.faults["X"], np.array([expected_x], dtype=np.int8)) + assert np.array_equal(updated.faults["Z"], np.array([expected_z], dtype=np.int8)) + + +def test_apply_ccx_unit_tests() -> None: + """Unit tests: verify CCX (Toffoli) X-output mapping for control/target combinations.""" + # controls are always qubits 0 and 1, target is qubit 2 + # For CCX (Toffoli) with initial Z=0, the resulting X is (c1, c2, t + c1 & c2) + unit_tests = [ + ((0, 0, 0), (0, 0, 0)), + ((0, 0, 1), (0, 0, 1)), + ((0, 1, 0), (0, 1, 0)), + ((0, 1, 1), (0, 1, 1)), + ((1, 0, 0), (1, 0, 0)), + ((1, 0, 1), (1, 0, 1)), + ((1, 1, 0), (1, 1, 1)), + ((1, 1, 1), (1, 1, 0)), + ] + + for input_x, expected_x in unit_tests: + faults = XZFaultList(num_qubits=3) + faults.add_fault((np.array(input_x, dtype=np.int8), np.array([0, 0, 0], dtype=np.int8))) + + updated = faults.apply_ccx(control1=0, control2=1, target=2, inplace=False) + + assert np.array_equal(updated.faults["X"], np.array([expected_x], dtype=np.int8)) + assert np.array_equal(updated.faults["Z"], np.array([[0, 0, 0]], dtype=np.int8)) + + +def test_apply_reset_clears_selected_qubit_errors(fault_list: XZFaultList) -> None: + """Reset clears X and Z errors on the given qubit without modifying source when not inplace.""" + updated = fault_list.apply_reset(qubit=1, inplace=False) + + expected_x = np.array([[1, 0, 1], [0, 0, 0]], dtype=np.int8) + expected_z = np.array([[0, 0, 0], [1, 0, 1]], dtype=np.int8) + + assert np.array_equal(updated.faults["X"], expected_x) + assert np.array_equal(updated.faults["Z"], expected_z) + assert np.array_equal(fault_list.faults["X"], np.array([[1, 0, 1], [0, 1, 0]], dtype=np.int8)) + assert np.array_equal(fault_list.faults["Z"], np.array([[0, 1, 0], [1, 0, 1]], dtype=np.int8)) + + +def test_apply_reset_clears_selected_qubit_errors_inplace(fault_list: XZFaultList) -> None: + """Inplace reset clears errors on the specified qubit and returns the same object.""" + result = fault_list.apply_reset(qubit=1, inplace=True) + + expected_x = np.array([[1, 0, 1], [0, 0, 0]], dtype=np.int8) + expected_z = np.array([[0, 0, 0], [1, 0, 1]], dtype=np.int8) + + assert result is fault_list + assert np.array_equal(fault_list.faults["X"], expected_x) + assert np.array_equal(fault_list.faults["Z"], expected_z) + + +# based on tests for mqt.qecc.circuit_synthesis.faults.coset_leader +def test_reduce_to_coset_leaders_no_generators() -> None: + """Coset leader reduction with no generators leaves faults unchanged.""" + faults = XZFaultList(num_qubits=3) + faults.add_fault((np.array([1, 0, 1], dtype=np.int8), np.array([0, 1, 0], dtype=np.int8))) + + # No generators - faults should remain unchanged + reduced = faults.reduce_to_coset_leaders((None, None), inplace=False) + + assert np.array_equal(reduced.faults["X"], np.array([[1, 0, 1]], dtype=np.int8)) + assert np.array_equal(reduced.faults["Z"], np.array([[0, 1, 0]], dtype=np.int8)) + + +def test_reduce_to_coset_leaders_x_generators() -> None: + """Coset leader reduction applies X generators to reduce X faults to leaders.""" + faults = XZFaultList(num_qubits=3) + # Add X fault that is in the stabilizer group + faults.add_fault((np.array([1, 0, 1], dtype=np.int8), np.array([0, 0, 0], dtype=np.int8))) + + # X generator that matches the X fault + x_generators = np.array([[1, 0, 1]], dtype=np.int8) + + reduced = faults.reduce_to_coset_leaders((x_generators, None), inplace=False) + + # X fault should be reduced to zero (it's in the stabilizer group) + assert np.array_equal(reduced.faults["X"], np.array([[0, 0, 0]], dtype=np.int8)) + # Z fault should remain unchanged + assert np.array_equal(reduced.faults["Z"], np.array([[0, 0, 0]], dtype=np.int8)) + + +def test_reduce_to_coset_leaders_z_generators() -> None: + """Coset leader reduction applies Z generators to reduce Z faults to leaders.""" + faults = XZFaultList(num_qubits=3) + # Add Z fault that is in the stabilizer group + faults.add_fault((np.array([0, 0, 0], dtype=np.int8), np.array([0, 1, 1], dtype=np.int8))) + + # Z generator that matches the Z fault + z_generators = np.array([[0, 1, 1]], dtype=np.int8) + + reduced = faults.reduce_to_coset_leaders((None, z_generators), inplace=False) + + # X fault should remain unchanged + assert np.array_equal(reduced.faults["X"], np.array([[0, 0, 0]], dtype=np.int8)) + # Z fault should be reduced to zero + assert np.array_equal(reduced.faults["Z"], np.array([[0, 0, 0]], dtype=np.int8)) + + +def test_reduce_to_coset_leaders_both_generators() -> None: + """Coset leader reduction handles simultaneous X and Z generator reduction.""" + faults = XZFaultList(num_qubits=3) + faults.add_fault((np.array([1, 0, 1], dtype=np.int8), np.array([0, 1, 1], dtype=np.int8))) + faults.add_fault((np.array([0, 1, 0], dtype=np.int8), np.array([1, 0, 0], dtype=np.int8))) + + # Generators that match the faults + x_generators = np.array([[1, 0, 1]], dtype=np.int8) + z_generators = np.array([[0, 1, 1]], dtype=np.int8) + + reduced = faults.reduce_to_coset_leaders((x_generators, z_generators), inplace=False) + + # Both matching faults should be reduced to zero + assert np.array_equal(reduced.faults["X"][0], np.array([0, 0, 0], dtype=np.int8)) + assert np.array_equal(reduced.faults["Z"][0], np.array([0, 0, 0], dtype=np.int8)) + # Non-matching faults should be reduced to their coset leaders + assert reduced.faults["X"].shape[0] == 2 + assert reduced.faults["Z"].shape[0] == 2 + + +def test_reduce_to_coset_leaders_inplace() -> None: + """reduce_to_coset_leaders with inplace=True modifies the original fault list.""" + faults = XZFaultList(num_qubits=3) + faults.add_fault((np.array([1, 0, 1], dtype=np.int8), np.array([0, 1, 0], dtype=np.int8))) + + x_generators = np.array([[1, 0, 1]], dtype=np.int8) + + result = faults.reduce_to_coset_leaders((x_generators, None), inplace=True) + + # Result should be the same object + assert result is faults + # X fault should be reduced + assert np.array_equal(faults.faults["X"], np.array([[0, 0, 0]], dtype=np.int8)) + # Z fault should remain unchanged + assert np.array_equal(faults.faults["Z"], np.array([[0, 1, 0]], dtype=np.int8)) + + +def test_reduce_to_coset_leaders_not_inplace() -> None: + """reduce_to_coset_leaders with inplace=False returns a new modified copy.""" + original = XZFaultList(num_qubits=3) + original.add_fault((np.array([1, 0, 1], dtype=np.int8), np.array([0, 1, 0], dtype=np.int8))) + + x_generators = np.array([[1, 0, 1]], dtype=np.int8) + reduced = original.reduce_to_coset_leaders((x_generators, None), inplace=False) + + # Result should be a different object + assert reduced is not original + # Reduced fault list should be modified + assert np.array_equal(reduced.faults["X"], np.array([[0, 0, 0]], dtype=np.int8)) + # Original should remain unchanged + assert np.array_equal(original.faults["X"], np.array([[1, 0, 1]], dtype=np.int8)) + + +def test_reduce_to_coset_leaders_multiple_faults() -> None: + """Reduction correctly handles multiple faults, reducing those in stabilizer group.""" + faults = XZFaultList(num_qubits=3) + faults.add_fault((np.array([1, 0, 1], dtype=np.int8), np.array([0, 0, 0], dtype=np.int8))) + faults.add_fault((np.array([0, 1, 0], dtype=np.int8), np.array([0, 0, 0], dtype=np.int8))) + faults.add_fault((np.array([1, 1, 1], dtype=np.int8), np.array([0, 0, 0], dtype=np.int8))) + + # Two X generators + x_generators = np.array([[1, 0, 1], [0, 1, 0]], dtype=np.int8) + + reduced = faults.reduce_to_coset_leaders((x_generators, None), inplace=False) + + # First two faults are in the stabilizer group, third should be reduced to coset leader + assert np.array_equal(reduced.faults["X"][0], np.array([0, 0, 0], dtype=np.int8)) + assert np.array_equal(reduced.faults["X"][1], np.array([0, 0, 0], dtype=np.int8)) + + +def test_reduce_to_coset_leaders_invalid_generator_shape() -> None: + """reduce_to_coset_leaders raises ValueError for generators with wrong shape.""" + faults = XZFaultList(num_qubits=3) + faults.add_fault((np.array([1, 0, 1], dtype=np.int8), np.array([0, 1, 0], dtype=np.int8))) + + # Wrong number of columns in generator + x_generators = np.array([[1, 0]], dtype=np.int8) + + with pytest.raises(ValueError, match=r"Generators must be a 2D array with 3 columns."): + faults.reduce_to_coset_leaders((x_generators, None), inplace=False) + + +def test_reduce_to_coset_leaders_invalid_generator_dimension() -> None: + """reduce_to_coset_leaders raises ValueError when generators are 1D arrays.""" + faults = XZFaultList(num_qubits=3) + faults.add_fault((np.array([1, 0, 1], dtype=np.int8), np.array([0, 1, 0], dtype=np.int8))) + + # 1D array instead of 2D + x_generators = np.array([1, 0, 1], dtype=np.int8) + + with pytest.raises(ValueError, match=r"Generators must be a 2D array with 3 columns."): + faults.reduce_to_coset_leaders((x_generators, None), inplace=False) + + +def test_reduce_to_coset_leaders_empty_fault_list() -> None: + """Reduction on an empty fault list should remain empty and not error.""" + faults = XZFaultList(num_qubits=3) + + x_generators = np.array([[1, 0, 1]], dtype=np.int8) + + reduced = faults.reduce_to_coset_leaders((x_generators, None), inplace=False) + + # Should remain empty + assert reduced.faults["X"].shape == (0, 3) + assert reduced.faults["Z"].shape == (0, 3) + + +def test_reduce_to_coset_leaders_empty_generators() -> None: + """Empty generator arrays result in no reduction and leave faults unchanged.""" + faults = XZFaultList(num_qubits=3) + faults.add_fault((np.array([1, 0, 1], dtype=np.int8), np.array([0, 1, 0], dtype=np.int8))) + + # Empty generators + x_generators = np.empty((0, 3), dtype=np.int8) + + reduced = faults.reduce_to_coset_leaders((x_generators, None), inplace=False) + + # Faults should remain unchanged + assert np.array_equal(reduced.faults["X"], np.array([[1, 0, 1]], dtype=np.int8)) + assert np.array_equal(reduced.faults["Z"], np.array([[0, 1, 0]], dtype=np.int8)) + + +def test_xzfaultlist_repr() -> None: + """Test __repr__ of XZFaultList returns a string representation.""" + faults = XZFaultList(num_qubits=3) + faults.add_fault((np.array([1, 0, 1], dtype=np.int8), np.array([0, 1, 0], dtype=np.int8))) + + repr_str = repr(faults) + + # Check that repr returns a string + assert isinstance(repr_str, str) + # Check that the representation contains relevant information + assert "XZFaultList" in repr_str + assert "num_qubits: 3" in repr_str + assert "[1, 0, 1]" in repr_str + assert "[0, 1, 0]" in repr_str + + +def test_xzfaultlist_repr_empty() -> None: + """Test __repr__ of empty XZFaultList.""" + faults = XZFaultList(num_qubits=2) + + repr_str = repr(faults) + + # Check that repr returns a string + assert isinstance(repr_str, str) + # Check that the representation contains relevant information + assert "XZFaultList" in repr_str + assert "num_qubits: 2" in repr_str