Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9835f44
create function for applying CNOT to a PureFaultSet
sunjerry019 May 14, 2026
e145970
Test core functionality of apply_cnot
sunjerry019 May 14, 2026
4fe853a
Make kind a property of PureFaultSet
sunjerry019 May 14, 2026
5170585
Test invalid qubit indices for apply_cnot
sunjerry019 May 14, 2026
4fd0446
🎨 pre-commit fixes
pre-commit-ci[bot] May 14, 2026
76afbee
Merge branch 'main' into PureFaultSet_Extension
sunjerry019 May 14, 2026
ab450e9
add new `apply_cnot` function to changelog
sunjerry019 May 14, 2026
0498c8b
new PureFaultSet in apply_cnot should return the correct kind
sunjerry019 May 14, 2026
3f67ea5
Add a test for `inplace` parameter for apply_cnot
sunjerry019 May 14, 2026
015f991
code coverage: Add test for invalid dimensions when creating a PureFa…
sunjerry019 May 14, 2026
451da2d
🎨 pre-commit fixes
pre-commit-ci[bot] May 14, 2026
79d0164
fix pre-commit check > ruff: make `test_PureFaultSet_invalid_kind` lo…
sunjerry019 May 18, 2026
71ceca0
fix pre-commit check > ruff: `pytest.raises()` now only has a single …
sunjerry019 May 18, 2026
a5efac9
Merge remote-tracking branch 'origin/PureFaultSet_Extension' into Pur…
sunjerry019 May 18, 2026
2342029
resolve merge conflict from main due to CHANGELOG.md
sunjerry019 May 22, 2026
c0d98b1
Merge branch 'main' into PureFaultSet_Extension
sunjerry019 May 22, 2026
58ea954
🎨 pre-commit fixes
pre-commit-ci[bot] May 22, 2026
ea5d490
Merge branch 'main' into PureFaultSet_Extension
sunjerry019 Jun 1, 2026
3984df5
fixes: `copy` does not propagate `kind`
sunjerry019 Jun 2, 2026
d7cffab
fixes: filter_faults does not propagate kind when inplace=False.
sunjerry019 Jun 2, 2026
3d90919
fixes: combine does not propagate kind and ignores kind mismatch.
sunjerry019 Jun 2, 2026
98d32b6
fixes: permute_qubits does not propagate kind when inplace=False. AND…
sunjerry019 Jun 2, 2026
3d34523
fixes: Missing validation for negative qubit indices
sunjerry019 Jun 2, 2026
7f5f0c3
Merge remote-tracking branch 'refs/remotes/origin/PureFaultSet_Extens…
sunjerry019 Jun 2, 2026
201bf81
🎨 pre-commit fixes
pre-commit-ci[bot] Jun 2, 2026
4c896e0
Merge branch 'main' into PureFaultSet_Extension
sunjerry019 Jun 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 59 additions & 9 deletions src/mqt/qecc/circuit_synthesis/faults.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -72,10 +85,14 @@ def combine(self, other: PureFaultSet, inplace: bool = False) -> PureFaultSet:
raise ValueError(msg)
combined_faults = np.vstack([self.faults, other.faults])

if self.kind != other.kind:
msg = "Fault sets must have the same kind to combine."
raise ValueError(msg)

if inplace:
self.faults = combined_faults
return self
return PureFaultSet.from_fault_array(combined_faults)
return PureFaultSet.from_fault_array(combined_faults, kind=self.kind)

def to_array(self) -> npt.NDArray[np.int8]:
"""Convert the fault set to a numpy array.
Expand All @@ -86,7 +103,7 @@ def to_array(self) -> npt.NDArray[np.int8]:
return self.faults

@classmethod
def from_fault_array(cls, array: npt.NDArray[np.int8]) -> PureFaultSet:
def from_fault_array(cls, array: npt.NDArray[np.int8], kind: str = "X") -> PureFaultSet:
"""Create a PureFaultSet from a numpy array of faults.

Returns:
Expand All @@ -95,7 +112,7 @@ def from_fault_array(cls, array: npt.NDArray[np.int8]) -> PureFaultSet:
if array.ndim != 2:
msg = "Input array must be 2-dimensional."
raise ValueError(msg)
fault_set = cls(array.shape[1])
fault_set = cls(array.shape[1], kind=kind)
fault_set.faults = np.unique(array, axis=0)
return fault_set

Expand Down Expand Up @@ -124,7 +141,9 @@ def from_cnot_circuit(cls, circ: CNOTCircuit, kind: str = "X", reduce: bool = Fa
qubit_faults[ctrl].append(new_fault)

# Create the fault set
fs = cls.from_fault_array(np.array([fault for faults in qubit_faults for fault in faults], dtype=np.int8))
fs = cls.from_fault_array(
np.array([fault for faults in qubit_faults for fault in faults], dtype=np.int8), kind=kind
)
if not reduce:
return fs

Expand Down Expand Up @@ -242,7 +261,7 @@ def __eq__(self, other: object) -> bool:
"""
if not isinstance(other, PureFaultSet):
return False
return self.num_qubits == other.num_qubits and self.to_set() == other.to_set()
return self.num_qubits == other.num_qubits and self.to_set() == other.to_set() and self.kind == other.kind

def __hash__(self) -> int:
"""Return a hash of the PureFaultSet.
Expand All @@ -258,7 +277,7 @@ def copy(self) -> PureFaultSet:
Returns:
A new PureFaultSet object with the same faults and number of qubits.
"""
new_set = PureFaultSet(self.num_qubits)
new_set = PureFaultSet(self.num_qubits, kind=self.kind)
new_set.faults = np.copy(self.faults)
return new_set

Expand Down Expand Up @@ -352,7 +371,7 @@ def filter_faults(self, pred: Callable[[npt.NDArray[np.int8]], bool], inplace: b
self.faults = filtered
return self

return PureFaultSet.from_fault_array(filtered)
return PureFaultSet.from_fault_array(filtered, kind=self.kind)

def permute_qubits(self, permutation: npt.NDArray[np.int8] | list[int], inplace: bool = True) -> PureFaultSet:
"""Permute the qubits in the fault set according to a given permutation.
Expand All @@ -373,7 +392,38 @@ def permute_qubits(self, permutation: npt.NDArray[np.int8] | list[int], inplace:
self.faults = permuted_faults
return self

return PureFaultSet.from_fault_array(permuted_faults)
return PureFaultSet.from_fault_array(permuted_faults, kind=self.kind)

def apply_cnot(self, control: int, target: int, inplace: bool = True) -> PureFaultSet:
"""Apply a CNOT gate to the faults in the set, based on the type of faults (X or Z).

Args:
control: The index of the control qubit.
target: The index of the target qubit.
inplace: If True, modifies the current fault set. If False, returns a new PureFaultSet with updated faults.

Returns:
A new PureFaultSet with updated faults if inplace is False.
"""
if not (0 <= control < self.num_qubits) or not (0 <= target < self.num_qubits):
msg = f"Control and target indices must be between 0 and {self.num_qubits - 1}."
raise ValueError(msg)
# Dev Note: We do not allow negative indices so that we can easily check if control and target are different
if control == target:
msg = "Control and target qubits must be different."
raise ValueError(msg)

updated_faults = np.copy(self.faults)
if self.kind == "X":
updated_faults[:, target] ^= updated_faults[:, control]
else: # self.kind == "Z"
updated_faults[:, control] ^= updated_faults[:, target]

if inplace:
self.faults = updated_faults
return self

return PureFaultSet.from_fault_array(updated_faults, kind=self.kind)


def coset_leader(fault: npt.NDArray[np.int8], generators: npt.NDArray[np.int8]) -> npt.NDArray[np.int8]:
Expand Down
158 changes: 152 additions & 6 deletions tests/circuit_synthesis/test_faults.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand All @@ -98,6 +124,14 @@ def test_from_fault_array():
assert set(map(tuple, result)) == set(map(tuple, faults)), "Fault set was not created correctly from array."


def test_from_fault_array_invalid_dimension():
"""Test creating a PureFaultSet from an array with invalid dimensions."""
faults = np.array([1, 0, 1], dtype=np.int8) # 1D array instead of 2D

with pytest.raises(ValueError, match=r"Input array must be 2-dimensional."):
PureFaultSet.from_fault_array(faults)


@pytest.mark.parametrize(
("stabs_fixture", "initial_faults", "expected_faults"),
[
Expand Down Expand Up @@ -619,21 +653,133 @@ def test_not_t_distinct_four_qubits():
def test_permute_qubits_basic():
"""Test basic permutation of faults."""
faults = np.array([[1, 1, 0], [0, 1, 1]], dtype=np.int8)
fault_set = PureFaultSet.from_fault_array(faults)
fault_set = PureFaultSet.from_fault_array(faults, kind="Z")
permutation = [2, 0, 1]

permuted_fault_set = fault_set.permute_qubits(permutation, inplace=False)

assert np.array_equal(permuted_fault_set.faults, faults[:, permutation]), "Faults were not permuted correctly"
assert fault_set == PureFaultSet.from_fault_array(faults), "Original fault set should remain unchanged"
assert fault_set == PureFaultSet.from_fault_array(faults, kind="Z"), "Original fault set should remain unchanged"
assert permuted_fault_set.kind == "Z", "Fault kind should be preserved after permutation"
assert fault_set.kind == "Z", "Original fault kind should be preserved after permutation"


def test_permute_qubits_inplace():
"""Test inplace permutation of fault set."""
faults = np.array([[1, 1, 0], [0, 0, 1]], dtype=np.int8)
fault_set = PureFaultSet.from_fault_array(faults)
fault_set = PureFaultSet.from_fault_array(faults, kind="Z")
permutation = [2, 0, 1]

fault_set.permute_qubits(permutation, inplace=True)

assert fault_set != PureFaultSet.from_fault_array(faults), "Faults were not permuted correctly in place"
assert fault_set != PureFaultSet.from_fault_array(faults, kind="Z"), "Faults were not permuted correctly in place"


def test_invalid_fault_kind():
"""Test that an invalid kind raises an assertion error."""
with pytest.raises(AssertionError, match=r"Kind must be either 'X' or 'Z'."):
pfs = PureFaultSet(5, kind="Y")

with pytest.raises(AssertionError, match=r"Kind must be either 'X' or 'Z'."):
pfs = PureFaultSet.from_fault_array(np.array([[1, 0, 1]], dtype=np.int8), kind="Y")

pfs = PureFaultSet(5)
with pytest.raises(AssertionError, match=r"Kind must be either 'X' or 'Z'."):
pfs.kind = "Y"


def test_apply_cnot_x():
"""Test applying a CNOT gate to the fault set."""
faults1 = np.array([[1, 0, 0]], dtype=np.int8)
fault_set1 = PureFaultSet.from_fault_array(faults1, kind="X")

# Apply CNOT with control=0 and target=1
fault_set1.apply_cnot(control=0, target=1)

expected_faults1 = np.array([[1, 1, 0]], dtype=np.int8)
assert np.array_equal(fault_set1.to_array(), expected_faults1), (
"CNOT gate was not applied correctly to the fault set"
)

faults2 = np.array([[0, 1, 0]], dtype=np.int8)
fault_set2 = PureFaultSet.from_fault_array(faults2, kind="X")

# Apply CNOT with control=0 and target=1
fault_set2.apply_cnot(control=0, target=1)

expected_faults2 = np.array([[0, 1, 0]], dtype=np.int8)
assert np.array_equal(fault_set2.to_array(), expected_faults2), (
"CNOT gate was not applied correctly to the fault set"
)


def test_apply_cnot_z():
"""Test applying a CNOT gate to the fault set."""
faults1 = np.array([[1, 0, 0]], dtype=np.int8)
fault_set1 = PureFaultSet.from_fault_array(faults1, kind="Z")

# Apply CNOT with control=0 and target=1
fault_set1.apply_cnot(control=0, target=1)

expected_faults1 = np.array([[1, 0, 0]], dtype=np.int8)
assert np.array_equal(fault_set1.to_array(), expected_faults1), (
"CNOT gate was not applied correctly to the fault set"
)

faults2 = np.array([[0, 1, 0]], dtype=np.int8)
fault_set2 = PureFaultSet.from_fault_array(faults2, kind="Z")

# Apply CNOT with control=0 and target=1
fault_set2.apply_cnot(control=0, target=1)

expected_faults2 = np.array([[1, 1, 0]], dtype=np.int8)
assert np.array_equal(fault_set2.to_array(), expected_faults2), (
"CNOT gate was not applied correctly to the fault set"
)


def test_apply_cnot_invalid_qubits():
"""Test that applying a CNOT gate with invalid qubit indices raises an error."""
faults = np.array([[1, 0, 0]], dtype=np.int8)
fault_set = PureFaultSet.from_fault_array(faults)

with pytest.raises(ValueError, match=r"Control and target qubits must be different."):
fault_set.apply_cnot(control=0, target=0)

with pytest.raises(ValueError, match=r"Control and target indices must be between 0 and 2."):
fault_set.apply_cnot(control=3, target=1)
Comment thread
sunjerry019 marked this conversation as resolved.

with pytest.raises(ValueError, match=r"Control and target indices must be between 0 and 2."):
fault_set.apply_cnot(control=-1, target=1)


def test_apply_cnot_not_inplace():
"""Test that applying a CNOT gate does not modify the original fault set when inplace=False."""
faults = np.array([[1, 0, 0]], dtype=np.int8)
fault_set = PureFaultSet.from_fault_array(faults)

# Apply CNOT with control=0 and target=1 without modifying the original fault set
new_fault_set = fault_set.apply_cnot(control=0, target=1, inplace=False)

expected_new_faults = np.array([[1, 1, 0]], dtype=np.int8)
assert np.array_equal(new_fault_set.to_array(), expected_new_faults), (
"CNOT gate was not applied correctly to the new fault set"
)
assert np.array_equal(fault_set.to_array(), faults), "Original fault set should remain unchanged"


def test_pure_fault_set_copy():
"""Test that PureFaultSet.copy() returns an independent copy."""
faults = np.array([[1, 0, 0], [0, 1, 1]], dtype=np.int8)
fault_set = PureFaultSet.from_fault_array(faults, kind="Z")

copied_fault_set = fault_set.copy()

assert copied_fault_set is not fault_set
assert np.array_equal(copied_fault_set.to_array(), fault_set.to_array())
assert copied_fault_set.kind == fault_set.kind

copied_fault_set.apply_cnot(control=0, target=1)

assert not np.array_equal(copied_fault_set.to_array(), fault_set.to_array())
assert np.array_equal(fault_set.to_array(), np.unique(faults, axis=0))
Loading