Source code for qarp.blocks._primitives.modular_multiplication_block

"""Exact reference synthesis of reversible modular multiplication."""

from math import gcd
from typing import List, Optional

from .._block import SimpleBlock


[docs] class ModularMultiplicationBlock(SimpleBlock): r"""Permutation implementing multiplication modulo a small integer. For ``n = (modulus - 1).bit_length()`` this block acts on the complete ``2**n``-dimensional Hilbert space as .. math:: |x\rangle \mapsto \begin{cases} |m x \bmod N\rangle, & x < N,\\ |x\rangle, & x \geq N. \end{cases} The reference synthesizer enumerates basis labels and is exponential in the work-register width. It is intended for exact small-integer examples, not cryptographic-scale factoring. ``MAX_REFERENCE_WORK_QUBITS`` caps the width at six; measured exact sampling of ``OrderFindingBlock(2, N)`` on the default engine takes 2 s at six work qubits (``N=63``), 31 s at seven (``N=127``) and 155 s at eight (``N=255``), the statevector of ``3n`` total qubits dominating. Raising the constant needs new evidence. Args: multiplier: Integer multiplier. It is normalized modulo ``modulus`` and must be coprime to it. modulus: Integer modulus greater than one. target_qubits: Qubits occupied when embedded in a parent block. name: Block name. """ MAX_REFERENCE_WORK_QUBITS = 6 def __init__( self, multiplier: int, modulus: int, target_qubits: Optional[List[int]] = None, name: str = "ModularMultiplication", ) -> None: if isinstance(modulus, bool) or not isinstance(modulus, int): raise TypeError("modulus must be an integer") if modulus <= 1: raise ValueError("modulus must be greater than one") if isinstance(multiplier, bool) or not isinstance(multiplier, int): raise TypeError("multiplier must be an integer") normalized_multiplier = multiplier % modulus if gcd(normalized_multiplier, modulus) != 1: raise ValueError("multiplier and modulus must be coprime") n_work_qubits = (modulus - 1).bit_length() if n_work_qubits > self.MAX_REFERENCE_WORK_QUBITS: raise ValueError( "ModularMultiplicationBlock's exact reference synthesizer " f"supports at most {self.MAX_REFERENCE_WORK_QUBITS} work qubits; " f"modulus={modulus} requires {n_work_qubits}." ) self.multiplier = normalized_multiplier self.modulus = modulus self.n_work_qubits = n_work_qubits super().__init__( n_qubits=n_work_qubits, target_qubits=target_qubits, name=name, )
[docs] def build_vanilla(self) -> None: permutation = [ self.multiplier * label % self.modulus if label < self.modulus else label for label in range(2**self.n_work_qubits) ] visited: set[int] = set() for start in range(len(permutation)): if start in visited: continue cycle: list[int] = [] current = start while current not in visited: visited.add(current) cycle.append(current) current = permutation[current] # In circuit order these transpositions implement # (cycle[0] cycle[1] ... cycle[-1]). for endpoint in cycle[1:]: self._transpose_basis_states(cycle[0], endpoint)
def _transpose_basis_states(self, first: int, second: int) -> None: """Swap two labels while fixing every other computational basis state.""" if first == second: return path = [first] current = first for qubit in range(self.n_work_qubits): if ((first ^ second) >> qubit) & 1: current ^= 1 << qubit path.append(current) for left, right in zip(path, path[1:], strict=False): self._swap_adjacent_labels(left, right) for left, right in zip(reversed(path[:-2]), reversed(path[1:-1]), strict=True): self._swap_adjacent_labels(left, right) def _swap_adjacent_labels(self, first: int, second: int) -> None: """Swap labels differing in one bit using a mixed-polarity MCX.""" differing = first ^ second target = differing.bit_length() - 1 controls = [qubit for qubit in range(self.n_work_qubits) if qubit != target] zero_controls = [qubit for qubit in controls if ((first >> qubit) & 1) == 0] if zero_controls: self.x(zero_controls) self.h(target) self.mcz([*controls, target]) self.h(target) if zero_controls: self.x(zero_controls)