From 9835f4489837dcd1cf9591c68f007f5126441b04 Mon Sep 17 00:00:00 2001 From: Yudong Sun Date: Thu, 14 May 2026 14:58:09 +0200 Subject: [PATCH 01/34] create function for applying CNOT to a PureFaultSet Co-authored-by: Copilot --- src/mqt/qecc/circuit_synthesis/faults.py | 30 ++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/mqt/qecc/circuit_synthesis/faults.py b/src/mqt/qecc/circuit_synthesis/faults.py index 8da7d68ed..060b8b1e7 100644 --- a/src/mqt/qecc/circuit_synthesis/faults.py +++ b/src/mqt/qecc/circuit_synthesis/faults.py @@ -375,6 +375,36 @@ def permute_qubits(self, permutation: npt.NDArray[np.int8] | list[int], inplace: return PureFaultSet.from_fault_array(permuted_faults) + def apply_cnot(self, control: int, target: int, kind: str = "X", inplace: bool = True) -> PureFaultSet: + """Apply a CNOT gate to the faults in the set. + + Args: + control: The index of the control qubit. + target: The index of the target qubit. + kind: The type of faults to apply the CNOT to ('X' or 'Z'). + 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 control >= self.num_qubits or target >= self.num_qubits: + msg = f"Control and target indices must be less than {self.num_qubits}." + raise ValueError(msg) + if kind.capitalize() not in {"X", "Z"}: + msg = "Kind must be either 'X' or 'Z'." + raise ValueError(msg) + + updated_faults = np.copy(self.faults) + if kind == "X": + updated_faults[:, target] ^= updated_faults[:, control] + else: # kind == "Z" + updated_faults[:, control] ^= updated_faults[:, target] + + if inplace: + self.faults = updated_faults + return self + + return PureFaultSet.from_fault_array(updated_faults) def coset_leader(fault: npt.NDArray[np.int8], generators: npt.NDArray[np.int8]) -> npt.NDArray[np.int8]: """Compute the coset leader of a fault given a set of stabilizer generators.""" From e1459702bc1b1880b310296c5a8b45bf0d491566 Mon Sep 17 00:00:00 2001 From: Yudong Sun Date: Thu, 14 May 2026 14:59:01 +0200 Subject: [PATCH 02/34] Test core functionality of apply_cnot --- tests/circuit_synthesis/test_faults.py | 41 ++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/tests/circuit_synthesis/test_faults.py b/tests/circuit_synthesis/test_faults.py index a4f220d60..da5f83727 100644 --- a/tests/circuit_synthesis/test_faults.py +++ b/tests/circuit_synthesis/test_faults.py @@ -637,3 +637,44 @@ def test_permute_qubits_inplace(): fault_set.permute_qubits(permutation, inplace=True) assert fault_set != PureFaultSet.from_fault_array(faults), "Faults were not permuted correctly in place" + + +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) + + # Apply CNOT with control=0 and target=1 + fault_set1.apply_cnot(control=0, target=1, kind="X") + + 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) + + # Apply CNOT with control=0 and target=1 + fault_set2.apply_cnot(control=0, target=1, kind="X") + + 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) + + # Apply CNOT with control=0 and target=1 + fault_set1.apply_cnot(control=0, target=1, kind="Z") + + 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) + + # Apply CNOT with control=0 and target=1 + fault_set2.apply_cnot(control=0, target=1, kind="Z") + + 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" \ No newline at end of file From 4fe853a616ac472843e1d2d4d618d29814e72bb7 Mon Sep 17 00:00:00 2001 From: Yudong Sun Date: Thu, 14 May 2026 15:23:32 +0200 Subject: [PATCH 03/34] Make kind a property of PureFaultSet Co-authored-by: Copilot --- src/mqt/qecc/circuit_synthesis/faults.py | 35 +++++++++++++++--------- tests/circuit_synthesis/test_faults.py | 30 ++++++++++++++------ 2 files changed, 44 insertions(+), 21 deletions(-) diff --git a/src/mqt/qecc/circuit_synthesis/faults.py b/src/mqt/qecc/circuit_synthesis/faults.py index 060b8b1e7..110cf4a4b 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. @@ -86,7 +99,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 +108,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 +137,7 @@ 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 @@ -375,29 +388,25 @@ def permute_qubits(self, permutation: npt.NDArray[np.int8] | list[int], inplace: return PureFaultSet.from_fault_array(permuted_faults) - def apply_cnot(self, control: int, target: int, kind: str = "X", inplace: bool = True) -> PureFaultSet: - """Apply a CNOT gate to the faults in the set. + 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. - kind: The type of faults to apply the CNOT to ('X' or 'Z'). 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 control >= self.num_qubits or target >= self.num_qubits: - msg = f"Control and target indices must be less than {self.num_qubits}." - raise ValueError(msg) - if kind.capitalize() not in {"X", "Z"}: - msg = "Kind must be either 'X' or 'Z'." + msg = f"Control and target indices must be between 0 and {self.num_qubits - 1}." raise ValueError(msg) updated_faults = np.copy(self.faults) - if kind == "X": + if self.kind == "X": updated_faults[:, target] ^= updated_faults[:, control] - else: # kind == "Z" + else: # self.kind == "Z" updated_faults[:, control] ^= updated_faults[:, target] if inplace: diff --git a/tests/circuit_synthesis/test_faults.py b/tests/circuit_synthesis/test_faults.py index da5f83727..d1e86a7a6 100644 --- a/tests/circuit_synthesis/test_faults.py +++ b/tests/circuit_synthesis/test_faults.py @@ -639,42 +639,56 @@ def test_permute_qubits_inplace(): assert fault_set != PureFaultSet.from_fault_array(faults), "Faults were not permuted correctly in place" +def test_PureFaultSet_invalid_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") + + with pytest.raises(AssertionError, match=r"Kind must be either 'X' or 'Z'."): + pfs = PureFaultSet(5) + 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) + 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, kind="X") + 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) + 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, kind="X") + 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) + 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, kind="Z") + 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) + 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, kind="Z") + 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" \ No newline at end of file From 51705859d21ccedbc457b21ceea8da76db1f2fd8 Mon Sep 17 00:00:00 2001 From: Yudong Sun Date: Thu, 14 May 2026 15:25:26 +0200 Subject: [PATCH 04/34] Test invalid qubit indices for apply_cnot Co-authored-by: Copilot --- src/mqt/qecc/circuit_synthesis/faults.py | 3 +++ tests/circuit_synthesis/test_faults.py | 14 +++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/mqt/qecc/circuit_synthesis/faults.py b/src/mqt/qecc/circuit_synthesis/faults.py index 110cf4a4b..bac4355fa 100644 --- a/src/mqt/qecc/circuit_synthesis/faults.py +++ b/src/mqt/qecc/circuit_synthesis/faults.py @@ -402,6 +402,9 @@ def apply_cnot(self, control: int, target: int, inplace: bool = True) -> PureFau if control >= self.num_qubits or target >= self.num_qubits: msg = f"Control and target indices must be between 0 and {self.num_qubits - 1}." raise ValueError(msg) + if control == target: + msg = "Control and target qubits must be different." + raise ValueError(msg) updated_faults = np.copy(self.faults) if self.kind == "X": diff --git a/tests/circuit_synthesis/test_faults.py b/tests/circuit_synthesis/test_faults.py index d1e86a7a6..ab68861c0 100644 --- a/tests/circuit_synthesis/test_faults.py +++ b/tests/circuit_synthesis/test_faults.py @@ -691,4 +691,16 @@ def test_apply_cnot_z(): 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" \ No newline at end of file + 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) \ No newline at end of file From 4fd044688423fe8177c36173fead218e5f5cf3fa Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 14 May 2026 13:42:47 +0000 Subject: [PATCH 05/34] =?UTF-8?q?=F0=9F=8E=A8=20pre-commit=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mqt/qecc/circuit_synthesis/faults.py | 9 ++++++--- tests/circuit_synthesis/test_faults.py | 20 ++++++++++++++------ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/mqt/qecc/circuit_synthesis/faults.py b/src/mqt/qecc/circuit_synthesis/faults.py index bac4355fa..ea98e6e28 100644 --- a/src/mqt/qecc/circuit_synthesis/faults.py +++ b/src/mqt/qecc/circuit_synthesis/faults.py @@ -43,10 +43,10 @@ def __init__(self, num_qubits: int, kind: str = "X") -> None: 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').""" + """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() @@ -137,7 +137,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), kind=kind) + 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 @@ -418,6 +420,7 @@ def apply_cnot(self, control: int, target: int, inplace: bool = True) -> PureFau return PureFaultSet.from_fault_array(updated_faults) + def coset_leader(fault: npt.NDArray[np.int8], generators: npt.NDArray[np.int8]) -> npt.NDArray[np.int8]: """Compute the coset leader of a fault given a set of stabilizer generators.""" if len(generators) == 0: diff --git a/tests/circuit_synthesis/test_faults.py b/tests/circuit_synthesis/test_faults.py index ab68861c0..38276a17e 100644 --- a/tests/circuit_synthesis/test_faults.py +++ b/tests/circuit_synthesis/test_faults.py @@ -643,7 +643,7 @@ def test_PureFaultSet_invalid_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") @@ -661,7 +661,9 @@ def test_apply_cnot_x(): 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" + 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") @@ -670,7 +672,9 @@ def test_apply_cnot_x(): 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" + 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(): @@ -682,7 +686,9 @@ def test_apply_cnot_z(): 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" + 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") @@ -691,7 +697,9 @@ def test_apply_cnot_z(): 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" + 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(): @@ -703,4 +711,4 @@ def test_apply_cnot_invalid_qubits(): 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) \ No newline at end of file + fault_set.apply_cnot(control=3, target=1) From ab450e98ecabb161441ce3eb14fa480ace82fa26 Mon Sep 17 00:00:00 2001 From: Yudong Sun Date: Thu, 14 May 2026 15:55:20 +0200 Subject: [PATCH 06/34] add new `apply_cnot` function to changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 60282b635..d78d5f349 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ This project adheres to [Semantic Versioning], with the exception that minor rel - New `synthesize_clifford` function for general Clifford operation synthesis with configurable optimization strategies. ([#640]) ([**@pehamtom**]) - New `resynthesize_stim_circuit` function for optimizing existing Stim circuits by resynthesizing them with improved gate count or depth. ([#640]) ([**@pehamtom**]) - New `encoder_from_stabilizers_and_logicals` function for constructing encoding circuits from stabilizer and logical operator tableaux. ([#640]) ([**@pehamtom**]) +- New `apply_cnot` function to calculate the resulting `PureFaultSet` after a CNOT ([#690]) ([**@sunjerry019**]) ### Changed From 0498c8b45ef18881e7cbe7d76b12b45b2c066c4e Mon Sep 17 00:00:00 2001 From: Yudong Sun Date: Thu, 14 May 2026 16:39:28 +0200 Subject: [PATCH 07/34] new PureFaultSet in apply_cnot should return the correct kind --- src/mqt/qecc/circuit_synthesis/faults.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mqt/qecc/circuit_synthesis/faults.py b/src/mqt/qecc/circuit_synthesis/faults.py index ea98e6e28..1b6c81fdd 100644 --- a/src/mqt/qecc/circuit_synthesis/faults.py +++ b/src/mqt/qecc/circuit_synthesis/faults.py @@ -418,7 +418,7 @@ def apply_cnot(self, control: int, target: int, inplace: bool = True) -> PureFau self.faults = updated_faults return self - return PureFaultSet.from_fault_array(updated_faults) + 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]: From 3f67ea5c8a99f7ed73de657246956c7a8d7f7f37 Mon Sep 17 00:00:00 2001 From: Yudong Sun Date: Thu, 14 May 2026 16:44:26 +0200 Subject: [PATCH 08/34] Add a test for `inplace` parameter for apply_cnot Co-authored-by: Copilot --- tests/circuit_synthesis/test_faults.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/circuit_synthesis/test_faults.py b/tests/circuit_synthesis/test_faults.py index 38276a17e..5574566ce 100644 --- a/tests/circuit_synthesis/test_faults.py +++ b/tests/circuit_synthesis/test_faults.py @@ -712,3 +712,18 @@ def test_apply_cnot_invalid_qubits(): with pytest.raises(ValueError, match=r"Control and target indices must be between 0 and 2."): fault_set.apply_cnot(control=3, 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" \ No newline at end of file From 015f9913be914e405c31b6590ac4ddb78887ae8a Mon Sep 17 00:00:00 2001 From: Yudong Sun Date: Thu, 14 May 2026 16:46:32 +0200 Subject: [PATCH 09/34] code coverage: Add test for invalid dimensions when creating a PureFaultSet from a fault_array --- tests/circuit_synthesis/test_faults.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/circuit_synthesis/test_faults.py b/tests/circuit_synthesis/test_faults.py index 5574566ce..e9fff4e52 100644 --- a/tests/circuit_synthesis/test_faults.py +++ b/tests/circuit_synthesis/test_faults.py @@ -97,6 +97,13 @@ def test_from_fault_array(): # Check that the rows in the result match the expected rows, regardless of order 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"), From 451da2dbb9117bc32a53304637f64890524fda0f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 14 May 2026 14:47:18 +0000 Subject: [PATCH 10/34] =?UTF-8?q?=F0=9F=8E=A8=20pre-commit=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mqt/qecc/circuit_synthesis/faults.py | 2 +- tests/circuit_synthesis/test_faults.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/mqt/qecc/circuit_synthesis/faults.py b/src/mqt/qecc/circuit_synthesis/faults.py index 1b6c81fdd..7897c2dba 100644 --- a/src/mqt/qecc/circuit_synthesis/faults.py +++ b/src/mqt/qecc/circuit_synthesis/faults.py @@ -418,7 +418,7 @@ def apply_cnot(self, control: int, target: int, inplace: bool = True) -> PureFau self.faults = updated_faults return self - return PureFaultSet.from_fault_array(updated_faults, kind = self.kind) + 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]: diff --git a/tests/circuit_synthesis/test_faults.py b/tests/circuit_synthesis/test_faults.py index e9fff4e52..1cf4a2f48 100644 --- a/tests/circuit_synthesis/test_faults.py +++ b/tests/circuit_synthesis/test_faults.py @@ -97,6 +97,7 @@ def test_from_fault_array(): # Check that the rows in the result match the expected rows, regardless of order 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 @@ -733,4 +734,4 @@ def test_apply_cnot_not_inplace(): 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" \ No newline at end of file + assert np.array_equal(fault_set.to_array(), faults), "Original fault set should remain unchanged" From 79d0164963c1ab949664e62b844a0558ae4b50d9 Mon Sep 17 00:00:00 2001 From: Yudong Sun Date: Mon, 18 May 2026 13:16:31 +0200 Subject: [PATCH 11/34] fix pre-commit check > ruff: make `test_PureFaultSet_invalid_kind` lowercase --- tests/circuit_synthesis/test_faults.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/circuit_synthesis/test_faults.py b/tests/circuit_synthesis/test_faults.py index e9fff4e52..4dffb65ac 100644 --- a/tests/circuit_synthesis/test_faults.py +++ b/tests/circuit_synthesis/test_faults.py @@ -646,7 +646,7 @@ def test_permute_qubits_inplace(): assert fault_set != PureFaultSet.from_fault_array(faults), "Faults were not permuted correctly in place" -def test_PureFaultSet_invalid_kind(): +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") From 71ceca0b353c8270b1741f2cf5bf599b985338d7 Mon Sep 17 00:00:00 2001 From: Yudong Sun Date: Mon, 18 May 2026 13:20:23 +0200 Subject: [PATCH 12/34] fix pre-commit check > ruff: `pytest.raises()` now only has a single statement --- tests/circuit_synthesis/test_faults.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/circuit_synthesis/test_faults.py b/tests/circuit_synthesis/test_faults.py index 4dffb65ac..f13a6f5c4 100644 --- a/tests/circuit_synthesis/test_faults.py +++ b/tests/circuit_synthesis/test_faults.py @@ -653,9 +653,9 @@ def test_invalid_fault_kind(): 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 = PureFaultSet(5) pfs.kind = "Y" From 234202987bd1ea0f00541083b768171779c33ea2 Mon Sep 17 00:00:00 2001 From: Yudong Sun Date: Fri, 22 May 2026 23:25:42 +0200 Subject: [PATCH 13/34] resolve merge conflict from main due to CHANGELOG.md --- CHANGELOG.md | 88 ---------------------------------------------------- 1 file changed, 88 deletions(-) delete mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index d78d5f349..000000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,88 +0,0 @@ - - -# Changelog - -All notable changes to this project will be documented in this file. - -The format is based on a mixture of [Keep a Changelog] and [Common Changelog]. -This project adheres to [Semantic Versioning], with the exception that minor releases may include breaking changes. - -## [Unreleased] - -### Added - -- New `MinimalCodeSwitchingCompiler` class that implements a compiler for minimal-overhead code switching on the logical level. ([#524], [arXiv:2512.04170](https://arxiv.org/abs/2512.04170)) ([**@inctechs**]) -- Added `gottesman_encoding_circuit` methods that constructs a stim encoding circuit for a given stabilizer code using the method described in Gottesman's "Surviving as a Quantum Computer in a Classical World" Chapter 6.4.1. ([#486]) ([**@pehamtom**]) -- Added class `SteaneNDFTStatePrepSimulator` for simulating non-deterministic state preparation protocols for CSS codes using verification with multiple ancilla states. ([#462]) ([**@pehamtom**]) -- Extended estimation of error rates in `NoisyNDFTStatePrepSimulator` via `secondary_logical_error_rate`. Now Z (X) error rates can also be estimated for the preparation of logical zero (plus). ([#462]) ([**@pehamtom**]) -- Added `ComposedNoiseModel` class that allows for composition of noise models. )([#462]) ([**@pehamtom**]) -- Added functionality to concatenate stim circuits along specific qubits. Add functionality to concatenate stim circuits along specific qubits ([#461]) ([**@pehamtom**]) -- Added `NoiseModel` class for applying noise to a given stim circuit. ([#453]) ([**@pehamtom**]) -- New `PureFaultSet` class for representing collections of X or Z faults. ([#443]) ([**@pehamtom**]) -- New `CNOTCircuit` class to serve as an intermediate representation during circuit synthesis for simplifying work with CSS encoding isometries. ([#443]) ([**@pehamtom**]) -- Combinatorial search methods for constructing fault-tolerant cat state preparation circuits. ([#543]) ([**@pehamtom**]) -- Lattice surgery compilation for the color code with and without movable logical qubits and layout optimization. ([#559]) ([**@LSHerzog**]) -- Extendable synthesis framework for Clifford encoding isometries supporting custom candidate generators and optimization strategies. ([#640]) ([**@pehamtom**]) -- Synthesis of non-CSS Clifford encoding isometries with support for arbitrary stabilizer codes. ([#640]) ([**@pehamtom**]) -- Rollout heuristics for improved gate-count and depth optimization in synthesized Clifford circuits. ([#640]) ([**@pehamtom**]) -- Z3-based exact synthesis methods for depth-optimal and gate-optimal encoding circuits for both CSS and non-CSS codes. ([#640]) ([**@pehamtom**]) -- New `synthesize_clifford` function for general Clifford operation synthesis with configurable optimization strategies. ([#640]) ([**@pehamtom**]) -- New `resynthesize_stim_circuit` function for optimizing existing Stim circuits by resynthesizing them with improved gate count or depth. ([#640]) ([**@pehamtom**]) -- New `encoder_from_stabilizers_and_logicals` function for constructing encoding circuits from stabilizer and logical operator tableaux. ([#640]) ([**@pehamtom**]) -- New `apply_cnot` function to calculate the resulting `PureFaultSet` after a CNOT ([#690]) ([**@sunjerry019**]) - -### Changed - -- Stop testing on x86 macOS systems ([#592]) ([**@denialhaag**]) -- Move Python tests from `test/python` to `tests`. ([#482]) ([**@denialhaag**]) -- `NoisyNDFTStatePrepSimulator` simulates generalized post-selection based state preparation protocols. Old functionality for simulating state preparation protocols post-selected on stabilizer measurements can be found in the class `VerificationNDFTStatePrepSimulator`. ([#462]) ([**@pehamtom**]) -- Refactored state preparation circuit synthesis code to utilize the new `PureFaultSet` and `CNOTCircuit` classes. ([#443]) ([**@pehamtom**]) -- Refactored encoding circuit synthesis code to utilize the new `PureFaultSet` and `CNOTCircuit` classes. ([#443]) ([**@pehamtom**]) -- Renamed `StatePrepCircuit` class to `FaultyStatePrepCircuit`, reflecting its new role in combining circuit and fault information. ([#443]) ([**@pehamtom**]) -- Changed the construction in `CatStatePreparationExperiment` to allow for ancillas with less qubits than the data cat state. -- Added resets to Cat state preparation circuits in `CatStatePreparationExperiment`. ([#652]) ([**@pehamtom**]) -- Unified encoding circuit and state preparation circuit synthesis into a common framework. ([#640]) ([**@pehamtom**]) - -### Removed - -- Drop support for Python 3.9 ([#503]) ([**@denialhaag**]) - -## [1.9.0] - 2025-03-14 - -_📚 Refer to the [GitHub Release Notes](https://github.com/munich-quantum-toolkit/qecc/releases) for previous changelogs._ - - - -[unreleased]: https://github.com/munich-quantum-toolkit/qecc/compare/v1.9.0...HEAD -[1.9.0]: https://github.com/munich-quantum-toolkit/qecc/releases/tag/v1.9.0 - - - -[#524]: https://github.com/munich-quantum-toolkit/qecc/pull/524 -[#592]: https://github.com/munich-quantum-toolkit/qecc/pull/592 -[#543]: https://github.com/munich-quantum-toolkit/qecc/pull/543 -[#503]: https://github.com/munich-quantum-toolkit/qecc/pull/503 -[#499]: https://github.com/munich-quantum-toolkit/qecc/pull/499 -[#486]: https://github.com/munich-quantum-toolkit/qecc/pull/486 -[#482]: https://github.com/munich-quantum-toolkit/qecc/pull/482 -[#462]: https://github.com/munich-quantum-toolkit/qecc/pull/462 -[#461]: https://github.com/munich-quantum-toolkit/qecc/pull/461 -[#453]: https://github.com/munich-quantum-toolkit/qecc/pull/453 -[#443]: https://github.com/munich-quantum-toolkit/qecc/pull/443 -[#559]: https://github.com/munich-quantum-toolkit/qecc/pull/559 -[#640]: https://github.com/munich-quantum-toolkit/qecc/pull/640 -[#652]: https://github.com/munich-quantum-toolkit/qecc/pull/652 - - - -[**@pehamtom**]: https://github.com/pehamtom -[**@denialhaag**]: https://github.com/denialhaag -[**@LSHerzog**]: https://github.com/LSHerzog/ -[**@inctechs**]: https://github.com/inctechs - - - -[Keep a Changelog]: https://keepachangelog.com/en/1.1.0/ -[Common Changelog]: https://common-changelog.org -[Semantic Versioning]: https://semver.org/spec/v2.0.0.html -[GitHub Release Notes]: https://github.com/munich-quantum-toolkit/qecc/releases From 58ea9544a223d9031ce09ec539e83bee753cfbc5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 21:27:21 +0000 Subject: [PATCH 14/34] =?UTF-8?q?=F0=9F=8E=A8=20pre-commit=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/circuit_synthesis/test_faults.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/circuit_synthesis/test_faults.py b/tests/circuit_synthesis/test_faults.py index 5aa7bc42b..9aa862405 100644 --- a/tests/circuit_synthesis/test_faults.py +++ b/tests/circuit_synthesis/test_faults.py @@ -654,7 +654,7 @@ def test_invalid_fault_kind(): 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" From 45d12aaeb64379c5a1ea48fc2875b526a0dfc63e Mon Sep 17 00:00:00 2001 From: Yudong Sun Date: Mon, 1 Jun 2026 14:03:26 +0200 Subject: [PATCH 15/34] XZFaultList as a squash commit Assisted by (Auto) via GitHub Copilot --- src/mqt/qecc/circuit_synthesis/faults.py | 355 +++++++++++++++++- tests/circuit_synthesis/test_faults.py | 446 ++++++++++++++++++++++- 2 files changed, 799 insertions(+), 2 deletions(-) diff --git a/src/mqt/qecc/circuit_synthesis/faults.py b/src/mqt/qecc/circuit_synthesis/faults.py index 7897c2dba..3dbf91ea0 100644 --- a/src/mqt/qecc/circuit_synthesis/faults.py +++ b/src/mqt/qecc/circuit_synthesis/faults.py @@ -9,7 +9,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Tuple import numpy as np import z3 @@ -527,3 +527,356 @@ 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(): + 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: + raise ValueError("At least one fault must be provided.") + + 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: + raise ValueError("At least one fault array must be provided.") + + 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): + """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. + + # Adverserial 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. + + Raises: + ValueError: If any qubit index is out of range. + ValueError: If qubits are not distinct. + + Returns: + bool: True if everything is okay + """ + _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. + + Raises: + ValueError: If any generator array has incorrect dimensions (must be 2D with num_qubits columns). + AssertionError: If generators tuple length is not 2. + + Returns: + XZFaultList: The reduced fault list (self if inplace=True, otherwise a copy). + """ + # 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): + # 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: + _repr = [ + object.__repr__(self), + "X:", + repr(self.faults["X"]), + "Z:", + repr(self.faults["Z"]) + ] + return "\n".join(_repr) \ No newline at end of file diff --git a/tests/circuit_synthesis/test_faults.py b/tests/circuit_synthesis/test_faults.py index 9aa862405..1817e87d8 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, coset_leader, stabilizer_equivalent, t_distinct, XZFaultList @pytest.fixture @@ -735,3 +735,447 @@ def test_apply_cnot_not_inplace(): "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" + + +"""XZFaultList Tests""" +@pytest.fixture +def fault_list() -> XZFaultList: + 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: + 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: + 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: + 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: + 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: + 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: + 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: + 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: + 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: + 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: + 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: + 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: + 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: + 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: + 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) + + +def test_apply_hadamard_swaps_x_and_z_on_target_qubit(fault_list: XZFaultList) -> None: + 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: + 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: + 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: + 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: + 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: + 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: + 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: + 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: + # 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_reset_clears_selected_qubit_errors(fault_list: XZFaultList) -> None: + 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)) + +# based on tests for mqt.qecc.circuit_synthesis.faults.coset_leader +def test_reduce_to_coset_leaders_no_generators() -> None: + """Test coset leader reduction with no generators (should not modify faults).""" + 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: + """Test coset leader reduction with X generators.""" + 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: + """Test coset leader reduction with Z generators.""" + 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: + """Test coset leader reduction with both X and Z generators.""" + 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: + """Test that 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: + """Test that inplace=False returns a new independent fault list.""" + 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: + """Test coset leader reduction with multiple faults.""" + 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: + """Test that invalid generator shapes raise ValueError.""" + 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: + """Test that 1D generators raise ValueError.""" + 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: + """Test coset leader reduction with an empty fault list.""" + 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: + """Test coset leader reduction with empty generators (no reduction applied).""" + 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)) \ No newline at end of file From d0b4bc6871a387317ededa01047d8a92be717c01 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Jun 2026 12:07:54 +0000 Subject: [PATCH 16/34] =?UTF-8?q?=F0=9F=8E=A8=20pre-commit=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mqt/qecc/circuit_synthesis/faults.py | 90 ++-- tests/circuit_synthesis/test_faults.py | 545 +++++++++++------------ 2 files changed, 315 insertions(+), 320 deletions(-) diff --git a/src/mqt/qecc/circuit_synthesis/faults.py b/src/mqt/qecc/circuit_synthesis/faults.py index 3dbf91ea0..ae30bf652 100644 --- a/src/mqt/qecc/circuit_synthesis/faults.py +++ b/src/mqt/qecc/circuit_synthesis/faults.py @@ -9,7 +9,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Tuple +from typing import TYPE_CHECKING import numpy as np import z3 @@ -529,9 +529,9 @@ def t_distinct(fs1: PureFaultSet, fs2: PureFaultSet, t: int, stabs: npt.NDArray[ return True -class XZFaultList(): +class XZFaultList: def __init__(self, num_qubits: int) -> None: - """Initialise a XZFaultList object + """Initialise a XZFaultList object. Args: num_qubits (int): The number of qubits in the circuit @@ -542,12 +542,12 @@ def __init__(self, num_qubits: int) -> None: "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: + 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. + 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: @@ -558,7 +558,8 @@ def add_fault(self, faults: Tuple[npt.NDArray[np.int8] | None, npt.NDArray[np.in x_fault, z_fault = faults if x_fault is None and z_fault is None: - raise ValueError("At least one fault must be provided.") + 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) @@ -577,7 +578,7 @@ def add_fault(self, faults: Tuple[npt.NDArray[np.int8] | None, npt.NDArray[np.in 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: + 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: @@ -591,7 +592,8 @@ def add_faults(self, faults: Tuple[npt.NDArray[np.int8] | None, npt.NDArray[np.i """ x_faults, z_faults = faults if x_faults is None and z_faults is None: - raise ValueError("At least one fault array must be provided.") + 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) @@ -663,7 +665,7 @@ def apply_cnot(self, control: int, target: int, inplace: bool = True) -> XZFault ret = new_list # Apply CNOT - x_faults[:, target] ^= x_faults[:, control] + x_faults[:, target] ^= x_faults[:, control] z_faults[:, control] ^= z_faults[:, target] return ret @@ -705,7 +707,7 @@ def apply_hadamard(self, qubit: int, inplace: bool = True) -> XZFaultList: ) return new_list - + def apply_reset(self, qubit: int, inplace: bool = True) -> XZFaultList: """Apply a reset operation to the faults in the list. @@ -734,12 +736,12 @@ def apply_reset(self, qubit: int, inplace: bool = True) -> XZFaultList: 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. + 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 @@ -762,7 +764,7 @@ def apply_ccz(self, control1: int, control2: int, control3: int, inplace: bool = # By right, the state of the qubits matter, which is why you can't simply propagate pauli gates through a CCZ gate. - # Adverserial Fault Propagation for CCZ: + # 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} @@ -783,9 +785,9 @@ def apply_ccz(self, control1: int, control2: int, control3: int, inplace: bool = 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 + """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. @@ -802,12 +804,12 @@ def apply_ccx(self, control1: int, control2: int, target: int, inplace: bool = T """ 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) + fault_list = self.apply_hadamard(target, inplace=inplace) + fault_list.apply_ccz(control1, control2, target) + fault_list.apply_hadamard(target) + + return fault_list - return _fault_list - def ensure_apply_valid_input(self, *qubits: int) -> bool: """Ensures that the input into apply_* functions are valid. @@ -818,28 +820,30 @@ def ensure_apply_valid_input(self, *qubits: int) -> bool: Returns: bool: True if everything is okay """ - _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}." + 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: + 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: + 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 + 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 + 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. + inplace (bool, optional): If True, modify this fault list in place. If False, return a copy with reductions applied. Defaults to True. Raises: @@ -850,33 +854,27 @@ def reduce_to_coset_leaders(self, generators: Tuple[npt.NDArray[np.int8] | None, XZFaultList: The reduced fault list (self if inplace=True, otherwise a copy). """ # 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): + 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) + 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): + 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: + + 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) + ret.faults[error_type][i] = np.asarray(coset_leader(ret.faults[error_type][i], g), dtype=np.int8) return ret def __repr__(self) -> str: - _repr = [ - object.__repr__(self), - "X:", - repr(self.faults["X"]), - "Z:", - repr(self.faults["Z"]) - ] - return "\n".join(_repr) \ No newline at end of file + repr_ = [object.__repr__(self), "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 1817e87d8..adbcd88c4 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, XZFaultList +from mqt.qecc.circuit_synthesis.faults import PureFaultSet, XZFaultList, coset_leader, stabilizer_equivalent, t_distinct @pytest.fixture @@ -738,444 +738,441 @@ def test_apply_cnot_not_inplace(): """XZFaultList Tests""" + + @pytest.fixture def fault_list() -> XZFaultList: - 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 + 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: - faults = XZFaultList(num_qubits=4) + 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)) + 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: - faults = XZFaultList(num_qubits=3) + 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([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)) + 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: - faults = XZFaultList(num_qubits=3) + 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))) + 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: - faults = XZFaultList(num_qubits=3) + 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)) + 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)) + 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: - faults = XZFaultList(num_qubits=3) + faults = XZFaultList(num_qubits=3) - with pytest.raises(ValueError, match=r"At least one fault must be provided."): - faults.add_fault((None, None)) + 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: - faults = XZFaultList(num_qubits=3) + 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), - ) - ) + 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)) + 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: - faults = XZFaultList(num_qubits=3) + 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), - ) - ) + 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: - 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), - ) + 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: - faults = XZFaultList(num_qubits=3) + faults = XZFaultList(num_qubits=3) - with pytest.raises(ValueError, match=r"At least one fault array must be provided."): - faults.add_faults((None, None)) + 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: - copied = fault_list.copy() + copied = fault_list.copy() - copied.faults["X"][0, 0] = 0 - copied.faults["Z"][1, 2] = 0 + 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)) + 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: - pairs = list(fault_list) + 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)) + 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: - updated = fault_list.apply_cnot(control=0, target=1, inplace=False) + 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) + 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)) + 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: - result = fault_list.apply_cnot(control=1, target=2, inplace=True) + 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) + 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) + 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: - 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"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=3, target=1) def test_apply_hadamard_swaps_x_and_z_on_target_qubit(fault_list: XZFaultList) -> None: - updated = fault_list.apply_hadamard(qubit=1, inplace=False) + 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) + 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)) + 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: - result = fault_list.apply_hadamard(qubit=0, inplace=True) + 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) + 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) + 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: - with pytest.raises(ValueError, match=r"Qubit index must be between 0 and 2."): - fault_list.apply_hadamard(qubit=3) + 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: - with pytest.raises(ValueError, match=r"Qubit index must be between 0 and 2."): - fault_list.apply_reset(qubit=3) + 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: - faults = XZFaultList(num_qubits=3) - faults.add_fault((np.array([1, 1, 1], dtype=np.int8), np.array([0, 0, 0], dtype=np.int8))) + 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) + 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) + 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)) + 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: - faults = XZFaultList(num_qubits=3) - faults.add_fault((np.array([1, 1, 1], dtype=np.int8), np.array([0, 0, 0], dtype=np.int8))) + 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) + 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) + 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) + 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: - faults = XZFaultList(num_qubits=3) + 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"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) - 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: - faults = XZFaultList(num_qubits=3) + 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"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) - 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: - # 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)) + # 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_reset_clears_selected_qubit_errors(fault_list: XZFaultList) -> None: - updated = fault_list.apply_reset(qubit=1, inplace=False) + 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) + 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)) - 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)) # based on tests for mqt.qecc.circuit_synthesis.faults.coset_leader def test_reduce_to_coset_leaders_no_generators() -> None: - """Test coset leader reduction with no generators (should not modify faults).""" - faults = XZFaultList(num_qubits=3) - faults.add_fault((np.array([1, 0, 1], dtype=np.int8), np.array([0, 1, 0], dtype=np.int8))) + """Test coset leader reduction with no generators (should not modify faults).""" + 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) + # 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)) + 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: - """Test coset leader reduction with X generators.""" - 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))) + """Test coset leader reduction with X generators.""" + 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) + # 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) + 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)) + # 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: - """Test coset leader reduction with Z generators.""" - 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))) + """Test coset leader reduction with Z generators.""" + 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) + # 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) + 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)) + # 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: - """Test coset leader reduction with both X and Z generators.""" - 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))) + """Test coset leader reduction with both X and Z generators.""" + 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) + # 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) + 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 + # 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: - """Test that 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))) + """Test that 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) + x_generators = np.array([[1, 0, 1]], dtype=np.int8) - result = faults.reduce_to_coset_leaders((x_generators, None), inplace=True) + 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)) + # 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: - """Test that inplace=False returns a new independent fault list.""" - original = XZFaultList(num_qubits=3) - original.add_fault((np.array([1, 0, 1], dtype=np.int8), np.array([0, 1, 0], dtype=np.int8))) + """Test that inplace=False returns a new independent fault list.""" + 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) + 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)) + # 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: - """Test coset leader reduction with multiple faults.""" - 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))) + """Test coset leader reduction with multiple faults.""" + 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) + # 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) + 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)) + # 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: - """Test that invalid generator shapes raise ValueError.""" - faults = XZFaultList(num_qubits=3) - faults.add_fault((np.array([1, 0, 1], dtype=np.int8), np.array([0, 1, 0], dtype=np.int8))) + """Test that invalid generator shapes raise ValueError.""" + 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) + # 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) + 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: - """Test that 1D generators raise ValueError.""" - faults = XZFaultList(num_qubits=3) - faults.add_fault((np.array([1, 0, 1], dtype=np.int8), np.array([0, 1, 0], dtype=np.int8))) + """Test that 1D generators raise ValueError.""" + 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) + # 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) + 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: - """Test coset leader reduction with an empty fault list.""" - faults = XZFaultList(num_qubits=3) - - x_generators = np.array([[1, 0, 1]], dtype=np.int8) + """Test coset leader reduction with an empty fault list.""" + 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) + 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) + # 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: - """Test coset leader reduction with empty generators (no reduction applied).""" - faults = XZFaultList(num_qubits=3) - faults.add_fault((np.array([1, 0, 1], dtype=np.int8), np.array([0, 1, 0], dtype=np.int8))) + """Test coset leader reduction with empty generators (no reduction applied).""" + 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) + # Empty generators + x_generators = np.empty((0, 3), dtype=np.int8) - reduced = faults.reduce_to_coset_leaders((x_generators, None), inplace=False) + 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)) \ No newline at end of file + # 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)) From 8852a8223e17665401345064627670bacd426695 Mon Sep 17 00:00:00 2001 From: Yudong Sun Date: Tue, 2 Jun 2026 13:17:12 +0200 Subject: [PATCH 17/34] test apply_reset inplace = True --- tests/circuit_synthesis/test_faults.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/circuit_synthesis/test_faults.py b/tests/circuit_synthesis/test_faults.py index adbcd88c4..426e64828 100644 --- a/tests/circuit_synthesis/test_faults.py +++ b/tests/circuit_synthesis/test_faults.py @@ -1009,6 +1009,17 @@ def test_apply_reset_clears_selected_qubit_errors(fault_list: XZFaultList) -> No 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: + 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: """Test coset leader reduction with no generators (should not modify faults).""" From d449b33a72caa9250584c04ea4ff236cdf4df301 Mon Sep 17 00:00:00 2001 From: Yudong Sun Date: Tue, 2 Jun 2026 14:04:24 +0200 Subject: [PATCH 18/34] add ccx unit tests --- tests/circuit_synthesis/test_faults.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/circuit_synthesis/test_faults.py b/tests/circuit_synthesis/test_faults.py index 426e64828..78bde47fd 100644 --- a/tests/circuit_synthesis/test_faults.py +++ b/tests/circuit_synthesis/test_faults.py @@ -996,6 +996,29 @@ def test_apply_ccz_unit_tests() -> None: 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: + # 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: updated = fault_list.apply_reset(qubit=1, inplace=False) From 3984df5e82c013eaf8291c77a1ae168ac5872f9b Mon Sep 17 00:00:00 2001 From: Yudong Sun Date: Tue, 2 Jun 2026 14:14:36 +0200 Subject: [PATCH 19/34] fixes: `copy` does not propagate `kind` --- src/mqt/qecc/circuit_synthesis/faults.py | 2 +- tests/circuit_synthesis/test_faults.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/mqt/qecc/circuit_synthesis/faults.py b/src/mqt/qecc/circuit_synthesis/faults.py index 7897c2dba..8e92c9bcd 100644 --- a/src/mqt/qecc/circuit_synthesis/faults.py +++ b/src/mqt/qecc/circuit_synthesis/faults.py @@ -273,7 +273,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 diff --git a/tests/circuit_synthesis/test_faults.py b/tests/circuit_synthesis/test_faults.py index 9aa862405..bc54057f6 100644 --- a/tests/circuit_synthesis/test_faults.py +++ b/tests/circuit_synthesis/test_faults.py @@ -735,3 +735,21 @@ def test_apply_cnot_not_inplace(): "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)) + From d7cffabed632b109c47ec73c57b2eb839dfa09e2 Mon Sep 17 00:00:00 2001 From: Yudong Sun Date: Tue, 2 Jun 2026 14:31:43 +0200 Subject: [PATCH 20/34] fixes: filter_faults does not propagate kind when inplace=False. --- src/mqt/qecc/circuit_synthesis/faults.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mqt/qecc/circuit_synthesis/faults.py b/src/mqt/qecc/circuit_synthesis/faults.py index 8e92c9bcd..8877603e9 100644 --- a/src/mqt/qecc/circuit_synthesis/faults.py +++ b/src/mqt/qecc/circuit_synthesis/faults.py @@ -367,7 +367,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. From 3d909191453298bf7af106a544ee5c57bedd52cf Mon Sep 17 00:00:00 2001 From: Yudong Sun Date: Tue, 2 Jun 2026 14:43:13 +0200 Subject: [PATCH 21/34] fixes: combine does not propagate kind and ignores kind mismatch. --- src/mqt/qecc/circuit_synthesis/faults.py | 6 ++++- tests/circuit_synthesis/test_faults.py | 30 ++++++++++++++++++++++-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/mqt/qecc/circuit_synthesis/faults.py b/src/mqt/qecc/circuit_synthesis/faults.py index 8877603e9..0cc9441fa 100644 --- a/src/mqt/qecc/circuit_synthesis/faults.py +++ b/src/mqt/qecc/circuit_synthesis/faults.py @@ -85,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. diff --git a/tests/circuit_synthesis/test_faults.py b/tests/circuit_synthesis/test_faults.py index bc54057f6..ca03d6600 100644 --- a/tests/circuit_synthesis/test_faults.py +++ b/tests/circuit_synthesis/test_faults.py @@ -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(): From 98d32b6cd394cbeadb726ee26638a0599c0da6c5 Mon Sep 17 00:00:00 2001 From: Yudong Sun Date: Tue, 2 Jun 2026 14:47:25 +0200 Subject: [PATCH 22/34] fixes: permute_qubits does not propagate kind when inplace=False. AND __eq__ also checks kind --- src/mqt/qecc/circuit_synthesis/faults.py | 4 ++-- tests/circuit_synthesis/test_faults.py | 10 ++++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/mqt/qecc/circuit_synthesis/faults.py b/src/mqt/qecc/circuit_synthesis/faults.py index 0cc9441fa..dc126ba5c 100644 --- a/src/mqt/qecc/circuit_synthesis/faults.py +++ b/src/mqt/qecc/circuit_synthesis/faults.py @@ -261,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. @@ -392,7 +392,7 @@ 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). diff --git a/tests/circuit_synthesis/test_faults.py b/tests/circuit_synthesis/test_faults.py index ca03d6600..734d1d15d 100644 --- a/tests/circuit_synthesis/test_faults.py +++ b/tests/circuit_synthesis/test_faults.py @@ -653,24 +653,26 @@ 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(): From 3d34523c83c925f50eec0328f8d0a412858348d2 Mon Sep 17 00:00:00 2001 From: Yudong Sun Date: Tue, 2 Jun 2026 15:51:57 +0200 Subject: [PATCH 23/34] fixes: Missing validation for negative qubit indices --- src/mqt/qecc/circuit_synthesis/faults.py | 3 ++- tests/circuit_synthesis/test_faults.py | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/mqt/qecc/circuit_synthesis/faults.py b/src/mqt/qecc/circuit_synthesis/faults.py index dc126ba5c..d86ed7337 100644 --- a/src/mqt/qecc/circuit_synthesis/faults.py +++ b/src/mqt/qecc/circuit_synthesis/faults.py @@ -405,9 +405,10 @@ def apply_cnot(self, control: int, target: int, inplace: bool = True) -> PureFau Returns: A new PureFaultSet with updated faults if inplace is False. """ - if control >= self.num_qubits or target >= self.num_qubits: + 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) diff --git a/tests/circuit_synthesis/test_faults.py b/tests/circuit_synthesis/test_faults.py index 734d1d15d..5413d32fb 100644 --- a/tests/circuit_synthesis/test_faults.py +++ b/tests/circuit_synthesis/test_faults.py @@ -749,6 +749,9 @@ def test_apply_cnot_invalid_qubits(): 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.""" From 201bf814e479dd4c280305e96078cef62bcd0cec Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 13:54:32 +0000 Subject: [PATCH 24/34] =?UTF-8?q?=F0=9F=8E=A8=20pre-commit=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mqt/qecc/circuit_synthesis/faults.py | 6 +++--- tests/circuit_synthesis/test_faults.py | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/mqt/qecc/circuit_synthesis/faults.py b/src/mqt/qecc/circuit_synthesis/faults.py index d86ed7337..f227e62ba 100644 --- a/src/mqt/qecc/circuit_synthesis/faults.py +++ b/src/mqt/qecc/circuit_synthesis/faults.py @@ -277,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, kind = self.kind) + new_set = PureFaultSet(self.num_qubits, kind=self.kind) new_set.faults = np.copy(self.faults) return new_set @@ -371,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, kind = self.kind) + 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. @@ -392,7 +392,7 @@ 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, kind = self.kind) + 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). diff --git a/tests/circuit_synthesis/test_faults.py b/tests/circuit_synthesis/test_faults.py index 5413d32fb..8f972dfb3 100644 --- a/tests/circuit_synthesis/test_faults.py +++ b/tests/circuit_synthesis/test_faults.py @@ -783,4 +783,3 @@ def test_pure_fault_set_copy(): 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)) - From 93c7d7d97a213e65924f70f1c95366b9f57d364e Mon Sep 17 00:00:00 2001 From: Yudong Sun Date: Tue, 2 Jun 2026 16:10:30 +0200 Subject: [PATCH 25/34] fixes: error[D101]: Missing docstring in public class --- src/mqt/qecc/circuit_synthesis/faults.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/mqt/qecc/circuit_synthesis/faults.py b/src/mqt/qecc/circuit_synthesis/faults.py index ae30bf652..059eef281 100644 --- a/src/mqt/qecc/circuit_synthesis/faults.py +++ b/src/mqt/qecc/circuit_synthesis/faults.py @@ -530,6 +530,8 @@ def t_distinct(fs1: PureFaultSet, fs2: PureFaultSet, t: int, stabs: npt.NDArray[ 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. From e9e35e2d02454762a51b49b161f725daa4503e08 Mon Sep 17 00:00:00 2001 From: Yudong Sun Date: Tue, 2 Jun 2026 16:13:43 +0200 Subject: [PATCH 26/34] fixes: error[D420]: Section "Returns" appears after section "Raises" but should be before it --- src/mqt/qecc/circuit_synthesis/faults.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/mqt/qecc/circuit_synthesis/faults.py b/src/mqt/qecc/circuit_synthesis/faults.py index 059eef281..33b516cc7 100644 --- a/src/mqt/qecc/circuit_synthesis/faults.py +++ b/src/mqt/qecc/circuit_synthesis/faults.py @@ -815,12 +815,12 @@ def apply_ccx(self, control1: int, control2: int, target: int, inplace: bool = T 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. - - Returns: - bool: True if everything is okay """ n_q = len(qubits) if any(not 0 <= q < self.num_qubits for q in qubits): @@ -848,12 +848,12 @@ def reduce_to_coset_leaders( 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. - - Returns: - XZFaultList: The reduced fault list (self if inplace=True, otherwise a copy). """ # Setting the corresponding generator to None means no reduction is done From 651b37300501152d95cf5a4a02265417113fe4c8 Mon Sep 17 00:00:00 2001 From: Yudong Sun Date: Tue, 2 Jun 2026 16:17:14 +0200 Subject: [PATCH 27/34] fixes: error[D105]: Missing docstring in magic method --- src/mqt/qecc/circuit_synthesis/faults.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/mqt/qecc/circuit_synthesis/faults.py b/src/mqt/qecc/circuit_synthesis/faults.py index 33b516cc7..8c1af0058 100644 --- a/src/mqt/qecc/circuit_synthesis/faults.py +++ b/src/mqt/qecc/circuit_synthesis/faults.py @@ -878,5 +878,6 @@ def reduce_to_coset_leaders( return ret def __repr__(self) -> str: + """Return a string representation of the XZFaultList.""" repr_ = [object.__repr__(self), "X:", repr(self.faults["X"]), "Z:", repr(self.faults["Z"])] return "\n".join(repr_) From e3f3e1a15371b26056857a88c99fecd63ccf1b9a Mon Sep 17 00:00:00 2001 From: Yudong Sun Date: Tue, 2 Jun 2026 16:42:32 +0200 Subject: [PATCH 28/34] fixes: missing docstring for test functions --- tests/circuit_synthesis/test_faults.py | 49 ++++++++++++++++++++------ 1 file changed, 38 insertions(+), 11 deletions(-) diff --git a/tests/circuit_synthesis/test_faults.py b/tests/circuit_synthesis/test_faults.py index 78bde47fd..8c1d7ab10 100644 --- a/tests/circuit_synthesis/test_faults.py +++ b/tests/circuit_synthesis/test_faults.py @@ -742,6 +742,7 @@ def test_apply_cnot_not_inplace(): @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))) @@ -749,6 +750,7 @@ def fault_list() -> XZFaultList: 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 @@ -757,6 +759,7 @@ def test_initialization_creates_empty_fault_arrays() -> None: 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))) @@ -766,6 +769,7 @@ def test_add_fault_appends_x_and_z_rows() -> None: 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."): @@ -773,6 +777,7 @@ def test_add_fault_rejects_wrong_length() -> None: 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))) @@ -783,6 +788,7 @@ def test_add_fault_replaces_none_with_zeros() -> None: 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."): @@ -790,6 +796,7 @@ def test_add_fault_rejects_both_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(( @@ -802,6 +809,7 @@ def test_add_faults_appends_multiple_rows() -> None: 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."): @@ -812,6 +820,7 @@ def test_add_faults_rejects_wrong_column_count() -> None: 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(( @@ -834,6 +843,7 @@ def test_add_faults_replaces_none_with_zeros() -> None: 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."): @@ -841,6 +851,7 @@ def test_add_faults_rejects_both_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 @@ -851,6 +862,7 @@ def test_copy_returns_independent_fault_list(fault_list: XZFaultList) -> None: 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 @@ -861,6 +873,7 @@ def test_iter_yields_fault_pairs_in_order(fault_list: XZFaultList) -> None: 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) @@ -873,6 +886,7 @@ def test_apply_cnot_updates_x_and_z_faults(fault_list: XZFaultList) -> None: 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) @@ -884,6 +898,7 @@ def test_apply_cnot_inplace_modifies_current_fault_list(fault_list: XZFaultList) 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) @@ -892,6 +907,7 @@ def test_apply_cnot_rejects_invalid_qubits(fault_list: XZFaultList) -> None: 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) @@ -904,6 +920,7 @@ def test_apply_hadamard_swaps_x_and_z_on_target_qubit(fault_list: XZFaultList) - 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) @@ -915,16 +932,19 @@ def test_apply_hadamard_inplace_modifies_current_fault_list(fault_list: XZFaultL 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))) @@ -940,6 +960,7 @@ def test_apply_ccz_updates_z_faults_non_inplace() -> None: 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))) @@ -954,6 +975,7 @@ def test_apply_ccz_inplace_modifies_current_fault_list() -> None: 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."): @@ -964,6 +986,7 @@ def test_apply_ccz_rejects_invalid_controls() -> None: 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."): @@ -974,6 +997,7 @@ def test_apply_ccx_rejects_invalid_qubits() -> None: 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 = [ @@ -997,6 +1021,7 @@ def test_apply_ccz_unit_tests() -> None: 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 = [ @@ -1021,6 +1046,7 @@ def test_apply_ccx_unit_tests() -> None: 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) @@ -1033,6 +1059,7 @@ def test_apply_reset_clears_selected_qubit_errors(fault_list: XZFaultList) -> No 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) @@ -1045,7 +1072,7 @@ def test_apply_reset_clears_selected_qubit_errors_inplace(fault_list: XZFaultLis # based on tests for mqt.qecc.circuit_synthesis.faults.coset_leader def test_reduce_to_coset_leaders_no_generators() -> None: - """Test coset leader reduction with no generators (should not modify faults).""" + """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))) @@ -1057,7 +1084,7 @@ def test_reduce_to_coset_leaders_no_generators() -> None: def test_reduce_to_coset_leaders_x_generators() -> None: - """Test coset leader reduction with X generators.""" + """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))) @@ -1074,7 +1101,7 @@ def test_reduce_to_coset_leaders_x_generators() -> None: def test_reduce_to_coset_leaders_z_generators() -> None: - """Test coset leader reduction with Z generators.""" + """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))) @@ -1091,7 +1118,7 @@ def test_reduce_to_coset_leaders_z_generators() -> None: def test_reduce_to_coset_leaders_both_generators() -> None: - """Test coset leader reduction with both X and Z generators.""" + """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))) @@ -1111,7 +1138,7 @@ def test_reduce_to_coset_leaders_both_generators() -> None: def test_reduce_to_coset_leaders_inplace() -> None: - """Test that inplace=True modifies the original fault list.""" + """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))) @@ -1128,7 +1155,7 @@ def test_reduce_to_coset_leaders_inplace() -> None: def test_reduce_to_coset_leaders_not_inplace() -> None: - """Test that inplace=False returns a new independent fault list.""" + """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))) @@ -1144,7 +1171,7 @@ def test_reduce_to_coset_leaders_not_inplace() -> None: def test_reduce_to_coset_leaders_multiple_faults() -> None: - """Test coset leader reduction with multiple faults.""" + """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))) @@ -1161,7 +1188,7 @@ def test_reduce_to_coset_leaders_multiple_faults() -> None: def test_reduce_to_coset_leaders_invalid_generator_shape() -> None: - """Test that invalid generator shapes raise ValueError.""" + """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))) @@ -1173,7 +1200,7 @@ def test_reduce_to_coset_leaders_invalid_generator_shape() -> None: def test_reduce_to_coset_leaders_invalid_generator_dimension() -> None: - """Test that 1D generators raise ValueError.""" + """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))) @@ -1185,7 +1212,7 @@ def test_reduce_to_coset_leaders_invalid_generator_dimension() -> None: def test_reduce_to_coset_leaders_empty_fault_list() -> None: - """Test coset leader reduction with an empty fault list.""" + """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) @@ -1198,7 +1225,7 @@ def test_reduce_to_coset_leaders_empty_fault_list() -> None: def test_reduce_to_coset_leaders_empty_generators() -> None: - """Test coset leader reduction with empty generators (no reduction applied).""" + """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))) From 4f536412e31b33e3524ce599bc3be5125b5d6107 Mon Sep 17 00:00:00 2001 From: Yudong Sun Date: Tue, 2 Jun 2026 16:47:48 +0200 Subject: [PATCH 29/34] add num_qubits to repr and add test for repr of XZFaultList --- src/mqt/qecc/circuit_synthesis/faults.py | 2 +- tests/circuit_synthesis/test_faults.py | 30 ++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/mqt/qecc/circuit_synthesis/faults.py b/src/mqt/qecc/circuit_synthesis/faults.py index 8c1af0058..86e8ccd56 100644 --- a/src/mqt/qecc/circuit_synthesis/faults.py +++ b/src/mqt/qecc/circuit_synthesis/faults.py @@ -879,5 +879,5 @@ def reduce_to_coset_leaders( def __repr__(self) -> str: """Return a string representation of the XZFaultList.""" - repr_ = [object.__repr__(self), "X:", repr(self.faults["X"]), "Z:", repr(self.faults["Z"])] + 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 8c1d7ab10..24ed17810 100644 --- a/tests/circuit_synthesis/test_faults.py +++ b/tests/circuit_synthesis/test_faults.py @@ -1237,3 +1237,33 @@ def test_reduce_to_coset_leaders_empty_generators() -> None: # 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 + From 9e5b30d353953b33502b2f7c586a647fada4ffb1 Mon Sep 17 00:00:00 2001 From: Yudong Sun Date: Tue, 2 Jun 2026 16:50:17 +0200 Subject: [PATCH 30/34] add test for negative qubit indices for XZFaultList --- tests/circuit_synthesis/test_faults.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/circuit_synthesis/test_faults.py b/tests/circuit_synthesis/test_faults.py index 24ed17810..637df3381 100644 --- a/tests/circuit_synthesis/test_faults.py +++ b/tests/circuit_synthesis/test_faults.py @@ -905,6 +905,9 @@ def test_apply_cnot_rejects_invalid_qubits(fault_list: XZFaultList) -> None: 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.""" From 5c3efb9e0b32fa00033143c42779bca7d415dc65 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 14:53:39 +0000 Subject: [PATCH 31/34] =?UTF-8?q?=F0=9F=8E=A8=20pre-commit=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mqt/qecc/circuit_synthesis/faults.py | 8 +++++++- tests/circuit_synthesis/test_faults.py | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/mqt/qecc/circuit_synthesis/faults.py b/src/mqt/qecc/circuit_synthesis/faults.py index a37c4ea7f..7eb3b896f 100644 --- a/src/mqt/qecc/circuit_synthesis/faults.py +++ b/src/mqt/qecc/circuit_synthesis/faults.py @@ -884,5 +884,11 @@ def reduce_to_coset_leaders( 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"])] + 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 1c141d480..1729b18e9 100644 --- a/tests/circuit_synthesis/test_faults.py +++ b/tests/circuit_synthesis/test_faults.py @@ -1071,6 +1071,7 @@ def test_apply_ccz_unit_tests() -> None: 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 @@ -1317,4 +1318,3 @@ def test_xzfaultlist_repr_empty() -> None: # Check that the representation contains relevant information assert "XZFaultList" in repr_str assert "num_qubits: 2" in repr_str - From d6b6d62ecc009ac569584cd510833286220a75bd Mon Sep 17 00:00:00 2001 From: Yudong Sun Date: Tue, 2 Jun 2026 16:55:50 +0200 Subject: [PATCH 32/34] add return type for __iter__ --- src/mqt/qecc/circuit_synthesis/faults.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mqt/qecc/circuit_synthesis/faults.py b/src/mqt/qecc/circuit_synthesis/faults.py index 7eb3b896f..8e53290ce 100644 --- a/src/mqt/qecc/circuit_synthesis/faults.py +++ b/src/mqt/qecc/circuit_synthesis/faults.py @@ -630,7 +630,7 @@ def copy(self) -> XZFaultList: new_list.faults["Z"] = np.copy(self.faults["Z"]) return new_list - def __iter__(self): + def __iter__(self) -> Iterator[tuple[npt.NDArray[np.int8], npt.NDArray[np.int8]]]: """Iterate over fault pairs in the list. Yields: From 7d8d789b078b4e0ff8b8cc9f1aeec68c64fc69a7 Mon Sep 17 00:00:00 2001 From: Yudong Sun Date: Tue, 2 Jun 2026 16:59:55 +0200 Subject: [PATCH 33/34] fixes: error[PLW2901]: `for` loop variable `g` overwritten by assignment target --- src/mqt/qecc/circuit_synthesis/faults.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mqt/qecc/circuit_synthesis/faults.py b/src/mqt/qecc/circuit_synthesis/faults.py index 8e53290ce..e1c753a53 100644 --- a/src/mqt/qecc/circuit_synthesis/faults.py +++ b/src/mqt/qecc/circuit_synthesis/faults.py @@ -867,9 +867,9 @@ def reduce_to_coset_leaders( # 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): + 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) + 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): From 31c7a8145d4ff37d57b496be9e166b2ac3b0d682 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 15:00:28 +0000 Subject: [PATCH 34/34] =?UTF-8?q?=F0=9F=8E=A8=20pre-commit=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mqt/qecc/circuit_synthesis/faults.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mqt/qecc/circuit_synthesis/faults.py b/src/mqt/qecc/circuit_synthesis/faults.py index e1c753a53..32a220c0e 100644 --- a/src/mqt/qecc/circuit_synthesis/faults.py +++ b/src/mqt/qecc/circuit_synthesis/faults.py @@ -867,9 +867,9 @@ def reduce_to_coset_leaders( # 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): + 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) + 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):