From 7bd3e21affd6c79c2ce88d1f448b4028f6caa77e Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Thu, 27 Nov 2025 17:00:42 +0100 Subject: [PATCH 01/78] Add multigeometry utilities to base Amplitude Lattice --- qlbm/lattice/lattices/base.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/qlbm/lattice/lattices/base.py b/qlbm/lattice/lattices/base.py index cc4ad15..7582bf9 100644 --- a/qlbm/lattice/lattices/base.py +++ b/qlbm/lattice/lattices/base.py @@ -521,6 +521,15 @@ class AmplitudeLattice(Lattice, ABC): ``qlbm`` currently has 2 amplitude-based lattices: the :class:`.MSLattice` and :class:`.ABLattice` used in the :class:`.MSQLBM` and :class:`.ABQLBM`, respectively. """ + num_base_qubits: int + """The number of qubits required to represent the lattice.""" + + num_ancilla_qubits: int + """The number of ancillary qubits used to perform the algorithm for, i.e., boundary conditions.""" + + num_marker_qubits: int + """The number of qubits used to identify geometries, if parallel lattices are being simulated.""" + def __init__( self, lattice_data, @@ -624,6 +633,32 @@ def velocity_index(self, dim: int | None = None) -> List[int]: """ pass + @abstractmethod + def marker_index(self) -> List[int]: + """ + Get the indices of the qubits addressing the marker. + + This is only useful if multiple lattice geometries are addressed simultaneously. + + Returns + ------- + List[int] + The absolute indices of the marker qubits. + """ + pass + + @abstractmethod + def accumulation_index(self) -> List[int]: + """ + Get the indices of the qubits used for the accumulation register. + + Returns + ------- + List[int] + The absolute indices of the accumulation qubits. + """ + pass + @abstractmethod def get_encoding(self) -> ABEncodingType: """ From 9fe4a19f41b84de45bd0f02a04df1de7d3d0f3d1 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Thu, 27 Nov 2025 17:01:14 +0100 Subject: [PATCH 02/78] Implement multi-geometry utilities for AB, MS, and OH lattices --- qlbm/lattice/lattices/ab_lattice.py | 96 +++++++++++++++++++++++++++-- qlbm/lattice/lattices/ms_lattice.py | 8 +++ qlbm/lattice/lattices/oh_lattice.py | 54 ++++++++++++++-- 3 files changed, 146 insertions(+), 12 deletions(-) diff --git a/qlbm/lattice/lattices/ab_lattice.py b/qlbm/lattice/lattices/ab_lattice.py index db8e601..04b5a5a 100644 --- a/qlbm/lattice/lattices/ab_lattice.py +++ b/qlbm/lattice/lattices/ab_lattice.py @@ -147,17 +147,38 @@ def __init__( self.num_comparator_qubits = 2 * (self.num_dims - 1) self.num_ancilla_qubits = self.num_comparator_qubits + self.num_obstacle_qubits - self.num_total_qubits = self.num_base_qubits + self.num_ancilla_qubits + self.num_marker_qubits = ( + int(ceil(log2(len(self.geometries)))) + if self.has_multiple_geometries() + else 0 + ) + + self.num_total_qubits = ( + self.num_base_qubits + self.num_ancilla_qubits + self.num_marker_qubits + ) + + self.num_accumulation_qubits = 0 + + self.__update_registers() + + def __update_registers(self): + self.num_total_qubits = ( + self.num_base_qubits + self.num_ancilla_qubits + self.num_marker_qubits + ) + + temp_registers = self.get_registers() - temporary_registers = self.get_registers() ( self.grid_registers, self.velocity_registers, self.ancilla_comparator_register, self.ancilla_object_register, - ) = temporary_registers + self.marker_register, + self.accumulation_register, + ) = temp_registers + + self.registers = tuple(flatten(temp_registers)) - self.registers = tuple(flatten(temporary_registers)) self.circuit = QuantumCircuit(*self.registers) @override @@ -286,11 +307,30 @@ def get_registers(self) -> Tuple[List[QuantumRegister], ...]: for c, gp in enumerate(self.num_gridpoints) ] + marker_register = ( + [ + QuantumRegister( + int(ceil(log2(len(self.geometries)))), + name="m", + ) + ] + if self.has_multiple_geometries() + else [] + ) + + accumulation_register = ( + [QuantumRegister(self.num_accumulation_qubits, name="acc")] + if self.has_accumulation_register() + else [] + ) + return ( grid_registers, velocity_registers, ancilla_comparator_register, ancilla_object_register, + marker_register, + accumulation_register, ) @override @@ -303,8 +343,52 @@ def logger_name(self) -> str: return f"ablattice-{self.num_dims}d-{gp_string}-{len(flatten(list(self.shapes.values())))}-obstacle" @override - def has_multiple_geometries(self): - return False # multiple geometries unsupported for ABQLBM right now + def has_multiple_geometries(self) -> bool: + return len(self.geometries) > 1 + + def has_accumulation_register(self) -> bool: + """ + Whether the lattice has a register that accumulates quantities at each step. + + Returns + ------- + bool + Whether the lattice has a register that accumulates quantities at each step. + """ + return self.num_accumulation_qubits > 0 + + def use_accumulation_register(self): + """ + Sets up the accumulation register of the lattice. + + The amplitude-based accumulation method is only currently supported for 1 time step, + at the end of the simulation. More detail on amplitude accumulation can be found + in :cite:t:`qsearch`. + """ + self.num_accumulation_qubits = 1 + + self.__update_registers() + + @override + def marker_index(self) -> List[int]: + return list( + range( + self.num_base_qubits + self.num_ancilla_qubits, + self.num_base_qubits + self.num_ancilla_qubits + self.num_marker_qubits, + ) + ) + + @override + def accumulation_index(self) -> List[int]: + return list( + range( + self.num_base_qubits + self.num_ancilla_qubits + self.num_marker_qubits, + self.num_base_qubits + + self.num_ancilla_qubits + + self.num_marker_qubits + + self.num_accumulation_qubits, + ) + ) @override def get_encoding(self) -> ABEncodingType: diff --git a/qlbm/lattice/lattices/ms_lattice.py b/qlbm/lattice/lattices/ms_lattice.py index 93530f0..914714d 100644 --- a/qlbm/lattice/lattices/ms_lattice.py +++ b/qlbm/lattice/lattices/ms_lattice.py @@ -384,6 +384,14 @@ def velocity_dir_index(self, dim: int | None = None) -> List[int]: return [previous_qubits + dim] + @override + def marker_index(self): + raise LatticeException("Multiple geometries not yet supported for MSLattice.") + + @override + def accumulation_index(self): + raise LatticeException("Accumulation not yet supported for MSLattice.") + def get_registers(self) -> Tuple[List[QuantumRegister], ...]: """Generates the encoding-specific register required for the streaming step. diff --git a/qlbm/lattice/lattices/oh_lattice.py b/qlbm/lattice/lattices/oh_lattice.py index 1583b65..f57abf2 100644 --- a/qlbm/lattice/lattices/oh_lattice.py +++ b/qlbm/lattice/lattices/oh_lattice.py @@ -22,12 +22,12 @@ class OHLattice(ABLattice): r""" Implementation of the :class:`.Lattice` base specific to the 2D and 3D :class:`.ABQLBM` for the One-Hot (OH) encoding. - + In the OH encoding, the grid is compressed into logarithmically many qubits, while the the velocity register is not. For a :math:`1024 \times 1024` lattice with a :math:`D_2Q_9` discretization, the OH encoding requires :math:`2\log_2 1024 + 9 = 29` qubits. - + Each of the discrete velocities is assigned a vector :math:`\ket{\mathbf{e}_j}`, with entry :math:`1` at index :math:`j` and :math:`0` everywhere else. @@ -156,19 +156,42 @@ def __init__( self.num_comparator_qubits = 2 * (self.num_dims - 1) self.num_ancilla_qubits = self.num_comparator_qubits + self.num_obstacle_qubits - self.num_total_qubits = self.num_base_qubits + self.num_ancilla_qubits + self.num_total_qubits = ( + self.num_base_qubits + self.num_ancilla_qubits + self.num_marker_qubits + ) + + self.num_accumulation_qubits = 0 + + self.__update_registers() + + def __update_registers(self): + self.num_total_qubits = ( + self.num_base_qubits + self.num_ancilla_qubits + self.num_marker_qubits + ) + + temp_registers = self.get_registers() - temporary_registers = self.get_registers() ( self.grid_registers, self.velocity_registers, self.ancilla_comparator_register, self.ancilla_object_register, - ) = temporary_registers + self.marker_register, + self.accumulation_register, + ) = temp_registers + + self.registers = tuple(flatten(temp_registers)) - self.registers = tuple(flatten(temporary_registers)) self.circuit = QuantumCircuit(*self.registers) + @override + def marker_index(self): + raise LatticeException("Multiple geometries not yet supported for OHLattice.") + + @override + def accumulation_index(self): + raise LatticeException("Accumulation not yet supported for OHLattice.") + @override def get_registers(self) -> Tuple[List[QuantumRegister], ...]: """Generates the encoding-specific register required for the streaming step. @@ -203,11 +226,30 @@ def get_registers(self) -> Tuple[List[QuantumRegister], ...]: for c, gp in enumerate(self.num_gridpoints) ] + marker_register = ( + [ + QuantumRegister( + int(ceil(log2(len(self.geometries)))), + name="m", + ) + ] + if self.has_multiple_geometries() + else [] + ) + + accumulation_register = ( + [QuantumRegister(self.num_accumulation_qubits, name="acc")] + if self.has_accumulation_register() + else [] + ) + return ( grid_registers, velocity_registers, ancilla_comparator_register, ancilla_object_register, + marker_register, + accumulation_register, ) @override From ad3fe8a022f5f6403c02af33fc84931c2eb1b8a1 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Thu, 27 Nov 2025 17:01:46 +0100 Subject: [PATCH 03/78] Move MS lattice tests --- .../{collisionles_lattice_test.py => lattice/ms_lattice_test.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/unit/{collisionles_lattice_test.py => lattice/ms_lattice_test.py} (100%) diff --git a/test/unit/collisionles_lattice_test.py b/test/unit/lattice/ms_lattice_test.py similarity index 100% rename from test/unit/collisionles_lattice_test.py rename to test/unit/lattice/ms_lattice_test.py From 0f58847c0370dab5a07b58d892d0f9ee500062f9 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Thu, 27 Nov 2025 17:02:13 +0100 Subject: [PATCH 04/78] Add OH Lattice tests --- test/unit/lattice/conftest.py | 34 +++--- .../unit/lattice/oh_lattice_exception_test.py | 105 ++++++++++++++++++ .../lattice/oh_lattice_properties_test.py | 59 ++++++++++ 3 files changed, 185 insertions(+), 13 deletions(-) create mode 100644 test/unit/lattice/oh_lattice_exception_test.py create mode 100644 test/unit/lattice/oh_lattice_properties_test.py diff --git a/test/unit/lattice/conftest.py b/test/unit/lattice/conftest.py index 1c58d5b..29afbc3 100644 --- a/test/unit/lattice/conftest.py +++ b/test/unit/lattice/conftest.py @@ -2,6 +2,7 @@ from qlbm.lattice.geometry.shapes.block import Block from qlbm.lattice.lattices.ab_lattice import ABLattice +from qlbm.lattice.lattices.oh_lattice import OHLattice # 1D Lattices @@ -22,10 +23,7 @@ def dummy_1d_lattice() -> ABLattice: def lattice_1d_16_1_obstacle() -> ABLattice: return ABLattice( { - "lattice": { - "dim": {"x": 16}, - "velocities": "D1Q2" - }, + "lattice": {"dim": {"x": 16}, "velocities": "D1Q2"}, "geometry": [ {"shape": "cuboid", "x": [4, 6], "boundary": "bounceback"}, ], @@ -33,17 +31,13 @@ def lattice_1d_16_1_obstacle() -> ABLattice: ) - # 2D Lattices @pytest.fixture def dummy_2d_lattice() -> ABLattice: return ABLattice( 0, { - "lattice": { - "dim": {"x": 32, "y": 32}, - "velocities": "D2Q4" - }, + "lattice": {"dim": {"x": 32, "y": 32}, "velocities": "D2Q4"}, }, ) @@ -52,10 +46,24 @@ def dummy_2d_lattice() -> ABLattice: def lattice_2d_16x16_1_obstacle() -> ABLattice: return ABLattice( { - "lattice": { - "dim": {"x": 16, "y": 16}, - "velocities": "D2Q4" - }, + "lattice": {"dim": {"x": 16, "y": 16}, "velocities": "D2Q4"}, + "geometry": [ + { + "shape": "cuboid", + "x": [2, 6], + "y": [5, 10], + "boundary": "bounceback", + }, + ], + }, + ) + + +@pytest.fixture +def lattice_2d_16x16_1_obstacle_oh() -> OHLattice: + return OHLattice( + { + "lattice": {"dim": {"x": 16, "y": 16}, "velocities": "D2Q9"}, "geometry": [ { "shape": "cuboid", diff --git a/test/unit/lattice/oh_lattice_exception_test.py b/test/unit/lattice/oh_lattice_exception_test.py new file mode 100644 index 0000000..96a4037 --- /dev/null +++ b/test/unit/lattice/oh_lattice_exception_test.py @@ -0,0 +1,105 @@ +import pytest + +from qlbm.lattice import OHLattice +from qlbm.tools.exceptions import LatticeException + + +def test_lattice_exception_empty_dict(): + with pytest.raises(LatticeException) as excinfo: + OHLattice({}) + + assert 'Input configuration missing "lattice" properties.' == str(excinfo.value) + + +def test_lattice_exception_no_dims(): + with pytest.raises(LatticeException) as excinfo: + OHLattice({"lattice": {}}) + + assert 'Lattice configuration missing "dim" properties.' == str(excinfo.value) + + +def test_lattice_exception_no_velocities(): + with pytest.raises(LatticeException) as excinfo: + OHLattice({"lattice": {"dim": {}}}) + + assert 'Lattice configuration missing "velocities" properties.' == str( + excinfo.value + ) + + +def test_lattice_exception_mismatched_velocities_and_dims(): + with pytest.raises(LatticeException) as excinfo: + OHLattice({"lattice": {"dim": {"x": 64}, "velocities": "D2Q4"}}) + + assert ( + "Velocity specification dimensions (2) do not match lattice dimensions (1)." + == str(excinfo.value) + ) + + +def test_lattice_exception_unsupported_discretization(): + with pytest.raises(LatticeException) as excinfo: + OHLattice({"lattice": {"dim": {"x": 64}, "velocities": {"x": 4}}}) + + assert ( + "Discretization LatticeDiscretization.CFLDISCRETIZATION is not supported." + == str(excinfo.value) + ) + + +def test_lattice_exception_mismatched_bad_dimensions(): + with pytest.raises(LatticeException) as excinfo: + OHLattice( + { + "lattice": { + "dim": {"x": 64, "y": 127}, + "velocities": "D2Q9", + } + } + ) + + assert ( + "Lattice has a number of grid points that is not divisible by 2 in dimension y." + == str(excinfo.value) + ) + + +def test_lattice_exception_mismatched_bad_object_dimensions(): + with pytest.raises(LatticeException) as excinfo: + OHLattice( + { + "lattice": { + "dim": {"x": 64, "y": 64}, + "velocities": "D2Q9", + }, + "geometry": [ + { + "shape": "cuboid", + "x": [5, 6], + "y": [1, 2], + "z": [1, 2], + "boundary": "specular", + }, + ], + } + ) + + assert "Obstacle 1 has 3 dimensions whereas the lattice has 2." == str( + excinfo.value + ) + + +def test_lattice_exception_marker_index(): + lattice = OHLattice({"lattice": {"dim": {"x": 64, "y": 64}, "velocities": "D2Q9"}}) + with pytest.raises(LatticeException) as excinfo: + lattice.marker_index() + + assert "Multiple geometries not yet supported for OHLattice." == str(excinfo.value) + + +def test_lattice_exception_accumulation_index(): + lattice = OHLattice({"lattice": {"dim": {"x": 64, "y": 64}, "velocities": "D2Q9"}}) + with pytest.raises(LatticeException) as excinfo: + lattice.accumulation_index() + + assert "Accumulation not yet supported for OHLattice." == str(excinfo.value) diff --git a/test/unit/lattice/oh_lattice_properties_test.py b/test/unit/lattice/oh_lattice_properties_test.py new file mode 100644 index 0000000..c6dcc5b --- /dev/null +++ b/test/unit/lattice/oh_lattice_properties_test.py @@ -0,0 +1,59 @@ +import pytest + +from qlbm.lattice import OHLattice +from qlbm.tools.exceptions import LatticeException + + +def test_2d_lattice_basic_properties(lattice_2d_16x16_1_obstacle_oh: OHLattice): + assert lattice_2d_16x16_1_obstacle_oh.num_dims == 2 + assert lattice_2d_16x16_1_obstacle_oh.num_gridpoints == [15, 15] + assert lattice_2d_16x16_1_obstacle_oh.num_ancilla_qubits == 3 + assert lattice_2d_16x16_1_obstacle_oh.num_grid_qubits == 8 + assert lattice_2d_16x16_1_obstacle_oh.num_velocity_qubits == 9 + assert lattice_2d_16x16_1_obstacle_oh.num_total_qubits == 20 + + +def test_2d_lattice_grid_register(lattice_2d_16x16_1_obstacle_oh: OHLattice): + assert lattice_2d_16x16_1_obstacle_oh.grid_index(0) == list(range(4)) + assert lattice_2d_16x16_1_obstacle_oh.grid_index(1) == list(range(4, 8)) + assert lattice_2d_16x16_1_obstacle_oh.grid_index() == list(range(8)) + + with pytest.raises(LatticeException) as excinfo: + lattice_2d_16x16_1_obstacle_oh.grid_index(2) + assert ( + "Cannot index grid register for dimension 2 in 2-dimensional lattice." + == str(excinfo.value) + ) + + +def test_2d_lattice_velocity_register( + lattice_2d_16x16_1_obstacle_oh: OHLattice, +): + assert lattice_2d_16x16_1_obstacle_oh.velocity_index() == list(range(8, 17)) + + +def test_2d_lattice_ancilla_comparator_register( + lattice_2d_16x16_1_obstacle_oh: OHLattice, +): + assert lattice_2d_16x16_1_obstacle_oh.ancillae_comparator_index(0) == [17, 18] + assert lattice_2d_16x16_1_obstacle_oh.ancillae_comparator_index() == [17, 18] + + with pytest.raises(LatticeException) as excinfo: + lattice_2d_16x16_1_obstacle_oh.ancillae_comparator_index(1) + assert ( + "Cannot index ancilla comparator register for index 1 in 2-dimensional lattice. Maximum is 0." + == str(excinfo.value) + ) + + +def test_2d_lattice_ancilla_obstacle_register( + lattice_2d_16x16_1_obstacle_oh: OHLattice, +): + assert lattice_2d_16x16_1_obstacle_oh.ancillae_obstacle_index() == [19] + + with pytest.raises(LatticeException) as excinfo: + lattice_2d_16x16_1_obstacle_oh.ancillae_obstacle_index(2) + assert ( + "Cannot index ancilla obstacle register for index 2. Maximum index for this lattice is 0." + == str(excinfo.value) + ) From 3e911fdbe4a68e8bbbbe63797ce8f767af74639c Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Fri, 28 Nov 2025 13:43:22 +0100 Subject: [PATCH 05/78] Add multigeometry support for Amplitude Based QLBM --- qlbm/components/ab/reflection.py | 122 ++++++++++++++++++++++++++----- 1 file changed, 105 insertions(+), 17 deletions(-) diff --git a/qlbm/components/ab/reflection.py b/qlbm/components/ab/reflection.py index 92aa411..e161a52 100644 --- a/qlbm/components/ab/reflection.py +++ b/qlbm/components/ab/reflection.py @@ -3,7 +3,7 @@ from itertools import product from logging import Logger, getLogger from time import perf_counter_ns -from typing import List, Tuple +from typing import List, Tuple, cast from qiskit import QuantumCircuit from qiskit.circuit.library import MCMTGate, XGate @@ -57,12 +57,23 @@ class ABReflectionOperator(LBMOperator): def __init__( self, lattice: ABLattice, - blocks: List[Block], + blocks: List[Block] | None = None, logger: Logger = getLogger("qlbm"), ) -> None: super().__init__(lattice, logger) - self.blocks = blocks + self.blocks = ( + ( + cast(List[Block], flatten(list(self.lattice.geometries[0].values()))) + if not self.lattice.has_multiple_geometries() + else [ + gdict["bounceback"] + gdict["specular"] # type: ignore + for gdict in self.lattice.geometries # type: ignore + ] + ) + if blocks is None + else blocks + ) self.logger.info(f"Creating circuit {str(self)}...") circuit_creation_start_time = perf_counter_ns() @@ -73,24 +84,59 @@ def __init__( @override def create_circuit(self) -> QuantumCircuit: + if self.lattice.discretization not in [LatticeDiscretization.D2Q9]: + raise LatticeException("AB reflection only currently supported in D2Q9") + if self.lattice.discretization == LatticeDiscretization.D2Q9: - return self.__create_circuit_d2q9() + if not self.lattice.has_multiple_geometries(): + return self.__create_circuit_d2q9( + self.blocks, control_on_marker_state=False + ) + else: + circuit = self.lattice.circuit.copy() + for c, blocks in enumerate(self.blocks): + # Prepare the /ket{1} state in the marker register + qubits_to_invert = [ + q + self.lattice.marker_index()[0] + for q in get_qubits_to_invert(c, self.lattice.num_marker_qubits) + ] + + if qubits_to_invert: + circuit.x(qubits_to_invert) + + circuit.compose( + self.__create_circuit_d2q9( + blocks, control_on_marker_state=True + ), + inplace=True, + ) - raise LatticeException("AB reflection only currently supported in D2Q9") + print(f"Geometry {c} done") - def __create_circuit_d2q9(self): + if qubits_to_invert: + circuit.x(qubits_to_invert) + return circuit + + def __create_circuit_d2q9(self, blocks, control_on_marker_state: bool = False): + # Ignore accumulation and marker registers circuit = self.lattice.circuit.copy() # Mark populations inside the object - for block in self.blocks: - circuit.compose(self.set_inside_wall_ancilla_state(block), inplace=True) + for block in blocks: + circuit.compose( + self.set_inside_wall_ancilla_state( + block, control_on_marker_state=control_on_marker_state + ), + inplace=True, + ) circuit.compose( self.set_ancilla_of_point_state( flatten( - [[(p, None) for p in block.corners_inside] for block in self.blocks] + [[(p, None) for p in block.corners_inside] for block in blocks] ), ignore_velocity_data=True, + control_on_marker_state=control_on_marker_state, ), inplace=True, ) @@ -99,13 +145,18 @@ def __create_circuit_d2q9(self): circuit.compose(self.permute_and_stream(), inplace=True) # Reset the ancilla state of reflected populations - for block in self.blocks: - circuit.compose(self.reset_outside_wall_ancilla_state(block), inplace=True) + for block in blocks: + circuit.compose( + self.reset_outside_wall_ancilla_state( + block, control_on_marker_state=control_on_marker_state + ), + inplace=True, + ) # Re-reset near corner point ancillas point_data: List[Tuple[ReflectionPoint, List[int]]] = [] - for block in self.blocks: + for block in blocks: for dim in range(self.lattice.num_dims): for c, bounds in enumerate( product(*[[False, True]] * self.lattice.num_dims) @@ -132,13 +183,21 @@ def __create_circuit_d2q9(self): # Re-reset the ancilla state of the populations that # Shouldn't have been flipped in the previous step circuit.compose( - self.set_ancilla_of_point_state(point_data, ignore_velocity_data=False), + self.set_ancilla_of_point_state( + point_data, + ignore_velocity_data=False, + control_on_marker_state=control_on_marker_state, + ), inplace=True, ) + print(f"Depth at the end: {circuit.depth()}") + return circuit - def set_inside_wall_ancilla_state(self, block: Block) -> QuantumCircuit: + def set_inside_wall_ancilla_state( + self, block: Block, control_on_marker_state: bool = False + ) -> QuantumCircuit: """ Sets the state of the ancilla qubit for all the gridpoints lying inside the walls of the block. @@ -179,6 +238,9 @@ def set_inside_wall_ancilla_state(self, block: Block) -> QuantumCircuit: + self.lattice.ancillae_comparator_index() ) + if control_on_marker_state: + control_qubits.extend(self.lattice.marker_index()) + target_qubits = self.lattice.ancillae_obstacle_index(0) circuit.compose( @@ -196,9 +258,13 @@ def set_inside_wall_ancilla_state(self, block: Block) -> QuantumCircuit: circuit.compose(comparator_circuit, inplace=True) + print(f"Depth inside wall ancilla state: {circuit.depth()}") + return circuit - def reset_outside_wall_ancilla_state(self, block: Block) -> QuantumCircuit: + def reset_outside_wall_ancilla_state( + self, block: Block, control_on_marker_state: bool = False + ) -> QuantumCircuit: """ Resets the state of the obstacle ancilla qubit for all the gridpoints that are directly adjacent to the object, but in the fluid domain. @@ -258,6 +324,9 @@ def reset_outside_wall_ancilla_state(self, block: Block) -> QuantumCircuit: + self.lattice.velocity_index() # The reset step is additionally controlled on the velocity register ) + if control_on_marker_state: + control_qubits.extend(self.lattice.marker_index()) + target_qubits = self.lattice.ancillae_obstacle_index(0) circuit.compose( @@ -283,6 +352,9 @@ def reset_outside_wall_ancilla_state(self, block: Block) -> QuantumCircuit: ] ) + if control_on_marker_state: + control_qubits.extend(self.lattice.marker_index()) + target_qubits = self.lattice.ancillae_obstacle_index(0) circuit.compose( @@ -305,12 +377,15 @@ def reset_outside_wall_ancilla_state(self, block: Block) -> QuantumCircuit: circuit.compose(comparator_circuit, inplace=True) + print(f"Depth outside wall ancilla state: {circuit.depth()}") + return circuit def set_ancilla_of_point_state( self, points_data: List[Tuple[ReflectionPoint, List[int]]], ignore_velocity_data: bool, + control_on_marker_state: bool = False, ) -> QuantumCircuit: """ Sets the state of the obstacle ancilla qubit of a given gridpoint, conditioned on the velocity profile. @@ -367,6 +442,9 @@ def set_ancilla_of_point_state( ) # The reset step is additionally controlled on the velocity register ) + if control_on_marker_state: + control_qubits.extend(self.lattice.marker_index()) + target_qubits = self.lattice.ancillae_obstacle_index(0) circuit.compose( @@ -382,13 +460,18 @@ def set_ancilla_of_point_state( circuit.x(velocity_qubit_indices_to_invert) case ABEncodingType.OH: if ignore_velocity_data: + control_qubits = self.lattice.grid_index() + ( + self.lattice.marker_index() + if control_on_marker_state + else [] + ) circuit.compose( MCMTGate( XGate(), - len(self.lattice.grid_index()), + len(control_qubits), len(self.lattice.ancillae_obstacle_index(0)), ), - qubits=self.lattice.grid_index() + qubits=control_qubits + self.lattice.ancillae_obstacle_index(0), inplace=True, ) @@ -401,6 +484,9 @@ def set_ancilla_of_point_state( ) # Only one velocity control ) + if control_on_marker_state: + control_qubits.extend(self.lattice.marker_index()) + target_qubits = self.lattice.ancillae_obstacle_index(0) circuit.compose( @@ -419,6 +505,8 @@ def set_ancilla_of_point_state( if grid_qubit_indices_to_invert: circuit.x(grid_qubit_indices_to_invert) + print(f"Depth point ancilla state: {circuit.depth()}") + return circuit def permute_and_stream(self) -> QuantumCircuit: From 7f57200fdfaec43f888662ba003d56e34f5d1930 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Fri, 28 Nov 2025 13:43:48 +0100 Subject: [PATCH 06/78] Update lattices with multigeoemtry primitives --- qlbm/lattice/lattices/ab_lattice.py | 53 +++++++++++++++++++++++++++++ qlbm/lattice/lattices/base.py | 8 +++++ qlbm/lattice/lattices/ms_lattice.py | 15 ++++++++ 3 files changed, 76 insertions(+) diff --git a/qlbm/lattice/lattices/ab_lattice.py b/qlbm/lattice/lattices/ab_lattice.py index 04b5a5a..adca9b1 100644 --- a/qlbm/lattice/lattices/ab_lattice.py +++ b/qlbm/lattice/lattices/ab_lattice.py @@ -181,6 +181,46 @@ def __update_registers(self): self.circuit = QuantumCircuit(*self.registers) + def set_geometries(self, geometries): + """ + Updates the geometry setup of the lattice. + + For a given lattice (set number of gridpoints and velocity discretization), + set multiple geometry configurations to simulate simultaneously. + + .. plot:: + :include-source: + + from qlbm.lattice import ABLattice + + lattice = ABLattice( + { + "lattice": { + "dim": {"x": 16, "y": 16}, + "velocities": "D2Q9", + }, + }, + ) + + lattice.circuit.draw("mpl") + + Parameters + ---------- + geometries : Dict + A list of geometries to simulate on the same lattice. + """ + self.geometries = [self.parse_geometry_dict(g) for g in geometries] + if len(self.geometries) == 1: + # Remove this in the future... + self.shapes = self.geometries[0] + + self.num_marker_qubits = ( + int(ceil(log2(len(self.geometries)))) + if self.has_multiple_geometries() + else 0 + ) + self.__update_registers() + @override def grid_index(self, dim: int | None = None) -> List[int]: if dim is None: @@ -393,3 +433,16 @@ def accumulation_index(self) -> List[int]: @override def get_encoding(self) -> ABEncodingType: return ABEncodingType.AB + + @override + def get_base_circuit(self): + return QuantumCircuit( + *flatten( + [ + self.grid_registers, + self.velocity_registers, + self.ancilla_comparator_register, + self.ancilla_object_register, + ] + ), + ) diff --git a/qlbm/lattice/lattices/base.py b/qlbm/lattice/lattices/base.py index 7582bf9..a01d373 100644 --- a/qlbm/lattice/lattices/base.py +++ b/qlbm/lattice/lattices/base.py @@ -530,6 +530,9 @@ class AmplitudeLattice(Lattice, ABC): num_marker_qubits: int """The number of qubits used to identify geometries, if parallel lattices are being simulated.""" + geometries: List[Dict[str, List[Shape]]] + """The list of geometries, if multiple geometries are simulated in parallel on this lattice.""" + def __init__( self, lattice_data, @@ -670,3 +673,8 @@ def get_encoding(self) -> ABEncodingType: The encoding of this lattice. """ pass + + @abstractmethod + def get_base_circuit(self) -> QuantumCircuit: + """Get the base quantum circuit, without any multi-geometry or accumulation qubits.""" + pass diff --git a/qlbm/lattice/lattices/ms_lattice.py b/qlbm/lattice/lattices/ms_lattice.py index 914714d..c440e73 100644 --- a/qlbm/lattice/lattices/ms_lattice.py +++ b/qlbm/lattice/lattices/ms_lattice.py @@ -477,3 +477,18 @@ def has_multiple_geometries(self): @override def get_encoding(self): return ABEncodingType.MS + + @override + def get_base_circuit(self): + return QuantumCircuit( + *flatten( + [ + self.ancilla_velocity_register, + self.ancilla_object_register, + self.ancilla_comparator_register, + self.grid_registers, + self.velocity_registers, + self.velocity_dir_registers, + ] + ) + ) From 1820abe98647f70be1cb00dcc98f33ae789b8c98 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Fri, 28 Nov 2025 13:44:16 +0100 Subject: [PATCH 07/78] Add multigeometry support in initial conditions of Amplitude Based QLBM --- qlbm/components/ab/initial.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/qlbm/components/ab/initial.py b/qlbm/components/ab/initial.py index bf294bb..f9dbcc1 100644 --- a/qlbm/components/ab/initial.py +++ b/qlbm/components/ab/initial.py @@ -109,6 +109,9 @@ def create_circuit(self) -> QuantumCircuit: f"Encoding {self.lattice.get_encoding()} not supported." ) + if self.lattice.has_multiple_geometries(): + circuit.h(self.lattice.marker_index()) + return circuit def __oh_permutation(self) -> QuantumCircuit: From 8006f841b8262bf4d9def97ac76c61382dd0f417 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Fri, 28 Nov 2025 13:49:53 +0100 Subject: [PATCH 08/78] Add multigeometry support in ABQLM loop and result --- qlbm/components/ab/ab.py | 2 -- qlbm/infra/result/base.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/qlbm/components/ab/ab.py b/qlbm/components/ab/ab.py index a66b081..16f2b0c 100644 --- a/qlbm/components/ab/ab.py +++ b/qlbm/components/ab/ab.py @@ -11,7 +11,6 @@ from qlbm.lattice.geometry.shapes.block import Block from qlbm.lattice.lattices.ab_lattice import ABLattice from qlbm.tools.exceptions import LatticeException -from qlbm.tools.utils import flatten from .streaming import ABStreamingOperator @@ -86,7 +85,6 @@ def create_circuit(self): circuit.compose( ABReflectionOperator( self.lattice, - flatten(list(self.lattice.shapes.values())), # type: ignore logger=self.logger, ).circuit, inplace=True, diff --git a/qlbm/infra/result/base.py b/qlbm/infra/result/base.py index fce9140..2fb4a2b 100644 --- a/qlbm/infra/result/base.py +++ b/qlbm/infra/result/base.py @@ -63,7 +63,7 @@ def visualize_geometry(self): """ Creates ``stl`` files for each block in the lattice. - Output files are formatted as ``output_dir/paraview_dir/cube_.stl``. + Output files are formatted as ``output_dir/paraview_dir/shape_.stl``. The output is created through the :class:`.Shape`'s :meth:`.Shape.stl_mesh` method. """ if not self.lattice.has_multiple_geometries(): From 6952e19e103d48cf01335984c2584ab46dfe4d67 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Fri, 28 Nov 2025 13:59:08 +0100 Subject: [PATCH 09/78] Add citation --- docs/source/refs.bib | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/source/refs.bib b/docs/source/refs.bib index ab56d73..af332da 100644 --- a/docs/source/refs.bib +++ b/docs/source/refs.bib @@ -79,3 +79,10 @@ @article{lqlga1 year={2019}, publisher={MDPI} } + +@article{qsearch, + title={Quantum Search in Superposed Quantum Lattice Gas Automata and Lattice Boltzmann Systems}, + author={Georgescu, C{\u{a}}lin A and M{\"o}ller, Matthias}, + journal={arXiv preprint arXiv:2510.14062}, + year={2025} +} \ No newline at end of file From 8d507c5f0d59538ea1c09ebc75b5a4cd89193b4d Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Fri, 28 Nov 2025 14:51:35 +0100 Subject: [PATCH 10/78] Remove debug print statements --- qlbm/components/ab/initial.py | 2 ++ qlbm/components/ab/reflection.py | 11 ----------- qlbm/lattice/geometry/shapes/block.py | 1 - test/unit/lqlga/circuits/hamming_adder_test.py | 3 --- 4 files changed, 2 insertions(+), 15 deletions(-) diff --git a/qlbm/components/ab/initial.py b/qlbm/components/ab/initial.py index f9dbcc1..62aeef6 100644 --- a/qlbm/components/ab/initial.py +++ b/qlbm/components/ab/initial.py @@ -87,6 +87,8 @@ def create_circuit(self) -> QuantumCircuit: qubits=self.lattice.velocity_index(), inplace=True, ) + + circuit.h(self.lattice.grid_index(1)) case ABEncodingType.OH: nq = int(np.ceil(np.log2(self.lattice.num_velocity_qubits))) circuit.compose( diff --git a/qlbm/components/ab/reflection.py b/qlbm/components/ab/reflection.py index e161a52..dea1f6b 100644 --- a/qlbm/components/ab/reflection.py +++ b/qlbm/components/ab/reflection.py @@ -111,8 +111,6 @@ def create_circuit(self) -> QuantumCircuit: inplace=True, ) - print(f"Geometry {c} done") - if qubits_to_invert: circuit.x(qubits_to_invert) return circuit @@ -191,8 +189,6 @@ def __create_circuit_d2q9(self, blocks, control_on_marker_state: bool = False): inplace=True, ) - print(f"Depth at the end: {circuit.depth()}") - return circuit def set_inside_wall_ancilla_state( @@ -258,8 +254,6 @@ def set_inside_wall_ancilla_state( circuit.compose(comparator_circuit, inplace=True) - print(f"Depth inside wall ancilla state: {circuit.depth()}") - return circuit def reset_outside_wall_ancilla_state( @@ -376,9 +370,6 @@ def reset_outside_wall_ancilla_state( circuit.x(grid_qubit_indices_to_invert) circuit.compose(comparator_circuit, inplace=True) - - print(f"Depth outside wall ancilla state: {circuit.depth()}") - return circuit def set_ancilla_of_point_state( @@ -505,8 +496,6 @@ def set_ancilla_of_point_state( if grid_qubit_indices_to_invert: circuit.x(grid_qubit_indices_to_invert) - print(f"Depth point ancilla state: {circuit.depth()}") - return circuit def permute_and_stream(self) -> QuantumCircuit: diff --git a/qlbm/lattice/geometry/shapes/block.py b/qlbm/lattice/geometry/shapes/block.py index c3a0f62..aa4c586 100644 --- a/qlbm/lattice/geometry/shapes/block.py +++ b/qlbm/lattice/geometry/shapes/block.py @@ -873,7 +873,6 @@ def get_lbm_outside_corner_indices_to_reflect( @override def get_lqlga_reflection_data_d1q2(self): - print(self.num_grid_qubits[0]) return self.get_lqlga_reflection_data_1d_from_points( [tuple([self.bounds[0][0]])], 0, diff --git a/test/unit/lqlga/circuits/hamming_adder_test.py b/test/unit/lqlga/circuits/hamming_adder_test.py index 7eba82a..a34ae2a 100644 --- a/test/unit/lqlga/circuits/hamming_adder_test.py +++ b/test/unit/lqlga/circuits/hamming_adder_test.py @@ -55,7 +55,6 @@ def test_hamming_adder_2plus0(): counts = get_count_from_circuit(circuit) assert len(counts) == 1 - print(counts) assert all(int(s[:4], 2) == 2 for s in counts) @@ -72,7 +71,6 @@ def test_hamming_adder_2plus4(): counts = get_count_from_circuit(circuit) assert len(counts) == 1 - print(counts) assert all(int(s[:4], 2) == 6 for s in counts) @@ -100,6 +98,5 @@ def test_hamming_adder_superposition_x(): adder.measure_all() circuit.compose(adder, inplace=True) counts = get_count_from_circuit(circuit) - print(counts) assert len(counts) == 16 assert all(int(s[:4], 2) == (hamming_weight(s[4:]) + 4) for s in counts) From 62763bd5694cd78bb2df3e1b8956a83d20f1266d Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Wed, 10 Dec 2025 18:27:19 +0100 Subject: [PATCH 11/78] Refactor and parameterize incrementation utilities --- qlbm/components/ab/streaming.py | 2 +- qlbm/components/common/__init__.py | 4 + qlbm/components/common/adders.py | 275 +++++++++++++++++++++++++++ qlbm/components/common/primitives.py | 120 +++++++++++- qlbm/components/ms/__init__.py | 8 +- qlbm/components/ms/primitives.py | 76 +------- qlbm/components/ms/streaming.py | 142 +------------- 7 files changed, 406 insertions(+), 221 deletions(-) create mode 100644 qlbm/components/common/adders.py diff --git a/qlbm/components/ab/streaming.py b/qlbm/components/ab/streaming.py index 745606e..12935cc 100644 --- a/qlbm/components/ab/streaming.py +++ b/qlbm/components/ab/streaming.py @@ -10,7 +10,7 @@ from qlbm.components.ab.encodings import ABEncodingType from qlbm.components.base import LBMOperator -from qlbm.components.ms.streaming import PhaseShift +from qlbm.components.common.adders import PhaseShift from qlbm.lattice.lattices.base import AmplitudeLattice from qlbm.lattice.spacetime.properties_base import LatticeDiscretization from qlbm.tools.exceptions import LatticeException diff --git a/qlbm/components/common/__init__.py b/qlbm/components/common/__init__.py index 139a116..afabb00 100644 --- a/qlbm/components/common/__init__.py +++ b/qlbm/components/common/__init__.py @@ -1,5 +1,6 @@ """Common primitives used for multiple encodings.""" +from .adders import ParameterizedDraperAdder, ParameterizedPhaseShift, PhaseShift from .cbse_collision import EQCCollisionOperator, EQCPermutation, EQCRedistribution from .primitives import EmptyPrimitive, HammingWeightAdder @@ -9,4 +10,7 @@ "EQCPermutation", "EQCRedistribution", "HammingWeightAdder", + "ParameterizedDraperAdder", + "ParameterizedPhaseShift", + "PhaseShift", ] diff --git a/qlbm/components/common/adders.py b/qlbm/components/common/adders.py new file mode 100644 index 0000000..ac0e315 --- /dev/null +++ b/qlbm/components/common/adders.py @@ -0,0 +1,275 @@ +from logging import Logger, getLogger +from math import pi +from time import perf_counter_ns + +import numpy as np +from qiskit import QuantumCircuit +from qiskit.synthesis import synth_qft_full as QFT +from typing_extensions import override + +from qlbm.components.base import LBMPrimitive +from qlbm.tools import bit_value + + +class ParameterizedPhaseShift(LBMPrimitive): + r"""A primitive that applies the phase-shift as part of the :class:`.ParameterizedDraperAdder` used in :class:`.Comparator`\ s. + + The rotation applied is :math:`\pm \frac{\pi}{2^{n_q - 1 - j}}`, with :math:`j` the position of the qubit (indexed starting with 0). + Unlike the regular :class:`.PhaseShift`, the parameterized version additionally adds a phase relative to the number supplied. + For an in-depth mathematical explanation of the procedure, consult Sections 4 and 5.5 of :cite:t:`collisionless`. + + ========================= ====================================================================== + Attribute Summary + ========================= ====================================================================== + :attr:`num_qubits` The number of qubits to perform the phase shift for. + :attr:`positive` Whether the phase shift is applied to increment (T) + or decrement (F) the position of the particles. + Defaults to ``False``. + :attr:`num_to_add` The specific number to add as part of the Draper Adder. + :attr:`logger` The performance logger, by default ``getLogger("qlbm")``. + :attr:`num_ctrl_qubits` The number of qubits to control the PhaseShift. + ========================= ====================================================================== + + Example usage: + + .. plot:: + :include-source: + + from qlbm.components import ParameterizedPhaseShift + + # A phase shift of 5 qubits, adding the number 2 + ParameterizedPhaseShift(num_qubits=5, num_to_add=2, positive=True).draw("mpl") + + Including control qubits: + + .. plot:: + :include-source: + + from qlbm.components import ParameterizedPhaseShift + + # A phase shift of 5 qubits, controlled subtracting the number 1 + ParameterizedPhaseShift(num_qubits=5, num_to_add=1, positive=False, num_ctrl_qubits=3).draw("mpl") + """ + + num_qubits: int + """The number of qubits the phase shift is performed on.""" + + num_to_add: int + """The number to add to the basis states encoded in the qubits.""" + + positive: bool + """Whether the operation is an addition or a subtraction.""" + + num_ctrl_qubits: int + """Optional additional qubits to control the operation on. If any, the control qubits trail the target qubits.""" + + def __init__( + self, + num_qubits: int, + num_to_add: int, + positive: bool = False, + num_ctrl_qubits: int = 0, + logger: Logger = getLogger("qlbm"), + ) -> None: + super().__init__(logger) + + self.num_qubits = num_qubits + self.num_to_add = num_to_add + self.positive = positive + self.num_ctrl_qubits = num_ctrl_qubits + + self.logger.info(f"Creating circuit {str(self)}...") + circuit_creation_start_time = perf_counter_ns() + self.circuit = self.create_circuit() + self.logger.info( + f"Creating circuit {str(self)} took {perf_counter_ns() - circuit_creation_start_time} (ns)" + ) + + @override + def create_circuit(self) -> QuantumCircuit: + circuit = QuantumCircuit(self.num_qubits + self.num_ctrl_qubits) + angles = np.zeros(self.num_qubits) + + for qubit_index in range(self.num_qubits): + dig = bit_value(self.num_to_add, qubit_index) + for i in range(self.num_qubits - qubit_index): + # (2 * positive - 1) will flip the sign if positive is False + # This effectively inverts the circuit + angles[i] += ( + (2 * self.positive - 1) + * dig + * pi + / (2 ** (self.num_qubits - qubit_index - i - 1)) + ) + + for qubit_index in range(self.num_qubits): + if self.num_ctrl_qubits == 0: + circuit.p(angles[qubit_index], qubit_index) + else: + circuit.mcp( + angles[qubit_index], + list( + range(self.num_qubits, self.num_qubits + self.num_ctrl_qubits) + ), + qubit_index, + ) + + return circuit + + @override + def __str__(self) -> str: + return f"[Primitive ParameterizedPhaseShift of {self.num_qubits} qubits, num {self.num_to_add}, in direction {self.positive}, ctrl {self.num_ctrl_qubits}]" + + +class ParameterizedDraperAdder(LBMPrimitive): + r"""A QFT-based incrementer used to perform streaming in the algorithms based on amplitude encodings. + + Incrementation and decerementation are performed as rotations on grid qubits + that have been previously mapped to the Fourier basis. + This happens by nesting a :class:`.ParameterizedPhaseShift` primitive + between regular and inverse :math:`QFT`\ s. + + ========================= ====================================================================== + Attribute Summary + ========================= ====================================================================== + :attr:`num_qubits` Number of qubits of the circuit. + :attr:`num_to_add`. The number to add. + :attr:`positive` Whether to increment in in the positive (T) or negative (F) direction. + :attr:`num_ctrl_qubits` The number of qubits to control the PhaseShift. + :attr:`logger` The performance logger, by default getLogger("qlbm") + ========================= ====================================================================== + + Example usage: + + .. plot:: + :include-source: + + from qlbm.components import ParameterizedDraperAdder + + ParameterizedDraperAdder(4, 1, True).draw("mpl") + """ + + num_qubits: int + """The number of qubits the phase shift is performed on.""" + + num_to_add: int + """The number to add to the basis states encoded in the qubits.""" + + positive: bool + """Whether the operation is an addition or a subtraction.""" + + num_ctrl_qubits: int + """Optional additional qubits to control the operation on. If any, the control qubits trail the target qubits.""" + + def __init__( + self, + num_qubits: int, + num_to_add: int, + positive: bool, + num_ctrl_qubits: int = 0, + logger: Logger = getLogger("qlbm"), + ) -> None: + super().__init__(logger) + self.num_qubits = num_qubits + self.num_to_add = num_to_add + self.positive = positive + self.num_ctrl_qubits = num_ctrl_qubits + + self.logger.info(f"Creating circuit {str(self)}...") + circuit_creation_start_time = perf_counter_ns() + self.circuit = self.create_circuit() + self.logger.info( + f"Creating circuit {str(self)} took {perf_counter_ns() - circuit_creation_start_time} (ns)" + ) + + @override + def create_circuit(self) -> QuantumCircuit: + circuit = QuantumCircuit(self.num_qubits + self.num_ctrl_qubits) + + circuit.compose( + QFT(self.num_qubits), inplace=True, qubits=list(range(self.num_qubits)) + ) + circuit.compose( + ParameterizedPhaseShift( + self.num_qubits, + self.num_to_add, + self.positive, + num_ctrl_qubits=self.num_ctrl_qubits, + logger=self.logger, + ).circuit, + inplace=True, + ) + circuit.compose( + QFT(self.num_qubits, inverse=True), + inplace=True, + qubits=list(range(self.num_qubits)), + ) + + return circuit + + @override + def __str__(self) -> str: + return f"[Primitive SimpleAdder on {self.num_qubits} qubits, on velocity {self.num_to_add}, in direction {self.positive}]" + + +class PhaseShift(LBMPrimitive): + r""" + A primitive that applies the phase-shift as part of the :class:`.ControlledIncrementer` used in the :class:`.MSStreamingOperator`. + + The rotation applied is :math:`\pm\frac{\pi}{2^{n_q - 1 - j}}`, with :math:`j` the position of the qubit (indexed starting with 0). + For an in-depth mathematical explanation of the procedure, consult Section 4 of :cite:t:`collisionless`. + + ========================= ====================================================================== + Attribute Summary + ========================= ====================================================================== + :attr:`num_qubits` The number of qubits to perform the phase shift for. + :attr:`positive` Whether the phase shift is applied to increment (T) + or decrement (F) the position of the particles. + Defaults to ``False``. + :attr:`logger` The performance logger, by default ``getLogger("qlbm")``. + ========================= ====================================================================== + + Example usage: + + .. plot:: + :include-source: + + from qlbm.components.ms import PhaseShift + + # A phase shift of 5 qubits + PhaseShift(num_qubits=5, positive=False).draw("mpl") + """ + + def __init__( + self, + num_qubits: int, + positive: bool = False, + logger: Logger = getLogger("qlbm"), + ) -> None: + super().__init__(logger) + + self.num_qubits = num_qubits + self.positive = positive + + self.logger.info(f"Creating circuit {str(self)}...") + circuit_creation_start_time = perf_counter_ns() + self.circuit = self.create_circuit() + self.logger.info( + f"Creating circuit {str(self)} took {perf_counter_ns() - circuit_creation_start_time} (ns)" + ) + + @override + def create_circuit(self) -> QuantumCircuit: + circuit = QuantumCircuit(self.num_qubits) + + for c, qubit_index in enumerate(range(self.num_qubits)): + # (2 * positive - 1) will flip the sign if positive is False + # This effectively inverts the circuit + phase = (2 * self.positive - 1) * pi / (2 ** (self.num_qubits - 1 - c)) + circuit.p(phase, qubit_index) + + return circuit + + @override + def __str__(self) -> str: + return f"[Primitive PhaseShift of {self.num_qubits} qubits, in direction {self.positive}]" diff --git a/qlbm/components/common/primitives.py b/qlbm/components/common/primitives.py index cb2a4cf..b34ddd5 100644 --- a/qlbm/components/common/primitives.py +++ b/qlbm/components/common/primitives.py @@ -2,7 +2,7 @@ from logging import Logger, getLogger from time import perf_counter_ns -from typing import List, Tuple +from typing import List, Tuple, cast import numpy as np from numpy import pi @@ -13,7 +13,10 @@ from typing_extensions import override from qlbm.components.base import LBMPrimitive +from qlbm.components.common.adders import ParameterizedDraperAdder +from qlbm.components.ms.streaming import ControlledIncrementer from qlbm.lattice import Lattice +from qlbm.tools.utils import get_qubits_to_invert class EmptyPrimitive(LBMPrimitive): @@ -252,3 +255,118 @@ def create_circuit(self): @override def __str__(self): return f"[Primitive TuncatedQFT({self.num_qubits}, {self.dft_size})]" + + +class AdditionConversion(LBMPrimitive): + num_qubits: int + """The number of qubits the states are encoded in.""" + + state_from: int + """The starting state to convert.""" + + state_to: int + """The state to convert to.""" + + def __init__( + self, + num_qubits: int, + state_from: int, + state_to: int, + logger: Logger = getLogger("qlbm"), + ): + super().__init__(logger) + self.num_qubits = num_qubits + self.state_from = state_from + self.state_to = state_to + + self.logger.info(f"Creating circuit {str(self)}...") + circuit_creation_start_time = perf_counter_ns() + self.circuit = self.create_circuit() + self.logger.info( + f"Creating circuit {str(self)} took {perf_counter_ns() - circuit_creation_start_time} (ns)" + ) + + @override + def create_circuit(self): + circuit = QuantumCircuit(self.num_qubits + 1) + + state_setter_circ = StateSetter( + self.num_qubits, self.state_from, self.logger + ).circuit + + circuit.compose( + state_setter_circ, qubits=list(range(self.num_qubits)), inplace=True + ) + circuit.mcx(list(range(self.num_qubits)), self.num_qubits) + circuit.compose( + state_setter_circ, qubits=list(range(self.num_qubits)), inplace=True + ) + + circuit.compose( + ParameterizedDraperAdder( + self.num_qubits, + abs(self.state_to - self.state_from), + self.state_to > self.state_from, + 1, + self.logger, + ).circuit, + inplace=True, + ) + + state_setter_circ = StateSetter( + self.num_qubits, self.state_to, self.logger + ).circuit + + circuit.compose( + state_setter_circ, qubits=list(range(self.num_qubits)), inplace=True + ) + circuit.mcx(list(range(self.num_qubits)), self.num_qubits) + circuit.compose( + state_setter_circ, qubits=list(range(self.num_qubits)), inplace=True + ) + + return circuit + + @override + def __str__(self): + return f"[Primitive AdditionConversion({self.num_qubits}, {self.state_from}, {self.state_to})]" + + +class StateSetter(LBMPrimitive): + num_qubits: int + """The number of qubits the state is encoded in.""" + + state_to_set: int + """The state to convert to :math:`\ket{1}^{\otimes n}`""" + + def __init__( + self, + num_qubits: int, + state_to_set: int, + logger: Logger = getLogger("qlbm"), + ): + super().__init__(logger) + self.num_qubits = num_qubits + self.state_to_set = state_to_set + + self.logger.info(f"Creating circuit {str(self)}...") + circuit_creation_start_time = perf_counter_ns() + self.circuit = self.create_circuit() + self.logger.info( + f"Creating circuit {str(self)} took {perf_counter_ns() - circuit_creation_start_time} (ns)" + ) + + @override + def create_circuit(self): + circuit = QuantumCircuit(self.num_qubits) + + qs = get_qubits_to_invert(self.state_to_set, self.num_qubits) + + if qs: + circuit.x(qs) + + return circuit + + @override + def __str__(self): + return f"[Primitive StateSetter({self.num_qubits}, {self.state_to_set}]" diff --git a/qlbm/components/ms/__init__.py b/qlbm/components/ms/__init__.py index 8652182..a7e3af3 100644 --- a/qlbm/components/ms/__init__.py +++ b/qlbm/components/ms/__init__.py @@ -1,5 +1,6 @@ """Modular qlbm quantum circuit components for the MSQLBM algorithm :cite:p:`collisionless`.""" +from ..common.adders import PhaseShift, ParameterizedDraperAdder, ParameterizedPhaseShift from .bounceback_reflection import ( BounceBackReflectionOperator, BounceBackWallComparator, @@ -12,21 +13,18 @@ GridMeasurement, MSInitialConditions, MSInitialConditions3DSlim, - SpeedSensitiveAdder, ) from .specular_reflection import SpecularReflectionOperator, SpecularWallComparator from .streaming import ( ControlledIncrementer, MSStreamingOperator, - PhaseShift, - SpeedSensitivePhaseShift, StreamingAncillaPreparation, ) __all__ = [ "ComparatorMode", "Comparator", - "SpeedSensitiveAdder", + "ParameterizedDraperAdder", "StreamingAncillaPreparation", "ControlledIncrementer", "GridMeasurement", @@ -34,7 +32,7 @@ "MSInitialConditions", "MSInitialConditions3DSlim", "PhaseShift", - "SpeedSensitivePhaseShift", + "ParameterizedPhaseShift", "MSStreamingOperator", "SpecularReflectionOperator", "SpecularWallComparator", diff --git a/qlbm/components/ms/primitives.py b/qlbm/components/ms/primitives.py index 07a3491..06cef58 100644 --- a/qlbm/components/ms/primitives.py +++ b/qlbm/components/ms/primitives.py @@ -6,11 +6,10 @@ from typing import List from qiskit import ClassicalRegister, QuantumCircuit -from qiskit.synthesis import synth_qft_full as QFT from typing_extensions import override from qlbm.components.base import LBMPrimitive -from qlbm.components.ms.streaming import SpeedSensitivePhaseShift +from qlbm.components.common.adders import ParameterizedDraperAdder from qlbm.lattice import MSLattice from qlbm.lattice.geometry.encodings.ms import ReflectionResetEdge from qlbm.tools import flatten @@ -285,75 +284,6 @@ class ComparatorMode(Enum): GE = (4,) -class SpeedSensitiveAdder(LBMPrimitive): - r"""A QFT-based incrementer used to perform streaming in the algorithms based on amplitude encodings. - - Incrementation and decerementation are performed as rotations on grid qubits - that have been previously mapped to the Fourier basis. - This happens by nesting a :class:`.SpeedSensitivePhaseShift` primitive - between regular and inverse :math:`QFT`\ s. - - ========================= ====================================================================== - Attribute Summary - ========================= ====================================================================== - :attr:`num_qubits` Number of qubits of the circuit. - :attr:`speed` The index of the speed to increment. - :attr:`positive` Whether to increment the particles traveling at this speed in the positive (T) or negative (F) direction. - :attr:`logger` The performance logger, by default getLogger("qlbm") - ========================= ====================================================================== - - Example usage: - - .. plot:: - :include-source: - - from qlbm.components.ms import SpeedSensitiveAdder - - SpeedSensitiveAdder(4, 1, True).draw("mpl") - """ - - def __init__( - self, - num_qubits: int, - speed: int, - positive: bool, - logger: Logger = getLogger("qlbm"), - ) -> None: - super().__init__(logger) - self.num_qubits = num_qubits - self.speed = speed - self.positive = positive - - self.logger.info(f"Creating circuit {str(self)}...") - circuit_creation_start_time = perf_counter_ns() - self.circuit = self.create_circuit() - self.logger.info( - f"Creating circuit {str(self)} took {perf_counter_ns() - circuit_creation_start_time} (ns)" - ) - - @override - def create_circuit(self) -> QuantumCircuit: - circuit = QuantumCircuit(self.num_qubits) - - circuit.compose(QFT(self.num_qubits), inplace=True) - circuit.compose( - SpeedSensitivePhaseShift( - self.num_qubits, - self.speed, - self.positive, - logger=self.logger, - ).circuit, - inplace=True, - ) - circuit.compose(QFT(self.num_qubits, inverse=True), inplace=True) - - return circuit - - @override - def __str__(self) -> str: - return f"[Primitive SimpleAdder on {self.num_qubits} qubits, on velocity {self.speed}, in direction {self.positive}]" - - class Comparator(LBMPrimitive): """ Quantum comparator primitive that compares two a quantum state of ``num_qubits`` qubits and an integer ``num_to_compare`` with respect to a :class:`.ComparatorMode`. @@ -412,13 +342,13 @@ def __create_circuit( match mode: case ComparatorMode.LT: circuit.compose( - SpeedSensitiveAdder( + ParameterizedDraperAdder( num_qubits, num_to_compare, positive=False, logger=self.logger ).circuit, inplace=True, ) circuit.compose( - SpeedSensitiveAdder( + ParameterizedDraperAdder( num_qubits - 1, num_to_compare, positive=True, diff --git a/qlbm/components/ms/streaming.py b/qlbm/components/ms/streaming.py index 984a87c..8d99daa 100644 --- a/qlbm/components/ms/streaming.py +++ b/qlbm/components/ms/streaming.py @@ -1,17 +1,16 @@ """Quantum circuits for the implementation of QFT-based streaming as described in :cite:t:`collisionless`.""" from logging import Logger, getLogger -from math import pi from time import perf_counter_ns from typing import List -import numpy as np from qiskit import QuantumCircuit from qiskit.circuit.library import MCXGate from qiskit.synthesis import synth_qft_full as QFT from typing_extensions import override from qlbm.components.base import LBMPrimitive, MSOperator +from qlbm.components.common.adders import PhaseShift from qlbm.lattice import MSLattice from qlbm.tools import CircuitException, bit_value @@ -326,142 +325,3 @@ def __str__(self) -> str: return ( f"[Operator StreamingOperator for velocities {self.velocities_to_stream}]" ) - - -class PhaseShift(LBMPrimitive): - r""" - A primitive that applies the phase-shift as part of the :class:`.ControlledIncrementer` used in the :class:`.MSStreamingOperator`. - - The rotation applied is :math:`\pm\frac{\pi}{2^{n_q - 1 - j}}`, with :math:`j` the position of the qubit (indexed starting with 0). - For an in-depth mathematical explanation of the procedure, consult Section 4 of :cite:t:`collisionless`. - - ========================= ====================================================================== - Attribute Summary - ========================= ====================================================================== - :attr:`num_qubits` The number of qubits to perform the phase shift for. - :attr:`positive` Whether the phase shift is applied to increment (T) - or decrement (F) the position of the particles. - Defaults to ``False``. - :attr:`logger` The performance logger, by default ``getLogger("qlbm")``. - ========================= ====================================================================== - - Example usage: - - .. plot:: - :include-source: - - from qlbm.components.ms import PhaseShift - - # A phase shift of 5 qubits - PhaseShift(num_qubits=5, positive=False).draw("mpl") - """ - - def __init__( - self, - num_qubits: int, - positive: bool = False, - logger: Logger = getLogger("qlbm"), - ) -> None: - super().__init__(logger) - - self.num_qubits = num_qubits - self.positive = positive - - self.logger.info(f"Creating circuit {str(self)}...") - circuit_creation_start_time = perf_counter_ns() - self.circuit = self.create_circuit() - self.logger.info( - f"Creating circuit {str(self)} took {perf_counter_ns() - circuit_creation_start_time} (ns)" - ) - - @override - def create_circuit(self) -> QuantumCircuit: - circuit = QuantumCircuit(self.num_qubits) - - for c, qubit_index in enumerate(range(self.num_qubits)): - # (2 * positive - 1) will flip the sign if positive is False - # This effectively inverts the circuit - phase = (2 * self.positive - 1) * pi / (2 ** (self.num_qubits - 1 - c)) - circuit.p(phase, qubit_index) - - return circuit - - @override - def __str__(self) -> str: - return f"[Primitive PhaseShift of {self.num_qubits} qubits, in direction {self.positive}]" - - -class SpeedSensitivePhaseShift(LBMPrimitive): - r"""A primitive that applies the phase-shift as part of the :class:`.SpeedSensitiveAdder` used in :class:`.Comparator`\ s. - - The rotation applied is :math:`\pm \frac{\pi}{2^{n_q - 1 - j}}`, with :math:`j` the position of the qubit (indexed starting with 0). - Unlike the regular :class:`.PhaseShift`, the speed-sensitive version additionally depends on a specific speed index. - For an in-depth mathematical explanation of the procedure, consult Sections 4 and 5.5 of :cite:t:`collisionless`. - - ========================= ====================================================================== - Attribute Summary - ========================= ====================================================================== - :attr:`num_qubits` The number of qubits to perform the phase shift for. - :attr:`positive` Whether the phase shift is applied to increment (T) - or decrement (F) the position of the particles. - Defaults to ``False``. - :attr:`speed` The specific speed index to perform the phase shift for. - :attr:`logger` The performance logger, by default ``getLogger("qlbm")``. - ========================= ====================================================================== - - Example usage: - - .. plot:: - :include-source: - - from qlbm.components.ms import SpeedSensitivePhaseShift - - # A phase shift of 5 qubits, controlled on speed index 2 - SpeedSensitivePhaseShift(num_qubits=5, speed=2, positive=True).draw("mpl") - """ - - def __init__( - self, - num_qubits: int, - speed: int, - positive: bool = False, - logger: Logger = getLogger("qlbm"), - ) -> None: - super().__init__(logger) - - self.num_qubits = num_qubits - self.speed = speed - self.positive = positive - - self.logger.info(f"Creating circuit {str(self)}...") - circuit_creation_start_time = perf_counter_ns() - self.circuit = self.create_circuit() - self.logger.info( - f"Creating circuit {str(self)} took {perf_counter_ns() - circuit_creation_start_time} (ns)" - ) - - @override - def create_circuit(self) -> QuantumCircuit: - circuit = QuantumCircuit(self.num_qubits) - angles = np.zeros(self.num_qubits) - - for qubit_index in range(self.num_qubits): - dig = bit_value(self.speed, qubit_index) - for i in range(self.num_qubits - qubit_index): - # (2 * positive - 1) will flip the sign if positive is False - # This effectively inverts the circuit - angles[i] += ( - (2 * self.positive - 1) - * dig - * pi - / (2 ** (self.num_qubits - qubit_index - i - 1)) - ) - - for qubit_index in range(self.num_qubits): - circuit.p(angles[qubit_index], qubit_index) - - return circuit - - @override - def __str__(self) -> str: - return f"[Primitive SpeedSensitivePhaseShift of {self.num_qubits} qubits, speed {self.speed}, in direction {self.positive}]" From 20c7283f52d4b7c05edad77e568a071af420b563 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Wed, 10 Dec 2025 18:28:21 +0100 Subject: [PATCH 12/78] Add Binary to OH permutation class for ABQLBM --- qlbm/components/ab/utils.py | 67 +++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 qlbm/components/ab/utils.py diff --git a/qlbm/components/ab/utils.py b/qlbm/components/ab/utils.py new file mode 100644 index 0000000..112e52d --- /dev/null +++ b/qlbm/components/ab/utils.py @@ -0,0 +1,67 @@ +from logging import Logger, getLogger +from time import perf_counter_ns + +import numpy as np +from qiskit import QuantumCircuit +from qiskit.quantum_info import Operator +from typing_extensions import override + +from qlbm.components.base import LBMPrimitive +from qlbm.lattice.lattices.ab_lattice import ABLattice + + +class BinaryToOHPermutation(LBMPrimitive): + lattice: ABLattice + + def __init__( + self, + lattice: ABLattice, + logger: Logger = getLogger("qlbm"), + ) -> None: + super().__init__(logger) + self.lattice = lattice + + self.logger.info(f"Creating circuit {str(self)}...") + circuit_creation_start_time = perf_counter_ns() + self.circuit = self.create_circuit() + self.logger.info( + f"Creating circuit {str(self)} took {perf_counter_ns() - circuit_creation_start_time} (ns)" + ) + + @override + def create_circuit(self) -> QuantumCircuit: + circuit = QuantumCircuit(*self.lattice.registers) + + n = self.lattice.num_velocity_qubits + dim = 2**n + + perm = [-1] * dim + used_rows = set() + + for j in range(n): + row = 1 << j # 2^j + perm[j] = row + used_rows.add(row) + + # Fill in the rest of the permutation arbitrarily but bijectively. + remaining_rows = [r for r in range(dim) if r not in used_rows] + k = 0 + for col in range(n, dim): + perm[col] = remaining_rows[k] + k += 1 + + U = np.zeros((dim, dim), dtype=complex) + for col in range(dim): + row = perm[col] + U[row, col] = 1.0 + + op = Operator(U) + + circuit = QuantumCircuit(n) + circuit.unitary(op, range(n), label="binary_to_onehot") + + return circuit + + @override + def __str__(self) -> str: + return f"[Primitive BinaryOHPermutation with lattice {self.lattice}]" \ No newline at end of file From bbc8affa14928183c83ce3d36ae76a8aa7b674a7 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Wed, 10 Dec 2025 18:29:09 +0100 Subject: [PATCH 13/78] Add arbitrary uniform discrete velocity initial conditions for ABQLBM --- qlbm/components/__init__.py | 9 +- qlbm/components/ab/initial.py | 173 ++++++++++++++++++++++++---------- 2 files changed, 129 insertions(+), 53 deletions(-) diff --git a/qlbm/components/__init__.py b/qlbm/components/__init__.py index f969bbc..ac2003f 100644 --- a/qlbm/components/__init__.py +++ b/qlbm/components/__init__.py @@ -23,6 +23,7 @@ EQCRedistribution, HammingWeightAdder, ) +from .common.adders import ParameterizedDraperAdder, ParameterizedPhaseShift, PhaseShift from .cqlbm import CQLBM from .lqlga import ( LQLGA, @@ -41,11 +42,9 @@ MSStreamingOperator, SpecularReflectionOperator, ) -from .ms.primitives import Comparator, ComparatorMode, SpeedSensitiveAdder +from .ms.primitives import Comparator, ComparatorMode from .ms.streaming import ( ControlledIncrementer, - PhaseShift, - SpeedSensitivePhaseShift, StreamingAncillaPreparation, ) @@ -59,9 +58,9 @@ "LBMAlgorithm", "ComparatorMode", "Comparator", - "SpeedSensitiveAdder", + "ParameterizedDraperAdder", "PhaseShift", - "SpeedSensitivePhaseShift", + "ParameterizedPhaseShift", "EmptyPrimitive", "StreamingAncillaPreparation", "ControlledIncrementer", diff --git a/qlbm/components/ab/initial.py b/qlbm/components/ab/initial.py index 62aeef6..a10f81a 100644 --- a/qlbm/components/ab/initial.py +++ b/qlbm/components/ab/initial.py @@ -2,17 +2,19 @@ from logging import Logger, getLogger from time import perf_counter_ns +from typing import List, Tuple import numpy as np from qiskit import QuantumCircuit -from qiskit.quantum_info import Operator from typing_extensions import override from qlbm.components.ab.encodings import ABEncodingType +from qlbm.components.ab.utils import BinaryToOHPermutation from qlbm.components.base import LBMPrimitive -from qlbm.components.common.primitives import TruncatedQFT +from qlbm.components.common.primitives import AdditionConversion, TruncatedQFT from qlbm.lattice.lattices.ab_lattice import ABLattice from qlbm.tools.exceptions import LatticeException +from qlbm.tools.utils import dimension_letter class ABInitialConditions(LBMPrimitive): @@ -57,6 +59,8 @@ class ABInitialConditions(LBMPrimitive): ABInitialConditions(lattice).circuit.decompose(reps=2).draw("mpl") """ + lattice: ABLattice + def __init__( self, lattice: ABLattice, @@ -76,33 +80,23 @@ def __init__( def create_circuit(self) -> QuantumCircuit: circuit = QuantumCircuit(*self.lattice.registers) + nq = int(np.ceil(np.log2(self.lattice.num_velocities_per_point))) + circuit.compose( + TruncatedQFT( + nq, + self.lattice.num_velocity_qubits, + self.logger, + ).circuit, + qubits=self.lattice.velocity_index()[:nq], + inplace=True, + ) + match self.lattice.get_encoding(): case ABEncodingType.AB: - circuit.compose( - TruncatedQFT( - self.lattice.num_velocity_qubits, - self.lattice.num_velocities_per_point, - self.logger, - ).circuit, - qubits=self.lattice.velocity_index(), - inplace=True, - ) - circuit.h(self.lattice.grid_index(1)) case ABEncodingType.OH: - nq = int(np.ceil(np.log2(self.lattice.num_velocity_qubits))) - circuit.compose( - TruncatedQFT( - nq, - self.lattice.num_velocity_qubits, - self.logger, - ).circuit, - qubits=self.lattice.velocity_index()[:nq], - inplace=True, - ) - circuit.compose( - self.__oh_permutation(), + BinaryToOHPermutation(self.lattice, self.logger).circuit, qubits=self.lattice.velocity_index(), inplace=True, ) @@ -116,39 +110,122 @@ def create_circuit(self) -> QuantumCircuit: return circuit - def __oh_permutation(self) -> QuantumCircuit: - circuit = QuantumCircuit(*self.lattice.registers) + @override + def __str__(self) -> str: + return f"[Primitive ABEInitialConditions with lattice {self.lattice}]" - n = self.lattice.num_velocity_qubits - dim = 2**n - perm = [-1] * dim - used_rows = set() +class DiscreteUniformVelocityABInitialConditions(LBMPrimitive): + velocity_indices: List[int] - for j in range(9): - row = 1 << j # 2^j - perm[j] = row - used_rows.add(row) + grid_qubits_to_superpose: Tuple[List[int], ...] - # Fill in the rest of the permutation arbitrarily but bijectively. - remaining_rows = [r for r in range(dim) if r not in used_rows] - k = 0 - for col in range(9, dim): - perm[col] = remaining_rows[k] - k += 1 + lattice: ABLattice - U = np.zeros((dim, dim), dtype=complex) - for col in range(dim): - row = perm[col] - U[row, col] = 1.0 + def __init__( + self, + lattice: ABLattice, + velocity_indices: List[int], + grid_qubits_to_superpose: Tuple[List[int], ...], + logger: Logger = getLogger("qlbm"), + ) -> None: + super().__init__(logger) - op = Operator(U) + self.lattice = lattice + + if any( + map( + lambda x: x + not in list(range(0, self.lattice.num_velocities_per_point)), + velocity_indices, + ) + ): + raise LatticeException( + f"Velocity indices should be in the interval 0..{self.lattice.num_velocities_per_point}" + ) + + if len(grid_qubits_to_superpose) != self.lattice.num_dims: + raise LatticeException( + f"Lattice has {self.lattice.num_dims} dimensions, but provided grid qubit information has {len(grid_qubits_to_superpose)} entries." + ) + + for dim in range(self.lattice.num_dims): + if any( + map( + lambda x: x + not in list(range(self.lattice.num_gridpoints[dim].bit_length())), + grid_qubits_to_superpose[dim], + ), + ): + raise LatticeException( + f"Grid qubit specification in dimension {dimension_letter(dim)} out of range." + ) + + self.velocity_indices = sorted(velocity_indices) + self.grid_qubits_to_superpose = grid_qubits_to_superpose - circuit = QuantumCircuit(n) - circuit.unitary(op, range(n), label="binary_to_onehot") + self.logger.info(f"Creating circuit {str(self)}...") + circuit_creation_start_time = perf_counter_ns() + self.circuit = self.create_circuit() + self.logger.info( + f"Creating circuit {str(self)} took {perf_counter_ns() - circuit_creation_start_time} (ns)" + ) + + @override + def create_circuit(self) -> QuantumCircuit: + circuit = QuantumCircuit(*self.lattice.registers) + + nq = int(np.ceil(np.log2(len(self.velocity_indices)))) + + circuit.compose( + TruncatedQFT( + nq, + len(self.velocity_indices), + self.logger, + ).circuit, + qubits=self.lattice.velocity_index()[:nq], + inplace=True, + ) + + states_from = list(range(len(self.velocity_indices))) + states_to = self.velocity_indices.copy() + + # Remove indices that are already in place + for v in self.velocity_indices: + if v < len(self.velocity_indices): + states_from.remove(v) + states_to.remove(v) + + for v_from, v_to in zip(states_from, states_to): + circuit.compose( + AdditionConversion( + self.lattice.num_velocity_qubits, v_from, v_to, self.logger + ).circuit, + qubits=self.lattice.velocity_index()[ + : self.lattice.num_velocities_per_point + ] # Additional guard necessary of OH + + self.lattice.ancillae_obstacle_index(0), + inplace=True, + ) + + if self.lattice.get_encoding() == ABEncodingType.OH: + circuit.compose( + BinaryToOHPermutation(self.lattice, self.logger).circuit, + qubits=self.lattice.velocity_index(), + inplace=True, + ) + + for dim in range(self.lattice.num_dims): + if self.grid_qubits_to_superpose[dim]: + circuit.h( + [ + self.lattice.grid_index(dim)[0] + q + for q in self.grid_qubits_to_superpose[dim] + ] + ) return circuit @override def __str__(self) -> str: - return f"[Primitive ABEInitialConditions with lattice {self.lattice}]" + return f"[Primitive DiscreteUniformVelocityABInitialConditions with lattice {self.lattice}, v={self.velocity_indices}, g={self.grid_qubits_to_superpose}]" From faa6542bf5bd1fddb455524f0ba872acd374674b Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Wed, 10 Dec 2025 18:29:39 +0100 Subject: [PATCH 14/78] Add AB initial conditions tests --- test/unit/ab/ab_initial_codnitions_test.py | 76 ++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 test/unit/ab/ab_initial_codnitions_test.py diff --git a/test/unit/ab/ab_initial_codnitions_test.py b/test/unit/ab/ab_initial_codnitions_test.py new file mode 100644 index 0000000..43007c7 --- /dev/null +++ b/test/unit/ab/ab_initial_codnitions_test.py @@ -0,0 +1,76 @@ +from itertools import product + +import pytest +from qiskit import ClassicalRegister, QuantumCircuit, transpile +from qiskit_aer import AerSimulator + +from qlbm.components.ab.initial import DiscreteUniformVelocityABInitialConditions +from qlbm.tools.utils import bit_value + + +@pytest.mark.parametrize( + "velocities,lattice_fixture", + list( + product( + [[0], [0, 1], [6, 7, 8], [4, 5, 0, 7], list(range(9))], + ["ab_lattice_d2q9_8x8"], + ) + ), +) +def test_initial_ab_no_gird_superposition(velocities, lattice_fixture, request): + lattice = request.getfixturevalue(lattice_fixture) + sim = AerSimulator() + + qc = lattice.circuit.copy() + qc.add_register(ClassicalRegister(4)) + qc.compose( + DiscreteUniformVelocityABInitialConditions( + lattice, velocities, ([], []) + ).circuit, + inplace=True, + ) + + qc.measure(lattice.velocity_index(), list(range(4))) + tqc = transpile(qc, sim, optimization_level=0) + + counts = sim.run(tqc, shots=256).result().get_counts() + + output_velocities = list(set([int(c, 2) for c in counts.keys()])) + + assert sorted(output_velocities) == sorted(velocities), ( + f"Expected output velocities to be {velocities}, got {output_velocities}" + ) + + +@pytest.mark.parametrize( + "velocities,lattice_fixture", + list( + product( + [[0], [0, 1], [6, 7, 8], [4, 5, 0, 7], list(range(9))], + ["oh_lattice_d2q9_8x8"], + ) + ), +) +def test_initial_oh_no_gird_superposition(velocities, lattice_fixture, request): + lattice = request.getfixturevalue(lattice_fixture) + sim = AerSimulator() + + qc = lattice.circuit.copy() + qc.add_register(ClassicalRegister(9)) + qc.compose( + DiscreteUniformVelocityABInitialConditions( + lattice, velocities, ([], []) + ).circuit, + inplace=True, + ) + + qc.measure(lattice.velocity_index(), list(range(9))) + tqc = transpile(qc, sim, optimization_level=0) + + counts = sim.run(tqc, shots=256).result().get_counts() + + output_velocities = list(set([int(c, 2) for c in counts.keys()])) + + assert sorted(output_velocities) == sorted([2**v for v in velocities]), ( + f"Expected output velocities to be {velocities}, got {output_velocities}" + ) From e5f89b01cceeede3297ebc63aeed20311e072cf0 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Thu, 11 Dec 2025 14:33:21 +0100 Subject: [PATCH 15/78] Add conversion addition test --- test/unit/ab/addition_conversion_test.py | 33 ++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 test/unit/ab/addition_conversion_test.py diff --git a/test/unit/ab/addition_conversion_test.py b/test/unit/ab/addition_conversion_test.py new file mode 100644 index 0000000..72fb0c0 --- /dev/null +++ b/test/unit/ab/addition_conversion_test.py @@ -0,0 +1,33 @@ +from itertools import product + +import pytest +from qiskit import QuantumCircuit, transpile +from qiskit_aer import AerSimulator + +from qlbm.components.common.primitives import AdditionConversion +from qlbm.tools.utils import bit_value + + +@pytest.mark.parametrize( + "nq,state_in,state_out", list(product([4], [1, 4, 7, 11, 14], [0, 2, 8, 9, 12])) +) +def test_addition_conversion(nq, state_in, state_out): + sim = AerSimulator() + + qc = QuantumCircuit(nq + 1) + for q in range(nq): + if bit_value(state_in, q): + qc.x(q) + + qc.compose( + AdditionConversion(nq, state_in, state_out).circuit, + inplace=True, + ) + qc.measure_all() + tqc = transpile(qc, sim, optimization_level=0) + + counts = sim.run(tqc, shots=128).result().get_counts() + + assert all(int(c, 2) == state_out for c in counts.keys()), ( + f"{state_in} handled incorrectly. Expected {state_out}, got {counts}." + ) From ea158f608ee2cf3760b0da8ccf7ae3304ab5d421 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Thu, 11 Dec 2025 14:34:01 +0100 Subject: [PATCH 16/78] Add AB test fixtures --- test/unit/ab/conftest.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 test/unit/ab/conftest.py diff --git a/test/unit/ab/conftest.py b/test/unit/ab/conftest.py new file mode 100644 index 0000000..1ce13f9 --- /dev/null +++ b/test/unit/ab/conftest.py @@ -0,0 +1,28 @@ +import pytest + +from qlbm.lattice.lattices.ab_lattice import ABLattice +from qlbm.lattice.lattices.oh_lattice import OHLattice + + +@pytest.fixture +def oh_lattice_d2q9_8x8() -> OHLattice: + return OHLattice( + { + "lattice": { + "dim": {"x": 8, "y": 8}, + "velocities": "d2q9", + }, + }, + ) + + +@pytest.fixture +def ab_lattice_d2q9_8x8() -> ABLattice: + return ABLattice( + { + "lattice": { + "dim": {"x": 8, "y": 8}, + "velocities": "d2q9", + }, + }, + ) From e51356ee4101ff5dee1a53b06db42382185eb8af Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Thu, 11 Dec 2025 14:38:51 +0100 Subject: [PATCH 17/78] Fix imports in tests --- test/integration/compiler_test.py | 1 - test/unit/ab/ab_initial_codnitions_test.py | 3 +-- test/unit/collision/eqc_generator_test.py | 1 - test/unit/collision/eqc_velocity_discretization_test.py | 3 --- test/unit/lqlga/circuits/hamming_adder_test.py | 3 +-- test/unit/lqlga/circuits/streaming_test.py | 1 + 6 files changed, 3 insertions(+), 9 deletions(-) diff --git a/test/integration/compiler_test.py b/test/integration/compiler_test.py index be11dac..62a9bab 100644 --- a/test/integration/compiler_test.py +++ b/test/integration/compiler_test.py @@ -3,7 +3,6 @@ import pytest from qiskit import QuantumCircuit as QiskitQC from qiskit_aer import AerSimulator -from qulacs import QuantumCircuit as QulacsQC from qlbm.components import MSStreamingOperator from qlbm.infra import ( diff --git a/test/unit/ab/ab_initial_codnitions_test.py b/test/unit/ab/ab_initial_codnitions_test.py index 43007c7..6a2078e 100644 --- a/test/unit/ab/ab_initial_codnitions_test.py +++ b/test/unit/ab/ab_initial_codnitions_test.py @@ -1,11 +1,10 @@ from itertools import product import pytest -from qiskit import ClassicalRegister, QuantumCircuit, transpile +from qiskit import ClassicalRegister, transpile from qiskit_aer import AerSimulator from qlbm.components.ab.initial import DiscreteUniformVelocityABInitialConditions -from qlbm.tools.utils import bit_value @pytest.mark.parametrize( diff --git a/test/unit/collision/eqc_generator_test.py b/test/unit/collision/eqc_generator_test.py index 66934e3..b0d51c5 100644 --- a/test/unit/collision/eqc_generator_test.py +++ b/test/unit/collision/eqc_generator_test.py @@ -1,4 +1,3 @@ -import numpy as np from qlbm.lattice.eqc.eqc import EquivalenceClass from qlbm.lattice.eqc.eqc_generator import ( diff --git a/test/unit/collision/eqc_velocity_discretization_test.py b/test/unit/collision/eqc_velocity_discretization_test.py index 4f78d9e..47327aa 100644 --- a/test/unit/collision/eqc_velocity_discretization_test.py +++ b/test/unit/collision/eqc_velocity_discretization_test.py @@ -3,9 +3,6 @@ import pytest from qlbm.lattice.eqc.eqc import EquivalenceClass -from qlbm.lattice.eqc.eqc_generator import ( - EquivalenceClassGenerator, -) from qlbm.lattice.spacetime.properties_base import LatticeDiscretization from qlbm.tools.exceptions import LatticeException diff --git a/test/unit/lqlga/circuits/hamming_adder_test.py b/test/unit/lqlga/circuits/hamming_adder_test.py index a34ae2a..41fc39a 100644 --- a/test/unit/lqlga/circuits/hamming_adder_test.py +++ b/test/unit/lqlga/circuits/hamming_adder_test.py @@ -1,7 +1,6 @@ -import pytest from qiskit import QuantumCircuit, transpile -from qiskit_aer import AerSimulator from qiskit.result import Counts +from qiskit_aer import AerSimulator from qlbm.components.common import HammingWeightAdder diff --git a/test/unit/lqlga/circuits/streaming_test.py b/test/unit/lqlga/circuits/streaming_test.py index 444d474..5cce590 100644 --- a/test/unit/lqlga/circuits/streaming_test.py +++ b/test/unit/lqlga/circuits/streaming_test.py @@ -1,6 +1,7 @@ from typing import List, Tuple import pytest + from qlbm.components.lqlga.streaming import LQLGAStreamingOperator From a33e402ec8bcd45e6e59ffb54ed56d18eec31efb Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Thu, 11 Dec 2025 16:32:35 +0100 Subject: [PATCH 18/78] Add citations --- docs/source/refs.bib | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/docs/source/refs.bib b/docs/source/refs.bib index af332da..9c36c6a 100644 --- a/docs/source/refs.bib +++ b/docs/source/refs.bib @@ -85,4 +85,26 @@ @article{qsearch author={Georgescu, C{\u{a}}lin A and M{\"o}ller, Matthias}, journal={arXiv preprint arXiv:2510.14062}, year={2025} +} + +@article{erio, +title={Quantum Algorithms for the Lattice Boltzmann Method: Encoding and Evolution}, +author={D.T. Duong}, +journal={TU Delft MSc. Thesis}, +year={2025}, +url={https://repository.tudelft.nl/record/uuid:a7b20729-46b7-42d1-aaf7-c001fc93efd9} +} + +@article{draper, + title={Addition on a quantum computer}, + author={Draper, Thomas G}, + journal={arXiv preprint quant-ph/0008033}, + year={2000} +} + +@article{qftadder, + title={Circuit for Shor's algorithm using 2n+ 3 qubits}, + author={Beauregard, Stephane}, + journal={arXiv preprint quant-ph/0205095}, + year={2002} } \ No newline at end of file From 945ec93d495d210db7d9fcbd43d8e1b6b2f045d6 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Thu, 11 Dec 2025 16:34:36 +0100 Subject: [PATCH 19/78] Update adders documentation --- qlbm/components/common/adders.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/qlbm/components/common/adders.py b/qlbm/components/common/adders.py index ac0e315..7feecb0 100644 --- a/qlbm/components/common/adders.py +++ b/qlbm/components/common/adders.py @@ -1,3 +1,5 @@ +"""Circuits implementing componenets of quantum adders. See :cite:`draper` and :cite:`adder`.""" + from logging import Logger, getLogger from math import pi from time import perf_counter_ns @@ -217,7 +219,11 @@ class PhaseShift(LBMPrimitive): A primitive that applies the phase-shift as part of the :class:`.ControlledIncrementer` used in the :class:`.MSStreamingOperator`. The rotation applied is :math:`\pm\frac{\pi}{2^{n_q - 1 - j}}`, with :math:`j` the position of the qubit (indexed starting with 0). - For an in-depth mathematical explanation of the procedure, consult Section 4 of :cite:t:`collisionless`. + For an in-depth mathematical explanation of the procedure and its use within QLBM, + consult Section 4 of :cite:t:`collisionless`. + The Draper adder was originally formulated in :cite:`draper`, while the version implemented + here uses the one-register approach, which was + first described in :cite:`adder`. ========================= ====================================================================== Attribute Summary From 5e33f16b85ea74801d7bce6ea4bdeab4c18ec02b Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Thu, 11 Dec 2025 17:53:33 +0100 Subject: [PATCH 20/78] Add UniformStatePrep primitive --- qlbm/components/common/__init__.py | 9 +- qlbm/components/common/primitives.py | 146 ++++++++++++++++++++++++++- 2 files changed, 152 insertions(+), 3 deletions(-) diff --git a/qlbm/components/common/__init__.py b/qlbm/components/common/__init__.py index afabb00..0999cd4 100644 --- a/qlbm/components/common/__init__.py +++ b/qlbm/components/common/__init__.py @@ -2,7 +2,12 @@ from .adders import ParameterizedDraperAdder, ParameterizedPhaseShift, PhaseShift from .cbse_collision import EQCCollisionOperator, EQCPermutation, EQCRedistribution -from .primitives import EmptyPrimitive, HammingWeightAdder +from .primitives import ( + EmptyPrimitive, + HammingWeightAdder, + TruncatedQFT, + UniformStatePrep, +) __all__ = [ "EmptyPrimitive", @@ -13,4 +18,6 @@ "ParameterizedDraperAdder", "ParameterizedPhaseShift", "PhaseShift", + "TruncatedQFT", + "UniformStatePrep" ] diff --git a/qlbm/components/common/primitives.py b/qlbm/components/common/primitives.py index b34ddd5..5a0f437 100644 --- a/qlbm/components/common/primitives.py +++ b/qlbm/components/common/primitives.py @@ -2,7 +2,7 @@ from logging import Logger, getLogger from time import perf_counter_ns -from typing import List, Tuple, cast +from typing import List, Tuple import numpy as np from numpy import pi @@ -14,7 +14,6 @@ from qlbm.components.base import LBMPrimitive from qlbm.components.common.adders import ParameterizedDraperAdder -from qlbm.components.ms.streaming import ControlledIncrementer from qlbm.lattice import Lattice from qlbm.tools.utils import get_qubits_to_invert @@ -257,6 +256,149 @@ def __str__(self): return f"[Primitive TuncatedQFT({self.num_qubits}, {self.dft_size})]" +class UniformStatePrep(LBMPrimitive): + r"""Efficient uniform state preparation primitive used to create an equal magnitude superposition over the first :math:`k` basis states. + + This is an implementation of Algorithm 1 described by :cite:t:`uniprep`. + It is used to create an uniform magnitude superposition over arbitrary + velocity states in :class:`.ABDiscreteUniformInitialConditions`. + + Example usage: + + .. plot:: + :include-source: + + from qlbm.components.common import UniformStatePrep + + UniformStatePrep(4, 7).decompose(reps=2).draw("mpl") + """ + + num_qubits: int + """The number of qubits the operator acts on.""" + + num_states: int + """The number of states to generate.""" + + def __init__( + self, + num_qubits: int, + num_states: int, + logger: Logger = getLogger("qlbm"), + ): + super().__init__(logger) + self.num_qubits = num_qubits + self.num_states = num_states + + self.logger.info(f"Creating circuit {str(self)}...") + circuit_creation_start_time = perf_counter_ns() + self.circuit = self.create_circuit() + self.logger.info( + f"Creating circuit {str(self)} took {perf_counter_ns() - circuit_creation_start_time} (ns)" + ) + + @override + def create_circuit(self): + circuit = QuantumCircuit( + self.num_qubits, name=f"UniformStatePrep{self.num_states}" + ) + + # M = 1 : do nothing, stays in |0...0> + if self.num_states == 1: + return circuit + + # If M is a power of two, the solution is trivial: Hadamards on log2(M) qubits + is_power_of_two = (self.num_states & (self.num_states - 1)) == 0 + if is_power_of_two: + r = int(np.log2(self.num_states)) + for q in range(r): + circuit.h(q) + return circuit + + # --- General case: Algorithm 1 (Section 2.1 of the paper) --- + + # We only need n_eff = ceil(log2 M) active qubits; the rest stay in |0> + n_eff = self._ceil_log2_M(self.num_states) + if n_eff > self.num_qubits: + raise ValueError("Internal error: n_eff > num_qubits.") + + # Binary decomposition: M = Σ_j 2^{l_j}, with 0 <= l0 < l1 < ... < lk + bit_positions = [i for i in range(n_eff) if (self.num_states >> i) & 1] + bit_positions.sort() + l0 = bit_positions[0] + k = len(bit_positions) - 1 # number of "higher" bits + + # Helper: safe acos for numerical stability + def safe_acos(x: float) -> float: + return np.acos(max(-1.0, min(1.0, x))) + + # Step 4: Apply X on qubits at positions l1, l2, ..., lk + for j in range(1, len(bit_positions)): + circuit.x(bit_positions[j]) + + # Step 5: M0 = 2^{l0} + M_prev = 2**l0 # This is M_0 in the paper + + # Step 6–7: If l0 > 0, apply H on qubits 0..(l0-1) + if l0 > 0: + for q in range(l0): + circuit.h(q) + + # Step 8: Apply RY(theta0) on |q_{l1}>, theta0 = -2 arccos( sqrt(M0 / M) ) + l1 = bit_positions[1] + theta0 = -2.0 * safe_acos(np.sqrt(M_prev / self.num_states)) + circuit.ry(theta0, l1) + + # Step 9: Controlled H on qubits i in [l0, l1) with open control on q_{l1} == |0> + ctrl = l1 + circuit.x(ctrl) # convert open control (on |0>) to normal control (on |1>) + for i in range(l0, l1): + circuit.ch(ctrl, i) + circuit.x(ctrl) + + # Steps 10–13: For-loop over remaining bits + for m in range(1, k): + l_m = bit_positions[m] + l_next = bit_positions[m + 1] + + # Step 11: Controlled RY(theta_m) on q_{l_{m+1}} with open control on q_{l_m} == |0> + numerator = 2**l_m + denominator = self.num_states - M_prev + theta_m = -2.0 * safe_acos(np.sqrt(numerator / denominator)) + + # open control on q_{l_m} + ctrl = l_m + target = l_next + circuit.x(ctrl) + circuit.cry(theta_m, ctrl, target) + circuit.x(ctrl) + + # Step 12: Controlled H on qubits i in [l_m, l_{m+1}) with open control on q_{l_{m+1}} == |0> + ctrl_next = l_next + circuit.x(ctrl_next) + for i in range(l_m, l_next): + circuit.ch(ctrl_next, i) + circuit.x(ctrl_next) + + # Step 13: M_m = M_{m-1} + 2^{l_m} + M_prev += 2**l_m + + return circuit + + def _ceil_log2_M(self, M: int) -> int: + """Minimal number of qubits n such that M <= 2**n.""" + if M <= 1: + return 1 + # Power of two? + if M & (M - 1) == 0: + return int(np.log2(M)) + # Non power-of-two + return M.bit_length() + + @override + def __str__(self): + return f"[Primitive UniformStatePrep({self.num_qubits}, {self.num_states})]" + + class AdditionConversion(LBMPrimitive): num_qubits: int """The number of qubits the states are encoded in.""" From 46d4f5cfd4050b74ec156dbdb77b035f1774d6fc Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Thu, 11 Dec 2025 17:55:18 +0100 Subject: [PATCH 21/78] Add documentation for AB initial conditions --- qlbm/components/ab/__init__.py | 3 +- qlbm/components/ab/initial.py | 51 ++++++++++++++++++++++++++++++---- qlbm/components/ms/__init__.py | 6 +++- 3 files changed, 53 insertions(+), 7 deletions(-) diff --git a/qlbm/components/ab/__init__.py b/qlbm/components/ab/__init__.py index c1f69b3..4ae453d 100644 --- a/qlbm/components/ab/__init__.py +++ b/qlbm/components/ab/__init__.py @@ -2,13 +2,14 @@ from .ab import ABQLBM from .encodings import ABEncodingType -from .initial import ABInitialConditions +from .initial import ABDiscreteUniformInitialConditions, ABInitialConditions from .measurement import ABGridMeasurement from .reflection import ABReflectionOperator, ABReflectionPermutation from .streaming import ABStreamingOperator __all__ = [ "ABQLBM", + "ABDiscreteUniformInitialConditions", "ABInitialConditions", "ABGridMeasurement", "ABReflectionOperator", diff --git a/qlbm/components/ab/initial.py b/qlbm/components/ab/initial.py index a10f81a..89c712f 100644 --- a/qlbm/components/ab/initial.py +++ b/qlbm/components/ab/initial.py @@ -11,7 +11,10 @@ from qlbm.components.ab.encodings import ABEncodingType from qlbm.components.ab.utils import BinaryToOHPermutation from qlbm.components.base import LBMPrimitive -from qlbm.components.common.primitives import AdditionConversion, TruncatedQFT +from qlbm.components.common.primitives import ( + AdditionConversion, + UniformStatePrep, +) from qlbm.lattice.lattices.ab_lattice import ABLattice from qlbm.tools.exceptions import LatticeException from qlbm.tools.utils import dimension_letter @@ -22,7 +25,7 @@ class ABInitialConditions(LBMPrimitive): Initial conditions for the :class:`ABQLBM` algorithm. This component creates an equal magnitude superposition of all velocity - basis states at position ``(0, 0)`` using the :class:`TruncatedQFT`. + basis states at position ``(0, 0)`` using the :class:`UniformStatePrep`. Example usage: @@ -82,7 +85,7 @@ def create_circuit(self) -> QuantumCircuit: nq = int(np.ceil(np.log2(self.lattice.num_velocities_per_point))) circuit.compose( - TruncatedQFT( + UniformStatePrep( nq, self.lattice.num_velocity_qubits, self.logger, @@ -115,7 +118,45 @@ def __str__(self) -> str: return f"[Primitive ABEInitialConditions with lattice {self.lattice}]" -class DiscreteUniformVelocityABInitialConditions(LBMPrimitive): +class ABDiscreteUniformInitialConditions(LBMPrimitive): + """ + Initial conditions for the :class:`ABQLBM` algorithm. + + This component creates an equal magnitude superposition of a configurable set of velocity and grid indices. + + Example usage: + + .. plot:: + :include-source: + + from qlbm.components.ab import ABDiscreteUniformInitialConditions + from qlbm.lattice import ABLattice + + lattice = ABLattice( + { + "lattice": {"dim": {"x": 16, "y": 8}, "velocities": "d2q9"}, + } + ) + + ABDiscreteUniformInitialConditions(lattice, [1, 3, 4], ([], [])).draw("mpl") + + The primitive can also applied to the :class:`.OHLattice`: + + .. plot:: + :include-source: + + from qlbm.components.ab import ABDiscreteUniformInitialConditions + from qlbm.lattice import OHLattice + + lattice = OHLattice( + { + "lattice": {"dim": {"x": 16, "y": 8}, "velocities": "d2q9"}, + } + ) + + ABDiscreteUniformInitialConditions(lattice, [0, 1], ([0, 1], [0])).draw("mpl") + """ + velocity_indices: List[int] grid_qubits_to_superpose: Tuple[List[int], ...] @@ -178,7 +219,7 @@ def create_circuit(self) -> QuantumCircuit: nq = int(np.ceil(np.log2(len(self.velocity_indices)))) circuit.compose( - TruncatedQFT( + UniformStatePrep( nq, len(self.velocity_indices), self.logger, diff --git a/qlbm/components/ms/__init__.py b/qlbm/components/ms/__init__.py index a7e3af3..7e21e94 100644 --- a/qlbm/components/ms/__init__.py +++ b/qlbm/components/ms/__init__.py @@ -1,6 +1,10 @@ """Modular qlbm quantum circuit components for the MSQLBM algorithm :cite:p:`collisionless`.""" -from ..common.adders import PhaseShift, ParameterizedDraperAdder, ParameterizedPhaseShift +from ..common.adders import ( + ParameterizedDraperAdder, + ParameterizedPhaseShift, + PhaseShift, +) from .bounceback_reflection import ( BounceBackReflectionOperator, BounceBackWallComparator, From d727abe3ca8abd7bac4cbe3804bc61c282e6ca90 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Thu, 11 Dec 2025 17:55:46 +0100 Subject: [PATCH 22/78] Add UniformStatePrep tests --- test/unit/ab/uniform_state_prep_test.py | 34 +++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 test/unit/ab/uniform_state_prep_test.py diff --git a/test/unit/ab/uniform_state_prep_test.py b/test/unit/ab/uniform_state_prep_test.py new file mode 100644 index 0000000..839b088 --- /dev/null +++ b/test/unit/ab/uniform_state_prep_test.py @@ -0,0 +1,34 @@ +import numpy as np +import pytest +from qiskit import QuantumCircuit, transpile +from qiskit_aer import AerSimulator + +from qlbm.components.common.primitives import UniformStatePrep + + +@pytest.mark.parametrize( + "num_states", + list(range(1, 10)), +) +def test_uniform_State_prep(num_states): + nq = 5 + sim = AerSimulator() + + qc = QuantumCircuit(nq) + qc.compose( + UniformStatePrep(nq, num_states).circuit, + inplace=True, + ) + tqc = transpile(qc, sim) + tqc.save_statevector() + result = sim.run(tqc).result() + state = result.get_statevector(tqc) + + expected = 1.0 / np.sqrt(num_states) + + assert np.allclose(np.abs(state)[:num_states], expected, atol=1e-8), ( + "Uniform state prep results in wrong magnitudes for the first k basis states" + ) + assert np.allclose(np.abs(state)[num_states:], 0.0, atol=1e-8), ( + "Uniform state prep results in wrong magnitudes for the trailing basis states" + ) From be6556f97a5c49449af44405744919bfb7d4ce14 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Thu, 11 Dec 2025 18:02:22 +0100 Subject: [PATCH 23/78] Add documentation to OHPermutation --- qlbm/components/ab/__init__.py | 2 ++ qlbm/components/ab/utils.py | 27 ++++++++++++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/qlbm/components/ab/__init__.py b/qlbm/components/ab/__init__.py index 4ae453d..0652430 100644 --- a/qlbm/components/ab/__init__.py +++ b/qlbm/components/ab/__init__.py @@ -6,6 +6,7 @@ from .measurement import ABGridMeasurement from .reflection import ABReflectionOperator, ABReflectionPermutation from .streaming import ABStreamingOperator +from .utils import BinaryToOHPermutation __all__ = [ "ABQLBM", @@ -16,4 +17,5 @@ "ABReflectionPermutation", "ABStreamingOperator", "ABEncodingType", + "BinaryToOHPermutation", ] diff --git a/qlbm/components/ab/utils.py b/qlbm/components/ab/utils.py index 112e52d..541db78 100644 --- a/qlbm/components/ab/utils.py +++ b/qlbm/components/ab/utils.py @@ -1,3 +1,5 @@ +"""Utilities for the Amplitude-Based QLBM.""" + from logging import Logger, getLogger from time import perf_counter_ns @@ -11,6 +13,29 @@ class BinaryToOHPermutation(LBMPrimitive): + """ + Permutes the first :math:`q` basis states of the binary encoding into the :math:`q` one-hot states of the OH encoding. + + This operator is implemented as a decomposed permutation matrix. + As such, its decomposition will be exponentially expensive in the number of qubits. + By default, the unitary acts the :math:`q` qubits of the of the one hot encoding (in a :math:`D_dQ_q` discretization). + + Example usage: + .. plot:: + :include-source: + + from qlbm.components.ab import BinaryToOHPermutation + from qlbm.lattice import OHLattice + + lattice = OHLattice( + { + "lattice": {"dim": {"x": 16, "y": 8}, "velocities": "d2q9"}, + } + ) + + BinaryToOHPermutation(lattice).draw("mpl") + """ + lattice: ABLattice def __init__( @@ -64,4 +89,4 @@ def create_circuit(self) -> QuantumCircuit: @override def __str__(self) -> str: - return f"[Primitive BinaryOHPermutation with lattice {self.lattice}]" \ No newline at end of file + return f"[Primitive BinaryOHPermutation with lattice {self.lattice}]" From 063fb696c76f41aaf1c3c9c415c2896044b3f2a3 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Thu, 11 Dec 2025 18:16:38 +0100 Subject: [PATCH 24/78] Add documentation to common primitives --- qlbm/components/common/__init__.py | 6 +++- qlbm/components/common/primitives.py | 32 ++++++++++++++++++++++ test/unit/ab/ab_initial_codnitions_test.py | 6 ++-- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/qlbm/components/common/__init__.py b/qlbm/components/common/__init__.py index 0999cd4..172badd 100644 --- a/qlbm/components/common/__init__.py +++ b/qlbm/components/common/__init__.py @@ -3,13 +3,16 @@ from .adders import ParameterizedDraperAdder, ParameterizedPhaseShift, PhaseShift from .cbse_collision import EQCCollisionOperator, EQCPermutation, EQCRedistribution from .primitives import ( + AdditionConversion, EmptyPrimitive, HammingWeightAdder, + StateSetter, TruncatedQFT, UniformStatePrep, ) __all__ = [ + "AdditionConversion", "EmptyPrimitive", "EQCCollisionOperator", "EQCPermutation", @@ -18,6 +21,7 @@ "ParameterizedDraperAdder", "ParameterizedPhaseShift", "PhaseShift", + "StateSetter", "TruncatedQFT", - "UniformStatePrep" + "UniformStatePrep", ] diff --git a/qlbm/components/common/primitives.py b/qlbm/components/common/primitives.py index 5a0f437..1477ef1 100644 --- a/qlbm/components/common/primitives.py +++ b/qlbm/components/common/primitives.py @@ -400,6 +400,24 @@ def __str__(self): class AdditionConversion(LBMPrimitive): + """ + Converts one basis state to another by incrementation/decrementation. + + Useful for performing permutations in which the initial superposition contains no basis states + of the target superposition. + + The circuit utilizes a :class:`.ParameterizedDraperAdder` which controlled on the state + of an ancilla qubit to add the difference only to the target basis state. + + + .. plot:: + :include-source: + + from qlbm.components.common import AdditionConversion + + AdditionConversion(4, 2, 7).draw("mpl") + """ + num_qubits: int """The number of qubits the states are encoded in.""" @@ -475,6 +493,20 @@ def __str__(self): class StateSetter(LBMPrimitive): + r""" + Permutes the superposition such that a target state :math:`\ket{k}` is permuted to :math:`\ket{1}^{\otimes n}`. + + The primitive acts a single layer of :math:`\mathrm{X}` gates on the qubit + indices that have value :math:`\ket{0}` for the input state. + + .. plot:: + :include-source: + + from qlbm.components.common import StateSetter + + StateSetter(4, 6).draw("mpl") + """ + num_qubits: int """The number of qubits the state is encoded in.""" diff --git a/test/unit/ab/ab_initial_codnitions_test.py b/test/unit/ab/ab_initial_codnitions_test.py index 6a2078e..bf47fb2 100644 --- a/test/unit/ab/ab_initial_codnitions_test.py +++ b/test/unit/ab/ab_initial_codnitions_test.py @@ -4,7 +4,7 @@ from qiskit import ClassicalRegister, transpile from qiskit_aer import AerSimulator -from qlbm.components.ab.initial import DiscreteUniformVelocityABInitialConditions +from qlbm.components.ab.initial import ABDiscreteUniformInitialConditions @pytest.mark.parametrize( @@ -23,7 +23,7 @@ def test_initial_ab_no_gird_superposition(velocities, lattice_fixture, request): qc = lattice.circuit.copy() qc.add_register(ClassicalRegister(4)) qc.compose( - DiscreteUniformVelocityABInitialConditions( + ABDiscreteUniformInitialConditions( lattice, velocities, ([], []) ).circuit, inplace=True, @@ -57,7 +57,7 @@ def test_initial_oh_no_gird_superposition(velocities, lattice_fixture, request): qc = lattice.circuit.copy() qc.add_register(ClassicalRegister(9)) qc.compose( - DiscreteUniformVelocityABInitialConditions( + ABDiscreteUniformInitialConditions( lattice, velocities, ([], []) ).circuit, inplace=True, From 1032e989e4452a4eea6e3c4c92530d736c01ffd7 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Fri, 12 Dec 2025 12:20:15 +0100 Subject: [PATCH 25/78] Fix documentation errors --- qlbm/components/ab/initial.py | 2 +- qlbm/components/common/__init__.py | 2 ++ qlbm/components/common/adders.py | 2 +- qlbm/components/common/primitives.py | 22 ++++++++++++++++------ 4 files changed, 20 insertions(+), 8 deletions(-) diff --git a/qlbm/components/ab/initial.py b/qlbm/components/ab/initial.py index 89c712f..5e92098 100644 --- a/qlbm/components/ab/initial.py +++ b/qlbm/components/ab/initial.py @@ -25,7 +25,7 @@ class ABInitialConditions(LBMPrimitive): Initial conditions for the :class:`ABQLBM` algorithm. This component creates an equal magnitude superposition of all velocity - basis states at position ``(0, 0)`` using the :class:`UniformStatePrep`. + basis states at position ``(0, 0)`` using the :class:`.UniformStatePrep`. Example usage: diff --git a/qlbm/components/common/__init__.py b/qlbm/components/common/__init__.py index 172badd..4c050bb 100644 --- a/qlbm/components/common/__init__.py +++ b/qlbm/components/common/__init__.py @@ -6,6 +6,7 @@ AdditionConversion, EmptyPrimitive, HammingWeightAdder, + MCSwap, StateSetter, TruncatedQFT, UniformStatePrep, @@ -24,4 +25,5 @@ "StateSetter", "TruncatedQFT", "UniformStatePrep", + "MCSwap", ] diff --git a/qlbm/components/common/adders.py b/qlbm/components/common/adders.py index 7feecb0..d411cef 100644 --- a/qlbm/components/common/adders.py +++ b/qlbm/components/common/adders.py @@ -223,7 +223,7 @@ class PhaseShift(LBMPrimitive): consult Section 4 of :cite:t:`collisionless`. The Draper adder was originally formulated in :cite:`draper`, while the version implemented here uses the one-register approach, which was - first described in :cite:`adder`. + first described in :cite:`qftadder`. ========================= ====================================================================== Attribute Summary diff --git a/qlbm/components/common/primitives.py b/qlbm/components/common/primitives.py index 1477ef1..c50cfe6 100644 --- a/qlbm/components/common/primitives.py +++ b/qlbm/components/common/primitives.py @@ -28,7 +28,7 @@ class EmptyPrimitive(LBMPrimitive): ========================= ====================================================================== Attribute Summary ========================= ====================================================================== - :attr:`lattice` The :class:`.MSLattice` or :class:`.SpaceTimeLattice` based on which the number of qubits is inferred. + :attr:`lattice` The :class:`.Lattice` based on which the number of qubits is inferred. :attr:`logger` The performance logger, by default ``getLogger("qlbm")``. ========================= ====================================================================== """ @@ -120,6 +120,16 @@ class HammingWeightAdder(LBMPrimitive): This primitive adds the hamming weight (number of 1s) in a given register :math:`x` to the binary-encoded value of a second register :math:`y`. + + Example usage: + + .. plot:: + :include-source: + + from qlbm.components.common import HammingWeightAdder + + # Add the Hamming weight of a 3-qubit register onto a 5-qubit register + HammingWeightAdder(3, 5).draw("mpl") """ x_register_size: int @@ -189,11 +199,11 @@ class TruncatedQFT(LBMPrimitive): For a superposition of the first :math:`k` basis states encoded in :math:`n` qubits, the operator consists of discrete fourier transform block of size :math:`k\times k`, - padded with :math:`2^n - k` :math:`1`s on the main diagonal. + padded with :math:`2^n - k` :math:`1`\ s on the main diagonal. The rationale and properties of this operator are described in :cite:`spacetime2`. This primitive is used in both amplitude-based and computational basis state encodings. - In the :class:`ABInitialConditions`, it creates an equal magnitude superposition over the velocity space. - In the :class:`EQCRedistribution`, the superposition is over all basis states with an equivalent mass and momenta. + In the :class:`.ABInitialConditions`, it creates an equal magnitude superposition over the velocity space. + In the :class:`.EQCRedistribution`, the superposition is over all basis states with an equivalent mass and momenta. Example usage: @@ -202,7 +212,7 @@ class TruncatedQFT(LBMPrimitive): from qlbm.components.common import TruncatedQFT - TruncatedQFT(4, 7).decompose(reps=2).draw("mpl") + TruncatedQFT(4, 5).circuit.decompose(reps=2).draw("mpl") """ num_qubits: int @@ -270,7 +280,7 @@ class UniformStatePrep(LBMPrimitive): from qlbm.components.common import UniformStatePrep - UniformStatePrep(4, 7).decompose(reps=2).draw("mpl") + UniformStatePrep(4, 5).draw("mpl") """ num_qubits: int From 69e70bd4ec1cb563561b54f313a02b96b6563f62 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Fri, 12 Dec 2025 12:22:08 +0100 Subject: [PATCH 26/78] Update ABQLBM web documentation --- docs/source/code/comps_cqlbm.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/source/code/comps_cqlbm.rst b/docs/source/code/comps_cqlbm.rst index ac97ebe..67fbad2 100644 --- a/docs/source/code/comps_cqlbm.rst +++ b/docs/source/code/comps_cqlbm.rst @@ -13,7 +13,7 @@ Amplitude-Based Circuits MSStreamingOperator, ControlledIncrementer, SpecularReflectionOperator, - SpeedSensitivePhaseShift, + ParameterizedPhaseShift, ) from qlbm.lattice import MSLattice print("ok") @@ -75,6 +75,8 @@ Initial Conditions .. autoclass:: qlbm.components.ms.primitives.MSInitialConditions3DSlim +.. autoclass:: qlbm.components.ab.initial.ABDiscreteUniformInitialConditions + .. autoclass:: qlbm.components.ab.initial.ABInitialConditions .. _cqlbm_streaming: @@ -88,9 +90,9 @@ Streaming .. autoclass:: qlbm.components.ms.streaming.ControlledIncrementer -.. autoclass:: qlbm.components.ms.primitives.SpeedSensitiveAdder +.. autoclass:: qlbm.components.common.ParameterizedDraperAdder -.. autoclass:: qlbm.components.ms.streaming.SpeedSensitivePhaseShift +.. autoclass:: qlbm.components.common.ParameterizedPhaseShift .. autoclass:: qlbm.components.ms.streaming.PhaseShift From 174fe01fa91f2a0afee2352607e1df02abd3e36a Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Fri, 12 Dec 2025 12:22:44 +0100 Subject: [PATCH 27/78] Add state preparation citation --- docs/source/refs.bib | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/docs/source/refs.bib b/docs/source/refs.bib index 9c36c6a..09f89b5 100644 --- a/docs/source/refs.bib +++ b/docs/source/refs.bib @@ -107,4 +107,18 @@ @article{qftadder author={Beauregard, Stephane}, journal={arXiv preprint quant-ph/0205095}, year={2002} -} \ No newline at end of file +} + + +@article{uniprep, + abstract = {Quantum state preparation involving a uniform superposition over a non-empty subset of n-qubit computational basis states is an important and challenging step in many quantum computation algorithms and applications. In this work, we address the problem of preparation of a uniform superposition state of the form {\$}{\$}{$\backslash$}left{$|$} {\{}{$\backslash$}Psi {\}}{$\backslash$}right{$\backslash$}rangle = {$\backslash$}frac{\{}1{\}}{\{}{$\backslash$}sqrt{\{}M{\}}{\}}{$\backslash$}sum {\_}{\{}j = 0{\}}\^{}{\{}M - 1{\}} {$\backslash$}left{$|$} {\{}j{\}}{$\backslash$}right{$\backslash$}rangle {\$}{\$}, where M denotes the number of distinct states in the superposition state and {\$}{\$}2 {$\backslash$}le M {$\backslash$}le 2\^{}n{\$}{\$}. We show that the superposition state {\$}{\$}{$\backslash$}left{$|$} {\{}{$\backslash$}Psi {\}}{$\backslash$}right{$\backslash$}rangle {\$}{\$}can be efficiently prepared, using a deterministic approach, with a gate complexity and circuit depth of only {\$}{\$}O({$\backslash$}log {\_}2\~{}M){\$}{\$}for all M. This demonstrates an exponential reduction in gate complexity in comparison with other existing deterministic approaches in the literature for the general case of this problem. Another advantage of the proposed approach is that it requires only {\$}{\$}n={$\backslash$}lceil {$\backslash$}log {\_}2\~{}M{$\backslash$}rceil {\$}{\$}qubits. Furthermore, neither ancilla qubits nor any quantum gates with multiple controls are needed in our approach for creating the uniform superposition state {\$}{\$}{$\backslash$}left{$|$} {\{}{$\backslash$}Psi {\}}{$\backslash$}right{$\backslash$}rangle {\$}{\$}. It is also shown that a broad class of nonuniform superposition states that involve a mixture of uniform superposition states can also be efficiently created with the same circuit configuration that is used for creating the uniform superposition state {\$}{\$}{$\backslash$}left{$|$} {\{}{$\backslash$}Psi {\}}{$\backslash$}right{$\backslash$}rangle {\$}{\$}described earlier, but with modified parameters.}, + author = {Shukla, Alok and Vedula, Prakash}, + date = {2024/01/29}, + doi = {10.1007/s11128-024-04258-4}, + journal = {Quantum Information Processing}, + number = {2}, + pages = {38}, + title = {An efficient quantum algorithm for preparation of uniform quantum superposition states}, + url = {https://doi.org/10.1007/s11128-024-04258-4}, + volume = {23}, + year = {2024}} From ee5f9961bc849324937d9f6ee09abb38463a56d2 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Fri, 12 Dec 2025 12:28:38 +0100 Subject: [PATCH 28/78] Add web docs for misc circuits --- docs/source/code/comps_other.rst | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 docs/source/code/comps_other.rst diff --git a/docs/source/code/comps_other.rst b/docs/source/code/comps_other.rst new file mode 100644 index 0000000..c4634d4 --- /dev/null +++ b/docs/source/code/comps_other.rst @@ -0,0 +1,22 @@ +.. _misc_components: + +==================================== +Miscellaneous Circuits +==================================== + +Circuits that are used throughout different algorithms, +have niche use cases, or are not encoding-specific. + +.. autoclass:: qlbm.components.common.EmptyPrimitive + +.. autoclass:: qlbm.components.common.MCSwap + +.. autoclass:: qlbm.components.common.HammingWeightAdder + +.. autoclass:: qlbm.components.common.TruncatedQFT + +.. autoclass:: qlbm.components.common.UniformStatePrep + +.. autoclass:: qlbm.components.common.AdditionConversion + +.. autoclass:: qlbm.components.common.StateSetter \ No newline at end of file From c77d5312990085c185003c07f54a470ea42e3ec7 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Fri, 12 Dec 2025 12:29:02 +0100 Subject: [PATCH 29/78] Update web docs API --- docs/source/_static/css/custom.css | 5 +++++ docs/source/code/comps_lga.rst | 2 +- docs/source/code/index.rst | 28 ++++++++++++++++++++++++++-- docs/source/examples/index.rst | 2 +- 4 files changed, 33 insertions(+), 4 deletions(-) diff --git a/docs/source/_static/css/custom.css b/docs/source/_static/css/custom.css index 94f94ac..c75f98c 100644 --- a/docs/source/_static/css/custom.css +++ b/docs/source/_static/css/custom.css @@ -10,4 +10,9 @@ .bd-sidebar-primary { max-width: 230px +} + +table.center-align-col { + margin-left: auto; + margin-right: auto; } \ No newline at end of file diff --git a/docs/source/code/comps_lga.rst b/docs/source/code/comps_lga.rst index 71a16a2..c7a129d 100644 --- a/docs/source/code/comps_lga.rst +++ b/docs/source/code/comps_lga.rst @@ -19,7 +19,7 @@ QLGA Circuits MSStreamingOperator, ControlledIncrementer, SpecularReflectionOperator, - SpeedSensitivePhaseShift, + ParameterizedPhaseShift, ) from qlbm.lattice import MSLattice, LQLGALattice print("ok") diff --git a/docs/source/code/index.rst b/docs/source/code/index.rst index 5174384..0967420 100644 --- a/docs/source/code/index.rst +++ b/docs/source/code/index.rst @@ -4,7 +4,7 @@ Internal Documentation ================================ ``qlbm`` is made up of 4 main modules. -Together, the :ref:`base_components`, :ref:`amplitude_components`, and :ref:`qlga_components` +Together, the :ref:`base_components`, :ref:`amplitude_components`, :ref:`qlga_components`, and :ref:`misc_components` module handle the parameterized creation of quantum circuits that compose QBMs. The :ref:`lattice` module parses external information into quantum registers and provides uniform interfaces for underlying algorithms. @@ -12,12 +12,36 @@ The :ref:`infra` module integrates the quantum components with Tket, Qiskit, and Qulacs transpilers and runners. The :ref:`tools` module contains miscellaneous utilities. +.. rst-class:: center-align-col + ++----------------------------+----------------------------------+----------------------------------+----------------------------------+----------------------------------+----------------------------------+ +| | Encodings | ++----------------------------+----------------------------------+----------------------------------+----------------------------------+----------------------------------+----------------------------------+ +| | Amplitude Encodings | Computational Basis State Encoding | ++============================+==================================+==================================+==================================+==================================+==================================+ +| | Ampl. Based | One-Hot | Multi-Speed | Space-Time | Linear | ++----------------------------+----------------------------------+----------------------------------+----------------------------------+----------------------------------+----------------------------------+ +| Algorithm | QLBM |QLGA | ++----------------------------+----------------------------------+----------------------------------+----------------------------------+----------------------------------+----------------------------------+ +| Reference | N/A | :cite:`erio` | :cite:`collisionless` | :cite:`spacetime` | :cite:`lqlga1, spacetime2` | ++----------------------------+----------------------------------+----------------------------------+----------------------------------+----------------------------------+----------------------------------+ +| Discretization | :math:`D_dQ_q` | MS :cite:`collisionless` | :math:`D_dQ_q` | ++----------------------------+----------------------------------+----------------------------------+----------------------------------+----------------------------------+----------------------------------+ +| Implementation | :math:`D_2Q_9` | 2D, 3D, :math:`\geq 4` speeds | :math:`D_1Q_2`, :math:`D_1Q_3`, :math:`D_2Q_4` | ++----------------------------+----------------------------------+----------------------------------+----------------------------------+----------------------------------+----------------------------------+ +| Required Qubits | :math:`{O}(\log(qN_g))` | :math:`{O}(\log(N_g)+q)` | :math:`{O}(\log(qN_g))` | :math:`{O}(\log(N_g)+N_t^d)` | :math:`qN_g` | ++----------------------------+----------------------------------+----------------------------------+----------------------------------+----------------------------------+----------------------------------+ + + + .. toctree:: + :caption: Tutorials + :maxdepth: 1 lattice comps_base comps_cqlbm comps_lga + comps_other infra tools - diff --git a/docs/source/examples/index.rst b/docs/source/examples/index.rst index d247629..f290535 100644 --- a/docs/source/examples/index.rst +++ b/docs/source/examples/index.rst @@ -18,4 +18,4 @@ Currently, the following visualization tutorials are available online: notebooks/spacetime_vis notebooks/lqlga_vis notebooks/geometry_vis - notebooks/flowfield_vis + notebooks/flowfield_vis \ No newline at end of file From 03e9e7214c893f06514663080a9644255adf94c5 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Fri, 12 Dec 2025 13:29:25 +0100 Subject: [PATCH 30/78] Fix configuration in MS simulation demo --- demos/simulation/ms_simulation.ipynb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/demos/simulation/ms_simulation.ipynb b/demos/simulation/ms_simulation.ipynb index 9dc16b3..239aaa6 100644 --- a/demos/simulation/ms_simulation.ipynb +++ b/demos/simulation/ms_simulation.ipynb @@ -38,14 +38,14 @@ "lattice = MSLattice(\n", " {\n", " \"lattice\": {\n", - " \"dim\": {\"x\": 64, \"y\": 32},\n", + " \"dim\": {\"x\": 16, \"y\": 16},\n", " \"velocities\": {\n", " \"x\": 4,\n", " \"y\": 4,\n", " },\n", " },\n", " \"geometry\": [\n", - " {\"shape\": \"cuboid\", \"x\": [10, 13], \"y\": [14, 17], \"boundary\": \"bounceback\"},\n", + " {\"shape\": \"cuboid\", \"x\": [6, 12], \"y\": [6, 12], \"boundary\": \"bounceback\"},\n", " ],\n", " }\n", ")\n", From c0daaa54aa3173ecf23a16d5253c86c45aedd55c Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Tue, 23 Dec 2025 00:01:24 +0100 Subject: [PATCH 31/78] Update citation --- README.md | 4 ++-- docs/source/refs.bib | 14 ++++++++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 06b90bc..3f43552 100644 --- a/README.md +++ b/README.md @@ -66,8 +66,8 @@ There are also Docker container images in the `Docker` directory that can be use Currently, `qlbm` supports two algorithms: - The Quantum Transport Method (Collisionless QLBM) described in [Efficient and fail-safe quantum algorithm for the transport equation](https://doi.org/10.1016/j.jcp.2024.112816) ([arXiv:2211.14269](https://arxiv.org/abs/2211.14269)) by M.A. Schalkers and M. Möller. - - The Space-Time QLBM/QLGA described in [On the importance of data encoding in quantum Boltzmann methods](https://link.springer.com/article/10.1007/s11128-023-04216-6) by M.A. Schalkers and M. Möller and expanded in [Fully Quantum Lattice Gas Automata Building Blocks for Computational Basis State Encodings](https://arxiv.org/abs/2506.12662). - - The Linear-encoding Quantum Lattice Gas Automata (LQLGA) described in [On quantum extensions of hydrodynamic lattice gas automata](https://www.mdpi.com/2410-3896/4/2/48) by P. Love and [Fully Quantum Lattice Gas Automata Building Blocks for Computational Basis State Encodings](https://arxiv.org/abs/2506.12662). + - The Space-Time QLBM/QLGA described in [On the importance of data encoding in quantum Boltzmann methods](https://link.springer.com/article/10.1007/s11128-023-04216-6) by M.A. Schalkers and M. Möller and expanded in [Fully Quantum Lattice Gas Automata Building Blocks for Computational Basis State Encodings](https://doi.org/10.1016/j.jcp.2025.114595). + - The Linear-encoding Quantum Lattice Gas Automata (LQLGA) described in [On quantum extensions of hydrodynamic lattice gas automata](https://www.mdpi.com/2410-3896/4/2/48) by P. Love and [Fully Quantum Lattice Gas Automata Building Blocks for Computational Basis State Encodings](https://doi.org/10.1016/j.jcp.2025.114595). The `demos` directory contains several use cases for simulating and analyzing these algorithms. Each demo requires minimal setup once the virtual environment has been configured. Consult the `README.md` file in the `demos` directory for further details. diff --git a/docs/source/refs.bib b/docs/source/refs.bib index 09f89b5..78cf16e 100644 --- a/docs/source/refs.bib +++ b/docs/source/refs.bib @@ -42,10 +42,16 @@ @article{qlbm } @article{spacetime2, - title={Fully Quantum Lattice Gas Automata Building Blocks for Computational Basis State Encodings}, - author={Georgescu, C{\u{a}}lin A and Schalkers, Merel A and M{\"o}ller, Matthias}, - journal={arXiv preprint arXiv:2506.12662}, - year={2025} +title = {Fully quantum lattice gas automata building blocks for computational basis state encodings}, +journal = {Journal of Computational Physics}, +volume = {549}, +pages = {114595}, +year = {2026}, +issn = {0021-9991}, +doi = {https://doi.org/10.1016/j.jcp.2025.114595}, +url = {https://www.sciencedirect.com/science/article/pii/S0021999125008770}, +author = {C{\u{a}}lin A. Georgescu and Merel A. Schalkers and Matthias M{\"o}ller}, +keywords = {Quantum computing, Lattice gas automata, Computational fluid dynamics}, } From b62fdf36833c974a3d197a95357fdbba6b6ef47e Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Fri, 16 Jan 2026 07:26:55 +0100 Subject: [PATCH 32/78] Add support for control qubits in UniformStatePrep --- qlbm/components/common/primitives.py | 93 +++++++++++++++++++++++----- 1 file changed, 77 insertions(+), 16 deletions(-) diff --git a/qlbm/components/common/primitives.py b/qlbm/components/common/primitives.py index c50cfe6..a8871ab 100644 --- a/qlbm/components/common/primitives.py +++ b/qlbm/components/common/primitives.py @@ -7,7 +7,7 @@ import numpy as np from numpy import pi from qiskit import QuantumCircuit -from qiskit.circuit.library import MCMTGate, XGate +from qiskit.circuit.library import HGate, MCMTGate, XGate from qiskit.quantum_info import Operator from qiskit.synthesis import synth_qft_full as QFT from typing_extensions import override @@ -15,6 +15,7 @@ from qlbm.components.base import LBMPrimitive from qlbm.components.common.adders import ParameterizedDraperAdder from qlbm.lattice import Lattice +from qlbm.tools.exceptions import CircuitException from qlbm.tools.utils import get_qubits_to_invert @@ -293,11 +294,13 @@ def __init__( self, num_qubits: int, num_states: int, + num_ctrl_qubits: int = 0, logger: Logger = getLogger("qlbm"), ): super().__init__(logger) self.num_qubits = num_qubits self.num_states = num_states + self.num_ctrl_qubits = num_ctrl_qubits self.logger.info(f"Creating circuit {str(self)}...") circuit_creation_start_time = perf_counter_ns() @@ -309,7 +312,14 @@ def __init__( @override def create_circuit(self): circuit = QuantumCircuit( - self.num_qubits, name=f"UniformStatePrep{self.num_states}" + self.num_qubits + self.num_ctrl_qubits, + name=f"UniformStatePrep{self.num_states}", + ) + + ctrl_qubits = ( + list(range(self.num_qubits, self.num_qubits + self.num_ctrl_qubits)) + if self.num_ctrl_qubits > 0 + else [] ) # M = 1 : do nothing, stays in |0...0> @@ -321,7 +331,15 @@ def create_circuit(self): if is_power_of_two: r = int(np.log2(self.num_states)) for q in range(r): - circuit.h(q) + if ctrl_qubits: + circuit.compose( + MCMTGate(HGate(), self.num_ctrl_qubits, 1), + qubits=ctrl_qubits + [q], + inplace=True, + ) + else: + circuit.h(q) + return circuit # --- General case: Algorithm 1 (Section 2.1 of the paper) --- @@ -329,7 +347,7 @@ def create_circuit(self): # We only need n_eff = ceil(log2 M) active qubits; the rest stay in |0> n_eff = self._ceil_log2_M(self.num_states) if n_eff > self.num_qubits: - raise ValueError("Internal error: n_eff > num_qubits.") + raise CircuitException("Internal error: n_eff > num_qubits.") # Binary decomposition: M = Σ_j 2^{l_j}, with 0 <= l0 < l1 < ... < lk bit_positions = [i for i in range(n_eff) if (self.num_states >> i) & 1] @@ -343,7 +361,10 @@ def safe_acos(x: float) -> float: # Step 4: Apply X on qubits at positions l1, l2, ..., lk for j in range(1, len(bit_positions)): - circuit.x(bit_positions[j]) + if ctrl_qubits: + circuit.mcx(control_qubits=ctrl_qubits, target_qubit=bit_positions[j]) + else: + circuit.x(bit_positions[j]) # Step 5: M0 = 2^{l0} M_prev = 2**l0 # This is M_0 in the paper @@ -351,19 +372,43 @@ def safe_acos(x: float) -> float: # Step 6–7: If l0 > 0, apply H on qubits 0..(l0-1) if l0 > 0: for q in range(l0): - circuit.h(q) + if ctrl_qubits: + circuit.compose( + MCMTGate(HGate(), self.num_ctrl_qubits, 1), + qubits=ctrl_qubits + [q], + inplace=True, + ) + else: + circuit.h(q) # Step 8: Apply RY(theta0) on |q_{l1}>, theta0 = -2 arccos( sqrt(M0 / M) ) l1 = bit_positions[1] theta0 = -2.0 * safe_acos(np.sqrt(M_prev / self.num_states)) - circuit.ry(theta0, l1) + + if ctrl_qubits: + circuit.mcry(theta0, ctrl_qubits, l1) + else: + circuit.ry(theta0, l1) # Step 9: Controlled H on qubits i in [l0, l1) with open control on q_{l1} == |0> ctrl = l1 - circuit.x(ctrl) # convert open control (on |0>) to normal control (on |1>) + + if ctrl_qubits: + circuit.mcx(ctrl_qubits, ctrl) + else: + circuit.x(ctrl) # convert open control (on |0>) to normal control (on |1>) + for i in range(l0, l1): - circuit.ch(ctrl, i) - circuit.x(ctrl) + circuit.compose( + MCMTGate(HGate(), self.num_ctrl_qubits + 1, 1), + qubits=ctrl_qubits + [ctrl, i], + inplace=True, + ) + + if ctrl_qubits: + circuit.mcx(ctrl_qubits, ctrl) + else: + circuit.x(ctrl) # Steps 10–13: For-loop over remaining bits for m in range(1, k): @@ -378,16 +423,32 @@ def safe_acos(x: float) -> float: # open control on q_{l_m} ctrl = l_m target = l_next - circuit.x(ctrl) - circuit.cry(theta_m, ctrl, target) - circuit.x(ctrl) + if ctrl_qubits: + circuit.mcx(ctrl_qubits, ctrl) + else: + circuit.x(ctrl) + circuit.mcry(theta_m, ctrl_qubits + [ctrl], target) + if ctrl_qubits: + circuit.mcx(ctrl_qubits, ctrl) + else: + circuit.x(ctrl) # Step 12: Controlled H on qubits i in [l_m, l_{m+1}) with open control on q_{l_{m+1}} == |0> ctrl_next = l_next - circuit.x(ctrl_next) + if ctrl_qubits: + circuit.mcx(ctrl_qubits, ctrl_next) + else: + circuit.x(ctrl_next) for i in range(l_m, l_next): - circuit.ch(ctrl_next, i) - circuit.x(ctrl_next) + circuit.compose( + MCMTGate(HGate(), self.num_ctrl_qubits + 1, 1), + qubits=ctrl_qubits + [ctrl_next] + [i], + inplace=True, + ) + if ctrl_qubits: + circuit.mcx(ctrl_qubits, ctrl_next) + else: + circuit.x(ctrl_next) # Step 13: M_m = M_{m-1} + 2^{l_m} M_prev += 2**l_m From 4d0cc47504d48b6ff964ee1356b046e42e9a361a Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Fri, 16 Jan 2026 18:07:02 +0100 Subject: [PATCH 33/78] Add ABQLBM parallel uniform discrete initial conditions --- qlbm/components/ab/initial.py | 185 +++++++++++++++++++++++++++++++++- 1 file changed, 181 insertions(+), 4 deletions(-) diff --git a/qlbm/components/ab/initial.py b/qlbm/components/ab/initial.py index 5e92098..e1e3b84 100644 --- a/qlbm/components/ab/initial.py +++ b/qlbm/components/ab/initial.py @@ -6,6 +6,7 @@ import numpy as np from qiskit import QuantumCircuit +from qiskit.circuit.library import HGate, MCMTGate from typing_extensions import override from qlbm.components.ab.encodings import ABEncodingType @@ -13,10 +14,11 @@ from qlbm.components.base import LBMPrimitive from qlbm.components.common.primitives import ( AdditionConversion, + StateSetter, UniformStatePrep, ) from qlbm.lattice.lattices.ab_lattice import ABLattice -from qlbm.tools.exceptions import LatticeException +from qlbm.tools.exceptions import CircuitException, LatticeException from qlbm.tools.utils import dimension_letter @@ -222,7 +224,7 @@ def create_circuit(self) -> QuantumCircuit: UniformStatePrep( nq, len(self.velocity_indices), - self.logger, + logger=self.logger, ).circuit, qubits=self.lattice.velocity_index()[:nq], inplace=True, @@ -240,7 +242,7 @@ def create_circuit(self) -> QuantumCircuit: for v_from, v_to in zip(states_from, states_to): circuit.compose( AdditionConversion( - self.lattice.num_velocity_qubits, v_from, v_to, self.logger + self.lattice.num_velocity_qubits, v_from, v_to, logger=self.logger ).circuit, qubits=self.lattice.velocity_index()[ : self.lattice.num_velocities_per_point @@ -269,4 +271,179 @@ def create_circuit(self) -> QuantumCircuit: @override def __str__(self) -> str: - return f"[Primitive DiscreteUniformVelocityABInitialConditions with lattice {self.lattice}, v={self.velocity_indices}, g={self.grid_qubits_to_superpose}]" + return f"[Primitive ABDiscreteUniformInitialConditions with lattice {self.lattice}, v={self.velocity_indices}, g={self.grid_qubits_to_superpose}]" + + +class ABParallelDiscreteUniformInitialConditions(LBMPrimitive): + """ + TODO. + """ + + velocity_indices: List[List[int]] + + grid_qubits_to_superpose: List[Tuple[List[int], ...]] + + lattice: ABLattice + + marker_indices: List[int] + + def __init__( + self, + lattice: ABLattice, + velocity_indices_list: List[List[int]], + grid_qubits_to_superpose_list: List[Tuple[List[int], ...]], + marker_indices: List[int], + logger: Logger = getLogger("qlbm"), + ) -> None: + super().__init__(logger) + + self.lattice = lattice + + if self.lattice.get_encoding() == ABEncodingType.OH: + raise LatticeException( + "OHLattice does not currently support parallel initial conditions." + ) + + if (len(velocity_indices_list) != len(marker_indices)) or ( + len(velocity_indices_list) != len(grid_qubits_to_superpose_list) + ): + raise CircuitException("Input lists have mismatched lengths.") + + for velocity_indices, grid_qubits_to_superpose in zip( + velocity_indices_list, grid_qubits_to_superpose_list + ): + if any( + map( + lambda x: x + not in list(range(0, self.lattice.num_velocities_per_point)), + velocity_indices, + ) + ): + raise LatticeException( + f"Velocity indices should be in the interval 0..{self.lattice.num_velocities_per_point}" + ) + + if len(grid_qubits_to_superpose) != self.lattice.num_dims: + raise LatticeException( + f"Lattice has {self.lattice.num_dims} dimensions, but provided grid qubit information has {len(grid_qubits_to_superpose)} entries." + ) + + for dim in range(self.lattice.num_dims): + if any( + map( + lambda x: x + not in list( + range(self.lattice.num_gridpoints[dim].bit_length()) + ), + grid_qubits_to_superpose[dim], + ), + ): + raise LatticeException( + f"Grid qubit specification in dimension {dimension_letter(dim)} out of range." + ) + + self.velocity_indices_list = [ + sorted(velocity_indices) for velocity_indices in velocity_indices_list + ] + self.grid_qubits_to_superpose_list = grid_qubits_to_superpose_list + self.marker_indices = marker_indices + + self.logger.info(f"Creating circuit {str(self)}...") + circuit_creation_start_time = perf_counter_ns() + self.circuit = self.create_circuit() + self.logger.info( + f"Creating circuit {str(self)} took {perf_counter_ns() - circuit_creation_start_time} (ns)" + ) + + @override + def create_circuit(self) -> QuantumCircuit: + circuit = QuantumCircuit(*self.lattice.registers) + + # Uniform superposition over the marker index + circuit.compose( + UniformStatePrep( + self.lattice.num_marker_qubits, + len(self.marker_indices), + logger=self.logger, + ).circuit, + qubits=self.lattice.marker_index(), + inplace=True, + ) + + for marker_index, velocity_indices, grid_qubits_to_superpose in zip( + self.marker_indices, + self.velocity_indices_list, + self.grid_qubits_to_superpose_list, + ): + nq = int(np.ceil(np.log2(len(velocity_indices)))) + + state_setter_circ = StateSetter( + self.lattice.num_marker_qubits, marker_index, self.logger + ).circuit + + circuit.compose( + state_setter_circ, qubits=self.lattice.marker_index(), inplace=True + ) + + circuit.compose( + UniformStatePrep( + nq, + len(velocity_indices), + num_ctrl_qubits=self.lattice.num_marker_qubits, + logger=self.logger, + ).circuit, + qubits=self.lattice.velocity_index()[:nq] + self.lattice.marker_index(), + inplace=True, + ) + + states_from: List[int] = list(range(len(velocity_indices))) + states_to: List[int] = velocity_indices.copy() + + # Remove indices that are already in place + for v in velocity_indices: + if v < len(velocity_indices): + states_from.remove(v) + states_to.remove(v) + + for v_from, v_to in zip(states_from, states_to): + circuit.compose( + AdditionConversion( + self.lattice.num_velocity_qubits, + v_from, + v_to, + num_ctrl_qubits=self.lattice.num_marker_qubits, + logger=self.logger, + ).circuit, + qubits=self.lattice.velocity_index()[ + : self.lattice.num_velocities_per_point + ] # Additional guard necessary of OH + + self.lattice.ancillae_obstacle_index(0) + + self.lattice.marker_index(), + inplace=True, + ) + + for dim in range(self.lattice.num_dims): + if grid_qubits_to_superpose[dim]: + qs_to_superpose = [ + self.lattice.grid_index(dim)[0] + q + for q in grid_qubits_to_superpose[dim] + ] + circuit.compose( + MCMTGate( + HGate(), + self.lattice.num_marker_qubits, + len(qs_to_superpose), + ), + qubits=self.lattice.marker_index() + qs_to_superpose, + inplace=True, + ) + + circuit.compose( + state_setter_circ, qubits=self.lattice.marker_index(), inplace=True + ) + + return circuit + + @override + def __str__(self) -> str: + return f"[Primitive ABParallelDiscreteUniformInitialConditions with lattice {self.lattice}, v={self.velocity_indices_list}, g={self.grid_qubits_to_superpose_list}, m={self.marker_indices}]" From 2d3505d5c1ec0db235db1648d45559bb6fe5ffe1 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Fri, 16 Jan 2026 18:07:45 +0100 Subject: [PATCH 34/78] Add optional controls to AdditionConversion primitive --- qlbm/components/common/primitives.py | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/qlbm/components/common/primitives.py b/qlbm/components/common/primitives.py index a8871ab..d16256b 100644 --- a/qlbm/components/common/primitives.py +++ b/qlbm/components/common/primitives.py @@ -349,7 +349,7 @@ def create_circuit(self): if n_eff > self.num_qubits: raise CircuitException("Internal error: n_eff > num_qubits.") - # Binary decomposition: M = Σ_j 2^{l_j}, with 0 <= l0 < l1 < ... < lk + # Binary decomposition: M = \Sum_j 2^{l_j}, with 0 <= l0 < l1 < ... < lk bit_positions = [i for i in range(n_eff) if (self.num_states >> i) & 1] bit_positions.sort() l0 = bit_positions[0] @@ -498,17 +498,22 @@ class AdditionConversion(LBMPrimitive): state_to: int """The state to convert to.""" + num_ctrl_qubits: int + """The number of qubits to control the operation.""" + def __init__( self, num_qubits: int, state_from: int, state_to: int, + num_ctrl_qubits: int = 0, logger: Logger = getLogger("qlbm"), ): super().__init__(logger) self.num_qubits = num_qubits self.state_from = state_from self.state_to = state_to + self.num_ctrl_qubits = num_ctrl_qubits self.logger.info(f"Creating circuit {str(self)}...") circuit_creation_start_time = perf_counter_ns() @@ -519,7 +524,7 @@ def __init__( @override def create_circuit(self): - circuit = QuantumCircuit(self.num_qubits + 1) + circuit = QuantumCircuit(self.num_qubits + self.num_ctrl_qubits + 1) state_setter_circ = StateSetter( self.num_qubits, self.state_from, self.logger @@ -528,7 +533,13 @@ def create_circuit(self): circuit.compose( state_setter_circ, qubits=list(range(self.num_qubits)), inplace=True ) - circuit.mcx(list(range(self.num_qubits)), self.num_qubits) + circuit.mcx( + list(range(self.num_qubits)) + + list( + range(self.num_qubits + 1, self.num_qubits + 1 + self.num_ctrl_qubits) + ), + self.num_qubits, + ) circuit.compose( state_setter_circ, qubits=list(range(self.num_qubits)), inplace=True ) @@ -538,7 +549,7 @@ def create_circuit(self): self.num_qubits, abs(self.state_to - self.state_from), self.state_to > self.state_from, - 1, + self.num_ctrl_qubits + 1, self.logger, ).circuit, inplace=True, @@ -551,7 +562,13 @@ def create_circuit(self): circuit.compose( state_setter_circ, qubits=list(range(self.num_qubits)), inplace=True ) - circuit.mcx(list(range(self.num_qubits)), self.num_qubits) + circuit.mcx( + list(range(self.num_qubits)) + + list( + range(self.num_qubits + 1, self.num_qubits + 1 + self.num_ctrl_qubits) + ), + self.num_qubits, + ) circuit.compose( state_setter_circ, qubits=list(range(self.num_qubits)), inplace=True ) From 71597db3c939dac5779eae61103b1a387800bdd0 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Fri, 16 Jan 2026 18:09:34 +0100 Subject: [PATCH 35/78] Add marker setter to ABLattice --- qlbm/lattice/lattices/ab_lattice.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/qlbm/lattice/lattices/ab_lattice.py b/qlbm/lattice/lattices/ab_lattice.py index adca9b1..b34a101 100644 --- a/qlbm/lattice/lattices/ab_lattice.py +++ b/qlbm/lattice/lattices/ab_lattice.py @@ -181,6 +181,11 @@ def __update_registers(self): self.circuit = QuantumCircuit(*self.registers) + def set_num_markers(self, num_markers: int): + self.num_marker_qubits = num_markers + + self.__update_registers() + def set_geometries(self, geometries): """ Updates the geometry setup of the lattice. @@ -347,16 +352,22 @@ def get_registers(self) -> Tuple[List[QuantumRegister], ...]: for c, gp in enumerate(self.num_gridpoints) ] - marker_register = ( - [ + if self.has_multiple_geometries(): + marker_register = [ QuantumRegister( int(ceil(log2(len(self.geometries)))), name="m", ) ] - if self.has_multiple_geometries() - else [] - ) + elif self.num_marker_qubits > 0: + marker_register = [ + QuantumRegister( + self.num_marker_qubits, + name="m", + ) + ] + else: + marker_register = [] accumulation_register = ( [QuantumRegister(self.num_accumulation_qubits, name="acc")] From 12294e6d6beb2eec8219273b561ed3bf76efc0cd Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Sat, 17 Jan 2026 23:34:26 +0100 Subject: [PATCH 36/78] Add documentation and simplify parallel ICs interface --- qlbm/components/ab/initial.py | 51 ++++++++++++++++++++++------- qlbm/lattice/lattices/ab_lattice.py | 17 ++++++++-- 2 files changed, 54 insertions(+), 14 deletions(-) diff --git a/qlbm/components/ab/initial.py b/qlbm/components/ab/initial.py index e1e3b84..61d28b5 100644 --- a/qlbm/components/ab/initial.py +++ b/qlbm/components/ab/initial.py @@ -90,7 +90,7 @@ def create_circuit(self) -> QuantumCircuit: UniformStatePrep( nq, self.lattice.num_velocity_qubits, - self.logger, + logger=self.logger, ).circuit, qubits=self.lattice.velocity_index()[:nq], inplace=True, @@ -276,7 +276,35 @@ def __str__(self) -> str: class ABParallelDiscreteUniformInitialConditions(LBMPrimitive): """ - TODO. + Marker-sensitive initial conditions for the :class:`ABQLBM` algorithm. + + This component creates an equal magnitude superposition of a configurable set of velocity and grid indices, + entangled with the state of the marker register. + Used in parallel realizations of configurations. + + Example usage: + + .. plot:: + :include-source: + + from qlbm.components.ab import ABParallelDiscreteUniformInitialConditions + from qlbm.lattice import ABLattice + + lattice = ABLattice( + { + "lattice": {"dim": {"x": 16, "y": 8}, "velocities": "d2q9"}, + } + ) + + lattice.set_num_marker_qubits(2) + + ABParallelDiscreteUniformInitialConditions( + lattice, + [[0, 1], [0, 3], [0], [0, 5]], + [([0], [0])] * 4, + [0, 1, 2, 3], + ).draw("mpl") + """ velocity_indices: List[List[int]] @@ -285,14 +313,11 @@ class ABParallelDiscreteUniformInitialConditions(LBMPrimitive): lattice: ABLattice - marker_indices: List[int] - def __init__( self, lattice: ABLattice, velocity_indices_list: List[List[int]], grid_qubits_to_superpose_list: List[Tuple[List[int], ...]], - marker_indices: List[int], logger: Logger = getLogger("qlbm"), ) -> None: super().__init__(logger) @@ -304,11 +329,14 @@ def __init__( "OHLattice does not currently support parallel initial conditions." ) - if (len(velocity_indices_list) != len(marker_indices)) or ( - len(velocity_indices_list) != len(grid_qubits_to_superpose_list) - ): + if len(velocity_indices_list) != len(grid_qubits_to_superpose_list): raise CircuitException("Input lists have mismatched lengths.") + if len(velocity_indices_list) > 2**self.lattice.num_marker_qubits: + raise LatticeException( + f"{self.lattice.num_marker_qubits} cannot encode {len(velocity_indices_list)} configurations." + ) + for velocity_indices, grid_qubits_to_superpose in zip( velocity_indices_list, grid_qubits_to_superpose_list ): @@ -346,7 +374,6 @@ def __init__( sorted(velocity_indices) for velocity_indices in velocity_indices_list ] self.grid_qubits_to_superpose_list = grid_qubits_to_superpose_list - self.marker_indices = marker_indices self.logger.info(f"Creating circuit {str(self)}...") circuit_creation_start_time = perf_counter_ns() @@ -363,7 +390,7 @@ def create_circuit(self) -> QuantumCircuit: circuit.compose( UniformStatePrep( self.lattice.num_marker_qubits, - len(self.marker_indices), + len(self.velocity_indices_list), logger=self.logger, ).circuit, qubits=self.lattice.marker_index(), @@ -371,7 +398,7 @@ def create_circuit(self) -> QuantumCircuit: ) for marker_index, velocity_indices, grid_qubits_to_superpose in zip( - self.marker_indices, + list(range(len(self.velocity_indices_list))), self.velocity_indices_list, self.grid_qubits_to_superpose_list, ): @@ -446,4 +473,4 @@ def create_circuit(self) -> QuantumCircuit: @override def __str__(self) -> str: - return f"[Primitive ABParallelDiscreteUniformInitialConditions with lattice {self.lattice}, v={self.velocity_indices_list}, g={self.grid_qubits_to_superpose_list}, m={self.marker_indices}]" + return f"[Primitive ABParallelDiscreteUniformInitialConditions with lattice {self.lattice}, v={self.velocity_indices_list}, g={self.grid_qubits_to_superpose_list}]" diff --git a/qlbm/lattice/lattices/ab_lattice.py b/qlbm/lattice/lattices/ab_lattice.py index b34a101..125645f 100644 --- a/qlbm/lattice/lattices/ab_lattice.py +++ b/qlbm/lattice/lattices/ab_lattice.py @@ -181,8 +181,21 @@ def __update_registers(self): self.circuit = QuantumCircuit(*self.registers) - def set_num_markers(self, num_markers: int): - self.num_marker_qubits = num_markers + def set_num_marker_qubits(self, num_marker_qubits: int): + """ + Sets the number of marker qubits and updates the registers accordingly. + + Note that the previous marker logic, inferred by the geometry, is overwritten, + and therefore might be inconsistent. + + Parameters + ---------- + num_marker_qubits : int + The number of marker qubits that lattice circuits use. + """ + if num_marker_qubits < 0: + raise LatticeException("Cannot set a negative number of markers.") + self.num_marker_qubits = num_marker_qubits self.__update_registers() From b248cf376c79fdb82f0b0e1e87883ed277a6a045 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Sat, 17 Jan 2026 23:35:18 +0100 Subject: [PATCH 37/78] Include parallel ICs in module intercace --- qlbm/components/__init__.py | 4 ++++ qlbm/components/ab/__init__.py | 7 ++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/qlbm/components/__init__.py b/qlbm/components/__init__.py index ac2003f..91ae684 100644 --- a/qlbm/components/__init__.py +++ b/qlbm/components/__init__.py @@ -2,8 +2,10 @@ from .ab import ( ABQLBM, + ABDiscreteUniformInitialConditions, ABGridMeasurement, ABInitialConditions, + ABParallelDiscreteUniformInitialConditions, ABReflectionOperator, ABReflectionPermutation, ABStreamingOperator, @@ -88,4 +90,6 @@ "ABReflectionOperator", "ABReflectionPermutation", "ABStreamingOperator", + "ABDiscreteUniformInitialConditions", + "ABParallelDiscreteUniformInitialConditions", ] diff --git a/qlbm/components/ab/__init__.py b/qlbm/components/ab/__init__.py index 0652430..8f30bc9 100644 --- a/qlbm/components/ab/__init__.py +++ b/qlbm/components/ab/__init__.py @@ -2,7 +2,11 @@ from .ab import ABQLBM from .encodings import ABEncodingType -from .initial import ABDiscreteUniformInitialConditions, ABInitialConditions +from .initial import ( + ABDiscreteUniformInitialConditions, + ABInitialConditions, + ABParallelDiscreteUniformInitialConditions, +) from .measurement import ABGridMeasurement from .reflection import ABReflectionOperator, ABReflectionPermutation from .streaming import ABStreamingOperator @@ -11,6 +15,7 @@ __all__ = [ "ABQLBM", "ABDiscreteUniformInitialConditions", + "ABParallelDiscreteUniformInitialConditions", "ABInitialConditions", "ABGridMeasurement", "ABReflectionOperator", From d3e9b6fe63a5cc9631b4260d2aca13db594f7e04 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 19 Jan 2026 00:33:22 +0100 Subject: [PATCH 38/78] Add parallel ICs to documentation website --- docs/source/code/comps_cqlbm.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/source/code/comps_cqlbm.rst b/docs/source/code/comps_cqlbm.rst index 67fbad2..05309fe 100644 --- a/docs/source/code/comps_cqlbm.rst +++ b/docs/source/code/comps_cqlbm.rst @@ -77,6 +77,8 @@ Initial Conditions .. autoclass:: qlbm.components.ab.initial.ABDiscreteUniformInitialConditions +.. autoclass:: qlbm.components.ab.initial.ABParallelDiscreteUniformInitialConditions + .. autoclass:: qlbm.components.ab.initial.ABInitialConditions .. _cqlbm_streaming: From bf79e4b7f0ab1bd437b9bbe20b14cc2fd5aba9d6 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Thu, 29 Jan 2026 22:37:24 +0100 Subject: [PATCH 39/78] Add ABQLBM zone agnostic reflection operator --- qlbm/components/ab/reflection.py | 102 ++++++++++++++++++++++++++++++- 1 file changed, 101 insertions(+), 1 deletion(-) diff --git a/qlbm/components/ab/reflection.py b/qlbm/components/ab/reflection.py index dea1f6b..ae37f47 100644 --- a/qlbm/components/ab/reflection.py +++ b/qlbm/components/ab/reflection.py @@ -535,7 +535,107 @@ def permute_and_stream(self) -> QuantumCircuit: @override def __str__(self) -> str: - return f"[Operator ABStreaming with lattice {self.lattice}]" + return f"[Operator ABReflection with lattice {self.lattice}]" + + +class ABZoneAgnosticReflectionOperator(ABReflectionOperator): + lattice: AmplitudeLattice + + def __init__( + self, + lattice: ABLattice, + blocks: List[Block] | None = None, + logger: Logger = getLogger("qlbm"), + ) -> None: + super().__init__(lattice, blocks, logger) + + self.blocks = ( + ( + cast(List[Block], flatten(list(self.lattice.geometries[0].values()))) + if not self.lattice.has_multiple_geometries() + else [ + gdict["bounceback"] + gdict["specular"] # type: ignore + for gdict in self.lattice.geometries # type: ignore + ] + ) + if blocks is None + else blocks + ) + + self.logger.info(f"Creating circuit {str(self)}...") + circuit_creation_start_time = perf_counter_ns() + self.circuit = self.create_circuit() + self.logger.info( + f"Creating circuit {str(self)} took {perf_counter_ns() - circuit_creation_start_time} (ns)" + ) + + @override + def create_circuit(self) -> QuantumCircuit: + print("Ok!") + + if self.lattice.discretization not in [LatticeDiscretization.D2Q9]: + raise LatticeException("AB reflection only currently supported in D2Q9") + circuit = self.lattice.circuit.copy() + + for block in self.blocks: + circuit.compose( + self.set_inside_wall_ancilla_state( + block, control_on_marker_state=False + ), + inplace=True, + ) + + circuit.compose( + self.set_ancilla_of_point_state( + flatten( + [[(p, None) for p in block.corners_inside] for block in self.blocks] + ), + ignore_velocity_data=True, + control_on_marker_state=False, + ), + inplace=True, + ) + + # 3-4. controlled permutation and stream + circuit.compose(self.permute_and_stream(), inplace=True) + + # 4-5. uncontrolled inverse stream + circuit.compose( + ABStreamingOperator(self.lattice, logger=self.logger).circuit.inverse(), + inplace=True, + ) + + # 5-6. oracle + for block in self.blocks: + circuit.compose( + self.set_inside_wall_ancilla_state( + block, control_on_marker_state=False + ), + inplace=True, + ) + + circuit.compose( + self.set_ancilla_of_point_state( + flatten( + [[(p, None) for p in block.corners_inside] for block in self.blocks] + ), + ignore_velocity_data=True, + control_on_marker_state=False, + ), + inplace=True, + ) + + # 6-7. uncontrolled regular stream + circuit.compose( + ABStreamingOperator(self.lattice, logger=self.logger).circuit, + inplace=True, + ) + + return circuit + + @override + def __str__(self) -> str: + return f"[Operator ABZoneAgnosticReflection with lattice {self.lattice}]" class ABReflectionPermutation(LBMPrimitive): From cc20e7c051e17b7f76d98a39f6e769cd60350d05 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Thu, 29 Jan 2026 22:37:53 +0100 Subject: [PATCH 40/78] Fix bug in AB lattice marker register size inference --- qlbm/lattice/lattices/ab_lattice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qlbm/lattice/lattices/ab_lattice.py b/qlbm/lattice/lattices/ab_lattice.py index 125645f..e85e823 100644 --- a/qlbm/lattice/lattices/ab_lattice.py +++ b/qlbm/lattice/lattices/ab_lattice.py @@ -368,7 +368,7 @@ def get_registers(self) -> Tuple[List[QuantumRegister], ...]: if self.has_multiple_geometries(): marker_register = [ QuantumRegister( - int(ceil(log2(len(self.geometries)))), + self.num_marker_qubits, name="m", ) ] From 44b4131cc58ab64c83c95ca4f03d7605a494e17a Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Fri, 30 Jan 2026 07:58:55 +0100 Subject: [PATCH 41/78] Fix bugs in AB initial conditions and documentation --- qlbm/components/ab/initial.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/qlbm/components/ab/initial.py b/qlbm/components/ab/initial.py index 61d28b5..059dd68 100644 --- a/qlbm/components/ab/initial.py +++ b/qlbm/components/ab/initial.py @@ -85,14 +85,13 @@ def __init__( def create_circuit(self) -> QuantumCircuit: circuit = QuantumCircuit(*self.lattice.registers) - nq = int(np.ceil(np.log2(self.lattice.num_velocities_per_point))) circuit.compose( UniformStatePrep( - nq, self.lattice.num_velocity_qubits, + self.lattice.num_velocities_per_point, logger=self.logger, ).circuit, - qubits=self.lattice.velocity_index()[:nq], + qubits=self.lattice.velocity_index()[: self.lattice.num_velocity_qubits], inplace=True, ) @@ -302,7 +301,6 @@ class ABParallelDiscreteUniformInitialConditions(LBMPrimitive): lattice, [[0, 1], [0, 3], [0], [0, 5]], [([0], [0])] * 4, - [0, 1, 2, 3], ).draw("mpl") """ From ce0fccaf537a25559a08a63da5ecdcf9761b80b1 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Thu, 12 Feb 2026 12:48:46 +0100 Subject: [PATCH 42/78] Refactor AB reflection into separate module --- qlbm/components/ab/__init__.py | 7 +- qlbm/components/ab/ab.py | 7 +- qlbm/components/ab/reflection/__init__.py | 9 + .../ab/reflection/agnosotic_reflection.py | 118 +++++++++ qlbm/components/ab/reflection/common.py | 114 +++++++++ .../standard_reflection.py} | 231 ++---------------- 6 files changed, 266 insertions(+), 220 deletions(-) create mode 100644 qlbm/components/ab/reflection/__init__.py create mode 100644 qlbm/components/ab/reflection/agnosotic_reflection.py create mode 100644 qlbm/components/ab/reflection/common.py rename qlbm/components/ab/{reflection.py => reflection/standard_reflection.py} (76%) diff --git a/qlbm/components/ab/__init__.py b/qlbm/components/ab/__init__.py index 8f30bc9..c246a42 100644 --- a/qlbm/components/ab/__init__.py +++ b/qlbm/components/ab/__init__.py @@ -8,7 +8,11 @@ ABParallelDiscreteUniformInitialConditions, ) from .measurement import ABGridMeasurement -from .reflection import ABReflectionOperator, ABReflectionPermutation +from .reflection import ( + ABReflectionOperator, + ABReflectionPermutation, + ABZoneAgnosticReflectionOperator, +) from .streaming import ABStreamingOperator from .utils import BinaryToOHPermutation @@ -23,4 +27,5 @@ "ABStreamingOperator", "ABEncodingType", "BinaryToOHPermutation", + "ABZoneAgnosticReflectionOperator" ] diff --git a/qlbm/components/ab/ab.py b/qlbm/components/ab/ab.py index 16f2b0c..7f58ed1 100644 --- a/qlbm/components/ab/ab.py +++ b/qlbm/components/ab/ab.py @@ -6,7 +6,10 @@ from qiskit import QuantumCircuit from typing_extensions import override -from qlbm.components.ab.reflection import ABReflectionOperator +from qlbm.components.ab.reflection import ( + ABReflectionOperator, + ABZoneAgnosticReflectionOperator, +) from qlbm.components.base import LBMAlgorithm from qlbm.lattice.geometry.shapes.block import Block from qlbm.lattice.lattices.ab_lattice import ABLattice @@ -83,7 +86,7 @@ def create_circuit(self): ) circuit.compose( - ABReflectionOperator( + ABZoneAgnosticReflectionOperator( self.lattice, logger=self.logger, ).circuit, diff --git a/qlbm/components/ab/reflection/__init__.py b/qlbm/components/ab/reflection/__init__.py new file mode 100644 index 0000000..3132dff --- /dev/null +++ b/qlbm/components/ab/reflection/__init__.py @@ -0,0 +1,9 @@ +from .agnosotic_reflection import ABZoneAgnosticReflectionOperator +from .common import ABReflectionPermutation +from .standard_reflection import ABReflectionOperator + +__all__ = [ + "ABZoneAgnosticReflectionOperator", + "ABReflectionPermutation", + "ABReflectionOperator", +] diff --git a/qlbm/components/ab/reflection/agnosotic_reflection.py b/qlbm/components/ab/reflection/agnosotic_reflection.py new file mode 100644 index 0000000..31169a4 --- /dev/null +++ b/qlbm/components/ab/reflection/agnosotic_reflection.py @@ -0,0 +1,118 @@ +from qlbm.components.ab.reflection.standard_reflection import ABReflectionOperator +from qlbm.components.ab.streaming import ABStreamingOperator +from qlbm.lattice.geometry.shapes.block import Block +from qlbm.lattice.lattices.ab_lattice import ABLattice +from qlbm.lattice.lattices.base import AmplitudeLattice +from qlbm.lattice.spacetime.properties_base import LatticeDiscretization +from qlbm.tools.exceptions import LatticeException +from qlbm.tools.utils import flatten + + +from qiskit import QuantumCircuit + + +from logging import Logger, getLogger +from time import perf_counter_ns +from typing import List, cast +from typing_extensions import override + + +class ABZoneAgnosticReflectionOperator(ABReflectionOperator): + lattice: AmplitudeLattice + + def __init__( + self, + lattice: ABLattice, + blocks: List[Block] | None = None, + logger: Logger = getLogger("qlbm"), + ) -> None: + super().__init__(lattice, blocks, logger) + + self.blocks = ( + ( + cast(List[Block], flatten(list(self.lattice.geometries[0].values()))) + if not self.lattice.has_multiple_geometries() + else [ + gdict["bounceback"] + gdict["specular"] # type: ignore + for gdict in self.lattice.geometries # type: ignore + ] + ) + if blocks is None + else blocks + ) + + self.logger.info(f"Creating circuit {str(self)}...") + circuit_creation_start_time = perf_counter_ns() + self.circuit = self.create_circuit() + self.logger.info( + f"Creating circuit {str(self)} took {perf_counter_ns() - circuit_creation_start_time} (ns)" + ) + + @override + def create_circuit(self) -> QuantumCircuit: + print("Ok!") + + if self.lattice.discretization not in [LatticeDiscretization.D2Q9]: + raise LatticeException("AB reflection only currently supported in D2Q9") + circuit = self.lattice.circuit.copy() + + # 2-3. oracle + for block in self.blocks: + circuit.compose( + self.set_inside_wall_ancilla_state( + block, control_on_marker_state=False + ), + inplace=True, + ) + + circuit.compose( + self.set_ancilla_of_point_state( + flatten( + [[(p, None) for p in block.corners_inside] for block in self.blocks] + ), + ignore_velocity_data=True, + control_on_marker_state=False, + ), + inplace=True, + ) + + # 3-4. controlled permutation and stream + circuit.compose(self.permute_and_stream(), inplace=True) + + # 4-5. uncontrolled inverse stream + circuit.compose( + ABStreamingOperator(self.lattice, logger=self.logger).circuit.inverse(), + inplace=True, + ) + + # 5-6. oracle + for block in self.blocks: + circuit.compose( + self.set_inside_wall_ancilla_state( + block, control_on_marker_state=False + ), + inplace=True, + ) + + circuit.compose( + self.set_ancilla_of_point_state( + flatten( + [[(p, None) for p in block.corners_inside] for block in self.blocks] + ), + ignore_velocity_data=True, + control_on_marker_state=False, + ), + inplace=True, + ) + + # 6-7. uncontrolled regular stream + circuit.compose( + ABStreamingOperator(self.lattice, logger=self.logger).circuit, + inplace=True, + ) + + return circuit + + @override + def __str__(self) -> str: + return f"[Operator ABZoneAgnosticReflection with lattice {self.lattice}]" \ No newline at end of file diff --git a/qlbm/components/ab/reflection/common.py b/qlbm/components/ab/reflection/common.py new file mode 100644 index 0000000..5d3cf04 --- /dev/null +++ b/qlbm/components/ab/reflection/common.py @@ -0,0 +1,114 @@ +from logging import Logger, getLogger +from time import perf_counter_ns + +from qiskit import QuantumCircuit +from typing_extensions import override + +from qlbm.components.ab.encodings import ABEncodingType +from qlbm.components.base import LBMPrimitive +from qlbm.lattice.spacetime.properties_base import LatticeDiscretization +from qlbm.tools.exceptions import LatticeException + + +class ABReflectionPermutation(LBMPrimitive): + """ + Permutes velocity state to implement reflection in the amplitude-based encoding for :math:`D_dQ_q` discretizations. + + Example usage: + + .. plot:: + :include-source: + + from qlbm.components.ab import ABEncodingType, ABReflectionPermutation + from qlbm.lattice import LatticeDiscretization + + ABReflectionPermutation(4, LatticeDiscretization.D2Q9, ABEncodingType.AB).draw("mpl") + + """ + + num_qubits: int + """ + The number of qubits that encode the velocity state. + """ + + discretization: LatticeDiscretization + """ + The lattice discretization the permutation adheres to. + """ + + encoding: ABEncodingType + """ + The type of encoding to permute for. + """ + + def __init__( + self, + num_qubits: int, + discretization: LatticeDiscretization, + encoding: ABEncodingType, + logger: Logger = getLogger("qlbm"), + ) -> None: + super().__init__(logger) + + self.num_qubits = num_qubits + self.discretization = discretization + self.encoding = encoding + + self.logger.info(f"Creating circuit {str(self)}...") + circuit_creation_start_time = perf_counter_ns() + self.circuit = self.create_circuit() + self.logger.info( + f"Creating circuit {str(self)} took {perf_counter_ns() - circuit_creation_start_time} (ns)" + ) + + @override + def create_circuit(self) -> QuantumCircuit: + if self.discretization == LatticeDiscretization.D2Q9: + return self.__create_circuit_d2q9() + + raise LatticeException("AB reflection only currently supported in D2Q9") + + def __create_circuit_d2q9(self): + circuit = QuantumCircuit(self.num_qubits) + match self.encoding: + case ABEncodingType.OH: + circuit.swap(1, 3) + circuit.swap(2, 4) + circuit.swap(5, 7) + circuit.swap(6, 8) + + case ABEncodingType.AB: + # 1 <-> 3 + circuit.x([0, 1]) + circuit.mcx([0, 1, 3], 2) + circuit.x([0, 1]) + + # 2 <-> 4 + circuit.x([0, 3]) + circuit.cx(1, 2) + circuit.mcx([0, 2, 3], 1) + circuit.cx(1, 2) + circuit.x([0, 3]) + + # 5 <-> 7 + circuit.x(0) + circuit.mcx([0, 1, 3], 2) + circuit.x(0) + + # 6 <-> 8 + circuit.cx(0, 1) + circuit.cx(0, 2) + circuit.x(3) + circuit.mcx([1, 2, 3], 0) + circuit.cx(0, 2) + circuit.cx(0, 1) + circuit.x(3) + + case _: + raise LatticeException(f"Unsupported lattice encoding: {self.encoding}") + + return circuit.reverse_bits() if self.encoding == ABEncodingType.AB else circuit + + @override + def __str__(self) -> str: + return f"[Primitive ABReflectionPermutation with {self.num_qubits} qubits on {self.discretization}]" \ No newline at end of file diff --git a/qlbm/components/ab/reflection.py b/qlbm/components/ab/reflection/standard_reflection.py similarity index 76% rename from qlbm/components/ab/reflection.py rename to qlbm/components/ab/reflection/standard_reflection.py index ae37f47..e8c4adf 100644 --- a/qlbm/components/ab/reflection.py +++ b/qlbm/components/ab/reflection/standard_reflection.py @@ -1,17 +1,7 @@ -"""Quantum circuits used for reflection in the :class:`ABQLBM` algorithm.""" - -from itertools import product -from logging import Logger, getLogger -from time import perf_counter_ns -from typing import List, Tuple, cast - -from qiskit import QuantumCircuit -from qiskit.circuit.library import MCMTGate, XGate -from typing_extensions import override - from qlbm.components.ab.encodings import ABEncodingType +from qlbm.components.ab.reflection.common import ABReflectionPermutation from qlbm.components.ab.streaming import ABStreamingOperator -from qlbm.components.base import LBMOperator, LBMPrimitive +from qlbm.components.base import LBMOperator from qlbm.components.ms.specular_reflection import SpecularWallComparator from qlbm.lattice.geometry.encodings.ms import ReflectionPoint from qlbm.lattice.geometry.shapes.block import Block @@ -22,6 +12,17 @@ from qlbm.tools.utils import flatten, get_qubits_to_invert +from qiskit import QuantumCircuit +from qiskit.circuit.library import MCMTGate, XGate + + +from itertools import product +from logging import Logger, getLogger +from time import perf_counter_ns +from typing import List, Tuple, cast +from typing_extensions import override + + class ABReflectionOperator(LBMOperator): """ Implements bounceback reflection in the amplitude-based encoding of :class:`.ABQLBM` for :math:`D_dQ_q` discretizations. @@ -535,208 +536,4 @@ def permute_and_stream(self) -> QuantumCircuit: @override def __str__(self) -> str: - return f"[Operator ABReflection with lattice {self.lattice}]" - - -class ABZoneAgnosticReflectionOperator(ABReflectionOperator): - lattice: AmplitudeLattice - - def __init__( - self, - lattice: ABLattice, - blocks: List[Block] | None = None, - logger: Logger = getLogger("qlbm"), - ) -> None: - super().__init__(lattice, blocks, logger) - - self.blocks = ( - ( - cast(List[Block], flatten(list(self.lattice.geometries[0].values()))) - if not self.lattice.has_multiple_geometries() - else [ - gdict["bounceback"] + gdict["specular"] # type: ignore - for gdict in self.lattice.geometries # type: ignore - ] - ) - if blocks is None - else blocks - ) - - self.logger.info(f"Creating circuit {str(self)}...") - circuit_creation_start_time = perf_counter_ns() - self.circuit = self.create_circuit() - self.logger.info( - f"Creating circuit {str(self)} took {perf_counter_ns() - circuit_creation_start_time} (ns)" - ) - - @override - def create_circuit(self) -> QuantumCircuit: - print("Ok!") - - if self.lattice.discretization not in [LatticeDiscretization.D2Q9]: - raise LatticeException("AB reflection only currently supported in D2Q9") - circuit = self.lattice.circuit.copy() - - for block in self.blocks: - circuit.compose( - self.set_inside_wall_ancilla_state( - block, control_on_marker_state=False - ), - inplace=True, - ) - - circuit.compose( - self.set_ancilla_of_point_state( - flatten( - [[(p, None) for p in block.corners_inside] for block in self.blocks] - ), - ignore_velocity_data=True, - control_on_marker_state=False, - ), - inplace=True, - ) - - # 3-4. controlled permutation and stream - circuit.compose(self.permute_and_stream(), inplace=True) - - # 4-5. uncontrolled inverse stream - circuit.compose( - ABStreamingOperator(self.lattice, logger=self.logger).circuit.inverse(), - inplace=True, - ) - - # 5-6. oracle - for block in self.blocks: - circuit.compose( - self.set_inside_wall_ancilla_state( - block, control_on_marker_state=False - ), - inplace=True, - ) - - circuit.compose( - self.set_ancilla_of_point_state( - flatten( - [[(p, None) for p in block.corners_inside] for block in self.blocks] - ), - ignore_velocity_data=True, - control_on_marker_state=False, - ), - inplace=True, - ) - - # 6-7. uncontrolled regular stream - circuit.compose( - ABStreamingOperator(self.lattice, logger=self.logger).circuit, - inplace=True, - ) - - return circuit - - @override - def __str__(self) -> str: - return f"[Operator ABZoneAgnosticReflection with lattice {self.lattice}]" - - -class ABReflectionPermutation(LBMPrimitive): - """ - Permutes velocity state to implement reflection in the amplitude-based encoding for :math:`D_dQ_q` discretizations. - - Example usage: - - .. plot:: - :include-source: - - from qlbm.components.ab import ABEncodingType, ABReflectionPermutation - from qlbm.lattice import LatticeDiscretization - - ABReflectionPermutation(4, LatticeDiscretization.D2Q9, ABEncodingType.AB).draw("mpl") - - """ - - num_qubits: int - """ - The number of qubits that encode the velocity state. - """ - - discretization: LatticeDiscretization - """ - The lattice discretization the permutation adheres to. - """ - - encoding: ABEncodingType - """ - The type of encoding to permute for. - """ - - def __init__( - self, - num_qubits: int, - discretization: LatticeDiscretization, - encoding: ABEncodingType, - logger: Logger = getLogger("qlbm"), - ) -> None: - super().__init__(logger) - - self.num_qubits = num_qubits - self.discretization = discretization - self.encoding = encoding - - self.logger.info(f"Creating circuit {str(self)}...") - circuit_creation_start_time = perf_counter_ns() - self.circuit = self.create_circuit() - self.logger.info( - f"Creating circuit {str(self)} took {perf_counter_ns() - circuit_creation_start_time} (ns)" - ) - - @override - def create_circuit(self) -> QuantumCircuit: - if self.discretization == LatticeDiscretization.D2Q9: - return self.__create_circuit_d2q9() - - raise LatticeException("AB reflection only currently supported in D2Q9") - - def __create_circuit_d2q9(self): - circuit = QuantumCircuit(self.num_qubits) - match self.encoding: - case ABEncodingType.OH: - circuit.swap(1, 3) - circuit.swap(2, 4) - circuit.swap(5, 7) - circuit.swap(6, 8) - - case ABEncodingType.AB: - # 1 <-> 3 - circuit.x([0, 1]) - circuit.mcx([0, 1, 3], 2) - circuit.x([0, 1]) - - # 2 <-> 4 - circuit.x([0, 3]) - circuit.cx(1, 2) - circuit.mcx([0, 2, 3], 1) - circuit.cx(1, 2) - circuit.x([0, 3]) - - # 5 <-> 7 - circuit.x(0) - circuit.mcx([0, 1, 3], 2) - circuit.x(0) - - # 6 <-> 8 - circuit.cx(0, 1) - circuit.cx(0, 2) - circuit.x(3) - circuit.mcx([1, 2, 3], 0) - circuit.cx(0, 2) - circuit.cx(0, 1) - circuit.x(3) - - case _: - raise LatticeException(f"Unsupported lattice encoding: {self.encoding}") - - return circuit.reverse_bits() if self.encoding == ABEncodingType.AB else circuit - - @override - def __str__(self) -> str: - return f"[Primitive ABReflectionPermutation with {self.num_qubits} qubits on {self.discretization}]" + return f"[Operator ABReflection with lattice {self.lattice}]" \ No newline at end of file From 0c5c3680f6e93a88c12f334a865395785bfb2348 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Fri, 13 Feb 2026 14:24:51 +0100 Subject: [PATCH 43/78] Add dedicated rectangle oracel for AB agnostic BCs --- .../ab/reflection/agnosotic_reflection.py | 153 +++++++++++++----- 1 file changed, 112 insertions(+), 41 deletions(-) diff --git a/qlbm/components/ab/reflection/agnosotic_reflection.py b/qlbm/components/ab/reflection/agnosotic_reflection.py index 31169a4..a8cf35d 100644 --- a/qlbm/components/ab/reflection/agnosotic_reflection.py +++ b/qlbm/components/ab/reflection/agnosotic_reflection.py @@ -1,22 +1,25 @@ +from logging import Logger, getLogger +from time import perf_counter_ns +from typing import List, cast + +from qiskit import QuantumCircuit +from typing_extensions import override + from qlbm.components.ab.reflection.standard_reflection import ABReflectionOperator from qlbm.components.ab.streaming import ABStreamingOperator +from qlbm.components.base import LBMPrimitive +from qlbm.components.common.adders import ParameterizedDraperAdder +from qlbm.components.ms.primitives import Comparator, ComparatorMode +from qlbm.lattice.geometry.shapes.base import Shape from qlbm.lattice.geometry.shapes.block import Block +from qlbm.lattice.geometry.shapes.circle import Circle from qlbm.lattice.lattices.ab_lattice import ABLattice from qlbm.lattice.lattices.base import AmplitudeLattice from qlbm.lattice.spacetime.properties_base import LatticeDiscretization -from qlbm.tools.exceptions import LatticeException +from qlbm.tools.exceptions import CircuitException, LatticeException from qlbm.tools.utils import flatten -from qiskit import QuantumCircuit - - -from logging import Logger, getLogger -from time import perf_counter_ns -from typing import List, cast -from typing_extensions import override - - class ABZoneAgnosticReflectionOperator(ABReflectionOperator): lattice: AmplitudeLattice @@ -50,8 +53,6 @@ def __init__( @override def create_circuit(self) -> QuantumCircuit: - print("Ok!") - if self.lattice.discretization not in [LatticeDiscretization.D2Q9]: raise LatticeException("AB reflection only currently supported in D2Q9") circuit = self.lattice.circuit.copy() @@ -59,23 +60,12 @@ def create_circuit(self) -> QuantumCircuit: # 2-3. oracle for block in self.blocks: circuit.compose( - self.set_inside_wall_ancilla_state( - block, control_on_marker_state=False - ), + ABZoneAgnosticReflectionOracle( + self.lattice, block, logger=self.logger + ).circuit, inplace=True, ) - circuit.compose( - self.set_ancilla_of_point_state( - flatten( - [[(p, None) for p in block.corners_inside] for block in self.blocks] - ), - ignore_velocity_data=True, - control_on_marker_state=False, - ), - inplace=True, - ) - # 3-4. controlled permutation and stream circuit.compose(self.permute_and_stream(), inplace=True) @@ -88,23 +78,12 @@ def create_circuit(self) -> QuantumCircuit: # 5-6. oracle for block in self.blocks: circuit.compose( - self.set_inside_wall_ancilla_state( - block, control_on_marker_state=False - ), + ABZoneAgnosticReflectionOracle( + self.lattice, block, logger=self.logger + ).circuit, inplace=True, ) - circuit.compose( - self.set_ancilla_of_point_state( - flatten( - [[(p, None) for p in block.corners_inside] for block in self.blocks] - ), - ignore_velocity_data=True, - control_on_marker_state=False, - ), - inplace=True, - ) - # 6-7. uncontrolled regular stream circuit.compose( ABStreamingOperator(self.lattice, logger=self.logger).circuit, @@ -115,4 +94,96 @@ def create_circuit(self) -> QuantumCircuit: @override def __str__(self) -> str: - return f"[Operator ABZoneAgnosticReflection with lattice {self.lattice}]" \ No newline at end of file + return f"[Operator ABZoneAgnosticReflection with lattice {self.lattice}]" + + +class ABZoneAgnosticReflectionOracle(LBMPrimitive): + lattice: AmplitudeLattice + + def __init__( + self, + lattice: ABLattice, + shape: Shape, + logger: Logger = getLogger("qlbm"), + ) -> None: + super().__init__(logger) + + self.lattice = lattice + self.shape = shape + + self.logger.info(f"Creating circuit {str(self)}...") + circuit_creation_start_time = perf_counter_ns() + self.circuit = self.create_circuit() + self.logger.info( + f"Creating circuit {str(self)} took {perf_counter_ns() - circuit_creation_start_time} (ns)" + ) + + @override + def create_circuit(self) -> QuantumCircuit: + if isinstance(self.shape, Block): + return self.__create_circuit_block() + elif isinstance(self.shape, Circle): + return self.__create_circuit_circle() + + def __create_circuit_block(self) -> QuantumCircuit: + circuit = self.lattice.circuit.copy() + + block: Block = cast(Block, self.shape) + + for dim in range(self.lattice.num_dims): + circuit.compose( + ParameterizedDraperAdder( + len(self.lattice.grid_index(dim)), + block.bounds[dim][0], + positive=False, + ).circuit, + qubits=self.lattice.grid_index(dim), + inplace=True, + ) + + circuit.compose( + Comparator( + num_qubits=len(self.lattice.grid_index(dim)) + 1, + num_to_compare=block.bounds[dim][1] - block.bounds[dim][0], + mode=ComparatorMode.LE, + ).circuit, + qubits=self.lattice.grid_index(dim) + + [self.lattice.ancillae_comparator_index(0)[dim]], + inplace=True, + ) + + circuit.mcx( + self.lattice.ancillae_comparator_index(0)[: self.lattice.num_dims], + self.lattice.ancillae_obstacle_index()[0], + ) + + for dim in range(self.lattice.num_dims): + circuit.compose( + Comparator( + num_qubits=len(self.lattice.grid_index(dim)) + 1, + num_to_compare=block.bounds[dim][1] - block.bounds[dim][0], + mode=ComparatorMode.LE, + ).circuit, + qubits=self.lattice.grid_index(dim) + + [self.lattice.ancillae_comparator_index(0)[dim]], + inplace=True, + ) + + circuit.compose( + ParameterizedDraperAdder( + len(self.lattice.grid_index(dim)), + block.bounds[dim][0], + positive=True, + ).circuit, + qubits=self.lattice.grid_index(dim), + inplace=True, + ) + + return circuit + + def __create_circuit_circle(self) -> QuantumCircuit: + raise CircuitException("Not implemented") + + @override + def __str__(self) -> str: + return f"[Primitive ABZoneAgnosticReflectionOracle with lattice {self.lattice}, shape={self.shape}]" From c43749acac209ad229a9d76db50365869212cac9 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Fri, 13 Feb 2026 14:25:18 +0100 Subject: [PATCH 44/78] Sort imports --- .../ab/reflection/standard_reflection.py | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/qlbm/components/ab/reflection/standard_reflection.py b/qlbm/components/ab/reflection/standard_reflection.py index e8c4adf..488de6c 100644 --- a/qlbm/components/ab/reflection/standard_reflection.py +++ b/qlbm/components/ab/reflection/standard_reflection.py @@ -1,3 +1,12 @@ +from itertools import product +from logging import Logger, getLogger +from time import perf_counter_ns +from typing import List, Tuple, cast + +from qiskit import QuantumCircuit +from qiskit.circuit.library import MCMTGate, XGate +from typing_extensions import override + from qlbm.components.ab.encodings import ABEncodingType from qlbm.components.ab.reflection.common import ABReflectionPermutation from qlbm.components.ab.streaming import ABStreamingOperator @@ -12,17 +21,6 @@ from qlbm.tools.utils import flatten, get_qubits_to_invert -from qiskit import QuantumCircuit -from qiskit.circuit.library import MCMTGate, XGate - - -from itertools import product -from logging import Logger, getLogger -from time import perf_counter_ns -from typing import List, Tuple, cast -from typing_extensions import override - - class ABReflectionOperator(LBMOperator): """ Implements bounceback reflection in the amplitude-based encoding of :class:`.ABQLBM` for :math:`D_dQ_q` discretizations. From 241ff3c052dcb15f88cb7adf866af2062c130f21 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Fri, 13 Feb 2026 15:07:19 +0100 Subject: [PATCH 45/78] Add documentation and improve zone agnostic AB reflection oracle --- qlbm/components/__init__.py | 4 + qlbm/components/ab/__init__.py | 4 +- qlbm/components/ab/ab.py | 1 - qlbm/components/ab/reflection/__init__.py | 8 +- .../ab/reflection/agnosotic_reflection.py | 92 +++++++++++++++++-- qlbm/components/ab/reflection/common.py | 4 +- .../ab/reflection/standard_reflection.py | 3 + 7 files changed, 102 insertions(+), 14 deletions(-) diff --git a/qlbm/components/__init__.py b/qlbm/components/__init__.py index 91ae684..f448351 100644 --- a/qlbm/components/__init__.py +++ b/qlbm/components/__init__.py @@ -9,6 +9,8 @@ ABReflectionOperator, ABReflectionPermutation, ABStreamingOperator, + ABZoneAgnosticReflectionOperator, + ABZoneAgnosticReflectionOracle, ) from .base import ( LBMAlgorithm, @@ -92,4 +94,6 @@ "ABStreamingOperator", "ABDiscreteUniformInitialConditions", "ABParallelDiscreteUniformInitialConditions", + "ABZoneAgnosticReflectionOperator", + "ABZoneAgnosticReflectionOracle", ] diff --git a/qlbm/components/ab/__init__.py b/qlbm/components/ab/__init__.py index c246a42..ce5a98a 100644 --- a/qlbm/components/ab/__init__.py +++ b/qlbm/components/ab/__init__.py @@ -12,6 +12,7 @@ ABReflectionOperator, ABReflectionPermutation, ABZoneAgnosticReflectionOperator, + ABZoneAgnosticReflectionOracle, ) from .streaming import ABStreamingOperator from .utils import BinaryToOHPermutation @@ -27,5 +28,6 @@ "ABStreamingOperator", "ABEncodingType", "BinaryToOHPermutation", - "ABZoneAgnosticReflectionOperator" + "ABZoneAgnosticReflectionOperator", + "ABZoneAgnosticReflectionOracle", ] diff --git a/qlbm/components/ab/ab.py b/qlbm/components/ab/ab.py index 7f58ed1..1985c3f 100644 --- a/qlbm/components/ab/ab.py +++ b/qlbm/components/ab/ab.py @@ -7,7 +7,6 @@ from typing_extensions import override from qlbm.components.ab.reflection import ( - ABReflectionOperator, ABZoneAgnosticReflectionOperator, ) from qlbm.components.base import LBMAlgorithm diff --git a/qlbm/components/ab/reflection/__init__.py b/qlbm/components/ab/reflection/__init__.py index 3132dff..4b2a9e2 100644 --- a/qlbm/components/ab/reflection/__init__.py +++ b/qlbm/components/ab/reflection/__init__.py @@ -1,9 +1,15 @@ -from .agnosotic_reflection import ABZoneAgnosticReflectionOperator +"""Reflection for the :class:`.ABQLBM` algorithm.""" + +from .agnosotic_reflection import ( + ABZoneAgnosticReflectionOperator, + ABZoneAgnosticReflectionOracle, +) from .common import ABReflectionPermutation from .standard_reflection import ABReflectionOperator __all__ = [ "ABZoneAgnosticReflectionOperator", + "ABZoneAgnosticReflectionOracle", "ABReflectionPermutation", "ABReflectionOperator", ] diff --git a/qlbm/components/ab/reflection/agnosotic_reflection.py b/qlbm/components/ab/reflection/agnosotic_reflection.py index a8cf35d..2c30ad2 100644 --- a/qlbm/components/ab/reflection/agnosotic_reflection.py +++ b/qlbm/components/ab/reflection/agnosotic_reflection.py @@ -1,3 +1,5 @@ +"""Zone-agnostic reflection utilities for the :class:`.ABQLBM` algorithm.""" + from logging import Logger, getLogger from time import perf_counter_ns from typing import List, cast @@ -21,6 +23,38 @@ class ABZoneAgnosticReflectionOperator(ABReflectionOperator): + """ + Implements bounceback reflection in the amplitude-based encoding of :class:`.ABQLBM` for :math:`D_dQ_q` discretizations. + + Uses a zone-agnostic approach that relies on the existence of an oracle that marks the + basis states belonging to the inside of the solid geometry. + For more details on the oracle, see :class:`.ABZoneAgnosticReflectionOracle`. + + Example usage: + + .. code-block:: python + + from qlbm.components.ab import ABZoneAgnosticReflectionOperator + from qlbm.lattice import ABLattice + + lattice = ABLattice( + { + "lattice": {"dim": {"x": 4, "y": 4}, "velocities": "d2q9"}, + "geometry": [ + { + "shape": "cuboid", + "x": [1, 3], + "y": [1, 3], + "boundary": "bounceback", + } + ], + } + ) + + ABZoneAgnosticReflectionOperator(lattice, blocks=lattice.shapes["bounceback"]).draw("mpl") + + """ + lattice: AmplitudeLattice def __init__( @@ -57,15 +91,19 @@ def create_circuit(self) -> QuantumCircuit: raise LatticeException("AB reflection only currently supported in D2Q9") circuit = self.lattice.circuit.copy() - # 2-3. oracle + oracle = self.lattice.circuit.copy() + # build the oracle once for block in self.blocks: - circuit.compose( + oracle.compose( ABZoneAgnosticReflectionOracle( self.lattice, block, logger=self.logger ).circuit, inplace=True, ) + # 2-3. oracle + circuit.compose(oracle, inplace=True) + # 3-4. controlled permutation and stream circuit.compose(self.permute_and_stream(), inplace=True) @@ -76,13 +114,7 @@ def create_circuit(self) -> QuantumCircuit: ) # 5-6. oracle - for block in self.blocks: - circuit.compose( - ABZoneAgnosticReflectionOracle( - self.lattice, block, logger=self.logger - ).circuit, - inplace=True, - ) + circuit.compose(oracle, inplace=True) # 6-7. uncontrolled regular stream circuit.compose( @@ -98,11 +130,51 @@ def __str__(self) -> str: class ABZoneAgnosticReflectionOracle(LBMPrimitive): + r""" + Implementation of the oracle required for :class:`.ABZoneAgnosticReflectionOperator`. + + An oracle is an operator :math:`U_{\omega}` for an obstacle's region + :math:`\omega` such that, in the amplitude-based encoding, + :math:`U_\omega\ket{x}\ket{v}\ket{0}_\mathbb{o} = \ket{x}\ket{v}\ket{x \in \omega}_\mathbb{o}`. + Intuitively, the operator flips the object ancilla qubit if and only if the position :math:`x` + falls within the bounds of the object. + + Currently, the only available implementation is for 2D axis-aligned objects. + This is an improvement in asymptotic and practical complexity compared to + the methods described in :cite:`collisionless`. + This operation relies on basic arithmetic through the :class:`.ParameterizedDraperAdder` class + and comparison operation through the :class:`Comparator` circuits. + + Example usage: + + .. code-block:: python + + from qlbm.components.ab import ABZoneAgnosticReflectionOperator + from qlbm.lattice import ABLattice + + lattice = ABLattice( + { + "lattice": {"dim": {"x": 4, "y": 4}, "velocities": "d2q9"}, + "geometry": [ + { + "shape": "cuboid", + "x": [1, 3], + "y": [1, 3], + "boundary": "bounceback", + } + ], + } + ) + + ABZoneAgnosticReflectionOperator(lattice, blocks=lattice.shapes["bounceback"]).draw("mpl") + + """ + lattice: AmplitudeLattice def __init__( self, - lattice: ABLattice, + lattice: AmplitudeLattice, shape: Shape, logger: Logger = getLogger("qlbm"), ) -> None: diff --git a/qlbm/components/ab/reflection/common.py b/qlbm/components/ab/reflection/common.py index 5d3cf04..bc570dc 100644 --- a/qlbm/components/ab/reflection/common.py +++ b/qlbm/components/ab/reflection/common.py @@ -1,3 +1,5 @@ +"""Common utilities for reflection in the :class:`.ABQLBM` algorithm.""" + from logging import Logger, getLogger from time import perf_counter_ns @@ -111,4 +113,4 @@ def __create_circuit_d2q9(self): @override def __str__(self) -> str: - return f"[Primitive ABReflectionPermutation with {self.num_qubits} qubits on {self.discretization}]" \ No newline at end of file + return f"[Primitive ABReflectionPermutation with {self.num_qubits} qubits on {self.discretization}]" diff --git a/qlbm/components/ab/reflection/standard_reflection.py b/qlbm/components/ab/reflection/standard_reflection.py index 488de6c..2367122 100644 --- a/qlbm/components/ab/reflection/standard_reflection.py +++ b/qlbm/components/ab/reflection/standard_reflection.py @@ -1,3 +1,6 @@ +"""Reflection utilities for the :class:`.ABQLBM` algorithm; generalizations of :cite:`collisionless`.""" + + from itertools import product from logging import Logger, getLogger from time import perf_counter_ns From ddb3ea9a9e6d220b6b8ce1642c5e45301abfe530 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Fri, 13 Feb 2026 15:08:26 +0100 Subject: [PATCH 46/78] Add zone agnosit AB reflection to documentation website --- docs/source/code/comps_cqlbm.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/source/code/comps_cqlbm.rst b/docs/source/code/comps_cqlbm.rst index 05309fe..a6bc4b7 100644 --- a/docs/source/code/comps_cqlbm.rst +++ b/docs/source/code/comps_cqlbm.rst @@ -123,6 +123,10 @@ Reflection .. autoclass:: qlbm.components.ab.reflection.ABReflectionOperator +.. autoclass:: qlbm.components.ab.reflection.ABZoneAgnosticReflectionOperator + +.. autoclass:: qlbm.components.ab.reflection.ABZoneAgnosticReflectionOracle + .. _cqlbm_measurement: Measurement From 1c94e67199c8768ca4c3be378cba49fe22b815a1 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Wed, 18 Feb 2026 16:19:45 +0100 Subject: [PATCH 47/78] Make comparator modes parsable --- qlbm/components/ms/primitives.py | 19 +++++++++++++++++++ test/unit/comparator_mode_test.py | 20 ++++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 test/unit/comparator_mode_test.py diff --git a/qlbm/components/ms/primitives.py b/qlbm/components/ms/primitives.py index 06cef58..5eff644 100644 --- a/qlbm/components/ms/primitives.py +++ b/qlbm/components/ms/primitives.py @@ -13,6 +13,7 @@ from qlbm.lattice import MSLattice from qlbm.lattice.geometry.encodings.ms import ReflectionResetEdge from qlbm.tools import flatten +from qlbm.tools.exceptions import LatticeException class GridMeasurement(LBMPrimitive): @@ -283,6 +284,24 @@ class ComparatorMode(Enum): GT = (3,) GE = (4,) + @classmethod + def from_string(cls, mode: str) -> "ComparatorMode": + mode_map = { + "<": cls.LT, + "<=": cls.LE, + ">": cls.GT, + ">=": cls.GE, + } + + normalized_mode = mode.strip() + + try: + return mode_map[normalized_mode] + except KeyError as exc: + raise LatticeException( + f"Unsupported comparator mode '{mode}'. Expected one of: <, <=, >, >=." + ) from exc + class Comparator(LBMPrimitive): """ diff --git a/test/unit/comparator_mode_test.py b/test/unit/comparator_mode_test.py new file mode 100644 index 0000000..4af165f --- /dev/null +++ b/test/unit/comparator_mode_test.py @@ -0,0 +1,20 @@ +import pytest + +from qlbm.components.ms import ComparatorMode +from qlbm.tools.exceptions import LatticeException + + +def test_comparator_mode_from_string(): + assert ComparatorMode.from_string("<") == ComparatorMode.LT + assert ComparatorMode.from_string("<=") == ComparatorMode.LE + assert ComparatorMode.from_string(">") == ComparatorMode.GT + assert ComparatorMode.from_string(">=") == ComparatorMode.GE + + +def test_comparator_mode_from_string_whitespace(): + assert ComparatorMode.from_string(" < ") == ComparatorMode.LT + + +def test_comparator_mode_from_string_invalid(): + with pytest.raises(LatticeException, match="Unsupported comparator mode"): + ComparatorMode.from_string("!=") From b2a9b144c784cbad813e1d266bc7d89ca55f56fa Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Thu, 19 Feb 2026 14:36:57 +0100 Subject: [PATCH 48/78] Refactor and document ComparatorMode class --- qlbm/components/__init__.py | 3 +- .../ab/reflection/agnosotic_reflection.py | 4 +- qlbm/components/ms/__init__.py | 2 +- qlbm/components/ms/bounceback_reflection.py | 3 +- qlbm/components/ms/primitives.py | 38 +------------- qlbm/components/ms/specular_reflection.py | 3 +- .../spacetime/initial/volumetric.py | 4 +- .../spacetime/reflection/volumetric.py | 4 +- qlbm/tools/utils.py | 51 +++++++++++++++++++ test/unit/comparator_mode_test.py | 2 +- 10 files changed, 64 insertions(+), 50 deletions(-) diff --git a/qlbm/components/__init__.py b/qlbm/components/__init__.py index f448351..479a01c 100644 --- a/qlbm/components/__init__.py +++ b/qlbm/components/__init__.py @@ -1,5 +1,6 @@ """Modular and extendible quantum circuits that perform parts of the QLBM algorithm.""" +from ..tools.utils import ComparatorMode from .ab import ( ABQLBM, ABDiscreteUniformInitialConditions, @@ -46,7 +47,7 @@ MSStreamingOperator, SpecularReflectionOperator, ) -from .ms.primitives import Comparator, ComparatorMode +from .ms.primitives import Comparator from .ms.streaming import ( ControlledIncrementer, StreamingAncillaPreparation, diff --git a/qlbm/components/ab/reflection/agnosotic_reflection.py b/qlbm/components/ab/reflection/agnosotic_reflection.py index 2c30ad2..4fa7cec 100644 --- a/qlbm/components/ab/reflection/agnosotic_reflection.py +++ b/qlbm/components/ab/reflection/agnosotic_reflection.py @@ -11,7 +11,7 @@ from qlbm.components.ab.streaming import ABStreamingOperator from qlbm.components.base import LBMPrimitive from qlbm.components.common.adders import ParameterizedDraperAdder -from qlbm.components.ms.primitives import Comparator, ComparatorMode +from qlbm.components.ms.primitives import Comparator from qlbm.lattice.geometry.shapes.base import Shape from qlbm.lattice.geometry.shapes.block import Block from qlbm.lattice.geometry.shapes.circle import Circle @@ -19,7 +19,7 @@ from qlbm.lattice.lattices.base import AmplitudeLattice from qlbm.lattice.spacetime.properties_base import LatticeDiscretization from qlbm.tools.exceptions import CircuitException, LatticeException -from qlbm.tools.utils import flatten +from qlbm.tools.utils import ComparatorMode, flatten class ABZoneAgnosticReflectionOperator(ABReflectionOperator): diff --git a/qlbm/components/ms/__init__.py b/qlbm/components/ms/__init__.py index 7e21e94..4f0bb36 100644 --- a/qlbm/components/ms/__init__.py +++ b/qlbm/components/ms/__init__.py @@ -1,5 +1,6 @@ """Modular qlbm quantum circuit components for the MSQLBM algorithm :cite:p:`collisionless`.""" +from ...tools.utils import ComparatorMode from ..common.adders import ( ParameterizedDraperAdder, ParameterizedPhaseShift, @@ -12,7 +13,6 @@ from .msqlbm import MSQLBM from .primitives import ( Comparator, - ComparatorMode, EdgeComparator, GridMeasurement, MSInitialConditions, diff --git a/qlbm/components/ms/bounceback_reflection.py b/qlbm/components/ms/bounceback_reflection.py index 5acc32b..61245e9 100644 --- a/qlbm/components/ms/bounceback_reflection.py +++ b/qlbm/components/ms/bounceback_reflection.py @@ -11,7 +11,6 @@ from qlbm.components.base import LBMPrimitive, MSOperator from qlbm.components.ms.primitives import ( Comparator, - ComparatorMode, ) from qlbm.components.ms.specular_reflection import SpecularWallComparator from qlbm.lattice import MSLattice @@ -22,7 +21,7 @@ ) from qlbm.lattice.geometry.shapes.block import Block from qlbm.tools.exceptions import CircuitException -from qlbm.tools.utils import flatten +from qlbm.tools.utils import ComparatorMode, flatten from .primitives import EdgeComparator from .streaming import ControlledIncrementer diff --git a/qlbm/components/ms/primitives.py b/qlbm/components/ms/primitives.py index 5eff644..c696676 100644 --- a/qlbm/components/ms/primitives.py +++ b/qlbm/components/ms/primitives.py @@ -1,6 +1,5 @@ """Primitives for the implementation of the Collisionless Quantum Lattice Boltzmann Method introduced in :cite:t:`collisionless`.""" -from enum import Enum from logging import Logger, getLogger from time import perf_counter_ns from typing import List @@ -13,7 +12,7 @@ from qlbm.lattice import MSLattice from qlbm.lattice.geometry.encodings.ms import ReflectionResetEdge from qlbm.tools import flatten -from qlbm.tools.exceptions import LatticeException +from qlbm.tools.utils import ComparatorMode class GridMeasurement(LBMPrimitive): @@ -268,41 +267,6 @@ def __str__(self) -> str: return f"[Primitive InitialConditions with lattice {self.lattice}]" -class ComparatorMode(Enum): - r"""Enumerator for the modes of quantum comparator circuits. - - The modes are as follows: - - * (1, ``ComparatorMode.LT``, :math:`<`); - * (2, ``ComparatorMode.LE``, :math:`\leq`); - * (3, ``ComparatorMode.GT``, :math:`>`); - * (4, ``ComparatorMode.GE``, :math:`\geq`). - """ - - LT = (1,) - LE = (2,) - GT = (3,) - GE = (4,) - - @classmethod - def from_string(cls, mode: str) -> "ComparatorMode": - mode_map = { - "<": cls.LT, - "<=": cls.LE, - ">": cls.GT, - ">=": cls.GE, - } - - normalized_mode = mode.strip() - - try: - return mode_map[normalized_mode] - except KeyError as exc: - raise LatticeException( - f"Unsupported comparator mode '{mode}'. Expected one of: <, <=, >, >=." - ) from exc - - class Comparator(LBMPrimitive): """ Quantum comparator primitive that compares two a quantum state of ``num_qubits`` qubits and an integer ``num_to_compare`` with respect to a :class:`.ComparatorMode`. diff --git a/qlbm/components/ms/specular_reflection.py b/qlbm/components/ms/specular_reflection.py index 1930ad3..0ceeba5 100644 --- a/qlbm/components/ms/specular_reflection.py +++ b/qlbm/components/ms/specular_reflection.py @@ -11,7 +11,6 @@ from qlbm.components.base import LBMPrimitive, MSOperator from qlbm.components.ms.primitives import ( Comparator, - ComparatorMode, ) from qlbm.lattice import ( MSLattice, @@ -24,7 +23,7 @@ from qlbm.lattice.geometry.shapes.block import Block from qlbm.lattice.lattices.base import AmplitudeLattice from qlbm.tools.exceptions import CircuitException -from qlbm.tools.utils import flatten +from qlbm.tools.utils import ComparatorMode, flatten from .primitives import EdgeComparator from .streaming import ControlledIncrementer diff --git a/qlbm/components/spacetime/initial/volumetric.py b/qlbm/components/spacetime/initial/volumetric.py index b8f8868..73f6879 100644 --- a/qlbm/components/spacetime/initial/volumetric.py +++ b/qlbm/components/spacetime/initial/volumetric.py @@ -9,9 +9,9 @@ from typing_extensions import override from qlbm.components.base import LBMPrimitive -from qlbm.components.ms.primitives import Comparator, ComparatorMode +from qlbm.components.ms.primitives import Comparator from qlbm.lattice.lattices.spacetime_lattice import SpaceTimeLattice -from qlbm.tools.utils import flatten +from qlbm.tools.utils import ComparatorMode, flatten class VolumetricSpaceTimeInitialConditions(LBMPrimitive): diff --git a/qlbm/components/spacetime/reflection/volumetric.py b/qlbm/components/spacetime/reflection/volumetric.py index f0bcd86..2f53d45 100644 --- a/qlbm/components/spacetime/reflection/volumetric.py +++ b/qlbm/components/spacetime/reflection/volumetric.py @@ -8,11 +8,11 @@ from typing_extensions import override from qlbm.components.base import SpaceTimeOperator -from qlbm.components.ms.primitives import Comparator, ComparatorMode +from qlbm.components.ms.primitives import Comparator from qlbm.lattice.geometry.shapes.block import Block from qlbm.lattice.lattices.spacetime_lattice import SpaceTimeLattice from qlbm.tools.exceptions import CircuitException -from qlbm.tools.utils import flatten +from qlbm.tools.utils import ComparatorMode, flatten class VolumetricSpaceTimeReflectionOperator(SpaceTimeOperator): diff --git a/qlbm/tools/utils.py b/qlbm/tools/utils.py index 3a56be4..3c67f35 100644 --- a/qlbm/tools/utils.py +++ b/qlbm/tools/utils.py @@ -1,6 +1,7 @@ """General qlbm utilities.""" import re +from enum import Enum from math import pi from pathlib import Path from typing import List, Tuple @@ -13,6 +14,8 @@ from qulacs import QuantumCircuit as QulacsQC from qulacs.converter import convert_QASM_to_qulacs_circuit +from qlbm.tools.exceptions import LatticeException + def create_directory_and_parents(directory: str) -> None: """ @@ -272,3 +275,51 @@ def get_qubits_to_invert(number_encoded: int, num_qubits: int) -> List[int]: The indices of the (qu)bits that have value 0. """ return [i for i in range(num_qubits) if not bit_value(number_encoded, i)] + + +class ComparatorMode(Enum): + r"""Enumerator for the modes of quantum comparator circuits. + + The modes are as follows: + + * (1, ``ComparatorMode.LT``, :math:`<`); + * (2, ``ComparatorMode.LE``, :math:`\leq`); + * (3, ``ComparatorMode.GT``, :math:`>`); + * (4, ``ComparatorMode.GE``, :math:`\geq`). + """ + + LT = (1,) + LE = (2,) + GT = (3,) + GE = (4,) + + @classmethod + def from_string(cls, mode: str) -> "ComparatorMode": + """ + Parses inequality strings to :class:`.ComparatorMode` objects. + + Parameters + ---------- + mode : str + One of ">=", ">", "<=", and "<". + + Returns + ------- + ComparatorMode + The :class:`ComparatorMode` representing the inequality + """ + mode_map = { + "<": cls.LT, + "<=": cls.LE, + ">": cls.GT, + ">=": cls.GE, + } + + normalized_mode = mode.strip() + + try: + return mode_map[normalized_mode] + except KeyError as exc: + raise LatticeException( + f"Unsupported comparator mode '{mode}'. Expected one of: <, <=, >, >=." + ) from exc diff --git a/test/unit/comparator_mode_test.py b/test/unit/comparator_mode_test.py index 4af165f..0229da1 100644 --- a/test/unit/comparator_mode_test.py +++ b/test/unit/comparator_mode_test.py @@ -1,6 +1,6 @@ import pytest -from qlbm.components.ms import ComparatorMode +from qlbm.tools.utils import ComparatorMode from qlbm.tools.exceptions import LatticeException From 3578e211f9d39e5980fc0ec7aee100de10707531 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Thu, 19 Feb 2026 15:40:42 +0100 Subject: [PATCH 49/78] Add string export to comparator mode --- qlbm/tools/utils.py | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/qlbm/tools/utils.py b/qlbm/tools/utils.py index 3c67f35..c7d5b73 100644 --- a/qlbm/tools/utils.py +++ b/qlbm/tools/utils.py @@ -3,8 +3,9 @@ import re from enum import Enum from math import pi +from operator import ge, gt, le, lt from pathlib import Path -from typing import List, Tuple +from typing import Callable, List, Tuple import numpy as np from pytket.extensions.qiskit import qiskit_to_tk @@ -323,3 +324,38 @@ def from_string(cls, mode: str) -> "ComparatorMode": raise LatticeException( f"Unsupported comparator mode '{mode}'. Expected one of: <, <=, >, >=." ) from exc + + def to_string(self) -> str: + """ + Get the string representation of this object. + + Returns + ------- + str + One of "<", "<=", ">", ">=". + """ + comparator_strings = { + ComparatorMode.LT: "<", + ComparatorMode.LE: "<=", + ComparatorMode.GT: ">", + ComparatorMode.GE: ">=", + } + + return comparator_strings[self] + + def to_operator(self) -> Callable[[int, int], bool]: + """ + Get the Python comparison operator represented by this mode. + + Returns + ------- + Callable[[int, int], bool] + The function taking to integers and returning a boolean representing the comparison of the integers. + """ + comparator_operations = { + ComparatorMode.LT: lt, + ComparatorMode.LE: le, + ComparatorMode.GT: gt, + ComparatorMode.GE: ge, + } + return comparator_operations[self] From 903fe4db9d28a1796b1c21e6b289a56959cb9cda Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Thu, 19 Feb 2026 15:41:11 +0100 Subject: [PATCH 50/78] Add draft implementation of ymonomial BCs for ABQLBM --- qlbm/lattice/geometry/shapes/ymonomial.py | 91 +++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 qlbm/lattice/geometry/shapes/ymonomial.py diff --git a/qlbm/lattice/geometry/shapes/ymonomial.py b/qlbm/lattice/geometry/shapes/ymonomial.py new file mode 100644 index 0000000..610bb6c --- /dev/null +++ b/qlbm/lattice/geometry/shapes/ymonomial.py @@ -0,0 +1,91 @@ +"""Base classes for geometrical shapes.""" + +from json import dumps +from typing import Dict, List, override + +import numpy as np +from stl import mesh + +from qlbm.tools.utils import ComparatorMode + +from .base import Shape + + +class YMonomial(Shape): + """Base class for all geometrical shapes.""" + + def __init__( + self, + num_grid_qubits: List[int], + boundary_condition: str, + exponent: int, + comparator_mode: ComparatorMode, + ): + super().__init__(num_grid_qubits, boundary_condition) + + self.num_grid_qubits = num_grid_qubits + self.boundary_condition = boundary_condition + self.num_dims = len(num_grid_qubits) + # The number of qubits used to offset "higher" dimensions + self.previous_qubits: List[int] = [ + sum(num_grid_qubits[previous_dim] for previous_dim in range(dim)) + for dim in range(self.num_dims) + ] + self.comparator_mode = comparator_mode + + self.exponent = exponent + self.boundary_points: List[List[bool]] = [ + [ + ComparatorMode.to_operator(comparator_mode)(y, x**exponent) + for x in range(2 ** num_grid_qubits[0]) + ] + for y in range(2 ** num_grid_qubits[1]) + ] + + @override + def stl_mesh(self) -> mesh.Mesh: + """ + Provides the ``stl`` representation of the shape. + + Returns + ------- + ``stl.mesh.Mesh`` + The mesh representing the shape. + """ + triangles: List[np.ndarray] = [] + + for y, row in enumerate(self.boundary_points): + for x, is_inside in enumerate(row): + if not is_inside: + continue + + v00 = np.array([x, y, 1.0]) + v10 = np.array([x + 1, y, 1.0]) + v01 = np.array([x, y + 1, 1.0]) + v11 = np.array([x + 1, y + 1, 1.0]) + + triangles.append(np.array([v00, v10, v11])) + triangles.append(np.array([v00, v11, v01])) + + ymonomial_mesh = mesh.Mesh(np.zeros(len(triangles), dtype=mesh.Mesh.dtype)) + for i, triangle in enumerate(triangles): + ymonomial_mesh.vectors[i] = triangle + + return ymonomial_mesh + + @override + def to_json(self) -> str: + return dumps(self.to_dict()) + + @override + def name(self) -> str: + return "ymonomial" + + @override + def to_dict(self) -> Dict[str, int | str]: + return { + "shape": self.name(), + "exponent": self.exponent, + "comparator": self.comparator_mode.to_string(), + "boundary": self.boundary_condition, + } From 3e5d0220d63cede99419846745d9ce99e27e0ea9 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Thu, 19 Feb 2026 16:14:56 +0100 Subject: [PATCH 51/78] Add parsing utilities for ymonomial shape --- qlbm/lattice/geometry/shapes/base.py | 24 +- qlbm/lattice/lattices/base.py | 51 +++- test/unit/lattice/ms_lattice_test.py | 2 +- .../lattice/ymonomial_lattice_parser_test.py | 273 ++++++++++++++++++ 4 files changed, 334 insertions(+), 16 deletions(-) create mode 100644 test/unit/lattice/ymonomial_lattice_parser_test.py diff --git a/qlbm/lattice/geometry/shapes/base.py b/qlbm/lattice/geometry/shapes/base.py index 8bac33d..30f417c 100644 --- a/qlbm/lattice/geometry/shapes/base.py +++ b/qlbm/lattice/geometry/shapes/base.py @@ -65,6 +65,18 @@ def to_dict(self): """ pass + @abstractmethod + def name(self) -> str: + """ + The name of the shape. + + Returns + ------- + str + The name of the shape. + """ + pass + class LQLGAShape(Shape): """Base class for all shapes compatible with the :class:`.LQLGA` algorithm.""" @@ -326,15 +338,3 @@ def get_d2q4_volumetric_reflection_data( The information encoding the reflections to be performed. """ pass - - @abstractmethod - def name(self) -> str: - """ - The name of the shape. - - Returns - ------- - str - The name of the shape. - """ - pass diff --git a/qlbm/lattice/lattices/base.py b/qlbm/lattice/lattices/base.py index a01d373..277a27b 100644 --- a/qlbm/lattice/lattices/base.py +++ b/qlbm/lattice/lattices/base.py @@ -11,12 +11,13 @@ from qlbm.lattice.geometry.shapes.base import Shape from qlbm.lattice.geometry.shapes.block import Block from qlbm.lattice.geometry.shapes.circle import Circle +from qlbm.lattice.geometry.shapes.ymonomial import YMonomial from qlbm.lattice.spacetime.properties_base import ( LatticeDiscretization, LatticeDiscretizationProperties, ) from qlbm.tools.exceptions import LatticeException -from qlbm.tools.utils import dimension_letter, flatten +from qlbm.tools.utils import ComparatorMode, dimension_letter, flatten class Lattice(ABC): @@ -356,9 +357,9 @@ def parse_geometry_dict(self, geometry_list) -> Dict[str, List[Shape]]: f"Obstacle {c + 1} specification includes no shape." ) - if obstacle_dict["shape"] not in ["cuboid", "sphere"]: + if obstacle_dict["shape"] not in ["cuboid", "sphere", "ymonomial"]: raise LatticeException( - f'Obstacle {c + 1} has unsupported shape "{obstacle_dict["shape"]}". Supported shapes are cuboid and sphere.' + f'Obstacle {c + 1} has unsupported shape "{obstacle_dict["shape"]}". Supported shapes are cuboid, sphere, and ymonomial.' ) # Parsing blocks if obstacle_dict["shape"] == "cuboid": @@ -434,6 +435,50 @@ def parse_geometry_dict(self, geometry_list) -> Dict[str, List[Shape]]: obstacle_dict["boundary"], # type: ignore ) ) + elif obstacle_dict["shape"] == "ymonomial": + if self.num_dims != 2: + raise LatticeException( + f"Obstacle {c + 1}: ymonomial is only supported for 2-dimensional lattices." + ) + + if "exponent" not in obstacle_dict: + raise LatticeException( + f"Obstacle {c + 1}: ymonomial obstacle does not specify an exponent." + ) + + try: + exponent = int(obstacle_dict["exponent"]) + except (ValueError, TypeError): + raise LatticeException( + f"Obstacle {c + 1}: ymonomial exponent {obstacle_dict['exponent']} is not an integer." + ) + + if exponent < 0: + raise LatticeException( + f"Obstacle {c + 1}: ymonomial exponent {obstacle_dict['exponent']} must be non-negative." + ) + + if "comparator" not in obstacle_dict: + raise LatticeException( + f"Obstacle {c + 1}: ymonomial obstacle does not specify a comparator." + ) + + if not isinstance(obstacle_dict["comparator"], str): + raise LatticeException( + f"Obstacle {c + 1}: ymonomial comparator must be a string." + ) + + parsed_obstacles[obstacle_dict["boundary"]].append( # type: ignore + YMonomial( + [ + (self.num_gridpoints[numeric_dim_index]).bit_length() + for numeric_dim_index in range(self.num_dims) + ], + obstacle_dict["boundary"], # type: ignore + exponent, + ComparatorMode.from_string(obstacle_dict["comparator"]), + ) + ) return parsed_obstacles diff --git a/test/unit/lattice/ms_lattice_test.py b/test/unit/lattice/ms_lattice_test.py index fd4d850..c3205cc 100644 --- a/test/unit/lattice/ms_lattice_test.py +++ b/test/unit/lattice/ms_lattice_test.py @@ -399,7 +399,7 @@ def test_lattice_exception_unsupported_shape(): ) assert ( - 'Obstacle 1 has unsupported shape "cuboidz". Supported shapes are cuboid and sphere.' + 'Obstacle 1 has unsupported shape "cuboidz". Supported shapes are cuboid, sphere, and ymonomial.' == str(excinfo.value) ) diff --git a/test/unit/lattice/ymonomial_lattice_parser_test.py b/test/unit/lattice/ymonomial_lattice_parser_test.py new file mode 100644 index 0000000..ed6b33f --- /dev/null +++ b/test/unit/lattice/ymonomial_lattice_parser_test.py @@ -0,0 +1,273 @@ +"""Unit tests for ymonomial geometry parsing in lattice base parser.""" + +import json + +import pytest + +from qlbm.lattice import MSLattice +from qlbm.lattice.geometry.shapes.block import Block +from qlbm.lattice.geometry.shapes.ymonomial import YMonomial +from qlbm.tools.exceptions import LatticeException +from qlbm.tools.utils import ComparatorMode + + +def _lattice_with_geometry(geometry): + """Build a minimal valid 2D MS lattice with a configurable geometry list.""" + return MSLattice( + { + "lattice": { + "dim": {"x": 16, "y": 16}, + "velocities": {"x": 4, "y": 4}, + }, + "geometry": geometry, + } + ) + + +def test_parse_ymonomial_specular_geometry(): + """Parses a specular ymonomial obstacle and stores the expected mode.""" + lattice = _lattice_with_geometry( + [ + { + "shape": "ymonomial", + "exponent": 2, + "comparator": "<=", + "boundary": "specular", + } + ] + ) + + assert len(lattice.shapes["specular"]) == 1 + shape = lattice.shapes["specular"][0] + assert isinstance(shape, YMonomial) + assert shape.exponent == 2 + assert shape.comparator_mode == ComparatorMode.LE + + +def test_parse_ymonomial_bounceback_geometry(): + """Parses a bounceback ymonomial obstacle and stores the expected mode.""" + lattice = _lattice_with_geometry( + [ + { + "shape": "ymonomial", + "exponent": 3, + "comparator": ">", + "boundary": "bounceback", + } + ] + ) + + assert len(lattice.shapes["bounceback"]) == 1 + shape = lattice.shapes["bounceback"][0] + assert isinstance(shape, YMonomial) + assert shape.comparator_mode == ComparatorMode.GT + + +@pytest.mark.parametrize( + "comparator_symbol, expected_mode", + [ + ("<", ComparatorMode.LT), + ("<=", ComparatorMode.LE), + (">", ComparatorMode.GT), + (">=", ComparatorMode.GE), + ], +) +def test_parse_ymonomial_comparator_modes(comparator_symbol, expected_mode): + """Parses each supported comparator symbol into the expected mode.""" + lattice = _lattice_with_geometry( + [ + { + "shape": "ymonomial", + "exponent": 1, + "comparator": comparator_symbol, + "boundary": "specular", + } + ] + ) + + ymonomial_shape = lattice.shapes["specular"][0] + assert isinstance(ymonomial_shape, YMonomial) + assert ymonomial_shape.comparator_mode == expected_mode + + +def test_parse_ymonomial_and_cuboid_geometry_together(): + """Keeps cuboid parsing intact while adding ymonomial parsing.""" + lattice = _lattice_with_geometry( + [ + {"shape": "cuboid", "x": [4, 6], "y": [3, 5], "boundary": "specular"}, + { + "shape": "ymonomial", + "exponent": 2, + "comparator": "<=", + "boundary": "bounceback", + }, + ] + ) + + assert len(lattice.shapes["specular"]) == 1 + assert len(lattice.shapes["bounceback"]) == 1 + assert isinstance(lattice.shapes["specular"][0], Block) + assert isinstance(lattice.shapes["bounceback"][0], YMonomial) + + +def test_ymonomial_to_json_roundtrip_shape_and_parameters(): + """Serializes parsed ymonomial geometry back with expected parameters.""" + lattice = _lattice_with_geometry( + [ + { + "shape": "ymonomial", + "exponent": 4, + "comparator": ">=", + "boundary": "specular", + } + ] + ) + + geometry = json.loads(lattice.to_json())["geometry"] + + assert len(geometry) == 1 + assert geometry[0]["shape"] == "ymonomial" + assert geometry[0]["exponent"] == 4 + assert geometry[0]["comparator"] == ">=" + assert geometry[0]["boundary"] == "specular" + + +def test_lattice_exception_ymonomial_missing_exponent(): + """Raises an informative exception when ymonomial exponent is missing.""" + with pytest.raises(LatticeException) as excinfo: + _lattice_with_geometry( + [ + { + "shape": "ymonomial", + "comparator": "<=", + "boundary": "specular", + } + ] + ) + + assert ( + "Obstacle 1: ymonomial obstacle does not specify an exponent." + == str(excinfo.value) + ) + + +def test_lattice_exception_ymonomial_non_integer_exponent(): + """Raises an informative exception when ymonomial exponent is non-integer.""" + with pytest.raises(LatticeException) as excinfo: + _lattice_with_geometry( + [ + { + "shape": "ymonomial", + "exponent": "abc", + "comparator": "<=", + "boundary": "specular", + } + ] + ) + + assert ( + "Obstacle 1: ymonomial exponent abc is not an integer." + == str(excinfo.value) + ) + + +def test_lattice_exception_ymonomial_negative_exponent(): + """Raises an informative exception when ymonomial exponent is negative.""" + with pytest.raises(LatticeException) as excinfo: + _lattice_with_geometry( + [ + { + "shape": "ymonomial", + "exponent": -1, + "comparator": "<=", + "boundary": "specular", + } + ] + ) + + assert ( + "Obstacle 1: ymonomial exponent -1 must be non-negative." + == str(excinfo.value) + ) + + +def test_lattice_exception_ymonomial_missing_comparator(): + """Raises an informative exception when ymonomial comparator is missing.""" + with pytest.raises(LatticeException) as excinfo: + _lattice_with_geometry( + [ + { + "shape": "ymonomial", + "exponent": 2, + "boundary": "specular", + } + ] + ) + + assert ( + "Obstacle 1: ymonomial obstacle does not specify a comparator." + == str(excinfo.value) + ) + + +def test_lattice_exception_ymonomial_non_string_comparator(): + """Raises an informative exception when ymonomial comparator is not a string.""" + with pytest.raises(LatticeException) as excinfo: + _lattice_with_geometry( + [ + { + "shape": "ymonomial", + "exponent": 2, + "comparator": 123, + "boundary": "specular", + } + ] + ) + + assert "Obstacle 1: ymonomial comparator must be a string." == str(excinfo.value) + + +def test_lattice_exception_ymonomial_invalid_comparator_symbol(): + """Propagates comparator symbol validation for unsupported operators.""" + with pytest.raises(LatticeException) as excinfo: + _lattice_with_geometry( + [ + { + "shape": "ymonomial", + "exponent": 2, + "comparator": "!=", + "boundary": "specular", + } + ] + ) + + assert ( + "Unsupported comparator mode '!='. Expected one of: <, <=, >, >=." + == str(excinfo.value) + ) + + +def test_lattice_exception_ymonomial_in_1d_lattice(): + """Raises an informative exception when ymonomial is used outside 2D.""" + with pytest.raises(LatticeException) as excinfo: + MSLattice( + { + "lattice": { + "dim": {"x": 16}, + "velocities": {"x": 4}, + }, + "geometry": [ + { + "shape": "ymonomial", + "exponent": 2, + "comparator": "<=", + "boundary": "specular", + } + ], + } + ) + + assert ( + "Obstacle 1: ymonomial is only supported for 2-dimensional lattices." + == str(excinfo.value) + ) \ No newline at end of file From 40a75924d7037c538dc2389bd4b279271dfdd231 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Thu, 19 Feb 2026 23:07:03 +0100 Subject: [PATCH 52/78] Add monomial BC parsing to lattice --- qlbm/lattice/lattices/ab_lattice.py | 227 ++++++++++++++++++++++++---- qlbm/lattice/lattices/base.py | 26 ++-- 2 files changed, 208 insertions(+), 45 deletions(-) diff --git a/qlbm/lattice/lattices/ab_lattice.py b/qlbm/lattice/lattices/ab_lattice.py index e85e823..df9074e 100644 --- a/qlbm/lattice/lattices/ab_lattice.py +++ b/qlbm/lattice/lattices/ab_lattice.py @@ -1,7 +1,7 @@ """Implementation of the Amplitude-Based (AB) encoding lattice for generic DdQq discretizations.""" from logging import getLogger -from typing import Dict, List, Tuple +from typing import Dict, List, Tuple, cast from numpy import ceil, log2 from qiskit import QuantumCircuit, QuantumRegister @@ -9,6 +9,7 @@ from qlbm.components.ab.encodings import ABEncodingType from qlbm.lattice.geometry.shapes.base import Shape +from qlbm.lattice.geometry.shapes.ymonomial import YMonomial from qlbm.lattice.spacetime.properties_base import ( LatticeDiscretization, LatticeDiscretizationProperties, @@ -113,6 +114,15 @@ class ABLattice(AmplitudeLattice): num_base_qubits: int """The number of qubits required to represent the lattice.""" + num_monomial_qubits: int + r"""The number of qubits used for the imposition of monomially-shaped BCs. + Currently, only the :class:`.YMonomial` is supported. + The number of qubits for a monomial with exponent :math:`n` + is :math:`(n+1)\lceil \log_2 N_{g_x}\rceil`. + The first :math:`\log_2 N_{g_x}\rceil`-sized register is allocated + to making a copy, while the other :math:`n` chunks + are allotted to the computation of the monomial via quantum arithmetic.""" + registers: Tuple[QuantumRegister, ...] """The registers of the lattice.""" @@ -144,7 +154,8 @@ def __init__( self.num_base_qubits = self.num_grid_qubits + self.num_velocity_qubits self.num_obstacle_qubits = self.__num_obstacle_qubits() - self.num_comparator_qubits = 2 * (self.num_dims - 1) + self.num_monomial_qubits = self.__num_monomial_qubits() + self.num_comparator_qubits = self.__num_comparator_qubits() self.num_ancilla_qubits = self.num_comparator_qubits + self.num_obstacle_qubits self.num_marker_qubits = ( @@ -158,24 +169,48 @@ def __init__( ) self.num_accumulation_qubits = 0 + self.shape_list = flatten(self.__geometry_shape_lists()) self.__update_registers() def __update_registers(self): + self.num_obstacle_qubits = self.__num_obstacle_qubits() + self.num_monomial_qubits = self.__num_monomial_qubits() + self.num_comparator_qubits = self.__num_comparator_qubits() + self.num_ancilla_qubits = self.num_comparator_qubits + self.num_obstacle_qubits + self.num_total_qubits = ( self.num_base_qubits + self.num_ancilla_qubits + self.num_marker_qubits ) temp_registers = self.get_registers() - ( - self.grid_registers, - self.velocity_registers, - self.ancilla_comparator_register, - self.ancilla_object_register, - self.marker_register, - self.accumulation_register, - ) = temp_registers + if len(temp_registers) == 8: + ( + self.grid_registers, + self.velocity_registers, + self.ancilla_comparator_register, + self.ancilla_object_register, + self.marker_register, + self.accumulation_register, + self.copy_register, + self.monomial_register, + ) = temp_registers + elif len(temp_registers) == 6: + ( + self.grid_registers, + self.velocity_registers, + self.ancilla_comparator_register, + self.ancilla_object_register, + self.marker_register, + self.accumulation_register, + ) = temp_registers + self.copy_register = [] + self.monomial_register = [] + else: + raise LatticeException( + "Invalid register tuple returned by get_registers()." + ) self.registers = tuple(flatten(temp_registers)) @@ -231,6 +266,7 @@ def set_geometries(self, geometries): if len(self.geometries) == 1: # Remove this in the future... self.shapes = self.geometries[0] + self.shape_list = flatten(self.__geometry_shape_lists()) self.num_marker_qubits = ( int(ceil(log2(len(self.geometries)))) @@ -276,11 +312,18 @@ def velocity_index(self, dim: int | None = None) -> List[int]: @override def ancillae_comparator_index(self, index: int | None = None) -> List[int]: + if self.num_comparator_qubits == 0: + if index is None: + return [] + raise LatticeException( + "Cannot index ancilla comparator register because this lattice has no comparator qubits." + ) + if index is None: return list( range( self.num_base_qubits, - self.num_base_qubits + 2 * (self.num_dims - 1), + self.num_base_qubits + self.num_comparator_qubits, ) ) @@ -289,12 +332,22 @@ def ancillae_comparator_index(self, index: int | None = None) -> List[int]: f"Cannot index ancilla comparator register for index {index} in {self.num_dims}-dimensional lattice. Maximum is {self.num_dims - 2}." ) - return list( - range( - self.num_base_qubits, self.num_base_qubits + self.num_comparator_qubits - ) + qubits_per_dim = ( + 2 if self.num_comparator_qubits >= 2 * (self.num_dims - 1) else 1 + ) + previous_qubits = self.num_base_qubits + qubits_per_dim * index + final_qubit = min( + self.num_base_qubits + self.num_comparator_qubits, + previous_qubits + qubits_per_dim, ) + if previous_qubits >= final_qubit: + raise LatticeException( + f"Cannot index ancilla comparator register for index {index} in {self.num_dims}-dimensional lattice. Maximum is {self.num_dims - 2}." + ) + + return list(range(previous_qubits, final_qubit)) + @override def ancillae_obstacle_index(self, index: int | None = None) -> List[int]: if index is None: @@ -314,22 +367,112 @@ def ancillae_obstacle_index(self, index: int | None = None) -> List[int]: return [self.num_base_qubits + self.num_comparator_qubits + index] + def ancillae_copy_index(self) -> List[int]: + """ + Gets the index of the ancilla qubits used to copy the state of the x grid qubits. + + Returns + ------- + List[int] + The indices of the copy register qubits. + """ + if self.num_monomial_qubits == 0: + raise LatticeException( + "This lattice does not have any copy register qubits." + ) + + return list( + range( + self.num_base_qubits + + self.num_comparator_qubits + + self.num_obstacle_qubits, + self.num_base_qubits + + self.num_comparator_qubits + + self.num_obstacle_qubits + + self.num_gridpoints[0].bit_length(), + ) + ) + + def ancillae_monomial_index(self) -> List[int]: + """ + Gets the index of the ancilla qubits used to compute the function of the x register for BC purposes. + + Returns + ------- + List[int] + The indices of the monomial register qubits. + """ + if self.num_monomial_qubits == 0: + raise LatticeException("This lattice does not have any monomial BC qubits.") + + return list( + range( + self.num_base_qubits + + self.num_comparator_qubits + + self.num_obstacle_qubits + + self.num_gridpoints[0].bit_length(), + self.num_base_qubits + + self.num_comparator_qubits + + self.num_obstacle_qubits + + self.num_monomial_qubits, + ) + ) + def __num_obstacle_qubits(self) -> int: - all_obstacle_bounceback: bool = len( + return max( [ - b - for b in flatten(list(self.shapes.values())) - if b.boundary_condition == "bounceback" + ( + 1 + if len( + [ + shape + for shape in geometry_shapes + if shape.boundary_condition == "bounceback" + ] + ) + == len(geometry_shapes) + else self.num_dims + ) + for geometry_shapes in self.__geometry_shape_lists() ] - ) == len(flatten(list(self.shapes.values()))) - if all_obstacle_bounceback: - # A single qubit suffices to determine - # Whether particles have streamed inside the object - return 1 - # If there is at least one object with specular reflection - # 2 ancilla qubits are required for velocity inversion - else: - return self.num_dims + + [1] + ) + + def __num_comparator_qubits(self) -> int: + comp_qs_cuboids = ( + 2 * (self.num_dims - 1) + if any( + shape.name() == "cuboid" + for shape in flatten(self.__geometry_shape_lists()) + ) + else 0 + ) + comp_qs_monomials = ( + 1 + if any( + shape.name() == "ymonomial" + for shape in flatten(self.__geometry_shape_lists()) + ) + else 0 + ) + return max(comp_qs_cuboids, comp_qs_monomials) + + def __num_monomial_qubits(self) -> int: + monomial_shapes_exponent = [ + cast(YMonomial, x).exponent + for x in flatten(self.__geometry_shape_lists()) + if x.name() == "ymonomial" + ] + # ! This only works for the y monomial example + return ( + 0 + if not monomial_shapes_exponent + else (max(monomial_shapes_exponent) + 1) + * self.num_gridpoints[0].bit_length() + ) + + def __geometry_shape_lists(self) -> List[List[Shape]]: + return [flatten(list(geometry.values())) for geometry in self.geometries] @override def get_registers(self) -> Tuple[List[QuantumRegister], ...]: @@ -339,7 +482,8 @@ def get_registers(self) -> Tuple[List[QuantumRegister], ...]: (i) the logarithmically compressed grid, (ii) the logarithmically compressed discrete velocities, (iii) the comparator qubits, - (iv) the object qubits. + (iv) the object qubit(s), + (v) the monomial BC qubits, if present. Returns ------- @@ -352,9 +496,11 @@ def get_registers(self) -> Tuple[List[QuantumRegister], ...]: ] # 2(d-1) ancilla qubits - ancilla_comparator_register = [ - QuantumRegister(self.num_comparator_qubits, name="a_c") - ] + ancilla_comparator_register = ( + [QuantumRegister(self.num_comparator_qubits, name="a_c")] + if self.num_comparator_qubits > 0 + else [] + ) # Velocity qubits velocity_registers = [QuantumRegister(self.num_velocity_qubits, name="v")] @@ -365,6 +511,21 @@ def get_registers(self) -> Tuple[List[QuantumRegister], ...]: for c, gp in enumerate(self.num_gridpoints) ] + # Monomial qubits + # ! Only works for Ymonomials + copy_register = ( + [QuantumRegister(self.num_gridpoints[0].bit_length(), name="a_copy")] + if self.num_monomial_qubits > 0 + else [] + ) + + # ! Only works for Ymonomials + monomial_register = ( + [QuantumRegister(self.num_monomial_qubits, name="monomial")] + if self.num_monomial_qubits > 0 + else [] + ) + if self.has_multiple_geometries(): marker_register = [ QuantumRegister( @@ -395,6 +556,8 @@ def get_registers(self) -> Tuple[List[QuantumRegister], ...]: ancilla_object_register, marker_register, accumulation_register, + copy_register, + monomial_register, ) @override diff --git a/qlbm/lattice/lattices/base.py b/qlbm/lattice/lattices/base.py index 277a27b..09abe2c 100644 --- a/qlbm/lattice/lattices/base.py +++ b/qlbm/lattice/lattices/base.py @@ -258,14 +258,12 @@ def __parse_input_dict( # Set for access to the geometry parsing utilities self.num_dims = num_dimensions - grid_list: List[int] = [ + self.num_gridpoints: List[int] = [ # -1 because the bit_length() would "overshoot" for powers of 2 lattice_dict["dim"][dimension_letter(dim)] - 1 for dim in range(num_dimensions) ] - self.num_gridpoints = grid_list - discretization: LatticeDiscretization = LatticeDiscretization.CFLDISCRETIZATION velocity_list: List[int] = [] @@ -312,7 +310,7 @@ def __parse_input_dict( if "geometry" not in input_dict: return ( - grid_list, + self.num_gridpoints, velocity_list, {"specular": [], "bounceback": []}, discretization, @@ -322,7 +320,7 @@ def __parse_input_dict( parsed_obstacles = self.parse_geometry_dict(geometry_list) - return grid_list, velocity_list, parsed_obstacles, discretization + return self.num_gridpoints, velocity_list, parsed_obstacles, discretization def parse_geometry_dict(self, geometry_list) -> Dict[str, List[Shape]]: """ @@ -497,14 +495,16 @@ def to_json(self) -> str: dimension_letter(dim): self.num_gridpoints[dim] + 1 for dim in range(self.num_dims) }, - "velocities": { - dimension_letter(dim): self.num_velocities[dim] + 1 - for dim in range(self.num_dims) - } - if self.discretization == LatticeDiscretization.CFLDISCRETIZATION - else LatticeDiscretizationProperties.string_representation[ - self.discretization - ], # type: ignore + "velocities": ( + { + dimension_letter(dim): self.num_velocities[dim] + 1 + for dim in range(self.num_dims) + } + if self.discretization == LatticeDiscretization.CFLDISCRETIZATION + else LatticeDiscretizationProperties.string_representation[ + self.discretization + ] + ), # type: ignore }, } From 4d010805aef67d5345abcb81992c078a27fbf368 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Thu, 19 Feb 2026 23:07:23 +0100 Subject: [PATCH 53/78] Update lattice tests --- .../lattice/abe_lattice_properties_test.py | 327 ++++++++++++++++++ test/unit/lattice/conftest.py | 2 - 2 files changed, 327 insertions(+), 2 deletions(-) diff --git a/test/unit/lattice/abe_lattice_properties_test.py b/test/unit/lattice/abe_lattice_properties_test.py index 278e1d1..eae27d9 100644 --- a/test/unit/lattice/abe_lattice_properties_test.py +++ b/test/unit/lattice/abe_lattice_properties_test.py @@ -56,3 +56,330 @@ def test_2d_lattice_ancilla_obstacle_register( "Cannot index ancilla obstacle register for index 2. Maximum index for this lattice is 0." == str(excinfo.value) ) + + +def test_2d_lattice_no_cuboid_has_no_comparator_register(): + lattice = ABLattice( + { + "lattice": {"dim": {"x": 16, "y": 16}, "velocities": "D2Q4"}, + "geometry": [ + { + "shape": "ymonomial", + "exponent": 2, + "comparator": "<=", + "boundary": "bounceback", + } + ], + } + ) + + assert lattice.num_comparator_qubits == 1 + assert lattice.ancillae_comparator_index() == [10] + assert lattice.ancillae_comparator_index(0) == [10] + assert len(lattice.ancilla_comparator_register) == 1 + assert lattice.ancillae_obstacle_index() == [11] + + +def test_set_geometries_updates_comparator_register_allocation(): + lattice = ABLattice( + { + "lattice": {"dim": {"x": 16, "y": 16}, "velocities": "D2Q4"}, + "geometry": [ + { + "shape": "cuboid", + "x": [2, 6], + "y": [5, 10], + "boundary": "bounceback", + } + ], + } + ) + + assert lattice.num_comparator_qubits == 2 + assert lattice.ancillae_comparator_index() == [10, 11] + + lattice.set_geometries( + [ + [ + { + "shape": "ymonomial", + "exponent": 3, + "comparator": ">", + "boundary": "bounceback", + } + ] + ] + ) + + assert lattice.num_comparator_qubits == 1 + assert lattice.ancillae_comparator_index() == [10] + assert lattice.ancillae_comparator_index(0) == [10] + assert len(lattice.ancilla_comparator_register) == 1 + assert lattice.ancillae_obstacle_index() == [11] + + +def test_2d_lattice_no_objects_has_no_comparator_register(): + lattice = ABLattice( + { + "lattice": {"dim": {"x": 16, "y": 16}, "velocities": "D2Q4"}, + } + ) + + assert lattice.num_comparator_qubits == 0 + assert lattice.ancillae_comparator_index() == [] + assert len(lattice.ancilla_comparator_register) == 0 + + with pytest.raises(LatticeException) as excinfo: + lattice.ancillae_comparator_index(0) + assert ( + "Cannot index ancilla comparator register because this lattice has no comparator qubits." + == str(excinfo.value) + ) + + +@pytest.mark.parametrize( + "geometry, expected_comparator_qubits, expected_obstacle_qubits, expected_monomial_qubits", + [ + ( + [ + { + "shape": "cuboid", + "x": [2, 6], + "y": [5, 10], + "boundary": "bounceback", + } + ], + 2, + 1, + 0, + ), + ( + [ + { + "shape": "ymonomial", + "exponent": 2, + "comparator": "<=", + "boundary": "bounceback", + } + ], + 1, + 1, + 12, + ), + ( + [ + { + "shape": "cuboid", + "x": [1, 3], + "y": [7, 11], + "boundary": "specular", + }, + { + "shape": "ymonomial", + "exponent": 1, + "comparator": ">=", + "boundary": "bounceback", + }, + ], + 2, + 2, + 8, + ), + ( + [ + { + "shape": "ymonomial", + "exponent": 1, + "comparator": "<", + "boundary": "bounceback", + }, + { + "shape": "ymonomial", + "exponent": 4, + "comparator": ">", + "boundary": "bounceback", + }, + ], + 1, + 1, + 20, + ), + ], +) +def test_2d_ab_lattice_cuboid_ymonomial_combinations( + geometry, + expected_comparator_qubits, + expected_obstacle_qubits, + expected_monomial_qubits, +): + lattice = ABLattice( + { + "lattice": {"dim": {"x": 16, "y": 16}, "velocities": "D2Q4"}, + "geometry": geometry, + } + ) + + assert lattice.num_comparator_qubits == expected_comparator_qubits + assert lattice.num_obstacle_qubits == expected_obstacle_qubits + assert lattice.num_monomial_qubits == expected_monomial_qubits + assert lattice.num_ancilla_qubits == ( + expected_comparator_qubits + expected_obstacle_qubits + ) + + if expected_comparator_qubits == 2: + assert lattice.ancillae_comparator_index() == [10, 11] + assert lattice.ancillae_comparator_index(0) == [10, 11] + assert len(lattice.ancilla_comparator_register) == 1 + elif expected_comparator_qubits == 1: + assert lattice.ancillae_comparator_index() == [10] + assert lattice.ancillae_comparator_index(0) == [10] + assert len(lattice.ancilla_comparator_register) == 1 + else: + assert lattice.ancillae_comparator_index() == [] + assert len(lattice.ancilla_comparator_register) == 0 + with pytest.raises(LatticeException) as excinfo: + lattice.ancillae_comparator_index(0) + assert ( + "Cannot index ancilla comparator register because this lattice has no comparator qubits." + == str(excinfo.value) + ) + + expected_obstacle_start = 10 + expected_comparator_qubits + assert lattice.ancillae_obstacle_index()[0] == expected_obstacle_start + + +@pytest.mark.parametrize( + "new_geometries, expected_comparator_qubits, expected_obstacle_qubits, expected_monomial_qubits, expected_marker_qubits", + [ + ( + [ + [ + { + "shape": "cuboid", + "x": [2, 6], + "y": [5, 10], + "boundary": "bounceback", + } + ], + [ + { + "shape": "ymonomial", + "exponent": 2, + "comparator": "<=", + "boundary": "bounceback", + } + ], + ], + 2, + 1, + 12, + 1, + ), + ( + [ + [ + { + "shape": "ymonomial", + "exponent": 1, + "comparator": "<", + "boundary": "specular", + } + ], + [ + { + "shape": "ymonomial", + "exponent": 3, + "comparator": ">", + "boundary": "bounceback", + } + ], + ], + 1, + 2, + 16, + 1, + ), + ( + [ + [ + { + "shape": "cuboid", + "x": [1, 4], + "y": [1, 4], + "boundary": "specular", + } + ], + [ + { + "shape": "cuboid", + "x": [8, 10], + "y": [8, 10], + "boundary": "bounceback", + }, + { + "shape": "ymonomial", + "exponent": 0, + "comparator": ">=", + "boundary": "bounceback", + }, + ], + [ + { + "shape": "ymonomial", + "exponent": 5, + "comparator": "<=", + "boundary": "bounceback", + } + ], + ], + 2, + 2, + 24, + 2, + ), + ], +) +def test_set_geometries_updates_registers_for_ymonomial_cuboid_combinations( + new_geometries, + expected_comparator_qubits, + expected_obstacle_qubits, + expected_monomial_qubits, + expected_marker_qubits, +): + lattice = ABLattice( + { + "lattice": {"dim": {"x": 16, "y": 16}, "velocities": "D2Q4"}, + "geometry": [ + { + "shape": "cuboid", + "x": [2, 6], + "y": [5, 10], + "boundary": "bounceback", + } + ], + } + ) + + lattice.set_geometries(new_geometries) + + assert lattice.num_comparator_qubits == expected_comparator_qubits + assert lattice.num_obstacle_qubits == expected_obstacle_qubits + assert lattice.num_monomial_qubits == expected_monomial_qubits + assert lattice.num_marker_qubits == expected_marker_qubits + assert lattice.has_multiple_geometries() + + assert lattice.num_ancilla_qubits == ( + expected_comparator_qubits + expected_obstacle_qubits + ) + assert len(lattice.ancilla_comparator_register) == ( + 1 if expected_comparator_qubits > 0 else 0 + ) + + if expected_monomial_qubits > 0: + assert len(lattice.copy_register) == 1 + assert len(lattice.monomial_register) == 1 + else: + assert len(lattice.copy_register) == 0 + assert len(lattice.monomial_register) == 0 + + assert len(lattice.marker_index()) == expected_marker_qubits diff --git a/test/unit/lattice/conftest.py b/test/unit/lattice/conftest.py index 29afbc3..e21e16e 100644 --- a/test/unit/lattice/conftest.py +++ b/test/unit/lattice/conftest.py @@ -9,7 +9,6 @@ @pytest.fixture def dummy_1d_lattice() -> ABLattice: return ABLattice( - 0, { "lattice": { "dim": {"x": 256}, @@ -35,7 +34,6 @@ def lattice_1d_16_1_obstacle() -> ABLattice: @pytest.fixture def dummy_2d_lattice() -> ABLattice: return ABLattice( - 0, { "lattice": {"dim": {"x": 32, "y": 32}, "velocities": "D2Q4"}, }, From 3a1dd4c20fa55fe2a7592dc4504ce22cf9b4dfc1 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Fri, 20 Feb 2026 14:09:17 +0100 Subject: [PATCH 54/78] Add two register comparator primitive --- qlbm/components/ms/primitives.py | 93 +++++++++++++++++++++++++++++++- 1 file changed, 91 insertions(+), 2 deletions(-) diff --git a/qlbm/components/ms/primitives.py b/qlbm/components/ms/primitives.py index c696676..947f599 100644 --- a/qlbm/components/ms/primitives.py +++ b/qlbm/components/ms/primitives.py @@ -5,6 +5,7 @@ from typing import List from qiskit import ClassicalRegister, QuantumCircuit +from qiskit.circuit.library import DraperQFTAdder from typing_extensions import override from qlbm.components.base import LBMPrimitive @@ -269,7 +270,7 @@ def __str__(self) -> str: class Comparator(LBMPrimitive): """ - Quantum comparator primitive that compares two a quantum state of ``num_qubits`` qubits and an integer ``num_to_compare`` with respect to a :class:`.ComparatorMode`. + Quantum comparator primitive that compares a quantum state of ``num_qubits`` qubits and an integer ``num_to_compare`` with respect to a :class:`.ComparatorMode`. ========================= ====================================================================== Attribute Summary @@ -285,7 +286,8 @@ class Comparator(LBMPrimitive): .. plot:: :include-source: - from qlbm.components.ms import Comparator, ComparatorMode + from qlbm.components.ms import Comparator + from qlbm.tools import ComparatorMode # On a 5 qubit register, compare the number 3 Comparator(num_qubits=5, @@ -369,6 +371,93 @@ def __str__(self) -> str: return f"[Primitive Comparator of {self.num_qubits} and {self.num_to_compare}, mode={self.mode}]" +class TwoRegisterComparator(LBMPrimitive): + """ + Quantum comparator primitive that compares the states of 2 registers of ``num_qubits`` qubits a :class:`.ComparatorMode`. + + The generate circuit is of size ``2*num_qubits+1``, where the last qubit of the register holds the boolean result. + + Example usage: + + .. plot:: + :include-source: + + from qlbm.components.ms import TwoRegisterComparator + from qlbm.tools import ComparatorMode + + # Compare two registers of size 4 + TwoRegisterComparator(num_qubits=4, mode=ComparatorMode.LT).draw("mpl") + """ + + def __init__( + self, + num_qubits: int, + mode: ComparatorMode, + logger: Logger = getLogger("qlbm"), + ) -> None: + super().__init__(logger) + + self.num_qubits = num_qubits + self.mode = mode + + self.logger.info(f"Creating circuit {str(self)}...") + circuit_creation_start_time = perf_counter_ns() + self.circuit = self.create_circuit() + self.logger.info( + f"Creating circuit {str(self)} took {perf_counter_ns() - circuit_creation_start_time} (ns)" + ) + + @override + def create_circuit(self) -> QuantumCircuit: + circuit = QuantumCircuit(2 * self.num_qubits + 1) + x_register = list(range(self.num_qubits)) + y_register = list(range(self.num_qubits, 2 * self.num_qubits)) + output_qubit = 2 * self.num_qubits + + match self.mode: + case ComparatorMode.GT: + self.__compose_gt(circuit, x_register, y_register, output_qubit) + case ComparatorMode.LE: + self.__compose_gt(circuit, x_register, y_register, output_qubit) + circuit.x(output_qubit) + case ComparatorMode.LT: + self.__compose_gt(circuit, y_register, x_register, output_qubit) + case ComparatorMode.GE: + self.__compose_gt(circuit, y_register, x_register, output_qubit) + circuit.x(output_qubit) + case _: + raise ValueError("Invalid Comparator Mode") + + return circuit + + def __compose_gt( + self, + circuit: QuantumCircuit, + x_register: List[int], + y_register: List[int], + output_qubit: int, + ) -> None: + add_half = DraperQFTAdder(self.num_qubits, kind="half") + add_fixed_inv = DraperQFTAdder(self.num_qubits, kind="fixed").inverse() + + circuit.x(y_register) + circuit.compose( + add_half, + qubits=x_register + y_register + [output_qubit], + inplace=True, + ) + circuit.compose( + add_fixed_inv, + qubits=x_register + y_register, + inplace=True, + ) + circuit.x(y_register) + + @override + def __str__(self) -> str: + return f"[Primitive TwoRegisterComparator of {self.num_qubits} qubits, mode={self.mode}]" + + class EdgeComparator(LBMPrimitive): """ A primitive used in the 3D collisionless :class:`SpecularReflectionOperator` and :class:`BounceBackReflectionOperator` described in :cite:t:`collisionless`. From cb665cf788043b0a90f7c0423564b18c96f4e5b8 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Fri, 20 Feb 2026 14:09:47 +0100 Subject: [PATCH 55/78] Add YMonomial oracle in AB zone agnostic BCs --- .../ab/reflection/agnosotic_reflection.py | 105 ++++++++++++++++-- .../ab/reflection/standard_reflection.py | 40 +++---- 2 files changed, 110 insertions(+), 35 deletions(-) diff --git a/qlbm/components/ab/reflection/agnosotic_reflection.py b/qlbm/components/ab/reflection/agnosotic_reflection.py index 4fa7cec..4c27ee5 100644 --- a/qlbm/components/ab/reflection/agnosotic_reflection.py +++ b/qlbm/components/ab/reflection/agnosotic_reflection.py @@ -5,16 +5,16 @@ from typing import List, cast from qiskit import QuantumCircuit +from qiskit.circuit.library import RGQFTMultiplier from typing_extensions import override from qlbm.components.ab.reflection.standard_reflection import ABReflectionOperator from qlbm.components.ab.streaming import ABStreamingOperator from qlbm.components.base import LBMPrimitive from qlbm.components.common.adders import ParameterizedDraperAdder -from qlbm.components.ms.primitives import Comparator +from qlbm.components.ms.primitives import Comparator, TwoRegisterComparator +from qlbm.lattice.geometry.shapes import Block, Circle, YMonomial from qlbm.lattice.geometry.shapes.base import Shape -from qlbm.lattice.geometry.shapes.block import Block -from qlbm.lattice.geometry.shapes.circle import Circle from qlbm.lattice.lattices.ab_lattice import ABLattice from qlbm.lattice.lattices.base import AmplitudeLattice from qlbm.lattice.spacetime.properties_base import LatticeDiscretization @@ -60,12 +60,12 @@ class ABZoneAgnosticReflectionOperator(ABReflectionOperator): def __init__( self, lattice: ABLattice, - blocks: List[Block] | None = None, + shapes: List[Shape] | None = None, logger: Logger = getLogger("qlbm"), ) -> None: - super().__init__(lattice, blocks, logger) + super().__init__(lattice, [], logger) - self.blocks = ( + self.shapes = ( ( cast(List[Block], flatten(list(self.lattice.geometries[0].values()))) if not self.lattice.has_multiple_geometries() @@ -74,10 +74,17 @@ def __init__( for gdict in self.lattice.geometries # type: ignore ] ) - if blocks is None - else blocks + if shapes is None + else shapes ) + supported_shapes = ["cuboid", "ymonomial"] + + if any([x.name() not in supported_shapes for x in self.shapes]): + raise CircuitException( + f"Agnostic reflection operator only supports the following shapes: {supported_shapes}." + ) + self.logger.info(f"Creating circuit {str(self)}...") circuit_creation_start_time = perf_counter_ns() self.circuit = self.create_circuit() @@ -93,10 +100,10 @@ def create_circuit(self) -> QuantumCircuit: oracle = self.lattice.circuit.copy() # build the oracle once - for block in self.blocks: + for shape in self.shapes: oracle.compose( ABZoneAgnosticReflectionOracle( - self.lattice, block, logger=self.logger + self.lattice, shape, logger=self.logger ).circuit, inplace=True, ) @@ -140,6 +147,14 @@ class ABZoneAgnosticReflectionOracle(LBMPrimitive): falls within the bounds of the object. Currently, the only available implementation is for 2D axis-aligned objects. + + .. important:: + + The ``YMonomial`` implementation is a work in progress. + At present, only the :math:`x^2` monomial case is supported, + and only when the monomial result register width matches the :math:`y` + grid register width. + This is an improvement in asymptotic and practical complexity compared to the methods described in :cite:`collisionless`. This operation relies on basic arithmetic through the :class:`.ParameterizedDraperAdder` class @@ -170,11 +185,11 @@ class ABZoneAgnosticReflectionOracle(LBMPrimitive): """ - lattice: AmplitudeLattice + lattice: ABLattice def __init__( self, - lattice: AmplitudeLattice, + lattice: ABLattice, shape: Shape, logger: Logger = getLogger("qlbm"), ) -> None: @@ -196,6 +211,8 @@ def create_circuit(self) -> QuantumCircuit: return self.__create_circuit_block() elif isinstance(self.shape, Circle): return self.__create_circuit_circle() + elif isinstance(self.shape, YMonomial): + return self.__create_circuit_ymonomial() def __create_circuit_block(self) -> QuantumCircuit: circuit = self.lattice.circuit.copy() @@ -256,6 +273,70 @@ def __create_circuit_block(self) -> QuantumCircuit: def __create_circuit_circle(self) -> QuantumCircuit: raise CircuitException("Not implemented") + def __create_circuit_ymonomial(self) -> QuantumCircuit: + circuit = self.lattice.circuit.copy() + + ym: YMonomial = cast(YMonomial, self.shape) + + if ym.exponent != 2: + raise CircuitException( + "YMonomial oracle is a work in progress: only exponent=2 (x^2) is currently supported." + ) + + # Qubits used in this oracle + grid_x_qubits = self.lattice.grid_index(0) + grid_y_qubits = self.lattice.grid_index(1) + copy_qubits = self.lattice.ancillae_copy_index() + result_qubits = self.lattice.ancillae_monomial_index() + + if len(result_qubits) != len(grid_y_qubits): + raise CircuitException( + "YMonomial oracle is a work in progress: only configurations with equal y and monomial result register sizes are currently supported." + ) + + # circuits used more than once + multiplication_circuit = RGQFTMultiplier( + num_state_qubits=len(grid_x_qubits), + num_result_qubits=len(result_qubits), + ) + + comparator_circuit = TwoRegisterComparator( + len(grid_y_qubits), ym.comparator_mode + ).circuit + + # Copy x into the copy register + for qc, qt in zip(grid_x_qubits, copy_qubits): + circuit.cx(qc, qt) + + # Do the multiplication + circuit.compose( + multiplication_circuit, + qubits=grid_x_qubits + copy_qubits + result_qubits, + inplace=True, + ) + + # Comparator + circuit.compose( + comparator_circuit, + qubits=grid_y_qubits + + result_qubits + + self.lattice.ancillae_obstacle_index(), + inplace=True, + ) + + # Undo multiplication + circuit.compose( + multiplication_circuit.inverse(), + qubits=grid_x_qubits + copy_qubits + result_qubits, + inplace=True, + ) + + # Undo copy + for qc, qt in zip(grid_x_qubits, copy_qubits): + circuit.cx(qc, qt) + + return circuit + @override def __str__(self) -> str: return f"[Primitive ABZoneAgnosticReflectionOracle with lattice {self.lattice}, shape={self.shape}]" diff --git a/qlbm/components/ab/reflection/standard_reflection.py b/qlbm/components/ab/reflection/standard_reflection.py index 2367122..6721908 100644 --- a/qlbm/components/ab/reflection/standard_reflection.py +++ b/qlbm/components/ab/reflection/standard_reflection.py @@ -1,6 +1,5 @@ """Reflection utilities for the :class:`.ABQLBM` algorithm; generalizations of :cite:`collisionless`.""" - from itertools import product from logging import Logger, getLogger from time import perf_counter_ns @@ -16,6 +15,7 @@ from qlbm.components.base import LBMOperator from qlbm.components.ms.specular_reflection import SpecularWallComparator from qlbm.lattice.geometry.encodings.ms import ReflectionPoint +from qlbm.lattice.geometry.shapes.base import Shape from qlbm.lattice.geometry.shapes.block import Block from qlbm.lattice.lattices.ab_lattice import ABLattice from qlbm.lattice.lattices.base import AmplitudeLattice @@ -59,22 +59,22 @@ class ABReflectionOperator(LBMOperator): def __init__( self, lattice: ABLattice, - blocks: List[Block] | None = None, + shapes: List[Shape] | None = None, logger: Logger = getLogger("qlbm"), ) -> None: super().__init__(lattice, logger) - self.blocks = ( + self.shapes = ( ( - cast(List[Block], flatten(list(self.lattice.geometries[0].values()))) + flatten(list(self.lattice.geometries[0].values())) if not self.lattice.has_multiple_geometries() else [ gdict["bounceback"] + gdict["specular"] # type: ignore for gdict in self.lattice.geometries # type: ignore ] ) - if blocks is None - else blocks + if shapes is None + else shapes ) self.logger.info(f"Creating circuit {str(self)}...") @@ -92,11 +92,11 @@ def create_circuit(self) -> QuantumCircuit: if self.lattice.discretization == LatticeDiscretization.D2Q9: if not self.lattice.has_multiple_geometries(): return self.__create_circuit_d2q9( - self.blocks, control_on_marker_state=False + self.shapes, control_on_marker_state=False ) else: circuit = self.lattice.circuit.copy() - for c, blocks in enumerate(self.blocks): + for c, blocks in enumerate(self.shapes): # Prepare the /ket{1} state in the marker register qubits_to_invert = [ q + self.lattice.marker_index()[0] @@ -426,14 +426,11 @@ def set_ancilla_of_point_state( if velocity_qubit_indices_to_invert: circuit.x(velocity_qubit_indices_to_invert) - control_qubits = ( - self.lattice.grid_index() - + ( - self.lattice.velocity_index() - if not ignore_velocity_data - else [] - ) # The reset step is additionally controlled on the velocity register - ) + control_qubits = self.lattice.grid_index() + ( + self.lattice.velocity_index() + if not ignore_velocity_data + else [] + ) # The reset step is additionally controlled on the velocity register if control_on_marker_state: control_qubits.extend(self.lattice.marker_index()) @@ -470,12 +467,9 @@ def set_ancilla_of_point_state( ) else: for v in velocities: - control_qubits = ( - self.lattice.grid_index() - + ( - [self.lattice.velocity_index()[v]] - ) # Only one velocity control - ) + control_qubits = self.lattice.grid_index() + ( + [self.lattice.velocity_index()[v]] + ) # Only one velocity control if control_on_marker_state: control_qubits.extend(self.lattice.marker_index()) @@ -537,4 +531,4 @@ def permute_and_stream(self) -> QuantumCircuit: @override def __str__(self) -> str: - return f"[Operator ABReflection with lattice {self.lattice}]" \ No newline at end of file + return f"[Operator ABReflection with lattice {self.lattice}]" From 9c6a1a7dab8d1088d750b0c31cc924baaaf6cceb Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Fri, 20 Feb 2026 14:10:28 +0100 Subject: [PATCH 56/78] Use zone agnostic operator by default in ABQLBM --- qlbm/components/ab/ab.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/qlbm/components/ab/ab.py b/qlbm/components/ab/ab.py index 1985c3f..7480171 100644 --- a/qlbm/components/ab/ab.py +++ b/qlbm/components/ab/ab.py @@ -74,19 +74,10 @@ def create_circuit(self): inplace=True, ) - for bc in ["bounceback", "specular"]: - if self.lattice.shapes[bc]: - if not all( - isinstance(shape, Block) - for shape in self.lattice.shapes["specular"] - ): - raise LatticeException( - f"All shapes with the {bc} boundary condition must be cuboids for the ABQLBM algorithm. " - ) - circuit.compose( ABZoneAgnosticReflectionOperator( self.lattice, + self.lattice.shape_list, logger=self.logger, ).circuit, inplace=True, From e6b542da4592e3bc1eeda3f15947ba6e18a19de9 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Fri, 20 Feb 2026 14:11:08 +0100 Subject: [PATCH 57/78] Normalize segment width in YMonomial STL mesh --- qlbm/lattice/geometry/shapes/ymonomial.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/qlbm/lattice/geometry/shapes/ymonomial.py b/qlbm/lattice/geometry/shapes/ymonomial.py index 610bb6c..a2e70f5 100644 --- a/qlbm/lattice/geometry/shapes/ymonomial.py +++ b/qlbm/lattice/geometry/shapes/ymonomial.py @@ -53,16 +53,25 @@ def stl_mesh(self) -> mesh.Mesh: The mesh representing the shape. """ triangles: List[np.ndarray] = [] + x_segments = 2 ** self.num_grid_qubits[0] + y_segments = 2 ** self.num_grid_qubits[1] + x_segment_width = (x_segments - 1) / x_segments + y_segment_height = (y_segments - 1) / y_segments for y, row in enumerate(self.boundary_points): for x, is_inside in enumerate(row): if not is_inside: continue - v00 = np.array([x, y, 1.0]) - v10 = np.array([x + 1, y, 1.0]) - v01 = np.array([x, y + 1, 1.0]) - v11 = np.array([x + 1, y + 1, 1.0]) + x0 = x * x_segment_width + x1 = (x + 1) * x_segment_width + y0 = y * y_segment_height + y1 = (y + 1) * y_segment_height + + v00 = np.array([x0, y0, 1.0]) + v10 = np.array([x1, y0, 1.0]) + v01 = np.array([x0, y1, 1.0]) + v11 = np.array([x1, y1, 1.0]) triangles.append(np.array([v00, v10, v11])) triangles.append(np.array([v00, v11, v01])) From 8d4242f890022f8382debf8c8361ffef7be57cbe Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Fri, 20 Feb 2026 14:27:47 +0100 Subject: [PATCH 58/78] Update adaptive register functionality and clarify register names --- qlbm/lattice/__init__.py | 2 + qlbm/lattice/geometry/__init__.py | 3 +- qlbm/lattice/geometry/shapes/__init__.py | 3 +- qlbm/lattice/lattices/ab_lattice.py | 69 ++++++++++++------------ 4 files changed, 42 insertions(+), 35 deletions(-) diff --git a/qlbm/lattice/__init__.py b/qlbm/lattice/__init__.py index 5d5620e..8b46f27 100644 --- a/qlbm/lattice/__init__.py +++ b/qlbm/lattice/__init__.py @@ -12,6 +12,7 @@ from .geometry.shapes.circle import ( Circle, ) +from .geometry.shapes.ymonomial import YMonomial from .lattices import Lattice, MSLattice from .lattices.ab_lattice import ABLattice from .lattices.lqlga_lattice import LQLGALattice @@ -37,4 +38,5 @@ "Circle", "LatticeDiscretization", "LatticeDiscretizationProperties", + "YMonomial", ] diff --git a/qlbm/lattice/geometry/__init__.py b/qlbm/lattice/geometry/__init__.py index d78094e..498a881 100644 --- a/qlbm/lattice/geometry/__init__.py +++ b/qlbm/lattice/geometry/__init__.py @@ -11,7 +11,7 @@ SpaceTimePWReflectionData, SpaceTimeVolumetricReflectionData, ) -from .shapes import Block, Circle +from .shapes import Block, Circle, YMonomial __all__ = [ "DimensionalReflectionData", @@ -25,4 +25,5 @@ "Circle", "LQLGAPointwiseReflectionData", "LQLGAReflectionData", + "YMonomial", ] diff --git a/qlbm/lattice/geometry/shapes/__init__.py b/qlbm/lattice/geometry/shapes/__init__.py index 11c2a6d..beb839c 100644 --- a/qlbm/lattice/geometry/shapes/__init__.py +++ b/qlbm/lattice/geometry/shapes/__init__.py @@ -2,5 +2,6 @@ from .block import Block from .circle import Circle +from .ymonomial import YMonomial -__all__ = ["Block", "Circle"] +__all__ = ["Block", "Circle", "YMonomial"] diff --git a/qlbm/lattice/lattices/ab_lattice.py b/qlbm/lattice/lattices/ab_lattice.py index df9074e..5a06887 100644 --- a/qlbm/lattice/lattices/ab_lattice.py +++ b/qlbm/lattice/lattices/ab_lattice.py @@ -118,10 +118,14 @@ class ABLattice(AmplitudeLattice): r"""The number of qubits used for the imposition of monomially-shaped BCs. Currently, only the :class:`.YMonomial` is supported. The number of qubits for a monomial with exponent :math:`n` - is :math:`(n+1)\lceil \log_2 N_{g_x}\rceil`. - The first :math:`\log_2 N_{g_x}\rceil`-sized register is allocated - to making a copy, while the other :math:`n` chunks - are allotted to the computation of the monomial via quantum arithmetic.""" + is :math:`n\lceil \log_2 N_{g_x}\rceil`. + This does not include copy-register qubits, which are tracked separately + in :attr:`num_copy_qubits`.""" + + num_copy_qubits: int + r"""The number of qubits used to copy the :math:`x` coordinate register for monomial BCs. + If at least one :class:`.YMonomial` is present, this is :math:`\lceil \log_2 N_{g_x}\rceil`, + otherwise it is ``0``.""" registers: Tuple[QuantumRegister, ...] """The registers of the lattice.""" @@ -154,6 +158,7 @@ def __init__( self.num_base_qubits = self.num_grid_qubits + self.num_velocity_qubits self.num_obstacle_qubits = self.__num_obstacle_qubits() + self.num_copy_qubits = self.__num_copy_qubits() self.num_monomial_qubits = self.__num_monomial_qubits() self.num_comparator_qubits = self.__num_comparator_qubits() self.num_ancilla_qubits = self.num_comparator_qubits + self.num_obstacle_qubits @@ -175,6 +180,7 @@ def __init__( def __update_registers(self): self.num_obstacle_qubits = self.__num_obstacle_qubits() + self.num_copy_qubits = self.__num_copy_qubits() self.num_monomial_qubits = self.__num_monomial_qubits() self.num_comparator_qubits = self.__num_comparator_qubits() self.num_ancilla_qubits = self.num_comparator_qubits + self.num_obstacle_qubits @@ -327,26 +333,17 @@ def ancillae_comparator_index(self, index: int | None = None) -> List[int]: ) ) - if index >= self.num_dims - 1 or index < 0: + if index != 0: raise LatticeException( - f"Cannot index ancilla comparator register for index {index} in {self.num_dims}-dimensional lattice. Maximum is {self.num_dims - 2}." + f"Cannot index ancilla comparator register for index {index} in {self.num_dims}-dimensional lattice. Maximum is 0." ) - qubits_per_dim = ( - 2 if self.num_comparator_qubits >= 2 * (self.num_dims - 1) else 1 - ) - previous_qubits = self.num_base_qubits + qubits_per_dim * index - final_qubit = min( - self.num_base_qubits + self.num_comparator_qubits, - previous_qubits + qubits_per_dim, - ) - - if previous_qubits >= final_qubit: - raise LatticeException( - f"Cannot index ancilla comparator register for index {index} in {self.num_dims}-dimensional lattice. Maximum is {self.num_dims - 2}." + return list( + range( + self.num_base_qubits, + self.num_base_qubits + self.num_comparator_qubits, ) - - return list(range(previous_qubits, final_qubit)) + ) @override def ancillae_obstacle_index(self, index: int | None = None) -> List[int]: @@ -376,7 +373,7 @@ def ancillae_copy_index(self) -> List[int]: List[int] The indices of the copy register qubits. """ - if self.num_monomial_qubits == 0: + if self.num_copy_qubits == 0: raise LatticeException( "This lattice does not have any copy register qubits." ) @@ -389,7 +386,7 @@ def ancillae_copy_index(self) -> List[int]: self.num_base_qubits + self.num_comparator_qubits + self.num_obstacle_qubits - + self.num_gridpoints[0].bit_length(), + + self.num_copy_qubits, ) ) @@ -410,10 +407,11 @@ def ancillae_monomial_index(self) -> List[int]: self.num_base_qubits + self.num_comparator_qubits + self.num_obstacle_qubits - + self.num_gridpoints[0].bit_length(), + + self.num_copy_qubits, self.num_base_qubits + self.num_comparator_qubits + self.num_obstacle_qubits + + self.num_copy_qubits + self.num_monomial_qubits, ) ) @@ -439,23 +437,24 @@ def __num_obstacle_qubits(self) -> int: ) def __num_comparator_qubits(self) -> int: - comp_qs_cuboids = ( - 2 * (self.num_dims - 1) + return ( + self.num_dims if any( shape.name() == "cuboid" for shape in flatten(self.__geometry_shape_lists()) ) else 0 ) - comp_qs_monomials = ( - 1 + + def __num_copy_qubits(self) -> int: + return ( + self.num_gridpoints[0].bit_length() if any( shape.name() == "ymonomial" for shape in flatten(self.__geometry_shape_lists()) ) else 0 ) - return max(comp_qs_cuboids, comp_qs_monomials) def __num_monomial_qubits(self) -> int: monomial_shapes_exponent = [ @@ -467,8 +466,7 @@ def __num_monomial_qubits(self) -> int: return ( 0 if not monomial_shapes_exponent - else (max(monomial_shapes_exponent) + 1) - * self.num_gridpoints[0].bit_length() + else max(monomial_shapes_exponent) * self.num_gridpoints[0].bit_length() ) def __geometry_shape_lists(self) -> List[List[Shape]]: @@ -514,14 +512,19 @@ def get_registers(self) -> Tuple[List[QuantumRegister], ...]: # Monomial qubits # ! Only works for Ymonomials copy_register = ( - [QuantumRegister(self.num_gridpoints[0].bit_length(), name="a_copy")] - if self.num_monomial_qubits > 0 + [QuantumRegister(self.num_copy_qubits, name="a_copy")] + if self.num_copy_qubits > 0 else [] ) # ! Only works for Ymonomials monomial_register = ( - [QuantumRegister(self.num_monomial_qubits, name="monomial")] + [ + QuantumRegister( + self.num_monomial_qubits, + name="monomial", + ) + ] if self.num_monomial_qubits > 0 else [] ) From bd2836c88b5732978bcec1376900eacf187349b8 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Fri, 20 Feb 2026 14:28:57 +0100 Subject: [PATCH 59/78] Update AB lattice tests --- .../lattice/abe_lattice_properties_test.py | 118 ++++++++++++++---- 1 file changed, 91 insertions(+), 27 deletions(-) diff --git a/test/unit/lattice/abe_lattice_properties_test.py b/test/unit/lattice/abe_lattice_properties_test.py index eae27d9..1a80d52 100644 --- a/test/unit/lattice/abe_lattice_properties_test.py +++ b/test/unit/lattice/abe_lattice_properties_test.py @@ -73,11 +73,16 @@ def test_2d_lattice_no_cuboid_has_no_comparator_register(): } ) - assert lattice.num_comparator_qubits == 1 - assert lattice.ancillae_comparator_index() == [10] - assert lattice.ancillae_comparator_index(0) == [10] - assert len(lattice.ancilla_comparator_register) == 1 - assert lattice.ancillae_obstacle_index() == [11] + assert lattice.num_comparator_qubits == 0 + assert lattice.ancillae_comparator_index() == [] + assert len(lattice.ancilla_comparator_register) == 0 + with pytest.raises(LatticeException) as excinfo: + lattice.ancillae_comparator_index(0) + assert ( + "Cannot index ancilla comparator register because this lattice has no comparator qubits." + == str(excinfo.value) + ) + assert lattice.ancillae_obstacle_index() == [10] def test_set_geometries_updates_comparator_register_allocation(): @@ -111,11 +116,16 @@ def test_set_geometries_updates_comparator_register_allocation(): ] ) - assert lattice.num_comparator_qubits == 1 - assert lattice.ancillae_comparator_index() == [10] - assert lattice.ancillae_comparator_index(0) == [10] - assert len(lattice.ancilla_comparator_register) == 1 - assert lattice.ancillae_obstacle_index() == [11] + assert lattice.num_comparator_qubits == 0 + assert lattice.ancillae_comparator_index() == [] + assert len(lattice.ancilla_comparator_register) == 0 + with pytest.raises(LatticeException) as excinfo: + lattice.ancillae_comparator_index(0) + assert ( + "Cannot index ancilla comparator register because this lattice has no comparator qubits." + == str(excinfo.value) + ) + assert lattice.ancillae_obstacle_index() == [10] def test_2d_lattice_no_objects_has_no_comparator_register(): @@ -138,7 +148,7 @@ def test_2d_lattice_no_objects_has_no_comparator_register(): @pytest.mark.parametrize( - "geometry, expected_comparator_qubits, expected_obstacle_qubits, expected_monomial_qubits", + "geometry, expected_comparator_qubits, expected_obstacle_qubits, expected_copy_qubits, expected_monomial_qubits", [ ( [ @@ -152,6 +162,7 @@ def test_2d_lattice_no_objects_has_no_comparator_register(): 2, 1, 0, + 0, ), ( [ @@ -162,9 +173,10 @@ def test_2d_lattice_no_objects_has_no_comparator_register(): "boundary": "bounceback", } ], + 0, 1, - 1, - 12, + 4, + 8, ), ( [ @@ -183,7 +195,8 @@ def test_2d_lattice_no_objects_has_no_comparator_register(): ], 2, 2, - 8, + 4, + 4, ), ( [ @@ -200,9 +213,10 @@ def test_2d_lattice_no_objects_has_no_comparator_register(): "boundary": "bounceback", }, ], + 0, 1, - 1, - 20, + 4, + 16, ), ], ) @@ -210,6 +224,7 @@ def test_2d_ab_lattice_cuboid_ymonomial_combinations( geometry, expected_comparator_qubits, expected_obstacle_qubits, + expected_copy_qubits, expected_monomial_qubits, ): lattice = ABLattice( @@ -221,6 +236,7 @@ def test_2d_ab_lattice_cuboid_ymonomial_combinations( assert lattice.num_comparator_qubits == expected_comparator_qubits assert lattice.num_obstacle_qubits == expected_obstacle_qubits + assert lattice.num_copy_qubits == expected_copy_qubits assert lattice.num_monomial_qubits == expected_monomial_qubits assert lattice.num_ancilla_qubits == ( expected_comparator_qubits + expected_obstacle_qubits @@ -230,10 +246,6 @@ def test_2d_ab_lattice_cuboid_ymonomial_combinations( assert lattice.ancillae_comparator_index() == [10, 11] assert lattice.ancillae_comparator_index(0) == [10, 11] assert len(lattice.ancilla_comparator_register) == 1 - elif expected_comparator_qubits == 1: - assert lattice.ancillae_comparator_index() == [10] - assert lattice.ancillae_comparator_index(0) == [10] - assert len(lattice.ancilla_comparator_register) == 1 else: assert lattice.ancillae_comparator_index() == [] assert len(lattice.ancilla_comparator_register) == 0 @@ -249,7 +261,7 @@ def test_2d_ab_lattice_cuboid_ymonomial_combinations( @pytest.mark.parametrize( - "new_geometries, expected_comparator_qubits, expected_obstacle_qubits, expected_monomial_qubits, expected_marker_qubits", + "new_geometries, expected_comparator_qubits, expected_obstacle_qubits, expected_copy_qubits, expected_monomial_qubits, expected_marker_qubits", [ ( [ @@ -272,7 +284,8 @@ def test_2d_ab_lattice_cuboid_ymonomial_combinations( ], 2, 1, - 12, + 4, + 8, 1, ), ( @@ -294,9 +307,10 @@ def test_2d_ab_lattice_cuboid_ymonomial_combinations( } ], ], - 1, + 0, 2, - 16, + 4, + 12, 1, ), ( @@ -334,7 +348,8 @@ def test_2d_ab_lattice_cuboid_ymonomial_combinations( ], 2, 2, - 24, + 4, + 20, 2, ), ], @@ -343,6 +358,7 @@ def test_set_geometries_updates_registers_for_ymonomial_cuboid_combinations( new_geometries, expected_comparator_qubits, expected_obstacle_qubits, + expected_copy_qubits, expected_monomial_qubits, expected_marker_qubits, ): @@ -364,6 +380,7 @@ def test_set_geometries_updates_registers_for_ymonomial_cuboid_combinations( assert lattice.num_comparator_qubits == expected_comparator_qubits assert lattice.num_obstacle_qubits == expected_obstacle_qubits + assert lattice.num_copy_qubits == expected_copy_qubits assert lattice.num_monomial_qubits == expected_monomial_qubits assert lattice.num_marker_qubits == expected_marker_qubits assert lattice.has_multiple_geometries() @@ -375,11 +392,58 @@ def test_set_geometries_updates_registers_for_ymonomial_cuboid_combinations( 1 if expected_comparator_qubits > 0 else 0 ) - if expected_monomial_qubits > 0: + if expected_copy_qubits > 0: assert len(lattice.copy_register) == 1 - assert len(lattice.monomial_register) == 1 else: assert len(lattice.copy_register) == 0 + + if expected_monomial_qubits > 0: + assert len(lattice.monomial_register) == 1 + else: assert len(lattice.monomial_register) == 0 assert len(lattice.marker_index()) == expected_marker_qubits + + +def test_2d_ymonomial_register_sizes_and_indices(): + lattice = ABLattice( + { + "lattice": {"dim": {"x": 16, "y": 16}, "velocities": "D2Q4"}, + "geometry": [ + { + "shape": "ymonomial", + "exponent": 3, + "comparator": "<=", + "boundary": "bounceback", + } + ], + } + ) + + assert lattice.num_copy_qubits == 4 + assert lattice.num_monomial_qubits == 12 + assert len(lattice.ancillae_copy_index()) == 4 + assert len(lattice.ancillae_monomial_index()) == 12 + assert set(lattice.ancillae_copy_index()).isdisjoint(set(lattice.ancillae_monomial_index())) + + +def test_3d_cuboid_comparator_qubits_equal_num_dims(): + lattice = ABLattice( + { + "lattice": {"dim": {"x": 8, "y": 8, "z": 8}, "velocities": "D3Q6"}, + "geometry": [ + { + "shape": "cuboid", + "x": [1, 3], + "y": [2, 4], + "z": [0, 2], + "boundary": "bounceback", + } + ], + } + ) + + assert lattice.num_dims == 3 + assert lattice.num_comparator_qubits == 3 + assert len(lattice.ancillae_comparator_index()) == 3 + assert lattice.ancillae_comparator_index(0) == lattice.ancillae_comparator_index() From f8f0d6e5448ffe0e67b4d7f18bdbc5f2ee91be68 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Fri, 20 Feb 2026 14:29:53 +0100 Subject: [PATCH 60/78] Add tests for monomial and comparator components --- qlbm/tools/__init__.py | 2 + ...ab_zone_agnostic_reflection_oracle_test.py | 82 +++++++++++++ test/unit/two_register_comparator_test.py | 115 ++++++++++++++++++ test/unit/ymonomial_test.py | 18 +++ 4 files changed, 217 insertions(+) create mode 100644 test/unit/ab/ab_zone_agnostic_reflection_oracle_test.py create mode 100644 test/unit/two_register_comparator_test.py create mode 100644 test/unit/ymonomial_test.py diff --git a/qlbm/tools/__init__.py b/qlbm/tools/__init__.py index 94019bf..52f275f 100644 --- a/qlbm/tools/__init__.py +++ b/qlbm/tools/__init__.py @@ -8,6 +8,7 @@ ResultsException, ) from .utils import ( + ComparatorMode, bit_value, create_directory_and_parents, dimension_letter, @@ -34,4 +35,5 @@ "dimension_letter", "is_two_pow", "get_time_series", + "ComparatorMode", ] diff --git a/test/unit/ab/ab_zone_agnostic_reflection_oracle_test.py b/test/unit/ab/ab_zone_agnostic_reflection_oracle_test.py new file mode 100644 index 0000000..42809d4 --- /dev/null +++ b/test/unit/ab/ab_zone_agnostic_reflection_oracle_test.py @@ -0,0 +1,82 @@ +import pytest +from qiskit import QuantumCircuit + +from qlbm.components.ab.reflection.agnosotic_reflection import ( + ABZoneAgnosticReflectionOracle, +) +from qlbm.lattice import ABLattice +from qlbm.tools.exceptions import CircuitException + + +def test_ymonomial_oracle_raises_for_non_quadratic_exponent(): + lattice = ABLattice( + { + "lattice": {"dim": {"x": 8, "y": 8}, "velocities": "D2Q4"}, + "geometry": [ + { + "shape": "ymonomial", + "exponent": 1, + "comparator": "<=", + "boundary": "bounceback", + } + ], + } + ) + + shape = lattice.shapes["bounceback"][0] + + with pytest.raises(CircuitException) as excinfo: + ABZoneAgnosticReflectionOracle(lattice, shape) + + assert ( + "YMonomial oracle is a work in progress: only exponent=2 (x^2) is currently supported." + == str(excinfo.value) + ) + + +def test_ymonomial_oracle_raises_for_mismatched_y_and_result_registers(): + lattice = ABLattice( + { + "lattice": {"dim": {"x": 8, "y": 8}, "velocities": "D2Q4"}, + "geometry": [ + { + "shape": "ymonomial", + "exponent": 2, + "comparator": "<=", + "boundary": "bounceback", + } + ], + } + ) + + shape = lattice.shapes["bounceback"][0] + + with pytest.raises(CircuitException) as excinfo: + ABZoneAgnosticReflectionOracle(lattice, shape) + + assert ( + "YMonomial oracle is a work in progress: only configurations with equal y and monomial result register sizes are currently supported." + == str(excinfo.value) + ) + + +def test_ymonomial_oracle_builds_for_supported_work_in_progress_case(): + lattice = ABLattice( + { + "lattice": {"dim": {"x": 2, "y": 4}, "velocities": "D2Q4"}, + "geometry": [ + { + "shape": "ymonomial", + "exponent": 2, + "comparator": "<=", + "boundary": "bounceback", + } + ], + } + ) + + shape = lattice.shapes["bounceback"][0] + oracle = ABZoneAgnosticReflectionOracle(lattice, shape) + + assert isinstance(oracle.circuit, QuantumCircuit) + assert oracle.circuit is not None diff --git a/test/unit/two_register_comparator_test.py b/test/unit/two_register_comparator_test.py new file mode 100644 index 0000000..c41a30d --- /dev/null +++ b/test/unit/two_register_comparator_test.py @@ -0,0 +1,115 @@ +import pytest +from qiskit import QuantumCircuit +from qiskit.quantum_info import Statevector + +from qlbm.components.ms.primitives import TwoRegisterComparator +from qlbm.tools.utils import ComparatorMode + + +def _state_index(x_value: int, y_value: int, out_value: int, num_qubits: int) -> int: + return x_value + (y_value << num_qubits) + (out_value << (2 * num_qubits)) + + +def _extract_register_value(state_index: int, start: int, num_qubits: int) -> int: + # Reconstruct an integer from a contiguous little-endian qubit slice. + return sum(((state_index >> (start + bit)) & 1) << bit for bit in range(num_qubits)) + + +@pytest.mark.parametrize( + "mode", + [ComparatorMode.LT, ComparatorMode.LE, ComparatorMode.GT, ComparatorMode.GE], +) +@pytest.mark.parametrize("num_qubits", [1, 2, 3]) +def test_two_register_comparator_all_modes(mode: ComparatorMode, num_qubits: int): + # Build one comparator circuit per (mode, width) and reuse it for all inputs. + comparator = TwoRegisterComparator(num_qubits=num_qubits, mode=mode) + operator = mode.to_operator() + + # Exhaustively verify all (x, y) inputs for this register width. + for x_value in range(2**num_qubits): + for y_value in range(2**num_qubits): + qc = QuantumCircuit(2 * num_qubits + 1) + + # Prepare |x>|y>|0> in computational basis (LSB at the lower qubit index). + for bit in range(num_qubits): + if (x_value >> bit) & 1: + qc.x(bit) + if (y_value >> bit) & 1: + qc.x(num_qubits + bit) + + # Apply the reversible two-register comparator. + qc.compose(comparator.circuit, inplace=True) + + # The circuit is deterministic on basis input; a single basis state should have unit amplitude. + state = Statevector.from_instruction(qc) + amplitudes = state.data + max_index = max(range(len(amplitudes)), key=lambda idx: abs(amplitudes[idx])) + max_amplitude = amplitudes[max_index] + + # Ensure no superposition/leakage due to incorrect uncomputation. + assert abs(abs(max_amplitude) - 1.0) < 1e-9 + + x_after = _extract_register_value(max_index, 0, num_qubits) + y_after = _extract_register_value(max_index, num_qubits, num_qubits) + out_after = (max_index >> (2 * num_qubits)) & 1 + + # Comparator must preserve both input registers exactly. + assert x_after == x_value + assert y_after == y_value + + # Output ancilla must encode the selected inequality mode. + assert out_after == int(operator(x_value, y_value)) + + +@pytest.mark.parametrize( + "mode", + [ComparatorMode.LT, ComparatorMode.LE, ComparatorMode.GT, ComparatorMode.GE], +) +def test_two_register_comparator_superposition_inputs(mode: ComparatorMode): + num_qubits = 2 + num_total_qubits = 2 * num_qubits + 1 + comparator = TwoRegisterComparator(num_qubits=num_qubits, mode=mode) + operator = mode.to_operator() + + def assert_expected_superposition(circuit: QuantumCircuit): + input_state = Statevector.from_instruction(circuit) + + # Build expected output by routing each |x,y,0> amplitude to |x,y,f(x,y)>. + expected = [0j] * (2**num_total_qubits) + for x_value in range(2**num_qubits): + for y_value in range(2**num_qubits): + source_idx = _state_index(x_value, y_value, 0, num_qubits) + target_idx = _state_index( + x_value, + y_value, + int(operator(x_value, y_value)), + num_qubits, + ) + expected[target_idx] = input_state.data[source_idx] + + actual_state = input_state.evolve(comparator.circuit) + expected_state = Statevector(expected) + + assert actual_state.equiv(expected_state) + + # Superposition over x only (y fixed to 2). + x_superposed = QuantumCircuit(num_total_qubits) + x_superposed.h(0) + x_superposed.h(1) + x_superposed.x(num_qubits + 1) + assert_expected_superposition(x_superposed) + + # Superposition over y only (x fixed to 1). + y_superposed = QuantumCircuit(num_total_qubits) + y_superposed.x(0) + y_superposed.h(num_qubits) + y_superposed.h(num_qubits + 1) + assert_expected_superposition(y_superposed) + + # Superposition over both registers. + both_superposed = QuantumCircuit(num_total_qubits) + both_superposed.h(0) + both_superposed.h(1) + both_superposed.h(num_qubits) + both_superposed.h(num_qubits + 1) + assert_expected_superposition(both_superposed) \ No newline at end of file diff --git a/test/unit/ymonomial_test.py b/test/unit/ymonomial_test.py new file mode 100644 index 0000000..32e261a --- /dev/null +++ b/test/unit/ymonomial_test.py @@ -0,0 +1,18 @@ +from qlbm.lattice.geometry.shapes.ymonomial import YMonomial +from qlbm.tools.utils import ComparatorMode + + +def test_ymonomial_stl_mesh_stays_within_lattice_bounds(): + ymonomial = YMonomial([2, 4], "bounceback", 2, ComparatorMode.LE) + + ymonomial_mesh = ymonomial.stl_mesh() + vertices = ymonomial_mesh.vectors.reshape(-1, 3) + max_coords = vertices.max(axis=0) + min_coords = vertices.min(axis=0) + + assert min_coords[0] >= 0 + assert min_coords[1] >= 0 + assert max_coords[0] <= 2**ymonomial.num_grid_qubits[0] - 1 + assert max_coords[1] <= 2**ymonomial.num_grid_qubits[1] - 1 + assert max_coords[0] == 2**ymonomial.num_grid_qubits[0] - 1 + assert max_coords[1] > 9 From 4484e004c8ba7f80658a16f8b3420343b43e5792 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Fri, 20 Feb 2026 16:02:00 +0100 Subject: [PATCH 61/78] Move comparators to separate file, improve documentation --- qlbm/components/__init__.py | 6 +- qlbm/components/ab/ab.py | 2 - .../ab/reflection/agnosotic_reflection.py | 61 ++++-- .../ab/reflection/standard_reflection.py | 2 +- qlbm/components/common/__init__.py | 3 + qlbm/components/common/adders.py | 8 +- qlbm/components/common/comparators.py | 203 ++++++++++++++++++ qlbm/components/ms/__init__.py | 12 -- qlbm/components/ms/bounceback_reflection.py | 8 +- qlbm/components/ms/primitives.py | 197 +---------------- qlbm/components/ms/specular_reflection.py | 8 +- .../spacetime/initial/volumetric.py | 4 +- .../spacetime/reflection/volumetric.py | 4 +- qlbm/lattice/geometry/shapes/base.py | 33 +++ qlbm/lattice/geometry/shapes/block.py | 60 ++++++ qlbm/lattice/geometry/shapes/circle.py | 18 ++ qlbm/lattice/geometry/shapes/ymonomial.py | 39 +++- test/unit/two_register_comparator_test.py | 2 +- 18 files changed, 424 insertions(+), 246 deletions(-) create mode 100644 qlbm/components/common/comparators.py diff --git a/qlbm/components/__init__.py b/qlbm/components/__init__.py index 479a01c..cb7fe52 100644 --- a/qlbm/components/__init__.py +++ b/qlbm/components/__init__.py @@ -1,6 +1,5 @@ """Modular and extendible quantum circuits that perform parts of the QLBM algorithm.""" -from ..tools.utils import ComparatorMode from .ab import ( ABQLBM, ABDiscreteUniformInitialConditions, @@ -29,6 +28,7 @@ HammingWeightAdder, ) from .common.adders import ParameterizedDraperAdder, ParameterizedPhaseShift, PhaseShift +from .common.comparators import SingleRegisterComparator from .cqlbm import CQLBM from .lqlga import ( LQLGA, @@ -47,7 +47,6 @@ MSStreamingOperator, SpecularReflectionOperator, ) -from .ms.primitives import Comparator from .ms.streaming import ( ControlledIncrementer, StreamingAncillaPreparation, @@ -61,8 +60,7 @@ "MSOperator", "SpaceTimeOperator", "LBMAlgorithm", - "ComparatorMode", - "Comparator", + "SingleRegisterComparator", "ParameterizedDraperAdder", "PhaseShift", "ParameterizedPhaseShift", diff --git a/qlbm/components/ab/ab.py b/qlbm/components/ab/ab.py index 7480171..a75b763 100644 --- a/qlbm/components/ab/ab.py +++ b/qlbm/components/ab/ab.py @@ -10,9 +10,7 @@ ABZoneAgnosticReflectionOperator, ) from qlbm.components.base import LBMAlgorithm -from qlbm.lattice.geometry.shapes.block import Block from qlbm.lattice.lattices.ab_lattice import ABLattice -from qlbm.tools.exceptions import LatticeException from .streaming import ABStreamingOperator diff --git a/qlbm/components/ab/reflection/agnosotic_reflection.py b/qlbm/components/ab/reflection/agnosotic_reflection.py index 4c27ee5..48e557e 100644 --- a/qlbm/components/ab/reflection/agnosotic_reflection.py +++ b/qlbm/components/ab/reflection/agnosotic_reflection.py @@ -12,7 +12,10 @@ from qlbm.components.ab.streaming import ABStreamingOperator from qlbm.components.base import LBMPrimitive from qlbm.components.common.adders import ParameterizedDraperAdder -from qlbm.components.ms.primitives import Comparator, TwoRegisterComparator +from qlbm.components.common.comparators import ( + SingleRegisterComparator, + TwoRegisterComparator, +) from qlbm.lattice.geometry.shapes import Block, Circle, YMonomial from qlbm.lattice.geometry.shapes.base import Shape from qlbm.lattice.lattices.ab_lattice import ABLattice @@ -32,7 +35,8 @@ class ABZoneAgnosticReflectionOperator(ABReflectionOperator): Example usage: - .. code-block:: python + .. plot:: + :include-source: from qlbm.components.ab import ABZoneAgnosticReflectionOperator from qlbm.lattice import ABLattice @@ -51,7 +55,7 @@ class ABZoneAgnosticReflectionOperator(ABReflectionOperator): } ) - ABZoneAgnosticReflectionOperator(lattice, blocks=lattice.shapes["bounceback"]).draw("mpl") + ABZoneAgnosticReflectionOperator(lattice, shapes=lattice.shapes["bounceback"]).draw("mpl") """ @@ -103,7 +107,7 @@ def create_circuit(self) -> QuantumCircuit: for shape in self.shapes: oracle.compose( ABZoneAgnosticReflectionOracle( - self.lattice, shape, logger=self.logger + self.lattice, shape, logger=self.logger # type: ignore ).circuit, inplace=True, ) @@ -140,13 +144,14 @@ class ABZoneAgnosticReflectionOracle(LBMPrimitive): r""" Implementation of the oracle required for :class:`.ABZoneAgnosticReflectionOperator`. - An oracle is an operator :math:`U_{\omega}` for an obstacle's region - :math:`\omega` such that, in the amplitude-based encoding, - :math:`U_\omega\ket{x}\ket{v}\ket{0}_\mathbb{o} = \ket{x}\ket{v}\ket{x \in \omega}_\mathbb{o}`. + An oracle is an operator :math:`U_{\Omega}` for an obstacle's region + :math:`\Omega` such that, in the amplitude-based encoding, + :math:`U_\Omega\ket{x}\ket{v}\ket{0}_\mathbb{o} = \ket{x}\ket{v}\ket{x \in \Omega}_\mathbb{o}`. Intuitively, the operator flips the object ancilla qubit if and only if the position :math:`x` falls within the bounds of the object. - Currently, the only available implementation is for 2D axis-aligned objects. + Currently, the only available implementation is for 2D axis-aligned :class:`.Block` + and :class:`.YMonomial` objects. .. important:: @@ -160,16 +165,17 @@ class ABZoneAgnosticReflectionOracle(LBMPrimitive): This operation relies on basic arithmetic through the :class:`.ParameterizedDraperAdder` class and comparison operation through the :class:`Comparator` circuits. - Example usage: + Example usage for a cuboid :class:`.Block`: - .. code-block:: python + .. plot:: + :include-source: - from qlbm.components.ab import ABZoneAgnosticReflectionOperator + from qlbm.components.ab.reflection import ABZoneAgnosticReflectionOracle from qlbm.lattice import ABLattice lattice = ABLattice( { - "lattice": {"dim": {"x": 4, "y": 4}, "velocities": "d2q9"}, + "lattice": {"dim": {"x": 4, "y": 16}, "velocities": "d2q9"}, "geometry": [ { "shape": "cuboid", @@ -181,7 +187,32 @@ class ABZoneAgnosticReflectionOracle(LBMPrimitive): } ) - ABZoneAgnosticReflectionOperator(lattice, blocks=lattice.shapes["bounceback"]).draw("mpl") + ABZoneAgnosticReflectionOracle(lattice, shape=lattice.shapes["bounceback"][0]).draw("mpl") + + And for a :class:`.YMonomial`: + + .. plot:: + :include-source: + + from qlbm.components.ab.reflection import ABZoneAgnosticReflectionOracle + from qlbm.lattice import ABLattice + + lattice = ABLattice( + { + "lattice": {"dim": {"x": 4, "y": 16}, "velocities": "d2q9"}, + "geometry": [ + { + "shape": "ymonomial", + "exponent": 2, + "comparator": "<", + "boundary": "bounceback", + } + ], + } + ) + + ABZoneAgnosticReflectionOracle(lattice, shape=lattice.shapes["bounceback"][0]).draw("mpl") + """ @@ -231,7 +262,7 @@ def __create_circuit_block(self) -> QuantumCircuit: ) circuit.compose( - Comparator( + SingleRegisterComparator( num_qubits=len(self.lattice.grid_index(dim)) + 1, num_to_compare=block.bounds[dim][1] - block.bounds[dim][0], mode=ComparatorMode.LE, @@ -248,7 +279,7 @@ def __create_circuit_block(self) -> QuantumCircuit: for dim in range(self.lattice.num_dims): circuit.compose( - Comparator( + SingleRegisterComparator( num_qubits=len(self.lattice.grid_index(dim)) + 1, num_to_compare=block.bounds[dim][1] - block.bounds[dim][0], mode=ComparatorMode.LE, diff --git a/qlbm/components/ab/reflection/standard_reflection.py b/qlbm/components/ab/reflection/standard_reflection.py index 6721908..c5d8235 100644 --- a/qlbm/components/ab/reflection/standard_reflection.py +++ b/qlbm/components/ab/reflection/standard_reflection.py @@ -3,7 +3,7 @@ from itertools import product from logging import Logger, getLogger from time import perf_counter_ns -from typing import List, Tuple, cast +from typing import List, Tuple from qiskit import QuantumCircuit from qiskit.circuit.library import MCMTGate, XGate diff --git a/qlbm/components/common/__init__.py b/qlbm/components/common/__init__.py index 4c050bb..3c9f2fd 100644 --- a/qlbm/components/common/__init__.py +++ b/qlbm/components/common/__init__.py @@ -2,6 +2,7 @@ from .adders import ParameterizedDraperAdder, ParameterizedPhaseShift, PhaseShift from .cbse_collision import EQCCollisionOperator, EQCPermutation, EQCRedistribution +from .comparators import SingleRegisterComparator, TwoRegisterComparator from .primitives import ( AdditionConversion, EmptyPrimitive, @@ -26,4 +27,6 @@ "TruncatedQFT", "UniformStatePrep", "MCSwap", + "SingleRegisterComparator", + "TwoRegisterComparator", ] diff --git a/qlbm/components/common/adders.py b/qlbm/components/common/adders.py index d411cef..6405086 100644 --- a/qlbm/components/common/adders.py +++ b/qlbm/components/common/adders.py @@ -37,7 +37,7 @@ class ParameterizedPhaseShift(LBMPrimitive): .. plot:: :include-source: - from qlbm.components import ParameterizedPhaseShift + from qlbm.components.common.adders import ParameterizedPhaseShift # A phase shift of 5 qubits, adding the number 2 ParameterizedPhaseShift(num_qubits=5, num_to_add=2, positive=True).draw("mpl") @@ -47,7 +47,7 @@ class ParameterizedPhaseShift(LBMPrimitive): .. plot:: :include-source: - from qlbm.components import ParameterizedPhaseShift + from qlbm.components.common.adders import ParameterizedPhaseShift # A phase shift of 5 qubits, controlled subtracting the number 1 ParameterizedPhaseShift(num_qubits=5, num_to_add=1, positive=False, num_ctrl_qubits=3).draw("mpl") @@ -146,7 +146,7 @@ class ParameterizedDraperAdder(LBMPrimitive): .. plot:: :include-source: - from qlbm.components import ParameterizedDraperAdder + from qlbm.components.common.adders import ParameterizedDraperAdder ParameterizedDraperAdder(4, 1, True).draw("mpl") """ @@ -240,7 +240,7 @@ class PhaseShift(LBMPrimitive): .. plot:: :include-source: - from qlbm.components.ms import PhaseShift + from qlbm.components.common.adders import PhaseShift # A phase shift of 5 qubits PhaseShift(num_qubits=5, positive=False).draw("mpl") diff --git a/qlbm/components/common/comparators.py b/qlbm/components/common/comparators.py new file mode 100644 index 0000000..122d03f --- /dev/null +++ b/qlbm/components/common/comparators.py @@ -0,0 +1,203 @@ +"""Quantum circuits that perform arithmetic comparison operations.""" + +from logging import Logger, getLogger +from time import perf_counter_ns +from typing import List + +from qiskit import QuantumCircuit +from qiskit.circuit.library import DraperQFTAdder +from typing_extensions import override + +from qlbm.components.base import LBMPrimitive +from qlbm.components.common.adders import ParameterizedDraperAdder +from qlbm.tools.utils import ComparatorMode + + +class TwoRegisterComparator(LBMPrimitive): + """ + Quantum comparator primitive that compares the states of 2 registers of ``num_qubits`` qubits a :class:`.ComparatorMode`. + + The generate circuit is of size ``2*num_qubits+1``, where the last qubit of the register holds the boolean result. + + Example usage: + + .. plot:: + :include-source: + + from qlbm.components.common.comparators import TwoRegisterComparator + from qlbm.tools.utils import ComparatorMode + + # Compare two registers of size 4 + TwoRegisterComparator(num_qubits=4, mode=ComparatorMode.LT).draw("mpl") + """ + + def __init__( + self, + num_qubits: int, + mode: ComparatorMode, + logger: Logger = getLogger("qlbm"), + ) -> None: + super().__init__(logger) + + self.num_qubits = num_qubits + self.mode = mode + + self.logger.info(f"Creating circuit {str(self)}...") + circuit_creation_start_time = perf_counter_ns() + self.circuit = self.create_circuit() + self.logger.info( + f"Creating circuit {str(self)} took {perf_counter_ns() - circuit_creation_start_time} (ns)" + ) + + @override + def create_circuit(self) -> QuantumCircuit: + circuit = QuantumCircuit(2 * self.num_qubits + 1) + x_register = list(range(self.num_qubits)) + y_register = list(range(self.num_qubits, 2 * self.num_qubits)) + output_qubit = 2 * self.num_qubits + + match self.mode: + case ComparatorMode.GT: + self.__compose_gt(circuit, x_register, y_register, output_qubit) + case ComparatorMode.LE: + self.__compose_gt(circuit, x_register, y_register, output_qubit) + circuit.x(output_qubit) + case ComparatorMode.LT: + self.__compose_gt(circuit, y_register, x_register, output_qubit) + case ComparatorMode.GE: + self.__compose_gt(circuit, y_register, x_register, output_qubit) + circuit.x(output_qubit) + case _: + raise ValueError("Invalid Comparator Mode") + + return circuit + + def __compose_gt( + self, + circuit: QuantumCircuit, + x_register: List[int], + y_register: List[int], + output_qubit: int, + ) -> None: + add_half = DraperQFTAdder(self.num_qubits, kind="half") + add_fixed_inv = DraperQFTAdder(self.num_qubits, kind="fixed").inverse() + + circuit.x(y_register) + circuit.compose( + add_half, + qubits=x_register + y_register + [output_qubit], + inplace=True, + ) + circuit.compose( + add_fixed_inv, + qubits=x_register + y_register, + inplace=True, + ) + circuit.x(y_register) + + @override + def __str__(self) -> str: + return f"[Primitive TwoRegisterComparator of {self.num_qubits} qubits, mode={self.mode}]" + + +class SingleRegisterComparator(LBMPrimitive): + """ + Quantum comparator primitive that compares a quantum state of ``num_qubits`` qubits and an integer ``num_to_compare`` with respect to a :class:`.ComparatorMode`. + + ========================= ====================================================================== + Attribute Summary + ========================= ====================================================================== + :attr:`num_qubits` Number of qubits encoding the integer to compare. + :attr:`num_to_compare` The integer to compare against. + :attr:`mode` The :class:`.ComparatorMode` used to compare the two numbers. + :attr:`logger` The performance logger, by default getLogger("qlbm") + ========================= ====================================================================== + + Example usage: + + .. plot:: + :include-source: + + from qlbm.components.common.comparators import SingleRegisterComparator + from qlbm.tools.utils import ComparatorMode + + # On a 5 qubit register, compare the number 3 + SingleRegisterComparator(num_qubits=5, + num_to_compare=3, + mode=ComparatorMode.LT).draw("mpl") + """ + + def __init__( + self, + num_qubits: int, + num_to_compare: int, + mode: ComparatorMode, + logger: Logger = getLogger("qlbm"), + ) -> None: + super().__init__(logger) + + self.num_qubits = num_qubits + self.num_to_compare = num_to_compare + self.mode = mode + + self.logger.info(f"Creating circuit {str(self)}...") + circuit_creation_start_time = perf_counter_ns() + self.circuit = self.create_circuit() + self.logger.info( + f"Creating circuit {str(self)} took {perf_counter_ns() - circuit_creation_start_time} (ns)" + ) + + @override + def create_circuit(self) -> QuantumCircuit: + return self.__create_circuit(self.num_qubits, self.num_to_compare, self.mode) + + def __create_circuit( + self, num_qubits: int, num_to_compare: int, mode: ComparatorMode + ) -> QuantumCircuit: + circuit = QuantumCircuit(num_qubits) + + match mode: + case ComparatorMode.LT: + circuit.compose( + ParameterizedDraperAdder( + num_qubits, num_to_compare, positive=False, logger=self.logger + ).circuit, + inplace=True, + ) + circuit.compose( + ParameterizedDraperAdder( + num_qubits - 1, + num_to_compare, + positive=True, + logger=self.logger, + ).circuit, + inplace=True, + qubits=range(num_qubits - 1), + ) + return circuit + case ComparatorMode.LE: + if num_to_compare == 2 ** (num_qubits - 1) - 1: + return self.__create_circuit(num_qubits, 0, ComparatorMode.GE) + + return self.__create_circuit( + num_qubits, num_to_compare + 1, ComparatorMode.LT + ) + case ComparatorMode.GT: + if num_to_compare == 2 ** (num_qubits - 1) - 1: + return circuit + else: + return self.__create_circuit( + num_qubits, num_to_compare + 1, ComparatorMode.GE + ) + case ComparatorMode.GE: + circuit = self.__create_circuit( + num_qubits, num_to_compare, ComparatorMode.LT + ) + circuit.x(num_qubits - 1) + return circuit + case _: + raise ValueError("Invalid Comparator Mode") + + @override + def __str__(self) -> str: + return f"[Primitive Comparator of {self.num_qubits} and {self.num_to_compare}, mode={self.mode}]" diff --git a/qlbm/components/ms/__init__.py b/qlbm/components/ms/__init__.py index 4f0bb36..136dc72 100644 --- a/qlbm/components/ms/__init__.py +++ b/qlbm/components/ms/__init__.py @@ -1,18 +1,11 @@ """Modular qlbm quantum circuit components for the MSQLBM algorithm :cite:p:`collisionless`.""" -from ...tools.utils import ComparatorMode -from ..common.adders import ( - ParameterizedDraperAdder, - ParameterizedPhaseShift, - PhaseShift, -) from .bounceback_reflection import ( BounceBackReflectionOperator, BounceBackWallComparator, ) from .msqlbm import MSQLBM from .primitives import ( - Comparator, EdgeComparator, GridMeasurement, MSInitialConditions, @@ -26,17 +19,12 @@ ) __all__ = [ - "ComparatorMode", - "Comparator", - "ParameterizedDraperAdder", "StreamingAncillaPreparation", "ControlledIncrementer", "GridMeasurement", "EdgeComparator", "MSInitialConditions", "MSInitialConditions3DSlim", - "PhaseShift", - "ParameterizedPhaseShift", "MSStreamingOperator", "SpecularReflectionOperator", "SpecularWallComparator", diff --git a/qlbm/components/ms/bounceback_reflection.py b/qlbm/components/ms/bounceback_reflection.py index 61245e9..dae0ec4 100644 --- a/qlbm/components/ms/bounceback_reflection.py +++ b/qlbm/components/ms/bounceback_reflection.py @@ -9,8 +9,8 @@ from typing_extensions import override from qlbm.components.base import LBMPrimitive, MSOperator -from qlbm.components.ms.primitives import ( - Comparator, +from qlbm.components.common.comparators import ( + SingleRegisterComparator, ) from qlbm.components.ms.specular_reflection import SpecularWallComparator from qlbm.lattice import MSLattice @@ -83,7 +83,7 @@ def create_circuit(self) -> QuantumCircuit: # If the wall is inside the object, we build the comparators # Differently, as to not overlap lb_comparators = [ - Comparator( + SingleRegisterComparator( self.lattice.num_gridpoints[wall_alignment_dim].bit_length() + 1, self.wall.lower_bounds[c], ComparatorMode.GE @@ -95,7 +95,7 @@ def create_circuit(self) -> QuantumCircuit: ] ub_comparators = [ - Comparator( + SingleRegisterComparator( self.lattice.num_gridpoints[wall_alignment_dim].bit_length() + 1, self.wall.upper_bounds[c], ComparatorMode.LE diff --git a/qlbm/components/ms/primitives.py b/qlbm/components/ms/primitives.py index 947f599..f1d8515 100644 --- a/qlbm/components/ms/primitives.py +++ b/qlbm/components/ms/primitives.py @@ -5,11 +5,10 @@ from typing import List from qiskit import ClassicalRegister, QuantumCircuit -from qiskit.circuit.library import DraperQFTAdder from typing_extensions import override from qlbm.components.base import LBMPrimitive -from qlbm.components.common.adders import ParameterizedDraperAdder +from qlbm.components.common.comparators import SingleRegisterComparator from qlbm.lattice import MSLattice from qlbm.lattice.geometry.encodings.ms import ReflectionResetEdge from qlbm.tools import flatten @@ -268,196 +267,6 @@ def __str__(self) -> str: return f"[Primitive InitialConditions with lattice {self.lattice}]" -class Comparator(LBMPrimitive): - """ - Quantum comparator primitive that compares a quantum state of ``num_qubits`` qubits and an integer ``num_to_compare`` with respect to a :class:`.ComparatorMode`. - - ========================= ====================================================================== - Attribute Summary - ========================= ====================================================================== - :attr:`num_qubits` Number of qubits encoding the integer to compare. - :attr:`num_to_compare` The integer to compare against. - :attr:`mode` The :class:`.ComparatorMode` used to compare the two numbers. - :attr:`logger` The performance logger, by default getLogger("qlbm") - ========================= ====================================================================== - - Example usage: - - .. plot:: - :include-source: - - from qlbm.components.ms import Comparator - from qlbm.tools import ComparatorMode - - # On a 5 qubit register, compare the number 3 - Comparator(num_qubits=5, - num_to_compare=3, - mode=ComparatorMode.LT).draw("mpl") - """ - - def __init__( - self, - num_qubits: int, - num_to_compare: int, - mode: ComparatorMode, - logger: Logger = getLogger("qlbm"), - ) -> None: - super().__init__(logger) - - self.num_qubits = num_qubits - self.num_to_compare = num_to_compare - self.mode = mode - - self.logger.info(f"Creating circuit {str(self)}...") - circuit_creation_start_time = perf_counter_ns() - self.circuit = self.create_circuit() - self.logger.info( - f"Creating circuit {str(self)} took {perf_counter_ns() - circuit_creation_start_time} (ns)" - ) - - @override - def create_circuit(self) -> QuantumCircuit: - return self.__create_circuit(self.num_qubits, self.num_to_compare, self.mode) - - def __create_circuit( - self, num_qubits: int, num_to_compare: int, mode: ComparatorMode - ) -> QuantumCircuit: - circuit = QuantumCircuit(num_qubits) - - match mode: - case ComparatorMode.LT: - circuit.compose( - ParameterizedDraperAdder( - num_qubits, num_to_compare, positive=False, logger=self.logger - ).circuit, - inplace=True, - ) - circuit.compose( - ParameterizedDraperAdder( - num_qubits - 1, - num_to_compare, - positive=True, - logger=self.logger, - ).circuit, - inplace=True, - qubits=range(num_qubits - 1), - ) - return circuit - case ComparatorMode.LE: - if num_to_compare == 2 ** (num_qubits - 1) - 1: - return self.__create_circuit(num_qubits, 0, ComparatorMode.GE) - - return self.__create_circuit( - num_qubits, num_to_compare + 1, ComparatorMode.LT - ) - case ComparatorMode.GT: - if num_to_compare == 2 ** (num_qubits - 1) - 1: - return circuit - else: - return self.__create_circuit( - num_qubits, num_to_compare + 1, ComparatorMode.GE - ) - case ComparatorMode.GE: - circuit = self.__create_circuit( - num_qubits, num_to_compare, ComparatorMode.LT - ) - circuit.x(num_qubits - 1) - return circuit - case _: - raise ValueError("Invalid Comparator Mode") - - @override - def __str__(self) -> str: - return f"[Primitive Comparator of {self.num_qubits} and {self.num_to_compare}, mode={self.mode}]" - - -class TwoRegisterComparator(LBMPrimitive): - """ - Quantum comparator primitive that compares the states of 2 registers of ``num_qubits`` qubits a :class:`.ComparatorMode`. - - The generate circuit is of size ``2*num_qubits+1``, where the last qubit of the register holds the boolean result. - - Example usage: - - .. plot:: - :include-source: - - from qlbm.components.ms import TwoRegisterComparator - from qlbm.tools import ComparatorMode - - # Compare two registers of size 4 - TwoRegisterComparator(num_qubits=4, mode=ComparatorMode.LT).draw("mpl") - """ - - def __init__( - self, - num_qubits: int, - mode: ComparatorMode, - logger: Logger = getLogger("qlbm"), - ) -> None: - super().__init__(logger) - - self.num_qubits = num_qubits - self.mode = mode - - self.logger.info(f"Creating circuit {str(self)}...") - circuit_creation_start_time = perf_counter_ns() - self.circuit = self.create_circuit() - self.logger.info( - f"Creating circuit {str(self)} took {perf_counter_ns() - circuit_creation_start_time} (ns)" - ) - - @override - def create_circuit(self) -> QuantumCircuit: - circuit = QuantumCircuit(2 * self.num_qubits + 1) - x_register = list(range(self.num_qubits)) - y_register = list(range(self.num_qubits, 2 * self.num_qubits)) - output_qubit = 2 * self.num_qubits - - match self.mode: - case ComparatorMode.GT: - self.__compose_gt(circuit, x_register, y_register, output_qubit) - case ComparatorMode.LE: - self.__compose_gt(circuit, x_register, y_register, output_qubit) - circuit.x(output_qubit) - case ComparatorMode.LT: - self.__compose_gt(circuit, y_register, x_register, output_qubit) - case ComparatorMode.GE: - self.__compose_gt(circuit, y_register, x_register, output_qubit) - circuit.x(output_qubit) - case _: - raise ValueError("Invalid Comparator Mode") - - return circuit - - def __compose_gt( - self, - circuit: QuantumCircuit, - x_register: List[int], - y_register: List[int], - output_qubit: int, - ) -> None: - add_half = DraperQFTAdder(self.num_qubits, kind="half") - add_fixed_inv = DraperQFTAdder(self.num_qubits, kind="fixed").inverse() - - circuit.x(y_register) - circuit.compose( - add_half, - qubits=x_register + y_register + [output_qubit], - inplace=True, - ) - circuit.compose( - add_fixed_inv, - qubits=x_register + y_register, - inplace=True, - ) - circuit.x(y_register) - - @override - def __str__(self) -> str: - return f"[Primitive TwoRegisterComparator of {self.num_qubits} qubits, mode={self.mode}]" - - class EdgeComparator(LBMPrimitive): """ A primitive used in the 3D collisionless :class:`SpecularReflectionOperator` and :class:`BounceBackReflectionOperator` described in :cite:t:`collisionless`. @@ -508,13 +317,13 @@ def __init__( @override def create_circuit(self) -> QuantumCircuit: circuit = self.lattice.circuit.copy() - lb_comparator = Comparator( + lb_comparator = SingleRegisterComparator( self.lattice.num_gridpoints[self.edge.dim_disconnected].bit_length() + 1, self.edge.bounds_disconnected_dim[0], ComparatorMode.GE, logger=self.logger, ).circuit - ub_comparator = Comparator( + ub_comparator = SingleRegisterComparator( self.lattice.num_gridpoints[self.edge.dim_disconnected].bit_length() + 1, self.edge.bounds_disconnected_dim[1], ComparatorMode.LE, diff --git a/qlbm/components/ms/specular_reflection.py b/qlbm/components/ms/specular_reflection.py index 0ceeba5..31c4682 100644 --- a/qlbm/components/ms/specular_reflection.py +++ b/qlbm/components/ms/specular_reflection.py @@ -9,8 +9,8 @@ from typing_extensions import override from qlbm.components.base import LBMPrimitive, MSOperator -from qlbm.components.ms.primitives import ( - Comparator, +from qlbm.components.common.comparators import ( + SingleRegisterComparator, ) from qlbm.lattice import ( MSLattice, @@ -83,7 +83,7 @@ def create_circuit(self) -> QuantumCircuit: circuit = self.lattice.circuit.copy() lb_comparators = [ - Comparator( + SingleRegisterComparator( self.lattice.num_gridpoints[wall_alignment_dim].bit_length() + 1, self.wall.lower_bounds[c], ComparatorMode.GE, @@ -93,7 +93,7 @@ def create_circuit(self) -> QuantumCircuit: ] ub_comparators = [ - Comparator( + SingleRegisterComparator( self.lattice.num_gridpoints[wall_alignment_dim].bit_length() + 1, self.wall.upper_bounds[c], ComparatorMode.LE, diff --git a/qlbm/components/spacetime/initial/volumetric.py b/qlbm/components/spacetime/initial/volumetric.py index 73f6879..49ca0b3 100644 --- a/qlbm/components/spacetime/initial/volumetric.py +++ b/qlbm/components/spacetime/initial/volumetric.py @@ -9,7 +9,7 @@ from typing_extensions import override from qlbm.components.base import LBMPrimitive -from qlbm.components.ms.primitives import Comparator +from qlbm.components.common.comparators import SingleRegisterComparator from qlbm.lattice.lattices.spacetime_lattice import SpaceTimeLattice from qlbm.tools.utils import ComparatorMode, flatten @@ -83,7 +83,7 @@ def create_circuit(self) -> QuantumCircuit: comparators = [ [ - Comparator( + SingleRegisterComparator( self.lattice.properties.get_num_grid_qubits() + 1, pvb[0][bound], self.__adjusted_comparator_mode(bound), diff --git a/qlbm/components/spacetime/reflection/volumetric.py b/qlbm/components/spacetime/reflection/volumetric.py index 2f53d45..92774df 100644 --- a/qlbm/components/spacetime/reflection/volumetric.py +++ b/qlbm/components/spacetime/reflection/volumetric.py @@ -8,7 +8,7 @@ from typing_extensions import override from qlbm.components.base import SpaceTimeOperator -from qlbm.components.ms.primitives import Comparator +from qlbm.components.common.comparators import SingleRegisterComparator from qlbm.lattice.geometry.shapes.block import Block from qlbm.lattice.lattices.spacetime_lattice import SpaceTimeLattice from qlbm.tools.exceptions import CircuitException @@ -100,7 +100,7 @@ def create_circuit(self) -> QuantumCircuit: # Assemble the comparators only once comparators = [ [ - Comparator( + SingleRegisterComparator( self.lattice.properties.get_num_grid_qubits() + 1, pvb[0][bound], self.__adjusted_comparator_mode(bound), diff --git a/qlbm/lattice/geometry/shapes/base.py b/qlbm/lattice/geometry/shapes/base.py index 30f417c..6d7de93 100644 --- a/qlbm/lattice/geometry/shapes/base.py +++ b/qlbm/lattice/geometry/shapes/base.py @@ -17,7 +17,40 @@ class Shape(ABC): """Base class for all geometrical shapes.""" + num_grid_qubits: List[int] + """Number of grid-encoding qubits for each spatial dimension.""" + + boundary_condition: str + """Boundary condition mode associated with this shape.""" + + num_dims: int + """Number of spatial dimensions inferred from :attr:`num_grid_qubits`.""" + + previous_qubits: List[int] + """Cumulative per-dimension qubit offsets used for flattened indexing.""" + def __init__(self, num_grid_qubits: List[int], boundary_condition: str): + """ + Initialize a geometrical shape base object. + + Parameters + ---------- + num_grid_qubits : List[int] + Number of grid-encoding qubits per dimension. + boundary_condition : str + Boundary condition mode associated with this shape. + + Attributes + ---------- + num_grid_qubits : List[int] + Number of grid-encoding qubits per dimension. + boundary_condition : str + Boundary condition mode associated with this shape. + num_dims : int + Number of spatial dimensions inferred from ``num_grid_qubits``. + previous_qubits : List[int] + Cumulative qubit offsets used to flatten dimension-local qubit indices. + """ super().__init__() self.num_grid_qubits = num_grid_qubits diff --git a/qlbm/lattice/geometry/shapes/block.py b/qlbm/lattice/geometry/shapes/block.py index aa4c586..de5324e 100644 --- a/qlbm/lattice/geometry/shapes/block.py +++ b/qlbm/lattice/geometry/shapes/block.py @@ -79,8 +79,64 @@ class Block(SpaceTimeShape, LQLGAShape): - The ``List[ReflectionResetEdge]`` data encoding edges on the outside of the object that are adjacent either side of :attr:`corner_edges_3d`. These edges require additional logic in the quantum circuit for particles that have streamed without reflecting off the obstacle. There are 24 near-corner :class:`ReflectionResetEdge` \s per obstacle. * - :attr:`overlapping_near_corner_edge_points_3d` - The ``List[ReflectionPoint]`` data encoding the set of points at the intersections of :attr:`near_corner_edges_3d`. These points require additional logic in to account for the fact that the state of obstacle ancilla qubits was doubly reset (once by each edge). There are 24 such :class:`ReflectionPoint` \s per obstacle. + * - :attr:`num_gridpoints` + - The ``List[int]`` of gridpoint counts per dimension. + * - :attr:`mesh_vertices` + - The ``np.ndarray`` of obstacle mesh vertices used to build the ``stl`` representation. + * - :attr:`mesh_indices` + - The ``np.ndarray`` of triangle indices selecting faces from :attr:`mesh_vertices`. + * - :attr:`mesh_indices_list` + - A class-level ``List[np.ndarray]`` containing precomputed triangulations by dimensionality. + * - :attr:`ab_wall_indices_to_reset` + - A class-level lookup mapping wall configurations to velocity indices to reset for bounce-back schemes. + * - :attr:`ab_near_corner_indices_to_reset` + - A class-level lookup mapping near-corner configurations to velocity indices to reset. + * - :attr:`ab_corner_indices_to_reset` + - A class-level lookup mapping corner configurations to velocity indices to reset. """ + bounds: List[Tuple[int, int]] + """Lower and upper bounds of the block in each spatial dimension.""" + + num_gridpoints: List[int] + """Number of gridpoints in each spatial dimension.""" + + mesh_vertices: np.ndarray + """Vertices of the polygonal surface representation used for ``stl`` export.""" + + mesh_indices: np.ndarray + """Triangle index array selecting faces from :attr:`mesh_vertices`.""" + + inside_points_data: List[Tuple[DimensionalReflectionData, ...]] + """Per-dimension lower/upper reflection metadata for points inside the obstacle.""" + + outside_points_data: List[Tuple[DimensionalReflectionData, ...]] + """Per-dimension lower/upper reflection metadata for points outside the obstacle.""" + + walls_inside: List[List[ReflectionWall]] + """Reflection wall metadata for interior-facing obstacle walls by dimension.""" + + walls_outside: List[List[ReflectionWall]] + """Reflection wall metadata for exterior-facing obstacle walls by dimension.""" + + corners_inside: List[ReflectionPoint] + """Corner reflection points located on the interior boundary of the obstacle.""" + + corners_outside: List[ReflectionPoint] + """Corner reflection points located on the exterior boundary of the obstacle.""" + + near_corner_points_2d: List[ReflectionPoint] + """2D points adjacent to interior corners that require extra non-reflection logic.""" + + corner_edges_3d: List[ReflectionResetEdge] + """3D exterior edges adjacent to corners, used to reset ancilla state.""" + + near_corner_edges_3d: List[ReflectionResetEdge] + """3D exterior edges adjacent to :attr:`corner_edges_3d` requiring extra handling.""" + + overlapping_near_corner_edge_points_3d: List[ReflectionPoint] + """3D points where near-corner edge reset effects overlap and need compensation.""" + mesh_indices_list: List[np.ndarray] = [ np.array([[0, 1, 2], [1, 2, 3]]), np.array( @@ -100,6 +156,7 @@ class Block(SpaceTimeShape, LQLGAShape): ] ), ] + """Precomputed triangle-index templates by dimensionality for ``stl`` generation.""" ab_wall_indices_to_reset: Dict[ LatticeDiscretization, Dict[Tuple[int, bool], List[int]] @@ -111,6 +168,7 @@ class Block(SpaceTimeShape, LQLGAShape): (1, True): [2, 5, 6], } } + """Lookup of bounce-back velocity indices to reset for wall reflections.""" ab_near_corner_indices_to_reset: Dict[ LatticeDiscretization, Dict[int, Dict[Tuple[bool, ...], List[int]]] @@ -130,6 +188,7 @@ class Block(SpaceTimeShape, LQLGAShape): }, } } + """Lookup of bounce-back velocity indices to reset for near-corner reflections.""" ab_corner_indices_to_reset: Dict[ LatticeDiscretization, Dict[Tuple[bool, ...], List[int]] @@ -141,6 +200,7 @@ class Block(SpaceTimeShape, LQLGAShape): (True, True): [5], } } + """Lookup of bounce-back velocity indices to reset for corner reflections.""" def __init__( self, diff --git a/qlbm/lattice/geometry/shapes/circle.py b/qlbm/lattice/geometry/shapes/circle.py index 8979b34..fc9b657 100644 --- a/qlbm/lattice/geometry/shapes/circle.py +++ b/qlbm/lattice/geometry/shapes/circle.py @@ -47,11 +47,29 @@ class Circle(SpaceTimeShape): * - Attribute - Description + * - :attr:`center` + - The ``Tuple[int, ...]`` center coordinate of the circle. + * - :attr:`radius` + - The ``int`` radius of the circle in gridpoint units. + * - :attr:`num_mesh_segments` + - The ``int`` number of angular segments used for smooth ``stl`` mesh generation. * - :attr:`perimeter_points` - The ``List[Tuple[int, int]]`` of all gridpoints that lie on the perimeter of the circle, and are therefore relevant for boundary conditions. """ + center: Tuple[int, ...] + """Center coordinate of the circle in lattice grid coordinates.""" + + radius: int + """Circle radius in lattice gridpoint units.""" + + num_mesh_segments: int + """Number of angular segments used for smooth ``stl`` mesh construction.""" + + perimeter_points: List[Tuple[int, int]] + """All discrete gridpoints on the circle perimeter used for boundary handling.""" + def __init__( self, center: Tuple[int, ...], diff --git a/qlbm/lattice/geometry/shapes/ymonomial.py b/qlbm/lattice/geometry/shapes/ymonomial.py index a2e70f5..8635ae7 100644 --- a/qlbm/lattice/geometry/shapes/ymonomial.py +++ b/qlbm/lattice/geometry/shapes/ymonomial.py @@ -12,7 +12,44 @@ class YMonomial(Shape): - """Base class for all geometrical shapes.""" + r""" + Shape representing boundaries shaped by comparisons to monomials. + + Represents a 2D discretized shape defined by a comparison between the :math:`y` + coordinate and the :math:`x` coordinate raised to a given exponent (i.e. the set of + points satisfying ``y [comparator] x**exp``). The comparison operator is + provided via a ComparatorMode instance and the spatial resolution is determined + by the number of grid qubits per dimension. + + .. important:: + + The ``YMonomial`` implementation is a work in progress. + At present, only the :math:`x^2` monomial case is supported, + and only when the monomial result register width matches the :math:`y` + grid register width. + + .. list-table:: Class attributes + :widths: 25 50 + :header-rows: 1 + + * - Attribute + - Description + * - :attr:`comparator_mode` + - The :class:`.ComparatorMode` used to compare :math:`y` and :math:`x^\mathrm{exp}`. + * - :attr:`exponent` + - The monomial exponent used to evaluate :math:`x^\mathrm{exp}`. + * - :attr:`boundary_points` + - A ``List[List[bool]]`` occupancy grid indicating whether each point satisfies the monomial comparator. + """ + + comparator_mode: ComparatorMode + """Comparator mode defining the boundary predicate ``y [op] x**exponent``.""" + + exponent: int + """Exponent of the monomial evaluated on the :math:`x` coordinate register.""" + + boundary_points: List[List[bool]] + """Boolean occupancy grid indicating which lattice points satisfy the monomial boundary.""" def __init__( self, diff --git a/test/unit/two_register_comparator_test.py b/test/unit/two_register_comparator_test.py index c41a30d..1adc605 100644 --- a/test/unit/two_register_comparator_test.py +++ b/test/unit/two_register_comparator_test.py @@ -2,7 +2,7 @@ from qiskit import QuantumCircuit from qiskit.quantum_info import Statevector -from qlbm.components.ms.primitives import TwoRegisterComparator +from qlbm.components.common.comparators import TwoRegisterComparator from qlbm.tools.utils import ComparatorMode From 0652c07a78f390e3f0e5c57bfed3a8b822d4318b Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Fri, 20 Feb 2026 16:02:29 +0100 Subject: [PATCH 62/78] Update website docs --- docs/source/code/comps_cqlbm.rst | 8 -------- docs/source/code/comps_other.rst | 25 ++++++++++++++++++++++++- docs/source/code/lattice.rst | 3 +++ 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/docs/source/code/comps_cqlbm.rst b/docs/source/code/comps_cqlbm.rst index a6bc4b7..e88d6fe 100644 --- a/docs/source/code/comps_cqlbm.rst +++ b/docs/source/code/comps_cqlbm.rst @@ -92,10 +92,6 @@ Streaming .. autoclass:: qlbm.components.ms.streaming.ControlledIncrementer -.. autoclass:: qlbm.components.common.ParameterizedDraperAdder - -.. autoclass:: qlbm.components.common.ParameterizedPhaseShift - .. autoclass:: qlbm.components.ms.streaming.PhaseShift .. autoclass:: qlbm.components.ab.streaming.ABStreamingOperator @@ -117,10 +113,6 @@ Reflection .. autoclass:: qlbm.components.ms.primitives.EdgeComparator -.. autoclass:: qlbm.components.ms.primitives.Comparator - -.. autoclass:: qlbm.components.ms.primitives.ComparatorMode - .. autoclass:: qlbm.components.ab.reflection.ABReflectionOperator .. autoclass:: qlbm.components.ab.reflection.ABZoneAgnosticReflectionOperator diff --git a/docs/source/code/comps_other.rst b/docs/source/code/comps_other.rst index c4634d4..a1243e8 100644 --- a/docs/source/code/comps_other.rst +++ b/docs/source/code/comps_other.rst @@ -1,12 +1,35 @@ .. _misc_components: ==================================== -Miscellaneous Circuits +Common and Misc Circuits ==================================== Circuits that are used throughout different algorithms, have niche use cases, or are not encoding-specific. +This page documents components that are shared throughout different +encodings and different stages of algorithms. + +Comparators +---------------------------------- + +.. autoclass:: qlbm.components.common.SingleRegisterComparator + +.. autoclass:: qlbm.components.common.TwoRegisterComparator + +.. autoclass:: qlbm.tools.ComparatorMode + +Arithmetic +---------------------------------- + +.. autoclass:: qlbm.components.common.ParameterizedDraperAdder + +.. autoclass:: qlbm.components.common.ParameterizedPhaseShift + + +Miscellaneous +---------------------------------- + .. autoclass:: qlbm.components.common.EmptyPrimitive .. autoclass:: qlbm.components.common.MCSwap diff --git a/docs/source/code/lattice.rst b/docs/source/code/lattice.rst index b9bb399..ce8c0cd 100644 --- a/docs/source/code/lattice.rst +++ b/docs/source/code/lattice.rst @@ -93,6 +93,9 @@ The :class:`.SpaceTimeQLBM` algorithm on makes use of the following: .. autoclass:: qlbm.lattice.geometry.Circle :members: +.. autoclass:: qlbm.lattice.geometry.YMonomial + :members: + .. autoclass:: qlbm.lattice.geometry.DimensionalReflectionData .. autoclass:: qlbm.lattice.geometry.ReflectionPoint From d69dbbe542c053ed0ee1ccc0986cc29e1e2338e5 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Thu, 5 Mar 2026 16:22:02 +0100 Subject: [PATCH 63/78] Add decomposition to APDIC class to avoid transpiler bug --- qlbm/components/ab/initial.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qlbm/components/ab/initial.py b/qlbm/components/ab/initial.py index 059dd68..6456201 100644 --- a/qlbm/components/ab/initial.py +++ b/qlbm/components/ab/initial.py @@ -467,7 +467,7 @@ def create_circuit(self) -> QuantumCircuit: state_setter_circ, qubits=self.lattice.marker_index(), inplace=True ) - return circuit + return circuit.decompose(reps=2) @override def __str__(self) -> str: From d0b9bda1dd827b25d710f5249d37094210c842b6 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Thu, 5 Mar 2026 16:26:30 +0100 Subject: [PATCH 64/78] Ignore type --- qlbm/components/ab/reflection/agnosotic_reflection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qlbm/components/ab/reflection/agnosotic_reflection.py b/qlbm/components/ab/reflection/agnosotic_reflection.py index 48e557e..893d644 100644 --- a/qlbm/components/ab/reflection/agnosotic_reflection.py +++ b/qlbm/components/ab/reflection/agnosotic_reflection.py @@ -84,7 +84,7 @@ def __init__( supported_shapes = ["cuboid", "ymonomial"] - if any([x.name() not in supported_shapes for x in self.shapes]): + if any([x.name() not in supported_shapes for x in self.shapes]): # type: ignore raise CircuitException( f"Agnostic reflection operator only supports the following shapes: {supported_shapes}." ) From 63f62bcde822a67850e28e62aec99882407376e7 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Thu, 5 Mar 2026 16:27:01 +0100 Subject: [PATCH 65/78] Run apt-get update before installing dependencies in tests workflow --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index be1721f..3e70a76 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -23,7 +23,7 @@ jobs: cache: "pip" # caching pip dependencies - name: Install dependencies run: | - sudo apt-get install -y -qq libboost-all-dev cmake + sudo apt-get update && apt-get install -y -qq libboost-all-dev cmake - name: Install qlbm run: | pip install --upgrade pip From d8240abe993ee75358dada082e609d7dba874bd5 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Thu, 5 Mar 2026 16:29:01 +0100 Subject: [PATCH 66/78] Fix permissions in tests workflow --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3e70a76..0b2252f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -23,7 +23,7 @@ jobs: cache: "pip" # caching pip dependencies - name: Install dependencies run: | - sudo apt-get update && apt-get install -y -qq libboost-all-dev cmake + sudo apt-get update && sudo apt-get install -y -qq libboost-all-dev cmake - name: Install qlbm run: | pip install --upgrade pip From 517dcc781212de3723f57bd9463ec4c900eaa8b6 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Thu, 5 Mar 2026 16:35:08 +0100 Subject: [PATCH 67/78] Set qiskit to stable version in pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2ede310..376a330 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ dependencies = [ "pytket>=1.29.2", "pytket-qiskit", "pytket-qulacs>=0.33", - "qiskit>=2.0", + "qiskit==2.1.0", "qiskit_qasm3_import>=0.4.2", "qiskit-qulacs>=0.1.0", "tqdm>=4.66", From c1a767a8a17b7478e95d97330909107740cda4a9 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 9 Mar 2026 13:28:57 +0100 Subject: [PATCH 68/78] Update exception inheritance and remove unused runner export --- qlbm/__init__.py | 1 - qlbm/tools/exceptions.py | 10 +++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/qlbm/__init__.py b/qlbm/__init__.py index 7e98639..3ffff97 100644 --- a/qlbm/__init__.py +++ b/qlbm/__init__.py @@ -24,6 +24,5 @@ "CQLBM", "CircuitCompiler", "QiskitRunner", - "QulacsRunner", "AmplitudeResult", ] diff --git a/qlbm/tools/exceptions.py b/qlbm/tools/exceptions.py index 41cdb96..c79aabb 100644 --- a/qlbm/tools/exceptions.py +++ b/qlbm/tools/exceptions.py @@ -1,7 +1,7 @@ """Contains custom exceptions for the QLBM package.""" -class LatticeException(BaseException): +class LatticeException(Exception): """Exception raised when encountering invalid or misaligned lattice properties.""" def __init__(self, message: str) -> None: @@ -9,7 +9,7 @@ def __init__(self, message: str) -> None: super().__init__(self.message) -class ResultsException(BaseException): +class ResultsException(Exception): """Exception raised during the processing of :class:`QBMResults` objects.""" def __init__(self, message: str) -> None: @@ -17,7 +17,7 @@ def __init__(self, message: str) -> None: super().__init__(self.message) -class CompilerException(BaseException): +class CompilerException(Exception): """Exception raised when encountering a circuit compilation exception.""" def __init__(self, message: str) -> None: @@ -25,7 +25,7 @@ def __init__(self, message: str) -> None: super().__init__(self.message) -class CircuitException(BaseException): +class CircuitException(Exception): """Exception raised when attempting to compile to an unsupported target.""" def __init__(self, message: str) -> None: @@ -33,7 +33,7 @@ def __init__(self, message: str) -> None: super().__init__(self.message) -class ExecutionException(BaseException): +class ExecutionException(Exception): """Exception raised when attempting to execute circuits with mismatched properties.""" def __init__(self, message: str) -> None: From 753bc6c401714cc9db4477b0419570b8ccfd080c Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 9 Mar 2026 14:20:15 +0100 Subject: [PATCH 69/78] Update demo simulation notebooks --- demos/simulation/ab_simulation.ipynb | 12 ++++++++---- demos/simulation/ms_simulation.ipynb | 4 ++-- demos/simulation/spacetime_simulation.ipynb | 14 +++----------- 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/demos/simulation/ab_simulation.ipynb b/demos/simulation/ab_simulation.ipynb index e8f839b..5d7ef3f 100644 --- a/demos/simulation/ab_simulation.ipynb +++ b/demos/simulation/ab_simulation.ipynb @@ -19,13 +19,13 @@ "\n", "from qlbm.components import (\n", " CQLBM,\n", + " ABDiscreteUniformInitialConditions,\n", " ABGridMeasurement,\n", - " ABInitialConditions,\n", " EmptyPrimitive,\n", ")\n", "from qlbm.infra import QiskitRunner, SimulationConfig\n", "from qlbm.lattice import ABLattice\n", - "from qlbm.tools.utils import create_directory_and_parents\n" + "from qlbm.tools.utils import create_directory_and_parents" ] }, { @@ -57,7 +57,11 @@ "outputs": [], "source": [ "cfg = SimulationConfig(\n", - " initial_conditions=ABInitialConditions(lattice),\n", + " initial_conditions=ABDiscreteUniformInitialConditions(\n", + " lattice,\n", + " [1, 2, 5], # E, N, and NE\n", + " ([], list(range(5))), # Span the entire y axis, but nothing on the x axis\n", + " ),\n", " algorithm=CQLBM(lattice),\n", " postprocessing=EmptyPrimitive(lattice),\n", " measurement=ABGridMeasurement(lattice),\n", @@ -67,7 +71,7 @@ " statevector_sampling=True,\n", " execution_backend=AerSimulator(method=\"statevector\"),\n", " sampling_backend=AerSimulator(method=\"statevector\"),\n", - ")\n" + ")" ] }, { diff --git a/demos/simulation/ms_simulation.ipynb b/demos/simulation/ms_simulation.ipynb index 239aaa6..692e13a 100644 --- a/demos/simulation/ms_simulation.ipynb +++ b/demos/simulation/ms_simulation.ipynb @@ -45,13 +45,13 @@ " },\n", " },\n", " \"geometry\": [\n", - " {\"shape\": \"cuboid\", \"x\": [6, 12], \"y\": [6, 12], \"boundary\": \"bounceback\"},\n", + " {\"shape\": \"cuboid\", \"x\": [10, 12], \"y\": [6, 12], \"boundary\": \"bounceback\"},\n", " ],\n", " }\n", ")\n", "\n", "\n", - "output_dir = \"qlbm-output/ms-d2q9-64x32-1-obstacle-qiskit\"\n", + "output_dir = \"qlbm-output/ms-4x4-16x16-1-obstacle-qiskit\"\n", "create_directory_and_parents(output_dir)" ] }, diff --git a/demos/simulation/spacetime_simulation.ipynb b/demos/simulation/spacetime_simulation.ipynb index e135c7c..559557b 100644 --- a/demos/simulation/spacetime_simulation.ipynb +++ b/demos/simulation/spacetime_simulation.ipynb @@ -32,24 +32,16 @@ "metadata": {}, "outputs": [], "source": [ - "# Load example with mixed boundary conditions and create output directory\n", "lattice = SpaceTimeLattice(\n", " num_timesteps=1,\n", " lattice_data={\n", - " \"lattice\": {\"dim\": {\"x\": 64, \"y\": 64}, \"velocities\": \"D2Q4\"},\n", - " \"geometry\": [\n", - " {\n", - " \"shape\": \"sphere\",\n", - " \"center\": [30, 30],\n", - " \"radius\": 15,\n", - " \"boundary\": \"bounceback\",\n", - " }\n", - " ],\n", + " \"lattice\": {\"dim\": {\"x\": 8, \"y\": 8}, \"velocities\": \"D2Q4\"},\n", + " \"geometry\": [],\n", " },\n", " use_volumetric_ops=False,\n", ")\n", "\n", - "output_dir = \"qlbm-output/spacetime-d2q4-64x64-1-sphere-qiskit\"\n", + "output_dir = \"qlbm-output/spacetime-d2q4-8x8-qiskit\"\n", "create_directory_and_parents(output_dir)" ] }, From 36e3fe38dd5f282572806fa6c76d6d1cea9f7f56 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 9 Mar 2026 15:04:50 +0100 Subject: [PATCH 70/78] Delegate result instantiation to lattice class --- qlbm/infra/runner/base.py | 75 ++++------------------ qlbm/lattice/lattices/base.py | 70 +++++++++++++++++++- qlbm/lattice/lattices/lqlga_lattice.py | 20 +++++- qlbm/lattice/lattices/spacetime_lattice.py | 12 ++++ 4 files changed, 113 insertions(+), 64 deletions(-) diff --git a/qlbm/infra/runner/base.py b/qlbm/infra/runner/base.py index 1263312..10ae78b 100644 --- a/qlbm/infra/runner/base.py +++ b/qlbm/infra/runner/base.py @@ -2,29 +2,17 @@ from abc import ABC, abstractmethod from logging import Logger, getLogger -from typing import List, cast +from typing import List from qiskit import QuantumCircuit as QiskitQC from qiskit.circuit.library import Initialize from qiskit.quantum_info import Statevector from qiskit_aer import AerSimulator -from qlbm.infra.reinitialize import ( - Reinitializer, - SpaceTimeReinitializer, -) -from qlbm.infra.reinitialize.identity_reinitializer import IdentityReinitializer -from qlbm.infra.result import ( - AmplitudeResult, - LQLGAResult, - QBMResult, - SpaceTimeResult, -) -from qlbm.lattice import Lattice, MSLattice -from qlbm.lattice.lattices.ab_lattice import ABLattice -from qlbm.lattice.lattices.lqlga_lattice import LQLGALattice -from qlbm.lattice.lattices.spacetime_lattice import SpaceTimeLattice -from qlbm.tools.exceptions import CircuitException, ResultsException +from qlbm.infra.reinitialize.base import Reinitializer +from qlbm.infra.result.base import QBMResult +from qlbm.lattice import Lattice +from qlbm.tools.exceptions import CircuitException from .simulation_config import SimulationConfig @@ -107,6 +95,8 @@ def new_result(self, output_directory: str, output_file_name: str) -> QBMResult: """ Get a new result object for the current runner. + Delegates to the lattice's :meth:`.Lattice.create_result` factory method. + Parameters ---------- output_directory : str @@ -118,62 +108,23 @@ def new_result(self, output_directory: str, output_file_name: str) -> QBMResult: ------- QBMResult An empty result object. - - Raises - ------ - ResultsException - If there is no matching result object for the runner's lattice. """ - if isinstance(self.lattice, MSLattice) or isinstance( - self.lattice, ABLattice - ): - return AmplitudeResult( - self.lattice, # type: ignore - output_directory, - output_file_name, - ) - elif isinstance(self.lattice, SpaceTimeLattice): - return SpaceTimeResult( - cast(SpaceTimeLattice, self.lattice), output_directory, output_file_name - ) - elif isinstance(self.lattice, LQLGALattice): - return LQLGAResult( - cast(LQLGALattice, self.lattice), output_directory, output_file_name - ) - else: - raise ResultsException(f"Unsupported lattice: {self.lattice}.") + return self.lattice.create_result(output_directory, output_file_name) def new_reinitializer(self) -> Reinitializer: """ Creates a new reinitializer for a simulated algorithm. + Delegates to the lattice's :meth:`.Lattice.create_reinitializer` factory method. + Returns ------- Reinitializer A suitable reinitializer. - - Raises - ------ - ResultsException - If the underlying algorithm does not support reinitialization. """ - if ( - isinstance(self.lattice, MSLattice) - or isinstance(self.lattice, LQLGALattice) - or isinstance(self.lattice, ABLattice) - ): - return IdentityReinitializer( - self.lattice, - self.config.get_execution_compiler(), - self.logger, - ) - elif isinstance(self.lattice, SpaceTimeLattice): - return SpaceTimeReinitializer( - cast(SpaceTimeLattice, self.lattice), - self.config.get_execution_compiler(), - ) - else: - raise ResultsException(f"Unsupported lattice: {self.lattice}.") + return self.lattice.create_reinitializer( + self.config.get_execution_compiler(), self.logger + ) def statevector_to_circuit(self, statevector: Statevector) -> QiskitQC: """ diff --git a/qlbm/lattice/lattices/base.py b/qlbm/lattice/lattices/base.py index 09abe2c..e056c33 100644 --- a/qlbm/lattice/lattices/base.py +++ b/qlbm/lattice/lattices/base.py @@ -1,11 +1,19 @@ """Base class for all algorithm-specific Lattices.""" +from __future__ import annotations + import json from abc import ABC, abstractmethod from logging import Logger, getLogger -from typing import Dict, List, Tuple +from typing import TYPE_CHECKING, Dict, List, Tuple from qiskit import QuantumCircuit, QuantumRegister +from typing_extensions import override + +if TYPE_CHECKING: + from qlbm.infra.compiler import CircuitCompiler + from qlbm.infra.reinitialize.base import Reinitializer + from qlbm.infra.result.base import QBMResult from qlbm.components.ab.encodings import ABEncodingType from qlbm.lattice.geometry.shapes.base import Shape @@ -553,6 +561,48 @@ def has_multiple_geometries(self) -> bool: """ pass + @abstractmethod + def create_result(self, output_directory: str, output_file_name: str) -> QBMResult: + """ + Create the appropriate result object for this lattice type. + + Parameters + ---------- + output_directory : str + The directory where the result data will be stored. + output_file_name : str + The file name of the result data within the directory. + + Returns + ------- + QBMResult + A result object specific to this lattice type. + """ + pass + + @abstractmethod + def create_reinitializer( + self, + compiler: CircuitCompiler, + logger: Logger = getLogger("qlbm"), + ) -> Reinitializer: + """ + Create the appropriate reinitializer for this lattice type. + + Parameters + ---------- + compiler : CircuitCompiler + The compiler that converts the novel initial conditions circuits. + logger : Logger, optional + The performance logger, by default ``getLogger("qlbm")``. + + Returns + ------- + Reinitializer + A reinitializer specific to this lattice type. + """ + pass + class AmplitudeLattice(Lattice, ABC): r""" @@ -585,6 +635,24 @@ def __init__( ): super(AmplitudeLattice, self).__init__(lattice_data, logger) + @override + def create_result(self, output_directory: str, output_file_name: str) -> QBMResult: + from qlbm.infra.result import AmplitudeResult + + return AmplitudeResult(self, output_directory, output_file_name) + + @override + def create_reinitializer( + self, + compiler: CircuitCompiler, + logger: Logger = getLogger("qlbm"), + ) -> Reinitializer: + from qlbm.infra.reinitialize.identity_reinitializer import ( + IdentityReinitializer, + ) + + return IdentityReinitializer(self, compiler, logger) + @abstractmethod def grid_index(self, dim: int | None = None) -> List[int]: """Get the indices of the qubits used that encode the grid values for the specified dimension. diff --git a/qlbm/lattice/lattices/lqlga_lattice.py b/qlbm/lattice/lattices/lqlga_lattice.py index a8ba755..1690298 100644 --- a/qlbm/lattice/lattices/lqlga_lattice.py +++ b/qlbm/lattice/lattices/lqlga_lattice.py @@ -1,7 +1,7 @@ """Implementation of the :class:`.Lattice` base specific to the 2D and 3D :class:`.LQLGA` algorithm.""" from itertools import product -from logging import getLogger +from logging import Logger, getLogger from math import prod from typing import Dict, List, Tuple, cast, override @@ -331,6 +331,24 @@ def get_velocity_qubits_of_line(self, line_index: int) -> Tuple[int, int]: + self.num_velocities_per_point // 2, ) + @override + def create_result(self, output_directory, output_file_name): + from qlbm.infra.result import LQLGAResult + + return LQLGAResult(self, output_directory, output_file_name) + + @override + def create_reinitializer( + self, + compiler, + logger: Logger = getLogger("qlbm"), + ): + from qlbm.infra.reinitialize.identity_reinitializer import ( + IdentityReinitializer, + ) + + return IdentityReinitializer(self, compiler, logger) + @override def logger_name(self) -> str: gp_string = "" diff --git a/qlbm/lattice/lattices/spacetime_lattice.py b/qlbm/lattice/lattices/spacetime_lattice.py index ef993b8..a95887c 100644 --- a/qlbm/lattice/lattices/spacetime_lattice.py +++ b/qlbm/lattice/lattices/spacetime_lattice.py @@ -470,6 +470,18 @@ def comparator_periodic_volume_bounds( return adjusted_bounds + @override + def create_result(self, output_directory, output_file_name): + from qlbm.infra.result import SpaceTimeResult + + return SpaceTimeResult(self, output_directory, output_file_name) + + @override + def create_reinitializer(self, compiler, logger=getLogger("qlbm")): + from qlbm.infra.reinitialize import SpaceTimeReinitializer + + return SpaceTimeReinitializer(self, compiler, logger) + @override def has_multiple_geometries(self): return False # multiple geometries unsupported for STQBM From 4713ef443e88a5f55ac7c561bf07e9dfa24901f7 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 9 Mar 2026 15:05:09 +0100 Subject: [PATCH 71/78] Add result delegation tests --- test/unit/lattice/lattice_factory_test.py | 248 ++++++++++++++++++++++ 1 file changed, 248 insertions(+) create mode 100644 test/unit/lattice/lattice_factory_test.py diff --git a/test/unit/lattice/lattice_factory_test.py b/test/unit/lattice/lattice_factory_test.py new file mode 100644 index 0000000..5c6ca82 --- /dev/null +++ b/test/unit/lattice/lattice_factory_test.py @@ -0,0 +1,248 @@ +"""Tests for the Lattice factory methods create_result and create_reinitializer.""" + +import os +import shutil +import tempfile + +import pytest + +from qlbm.infra.compiler import CircuitCompiler +from qlbm.infra.reinitialize.base import Reinitializer +from qlbm.infra.reinitialize.identity_reinitializer import IdentityReinitializer +from qlbm.infra.reinitialize.spacetime_reinitializer import SpaceTimeReinitializer +from qlbm.infra.result.amplitude_result import AmplitudeResult +from qlbm.infra.result.base import QBMResult +from qlbm.infra.result.lqlga_result import LQLGAResult +from qlbm.infra.result.spacetime_result import SpaceTimeResult +from qlbm.lattice.lattices.ab_lattice import ABLattice +from qlbm.lattice.lattices.lqlga_lattice import LQLGALattice +from qlbm.lattice.lattices.ms_lattice import MSLattice +from qlbm.lattice.lattices.oh_lattice import OHLattice +from qlbm.lattice.lattices.spacetime_lattice import SpaceTimeLattice + + +@pytest.fixture +def ms_lattice() -> MSLattice: + return MSLattice( + { + "lattice": {"dim": {"x": 16, "y": 16}, "velocities": {"x": 4, "y": 4}}, + "geometry": [ + { + "shape": "cuboid", + "x": [2, 6], + "y": [5, 10], + "boundary": "bounceback", + }, + ], + }, + ) + + +@pytest.fixture +def ab_lattice() -> ABLattice: + return ABLattice( + { + "lattice": {"dim": {"x": 16, "y": 16}, "velocities": "D2Q4"}, + "geometry": [], + }, + ) + + +@pytest.fixture +def oh_lattice() -> OHLattice: + return OHLattice( + { + "lattice": {"dim": {"x": 8, "y": 8}, "velocities": "D2Q9"}, + "geometry": [], + }, + ) + + +@pytest.fixture +def spacetime_lattice() -> SpaceTimeLattice: + return SpaceTimeLattice( + 1, + { + "lattice": {"dim": {"x": 16, "y": 16}, "velocities": "D2Q4"}, + "geometry": [ + { + "shape": "cuboid", + "x": [2, 6], + "y": [5, 10], + "boundary": "bounceback", + }, + ], + }, + ) + + +@pytest.fixture +def lqlga_lattice() -> LQLGALattice: + return LQLGALattice( + { + "lattice": {"dim": {"x": 4, "y": 4}, "velocities": "D2Q4"}, + }, + ) + + +@pytest.fixture +def compiler() -> CircuitCompiler: + return CircuitCompiler("QISKIT", "QISKIT") + + +@pytest.fixture +def temp_dir(): + d = tempfile.mkdtemp() + yield d + shutil.rmtree(d) + + +# ======================== +# create_result tests +# ======================== + + +class TestCreateResult: + """Tests for the create_result factory method across all lattice types.""" + + def test_ms_lattice_creates_amplitude_result(self, ms_lattice, temp_dir): + result = ms_lattice.create_result(temp_dir, "step") + assert isinstance(result, AmplitudeResult) + assert isinstance(result, QBMResult) + + def test_ab_lattice_creates_amplitude_result(self, ab_lattice, temp_dir): + result = ab_lattice.create_result(temp_dir, "step") + assert isinstance(result, AmplitudeResult) + assert isinstance(result, QBMResult) + + def test_oh_lattice_creates_amplitude_result(self, oh_lattice, temp_dir): + result = oh_lattice.create_result(temp_dir, "step") + assert isinstance(result, AmplitudeResult) + assert isinstance(result, QBMResult) + + def test_spacetime_lattice_creates_spacetime_result( + self, spacetime_lattice, temp_dir + ): + result = spacetime_lattice.create_result(temp_dir, "step") + assert isinstance(result, SpaceTimeResult) + assert isinstance(result, QBMResult) + + def test_lqlga_lattice_creates_lqlga_result(self, lqlga_lattice, temp_dir): + result = lqlga_lattice.create_result(temp_dir, "step") + assert isinstance(result, LQLGAResult) + assert isinstance(result, QBMResult) + + def test_result_has_correct_directory(self, ms_lattice, temp_dir): + result = ms_lattice.create_result(temp_dir, "output") + assert result.directory == temp_dir + + def test_result_has_correct_file_name(self, ms_lattice, temp_dir): + result = ms_lattice.create_result(temp_dir, "my_output") + assert result.output_file_name == "my_output" + + def test_result_stores_lattice(self, ab_lattice, temp_dir): + result = ab_lattice.create_result(temp_dir, "step") + assert result.lattice is ab_lattice + + def test_result_creates_output_directory(self, ms_lattice, temp_dir): + output_dir = os.path.join(temp_dir, "nested", "output") + result = ms_lattice.create_result(output_dir, "step") + assert os.path.isdir(output_dir) + + def test_result_writes_lattice_json(self, spacetime_lattice, temp_dir): + spacetime_lattice.create_result(temp_dir, "step") + lattice_json_path = os.path.join(temp_dir, "lattice.json") + assert os.path.isfile(lattice_json_path) + + +class TestCreateReinitializer: + """Tests for the create_reinitializer factory method across all lattice types.""" + + def test_ms_lattice_creates_identity_reinitializer(self, ms_lattice, compiler): + reinit = ms_lattice.create_reinitializer(compiler) + assert isinstance(reinit, IdentityReinitializer) + assert isinstance(reinit, Reinitializer) + + def test_ab_lattice_creates_identity_reinitializer(self, ab_lattice, compiler): + reinit = ab_lattice.create_reinitializer(compiler) + assert isinstance(reinit, IdentityReinitializer) + assert isinstance(reinit, Reinitializer) + + def test_oh_lattice_creates_identity_reinitializer(self, oh_lattice, compiler): + reinit = oh_lattice.create_reinitializer(compiler) + assert isinstance(reinit, IdentityReinitializer) + assert isinstance(reinit, Reinitializer) + + def test_spacetime_lattice_creates_spacetime_reinitializer( + self, spacetime_lattice, compiler + ): + reinit = spacetime_lattice.create_reinitializer(compiler) + assert isinstance(reinit, SpaceTimeReinitializer) + assert isinstance(reinit, Reinitializer) + + def test_lqlga_lattice_creates_identity_reinitializer( + self, lqlga_lattice, compiler + ): + reinit = lqlga_lattice.create_reinitializer(compiler) + assert isinstance(reinit, IdentityReinitializer) + assert isinstance(reinit, Reinitializer) + + def test_reinitializer_stores_lattice(self, ms_lattice, compiler): + reinit = ms_lattice.create_reinitializer(compiler) + assert reinit.lattice is ms_lattice + + def test_reinitializer_stores_compiler(self, ab_lattice, compiler): + reinit = ab_lattice.create_reinitializer(compiler) + assert reinit.compiler is compiler + + def test_spacetime_reinitializer_stores_lattice(self, spacetime_lattice, compiler): + reinit = spacetime_lattice.create_reinitializer(compiler) + assert reinit.lattice is spacetime_lattice + + def test_identity_reinitializer_requires_statevector(self, ms_lattice, compiler): + reinit = ms_lattice.create_reinitializer(compiler) + assert reinit.requires_statevector() is True + + def test_spacetime_reinitializer_requires_statevector( + self, spacetime_lattice, compiler + ): + reinit = spacetime_lattice.create_reinitializer(compiler) + assert reinit.requires_statevector() is False + + +class TestFactoryConsistency: + """Tests ensuring the factory methods produce the same types as the old isinstance dispatch.""" + + @pytest.mark.parametrize( + "lattice_fixture,expected_result_type", + [ + ("ms_lattice", AmplitudeResult), + ("ab_lattice", AmplitudeResult), + ("oh_lattice", AmplitudeResult), + ("spacetime_lattice", SpaceTimeResult), + ("lqlga_lattice", LQLGAResult), + ], + ) + def test_result_type_matches_lattice( + self, lattice_fixture, expected_result_type, temp_dir, request + ): + lattice = request.getfixturevalue(lattice_fixture) + result = lattice.create_result(temp_dir, "step") + assert type(result) is expected_result_type + + @pytest.mark.parametrize( + "lattice_fixture,expected_reinit_type", + [ + ("ms_lattice", IdentityReinitializer), + ("ab_lattice", IdentityReinitializer), + ("oh_lattice", IdentityReinitializer), + ("spacetime_lattice", SpaceTimeReinitializer), + ("lqlga_lattice", IdentityReinitializer), + ], + ) + def test_reinitializer_type_matches_lattice( + self, lattice_fixture, expected_reinit_type, compiler, request + ): + lattice = request.getfixturevalue(lattice_fixture) + reinit = lattice.create_reinitializer(compiler) + assert type(reinit) is expected_reinit_type From ab6644ab4555316b07490151619fd784cce9dffa Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Tue, 10 Mar 2026 12:22:46 +0100 Subject: [PATCH 72/78] Add parallel BC support to zone agnostic BC implementation --- qlbm/components/ab/ab.py | 2 +- .../ab/reflection/agnosotic_reflection.py | 198 ++++++++++++++++-- 2 files changed, 183 insertions(+), 17 deletions(-) diff --git a/qlbm/components/ab/ab.py b/qlbm/components/ab/ab.py index a75b763..d4f8653 100644 --- a/qlbm/components/ab/ab.py +++ b/qlbm/components/ab/ab.py @@ -75,7 +75,7 @@ def create_circuit(self): circuit.compose( ABZoneAgnosticReflectionOperator( self.lattice, - self.lattice.shape_list, + None, logger=self.logger, ).circuit, inplace=True, diff --git a/qlbm/components/ab/reflection/agnosotic_reflection.py b/qlbm/components/ab/reflection/agnosotic_reflection.py index 893d644..3514955 100644 --- a/qlbm/components/ab/reflection/agnosotic_reflection.py +++ b/qlbm/components/ab/reflection/agnosotic_reflection.py @@ -8,9 +8,10 @@ from qiskit.circuit.library import RGQFTMultiplier from typing_extensions import override +from qlbm.components.ab.reflection.common import ABReflectionPermutation from qlbm.components.ab.reflection.standard_reflection import ABReflectionOperator from qlbm.components.ab.streaming import ABStreamingOperator -from qlbm.components.base import LBMPrimitive +from qlbm.components.base import LBMOperator, LBMPrimitive from qlbm.components.common.adders import ParameterizedDraperAdder from qlbm.components.common.comparators import ( SingleRegisterComparator, @@ -22,10 +23,10 @@ from qlbm.lattice.lattices.base import AmplitudeLattice from qlbm.lattice.spacetime.properties_base import LatticeDiscretization from qlbm.tools.exceptions import CircuitException, LatticeException -from qlbm.tools.utils import ComparatorMode, flatten +from qlbm.tools.utils import ComparatorMode, flatten, get_qubits_to_invert -class ABZoneAgnosticReflectionOperator(ABReflectionOperator): +class ABZoneAgnosticReflectionOperator(LBMOperator): """ Implements bounceback reflection in the amplitude-based encoding of :class:`.ABQLBM` for :math:`D_dQ_q` discretizations. @@ -67,11 +68,11 @@ def __init__( shapes: List[Shape] | None = None, logger: Logger = getLogger("qlbm"), ) -> None: - super().__init__(lattice, [], logger) + super().__init__(lattice, logger) self.shapes = ( ( - cast(List[Block], flatten(list(self.lattice.geometries[0].values()))) + flatten(list(self.lattice.geometries[0].values())) if not self.lattice.has_multiple_geometries() else [ gdict["bounceback"] + gdict["specular"] # type: ignore @@ -84,7 +85,15 @@ def __init__( supported_shapes = ["cuboid", "ymonomial"] - if any([x.name() not in supported_shapes for x in self.shapes]): # type: ignore + # For multi-geometry, self.shapes is a list of lists; + # for single geometry, it is a flat list. + all_shapes = ( + flatten(self.shapes) + if self.lattice.has_multiple_geometries() and shapes is None + else self.shapes + ) + + if any([x.name() not in supported_shapes for x in all_shapes]): # type: ignore raise CircuitException( f"Agnostic reflection operator only supports the following shapes: {supported_shapes}." ) @@ -100,10 +109,27 @@ def __init__( def create_circuit(self) -> QuantumCircuit: if self.lattice.discretization not in [LatticeDiscretization.D2Q9]: raise LatticeException("AB reflection only currently supported in D2Q9") + + if not self.lattice.has_multiple_geometries(): + return self.__create_circuit_single_geometry() + else: + return self.__create_circuit_multi_geometry() + + def __create_circuit_single_geometry(self) -> QuantumCircuit: + r"""Create the zone-agnostic reflection circuit for a single geometry. + + The circuit structure is: + + .. math:: + + U = S \cdot O \cdot S^{-1} \cdot (Perm \cdot S)_{\text{ctrl}\ a_o} \cdot O + + where :math:`O` is the oracle, :math:`S` is the streaming operator, + and :math:`Perm` is the velocity permutation. + """ circuit = self.lattice.circuit.copy() oracle = self.lattice.circuit.copy() - # build the oracle once for shape in self.shapes: oracle.compose( ABZoneAgnosticReflectionOracle( @@ -112,22 +138,55 @@ def create_circuit(self) -> QuantumCircuit: inplace=True, ) - # 2-3. oracle circuit.compose(oracle, inplace=True) - - # 3-4. controlled permutation and stream circuit.compose(self.permute_and_stream(), inplace=True) - - # 4-5. uncontrolled inverse stream circuit.compose( ABStreamingOperator(self.lattice, logger=self.logger).circuit.inverse(), inplace=True, ) - - # 5-6. oracle circuit.compose(oracle, inplace=True) + circuit.compose( + ABStreamingOperator(self.lattice, logger=self.logger).circuit, + inplace=True, + ) + + return circuit + + def __create_circuit_multi_geometry(self) -> QuantumCircuit: + r"""Create the zone-agnostic reflection circuit for multiple geometries. + + For :math:`m` geometries, a combined oracle :math:`O_{\text{combined}}` + is built by applying each geometry's oracle :math:`O_c` controlled on + the marker register being in state :math:`\ket{c}`. + Since different marker states occupy orthogonal subspaces, the oracles + do not interfere and the obstacle ancilla is correctly set for each + geometry independently. + + The circuit structure is: + + .. math:: - # 6-7. uncontrolled regular stream + U = S \cdot O_\text{combined} \cdot S^{-1} + \cdot (Perm \cdot S)_{\text{ctrl}\ a_o} + \cdot O_\text{combined} + + Only the oracle is controlled on the marker state; the permutation, + streaming, and inverse streaming are shared across all geometries. + The permutation and streaming are implicitly geometry-specific because + they are controlled on the obstacle ancilla, which the marker-controlled + oracle has already set correctly. + """ + circuit = self.lattice.circuit.copy() + + oracle = self.__build_combined_oracle() + + circuit.compose(oracle, inplace=True) + circuit.compose(self.permute_and_stream(), inplace=True) + circuit.compose( + ABStreamingOperator(self.lattice, logger=self.logger).circuit.inverse(), + inplace=True, + ) + circuit.compose(oracle, inplace=True) circuit.compose( ABStreamingOperator(self.lattice, logger=self.logger).circuit, inplace=True, @@ -135,6 +194,82 @@ def create_circuit(self) -> QuantumCircuit: return circuit + def __build_combined_oracle(self) -> QuantumCircuit: + r"""Build the combined oracle for all geometries. + + For each geometry index :math:`c`, the marker register qubits are + flipped so that geometry :math:`c` maps to the all-ones state. + The oracle for that geometry is then applied with its central MCX gate + additionally controlled on the marker register. + Finally, the marker qubits are unflipped to restore the original state. + + Returns + ------- + QuantumCircuit + The combined oracle circuit. + """ + oracle = self.lattice.circuit.copy() + + for c, shapes_for_geometry in enumerate(self.shapes): + qubits_to_invert = [ + q + self.lattice.marker_index()[0] + for q in get_qubits_to_invert(c, self.lattice.num_marker_qubits) + ] + + if qubits_to_invert: + oracle.x(qubits_to_invert) + + for shape in shapes_for_geometry: + oracle.compose( + ABZoneAgnosticReflectionOracle( + self.lattice, # type: ignore[arg-type] + shape, + control_on_marker_state=True, + logger=self.logger, + ).circuit, + inplace=True, + ) + + if qubits_to_invert: + oracle.x(qubits_to_invert) + + return oracle + + def permute_and_stream(self) -> QuantumCircuit: + """ + Performs the permutation of basis states that implements bounceback reflection in the amplitude-based encoding. + + Returns + ------- + QuantumCircuit + The permutation acting on only the velocity register. + """ + circuit = self.lattice.circuit.copy() + + # Permute the velocities according to reflection rules + circuit.compose( + ABReflectionPermutation( + self.lattice.num_velocity_qubits, + self.lattice.discretization, + self.lattice.get_encoding(), + self.logger, + ) + .circuit.control(1) + .decompose(), + qubits=self.lattice.ancillae_obstacle_index() + + self.lattice.velocity_index(), + inplace=True, + ) + + circuit.compose( + ABStreamingOperator( + self.lattice, self.lattice.ancillae_obstacle_index(), self.logger + ).circuit, + inplace=True, + ) + + return circuit + @override def __str__(self) -> str: return f"[Operator ABZoneAgnosticReflection with lattice {self.lattice}]" @@ -165,6 +300,20 @@ class ABZoneAgnosticReflectionOracle(LBMPrimitive): This operation relies on basic arithmetic through the :class:`.ParameterizedDraperAdder` class and comparison operation through the :class:`Comparator` circuits. + When ``control_on_marker_state`` is ``True``, the oracle additionally conditions + the obstacle ancilla flip on the marker register being in the all-ones state. + This is used for parallel boundary conditions where multiple geometries + are simulated on the same lattice, each identified by a marker state. + Only the central MCX gate (for cuboids) is controlled on the marker, + since the surrounding adder and comparator operations are self-inverse + and their net effect on the grid register is zero. + + .. important:: + + Marker-controlled oracles for :class:`.YMonomial` shapes are not yet supported. + Passing ``control_on_marker_state=True`` with a ``YMonomial`` shape will raise + a :class:`.CircuitException`. + Example usage for a cuboid :class:`.Block`: .. plot:: @@ -218,16 +367,21 @@ class ABZoneAgnosticReflectionOracle(LBMPrimitive): lattice: ABLattice + control_on_marker_state: bool + """Whether the oracle is additionally controlled on the marker register.""" + def __init__( self, lattice: ABLattice, shape: Shape, + control_on_marker_state: bool = False, logger: Logger = getLogger("qlbm"), ) -> None: super().__init__(logger) self.lattice = lattice self.shape = shape + self.control_on_marker_state = control_on_marker_state self.logger.info(f"Creating circuit {str(self)}...") circuit_creation_start_time = perf_counter_ns() @@ -243,6 +397,11 @@ def create_circuit(self) -> QuantumCircuit: elif isinstance(self.shape, Circle): return self.__create_circuit_circle() elif isinstance(self.shape, YMonomial): + if self.control_on_marker_state: + raise CircuitException( + "Marker-controlled oracles for YMonomial shapes are not yet supported. " + "Parallel boundary conditions with YMonomial geometries require a future extension." + ) return self.__create_circuit_ymonomial() def __create_circuit_block(self) -> QuantumCircuit: @@ -272,8 +431,15 @@ def __create_circuit_block(self) -> QuantumCircuit: inplace=True, ) + control_qubits = self.lattice.ancillae_comparator_index(0)[ + : self.lattice.num_dims + ] + + if self.control_on_marker_state: + control_qubits = control_qubits + self.lattice.marker_index() + circuit.mcx( - self.lattice.ancillae_comparator_index(0)[: self.lattice.num_dims], + control_qubits, self.lattice.ancillae_obstacle_index()[0], ) From 76b35a7a62ba458963e74887542c6dec4b90c6f4 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Tue, 10 Mar 2026 12:23:19 +0100 Subject: [PATCH 73/78] Add zone agnostic BC tests --- test/unit/ab/ab_standard_reflection_test.py | 286 ++++++++++ .../ab/ab_zone_agnostic_reflection_test.py | 520 ++++++++++++++++++ 2 files changed, 806 insertions(+) create mode 100644 test/unit/ab/ab_standard_reflection_test.py create mode 100644 test/unit/ab/ab_zone_agnostic_reflection_test.py diff --git a/test/unit/ab/ab_standard_reflection_test.py b/test/unit/ab/ab_standard_reflection_test.py new file mode 100644 index 0000000..46ffeaf --- /dev/null +++ b/test/unit/ab/ab_standard_reflection_test.py @@ -0,0 +1,286 @@ +"""Statevector-level tests for the standard ABReflectionOperator.""" + +import pytest +from qiskit import QuantumCircuit, transpile +from qiskit.quantum_info import Statevector +from qiskit_aer import AerSimulator + +from qlbm.components.ab.reflection.standard_reflection import ABReflectionOperator +from qlbm.lattice import ABLattice +from qlbm.lattice.geometry.shapes.block import Block + +_SIMULATOR = AerSimulator(method="statevector") + + +def _simulate_statevector(circuit: QuantumCircuit) -> Statevector: + """Run a circuit on AerSimulator and return the final statevector.""" + qc = circuit.copy() + qc.save_statevector() + tqc = transpile(qc, _SIMULATOR, optimization_level=0) + result = _SIMULATOR.run(tqc).result() + return result.data(0)["statevector"] + + +def _make_single_geometry_lattice( + dim_x=8, dim_y=8, x_bounds=(2, 5), y_bounds=(2, 5) +) -> ABLattice: + """Create a single-geometry ABLattice with a cuboid obstacle.""" + return ABLattice( + { + "lattice": {"dim": {"x": dim_x, "y": dim_y}, "velocities": "d2q9"}, + "geometry": [ + { + "shape": "cuboid", + "x": list(x_bounds), + "y": list(y_bounds), + "boundary": "bounceback", + } + ], + } + ) + + +def _make_multi_geometry_lattice() -> ABLattice: + """Create a multi-geometry ABLattice with two cuboid configurations.""" + lattice = ABLattice( + { + "lattice": {"dim": {"x": 8, "y": 8}, "velocities": "d2q9"}, + } + ) + + lattice.set_geometries( + [ + [ + { + "shape": "cuboid", + "x": [2, 5], + "y": [2, 5], + "boundary": "bounceback", + } + ], + [ + { + "shape": "cuboid", + "x": [1, 3], + "y": [1, 3], + "boundary": "bounceback", + } + ], + ] + ) + + return lattice + + +def _encode_basis_state(lattice: ABLattice, x: int, y: int, v: int, marker: int = 0): + """Encode a computational basis state on the lattice circuit. + + Grid positions and velocity are encoded in binary representation. + Ancillae are initialized to 0 and the marker is set via X gates. + """ + circuit = lattice.circuit.copy() + + for i, q in enumerate(lattice.grid_index(0)): + if (x >> i) & 1: + circuit.x(q) + + for i, q in enumerate(lattice.grid_index(1)): + if (y >> i) & 1: + circuit.x(q) + + for i, q in enumerate(lattice.velocity_index()): + if (v >> i) & 1: + circuit.x(q) + + if lattice.num_marker_qubits > 0: + for i, q in enumerate(lattice.marker_index()): + if (marker >> i) & 1: + circuit.x(q) + + return circuit + + +def _get_obstacle_ancilla_value(lattice: ABLattice, sv: Statevector) -> dict: + """Extract obstacle ancilla probabilities from a statevector. + + Returns a dict mapping obstacle ancilla value (0 or 1) to probability. + """ + obstacle_idx = lattice.ancillae_obstacle_index() + probs = {} + for val in [0, 1]: + prob = 0.0 + for i, amp in enumerate(sv.data): + obstacle_val = (i >> obstacle_idx[0]) & 1 + if obstacle_val == val: + prob += abs(amp) ** 2 + probs[val] = prob + return probs + + +# ============================================================================= +# Statevector: single geometry full operator +# ============================================================================= + + +class TestStandardReflectionSingleGeometryStatevector: + """Statevector-level verification of the standard reflection with a single geometry.""" + + def test_obstacle_ancilla_clean_outside_obstacle(self): + """Obstacle ancilla should be 0 for positions well outside the obstacle. + + For the rest velocity (v=0, stationary particles), positions far from + the obstacle should be unaffected by the reflection operator. + """ + lattice = _make_single_geometry_lattice() + op = ABReflectionOperator(lattice) + + # Position (0, 0) is far from obstacle [2,5]x[2,5] + prep = _encode_basis_state(lattice, x=0, y=0, v=0) + prep.compose(op.circuit, inplace=True) + sv = _simulate_statevector(prep) + + probs = _get_obstacle_ancilla_value(lattice, sv) + assert probs[0] == pytest.approx(1.0, abs=1e-10) + + def test_obstacle_ancilla_clean_at_corner_outside(self): + """Obstacle ancilla should be 0 at outside corners of the obstacle. + + The gridpoints immediately adjacent to the obstacle corners + (in the fluid domain) should have their obstacle ancilla correctly + reset after the full reflection operator. + """ + lattice = _make_single_geometry_lattice() + op = ABReflectionOperator(lattice) + + # Position (1, 1) is outside the obstacle [2,5]x[2,5] + # and is an outside corner point + prep = _encode_basis_state(lattice, x=1, y=1, v=0) + prep.compose(op.circuit, inplace=True) + sv = _simulate_statevector(prep) + + probs = _get_obstacle_ancilla_value(lattice, sv) + assert probs[0] == pytest.approx(1.0, abs=1e-10) + + def test_operator_is_consistent_across_velocities_outside(self): + """Obstacle ancilla should remain 0 for various velocities at positions outside. + + For positions outside the obstacle, no velocity should trigger + the obstacle ancilla. + """ + lattice = _make_single_geometry_lattice() + op = ABReflectionOperator(lattice) + + for v in range(9): + prep = _encode_basis_state(lattice, x=0, y=0, v=v) + prep.compose(op.circuit, inplace=True) + sv = _simulate_statevector(prep) + + probs = _get_obstacle_ancilla_value(lattice, sv) + assert probs[0] == pytest.approx( + 1.0, abs=1e-10 + ), f"Obstacle ancilla not clean for v={v} at (0,0)" + + +# ============================================================================= +# Statevector: set_inside_wall_ancilla_state +# ============================================================================= + + +class TestSetInsideWallAncillaStatevector: + """Statevector-level tests for the set_inside_wall_ancilla_state primitive. + + This primitive sets the obstacle ancilla for positions lying along the + walls of the obstacle (excluding corners). It uses SpecularWallComparator + to identify wall positions. + """ + + def test_wall_ancilla_set_for_interior_wall_point(self): + """Interior wall points should have their obstacle ancilla set. + + For a [2,5]x[2,5] obstacle, position (3, 2) lies on the y=2 wall + and is inside the obstacle. The wall ancilla state primitive should + set the obstacle ancilla for this position. + """ + lattice = _make_single_geometry_lattice() + op = ABReflectionOperator(lattice) + block: Block = lattice.shapes["bounceback"][0] # type: ignore[assignment] + + wall_circuit = op.set_inside_wall_ancilla_state(block) + + prep = _encode_basis_state(lattice, x=3, y=2, v=0) + prep.compose(wall_circuit, inplace=True) + sv = _simulate_statevector(prep) + + probs = _get_obstacle_ancilla_value(lattice, sv) + assert probs[1] == pytest.approx(1.0, abs=1e-10) + + def test_wall_ancilla_not_set_for_point_outside_obstacle(self): + """Positions clearly outside the obstacle should not have ancilla set.""" + lattice = _make_single_geometry_lattice() + op = ABReflectionOperator(lattice) + block: Block = lattice.shapes["bounceback"][0] # type: ignore[assignment] + + wall_circuit = op.set_inside_wall_ancilla_state(block) + + prep = _encode_basis_state(lattice, x=0, y=0, v=0) + prep.compose(wall_circuit, inplace=True) + sv = _simulate_statevector(prep) + + probs = _get_obstacle_ancilla_value(lattice, sv) + assert probs[0] == pytest.approx(1.0, abs=1e-10) + + +# ============================================================================= +# Statevector: multi-geometry +# ============================================================================= + + +class TestStandardReflectionMultiGeometry: + """Statevector tests for the standard reflection operator with multiple geometries.""" + + def test_multi_geometry_obstacle_ancilla_clean_outside_all(self): + """Positions outside all obstacles should have clean ancilla for all markers.""" + lattice = _make_multi_geometry_lattice() + op = ABReflectionOperator(lattice) + + # (7, 7) is outside both obstacle [2,5]x[2,5] and [1,3]x[1,3] + for marker_val in [0, 1]: + prep = _encode_basis_state(lattice, x=7, y=7, v=0, marker=marker_val) + prep.compose(op.circuit, inplace=True) + sv = _simulate_statevector(prep) + + probs = _get_obstacle_ancilla_value(lattice, sv) + assert probs[0] == pytest.approx( + 1.0, abs=1e-10 + ), f"Failed for marker={marker_val}" + + def test_multi_geometry_operator_consistent_between_explicit_and_inferred(self): + """Operator with shapes=None should produce same statevector as explicit shapes. + + For multi-geometry, shapes are inferred from lattice.geometries. + Passing None should give the same result as passing the grouped shapes. + """ + lattice = _make_multi_geometry_lattice() + + op_inferred = ABReflectionOperator(lattice) + + grouped_shapes = [ + gdict["bounceback"] + gdict["specular"] for gdict in lattice.geometries + ] + op_explicit = ABReflectionOperator(lattice, shapes=grouped_shapes) # type: ignore[arg-type] + + # Verify statevector equivalence at representative points + for marker_val in [0, 1]: + prep_a = _encode_basis_state( + lattice, x=7, y=7, v=0, marker=marker_val + ) + prep_a.compose(op_inferred.circuit, inplace=True) + sv_a = _simulate_statevector(prep_a) + + prep_b = _encode_basis_state( + lattice, x=7, y=7, v=0, marker=marker_val + ) + prep_b.compose(op_explicit.circuit, inplace=True) + sv_b = _simulate_statevector(prep_b) + + assert sv_a.equiv(sv_b), f"Mismatch at (7,7), marker={marker_val}" diff --git a/test/unit/ab/ab_zone_agnostic_reflection_test.py b/test/unit/ab/ab_zone_agnostic_reflection_test.py new file mode 100644 index 0000000..f12f00e --- /dev/null +++ b/test/unit/ab/ab_zone_agnostic_reflection_test.py @@ -0,0 +1,520 @@ +"""Statevector-level tests for the zone-agnostic reflection oracle and operator.""" + +import numpy as np +import pytest +from qiskit import QuantumCircuit, transpile +from qiskit.quantum_info import Statevector +from qiskit_aer import AerSimulator + +from qlbm.components.ab.reflection.agnosotic_reflection import ( + ABZoneAgnosticReflectionOperator, + ABZoneAgnosticReflectionOracle, +) +from qlbm.lattice import ABLattice +from qlbm.tools.exceptions import CircuitException + +# ============================================================================= +# Helper utilities +# ============================================================================= + +_SIMULATOR = AerSimulator(method="statevector") + + +def _simulate_statevector(circuit: QuantumCircuit) -> Statevector: + """Run a circuit on AerSimulator and return the final statevector.""" + qc = circuit.copy() + qc.save_statevector() + tqc = transpile(qc, _SIMULATOR, optimization_level=0) + result = _SIMULATOR.run(tqc).result() + return result.data(0)["statevector"] + + +def _make_single_geometry_lattice(dim_x=4, dim_y=4) -> ABLattice: + """Create a single-geometry ABLattice with a cuboid obstacle.""" + return ABLattice( + { + "lattice": {"dim": {"x": dim_x, "y": dim_y}, "velocities": "d2q9"}, + "geometry": [ + { + "shape": "cuboid", + "x": [1, 2], + "y": [1, 2], + "boundary": "bounceback", + } + ], + } + ) + + +def _make_multi_geometry_lattice(dim_x=4, dim_y=4) -> ABLattice: + """Create a multi-geometry ABLattice with two cuboid configurations.""" + lattice = ABLattice( + { + "lattice": {"dim": {"x": dim_x, "y": dim_y}, "velocities": "d2q9"}, + } + ) + + lattice.set_geometries( + [ + [ + { + "shape": "cuboid", + "x": [1, 2], + "y": [1, 2], + "boundary": "bounceback", + } + ], + [ + { + "shape": "cuboid", + "x": [0, 1], + "y": [0, 1], + "boundary": "bounceback", + } + ], + ] + ) + + return lattice + + +def _encode_basis_state(lattice: ABLattice, x: int, y: int, v: int, marker: int = 0): + r"""Encode a computational basis state |x>|y>|v>|ancillae>|marker>. + + The grid and velocity are encoded in the standard binary representation. + Ancillae are initialized to 0. Marker is set via X gates. + """ + circuit = lattice.circuit.copy() + + for i, q in enumerate(lattice.grid_index(0)): + if (x >> i) & 1: + circuit.x(q) + + for i, q in enumerate(lattice.grid_index(1)): + if (y >> i) & 1: + circuit.x(q) + + for i, q in enumerate(lattice.velocity_index()): + if (v >> i) & 1: + circuit.x(q) + + if lattice.num_marker_qubits > 0: + for i, q in enumerate(lattice.marker_index()): + if (marker >> i) & 1: + circuit.x(q) + + return circuit + + +def _get_obstacle_ancilla_value(lattice: ABLattice, sv: Statevector) -> dict: + """Extract the obstacle ancilla probabilities from a statevector. + + Returns a dict mapping obstacle ancilla value (0 or 1) to probability. + """ + obstacle_idx = lattice.ancillae_obstacle_index() + probs = {} + for val in [0, 1]: + prob = 0.0 + for i, amp in enumerate(sv.data): + obstacle_val = (i >> obstacle_idx[0]) & 1 + if obstacle_val == val: + prob += abs(amp) ** 2 + probs[val] = prob + return probs + + +# ============================================================================= +# Oracle: single geometry statevector tests +# ============================================================================= + + +class TestOracleSingleGeometry: + """Statevector tests for the oracle without marker control (single geometry).""" + + def test_oracle_marks_position_inside_block(self): + """Oracle should set obstacle ancilla for positions inside the block.""" + lattice = _make_single_geometry_lattice() + shape = lattice.shapes["bounceback"][0] + oracle = ABZoneAgnosticReflectionOracle(lattice, shape) + + # Position (1, 1) is inside the block [1,2] x [1,2] + prep = _encode_basis_state(lattice, x=1, y=1, v=0) + prep.compose(oracle.circuit, inplace=True) + sv = _simulate_statevector(prep) + + probs = _get_obstacle_ancilla_value(lattice, sv) + assert probs[1] == pytest.approx(1.0, abs=1e-10) + + def test_oracle_does_not_mark_position_outside_block(self): + """Oracle should not set obstacle ancilla for positions outside the block.""" + lattice = _make_single_geometry_lattice() + shape = lattice.shapes["bounceback"][0] + oracle = ABZoneAgnosticReflectionOracle(lattice, shape) + + # Position (0, 0) is outside the block [1,2] x [1,2] + prep = _encode_basis_state(lattice, x=0, y=0, v=0) + prep.compose(oracle.circuit, inplace=True) + sv = _simulate_statevector(prep) + + probs = _get_obstacle_ancilla_value(lattice, sv) + assert probs[0] == pytest.approx(1.0, abs=1e-10) + + def test_oracle_marks_corner_of_block(self): + """Oracle should mark the upper corner of the block.""" + lattice = _make_single_geometry_lattice() + shape = lattice.shapes["bounceback"][0] + oracle = ABZoneAgnosticReflectionOracle(lattice, shape) + + # Position (2, 2) is the upper corner of the block [1,2] x [1,2] + prep = _encode_basis_state(lattice, x=2, y=2, v=0) + prep.compose(oracle.circuit, inplace=True) + sv = _simulate_statevector(prep) + + probs = _get_obstacle_ancilla_value(lattice, sv) + assert probs[1] == pytest.approx(1.0, abs=1e-10) + + def test_oracle_is_self_inverse(self): + """Applying the oracle twice should return to the original state.""" + lattice = _make_single_geometry_lattice() + shape = lattice.shapes["bounceback"][0] + oracle = ABZoneAgnosticReflectionOracle(lattice, shape) + + prep = _encode_basis_state(lattice, x=1, y=1, v=0) + prep.compose(oracle.circuit, inplace=True) + prep.compose(oracle.circuit, inplace=True) + sv = _simulate_statevector(prep) + + probs = _get_obstacle_ancilla_value(lattice, sv) + assert probs[0] == pytest.approx(1.0, abs=1e-10) + + +# ============================================================================= +# Oracle: marker-controlled statevector tests +# ============================================================================= + + +class TestOracleWithMarkerControl: + """Statevector tests for the oracle with marker control (parallel BCs).""" + + def test_oracle_marks_only_when_marker_matches(self): + """Oracle controlled on marker should only set obstacle ancilla when marker is all-ones.""" + lattice = _make_multi_geometry_lattice() + shape = lattice.geometries[0]["bounceback"][0] + oracle = ABZoneAgnosticReflectionOracle( + lattice, shape, control_on_marker_state=True + ) + + # Position (1, 1) is inside the block. + # Marker = 1 (all ones for 1-qubit marker) -> should mark + prep = _encode_basis_state(lattice, x=1, y=1, v=0, marker=1) + prep.compose(oracle.circuit, inplace=True) + sv = _simulate_statevector(prep) + + probs = _get_obstacle_ancilla_value(lattice, sv) + assert probs[1] == pytest.approx(1.0, abs=1e-10) + + def test_oracle_does_not_mark_when_marker_does_not_match(self): + """Oracle controlled on marker should NOT set obstacle ancilla when marker is not all-ones.""" + lattice = _make_multi_geometry_lattice() + shape = lattice.geometries[0]["bounceback"][0] + oracle = ABZoneAgnosticReflectionOracle( + lattice, shape, control_on_marker_state=True + ) + + # Position (1, 1) is inside the block. + # Marker = 0 (not all ones) -> should NOT mark + prep = _encode_basis_state(lattice, x=1, y=1, v=0, marker=0) + prep.compose(oracle.circuit, inplace=True) + sv = _simulate_statevector(prep) + + probs = _get_obstacle_ancilla_value(lattice, sv) + assert probs[0] == pytest.approx(1.0, abs=1e-10) + + def test_oracle_preserves_grid_state_regardless_of_marker(self): + """Oracle should restore grid qubits to original state for all marker values. + + Even though the oracle temporarily modifies grid qubits (via subtraction/addition), + the net effect on the grid should be zero, regardless of marker state. + """ + lattice = _make_multi_geometry_lattice() + shape = lattice.geometries[0]["bounceback"][0] + oracle = ABZoneAgnosticReflectionOracle( + lattice, shape, control_on_marker_state=True + ) + + for marker_val in [0, 1]: + prep = _encode_basis_state(lattice, x=1, y=1, v=3, marker=marker_val) + original_sv = _simulate_statevector(prep) + + prep.compose(oracle.circuit, inplace=True) + after_sv = _simulate_statevector(prep) + + # Check that grid qubits are preserved by comparing marginal probabilities + grid_qubits = lattice.grid_index() + original_grid_probs = original_sv.probabilities(grid_qubits) + after_grid_probs = after_sv.probabilities(grid_qubits) + np.testing.assert_allclose(original_grid_probs, after_grid_probs, atol=1e-10) + + def test_ymonomial_raises_with_marker_control(self): + """YMonomial oracle should raise when control_on_marker_state=True.""" + lattice = ABLattice( + { + "lattice": {"dim": {"x": 2, "y": 4}, "velocities": "D2Q4"}, + "geometry": [ + { + "shape": "ymonomial", + "exponent": 2, + "comparator": "<=", + "boundary": "bounceback", + } + ], + } + ) + lattice.set_num_marker_qubits(1) + + shape = lattice.shapes["bounceback"][0] + + with pytest.raises(CircuitException, match="Marker-controlled oracles"): + ABZoneAgnosticReflectionOracle( + lattice, shape, control_on_marker_state=True + ) + + +# ============================================================================= +# Combined oracle statevector tests +# ============================================================================= + + +class TestCombinedOracle: + """Statevector tests for the combined oracle used in multi-geometry parallel BCs. + + The combined oracle applies each geometry's oracle controlled on the + corresponding marker state. It verifies that for a superposition of + marker states, the obstacle ancilla is correctly set per geometry. + """ + + def test_combined_oracle_marks_geometry_0_only(self): + """For marker=0, only geometry 0's obstacle region should be marked.""" + lattice = _make_multi_geometry_lattice() + operator = ABZoneAgnosticReflectionOperator(lattice) + + # Access the combined oracle through the private method + oracle = operator._ABZoneAgnosticReflectionOperator__build_combined_oracle() + + # Position (1, 1) is inside geometry 0 ([1,2]x[1,2]) but also inside geometry 1 ([0,1]x[0,1]) + # With marker=0, only geometry 0's oracle fires + prep = _encode_basis_state(lattice, x=1, y=1, v=0, marker=0) + prep.compose(oracle, inplace=True) + sv = _simulate_statevector(prep) + + probs = _get_obstacle_ancilla_value(lattice, sv) + assert probs[1] == pytest.approx(1.0, abs=1e-10) + + def test_combined_oracle_marks_geometry_1_only(self): + """For marker=1, only geometry 1's obstacle region should be marked.""" + lattice = _make_multi_geometry_lattice() + operator = ABZoneAgnosticReflectionOperator(lattice) + + oracle = operator._ABZoneAgnosticReflectionOperator__build_combined_oracle() + + # Position (0, 0) is inside geometry 1 ([0,1]x[0,1]) but NOT inside geometry 0 + # With marker=1, geometry 1's oracle fires + prep = _encode_basis_state(lattice, x=0, y=0, v=0, marker=1) + prep.compose(oracle, inplace=True) + sv = _simulate_statevector(prep) + + probs = _get_obstacle_ancilla_value(lattice, sv) + assert probs[1] == pytest.approx(1.0, abs=1e-10) + + def test_combined_oracle_does_not_mark_wrong_geometry(self): + """Position inside geometry 1 should NOT be marked when marker=0.""" + lattice = _make_multi_geometry_lattice() + operator = ABZoneAgnosticReflectionOperator(lattice) + + oracle = operator._ABZoneAgnosticReflectionOperator__build_combined_oracle() + + # Position (0, 0) is inside geometry 1 but NOT geometry 0 + # With marker=0, geometry 0's oracle fires but position is outside geo 0 + prep = _encode_basis_state(lattice, x=0, y=0, v=0, marker=0) + prep.compose(oracle, inplace=True) + sv = _simulate_statevector(prep) + + probs = _get_obstacle_ancilla_value(lattice, sv) + assert probs[0] == pytest.approx(1.0, abs=1e-10) + + def test_combined_oracle_outside_all_geometries(self): + """Position outside all geometries should never be marked.""" + lattice = _make_multi_geometry_lattice() + operator = ABZoneAgnosticReflectionOperator(lattice) + + oracle = operator._ABZoneAgnosticReflectionOperator__build_combined_oracle() + + # Position (3, 3) is outside both geometry 0 ([1,2]x[1,2]) and geometry 1 ([0,1]x[0,1]) + for marker_val in [0, 1]: + prep = _encode_basis_state(lattice, x=3, y=3, v=0, marker=marker_val) + prep.compose(oracle, inplace=True) + sv = _simulate_statevector(prep) + + probs = _get_obstacle_ancilla_value(lattice, sv) + assert probs[0] == pytest.approx( + 1.0, abs=1e-10 + ), f"Failed for marker={marker_val}" + + def test_combined_oracle_is_self_inverse(self): + """Applying the combined oracle twice should return to the original state.""" + lattice = _make_multi_geometry_lattice() + operator = ABZoneAgnosticReflectionOperator(lattice) + + oracle = operator._ABZoneAgnosticReflectionOperator__build_combined_oracle() + + for marker_val in [0, 1]: + for x, y in [(1, 1), (0, 0), (3, 3)]: + prep = _encode_basis_state(lattice, x=x, y=y, v=0, marker=marker_val) + prep.compose(oracle, inplace=True) + prep.compose(oracle, inplace=True) + sv = _simulate_statevector(prep) + + probs = _get_obstacle_ancilla_value(lattice, sv) + assert probs[0] == pytest.approx( + 1.0, abs=1e-10 + ), f"Not self-inverse for marker={marker_val}, pos=({x},{y})" + + +# ============================================================================= +# Operator-level statevector tests +# ============================================================================= + + +class TestOperatorSingleGeometry: + """Statevector tests for the zone-agnostic reflection operator with single geometry.""" + + def test_operator_obstacle_ancilla_is_clean_after_full_circuit(self): + """After the full reflection operator, the obstacle ancilla should be |0>. + + The operator structure is O -> PermStream -> S^{-1} -> O -> S. + After the second oracle, the obstacle ancilla should be uncomputed, + assuming the particle's position is correctly restored. + """ + lattice = _make_single_geometry_lattice() + operator = ABZoneAgnosticReflectionOperator( + lattice, shapes=lattice.shapes["bounceback"] + ) + + # Test with a position outside the obstacle + prep = _encode_basis_state(lattice, x=0, y=0, v=0) + prep.compose(operator.circuit, inplace=True) + sv = _simulate_statevector(prep) + + probs = _get_obstacle_ancilla_value(lattice, sv) + assert probs[0] == pytest.approx(1.0, abs=1e-10) + + +class TestOperatorMultiGeometry: + """Statevector tests for the zone-agnostic reflection operator with multiple geometries.""" + + def test_operator_obstacle_ancilla_is_clean_outside_all_geometries(self): + """For positions outside all geometries, obstacle ancilla should remain 0.""" + lattice = _make_multi_geometry_lattice() + operator = ABZoneAgnosticReflectionOperator(lattice) + + for marker_val in [0, 1]: + prep = _encode_basis_state(lattice, x=3, y=3, v=0, marker=marker_val) + prep.compose(operator.circuit, inplace=True) + sv = _simulate_statevector(prep) + + probs = _get_obstacle_ancilla_value(lattice, sv) + assert probs[0] == pytest.approx( + 1.0, abs=1e-10 + ), f"Failed for marker={marker_val}" + + +# ============================================================================= +# Backward compatibility +# ============================================================================= + + +class TestBackwardCompatibility: + """Tests verifying that single-geometry behavior is unchanged.""" + + def test_single_geometry_operator_statevector_unchanged(self): + """The operator output for a single geometry should match regardless of code path. + + This test constructs the operator via the explicit shapes parameter + and via None. Their outputs must match for a sample of input basis states. + """ + lattice = _make_single_geometry_lattice() + + op_explicit = ABZoneAgnosticReflectionOperator( + lattice, shapes=lattice.shapes["bounceback"] + ) + op_inferred = ABZoneAgnosticReflectionOperator(lattice) + + # Sample representative positions and velocities instead of exhaustive + for x, y in [(0, 0), (1, 1), (2, 2), (3, 0)]: + for v in [0, 3, 5]: + prep_a = _encode_basis_state(lattice, x=x, y=y, v=v) + prep_a.compose(op_explicit.circuit, inplace=True) + sv_a = _simulate_statevector(prep_a) + + prep_b = _encode_basis_state(lattice, x=x, y=y, v=v) + prep_b.compose(op_inferred.circuit, inplace=True) + sv_b = _simulate_statevector(prep_b) + + assert sv_a.equiv(sv_b), f"Mismatch at x={x}, y={y}, v={v}" + + def test_single_geometry_in_multi_geometry_lattice_produces_same_oracle_effect( + self, + ): + """A multi-geometry lattice with one geometry should produce the same oracle marking. + + When there is only one geometry, the operator should behave identically + to the single-geometry case (modulo the extra marker qubit). + """ + # Single geometry lattice + single_lattice = _make_single_geometry_lattice() + single_oracle = ABZoneAgnosticReflectionOracle( + single_lattice, single_lattice.shapes["bounceback"][0] + ) + + # Multi-geometry lattice with only one geometry + multi_lattice = ABLattice( + { + "lattice": {"dim": {"x": 4, "y": 4}, "velocities": "d2q9"}, + } + ) + multi_lattice.set_geometries( + [ + [ + { + "shape": "cuboid", + "x": [1, 2], + "y": [1, 2], + "boundary": "bounceback", + } + ], + ] + ) + + # With one geometry, has_multiple_geometries() returns False + assert not multi_lattice.has_multiple_geometries() + + # Oracle should work the same way + multi_oracle = ABZoneAgnosticReflectionOracle( + multi_lattice, multi_lattice.geometries[0]["bounceback"][0] + ) + + # Check that both oracles mark the same positions + for x, y in [(1, 1), (0, 0), (2, 2), (3, 3)]: + prep_s = _encode_basis_state(single_lattice, x=x, y=y, v=0) + prep_s.compose(single_oracle.circuit, inplace=True) + sv_s = _simulate_statevector(prep_s) + probs_s = _get_obstacle_ancilla_value(single_lattice, sv_s) + + prep_m = _encode_basis_state(multi_lattice, x=x, y=y, v=0) + prep_m.compose(multi_oracle.circuit, inplace=True) + sv_m = _simulate_statevector(prep_m) + probs_m = _get_obstacle_ancilla_value(multi_lattice, sv_m) + + assert probs_s[1] == pytest.approx( + probs_m[1], abs=1e-10 + ), f"Oracle mismatch at ({x},{y})" From 9e2f02694613f99babdd71d7f989a592f12ee727 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Tue, 10 Mar 2026 12:24:03 +0100 Subject: [PATCH 74/78] Change test name --- .../{lattice_factory_test.py => lattice_result_test.py} | 4 ---- 1 file changed, 4 deletions(-) rename test/unit/lattice/{lattice_factory_test.py => lattice_result_test.py} (99%) diff --git a/test/unit/lattice/lattice_factory_test.py b/test/unit/lattice/lattice_result_test.py similarity index 99% rename from test/unit/lattice/lattice_factory_test.py rename to test/unit/lattice/lattice_result_test.py index 5c6ca82..6ecc322 100644 --- a/test/unit/lattice/lattice_factory_test.py +++ b/test/unit/lattice/lattice_result_test.py @@ -97,10 +97,6 @@ def temp_dir(): shutil.rmtree(d) -# ======================== -# create_result tests -# ======================== - class TestCreateResult: """Tests for the create_result factory method across all lattice types.""" From 255facc126fbdf6213cd6ae48d8aa9282d17cd19 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Tue, 10 Mar 2026 12:24:21 +0100 Subject: [PATCH 75/78] Add e2e tests for all quantum algorithms --- test/e2e/__init__.py | 1 + test/e2e/test_ab_e2e.py | 284 +++++++++++++++++++++++++++++++++ test/e2e/test_lqlga_e2e.py | 182 +++++++++++++++++++++ test/e2e/test_ms_e2e.py | 238 +++++++++++++++++++++++++++ test/e2e/test_spacetime_e2e.py | 196 +++++++++++++++++++++++ test/e2e/utils.py | 225 ++++++++++++++++++++++++++ 6 files changed, 1126 insertions(+) create mode 100644 test/e2e/__init__.py create mode 100644 test/e2e/test_ab_e2e.py create mode 100644 test/e2e/test_lqlga_e2e.py create mode 100644 test/e2e/test_ms_e2e.py create mode 100644 test/e2e/test_spacetime_e2e.py create mode 100644 test/e2e/utils.py diff --git a/test/e2e/__init__.py b/test/e2e/__init__.py new file mode 100644 index 0000000..99978b9 --- /dev/null +++ b/test/e2e/__init__.py @@ -0,0 +1 @@ +"""End-to-end tests for the qlbm package.""" \ No newline at end of file diff --git a/test/e2e/test_ab_e2e.py b/test/e2e/test_ab_e2e.py new file mode 100644 index 0000000..639830e --- /dev/null +++ b/test/e2e/test_ab_e2e.py @@ -0,0 +1,284 @@ +"""End-to-end tests for the ABQLBM algorithm. + +The ABQLBM algorithm performs streaming followed by zone-agnostic reflection. +The D2Q9 velocity channels are indexed as: + + 0: [ 0, 0] (rest) + 1: [+1, 0] (right) + 2: [ 0,+1] (up) + 3: [-1, 0] (left) + 4: [ 0,-1] (down) + 5: [+1,+1] (right-up) + 6: [-1,+1] (left-up) + 7: [-1,-1] (left-down) + 8: [+1,-1] (right-down) +""" + +import pytest + +from qlbm.components.ab import ABQLBM +from qlbm.lattice import ABLattice + +from .utils import ( + decode_state, + get_nonzero_amplitudes, + make_ab_qubit_layout, + prepare_single_particle, + run_statevector, +) + +# Bounceback reverses velocity direction. +D2Q9_BOUNCEBACK = { + 0: 0, + 1: 3, + 2: 4, + 3: 1, + 4: 2, + 5: 7, + 6: 8, + 7: 5, + 8: 6, +} + + +# --------------------------------------------------------------------------- +# Free streaming (no geometry) +# --------------------------------------------------------------------------- + + +class TestABFreeStreaming: + """ABQLBM on a 4x4 D2Q9 lattice without obstacles. + + With no geometry the reflection operator reduces to an identity, + so the algorithm is pure streaming: a particle at position ``(x, y)`` + with velocity channel ``c`` moves to ``(x + vx, y + vy) mod 4``. + """ + + @pytest.fixture + def lattice(self): + """4x4 D2Q9 lattice with no obstacles (9 qubits).""" + return ABLattice( + { + "lattice": {"dim": {"x": 4, "y": 4}, "velocities": "D2Q9"}, + "geometry": [], + } + ) + + def test_lattice_qubit_count(self, lattice): + """Verify expected register sizes.""" + assert lattice.num_dims == 2 + assert lattice.num_gridpoints == [3, 3] + assert lattice.num_grid_qubits == 4 + assert lattice.num_velocity_qubits == 4 + assert lattice.num_total_qubits == 9 + + def test_stream_right(self, lattice): + """Channel 1 (+x): (0,0) -> (1,0) after 1 step.""" + alg = ABQLBM(lattice) + circuit = prepare_single_particle(lattice, (0, 0), 1) + circuit.compose(alg.circuit, inplace=True) + + sv = run_statevector(circuit) + amps = get_nonzero_amplitudes(sv) + layout = make_ab_qubit_layout(lattice) + + assert len(amps) == 1 + decoded = decode_state(list(amps.keys())[0], layout) + assert decoded["g_x"] == 1 + assert decoded["g_y"] == 0 + assert decoded["v"] == 1 + assert decoded["a_o"] == 0 + + def test_stream_up(self, lattice): + """Channel 2 (+y): (0,0) -> (0,1) after 1 step.""" + alg = ABQLBM(lattice) + circuit = prepare_single_particle(lattice, (0, 0), 2) + circuit.compose(alg.circuit, inplace=True) + + sv = run_statevector(circuit) + amps = get_nonzero_amplitudes(sv) + layout = make_ab_qubit_layout(lattice) + + assert len(amps) == 1 + decoded = decode_state(list(amps.keys())[0], layout) + assert decoded["g_x"] == 0 + assert decoded["g_y"] == 1 + assert decoded["v"] == 2 + assert decoded["a_o"] == 0 + + def test_stream_diagonal(self, lattice): + """Channel 5 (+x,+y): (0,0) -> (1,1) after 1 step.""" + alg = ABQLBM(lattice) + circuit = prepare_single_particle(lattice, (0, 0), 5) + circuit.compose(alg.circuit, inplace=True) + + sv = run_statevector(circuit) + amps = get_nonzero_amplitudes(sv) + layout = make_ab_qubit_layout(lattice) + + assert len(amps) == 1 + decoded = decode_state(list(amps.keys())[0], layout) + assert decoded["g_x"] == 1 + assert decoded["g_y"] == 1 + assert decoded["v"] == 5 + assert decoded["a_o"] == 0 + + def test_stream_left_wraps(self, lattice): + """Channel 3 (-x): (0,0) -> (3,0) via periodic wrap after 1 step.""" + alg = ABQLBM(lattice) + circuit = prepare_single_particle(lattice, (0, 0), 3) + circuit.compose(alg.circuit, inplace=True) + + sv = run_statevector(circuit) + amps = get_nonzero_amplitudes(sv) + layout = make_ab_qubit_layout(lattice) + + assert len(amps) == 1 + decoded = decode_state(list(amps.keys())[0], layout) + assert decoded["g_x"] == 3 + assert decoded["g_y"] == 0 + assert decoded["v"] == 3 + assert decoded["a_o"] == 0 + + def test_rest_particle_stays(self, lattice): + """Channel 0 (rest): (1,2) -> (1,2) after 1 step.""" + alg = ABQLBM(lattice) + circuit = prepare_single_particle(lattice, (1, 2), 0) + circuit.compose(alg.circuit, inplace=True) + + sv = run_statevector(circuit) + amps = get_nonzero_amplitudes(sv) + layout = make_ab_qubit_layout(lattice) + + assert len(amps) == 1 + decoded = decode_state(list(amps.keys())[0], layout) + assert decoded["g_x"] == 1 + assert decoded["g_y"] == 2 + assert decoded["v"] == 0 + assert decoded["a_o"] == 0 + + def test_two_steps_right(self, lattice): + """Channel 1 (+x): (0,0) -> (2,0) after 2 steps.""" + alg = ABQLBM(lattice) + circuit = prepare_single_particle(lattice, (0, 0), 1) + circuit.compose(alg.circuit, inplace=True) + circuit.compose(alg.circuit.copy(), inplace=True) + + sv = run_statevector(circuit) + amps = get_nonzero_amplitudes(sv) + layout = make_ab_qubit_layout(lattice) + + assert len(amps) == 1 + decoded = decode_state(list(amps.keys())[0], layout) + assert decoded["g_x"] == 2 + assert decoded["g_y"] == 0 + assert decoded["v"] == 1 + assert decoded["a_o"] == 0 + + +# Streaming with bounceback obstacle +class TestABBounceback: + """ABQLBM on an 8x8 D2Q9 lattice with one bounceback cuboid. + + The zone-agnostic reflection algorithm works as follows. After + streaming, if a particle ends up inside the obstacle, its velocity is + reversed and the position is corrected so that it stays just outside the + obstacle wall (i.e., the particle effectively reflects at the boundary). + + Analytically: ``|p, v> -> |p, -v>`` when ``p+v`` falls inside the + obstacle, where ``-v`` is the bounceback-reversed velocity. + """ + + @pytest.fixture + def lattice(self): + """8x8 D2Q9 lattice with a bounceback wall at x in [3,5], y in [0,6].""" + return ABLattice( + { + "lattice": {"dim": {"x": 8, "y": 8}, "velocities": "D2Q9"}, + "geometry": [ + { + "shape": "cuboid", + "x": [3, 5], + "y": [0, 6], + "boundary": "bounceback", + } + ], + } + ) + + def test_lattice_qubit_count(self, lattice): + """Verify register sizes with obstacle.""" + assert lattice.num_dims == 2 + assert lattice.num_gridpoints == [7, 7] + assert lattice.num_grid_qubits == 6 + assert lattice.num_velocity_qubits == 4 + assert lattice.num_obstacle_qubits == 1 + assert lattice.num_comparator_qubits == 2 + assert lattice.num_total_qubits == 13 + + def test_bounceback_right_into_wall(self, lattice): + """Particle at (2,1) with v=1 (+x) bounces off the obstacle. + + After streaming the particle would land at (3,1) which is inside + the obstacle [3,5]x[0,6]. Bounceback reflects it back to (2,1) + with reversed velocity v=3 (-x). + """ + alg = ABQLBM(lattice) + circuit = prepare_single_particle(lattice, (2, 1), 1) + circuit.compose(alg.circuit, inplace=True) + + sv = run_statevector(circuit) + amps = get_nonzero_amplitudes(sv) + layout = make_ab_qubit_layout(lattice) + + assert len(amps) == 1 + decoded = decode_state(list(amps.keys())[0], layout) + assert decoded["g_x"] == 2 + assert decoded["g_y"] == 1 + assert decoded["v"] == D2Q9_BOUNCEBACK[1] + assert decoded["a_o"] == 0 + assert decoded["a_c"] == 0 + + def test_free_streaming_away_from_obstacle(self, lattice): + """Particle at (1,1) with v=3 (-x) streams normally past the obstacle. + + Target position (0,1) is outside the obstacle. + """ + alg = ABQLBM(lattice) + circuit = prepare_single_particle(lattice, (1, 1), 3) + circuit.compose(alg.circuit, inplace=True) + + sv = run_statevector(circuit) + amps = get_nonzero_amplitudes(sv) + layout = make_ab_qubit_layout(lattice) + + assert len(amps) == 1 + decoded = decode_state(list(amps.keys())[0], layout) + assert decoded["g_x"] == 0 + assert decoded["g_y"] == 1 + assert decoded["v"] == 3 + assert decoded["a_o"] == 0 + assert decoded["a_c"] == 0 + + def test_bounceback_then_free_streaming(self, lattice): + """Two-step test: bounce off wall, then stream freely. + + Step 1: (2,1) v=1 -> bounces to (2,1) v=3 + Step 2: (2,1) v=3 -> streams to (1,1) v=3 + """ + alg = ABQLBM(lattice) + circuit = prepare_single_particle(lattice, (2, 1), 1) + circuit.compose(alg.circuit, inplace=True) + circuit.compose(alg.circuit.copy(), inplace=True) + + sv = run_statevector(circuit) + amps = get_nonzero_amplitudes(sv) + layout = make_ab_qubit_layout(lattice) + + assert len(amps) == 1 + decoded = decode_state(list(amps.keys())[0], layout) + assert decoded["g_x"] == 1 + assert decoded["g_y"] == 1 + assert decoded["v"] == 3 + assert decoded["a_o"] == 0 + assert decoded["a_c"] == 0 diff --git a/test/e2e/test_lqlga_e2e.py b/test/e2e/test_lqlga_e2e.py new file mode 100644 index 0000000..7fdc5c1 --- /dev/null +++ b/test/e2e/test_lqlga_e2e.py @@ -0,0 +1,182 @@ +"""End-to-end tests for the LQLGA algorithm. + +The LQLGA (Linear Quantum Lattice Gas Algorithm) uses one qubit per velocity +channel per gridpoint, with log-depth swap-based streaming and EQC collision. + +For D1Q2 on a 4-gridpoint lattice (8 qubits): + - Gridpoints 0, 1, 2, 3 + - Each gridpoint has 2 velocity qubits: vel_0 (+x) and vel_1 (-x) + - Qubit layout: gp0_v0, gp0_v1, gp1_v0, gp1_v1, gp2_v0, gp2_v1, gp3_v0, gp3_v1 + +After 1 algorithm step (collision + streaming + reflection): + - A particle at gridpoint ``g`` with vel_0 moves to ``g+1`` + - A particle at gridpoint ``g`` with vel_1 moves to ``g-1`` + - Periodic wrapping applies at lattice boundaries +""" + +import numpy as np +import pytest + +from qlbm.components.lqlga import LQLGA +from qlbm.components.lqlga.initial import LQGLAInitialConditions +from qlbm.lattice import LQLGALattice + +from .utils import run_statevector + + +def _decode_lqlga_state(sv, num_gridpoints: int, num_velocities: int): + """Decode LQLGA statevector into per-gridpoint velocity occupancy. + + Returns a dict ``{gridpoint: {velocity: 1}}`` for occupied channels. + """ + data = np.array(sv) + nonzero = np.where(np.abs(data) > 1e-8)[0] + occupied = {} + for idx in nonzero: + for gp in range(num_gridpoints): + for v in range(num_velocities): + bit = gp * num_velocities + v + if (idx >> bit) & 1: + occupied.setdefault(gp, {})[v] = 1 + return occupied + + +# Free streaming (no geometry) +class TestLQLGAFreeStreaming: + """LQLGA D1Q2 on a 4-gridpoint lattice, no obstacles (8 qubits).""" + + @pytest.fixture + def lattice(self): + """4-gridpoint D1Q2, no obstacles.""" + return LQLGALattice( + { + "lattice": {"dim": {"x": 4}, "velocities": "D1Q2"}, + "geometry": [], + } + ) + + def test_lattice_qubit_count(self, lattice): + """Verify expected register sizes.""" + assert lattice.num_total_qubits == 8 + + def test_stream_right(self, lattice): + """Particle at gp1 with vel_0 (+x) -> gp2 after 1 step.""" + alg = LQLGA(lattice) + ic = LQGLAInitialConditions(lattice, grid_data=[((1,), (True, False))]) + + circuit = ic.circuit.copy() + circuit.compose(alg.circuit, inplace=True) + sv = run_statevector(circuit) + + result = _decode_lqlga_state(sv, 4, 2) + assert result == {2: {0: 1}} + + def test_stream_left(self, lattice): + """Particle at gp1 with vel_1 (-x) -> gp0 after 1 step.""" + alg = LQLGA(lattice) + ic = LQGLAInitialConditions(lattice, grid_data=[((1,), (False, True))]) + + circuit = ic.circuit.copy() + circuit.compose(alg.circuit, inplace=True) + sv = run_statevector(circuit) + + result = _decode_lqlga_state(sv, 4, 2) + assert result == {0: {1: 1}} + + def test_stream_wraps(self, lattice): + """Particle at gp0 with vel_1 (-x) wraps to gp3.""" + alg = LQLGA(lattice) + ic = LQGLAInitialConditions(lattice, grid_data=[((0,), (False, True))]) + + circuit = ic.circuit.copy() + circuit.compose(alg.circuit, inplace=True) + sv = run_statevector(circuit) + + result = _decode_lqlga_state(sv, 4, 2) + assert result == {3: {1: 1}} + + def test_both_velocities_split(self, lattice): + """Particle at gp1 with both velocities splits to gp0 and gp2.""" + alg = LQLGA(lattice) + ic = LQGLAInitialConditions(lattice, grid_data=[((1,), (True, True))]) + + circuit = ic.circuit.copy() + circuit.compose(alg.circuit, inplace=True) + sv = run_statevector(circuit) + + result = _decode_lqlga_state(sv, 4, 2) + assert result == {0: {1: 1}, 2: {0: 1}} + + def test_two_steps_right(self, lattice): + """Particle at gp1 with vel_0 -> gp3 after 2 steps.""" + alg = LQLGA(lattice) + ic = LQGLAInitialConditions(lattice, grid_data=[((1,), (True, False))]) + + circuit = ic.circuit.copy() + circuit.compose(alg.circuit, inplace=True) + circuit.compose(alg.circuit.copy(), inplace=True) + sv = run_statevector(circuit) + + result = _decode_lqlga_state(sv, 4, 2) + assert result == {3: {0: 1}} + + +# Streaming with bounceback obstacle +class TestLQLGABounceback: + """LQLGA D1Q2 with a bounceback obstacle at gridpoint 3 (8 qubits).""" + + @pytest.fixture + def lattice(self): + """4-gridpoint D1Q2, obstacle at gp3.""" + return LQLGALattice( + { + "lattice": {"dim": {"x": 4}, "velocities": "D1Q2"}, + "geometry": [ + {"shape": "cuboid", "x": [3, 3], "boundary": "bounceback"} + ], + } + ) + + def test_bounceback_into_wall(self, lattice): + """Particle at gp2 with vel_0 (+x) bounces off obstacle at gp3. + + The particle stays at gp2 with reversed velocity vel_1 (-x). + """ + alg = LQLGA(lattice) + ic = LQGLAInitialConditions(lattice, grid_data=[((2,), (True, False))]) + + circuit = ic.circuit.copy() + circuit.compose(alg.circuit, inplace=True) + sv = run_statevector(circuit) + + result = _decode_lqlga_state(sv, 4, 2) + assert result == {2: {1: 1}} + + def test_free_streaming_away_from_obstacle(self, lattice): + """Particle at gp2 with vel_1 (-x) streams freely to gp1.""" + alg = LQLGA(lattice) + ic = LQGLAInitialConditions(lattice, grid_data=[((2,), (False, True))]) + + circuit = ic.circuit.copy() + circuit.compose(alg.circuit, inplace=True) + sv = run_statevector(circuit) + + result = _decode_lqlga_state(sv, 4, 2) + assert result == {1: {1: 1}} + + def test_bounceback_then_free_streaming(self, lattice): + """Two-step: bounce then stream freely. + + Step 1: gp2 vel_0 -> bounces -> gp2 vel_1 + Step 2: gp2 vel_1 -> streams -> gp1 vel_1 + """ + alg = LQLGA(lattice) + ic = LQGLAInitialConditions(lattice, grid_data=[((2,), (True, False))]) + + circuit = ic.circuit.copy() + circuit.compose(alg.circuit, inplace=True) + circuit.compose(alg.circuit.copy(), inplace=True) + sv = run_statevector(circuit) + + result = _decode_lqlga_state(sv, 4, 2) + assert result == {1: {1: 1}} diff --git a/test/e2e/test_ms_e2e.py b/test/e2e/test_ms_e2e.py new file mode 100644 index 0000000..b3c7fb4 --- /dev/null +++ b/test/e2e/test_ms_e2e.py @@ -0,0 +1,238 @@ +"""End-to-end tests for the MSQLBM algorithm. + +The MSQLBM algorithm uses per-dimension velocity encoding with separate +magnitude and direction qubits. Streaming is performed via CFL substeps. + +For a 4x4 lattice with 4 discrete velocities per dimension: + - 1 velocity magnitude qubit per dimension (values 0 or 1) + - 1 velocity direction qubit per dimension (1 = positive, 0 = negative) + - CFL time series ``get_time_series(4) = [[1], [1], [0, 1]]``: + magnitude 1 streams in all 3 substeps, magnitude 0 streams once. + +Therefore per algorithm step: + - A particle with magnitude 1 moves +/-3 gridpoints. + - A particle with magnitude 0 moves +/-1 gridpoint. +""" + +import pytest + +from qlbm.components.ms import MSQLBM +from qlbm.lattice import MSLattice + +from .utils import ( + decode_state, + get_nonzero_amplitudes, + make_ms_qubit_layout, + prepare_ms_particle, + run_statevector, +) + + +# Free streaming (no geometry) +class TestMSFreeStreaming: + """MSQLBM on a 4x4 lattice with 4 velocities/dim, no obstacles (13 qubits). + + With no geometry, the reflection operator is absent and the algorithm + is pure streaming through CFL substeps. + """ + + @pytest.fixture + def lattice(self): + """4x4, 4 vel/dim, no obstacles.""" + return MSLattice( + { + "lattice": {"dim": {"x": 4, "y": 4}, "velocities": {"x": 4, "y": 4}}, + "geometry": [], + } + ) + + def test_lattice_qubit_count(self, lattice): + """Verify expected register sizes.""" + assert lattice.num_total_qubits == 13 + + def test_stream_slow_positive(self, lattice): + """Magnitude 0 with positive direction: (0,0) -> (1,1) after 1 step. + + Magnitude 0 streams once (last CFL substep only), moving +1 in each dim. + """ + alg = MSQLBM(lattice) + circuit = prepare_ms_particle(lattice, (0, 0), (0, 0), (1, 1)) + circuit.compose(alg.circuit, inplace=True) + + sv = run_statevector(circuit) + amps = get_nonzero_amplitudes(sv) + layout = make_ms_qubit_layout(lattice) + + assert len(amps) == 1 + decoded = decode_state(list(amps.keys())[0], layout) + assert decoded["g_x"] == 1 + assert decoded["g_y"] == 1 + assert decoded["vd_x"] == 1 + assert decoded["vd_y"] == 1 + assert decoded["a_v"] == 0 + + def test_stream_slow_negative(self, lattice): + """Magnitude 0 with negative direction: (1,1) -> (0,0) after 1 step.""" + alg = MSQLBM(lattice) + circuit = prepare_ms_particle(lattice, (1, 1), (0, 0), (0, 0)) + circuit.compose(alg.circuit, inplace=True) + + sv = run_statevector(circuit) + amps = get_nonzero_amplitudes(sv) + layout = make_ms_qubit_layout(lattice) + + assert len(amps) == 1 + decoded = decode_state(list(amps.keys())[0], layout) + assert decoded["g_x"] == 0 + assert decoded["g_y"] == 0 + assert decoded["vd_x"] == 0 + assert decoded["vd_y"] == 0 + assert decoded["a_v"] == 0 + + def test_stream_slow_negative_wraps(self, lattice): + """Magnitude 0 with negative direction from (0,0) wraps to (3,3).""" + alg = MSQLBM(lattice) + circuit = prepare_ms_particle(lattice, (0, 0), (0, 0), (0, 0)) + circuit.compose(alg.circuit, inplace=True) + + sv = run_statevector(circuit) + amps = get_nonzero_amplitudes(sv) + layout = make_ms_qubit_layout(lattice) + + assert len(amps) == 1 + decoded = decode_state(list(amps.keys())[0], layout) + assert decoded["g_x"] == 3 + assert decoded["g_y"] == 3 + + def test_stream_fast_positive(self, lattice): + """Magnitude 1 with positive x, magnitude 0 with positive y. + + Magnitude 1 streams 3 times in x (+3), magnitude 0 streams once in y (+1). + (0,0) -> (3,1). + """ + alg = MSQLBM(lattice) + circuit = prepare_ms_particle(lattice, (0, 0), (1, 0), (1, 1)) + circuit.compose(alg.circuit, inplace=True) + + sv = run_statevector(circuit) + amps = get_nonzero_amplitudes(sv) + layout = make_ms_qubit_layout(lattice) + + assert len(amps) == 1 + decoded = decode_state(list(amps.keys())[0], layout) + assert decoded["g_x"] == 3 + assert decoded["g_y"] == 1 + assert decoded["v_x"] == 1 + assert decoded["vd_x"] == 1 + + def test_stream_mixed_directions(self, lattice): + """Positive x, negative y: (1,0) -> (2,3) after 1 step. + + Magnitude 0 in both dims, streams once: x+1=2, y-1=-1=3 mod 4. + """ + alg = MSQLBM(lattice) + circuit = prepare_ms_particle(lattice, (1, 0), (0, 0), (1, 0)) + circuit.compose(alg.circuit, inplace=True) + + sv = run_statevector(circuit) + amps = get_nonzero_amplitudes(sv) + layout = make_ms_qubit_layout(lattice) + + assert len(amps) == 1 + decoded = decode_state(list(amps.keys())[0], layout) + assert decoded["g_x"] == 2 + assert decoded["g_y"] == 3 + + def test_two_steps(self, lattice): + """Two steps with magnitude 0, positive direction: (0,0) -> (2,2).""" + alg = MSQLBM(lattice) + circuit = prepare_ms_particle(lattice, (0, 0), (0, 0), (1, 1)) + circuit.compose(alg.circuit, inplace=True) + circuit.compose(alg.circuit.copy(), inplace=True) + + sv = run_statevector(circuit) + amps = get_nonzero_amplitudes(sv) + layout = make_ms_qubit_layout(lattice) + + assert len(amps) == 1 + decoded = decode_state(list(amps.keys())[0], layout) + assert decoded["g_x"] == 2 + assert decoded["g_y"] == 2 + + +# --------------------------------------------------------------------------- +# Streaming with bounceback obstacle +# --------------------------------------------------------------------------- + + +class TestMSBounceback: + """MSQLBM on a 4x4 lattice with a bounceback obstacle (13 qubits). + + The obstacle spans x in [2,3], y in [0,3]. Particles streaming into + the obstacle have their velocity direction flipped and are streamed + back out. + """ + + @pytest.fixture + def lattice(self): + """4x4, 4 vel/dim, obstacle at x=[2,3], y=[0,3].""" + return MSLattice( + { + "lattice": {"dim": {"x": 4, "y": 4}, "velocities": {"x": 4, "y": 4}}, + "geometry": [ + { + "shape": "cuboid", + "x": [2, 3], + "y": [0, 3], + "boundary": "bounceback", + } + ], + } + ) + + def test_bounceback_into_wall(self, lattice): + """Particle at (1,0) with v_mag=0, vd=(+,+) bounces off obstacle. + + After streaming the particle would land at (2,1), which is inside + the obstacle [2,3]x[0,3]. Bounceback reflects it back to (1,0) + with flipped direction vd=(0,0) = (-,-). + + The position and velocity direction are the primary assertion. + The obstacle ancilla may be dirty after reflection. + """ + alg = MSQLBM(lattice) + circuit = prepare_ms_particle(lattice, (1, 0), (0, 0), (1, 1)) + circuit.compose(alg.circuit, inplace=True) + + sv = run_statevector(circuit) + amps = get_nonzero_amplitudes(sv) + layout = make_ms_qubit_layout(lattice) + + assert len(amps) == 1 + decoded = decode_state(list(amps.keys())[0], layout) + assert decoded["g_x"] == 1 + assert decoded["g_y"] == 0 + assert decoded["vd_x"] == 0 + assert decoded["vd_y"] == 0 + + def test_free_streaming_away_from_obstacle(self, lattice): + """Particle at (1,1) with vd=(-,+) streams freely away from obstacle. + + Target position (0,2) is outside the obstacle. + """ + alg = MSQLBM(lattice) + circuit = prepare_ms_particle(lattice, (1, 1), (0, 0), (0, 1)) + circuit.compose(alg.circuit, inplace=True) + + sv = run_statevector(circuit) + amps = get_nonzero_amplitudes(sv) + layout = make_ms_qubit_layout(lattice) + + assert len(amps) == 1 + decoded = decode_state(list(amps.keys())[0], layout) + assert decoded["g_x"] == 0 + assert decoded["g_y"] == 2 + assert decoded["vd_x"] == 0 + assert decoded["vd_y"] == 1 + assert decoded["a_o"] == 0 + assert decoded["a_v"] == 0 diff --git a/test/e2e/test_spacetime_e2e.py b/test/e2e/test_spacetime_e2e.py new file mode 100644 index 0000000..6c97ca2 --- /dev/null +++ b/test/e2e/test_spacetime_e2e.py @@ -0,0 +1,196 @@ +"""End-to-end tests for the SpaceTimeQLBM algorithm. + +The SpaceTimeQLBM uses a space-time encoding where velocity information for +neighboring gridpoints is pre-loaded into the register and streaming is +performed via SWAP gates. + +For D1Q2 on a 16-point 1D lattice with 1 timestep (10 qubits): + - 4 grid qubits (positions 0-15) + - 6 velocity qubits: 2 at origin, 2 for right neighbor, 2 for left neighbor + - Velocity 0 = positive direction (+x) + - Velocity 1 = negative direction (-x) + +After 1 streaming step, a particle at position ``x`` with velocity 0 +arrives at position ``x+1``, and with velocity 1 at ``x-1``. + +Only the origin velocity qubits (v0, v1) carry meaningful post-streaming data. +The neighbor velocity qubits contain residual swap artifacts. +""" + +import numpy as np +import pytest + +from qlbm.components.spacetime import SpaceTimeQLBM +from qlbm.components.spacetime.initial import PointWiseSpaceTimeInitialConditions +from qlbm.lattice import SpaceTimeLattice + +from .utils import run_statevector + + +def _origin_velocities_by_position(sv, num_grid_qubits: int, num_velocities: int): + """Extract origin velocity values keyed by grid position. + + Returns a dict ``{position: (vel_0, vel_1, ...)}`` for positions + where at least one origin velocity qubit is set. + """ + data = np.array(sv) + nonzero = np.where(np.abs(data) > 1e-8)[0] + result = {} + grid_mask = (1 << num_grid_qubits) - 1 + for idx in nonzero: + g = idx & grid_mask + vels = tuple((idx >> (num_grid_qubits + v)) & 1 for v in range(num_velocities)) + if any(vels): + result[g] = vels + return result + + +# Free streaming (no geometry) +class TestSpaceTimeFreeStreaming: + """SpaceTimeQLBM D1Q2 on a 16-point 1D lattice, 1 timestep, no obstacles (10 qubits).""" + + @pytest.fixture + def lattice(self): + """16-point D1Q2, 1 timestep, no obstacles.""" + return SpaceTimeLattice( + num_timesteps=1, + lattice_data={ + "lattice": {"dim": {"x": 16}, "velocities": "D1Q2"}, + "geometry": [], + }, + ) + + def test_lattice_qubit_count(self, lattice): + """Verify expected register sizes.""" + assert lattice.num_total_qubits == 10 + + def test_stream_right(self, lattice): + """Particle at x=5 with vel_0 (+x) -> x=6 after 1 step.""" + ic = PointWiseSpaceTimeInitialConditions( + lattice, grid_data=[((5,), (True, False))] + ) + alg = SpaceTimeQLBM(lattice) + + circuit = ic.circuit.copy() + circuit.compose(alg.circuit, inplace=True) + sv = run_statevector(circuit) + + result = _origin_velocities_by_position(sv, 4, 2) + assert result == {6: (1, 0)} + + def test_stream_left(self, lattice): + """Particle at x=10 with vel_1 (-x) -> x=9 after 1 step.""" + ic = PointWiseSpaceTimeInitialConditions( + lattice, grid_data=[((10,), (False, True))] + ) + alg = SpaceTimeQLBM(lattice) + + circuit = ic.circuit.copy() + circuit.compose(alg.circuit, inplace=True) + sv = run_statevector(circuit) + + result = _origin_velocities_by_position(sv, 4, 2) + assert result == {9: (0, 1)} + + def test_two_particles(self, lattice): + """Two particles streaming independently: x=5 right and x=10 left.""" + ic = PointWiseSpaceTimeInitialConditions( + lattice, + grid_data=[ + ((5,), (True, False)), + ((10,), (False, True)), + ], + ) + alg = SpaceTimeQLBM(lattice) + + circuit = ic.circuit.copy() + circuit.compose(alg.circuit, inplace=True) + sv = run_statevector(circuit) + + result = _origin_velocities_by_position(sv, 4, 2) + assert result == {6: (1, 0), 9: (0, 1)} + + def test_both_velocities_at_same_point(self, lattice): + """Particle at x=8 with both vel_0 and vel_1 splits to x=7 and x=9.""" + ic = PointWiseSpaceTimeInitialConditions( + lattice, grid_data=[((8,), (True, True))] + ) + alg = SpaceTimeQLBM(lattice) + + circuit = ic.circuit.copy() + circuit.compose(alg.circuit, inplace=True) + sv = run_statevector(circuit) + + result = _origin_velocities_by_position(sv, 4, 2) + assert result == {9: (1, 0), 7: (0, 1)} + + +# --------------------------------------------------------------------------- +# Streaming with bounceback obstacle +# --------------------------------------------------------------------------- + + +class TestSpaceTimeBounceback: + """SpaceTimeQLBM D1Q2 with a bounceback obstacle at x in [7, 8] (10 qubits).""" + + @pytest.fixture + def lattice(self): + """16-point D1Q2, 1 timestep, obstacle at x=[7,8].""" + return SpaceTimeLattice( + num_timesteps=1, + lattice_data={ + "lattice": {"dim": {"x": 16}, "velocities": "D1Q2"}, + "geometry": [ + {"shape": "cuboid", "x": [7, 8], "boundary": "bounceback"} + ], + }, + ) + + def test_bounceback_into_wall(self, lattice): + """Particle at x=6 with vel_0 (+x) bounces off obstacle at [7,8]. + + After streaming the particle would land at x=7 which is inside + the obstacle. Bounceback reflects it back to x=6 with vel_1 (-x). + """ + ic = PointWiseSpaceTimeInitialConditions( + lattice, grid_data=[((6,), (True, False))] + ) + alg = SpaceTimeQLBM(lattice) + + circuit = ic.circuit.copy() + circuit.compose(alg.circuit, inplace=True) + sv = run_statevector(circuit) + + result = _origin_velocities_by_position(sv, 4, 2) + assert result == {6: (0, 1)} + + def test_free_streaming_away_from_obstacle(self, lattice): + """Particle at x=6 with vel_1 (-x) streams freely to x=5.""" + ic = PointWiseSpaceTimeInitialConditions( + lattice, grid_data=[((6,), (False, True))] + ) + alg = SpaceTimeQLBM(lattice) + + circuit = ic.circuit.copy() + circuit.compose(alg.circuit, inplace=True) + sv = run_statevector(circuit) + + result = _origin_velocities_by_position(sv, 4, 2) + assert result == {5: (0, 1)} + + def test_bounceback_from_other_side(self, lattice): + """Particle at x=9 with vel_1 (-x) bounces off obstacle at [7,8]. + + Would land at x=8 (inside obstacle). Reflects back to x=9 with vel_0. + """ + ic = PointWiseSpaceTimeInitialConditions( + lattice, grid_data=[((9,), (False, True))] + ) + alg = SpaceTimeQLBM(lattice) + + circuit = ic.circuit.copy() + circuit.compose(alg.circuit, inplace=True) + sv = run_statevector(circuit) + + result = _origin_velocities_by_position(sv, 4, 2) + assert result == {9: (1, 0)} diff --git a/test/e2e/utils.py b/test/e2e/utils.py new file mode 100644 index 0000000..2727d10 --- /dev/null +++ b/test/e2e/utils.py @@ -0,0 +1,225 @@ +"""Shared utilities for end-to-end tests.""" + +from typing import Dict, Tuple + +import numpy as np +from qiskit import QuantumCircuit, transpile +from qiskit.quantum_info import Statevector +from qiskit_aer import AerSimulator + + +def run_statevector(circuit: QuantumCircuit) -> Statevector: + """Run a circuit on AerSimulator and return the final statevector. + + Parameters + ---------- + circuit : QuantumCircuit + The circuit to simulate. A ``save_statevector`` instruction is + appended automatically. + + Returns + ------- + Statevector + The resulting statevector. + """ + qc = circuit.copy() + qc.save_statevector() + sim = AerSimulator(method="statevector") + tqc = transpile(qc, sim, optimization_level=0) + result = sim.run(tqc).result() + return result.data(0)["statevector"] + + +def get_nonzero_amplitudes( + sv: Statevector, threshold: float = 1e-8 +) -> Dict[int, complex]: + """Return a dict mapping basis-state index to amplitude for nonzero entries. + + Parameters + ---------- + sv : Statevector + The statevector to inspect. + threshold : float + Amplitude magnitude below which entries are treated as zero. + + Returns + ------- + Dict[int, complex] + Mapping from statevector index to complex amplitude. + """ + data = np.array(sv) + nonzero_idx = np.where(np.abs(data) > threshold)[0] + return {int(idx): complex(data[idx]) for idx in nonzero_idx} + + +def decode_state( + index: int, qubit_ranges: Dict[str, Tuple[int, int]] +) -> Dict[str, int]: + """Decode a statevector index into named register values. + + Parameters + ---------- + index : int + The statevector basis-state index. + qubit_ranges : Dict[str, Tuple[int, int]] + Mapping from register name to ``(start_qubit, num_qubits)``. + + Returns + ------- + Dict[str, int] + Mapping from register name to the integer value stored in that register. + """ + result: Dict[str, int] = {} + for name, (start, size) in qubit_ranges.items(): + value = 0 + for i in range(size): + if index & (1 << (start + i)): + value |= 1 << i + result[name] = value + return result + + +def make_ab_qubit_layout(lattice) -> Dict[str, Tuple[int, int]]: + """Build a qubit-layout dictionary for an ABLattice. + + Parameters + ---------- + lattice : ABLattice + The lattice whose register structure to describe. + + Returns + ------- + Dict[str, Tuple[int, int]] + Mapping ``{register_name: (start_qubit, size)}``. + """ + dim_names = ["g_x", "g_y", "g_z"] + layout: Dict[str, Tuple[int, int]] = {} + for dim in range(lattice.num_dims): + layout[dim_names[dim]] = ( + lattice.grid_index(dim)[0], + len(lattice.grid_index(dim)), + ) + layout["v"] = (lattice.velocity_index()[0], lattice.num_velocity_qubits) + if lattice.num_comparator_qubits > 0: + layout["a_c"] = ( + lattice.ancillae_comparator_index()[0], + lattice.num_comparator_qubits, + ) + layout["a_o"] = ( + lattice.ancillae_obstacle_index()[0], + lattice.num_obstacle_qubits, + ) + return layout + + +def make_ms_qubit_layout(lattice) -> Dict[str, Tuple[int, int]]: + """Build a qubit-layout dictionary for an MSLattice. + + Parameters + ---------- + lattice : MSLattice + The lattice whose register structure to describe. + + Returns + ------- + Dict[str, Tuple[int, int]] + Mapping ``{register_name: (start_qubit, size)}``. + """ + dim_names = ["g_x", "g_y", "g_z"] + layout: Dict[str, Tuple[int, int]] = {} + layout["a_v"] = ( + lattice.ancillae_velocity_index()[0], + len(lattice.ancillae_velocity_index()), + ) + layout["a_o"] = ( + lattice.ancillae_obstacle_index()[0], + len(lattice.ancillae_obstacle_index()), + ) + if lattice.ancillae_comparator_index(): + layout["a_c"] = ( + lattice.ancillae_comparator_index()[0], + len(lattice.ancillae_comparator_index()), + ) + for dim in range(lattice.num_dims): + layout[dim_names[dim]] = ( + lattice.grid_index(dim)[0], + len(lattice.grid_index(dim)), + ) + for dim in range(lattice.num_dims): + vi = lattice.velocity_index(dim) + if vi: + layout[f"v_{dim_names[dim][-1]}"] = (vi[0], len(vi)) + for dim in range(lattice.num_dims): + layout[f"vd_{dim_names[dim][-1]}"] = ( + lattice.velocity_dir_index(dim)[0], + len(lattice.velocity_dir_index(dim)), + ) + return layout + + +def prepare_single_particle( + lattice, grid_pos: Tuple[int, ...], velocity_channel: int +) -> QuantumCircuit: + """Prepare a circuit with one particle at a specific position and velocity. + + Parameters + ---------- + lattice : ABLattice | MSLattice + The lattice that defines the register layout. + grid_pos : Tuple[int, ...] + Grid coordinates, one per dimension. + velocity_channel : int + Integer index of the velocity channel (binary-encoded into velocity qubits). + + Returns + ------- + QuantumCircuit + A circuit that prepares the desired initial state. + """ + circuit = QuantumCircuit(*lattice.registers) + for dim, pos in enumerate(grid_pos): + for i in range(lattice.num_gridpoints[dim].bit_length()): + if (pos >> i) & 1: + circuit.x(lattice.grid_index(dim)[i]) + for i in range(lattice.num_velocity_qubits): + if (velocity_channel >> i) & 1: + circuit.x(lattice.velocity_index()[i]) + return circuit + + +def prepare_ms_particle( + lattice, + grid_pos: Tuple[int, ...], + velocity_mag: Tuple[int, ...], + velocity_dir: Tuple[int, ...], +) -> QuantumCircuit: + """Prepare a circuit with one MS particle at a given position and velocity. + + Parameters + ---------- + lattice : MSLattice + The lattice that defines the register layout. + grid_pos : Tuple[int, ...] + Grid coordinates, one per dimension. + velocity_mag : Tuple[int, ...] + Velocity magnitude per dimension (binary-encoded). + velocity_dir : Tuple[int, ...] + Velocity direction per dimension (1 = positive, 0 = negative). + + Returns + ------- + QuantumCircuit + A circuit that prepares the desired initial state. + """ + circuit = QuantumCircuit(*lattice.registers) + for dim, pos in enumerate(grid_pos): + for i in range(len(lattice.grid_index(dim))): + if (pos >> i) & 1: + circuit.x(lattice.grid_index(dim)[i]) + for dim in range(lattice.num_dims): + for i in range(len(lattice.velocity_index(dim))): + if (velocity_mag[dim] >> i) & 1: + circuit.x(lattice.velocity_index(dim)[i]) + if velocity_dir[dim]: + circuit.x(lattice.velocity_dir_index(dim)[0]) + return circuit From dd9e8c3ce6f33e311aaf7f52d2022988e735f03d Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Tue, 10 Mar 2026 12:27:05 +0100 Subject: [PATCH 76/78] Add demo simulation notebooks for parallel ICs and BCs in ABQLBM --- demos/simulation/ab_simulation.ipynb | 8 + .../ab_simulation_parallel_bcs.ipynb | 184 ++++++++++++++++++ .../ab_simulation_parallel_ics.ipynb | 177 +++++++++++++++++ 3 files changed, 369 insertions(+) create mode 100644 demos/simulation/ab_simulation_parallel_bcs.ipynb create mode 100644 demos/simulation/ab_simulation_parallel_ics.ipynb diff --git a/demos/simulation/ab_simulation.ipynb b/demos/simulation/ab_simulation.ipynb index 5d7ef3f..45ab31a 100644 --- a/demos/simulation/ab_simulation.ipynb +++ b/demos/simulation/ab_simulation.ipynb @@ -119,6 +119,14 @@ " statevector_snapshots=True,\n", ")" ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b61274d7", + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { diff --git a/demos/simulation/ab_simulation_parallel_bcs.ipynb b/demos/simulation/ab_simulation_parallel_bcs.ipynb new file mode 100644 index 0000000..605513e --- /dev/null +++ b/demos/simulation/ab_simulation_parallel_bcs.ipynb @@ -0,0 +1,184 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "348de144", + "metadata": {}, + "source": [ + "# Simulating the Amplitude-Based Collisionless QLBM" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "181af34e", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit_aer import AerSimulator\n", + "\n", + "from qlbm.components import (\n", + " CQLBM,\n", + " ABDiscreteUniformInitialConditions,\n", + " ABGridMeasurement,\n", + " EmptyPrimitive,\n", + ")\n", + "from qlbm.infra import QiskitRunner, SimulationConfig\n", + "from qlbm.lattice import ABLattice\n", + "from qlbm.tools.utils import create_directory_and_parents" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "05e4915d", + "metadata": {}, + "outputs": [], + "source": [ + "lattice = ABLattice(\n", + " {\n", + " \"lattice\": {\"dim\": {\"x\": 16, \"y\": 16}, \"velocities\": \"d2q9\"},\n", + " \"geometry\": [],\n", + " }\n", + ")\n", + "\n", + "output_dir = \"qlbm-output/ab-pbc-d2q9-16x16-1-obstacle-qiskit\"\n", + "create_directory_and_parents(output_dir)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5e274b50", + "metadata": {}, + "outputs": [], + "source": [ + "# Geometry of marker |0>: two \"stacked\" \"wide\" rectangles\n", + "# Geometry of the marker |1>: one \"tall\" rectangle under the first two\n", + "# These geometries will act fully in parallel, and we will observe the average behavior\n", + "lattice.set_geometries(\n", + " [\n", + " [\n", + " {\"shape\": \"cuboid\", \"x\": [6, 12], \"y\": [12, 14], \"boundary\": \"bounceback\"},\n", + " {\"shape\": \"cuboid\", \"x\": [6, 12], \"y\": [8, 10], \"boundary\": \"bounceback\"},\n", + " # {\"shape\": \"cuboid\", \"x\": [6, 8], \"y\": [0, 6], \"boundary\": \"bounceback\"},\n", + " ],\n", + " [{\"shape\": \"cuboid\", \"x\": [6, 8], \"y\": [0, 6], \"boundary\": \"bounceback\"}],\n", + " ]\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "89709eb9", + "metadata": {}, + "outputs": [], + "source": [ + "lattice.geometries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "deb472a0", + "metadata": {}, + "outputs": [], + "source": [ + "from qlbm.components.ab.initial import ABParallelDiscreteUniformInitialConditions\n", + "\n", + "cfg = SimulationConfig(\n", + " initial_conditions=ABParallelDiscreteUniformInitialConditions(\n", + " lattice,\n", + " [[1], [1]],\n", + " [([0, 1], [0, 1, 2, 3]), ([0, 1], [0, 1, 2, 3])],\n", + " ),\n", + " algorithm=CQLBM(lattice),\n", + " postprocessing=EmptyPrimitive(lattice),\n", + " measurement=ABGridMeasurement(lattice),\n", + " target_platform=\"QISKIT\",\n", + " compiler_platform=\"QISKIT\",\n", + " optimization_level=0,\n", + " statevector_sampling=True,\n", + " execution_backend=AerSimulator(method=\"statevector\"),\n", + " sampling_backend=AerSimulator(method=\"statevector\"),\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "57240678", + "metadata": {}, + "outputs": [], + "source": [ + "cfg.prepare_for_simulation()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1da18eb8", + "metadata": {}, + "outputs": [], + "source": [ + "# Number of shots to simulate for each timestep when running the circuit\n", + "NUM_SHOTS = 2**12\n", + "\n", + "# Number of timesteps to simulate\n", + "NUM_STEPS = 20" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5c8c7d1e", + "metadata": {}, + "outputs": [], + "source": [ + "runner = QiskitRunner(\n", + " cfg,\n", + " lattice,\n", + ")\n", + "\n", + "\n", + "# Simulate the circuits using both snapshots\n", + "runner.run(\n", + " NUM_STEPS, # Number of time steps\n", + " NUM_SHOTS, # Number of shots per time step\n", + " output_dir,\n", + " statevector_snapshots=True,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "52a7ae28", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "qlbm-cpu-venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/demos/simulation/ab_simulation_parallel_ics.ipynb b/demos/simulation/ab_simulation_parallel_ics.ipynb new file mode 100644 index 0000000..42837d6 --- /dev/null +++ b/demos/simulation/ab_simulation_parallel_ics.ipynb @@ -0,0 +1,177 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "348de144", + "metadata": {}, + "source": [ + "# Simulating the Amplitude-Based Collisionless QLBM" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "181af34e", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit_aer import AerSimulator\n", + "\n", + "from qlbm.components import (\n", + " CQLBM,\n", + " ABGridMeasurement,\n", + " ABParallelDiscreteUniformInitialConditions,\n", + " EmptyPrimitive,\n", + ")\n", + "from qlbm.infra import QiskitRunner, SimulationConfig\n", + "from qlbm.lattice import ABLattice\n", + "from qlbm.tools.utils import create_directory_and_parents" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "05e4915d", + "metadata": {}, + "outputs": [], + "source": [ + "lattice = ABLattice(\n", + " {\n", + " \"lattice\": {\"dim\": {\"x\": 16, \"y\": 16}, \"velocities\": \"d2q9\"},\n", + " \"geometry\": [\n", + " {\"shape\": \"cuboid\", \"x\": [10, 13], \"y\": [6, 14], \"boundary\": \"bounceback\"},\n", + " ],\n", + " }\n", + ")\n", + "\n", + "output_dir = \"qlbm-output/ab-pic-d2q9-16x16-1-obstacle-qiskit\"\n", + "create_directory_and_parents(output_dir)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5e274b50", + "metadata": {}, + "outputs": [], + "source": [ + "# Set the number of marker registers, such that we can have up to 2 parallel initial conditions\n", + "lattice.set_num_marker_qubits(1)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1aaa1787", + "metadata": {}, + "outputs": [], + "source": [ + "# ICs moving E and NE, assembled on a rectangle [0, 1, 2, 3] x [0, ..., 15] on marker |0>\n", + "# and W and SW, assembled on a rectangle [0, ..., 15] x [0, 1, 2, 3] on marker |1>\n", + "ics = ABParallelDiscreteUniformInitialConditions(\n", + " lattice,\n", + " [[1, 5], [3, 7]],\n", + " [([0, 1], [0, 1, 2, 3]), ([0, 1, 2, 3], [0, 1])],\n", + ")\n", + "\n", + "ics.draw(\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "deb472a0", + "metadata": {}, + "outputs": [], + "source": [ + "cfg = SimulationConfig(\n", + " initial_conditions=ics,\n", + " algorithm=CQLBM(lattice),\n", + " postprocessing=EmptyPrimitive(lattice),\n", + " measurement=ABGridMeasurement(lattice),\n", + " target_platform=\"QISKIT\",\n", + " compiler_platform=\"QISKIT\",\n", + " optimization_level=0,\n", + " statevector_sampling=True,\n", + " execution_backend=AerSimulator(method=\"statevector\"),\n", + " sampling_backend=AerSimulator(method=\"statevector\"),\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "57240678", + "metadata": {}, + "outputs": [], + "source": [ + "cfg.prepare_for_simulation()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1da18eb8", + "metadata": {}, + "outputs": [], + "source": [ + "# Number of shots to simulate for each timestep when running the circuit\n", + "NUM_SHOTS = 2**12\n", + "\n", + "# Number of timesteps to simulate\n", + "NUM_STEPS = 20" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5c8c7d1e", + "metadata": {}, + "outputs": [], + "source": [ + "runner = QiskitRunner(\n", + " cfg,\n", + " lattice,\n", + ")\n", + "\n", + "\n", + "# Simulate the circuits using both snapshots\n", + "runner.run(\n", + " NUM_STEPS, # Number of time steps\n", + " NUM_SHOTS, # Number of shots per time step\n", + " output_dir,\n", + " statevector_snapshots=True,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "52a7ae28", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "qlbm-cpu-venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From a4d2b09f72a1748dd56ead5002536fe6bcae3f17 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Tue, 10 Mar 2026 12:47:33 +0100 Subject: [PATCH 77/78] Fix imports and type annotations --- .../ab/reflection/agnosotic_reflection.py | 5 ++--- test/e2e/test_lqlga_e2e.py | 4 +++- .../ab/ab_zone_agnostic_reflection_test.py | 18 +++++++++--------- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/qlbm/components/ab/reflection/agnosotic_reflection.py b/qlbm/components/ab/reflection/agnosotic_reflection.py index 3514955..3b2da04 100644 --- a/qlbm/components/ab/reflection/agnosotic_reflection.py +++ b/qlbm/components/ab/reflection/agnosotic_reflection.py @@ -9,7 +9,6 @@ from typing_extensions import override from qlbm.components.ab.reflection.common import ABReflectionPermutation -from qlbm.components.ab.reflection.standard_reflection import ABReflectionOperator from qlbm.components.ab.streaming import ABStreamingOperator from qlbm.components.base import LBMOperator, LBMPrimitive from qlbm.components.common.adders import ParameterizedDraperAdder @@ -178,7 +177,7 @@ def __create_circuit_multi_geometry(self) -> QuantumCircuit: """ circuit = self.lattice.circuit.copy() - oracle = self.__build_combined_oracle() + oracle = self.build_combined_oracle() circuit.compose(oracle, inplace=True) circuit.compose(self.permute_and_stream(), inplace=True) @@ -194,7 +193,7 @@ def __create_circuit_multi_geometry(self) -> QuantumCircuit: return circuit - def __build_combined_oracle(self) -> QuantumCircuit: + def build_combined_oracle(self) -> QuantumCircuit: r"""Build the combined oracle for all geometries. For each geometry index :math:`c`, the marker register qubits are diff --git a/test/e2e/test_lqlga_e2e.py b/test/e2e/test_lqlga_e2e.py index 7fdc5c1..957d9c1 100644 --- a/test/e2e/test_lqlga_e2e.py +++ b/test/e2e/test_lqlga_e2e.py @@ -14,6 +14,8 @@ - Periodic wrapping applies at lattice boundaries """ +from typing import Dict + import numpy as np import pytest @@ -31,7 +33,7 @@ def _decode_lqlga_state(sv, num_gridpoints: int, num_velocities: int): """ data = np.array(sv) nonzero = np.where(np.abs(data) > 1e-8)[0] - occupied = {} + occupied: Dict = {} for idx in nonzero: for gp in range(num_gridpoints): for v in range(num_velocities): diff --git a/test/unit/ab/ab_zone_agnostic_reflection_test.py b/test/unit/ab/ab_zone_agnostic_reflection_test.py index f12f00e..2d585e7 100644 --- a/test/unit/ab/ab_zone_agnostic_reflection_test.py +++ b/test/unit/ab/ab_zone_agnostic_reflection_test.py @@ -253,7 +253,9 @@ def test_oracle_preserves_grid_state_regardless_of_marker(self): grid_qubits = lattice.grid_index() original_grid_probs = original_sv.probabilities(grid_qubits) after_grid_probs = after_sv.probabilities(grid_qubits) - np.testing.assert_allclose(original_grid_probs, after_grid_probs, atol=1e-10) + np.testing.assert_allclose( + original_grid_probs, after_grid_probs, atol=1e-10 + ) def test_ymonomial_raises_with_marker_control(self): """YMonomial oracle should raise when control_on_marker_state=True.""" @@ -275,9 +277,7 @@ def test_ymonomial_raises_with_marker_control(self): shape = lattice.shapes["bounceback"][0] with pytest.raises(CircuitException, match="Marker-controlled oracles"): - ABZoneAgnosticReflectionOracle( - lattice, shape, control_on_marker_state=True - ) + ABZoneAgnosticReflectionOracle(lattice, shape, control_on_marker_state=True) # ============================================================================= @@ -299,7 +299,7 @@ def test_combined_oracle_marks_geometry_0_only(self): operator = ABZoneAgnosticReflectionOperator(lattice) # Access the combined oracle through the private method - oracle = operator._ABZoneAgnosticReflectionOperator__build_combined_oracle() + oracle = operator.build_combined_oracle() # Position (1, 1) is inside geometry 0 ([1,2]x[1,2]) but also inside geometry 1 ([0,1]x[0,1]) # With marker=0, only geometry 0's oracle fires @@ -315,7 +315,7 @@ def test_combined_oracle_marks_geometry_1_only(self): lattice = _make_multi_geometry_lattice() operator = ABZoneAgnosticReflectionOperator(lattice) - oracle = operator._ABZoneAgnosticReflectionOperator__build_combined_oracle() + oracle = operator.build_combined_oracle() # Position (0, 0) is inside geometry 1 ([0,1]x[0,1]) but NOT inside geometry 0 # With marker=1, geometry 1's oracle fires @@ -331,7 +331,7 @@ def test_combined_oracle_does_not_mark_wrong_geometry(self): lattice = _make_multi_geometry_lattice() operator = ABZoneAgnosticReflectionOperator(lattice) - oracle = operator._ABZoneAgnosticReflectionOperator__build_combined_oracle() + oracle = operator.build_combined_oracle() # Position (0, 0) is inside geometry 1 but NOT geometry 0 # With marker=0, geometry 0's oracle fires but position is outside geo 0 @@ -347,7 +347,7 @@ def test_combined_oracle_outside_all_geometries(self): lattice = _make_multi_geometry_lattice() operator = ABZoneAgnosticReflectionOperator(lattice) - oracle = operator._ABZoneAgnosticReflectionOperator__build_combined_oracle() + oracle = operator.build_combined_oracle() # Position (3, 3) is outside both geometry 0 ([1,2]x[1,2]) and geometry 1 ([0,1]x[0,1]) for marker_val in [0, 1]: @@ -365,7 +365,7 @@ def test_combined_oracle_is_self_inverse(self): lattice = _make_multi_geometry_lattice() operator = ABZoneAgnosticReflectionOperator(lattice) - oracle = operator._ABZoneAgnosticReflectionOperator__build_combined_oracle() + oracle = operator.build_combined_oracle() for marker_val in [0, 1]: for x, y in [(1, 1), (0, 0), (3, 3)]: From e53f46981b6e13b731003b93b4bbe20c213d3816 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Tue, 10 Mar 2026 12:48:05 +0100 Subject: [PATCH 78/78] Fix dependency version for qiskit_ibm_runtime --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 376a330..23c9ea9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,7 @@ dependencies = [ "pytket-qiskit", "pytket-qulacs>=0.33", "qiskit==2.1.0", + "qiskit_ibm_runtime==0.41", "qiskit_qasm3_import>=0.4.2", "qiskit-qulacs>=0.1.0", "tqdm>=4.66",