Skip to content

Commit 39d02a5

Browse files
Phase-sensitive Algebra (#768)
## Description This PR extends the symplectic representation with a **phase-sensitive algebra** so that the additional phase-bits are transformed according to the properties of the Pauli matrices. The Pauli-representation layer now supports phases of the whole **n-qubit Pauli group** $\mathfrak{P}_n$, meaning a `Pauli` represents an element of the n-qubit Pauli group and the rows of a `PauliTableau` a subgroup of $\mathfrak{P}_n$. As the symplectic representation `( x | z )` removes phase information, `Pauli` contains the member `phase` (or property `phase_exponent`), which together define the n-qubit Pauli operator $i^{\text{phase}} X^{\text{x}} Z^{\text{z}}$. With this representation, Pauli multiplication and the relation $Y = iXZ$ can be fully simulated with the symplectic vector and the phase exponent. I did *not* restrict the representation to **Hermitian** Paulis (in that case, `Pauli` would only have phases $\pm 1$) or to an Abelian subgroup without $-I$ (in that case, `PauliTableau` could use the commutation assumption to simplify phases to $\pm 1$ during Pauli-multiplication). In my *subjective* opinion, those restrictions should be enforced by the code layer above (`*Code`) and *not* the Pauli-representation. Consequently, however, `Pauli*` supports a slightly more complex algebra due to the possible phases $1, i, -1, -i$. I would also agree that there is no use case yet for the theoretically complete (but more complex) representation of the full Pauli group. So this is an open question. However, even with a restricted version, the simple **Boolean addition of the symplectic vector** (concatenated with a phase bit) is not the correct algebraic operation to simulate phase-sensitive Pauli multiplication, since we would need to include a **correction term** for the phase bit that depends on the $X$/$Z$ overlap. Eg: $P_1 = X \otimes X , P_2 = Z \otimes Z $ $P_1 * P_2 = (X \otimes X)*(Z \otimes Z) = (XZ) \otimes (XZ) = (-iY) \otimes (-iY) = i^2 (Y \otimes Y) = - Y \otimes Y$ Previous representation $(x | z | p)$ with $p \in \\{0,1\\}$ and $P = (-1)^p P_1 \otimes .. \otimes P_n$ and $(x_i | z_i) \sim I \text{ if } (0|0); X \text{ if } (1|0); Y \text{ if } (1|1); Z \text{ if } (0|1)$ meaning $P = (-1)^p i^{x * z} X^x Z^z$: $P_1 \sim (00|11|0) , P_2 \sim (11|00|0)$ $(11|00|0) + (00|11|0) = (11|11|0) \sim (-1)^0 i^{2} (XX) (ZZ) = Y \otimes Y \not = - Y \otimes Y$ thus normal binary addition $+$ does not work for the phases in this representation Phase-sensitive calculation should be $(x | z | e)$ with $e \in \\{0,1, 2, 3\\}$ with $i^e X^x Z^z$: $P_1 \sim (11|00|0) , P_2 \sim (11|00|0)$ $(11|00|0) \oplus (00|11|0) = (11|11|0) \sim i^0 (XX) (ZZ) = -Y \otimes Y $ with $\oplus$ calculating the new phase as $e = e_1 + e_2 + 2*(z_1 * x_2) \text{ mod } 4$ Thus, either way, the normal mod2 `row_echelon` cannot be used in the phase-sensitive context, and we would need a `pauli_ row_echelon` with the correction; nonetheless, no matter if we only restrict ourselves to Hermitian Paulis. Another open question is whether there is a need to **document** the representation layers (or at least high-level classes like `*Code`) and their assumptions in the generated Website. Additionally, we fix the assumption that `generators` of the code have to be **linearly independent**. Fixes #767 Assisted-by: GPT-5.6 Sol as first reviewer ## Checklist - [x] The pull request only contains commits that are focused and relevant to this change. - [x] I have added appropriate tests that cover the new/changed functionality. - [x] I have updated the documentation to reflect these changes. - [x] The changes follow the project's style guidelines and introduce no new warnings. - [x] The changes are fully tested and pass the CI checks. - [x] I have reviewed my own code changes. **If PR contains AI-assisted content:** - [x] Any agent that created, edited, or submitted GitHub content was explicitly authorized for that scope, as required by our [AI Usage Guidelines](https://github.com/munich-quantum-toolkit/qecc/blob/main/docs/ai_usage.md). - [ ] Every agent-authored or agent-edited public text body begins with the visible disclosure `🤖 *AI text below* 🤖` (titles are exempt). - [ ] AI-assisted commits include an `Assisted-by: [Model Name] via [Tool Name]` footer. - [x] I confirm that I have personally reviewed and understood all AI-generated content, and accept full responsibility for it. --------- Co-authored-by: Ludwig Schmid <117631861+lsschmid@users.noreply.github.com> Co-authored-by: Ludwig Schmid <ludwig.s.schmid@tum.de>
1 parent bee0b12 commit 39d02a5

17 files changed

Lines changed: 1017 additions & 158 deletions

File tree

docs/PauliConventions.md

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# Pauli Conventions
2+
3+
This page pins down how Pauli operators, their phases, and stabilizer groups are
4+
represented in `mqt.qecc.codes.core.pauli`. The conventions matter: a phase that
5+
is read as if it were a sign bit produces silently wrong results rather than an
6+
error.
7+
8+
## The representation
9+
10+
A `Pauli` on $n$ qubits is stored as a binary symplectic support $(x \mid z)$
11+
together with a **phase exponent** $p \in \{0,1,2,3\}$:
12+
13+
$$
14+
P = i^{p}\, X^{x} Z^{z}
15+
\qquad\text{where}\qquad
16+
X^{x} Z^{z} = \bigotimes_{j=1}^{n} X^{x_j} Z^{z_j}
17+
$$
18+
19+
The exponent is stored in `Pauli.phase_exponent`; for a `PauliTableau` the
20+
per-row exponents are in `PauliTableau.phase_exponents`.
21+
22+
The single-qubit letters follow from this with $Y = iXZ$:
23+
24+
| letter | $(x_j \mid z_j)$ | contribution to $p$ |
25+
| ------ | ---------------- | ------------------- |
26+
| `I` | $(0 \mid 0)$ | 0 |
27+
| `X` | $(1 \mid 0)$ | 0 |
28+
| `Z` | $(0 \mid 1)$ | 0 |
29+
| `Y` | $(1 \mid 1)$ | 1 |
30+
31+
So a Hermitian Pauli with a `+` sign has $p = x \cdot z$, which is the number of
32+
`Y` letters modulo four — **not** zero. `Pauli(support)` with no explicit
33+
exponent picks exactly this canonical positive Hermitian choice.
34+
35+
## Exponents are not sign bits
36+
37+
`phase_exponent` carries four values, not two. The two are related by
38+
39+
$$
40+
P = (-1)^{r}\, i^{\,x \cdot z}\, X^{x} Z^{z},
41+
\qquad
42+
r = \frac{p - x \cdot z}{2} \bmod 2
43+
$$
44+
45+
Use the explicit converters rather than reaching for the raw exponent:
46+
47+
- `Pauli.sign()` / `PauliTableau.signs()` return the binary sign $r$, and raise
48+
`InvalidPauliError` on a non-Hermitian operator, which has no real sign.
49+
- `Pauli.from_symplectic_and_sign(support, sign)` and
50+
`PauliTableau.phase_from_signs(matrix, signs)` go the other way.
51+
- `Pauli.is_hermitian()` / `PauliTableau.is_hermitian()` test whether a sign
52+
exists at all.
53+
54+
## Multiplication needs a correction term
55+
56+
Because $Z^{z_1} X^{x_2} = (-1)^{z_1 \cdot x_2} X^{x_2} Z^{z_1}$, the product of
57+
two Paulis is
58+
59+
$$
60+
P_1 P_2 = i^{\,p_1 + p_2 + 2 (z_1 \cdot x_2)}
61+
X^{x_1 \oplus x_2} Z^{z_1 \oplus z_2}
62+
$$
63+
64+
The extra $2(z_1 \cdot x_2)$ is why
65+
**XOR-ing symplectic rows and XOR-ing their signs is not Pauli multiplication**.
66+
Concretely:
67+
68+
$$(X \otimes X)(Z \otimes Z) = -\,Y \otimes Y$$
69+
70+
while XOR-ing the two `+` signs would predict $+\,Y \otimes Y$. Note the
71+
correction depends on the number of qubits: $(XXXX)(ZZZZ) = +YYYY$.
72+
73+
Consequences for anyone combining rows of a signed tableau:
74+
75+
- Use `PauliTableau.multiply_rows(target, source)`, never a raw XOR on
76+
`tableau.tableau.data` followed by an XOR on the phases.
77+
- Use `pauli_row_echelon`, not `mod2.row_echelon`, whenever phases must survive
78+
the reduction. A plain mod-2 reduction is only safe on a CSS tableau, where
79+
the pivoting never combines an X-type row with a Z-type row and the correction
80+
term vanishes.
81+
- `PauliTableau.independent_rows()` selects rows by support only and is
82+
explicitly phase-insensitive; do not use it to decide anything about signs.
83+
84+
## Which layer enforces what
85+
86+
The two layers deliberately allow different things:
87+
88+
- **`Pauli` / `PauliTableau` represent the full $n$-qubit Pauli group
89+
$\mathfrak{P}_n$.** Non-Hermitian elements such as `+iX` are legal and
90+
necessary: row reduction genuinely produces them as intermediates, since
91+
$X \cdot Z = -iY$.
92+
- **`StabilizerCode` enforces the stabilizer conditions.** Its constructor
93+
rejects generators that do not commute, are not Hermitian, or together
94+
generate $-I$. Those checks — not the Pauli layer — are what guarantee a valid
95+
code.
96+
97+
Generators need **not** be independent. A redundant generating set is accepted
98+
and kept as given, so `CSSCode` preserves the check matrices you pass in,
99+
including redundant rows that matter for single-shot decoding and meta-checks.
100+
Group-level comparisons (`equal_stabilizer_group`, `stabilizer_equivalent`,
101+
`is_stabilizer`) compare the generated groups and are unaffected by redundancy.
102+
103+
## Subgroups and rank
104+
105+
`pauli_row_echelon` returns a `PauliRowEchelon`, whose `rank` is $\log_2 |G|$
106+
for the generated subgroup $G$. This includes the central scalars, so it can
107+
exceed the number of pivot columns:
108+
109+
| generators | generated subgroup | order | `rank` |
110+
| ------------- | ------------------------------------------------- | ----- | ------ |
111+
| `["XX","ZZ"]` | $\{I, XX, ZZ, -YY\}$ — no scalars beyond $I$ | 4 | 2 |
112+
| `["+iX"]` | $\{I, iX, -I, -iX\}$ — one pivot, scalars $\pm I$ | 4 | 2 |
113+
| `["X","Z"]` | $\{\pm I, \pm X, \pm Z, \pm iY\}$ — anticommuting | 8 | 3 |
114+
115+
The middle row has a single pivot column yet rank 2: the generator squares to
116+
$-I$, so the subgroup contains scalars the support alone cannot account for. The
117+
last row picks up $-I$ from the anticommutator.
118+
119+
To test many Paulis against one subgroup, compute the echelon once and call
120+
`pauli_in_reduced_subgroup`; `PauliTableau.is_in_subgroup` redoes the
121+
elimination on every call.

docs/architecture.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,8 @@ Binary algebra
4242

4343
The layers describe a *builds on* relationship, not exclusively class
4444
inheritance.
45+
46+
Layer 3 carries the phase of a Pauli operator as an exponent rather than a sign
47+
bit, and layer 4 is where Hermiticity and the absence of $-I$ are enforced. The
48+
{doc}`Pauli conventions <PauliConventions>` page spells out both, along with why
49+
combining rows of a signed tableau is not a mod-2 row operation.

docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ tools/compilation
4545
4646
installation
4747
architecture
48+
PauliConventions
4849
references
4950
```
5051

src/mqt/qecc/circuit_synthesis/encoding.py

Lines changed: 31 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -361,23 +361,25 @@ def synthesize_encoding_circuit(
361361
"CliffordSynthesisConfig must be provided when use_cnots_if_css is False."
362362
)
363363

364+
additional_x_rows = code.x_logicals.tableau.data[additional_x_checks]
365+
additional_z_rows = code.z_logicals.tableau.data[additional_z_checks]
364366
gens_mat: npt.NDArray[np.int8] = np.vstack((
365367
code.symplectic,
366-
code.x_logicals.tableau.data[additional_x_checks],
367-
code.z_logicals.tableau.data[additional_z_checks],
368+
additional_x_rows,
369+
additional_z_rows,
368370
))
369371
gens_phase: npt.NDArray[np.int8] = np.hstack((
370-
code.generators.phase,
371-
np.zeros(len(additional_x_checks), dtype=np.int8),
372-
np.zeros(len(additional_z_checks), dtype=np.int8),
372+
code.generators.phase_exponents,
373+
code.x_logicals.phase_exponents[additional_x_checks],
374+
code.z_logicals.phase_exponents[additional_z_checks],
373375
))
374376
log_mat: npt.NDArray[np.int8] = np.vstack((
375377
np.delete(code.x_logicals.tableau.data, additional_checks, axis=0),
376378
np.delete(code.z_logicals.tableau.data, additional_checks, axis=0),
377379
))
378380
log_phase: npt.NDArray[np.int8] = np.hstack((
379-
np.delete(code.x_logicals.phase, additional_checks, axis=0),
380-
np.delete(code.z_logicals.phase, additional_checks, axis=0),
381+
np.delete(code.x_logicals.phase_exponents, additional_checks, axis=0),
382+
np.delete(code.z_logicals.phase_exponents, additional_checks, axis=0),
381383
))
382384

383385
return encoder_from_stabilizers_and_logicals(
@@ -462,32 +464,33 @@ def optimize_tableau(tableau: PauliTableau, stab_rows: list[int]) -> PauliTablea
462464
if i == j:
463465
continue
464466
tab = tableau.copy()
465-
mat = tab.tableau.data
466-
destabs = mat[:half][stab_rows]
467-
stabs = mat[half:][stab_rows]
468-
stabs[i] ^= stabs[j]
469-
destabs[j] ^= destabs[i]
470-
mat[:half][stab_rows] = destabs
471-
mat[half:][stab_rows] = stabs
472-
new_score, _ = score_symplectic(PauliTableau(mat, tableau.phase.copy()))
467+
468+
stab_i_abs, stab_j_abs = half + stab_rows[i], half + stab_rows[j]
469+
destab_i_abs, destab_j_abs = stab_rows[i], stab_rows[j]
470+
471+
# Multiplying a stabilizer onto another must be mirrored on the
472+
# destabilizers in the opposite direction to preserve the symplectic form.
473+
tab.multiply_rows(stab_i_abs, stab_j_abs)
474+
tab.multiply_rows(destab_j_abs, destab_i_abs)
475+
476+
new_score, _ = score_symplectic(tab)
473477
if lexicographical_compare_np(new_score, best[1]):
474478
best = (tab, new_score)
475479
improved = True
476480
for j in range(len(logical_rows)):
477481
tab = tableau.copy()
478-
mat = tab.tableau.data
479-
destabs = mat[:half][stab_rows]
480-
stabs = mat[half:][stab_rows]
481482

482-
other_log = mat[logical_rows[(j + k) % (2 * k)]]
483+
other_log_abs = logical_rows[(j + k) % (2 * k)]
484+
destab_i_abs = stab_rows[i]
485+
log_j_abs = logical_rows[j]
486+
stab_i_abs = half + stab_rows[i]
483487

484-
destabs[i] ^= other_log
485-
logj = mat[logical_rows[j]]
488+
# The stabilizer row is read by the second multiplication, so it must
489+
# not be one of the rows written by the first.
490+
tab.multiply_rows(destab_i_abs, other_log_abs)
491+
tab.multiply_rows(log_j_abs, stab_i_abs)
486492

487-
logj ^= stabs[i]
488-
mat[:half][stab_rows] = destabs
489-
mat[logical_rows[j]] = logj
490-
new_score, _ = score_symplectic(PauliTableau(mat, tableau.phase.copy()))
493+
new_score, _ = score_symplectic(tab)
491494
if lexicographical_compare_np(new_score, best[1]):
492495
best = (tab, new_score)
493496
improved = True
@@ -515,11 +518,11 @@ def combine_stabilizer_and_logical_tableau(stabilizers: PauliTableau, logicals:
515518
# Combine stabilizers and logicals into a single tableau
516519
x_logicals = logicals.tableau.data[: logicals.num_rows() // 2]
517520
z_logicals = logicals.tableau.data[logicals.num_rows() // 2 :]
518-
x_logicals_phase = logicals.phase[: logicals.num_rows() // 2]
519-
z_logicals_phase = logicals.phase[logicals.num_rows() // 2 :]
521+
x_logicals_phase = logicals.phase_exponents[: logicals.num_rows() // 2]
522+
z_logicals_phase = logicals.phase_exponents[logicals.num_rows() // 2 :]
520523
combined_matrix = np.vstack([x_logicals, z_logicals, stabilizers.tableau.data])
521524

522-
combined_phase = np.hstack([x_logicals_phase, z_logicals_phase, stabilizers.phase])
525+
combined_phase = np.hstack([x_logicals_phase, z_logicals_phase, stabilizers.phase_exponents])
523526
combined_tableau = PauliTableau(combined_matrix, combined_phase)
524527

525528
# Complete with destabilizers for the stabilizers only

src/mqt/qecc/circuit_synthesis/exact/search.py

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
from mqt.qecc import mod2
1919

20-
from ...codes.core.pauli import CheckMatrix, StabilizerTableau
20+
from ...codes.core.pauli import CheckMatrix, StabilizerTableau, pauli_row_echelon
2121
from ..circuits import CliffordIsometry, CNOTCircuit
2222
from .encoding_interface import (
2323
CliffordDepthEncoding,
@@ -506,9 +506,9 @@ def _combine_stabilizers_and_logicals(
506506
])
507507

508508
combined_phase = np.concatenate([
509-
x_logicals.phase,
510-
z_logicals.phase,
511-
stabilizers.phase,
509+
x_logicals.phase_exponents,
510+
z_logicals.phase_exponents,
511+
stabilizers.phase_exponents,
512512
])
513513

514514
return StabilizerTableau(combined_matrix, combined_phase)
@@ -671,7 +671,7 @@ def _logical_sign_corrections(
671671
k = num_target_rows - n
672672
target_x = target_tableau.tableau.data[:num_target_rows, :n].astype(np.int8)
673673
target_z = target_tableau.tableau.data[:num_target_rows, n:].astype(np.int8)
674-
target_signs = target_tableau.phase[:num_target_rows].astype(np.int8)
674+
target_signs = target_tableau.signs()[:num_target_rows].astype(np.int8)
675675

676676
x_correction = np.zeros(n, dtype=np.int8)
677677
z_correction = np.zeros(n, dtype=np.int8)
@@ -718,17 +718,24 @@ def _stabilizer_sign_corrections(
718718

719719
target_x = target_tableau.tableau.data[:num_target_rows, :n].astype(np.int8)
720720
target_z = target_tableau.tableau.data[:num_target_rows, n:].astype(np.int8)
721-
target_signs = target_tableau.phase[:num_target_rows].astype(np.int8)
721+
target_signs = target_tableau.signs()[:num_target_rows].astype(np.int8)
722722

723723
circ_stab_symp = np.hstack([circ.z2x[pivot_qubits], circ.z2z[pivot_qubits]]) # (num_stab x 2n)
724724
circ_stab_sign = circ.z_sign[np.array(pivot_qubits)]
725725
targ_stab_symp = np.hstack([target_x[2 * k :], target_z[2 * k :]]) # (num_stab x 2n)
726726

727-
_, r_circ = _gf2_rref_track(circ_stab_symp)
728-
_, r_targ = _gf2_rref_track(targ_stab_symp)
727+
# Phase-sensitive reduction: the sign of a product of Paulis is not the XOR of
728+
# their signs, so the canonical signs must come from the Pauli row echelon.
729+
circ_echelon = pauli_row_echelon(
730+
StabilizerTableau(circ_stab_symp, StabilizerTableau.phase_from_signs(circ_stab_symp, circ_stab_sign))
731+
)
732+
targ_echelon = pauli_row_echelon(
733+
StabilizerTableau(targ_stab_symp, StabilizerTableau.phase_from_signs(targ_stab_symp, target_signs[2 * k :]))
734+
)
729735

730-
s_circ_can = r_circ @ circ_stab_sign % 2
731-
s_targ_can = r_targ @ target_signs[2 * k :] % 2
736+
r_circ = circ_echelon.transform
737+
s_circ_can = circ_echelon.reduced.signs()
738+
s_targ_can = targ_echelon.reduced.signs()
732739

733740
phase_diff = (s_circ_can ^ s_targ_can).astype(np.int8)
734741
if not np.any(phase_diff):

src/mqt/qecc/circuit_synthesis/exact/verification.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,13 @@ def verify_clifford_unitary(circuit: CliffordIsometry, target: StabilizerTableau
4949

5050
act_x: npt.NDArray[np.int8] = actual.tableau.data[:n]
5151
act_z: npt.NDArray[np.int8] = actual.tableau.data[n:]
52-
act_xp: npt.NDArray[np.int8] = actual.phase[:n]
53-
act_zp: npt.NDArray[np.int8] = actual.phase[n:]
52+
act_xp: npt.NDArray[np.int8] = actual.phase_exponents[:n]
53+
act_zp: npt.NDArray[np.int8] = actual.phase_exponents[n:]
5454

5555
tgt_x: npt.NDArray[np.int8] = target.tableau.data[:n]
5656
tgt_z: npt.NDArray[np.int8] = target.tableau.data[n:]
57-
tgt_xp: npt.NDArray[np.int8] = target.phase[:n]
58-
tgt_zp: npt.NDArray[np.int8] = target.phase[n:]
57+
tgt_xp: npt.NDArray[np.int8] = target.phase_exponents[:n]
58+
tgt_zp: npt.NDArray[np.int8] = target.phase_exponents[n:]
5959

6060
# Find permutation π such that actual.X[q] == target.X[π(q)] for each q.
6161
perm: dict[int, int] = {}
@@ -117,9 +117,9 @@ def verify_clifford_isometry(
117117
if num_rows != expected_rows:
118118
return False
119119

120-
x_logicals = StabilizerTableau(target.tableau.data[:k, :], target.phase[:k])
121-
z_logicals = StabilizerTableau(target.tableau.data[k : 2 * k, :], target.phase[k : 2 * k])
122-
stabilizers = StabilizerTableau(target.tableau.data[2 * k :, :], target.phase[2 * k :])
120+
x_logicals = StabilizerTableau(target.tableau.data[:k, :], target.phase_exponents[:k])
121+
z_logicals = StabilizerTableau(target.tableau.data[k : 2 * k, :], target.phase_exponents[k : 2 * k])
122+
stabilizers = StabilizerTableau(target.tableau.data[2 * k :, :], target.phase_exponents[2 * k :])
123123

124124
circuit_code = circuit.get_code()
125125
target_code = StabilizerCode(stabilizers, x_logicals=x_logicals, z_logicals=z_logicals)

src/mqt/qecc/circuit_synthesis/operations.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,13 @@ def apply_stabilizer_tableau_inplace(self, tableau: PauliTableau) -> None:
121121
n = tableau.n
122122
mat = tableau.tableau.data
123123

124+
signs = tableau.signs()
124125
_apply_transvection_numba(
125-
mat[:, self.i], mat[:, self.i + n], mat[:, self.j], mat[:, self.j + n], tableau.phase, *self.v
126+
mat[:, self.i], mat[:, self.i + n], mat[:, self.j], mat[:, self.j + n], signs, *self.v
126127
)
128+
# Write into the existing array: the symplectic data above is mutated in place,
129+
# so rebinding here would leave a caller holding the old phase array.
130+
tableau.phase_exponents[:] = PauliTableau.phase_from_signs(mat, signs)
127131

128132
def apply_stabilizer_tableau(self, tableau: PauliTableau, inplace: bool = False) -> PauliTableau:
129133
"""Apply the transvection operation to a stabilizer tableau."""
@@ -132,9 +136,11 @@ def apply_stabilizer_tableau(self, tableau: PauliTableau, inplace: bool = False)
132136
n = out.n
133137
mat = out.tableau.data
134138

139+
signs = out.signs()
135140
_apply_transvection_numba(
136-
mat[:, self.i], mat[:, self.i + n], mat[:, self.j], mat[:, self.j + n], out.phase, *self.v
141+
mat[:, self.i], mat[:, self.i + n], mat[:, self.j], mat[:, self.j + n], signs, *self.v
137142
)
143+
out.phase_exponents[:] = PauliTableau.phase_from_signs(mat, signs)
138144

139145
return out
140146

src/mqt/qecc/circuit_synthesis/transvection.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ def _score_transvections(
138138
scored: list[tuple[TableauOperation, int | tuple[int, ...]]] = []
139139

140140
original_state = tableau.tableau.data.copy()
141-
original_phase = tableau.phase.copy()
141+
original_phase = tableau.phase_exponents.copy()
142142
for op in operations:
143143
op.apply_stabilizer_tableau_inplace(tableau)
144144
h_vec, _ = score_symplectic(tableau)
@@ -148,7 +148,7 @@ def _score_transvections(
148148
scored.append((op, score_value))
149149

150150
tableau.tableau.data[:] = original_state
151-
tableau.phase[:] = original_phase
151+
tableau.phase_exponents[:] = original_phase
152152
scored.sort(key=operator.itemgetter(1))
153153
return scored
154154

@@ -383,7 +383,7 @@ def fix_tableau_signs_in_place(tableau: PauliTableau) -> list[PauliOperation]:
383383
x_part = tableau.tableau.data[:, :n]
384384
z_part = tableau.tableau.data[:, n:]
385385

386-
phase = tableau.phase.copy()
386+
phase = tableau.signs()
387387

388388
if not np.any(phase):
389389
return []

0 commit comments

Comments
 (0)