From 8a546b123b64c2dc04bc11f7b7cb2b7430b6f019 Mon Sep 17 00:00:00 2001 From: Max Glick Date: Fri, 17 Oct 2025 09:27:20 -0400 Subject: [PATCH 01/10] Add a common base class for MultiControlPauli and its specializations. --- qualtran/bloqs/mcmt/multi_control_pauli.py | 74 +++++++++++++--------- 1 file changed, 44 insertions(+), 30 deletions(-) diff --git a/qualtran/bloqs/mcmt/multi_control_pauli.py b/qualtran/bloqs/mcmt/multi_control_pauli.py index aceff0c99d..a16e81667d 100644 --- a/qualtran/bloqs/mcmt/multi_control_pauli.py +++ b/qualtran/bloqs/mcmt/multi_control_pauli.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import abc import warnings from functools import cached_property from typing import Dict, Optional, Tuple, TYPE_CHECKING, Union @@ -45,27 +46,12 @@ from qualtran.resource_counting import BloqCountDictT, SympySymbolAllocator -@frozen -class MultiControlPauli(GateWithRegisters): - r"""Implements multi-control, single-target C^{n}P gate. - - Implements $C^{n}P = (1 - |1^{n}><1^{n}|) I + |1^{n}><1^{n}| P^{n}$ using $n-1$ - clean ancillas using a multi-controlled `AND` gate. Uses the Toffoli ladder - construction described in "n−2 Ancilla Bits" section of Ref[1] but uses an - $\text{AND} / \text{AND}^\dagger$ ladder instead for computing / uncomputing - using clean ancillas instead of the Toffoli ladder. The measurement based - uncomputation of $\text{AND}$ does not consume any magic states and thus has - better constant factors. - - References: - [Constructing Large Controlled Nots](https://algassert.com/circuits/2015/06/05/Constructing-Large-Controlled-Nots.html) +class MultiControlPauliBase(GateWithRegisters): + r"""Abstract base class for MultiControlPauli and its specializations. """ - cvs: Union[HasLength, Tuple[int, ...]] = field(converter=_to_tuple_or_has_length) - target_bloq: Bloq - - def __attrs_post_init__(self): - warnings.warn( + def __init__(self): + warnings.warn( "`MultiControlPauli` is deprecated. Use `bloq.controlled(...)` which now defaults" "to reducing controls using an `And` ladder." "For the same signature as `MultiControlPauli(cvs, target_bloq)`," @@ -73,6 +59,14 @@ def __attrs_post_init__(self): DeprecationWarning, ) + @property + @abc.abstractmethod + def cvs(self) -> Union[HasLength, Tuple[int, ...]]: ... + + @property + @abc.abstractmethod + def target_bloq(self) -> 'Bloq': ... + @cached_property def signature(self) -> 'Signature': ctrl = Register('controls', QBit(), shape=(self.n_ctrls,)) @@ -165,7 +159,27 @@ def _has_unitary_(self) -> bool: @frozen -class MultiControlX(MultiControlPauli): +class MultiControlPauli(MultiControlPauliBase): + r"""Implements multi-control, single-target C^{n}P gate. + + Implements $C^{n}P = (1 - |1^{n}><1^{n}|) I + |1^{n}><1^{n}| P^{n}$ using $n-1$ + clean ancillas using a multi-controlled `AND` gate. Uses the Toffoli ladder + construction described in "n−2 Ancilla Bits" section of Ref[1] but uses an + $\text{AND} / \text{AND}^\dagger$ ladder instead for computing / uncomputing + using clean ancillas instead of the Toffoli ladder. The measurement based + uncomputation of $\text{AND}$ does not consume any magic states and thus has + better constant factors. + + References: + [Constructing Large Controlled Nots](https://algassert.com/circuits/2015/06/05/Constructing-Large-Controlled-Nots.html) + """ + + cvs: Union[HasLength, Tuple[int, ...]] = field(converter=_to_tuple_or_has_length) + target_bloq: Bloq + + +@frozen +class MultiControlX(MultiControlPauliBase): r"""Implements multi-control, single-target X gate. Reduces multiple controls to a single control using an `And` ladder. @@ -181,15 +195,15 @@ class MultiControlX(MultiControlPauli): target: single qubit target register. """ - target_bloq: Bloq = field(init=False) - - @target_bloq.default - def _X(self): - return XGate() + cvs: Union[HasLength, Tuple[int, ...]] = field(converter=_to_tuple_or_has_length) def __attrs_post_init__(self): pass + @property + def target_bloq(self) -> 'Bloq': + return XGate() + def adjoint(self) -> 'Bloq': return self @@ -228,14 +242,14 @@ class MultiControlZ(MultiControlPauli): target: single qubit target register. """ - target_bloq: Bloq = field(init=False) - - @target_bloq.default - def _Z(self): - return ZGate() + cvs: Union[HasLength, Tuple[int, ...]] = field(converter=_to_tuple_or_has_length) def __attrs_post_init__(self): pass + @property + def target_bloq(self) -> 'Bloq': + return ZGate() + def adjoint(self) -> 'Bloq': return self From a80e56b95e2e0892849aa0a7d23d0744e0ff2a93 Mon Sep 17 00:00:00 2001 From: Max Glick Date: Fri, 17 Oct 2025 10:00:52 -0400 Subject: [PATCH 02/10] Correct the superclass of MultiControlZ. --- qualtran/bloqs/mcmt/multi_control_pauli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qualtran/bloqs/mcmt/multi_control_pauli.py b/qualtran/bloqs/mcmt/multi_control_pauli.py index a16e81667d..b447272535 100644 --- a/qualtran/bloqs/mcmt/multi_control_pauli.py +++ b/qualtran/bloqs/mcmt/multi_control_pauli.py @@ -226,7 +226,7 @@ def _ccpauli_symb() -> MultiControlX: @frozen -class MultiControlZ(MultiControlPauli): +class MultiControlZ(MultiControlPauliBase): r"""Implements multi-control, single-target Z gate. Reduces multiple controls to a single control using an `And` ladder. From 17b197c857ff039fa266aa3424136caed0dd7d7a Mon Sep 17 00:00:00 2001 From: Max Glick Date: Sun, 19 Oct 2025 17:35:09 -0400 Subject: [PATCH 03/10] Updates based on PR comments. --- qualtran/bloqs/mcmt/multi_control_pauli.py | 23 +++++++++++----------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/qualtran/bloqs/mcmt/multi_control_pauli.py b/qualtran/bloqs/mcmt/multi_control_pauli.py index b447272535..4ce308b696 100644 --- a/qualtran/bloqs/mcmt/multi_control_pauli.py +++ b/qualtran/bloqs/mcmt/multi_control_pauli.py @@ -46,18 +46,8 @@ from qualtran.resource_counting import BloqCountDictT, SympySymbolAllocator -class MultiControlPauliBase(GateWithRegisters): - r"""Abstract base class for MultiControlPauli and its specializations. - """ - - def __init__(self): - warnings.warn( - "`MultiControlPauli` is deprecated. Use `bloq.controlled(...)` which now defaults" - "to reducing controls using an `And` ladder." - "For the same signature as `MultiControlPauli(cvs, target_bloq)`," - "use `target_bloq.controlled(CtrlSpec(cvs=cvs))`.", - DeprecationWarning, - ) +class MultiControlPauliBase(GateWithRegisters, metaclass=abc.ABCMeta): + r"""Abstract base class for MultiControlPauli and its specializations.""" @property @abc.abstractmethod @@ -177,6 +167,15 @@ class MultiControlPauli(MultiControlPauliBase): cvs: Union[HasLength, Tuple[int, ...]] = field(converter=_to_tuple_or_has_length) target_bloq: Bloq + def __attrs_post_init__(self): + warnings.warn( + "`MultiControlPauli` is deprecated. Use `bloq.controlled(...)` which now defaults" + "to reducing controls using an `And` ladder." + "For the same signature as `MultiControlPauli(cvs, target_bloq)`," + "use `target_bloq.controlled(CtrlSpec(cvs=cvs))`.", + DeprecationWarning, + ) + @frozen class MultiControlX(MultiControlPauliBase): From e78143bc99349cab07e395cb4e8b6e7fe17a09e1 Mon Sep 17 00:00:00 2001 From: Max Glick Date: Mon, 20 Oct 2025 18:34:59 -0400 Subject: [PATCH 04/10] Make target_bloq a cached_property. --- qualtran/bloqs/mcmt/multi_control_pauli.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/qualtran/bloqs/mcmt/multi_control_pauli.py b/qualtran/bloqs/mcmt/multi_control_pauli.py index 4ce308b696..2953333a1c 100644 --- a/qualtran/bloqs/mcmt/multi_control_pauli.py +++ b/qualtran/bloqs/mcmt/multi_control_pauli.py @@ -196,10 +196,7 @@ class MultiControlX(MultiControlPauliBase): cvs: Union[HasLength, Tuple[int, ...]] = field(converter=_to_tuple_or_has_length) - def __attrs_post_init__(self): - pass - - @property + @cached_property def target_bloq(self) -> 'Bloq': return XGate() @@ -243,10 +240,7 @@ class MultiControlZ(MultiControlPauliBase): cvs: Union[HasLength, Tuple[int, ...]] = field(converter=_to_tuple_or_has_length) - def __attrs_post_init__(self): - pass - - @property + @cached_property def target_bloq(self) -> 'Bloq': return ZGate() From 68e1c34eae1d58d297f4e1fde6b546fc2ead467a Mon Sep 17 00:00:00 2001 From: Max Glick Date: Fri, 5 Dec 2025 17:09:37 -0500 Subject: [PATCH 05/10] Add a classical action for SelectedMajoranaFermion --- .../multiplexers/selected_majorana_fermion.py | 33 ++++++++++++++++++- .../selected_majorana_fermion_test.py | 14 +++++++- qualtran/testing.py | 26 +++++++++++++++ 3 files changed, 71 insertions(+), 2 deletions(-) diff --git a/qualtran/bloqs/multiplexers/selected_majorana_fermion.py b/qualtran/bloqs/multiplexers/selected_majorana_fermion.py index fd5a94952e..e2b09d9a66 100644 --- a/qualtran/bloqs/multiplexers/selected_majorana_fermion.py +++ b/qualtran/bloqs/multiplexers/selected_majorana_fermion.py @@ -13,7 +13,7 @@ # limitations under the License. from functools import cached_property -from typing import Iterator, Sequence, Tuple, Union +from typing import Dict, Iterator, Sequence, Tuple, Union import attrs import cirq @@ -137,5 +137,36 @@ def nth_operation( # type: ignore[override] yield self.target_gate(target[target_idx]).controlled_by(control) yield cirq.CZ(*accumulator, target[target_idx]) + def on_classical_vals(self, **vals ) -> Dict[str, 'ClassicalValT']: + if self.target_gate != cirq.X: + return NotImplemented + if len(self.control_registers) > 1 or len(self.selection_registers) > 1: + return NotImplemented + control_name = self.control_registers[0].name + control = vals[control_name] + selection_name = self.selection_registers[0].name + selection = vals[selection_name] + target = vals['target'] + if control: + max_selection = self.selection_registers[0].dtype.iteration_length - 1 + target = (2**(max_selection - selection)) ^ target + return {control_name: control, selection_name: selection, 'target': target} + + def basis_state_phase(self, **vals ) -> Union[complex, None]: + if self.target_gate != cirq.X: + return None + if len(self.control_registers) > 1 or len(self.selection_registers) > 1: + return None + control_name = self.control_registers[0].name + control = vals[control_name] + selection_name = self.selection_registers[0].name + selection = vals[selection_name] + target = vals['target'] + if control: + max_selection = self.selection_registers[0].dtype.iteration_length - 1 + num_phases = (target >> (max_selection - selection + 1)).bit_count() + return 1 if (num_phases % 2) == 0 else -1 + return 1 + def __str__(self): return f'SelectedMajoranaFermion({self.target_gate})' diff --git a/qualtran/bloqs/multiplexers/selected_majorana_fermion_test.py b/qualtran/bloqs/multiplexers/selected_majorana_fermion_test.py index d0b9644207..1fa024eabe 100644 --- a/qualtran/bloqs/multiplexers/selected_majorana_fermion_test.py +++ b/qualtran/bloqs/multiplexers/selected_majorana_fermion_test.py @@ -20,7 +20,7 @@ from qualtran._infra.gate_with_registers import get_named_qubits, total_bits from qualtran.bloqs.multiplexers.selected_majorana_fermion import SelectedMajoranaFermion from qualtran.cirq_interop.testing import GateHelper -from qualtran.testing import assert_valid_bloq_decomposition +from qualtran.testing import assert_valid_bloq_decomposition, assert_consistent_phased_classical_action @pytest.mark.slow @@ -148,3 +148,15 @@ def test_selected_majorana_fermion_gate_make_on(): op = gate.on_registers(**get_named_qubits(gate.signature)) op2 = SelectedMajoranaFermion.make_on(target_gate=cirq.X, **get_named_qubits(gate.signature)) assert op == op2 + +@pytest.mark.parametrize("selection_bitsize, target_bitsize", [(2, 4), (3, 5)]) +def test_selected_majorana_fermion_classical_action(selection_bitsize, target_bitsize): + gate = SelectedMajoranaFermion( + Register('selection', BQUInt(selection_bitsize, target_bitsize)), target_gate=cirq.X + ) + assert_consistent_phased_classical_action( + gate, + selection=range(target_bitsize), + target=range(2**target_bitsize), + control=range(2) + ) diff --git a/qualtran/testing.py b/qualtran/testing.py index e06e8f1f78..17b2e97f20 100644 --- a/qualtran/testing.py +++ b/qualtran/testing.py @@ -40,6 +40,7 @@ ) from qualtran._infra.composite_bloq import _get_flat_dangling_soqs from qualtran.symbolics import is_symbolic +from qualtran.simulation.classical_sim import do_phased_classical_simulation if TYPE_CHECKING: from qualtran.drawing import WireSymbol @@ -716,3 +717,28 @@ def assert_consistent_classical_action( np.testing.assert_equal( bloq_res, decomposed_res, err_msg=f'{bloq=} {call_with=} {bloq_res=} {decomposed_res=}' ) + +def assert_consistent_phased_classical_action( + bloq: Bloq, + **parameter_ranges: Union[NDArray, Sequence[int], Sequence[Union[Sequence[int], NDArray]]], +): + """Check that the bloq has a phased classical action consistent with its decomposition. + + Args: + bloq: bloq to test. + parameter_ranges: named arguments giving ranges for each of the registers of the bloq. + """ + cb = bloq.decompose_bloq() + parameter_names = tuple(parameter_ranges.keys()) + for vals in itertools.product(*[parameter_ranges[p] for p in parameter_names]): + call_with = {p: v for p, v in zip(parameter_names, vals)} + bloq_res, bloq_phase = do_phased_classical_simulation(bloq, call_with) + decomposed_res, decomposed_phase = do_phased_classical_simulation(cb, call_with) + np.testing.assert_equal( + bloq_res, decomposed_res, err_msg=f'{bloq=} {call_with=} {bloq_res=} {decomposed_res=}' + ) + np.testing.assert_equal( + bloq_phase, + decomposed_phase, + err_msg=f'{bloq=} {call_with=} {bloq_phase=} {decomposed_phase=}' + ) From 9305db5834f223b0c73bc8532a582597af0431b2 Mon Sep 17 00:00:00 2001 From: Max Glick Date: Tue, 9 Dec 2025 17:16:15 -0500 Subject: [PATCH 06/10] Fix format / typing. --- .../bloqs/multiplexers/selected_majorana_fermion.py | 12 ++++++------ .../multiplexers/selected_majorana_fermion_test.py | 11 ++++++----- qualtran/testing.py | 5 +++-- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/qualtran/bloqs/multiplexers/selected_majorana_fermion.py b/qualtran/bloqs/multiplexers/selected_majorana_fermion.py index e2b09d9a66..9f4875e16b 100644 --- a/qualtran/bloqs/multiplexers/selected_majorana_fermion.py +++ b/qualtran/bloqs/multiplexers/selected_majorana_fermion.py @@ -25,7 +25,7 @@ from qualtran._infra.data_types import BQUInt from qualtran._infra.gate_with_registers import total_bits from qualtran.bloqs.multiplexers.unary_iteration_bloq import UnaryIterationGate - +from qualtran.simulation.classical_sim import ClassicalValT @attrs.frozen class SelectedMajoranaFermion(UnaryIterationGate): @@ -137,7 +137,7 @@ def nth_operation( # type: ignore[override] yield self.target_gate(target[target_idx]).controlled_by(control) yield cirq.CZ(*accumulator, target[target_idx]) - def on_classical_vals(self, **vals ) -> Dict[str, 'ClassicalValT']: + def on_classical_vals(self, **vals) -> Dict[str, 'ClassicalValT']: if self.target_gate != cirq.X: return NotImplemented if len(self.control_registers) > 1 or len(self.selection_registers) > 1: @@ -148,11 +148,11 @@ def on_classical_vals(self, **vals ) -> Dict[str, 'ClassicalValT']: selection = vals[selection_name] target = vals['target'] if control: - max_selection = self.selection_registers[0].dtype.iteration_length - 1 - target = (2**(max_selection - selection)) ^ target + max_selection = self.selection_registers[0].dtype.iteration_length_or_zero() - 1 + target = (2 ** (max_selection - selection)) ^ target return {control_name: control, selection_name: selection, 'target': target} - def basis_state_phase(self, **vals ) -> Union[complex, None]: + def basis_state_phase(self, **vals) -> Union[complex, None]: if self.target_gate != cirq.X: return None if len(self.control_registers) > 1 or len(self.selection_registers) > 1: @@ -163,7 +163,7 @@ def basis_state_phase(self, **vals ) -> Union[complex, None]: selection = vals[selection_name] target = vals['target'] if control: - max_selection = self.selection_registers[0].dtype.iteration_length - 1 + max_selection = self.selection_registers[0].dtype.iteration_length_or_zero() - 1 num_phases = (target >> (max_selection - selection + 1)).bit_count() return 1 if (num_phases % 2) == 0 else -1 return 1 diff --git a/qualtran/bloqs/multiplexers/selected_majorana_fermion_test.py b/qualtran/bloqs/multiplexers/selected_majorana_fermion_test.py index 1fa024eabe..a604f2a87a 100644 --- a/qualtran/bloqs/multiplexers/selected_majorana_fermion_test.py +++ b/qualtran/bloqs/multiplexers/selected_majorana_fermion_test.py @@ -20,7 +20,10 @@ from qualtran._infra.gate_with_registers import get_named_qubits, total_bits from qualtran.bloqs.multiplexers.selected_majorana_fermion import SelectedMajoranaFermion from qualtran.cirq_interop.testing import GateHelper -from qualtran.testing import assert_valid_bloq_decomposition, assert_consistent_phased_classical_action +from qualtran.testing import ( + assert_consistent_phased_classical_action, + assert_valid_bloq_decomposition, +) @pytest.mark.slow @@ -149,14 +152,12 @@ def test_selected_majorana_fermion_gate_make_on(): op2 = SelectedMajoranaFermion.make_on(target_gate=cirq.X, **get_named_qubits(gate.signature)) assert op == op2 + @pytest.mark.parametrize("selection_bitsize, target_bitsize", [(2, 4), (3, 5)]) def test_selected_majorana_fermion_classical_action(selection_bitsize, target_bitsize): gate = SelectedMajoranaFermion( Register('selection', BQUInt(selection_bitsize, target_bitsize)), target_gate=cirq.X ) assert_consistent_phased_classical_action( - gate, - selection=range(target_bitsize), - target=range(2**target_bitsize), - control=range(2) + gate, selection=range(target_bitsize), target=range(2**target_bitsize), control=range(2) ) diff --git a/qualtran/testing.py b/qualtran/testing.py index 17b2e97f20..3adf726c77 100644 --- a/qualtran/testing.py +++ b/qualtran/testing.py @@ -39,8 +39,8 @@ Side, ) from qualtran._infra.composite_bloq import _get_flat_dangling_soqs -from qualtran.symbolics import is_symbolic from qualtran.simulation.classical_sim import do_phased_classical_simulation +from qualtran.symbolics import is_symbolic if TYPE_CHECKING: from qualtran.drawing import WireSymbol @@ -718,6 +718,7 @@ def assert_consistent_classical_action( bloq_res, decomposed_res, err_msg=f'{bloq=} {call_with=} {bloq_res=} {decomposed_res=}' ) + def assert_consistent_phased_classical_action( bloq: Bloq, **parameter_ranges: Union[NDArray, Sequence[int], Sequence[Union[Sequence[int], NDArray]]], @@ -740,5 +741,5 @@ def assert_consistent_phased_classical_action( np.testing.assert_equal( bloq_phase, decomposed_phase, - err_msg=f'{bloq=} {call_with=} {bloq_phase=} {decomposed_phase=}' + err_msg=f'{bloq=} {call_with=} {bloq_phase=} {decomposed_phase=}', ) From b133c37e91e6c3c7b49902d05ef318a47aab51b6 Mon Sep 17 00:00:00 2001 From: Max Glick Date: Tue, 9 Dec 2025 17:23:18 -0500 Subject: [PATCH 07/10] One more reformat. --- qualtran/bloqs/multiplexers/selected_majorana_fermion.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qualtran/bloqs/multiplexers/selected_majorana_fermion.py b/qualtran/bloqs/multiplexers/selected_majorana_fermion.py index 9f4875e16b..b9e7bd0f05 100644 --- a/qualtran/bloqs/multiplexers/selected_majorana_fermion.py +++ b/qualtran/bloqs/multiplexers/selected_majorana_fermion.py @@ -27,6 +27,7 @@ from qualtran.bloqs.multiplexers.unary_iteration_bloq import UnaryIterationGate from qualtran.simulation.classical_sim import ClassicalValT + @attrs.frozen class SelectedMajoranaFermion(UnaryIterationGate): """Implements U s.t. U|l>|Psi> -> |l> T_{l} . Z_{l - 1} ... Z_{0} |Psi> From a0ba13c9851d53cab44d1786a388f9caab1dca5a Mon Sep 17 00:00:00 2001 From: Max Glick Date: Thu, 15 Jan 2026 17:55:26 -0500 Subject: [PATCH 08/10] Also handle Z as the target gate. --- .../multiplexers/selected_majorana_fermion.py | 15 +++++++++++---- .../selected_majorana_fermion_test.py | 5 +++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/qualtran/bloqs/multiplexers/selected_majorana_fermion.py b/qualtran/bloqs/multiplexers/selected_majorana_fermion.py index b9e7bd0f05..ba593ce608 100644 --- a/qualtran/bloqs/multiplexers/selected_majorana_fermion.py +++ b/qualtran/bloqs/multiplexers/selected_majorana_fermion.py @@ -139,7 +139,7 @@ def nth_operation( # type: ignore[override] yield cirq.CZ(*accumulator, target[target_idx]) def on_classical_vals(self, **vals) -> Dict[str, 'ClassicalValT']: - if self.target_gate != cirq.X: + if self.target_gate != cirq.X and self.target_gate != cirq.Z: return NotImplemented if len(self.control_registers) > 1 or len(self.selection_registers) > 1: return NotImplemented @@ -148,13 +148,17 @@ def on_classical_vals(self, **vals) -> Dict[str, 'ClassicalValT']: selection_name = self.selection_registers[0].name selection = vals[selection_name] target = vals['target'] - if control: + + # When target_gate == cirq.X, the action is (modulo phase) a single bitflip. + if control and self.target_gate == cirq.X: max_selection = self.selection_registers[0].dtype.iteration_length_or_zero() - 1 target = (2 ** (max_selection - selection)) ^ target + # When target_gate == cirq.Z, the action is only in the phase. + return {control_name: control, selection_name: selection, 'target': target} def basis_state_phase(self, **vals) -> Union[complex, None]: - if self.target_gate != cirq.X: + if self.target_gate != cirq.X and self.target_gate != cirq.Z: return None if len(self.control_registers) > 1 or len(self.selection_registers) > 1: return None @@ -165,7 +169,10 @@ def basis_state_phase(self, **vals) -> Union[complex, None]: target = vals['target'] if control: max_selection = self.selection_registers[0].dtype.iteration_length_or_zero() - 1 - num_phases = (target >> (max_selection - selection + 1)).bit_count() + if self.target_gate == cirq.X: + num_phases = (target >> (max_selection - selection + 1)).bit_count() + else: + num_phases = (target >> (max_selection - selection)).bit_count() return 1 if (num_phases % 2) == 0 else -1 return 1 diff --git a/qualtran/bloqs/multiplexers/selected_majorana_fermion_test.py b/qualtran/bloqs/multiplexers/selected_majorana_fermion_test.py index a604f2a87a..5017f719dc 100644 --- a/qualtran/bloqs/multiplexers/selected_majorana_fermion_test.py +++ b/qualtran/bloqs/multiplexers/selected_majorana_fermion_test.py @@ -154,9 +154,10 @@ def test_selected_majorana_fermion_gate_make_on(): @pytest.mark.parametrize("selection_bitsize, target_bitsize", [(2, 4), (3, 5)]) -def test_selected_majorana_fermion_classical_action(selection_bitsize, target_bitsize): +@pytest.mark.parametrize("target_gate", [cirq.X, cirq.Z]) +def test_selected_majorana_fermion_classical_action(selection_bitsize, target_bitsize, target_gate): gate = SelectedMajoranaFermion( - Register('selection', BQUInt(selection_bitsize, target_bitsize)), target_gate=cirq.X + Register('selection', BQUInt(selection_bitsize, target_bitsize)), target_gate=target_gate ) assert_consistent_phased_classical_action( gate, selection=range(target_bitsize), target=range(2**target_bitsize), control=range(2) From 059c26e80f106fc18bb248a11099209c7c2da6fd Mon Sep 17 00:00:00 2001 From: Max Glick Date: Wed, 15 Apr 2026 18:48:35 +0000 Subject: [PATCH 09/10] Add more detailed comments. --- .../multiplexers/selected_majorana_fermion.py | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/qualtran/bloqs/multiplexers/selected_majorana_fermion.py b/qualtran/bloqs/multiplexers/selected_majorana_fermion.py index 5f26df7a62..49be5a746f 100644 --- a/qualtran/bloqs/multiplexers/selected_majorana_fermion.py +++ b/qualtran/bloqs/multiplexers/selected_majorana_fermion.py @@ -141,7 +141,7 @@ def nth_operation( # type: ignore[override] def on_classical_vals(self, **vals) -> Dict[str, 'ClassicalValT']: if self.target_gate != cirq.X and self.target_gate != cirq.Z: return NotImplemented - if len(self.control_registers) > 1 or len(self.selection_registers) > 1: + if len(self.control_registers) != 1 or len(self.selection_registers) != 1: return NotImplemented control_name = self.control_registers[0].name control = vals[control_name] @@ -149,7 +149,9 @@ def on_classical_vals(self, **vals) -> Dict[str, 'ClassicalValT']: selection = vals[selection_name] target = vals['target'] - # When target_gate == cirq.X, the action is (modulo phase) a single bitflip. + # When target_gate == cirq.X, flip the selection-th bit in target. The ith bit of a + # size N regirster is addressed with the unsigned integer 2^(N - 1 - i) in our big + # endian convention. if control and self.target_gate == cirq.X: max_selection = self.selection_registers[0].dtype.iteration_length_or_zero() - 1 target = (2 ** (max_selection - selection)) ^ target @@ -160,7 +162,7 @@ def on_classical_vals(self, **vals) -> Dict[str, 'ClassicalValT']: def basis_state_phase(self, **vals) -> Union[complex, None]: if self.target_gate != cirq.X and self.target_gate != cirq.Z: return None - if len(self.control_registers) > 1 or len(self.selection_registers) > 1: + if len(self.control_registers) != 1 or len(self.selection_registers) != 1: return None control_name = self.control_registers[0].name control = vals[control_name] @@ -168,10 +170,19 @@ def basis_state_phase(self, **vals) -> Union[complex, None]: selection = vals[selection_name] target = vals['target'] if control: - max_selection = self.selection_registers[0].dtype.iteration_length_or_zero() - 1 + max_selection = self.selection_registers[0].dtype.iteration_length_or_zero() - 1 + # This gate applies Z in positions 0 through (selection - 1). The effect is + # a phase of plus or minus 1 depending on the parity of the number of ones + # in those positions. For an N-bit big endien integer, the first j bits can + # be isolated by shifting right by N - j. + # + # The target gate X has no additional phase, so calculate as in the + # previous paragraph. if self.target_gate == cirq.X: num_phases = (target >> (max_selection - selection + 1)).bit_count() - else: + # The taget gate Z is applied in position selection, so consider the full + # range 0 through selection. + elif self.target_gate == cirq.Z: num_phases = (target >> (max_selection - selection)).bit_count() return 1 if (num_phases % 2) == 0 else -1 return 1 From ba4c7df3bcdd71e1eff236a090af7939d9f54657 Mon Sep 17 00:00:00 2001 From: Max Glick Date: Wed, 15 Apr 2026 20:06:26 +0000 Subject: [PATCH 10/10] Fix 2 lint issues. --- qualtran/bloqs/multiplexers/selected_majorana_fermion.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qualtran/bloqs/multiplexers/selected_majorana_fermion.py b/qualtran/bloqs/multiplexers/selected_majorana_fermion.py index 49be5a746f..7420f19cc9 100644 --- a/qualtran/bloqs/multiplexers/selected_majorana_fermion.py +++ b/qualtran/bloqs/multiplexers/selected_majorana_fermion.py @@ -170,7 +170,7 @@ def basis_state_phase(self, **vals) -> Union[complex, None]: selection = vals[selection_name] target = vals['target'] if control: - max_selection = self.selection_registers[0].dtype.iteration_length_or_zero() - 1 + max_selection = self.selection_registers[0].dtype.iteration_length_or_zero() - 1 # This gate applies Z in positions 0 through (selection - 1). The effect is # a phase of plus or minus 1 depending on the parity of the number of ones # in those positions. For an N-bit big endien integer, the first j bits can @@ -182,7 +182,7 @@ def basis_state_phase(self, **vals) -> Union[complex, None]: num_phases = (target >> (max_selection - selection + 1)).bit_count() # The taget gate Z is applied in position selection, so consider the full # range 0 through selection. - elif self.target_gate == cirq.Z: + else: num_phases = (target >> (max_selection - selection)).bit_count() return 1 if (num_phases % 2) == 0 else -1 return 1