From eb1a568db3c569d96a3a2710028ad57e1ee7c890 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 24 Mar 2025 12:03:58 +0100 Subject: [PATCH 01/92] Update sigstore action version --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7a78f71..b8e2c22 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -133,7 +133,7 @@ jobs: name: python-package-distributions path: dist/ - name: Sign the dists with Sigstore - uses: sigstore/gh-action-sigstore-python@v2.1.1 + uses: sigstore/gh-action-sigstore-python@v3.0.0 with: inputs: | ./dist/*.tar.gz From c24e0fa03ed4f036c32749ad420fb8e5af363648 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Tue, 24 Jun 2025 16:02:34 +0200 Subject: [PATCH 02/92] Remove support for qiskit-qulacs --- qlbm/__init__.py | 2 +- qlbm/infra/__init__.py | 3 +-- qlbm/infra/compiler.py | 6 ++++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/qlbm/__init__.py b/qlbm/__init__.py index 25fa576..f2d7a69 100644 --- a/qlbm/__init__.py +++ b/qlbm/__init__.py @@ -9,7 +9,7 @@ CollisionlessStreamingOperator, SpecularReflectionOperator, ) -from .infra import CircuitCompiler, CollisionlessResult, QiskitRunner, QulacsRunner +from .infra import CircuitCompiler, CollisionlessResult, QiskitRunner from .lattice import CollisionlessLattice, Lattice from .lattice.lattices.spacetime_lattice import SpaceTimeLattice diff --git a/qlbm/infra/__init__.py b/qlbm/infra/__init__.py index 132b189..f7f9839 100644 --- a/qlbm/infra/__init__.py +++ b/qlbm/infra/__init__.py @@ -7,7 +7,7 @@ from .compiler import CircuitCompiler from .result import CollisionlessResult, SpaceTimeResult -from .runner import CircuitRunner, QiskitRunner, QulacsRunner, SimulationConfig +from .runner import CircuitRunner, QiskitRunner, SimulationConfig __all__ = [ "CircuitCompiler", @@ -16,6 +16,5 @@ "CollisionlessResult", "SpaceTimeResult", "QiskitRunner", - "QulacsRunner", "SimulationConfig", ] diff --git a/qlbm/infra/compiler.py b/qlbm/infra/compiler.py index c280489..e1de1b0 100644 --- a/qlbm/infra/compiler.py +++ b/qlbm/infra/compiler.py @@ -12,7 +12,6 @@ from qiskit import QuantumCircuit as QiskitQC from qiskit.compiler import transpile from qiskit_aer.backends.aerbackend import AerBackend -from qiskit_qulacs import QulacsProvider from qulacs import QuantumCircuit as QulacsQC from qlbm.components.base import QuantumComponent @@ -196,7 +195,10 @@ def compile( self.logger.warn( "Provided backend is ignored. The qiskit-qulacs backend is always used when compiling to Qulacs from Qiskit." ) - backend = QulacsProvider().get_backend("qulacs_simulator") + raise CompilerException( + "The qiksit-qulacs extension does not currently support qiskit>=2.0." + ) + # backend = QulacsProvider().get_backend("qulacs_simulator") if self.compiler_target == "QISKIT": if not isinstance(backend, AerBackend): From cdc45318e7c621943006706c767d3ac89072d500 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Tue, 24 Jun 2025 17:20:12 +0200 Subject: [PATCH 03/92] Update qiskit gate API usage --- qlbm/components/collisionless/bounceback_reflection.py | 10 +++++----- qlbm/components/collisionless/primitives.py | 4 ++-- qlbm/components/collisionless/specular_reflection.py | 8 ++++---- qlbm/components/collisionless/streaming.py | 5 +++-- qlbm/components/common/primitives.py | 6 ++++-- qlbm/components/spacetime/collision.py | 4 ++-- qlbm/components/spacetime/initial/pointwise.py | 6 +++--- qlbm/components/spacetime/initial/volumetric.py | 4 ++-- qlbm/components/spacetime/measurement.py | 4 ++-- 9 files changed, 27 insertions(+), 24 deletions(-) diff --git a/qlbm/components/collisionless/bounceback_reflection.py b/qlbm/components/collisionless/bounceback_reflection.py index ffe9254..c0098e5 100644 --- a/qlbm/components/collisionless/bounceback_reflection.py +++ b/qlbm/components/collisionless/bounceback_reflection.py @@ -5,7 +5,7 @@ from typing import List from qiskit import QuantumCircuit -from qiskit.circuit.library import MCMT, XGate +from qiskit.circuit.library import MCMTGate, XGate from typing_extensions import override from qlbm.components.base import CQLBMOperator, LBMPrimitive @@ -321,7 +321,7 @@ def reflect_wall( target_qubits = self.lattice.ancillae_obstacle_index(0) circuit.compose( - MCMT( + MCMTGate( XGate(), len(control_qubits), len(target_qubits), @@ -397,7 +397,7 @@ def reset_edge_state( circuit.x(self.lattice.velocity_dir_index(dim)) circuit.compose( - MCMT( + MCMTGate( XGate(), len(control_qubits), len(target_qubits), @@ -467,7 +467,7 @@ def reset_point_state( target_qubits = self.lattice.ancillae_obstacle_index(0) circuit.compose( - MCMT( + MCMTGate( XGate(), len(control_qubits), len(target_qubits), @@ -509,7 +509,7 @@ def flip_and_stream( target_qubits = self.lattice.velocity_dir_index() circuit.compose( - MCMT( + MCMTGate( XGate(), len(control_qubits), len(target_qubits), diff --git a/qlbm/components/collisionless/primitives.py b/qlbm/components/collisionless/primitives.py index 81f1b61..ddc708c 100644 --- a/qlbm/components/collisionless/primitives.py +++ b/qlbm/components/collisionless/primitives.py @@ -6,7 +6,7 @@ from typing import List from qiskit import ClassicalRegister, QuantumCircuit -from qiskit.circuit.library import QFT +from qiskit.synthesis import synth_qft_full as QFT from typing_extensions import override from qlbm.components.base import LBMPrimitive @@ -49,7 +49,7 @@ class GridMeasurement(LBMPrimitive): } }, "geometry": [ - { + { "shape": "cuboid", "x": [5, 6], "y": [1, 2], diff --git a/qlbm/components/collisionless/specular_reflection.py b/qlbm/components/collisionless/specular_reflection.py index d652baa..ab4b978 100644 --- a/qlbm/components/collisionless/specular_reflection.py +++ b/qlbm/components/collisionless/specular_reflection.py @@ -5,7 +5,7 @@ from typing import List from qiskit import QuantumCircuit -from qiskit.circuit.library import MCMT, XGate +from qiskit.circuit.library import MCMTGate, XGate from typing_extensions import override from qlbm.components.base import CQLBMOperator, LBMPrimitive @@ -307,7 +307,7 @@ def reflect_wall( target_qubits = self.lattice.ancillae_obstacle_index(wall.dim) circuit.compose( - MCMT( + MCMTGate( XGate(), len(control_qubits), len(target_qubits), @@ -388,7 +388,7 @@ def reset_edge_state( circuit.x(self.lattice.velocity_dir_index(dim)) circuit.compose( - MCMT( + MCMTGate( XGate(), len(control_qubits), len(target_qubits), @@ -463,7 +463,7 @@ def reset_point_state( ) circuit.compose( - MCMT( + MCMTGate( XGate(), len(control_qubits), len(target_qubits), diff --git a/qlbm/components/collisionless/streaming.py b/qlbm/components/collisionless/streaming.py index 8406d4e..8e58210 100644 --- a/qlbm/components/collisionless/streaming.py +++ b/qlbm/components/collisionless/streaming.py @@ -7,7 +7,8 @@ import numpy as np from qiskit import QuantumCircuit -from qiskit.circuit.library import MCMT, QFT, XGate +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 CQLBMOperator, LBMPrimitive @@ -93,7 +94,7 @@ def create_circuit(self) -> QuantumCircuit: circuit.x(velocity_qubit_indices_to_invert) circuit.compose( - MCMT(XGate(), num_velocity_qubits, 1), + MCXGate(num_velocity_qubits), qubits=self.lattice.velocity_index(self.dim) + self.lattice.ancillae_velocity_index(self.dim), inplace=True, diff --git a/qlbm/components/common/primitives.py b/qlbm/components/common/primitives.py index cd37d92..456e2a7 100644 --- a/qlbm/components/common/primitives.py +++ b/qlbm/components/common/primitives.py @@ -5,7 +5,7 @@ from typing import List, Tuple from qiskit import QuantumCircuit -from qiskit.circuit.library import MCMT, XGate +from qiskit.circuit.library import MCMTGate, XGate from typing_extensions import override from qlbm.components.base import LBMPrimitive @@ -93,7 +93,9 @@ def create_circuit(self) -> QuantumCircuit: circuit.cx(self.target_qubits[1], self.target_qubits[0]) circuit.compose( - MCMT(XGate(), len(self.control_qubits) + 1, len(self.target_qubits) - 1), + MCMTGate( + XGate(), len(self.control_qubits) + 1, len(self.target_qubits) - 1 + ), qubits=self.control_qubits + list(self.target_qubits), inplace=True, ) diff --git a/qlbm/components/spacetime/collision.py b/qlbm/components/spacetime/collision.py index 5b768be..135ccd3 100644 --- a/qlbm/components/spacetime/collision.py +++ b/qlbm/components/spacetime/collision.py @@ -6,7 +6,7 @@ from qiskit import QuantumCircuit from qiskit.circuit import Gate -from qiskit.circuit.library import MCMT, RYGate +from qiskit.circuit.library import MCMTGate, RYGate from typing_extensions import override from qlbm.components.base import SpaceTimeOperator @@ -87,7 +87,7 @@ def create_circuit(self) -> QuantumCircuit: collision_circuit = self.local_collision_circuit(reset_state=False) collision_circuit.compose( - MCMT( + MCMTGate( self.gate_to_apply, self.lattice.properties.get_num_velocities_per_point() - 1, 1, diff --git a/qlbm/components/spacetime/initial/pointwise.py b/qlbm/components/spacetime/initial/pointwise.py index 923b8c4..7f8c421 100644 --- a/qlbm/components/spacetime/initial/pointwise.py +++ b/qlbm/components/spacetime/initial/pointwise.py @@ -4,7 +4,7 @@ from typing import List, Tuple from qiskit import QuantumCircuit -from qiskit.circuit.library import MCMT, XGate +from qiskit.circuit.library import MCMTGate, XGate from typing_extensions import override from qlbm.components.base import LBMPrimitive @@ -94,7 +94,7 @@ def create_circuit(self) -> QuantumCircuit: continue circuit.compose(self.set_grid_value(grid_point_data[0]), inplace=True) circuit.compose( - MCMT( + MCMTGate( XGate(), num_ctrl_qubits=self.lattice.properties.get_num_grid_qubits(), num_target_qubits=sum( @@ -204,7 +204,7 @@ def set_neighbor_velocity( self.set_grid_value(absolute_neighbor_coordinates), inplace=True ) circuit.compose( - MCMT( + MCMTGate( XGate(), num_ctrl_qubits=self.lattice.properties.get_num_grid_qubits(), num_target_qubits=sum( diff --git a/qlbm/components/spacetime/initial/volumetric.py b/qlbm/components/spacetime/initial/volumetric.py index f24b52b..9d28bc8 100644 --- a/qlbm/components/spacetime/initial/volumetric.py +++ b/qlbm/components/spacetime/initial/volumetric.py @@ -5,7 +5,7 @@ from typing import List, Tuple, cast from qiskit import QuantumCircuit -from qiskit.circuit.library import MCMT, XGate +from qiskit.circuit.library import MCMTGate, XGate from typing_extensions import override from qlbm.components.base import LBMPrimitive @@ -109,7 +109,7 @@ def create_circuit(self) -> QuantumCircuit: [any(pvb[1]) for pvb in periodic_volume_bounds] ): circuit.compose( - MCMT( + MCMTGate( XGate(), num_ctrl_qubits=len(control_qubit_sequence), num_target_qubits=sum( diff --git a/qlbm/components/spacetime/measurement.py b/qlbm/components/spacetime/measurement.py index cffbbe9..a6da9a1 100644 --- a/qlbm/components/spacetime/measurement.py +++ b/qlbm/components/spacetime/measurement.py @@ -4,7 +4,7 @@ from typing import Tuple from qiskit import ClassicalRegister -from qiskit.circuit.library import MCMT, XGate +from qiskit.circuit.library import MCMTGate, XGate from typing_extensions import override from qlbm.components.base import SpaceTimeOperator @@ -132,7 +132,7 @@ def create_circuit(self): target_qubits = self.lattice.ancilla_mass_index() circuit.compose( - MCMT( + MCMTGate( XGate(), len(control_qubits), len(target_qubits), From c451b98edfc72c1df8707143a02fe07f6afc6043 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Tue, 24 Jun 2025 17:20:38 +0200 Subject: [PATCH 04/92] Update tests to new qiskit API version --- test/unit/incrementer_test.py | 4 ++-- test/unit/spacetime/circuits/mcswap_test.py | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/test/unit/incrementer_test.py b/test/unit/incrementer_test.py index 03c7980..5d012ff 100644 --- a/test/unit/incrementer_test.py +++ b/test/unit/incrementer_test.py @@ -17,10 +17,10 @@ def lattice_symmetric_small_2d(): def test_3d_incrementer_size(lattice_asymmetric_medium_3d): inc: ControlledIncrementer = ControlledIncrementer(lattice_asymmetric_medium_3d) - assert inc.circuit.size() == 32 + assert inc.circuit.size() == 78 def test_2d_incrementer_size(lattice_symmetric_small_2d): inc: ControlledIncrementer = ControlledIncrementer(lattice_symmetric_small_2d) - assert inc.circuit.size() == 24 + assert inc.circuit.size() == 68 diff --git a/test/unit/spacetime/circuits/mcswap_test.py b/test/unit/spacetime/circuits/mcswap_test.py index bed58b7..2b86c8d 100644 --- a/test/unit/spacetime/circuits/mcswap_test.py +++ b/test/unit/spacetime/circuits/mcswap_test.py @@ -1,4 +1,4 @@ -from qiskit.circuit.library import MCMT, XGate +from qiskit.circuit.library import MCXGate from qlbm.components.common.primitives import MCSwap from qlbm.lattice.lattices.spacetime_lattice import SpaceTimeLattice @@ -21,13 +21,13 @@ def test_mcswap_13ctrl(): expected_circuit.cx(6, 5) expected_circuit.compose( - MCMT(XGate(), 4, 1), + MCXGate(4), qubits=[0, 2, 3, 5, 6], inplace=True, ) expected_circuit.cx(6, 5) - assert mcswap.circuit == expected_circuit + assert mcswap.circuit.decompose() == expected_circuit.decompose() def test_mcswap_grid_ctrl(): @@ -51,10 +51,10 @@ def test_mcswap_grid_ctrl(): expected_circuit.cx(11, 9) expected_circuit.compose( - MCMT(XGate(), 9, 1), + MCXGate(9), qubits=list(range(8)) + [9, 11], inplace=True, ) expected_circuit.cx(11, 9) - assert mcswap.circuit == expected_circuit + assert mcswap.circuit.decompose() == expected_circuit.decompose() From cf93599db3edc549a8c1cec78ec58b33f615064b Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Tue, 24 Jun 2025 17:31:16 +0200 Subject: [PATCH 05/92] Update dependency requirement to qiskit>=2.0 --- pyproject.toml | 2 +- qlbm/lattice/geometry/shapes/circle.py | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4aba192..7a6a5a5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ dependencies = [ "pytket>=1.29.2", "pytket-qiskit", "pytket-qulacs>=0.33", - "qiskit>=1.2, <1.3", + "qiskit>=2.0", "qiskit_qasm3_import>=0.4.2", "qiskit-qulacs>=0.1.0", "tqdm>=4.66", diff --git a/qlbm/lattice/geometry/shapes/circle.py b/qlbm/lattice/geometry/shapes/circle.py index c173476..8979b34 100644 --- a/qlbm/lattice/geometry/shapes/circle.py +++ b/qlbm/lattice/geometry/shapes/circle.py @@ -433,9 +433,6 @@ def get_spacetime_reflection_data_d2q4( expanded_diag_gridpoints: List[Tuple[int, ...]] = ( Circle.expand_diagonal_segments([diag_segment]) ) - - if (4, 1) in expanded_diag_gridpoints: - print("ok!") for reflection_dim in [0, 1]: other_dim = 1 - reflection_dim streaming_line_velocities = [0, 2] if reflection_dim == 0 else [1, 3] From 11eee6e97a5e533abe8f3d2cd570925126ac61be Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Thu, 26 Jun 2025 16:24:05 +0200 Subject: [PATCH 06/92] Make the decomposition of MCMX gates in PW ST initial conditions explicit --- .../components/spacetime/initial/pointwise.py | 70 ++++++++----------- 1 file changed, 29 insertions(+), 41 deletions(-) diff --git a/qlbm/components/spacetime/initial/pointwise.py b/qlbm/components/spacetime/initial/pointwise.py index 7f8c421..d252189 100644 --- a/qlbm/components/spacetime/initial/pointwise.py +++ b/qlbm/components/spacetime/initial/pointwise.py @@ -4,7 +4,7 @@ from typing import List, Tuple from qiskit import QuantumCircuit -from qiskit.circuit.library import MCMTGate, XGate +from qiskit.circuit.library import MCMTGate, MCXGate, XGate from typing_extensions import override from qlbm.components.base import LBMPrimitive @@ -93,26 +93,20 @@ def create_circuit(self) -> QuantumCircuit: if self.lattice.is_inside_an_obstacle(grid_point_data[0]): continue circuit.compose(self.set_grid_value(grid_point_data[0]), inplace=True) - circuit.compose( - MCMTGate( - XGate(), - num_ctrl_qubits=self.lattice.properties.get_num_grid_qubits(), - num_target_qubits=sum( - grid_point_data[1] - ), # The sum is equal to the number of velocities set to true - ), - qubits=list( - self.lattice.grid_index() - + flatten( - [ - self.lattice.velocity_index(0, c) - for c, is_velocity_enabled in enumerate(grid_point_data[1]) - if is_velocity_enabled - ] - ) - ), - inplace=True, - ) + for target_qubit in flatten( + [ + self.lattice.velocity_index(0, c) + for c, is_velocity_enabled in enumerate(grid_point_data[1]) + if is_velocity_enabled + ] + ): + circuit.compose( + MCXGate( + num_ctrl_qubits=self.lattice.properties.get_num_grid_qubits(), + ), + qubits=list(self.lattice.grid_index() + [target_qubit]), + inplace=True, + ) circuit.compose(self.set_grid_value(grid_point_data[0]), inplace=True) # Set the velocity state for neighbors in increasing velocity @@ -203,26 +197,20 @@ def set_neighbor_velocity( circuit.compose( self.set_grid_value(absolute_neighbor_coordinates), inplace=True ) - circuit.compose( - MCMTGate( - XGate(), - num_ctrl_qubits=self.lattice.properties.get_num_grid_qubits(), - num_target_qubits=sum( - velocity_values - ), # The sum is equal to the number of velocities set to true - ), - inplace=True, - qubits=list( - self.lattice.grid_index() - + flatten( - [ - self.lattice.velocity_index(neighbor.neighbor_index, c) - for c, is_velocity_enabled in enumerate(velocity_values) - if is_velocity_enabled - ] - ) - ), - ) + for target_qubit in flatten( + [ + self.lattice.velocity_index(neighbor.neighbor_index, c) + for c, is_velocity_enabled in enumerate(velocity_values) + if is_velocity_enabled + ] + ): + circuit.compose( + MCXGate( + num_ctrl_qubits=self.lattice.properties.get_num_grid_qubits(), + ), + qubits=list(self.lattice.grid_index() + [target_qubit]), + inplace=True, + ) circuit.compose( self.set_grid_value(absolute_neighbor_coordinates), inplace=True ) From bd44cd5a272a2aaf9155ce63d698c23cd0f3984f Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Thu, 26 Jun 2025 16:39:45 +0200 Subject: [PATCH 07/92] Remove unused imports --- qlbm/components/spacetime/initial/pointwise.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qlbm/components/spacetime/initial/pointwise.py b/qlbm/components/spacetime/initial/pointwise.py index d252189..63d604a 100644 --- a/qlbm/components/spacetime/initial/pointwise.py +++ b/qlbm/components/spacetime/initial/pointwise.py @@ -4,7 +4,7 @@ from typing import List, Tuple from qiskit import QuantumCircuit -from qiskit.circuit.library import MCMTGate, MCXGate, XGate +from qiskit.circuit.library import MCXGate from typing_extensions import override from qlbm.components.base import LBMPrimitive From 2b5fadd755e062e699f70084fcd609a7709511cb Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Thu, 26 Jun 2025 16:39:59 +0200 Subject: [PATCH 08/92] Ignore qiskit.syntehsis module in mypy --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 7a6a5a5..2d617b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -104,6 +104,7 @@ module = [ "qiskit.qasm3", "qiskit.quantum_info", "qiskit.result", + "qiskit.synthesis", "qiskit.transpiler.preset_passmanagers", "qiskit_aer", "qiskit_aer.backends.aerbackend", From a7ac603403c8be386ff380bc5e48e7a3335a02d8 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Fri, 27 Jun 2025 17:57:45 +0200 Subject: [PATCH 09/92] Refactor STQBM collision class into module, add equivalence class abstraction --- .../spacetime/collision/__init__.py | 5 + .../spacetime/collision/eqc_collision.py | 162 ++++++++++++++++++ .../collision/eqc_discretizations.py | 107 ++++++++++++ .../spacetime/collision/eqc_permutation.py | 0 4 files changed, 274 insertions(+) create mode 100644 qlbm/components/spacetime/collision/__init__.py create mode 100644 qlbm/components/spacetime/collision/eqc_collision.py create mode 100644 qlbm/components/spacetime/collision/eqc_discretizations.py create mode 100644 qlbm/components/spacetime/collision/eqc_permutation.py diff --git a/qlbm/components/spacetime/collision/__init__.py b/qlbm/components/spacetime/collision/__init__.py new file mode 100644 index 0000000..fedbb9b --- /dev/null +++ b/qlbm/components/spacetime/collision/__init__.py @@ -0,0 +1,5 @@ +from .eqc_collision import SpaceTimeCollisionOperator +from .eqc_discretizations import ( + EquivalenceClass, + EquivalenceClassGenerator, +) diff --git a/qlbm/components/spacetime/collision/eqc_collision.py b/qlbm/components/spacetime/collision/eqc_collision.py new file mode 100644 index 0000000..135ccd3 --- /dev/null +++ b/qlbm/components/spacetime/collision/eqc_collision.py @@ -0,0 +1,162 @@ +"""Collision operators for the :class:`.SpaceTimeQLBM` algorithm :cite:`spacetime`.""" + +from logging import Logger, getLogger +from math import pi +from time import perf_counter_ns + +from qiskit import QuantumCircuit +from qiskit.circuit import Gate +from qiskit.circuit.library import MCMTGate, RYGate +from typing_extensions import override + +from qlbm.components.base import SpaceTimeOperator +from qlbm.lattice.lattices.spacetime_lattice import SpaceTimeLattice + + +class SpaceTimeCollisionOperator(SpaceTimeOperator): + r"""An operator that performs collision part of the :class:`.SpaceTimeQLBM` algorithm. + + Collision is a local operation that is performed simultaneously on all velocity qubits corresponding to a grid location. + In practice, this means the same circuit is repeated across all "local" qubit register chunks. + Collision can be understood as follows: + + #. For each group of qubits, the states encoding velocities belonging to a particular equivalence class are first isolated with a series of :math:`X` and :math:`CX` gates. This leaves qubits not affected by the rotation in :math:`\ket{1}^{\otimes n_v-1}` state. + #. A rotation gate is applied to the qubit(s) relevant to the equivalence class shift, controlled on the qubits set in the previous step. + #. The operation performed in Step 1 is undone. + + The register setup of the :class:`.SpaceTimeLattice` is such that following each + time step, an additional "layer" neighboring velocity qubits can be discarded, + since the information they encode can never reach the relative origin in the remaining number of time steps. + As such, the complexity of the collision operator decreases with the number of steps (still) to be simulated. + For an in-depth mathematical explanation of the procedure, consult pages 11-15 of :cite:t:`spacetime`. + + + ========================= ====================================================================== + Attribute Summary + ========================= ====================================================================== + :attr:`lattice` The :class:`.SpaceTimeLattice` based on which the properties of the operator are inferred. + :attr:`timestep` The time step for which to perform streaming. + :attr:`gate_to_apply` The gate to apply to the velocities matching equivalence classes. Defaults to :math:`R_y(\frac{\pi}{2})`. + :attr:`logger` The performance logger, by default ``getLogger("qlbm")``. + ========================= ====================================================================== + + + Example usage: + + .. plot:: + :include-source: + + from qlbm.components.spacetime import SpaceTimeCollisionOperator + from qlbm.lattice import SpaceTimeLattice + + # Build an example lattice + lattice = SpaceTimeLattice( + num_timesteps=1, + lattice_data={ + "lattice": {"dim": {"x": 4, "y": 8}, "velocities": {"x": 2, "y": 2}}, + "geometry": [], + }, + ) + + # Draw the collision operator for 1 time step + SpaceTimeCollisionOperator(lattice=lattice, timestep=1).draw("mpl") + """ + + def __init__( + self, + lattice: SpaceTimeLattice, + timestep: int, + gate_to_apply: Gate = RYGate(pi / 2), + logger: Logger = getLogger("qlbm"), + ) -> None: + super().__init__(lattice, logger) + self.lattice = lattice + self.timestep = timestep + self.gate_to_apply = gate_to_apply + + 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 = self.lattice.circuit.copy() + + collision_circuit = self.local_collision_circuit(reset_state=False) + collision_circuit.compose( + MCMTGate( + self.gate_to_apply, + self.lattice.properties.get_num_velocities_per_point() - 1, + 1, + ), + qubits=list( + range(1, self.lattice.properties.get_num_velocities_per_point()) + ) + + [0], + inplace=True, + ) + + # Create the local circuit once + collision_circuit.compose( + self.local_collision_circuit(reset_state=True), inplace=True + ) + + # Append the collision circuit at each step + for velocity_qubit_indices in range( + self.lattice.properties.get_num_grid_qubits(), + self.lattice.properties.get_num_grid_qubits() + + self.lattice.properties.get_num_velocity_qubits(self.timestep), + self.lattice.properties.get_num_velocities_per_point(), + ): + circuit.compose( + collision_circuit, + inplace=True, + qubits=range(velocity_qubit_indices, velocity_qubit_indices + 4), + ) + return circuit + + def local_collision_circuit(self, reset_state: bool) -> QuantumCircuit: + """ + Sets the state for the collision circuit one local gridpoint and its corresponding velocity qubits. + + Parameters + ---------- + reset_state : bool + Whether the circuit sets or re-sets the state past the application of the rotation operator. + + Returns + ------- + QuantumCircuit + The collision (re-)set circuit for a single gridpoint. + """ + circuit = QuantumCircuit(self.lattice.properties.get_num_velocities_per_point()) + + # circuit.cx(1, 2) + # circuit.cx(0, 1) + # circuit.cx(0, 3) + + # return circuit if not reset_state else circuit.inverse() + + if not reset_state: + circuit.cx(control_qubit=0, target_qubit=2) + circuit.x(0) + circuit.cx(control_qubit=1, target_qubit=3) + circuit.cx(control_qubit=0, target_qubit=1) + circuit.x(list(range(circuit.num_qubits))) + # Same circuit, but mirrored + else: + circuit.x(list(range(circuit.num_qubits))) + circuit.cx(control_qubit=0, target_qubit=1) + circuit.cx(control_qubit=1, target_qubit=3) + circuit.x(0) + circuit.cx(control_qubit=0, target_qubit=2) + + return circuit + + @override + def __str__(self) -> str: + # TODO: Implement + return "Space Time Collision Operator" diff --git a/qlbm/components/spacetime/collision/eqc_discretizations.py b/qlbm/components/spacetime/collision/eqc_discretizations.py new file mode 100644 index 0000000..c6c1f3f --- /dev/null +++ b/qlbm/components/spacetime/collision/eqc_discretizations.py @@ -0,0 +1,107 @@ +from itertools import product +from typing import Dict, List, Set, Tuple + +import numpy as np + +from qlbm.lattice.spacetime.properties_base import ( + LatticeDiscretization, + LatticeDiscretizationProperties, +) +from qlbm.tools.exceptions import LatticeException + + +class EquivalenceClass: + discretization: LatticeDiscretization + velocity_configurations: Set[Tuple[bool, ...]] + mass: int + momentum: np.array + + def __init__( + self, + discretization: LatticeDiscretization, + velocity_configurations: Set[Tuple[bool, ...]], + ): + if len(velocity_configurations) < 2: + raise LatticeException( + f"Equivalence class must have at least two configurations. Provided velocity profiles are {velocity_configurations}." + ) + num_velocities = LatticeDiscretizationProperties.get_num_velocities( + discretization + ) + if not all(len(v) == num_velocities for v in velocity_configurations): + raise LatticeException( + f"All velocity configurations must have length {num_velocities}. Provided configurations are {velocity_configurations}." + ) + + self.discretization = discretization + self.velocity_configuration = velocity_configurations + self.mass = sum(list(velocity_configurations)[0]) + + velocity_vectors = LatticeDiscretizationProperties.get_velocity_vectors( + discretization + ) + if not all( + (sum(velocity_cfg) == self.mass) for velocity_cfg in velocity_configurations + ): + raise LatticeException("Velocity configurations have different masses.") + self.momentum = sum( + [ + velocity_vectors[i] * list(velocity_configurations)[0][i] + for i in range(len(list(velocity_configurations)[0])) + ] + ) + if not all( + ( + np.array_equal( + self.momentum, + np.sum( + [ + velocity_vectors[i] * v[i] + for i in range(len(velocity_vectors)) + ], + axis=0, + ), + ) + ) + for v in velocity_configurations + ): + raise LatticeException("Velocity configurations have different momenta.") + + def size(self) -> int: + return len(self.velocity_configuration) + + def id(self) -> Tuple[int, np.array]: + return (self.mass, self.momentum) + + +class EquivalenceClassGenerator: + discretization: LatticeDiscretization + + def __init__(self, discretization): + self.discretization = discretization + + def generate_equivalence_classes(self) -> Set[EquivalenceClass]: + equivalence_classes: Dict = {} + for state in product( + [0, 1], + repeat=LatticeDiscretizationProperties.get_num_velocities( + self.discretization + ), + ): + velocity_vectors = LatticeDiscretizationProperties.get_velocity_vectors( + self.discretization + ) + state = np.array(state) # type: ignore + mass = np.sum(state) + momentum = np.sum(state[:, None] * velocity_vectors, axis=0) # type: ignore + + key = (mass, tuple(momentum)) + if key not in equivalence_classes: + equivalence_classes[key] = [] + equivalence_classes[key].append(state) + + return { + EquivalenceClass(self.discretization, v) + for _, v in equivalence_classes.items() + if len(v) > 1 + } diff --git a/qlbm/components/spacetime/collision/eqc_permutation.py b/qlbm/components/spacetime/collision/eqc_permutation.py new file mode 100644 index 0000000..e69de29 From 04bd4cacd6eed8d5242ff710820494e8c46189f8 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Fri, 27 Jun 2025 17:58:18 +0200 Subject: [PATCH 10/92] Remove old STQBM collision operator --- qlbm/components/spacetime/collision.py | 162 ------------------------- 1 file changed, 162 deletions(-) delete mode 100644 qlbm/components/spacetime/collision.py diff --git a/qlbm/components/spacetime/collision.py b/qlbm/components/spacetime/collision.py deleted file mode 100644 index 135ccd3..0000000 --- a/qlbm/components/spacetime/collision.py +++ /dev/null @@ -1,162 +0,0 @@ -"""Collision operators for the :class:`.SpaceTimeQLBM` algorithm :cite:`spacetime`.""" - -from logging import Logger, getLogger -from math import pi -from time import perf_counter_ns - -from qiskit import QuantumCircuit -from qiskit.circuit import Gate -from qiskit.circuit.library import MCMTGate, RYGate -from typing_extensions import override - -from qlbm.components.base import SpaceTimeOperator -from qlbm.lattice.lattices.spacetime_lattice import SpaceTimeLattice - - -class SpaceTimeCollisionOperator(SpaceTimeOperator): - r"""An operator that performs collision part of the :class:`.SpaceTimeQLBM` algorithm. - - Collision is a local operation that is performed simultaneously on all velocity qubits corresponding to a grid location. - In practice, this means the same circuit is repeated across all "local" qubit register chunks. - Collision can be understood as follows: - - #. For each group of qubits, the states encoding velocities belonging to a particular equivalence class are first isolated with a series of :math:`X` and :math:`CX` gates. This leaves qubits not affected by the rotation in :math:`\ket{1}^{\otimes n_v-1}` state. - #. A rotation gate is applied to the qubit(s) relevant to the equivalence class shift, controlled on the qubits set in the previous step. - #. The operation performed in Step 1 is undone. - - The register setup of the :class:`.SpaceTimeLattice` is such that following each - time step, an additional "layer" neighboring velocity qubits can be discarded, - since the information they encode can never reach the relative origin in the remaining number of time steps. - As such, the complexity of the collision operator decreases with the number of steps (still) to be simulated. - For an in-depth mathematical explanation of the procedure, consult pages 11-15 of :cite:t:`spacetime`. - - - ========================= ====================================================================== - Attribute Summary - ========================= ====================================================================== - :attr:`lattice` The :class:`.SpaceTimeLattice` based on which the properties of the operator are inferred. - :attr:`timestep` The time step for which to perform streaming. - :attr:`gate_to_apply` The gate to apply to the velocities matching equivalence classes. Defaults to :math:`R_y(\frac{\pi}{2})`. - :attr:`logger` The performance logger, by default ``getLogger("qlbm")``. - ========================= ====================================================================== - - - Example usage: - - .. plot:: - :include-source: - - from qlbm.components.spacetime import SpaceTimeCollisionOperator - from qlbm.lattice import SpaceTimeLattice - - # Build an example lattice - lattice = SpaceTimeLattice( - num_timesteps=1, - lattice_data={ - "lattice": {"dim": {"x": 4, "y": 8}, "velocities": {"x": 2, "y": 2}}, - "geometry": [], - }, - ) - - # Draw the collision operator for 1 time step - SpaceTimeCollisionOperator(lattice=lattice, timestep=1).draw("mpl") - """ - - def __init__( - self, - lattice: SpaceTimeLattice, - timestep: int, - gate_to_apply: Gate = RYGate(pi / 2), - logger: Logger = getLogger("qlbm"), - ) -> None: - super().__init__(lattice, logger) - self.lattice = lattice - self.timestep = timestep - self.gate_to_apply = gate_to_apply - - 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 = self.lattice.circuit.copy() - - collision_circuit = self.local_collision_circuit(reset_state=False) - collision_circuit.compose( - MCMTGate( - self.gate_to_apply, - self.lattice.properties.get_num_velocities_per_point() - 1, - 1, - ), - qubits=list( - range(1, self.lattice.properties.get_num_velocities_per_point()) - ) - + [0], - inplace=True, - ) - - # Create the local circuit once - collision_circuit.compose( - self.local_collision_circuit(reset_state=True), inplace=True - ) - - # Append the collision circuit at each step - for velocity_qubit_indices in range( - self.lattice.properties.get_num_grid_qubits(), - self.lattice.properties.get_num_grid_qubits() - + self.lattice.properties.get_num_velocity_qubits(self.timestep), - self.lattice.properties.get_num_velocities_per_point(), - ): - circuit.compose( - collision_circuit, - inplace=True, - qubits=range(velocity_qubit_indices, velocity_qubit_indices + 4), - ) - return circuit - - def local_collision_circuit(self, reset_state: bool) -> QuantumCircuit: - """ - Sets the state for the collision circuit one local gridpoint and its corresponding velocity qubits. - - Parameters - ---------- - reset_state : bool - Whether the circuit sets or re-sets the state past the application of the rotation operator. - - Returns - ------- - QuantumCircuit - The collision (re-)set circuit for a single gridpoint. - """ - circuit = QuantumCircuit(self.lattice.properties.get_num_velocities_per_point()) - - # circuit.cx(1, 2) - # circuit.cx(0, 1) - # circuit.cx(0, 3) - - # return circuit if not reset_state else circuit.inverse() - - if not reset_state: - circuit.cx(control_qubit=0, target_qubit=2) - circuit.x(0) - circuit.cx(control_qubit=1, target_qubit=3) - circuit.cx(control_qubit=0, target_qubit=1) - circuit.x(list(range(circuit.num_qubits))) - # Same circuit, but mirrored - else: - circuit.x(list(range(circuit.num_qubits))) - circuit.cx(control_qubit=0, target_qubit=1) - circuit.cx(control_qubit=1, target_qubit=3) - circuit.x(0) - circuit.cx(control_qubit=0, target_qubit=2) - - return circuit - - @override - def __str__(self) -> str: - # TODO: Implement - return "Space Time Collision Operator" From e7dd2802fa5f0fb107f3adba998dddd2dc420e70 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Fri, 27 Jun 2025 17:58:34 +0200 Subject: [PATCH 11/92] Add basic equivalence class tests --- test/unit/spacetime/collision/__init__.py | 1 + .../eqc_velocity_discretization_test.py | 48 +++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 test/unit/spacetime/collision/__init__.py create mode 100644 test/unit/spacetime/collision/eqc_velocity_discretization_test.py diff --git a/test/unit/spacetime/collision/__init__.py b/test/unit/spacetime/collision/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/test/unit/spacetime/collision/__init__.py @@ -0,0 +1 @@ + diff --git a/test/unit/spacetime/collision/eqc_velocity_discretization_test.py b/test/unit/spacetime/collision/eqc_velocity_discretization_test.py new file mode 100644 index 0000000..f5c331d --- /dev/null +++ b/test/unit/spacetime/collision/eqc_velocity_discretization_test.py @@ -0,0 +1,48 @@ +import pytest + +from qlbm.components.spacetime.collision import ( + EquivalenceClass, + EquivalenceClassGenerator, +) +from qlbm.lattice.spacetime.properties_base import LatticeDiscretization +from qlbm.tools.exceptions import LatticeException + + +def test_equivalence_class_bad_number_of_velocity_configurations(): + velocity_profile = {(True, True, True, True)} + with pytest.raises(LatticeException) as excinfo: + EquivalenceClass(LatticeDiscretization.D2Q4, velocity_profile) + assert ( + f"Equivalence class must have at least two configurations. Provided velocity profiles are {velocity_profile}." + == str(excinfo.value) + ) + + +def test_equivalence_class_bad_number_velocity_configurations(): + velocity_profile = {(True, True, True), (True, True, True, False)} + with pytest.raises(LatticeException) as excinfo: + EquivalenceClass(LatticeDiscretization.D2Q4, velocity_profile) + assert ( + f"All velocity configurations must have length 4. Provided configurations are {velocity_profile}." + == str(excinfo.value) + ) + + +def test_equivalence_class_bad_mass(): + velocity_profile = {(True, False, True, False), (True, True, True, False)} + with pytest.raises(LatticeException) as excinfo: + EquivalenceClass(LatticeDiscretization.D2Q4, velocity_profile) + assert "Velocity configurations have different masses." == str(excinfo.value) + + +def test_equivalence_class_bad_momentum(): + velocity_profile = {(True, False, True, False), (True, True, False, False)} + with pytest.raises(LatticeException) as excinfo: + EquivalenceClass(LatticeDiscretization.D2Q4, velocity_profile) + assert "Velocity configurations have different momenta." == str(excinfo.value) + +def test_equivalence_class_ok(): + velocity_profile = {(True, False, True, False), (False, True, False, True)} + eqc = EquivalenceClass(LatticeDiscretization.D2Q4, velocity_profile) + assert eqc.mass == 2 + assert eqc.momentum.tolist() == [0, 0] From 3193a459089d67fc4075f9f29b0cee5c65f20680 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 30 Jun 2025 11:43:01 +0200 Subject: [PATCH 12/92] Add documentation to equivalence class module --- .../spacetime/collision/__init__.py | 8 ++ .../collision/eqc_discretizations.py | 98 +++++++++++++++++-- .../spacetime/collision/eqc_permutation.py | 1 + qlbm/lattice/spacetime/properties_base.py | 74 ++++++++++++++ 4 files changed, 175 insertions(+), 6 deletions(-) diff --git a/qlbm/components/spacetime/collision/__init__.py b/qlbm/components/spacetime/collision/__init__.py index fedbb9b..553e792 100644 --- a/qlbm/components/spacetime/collision/__init__.py +++ b/qlbm/components/spacetime/collision/__init__.py @@ -1,5 +1,13 @@ +"""Classes implementing the logic of the collision operator in the Space-Time Data encoding.""" + from .eqc_collision import SpaceTimeCollisionOperator from .eqc_discretizations import ( EquivalenceClass, EquivalenceClassGenerator, ) + +__all__ = [ + "SpaceTimeCollisionOperator", + "EquivalenceClass", + "EquivalenceClassGenerator", +] diff --git a/qlbm/components/spacetime/collision/eqc_discretizations.py b/qlbm/components/spacetime/collision/eqc_discretizations.py index c6c1f3f..1240e8d 100644 --- a/qlbm/components/spacetime/collision/eqc_discretizations.py +++ b/qlbm/components/spacetime/collision/eqc_discretizations.py @@ -1,5 +1,7 @@ +"""Equivalence class utility functions. For a discussion of equivalence classes, we refer the reader to Section 4 of :cite:`spacetime2`.""" + from itertools import product -from typing import Dict, List, Set, Tuple +from typing import Dict, Set, Tuple, override import numpy as np @@ -11,10 +13,39 @@ class EquivalenceClass: + """ + Class representing LGA equivalence classes. + + In ``qlbm``, an equivalence class is a set of velocity configurations that share the same mass and momentum. + For a more in depth explanation, consult Section 4 of :cite:p:`spacetime2`. + + .. list-table:: Constructor Attributes + :widths: 25 50 + :header-rows: 1 + + * - Attribute + - Description + * - :attr:`discretization` + - The :class:`.LatticeDiscretization` that the equivalence class belongs to. + * - :attr:`velocity_configurations` + - The :class:`Set[Tuple[bool, ...]]` that contains the velocity configurations of the equivalence class. Configurations are stored as ``q``-tuples where an entry is ``True`` if the velocity channel is occupied and ``False`` otherwise. + + .. list-table:: Class Attributes + :widths: 25 50 + :header-rows: 1 + + * - Attribute + - Description + * - :attr:`mass` + - The total mass of the equivalence class, which is the sum of all occupied velocity channels. + * - :attr:`momentum` + - The total momentum of the equivalence class, which is the vector sum of all occupied velocity channels multiplid by their :class:`.LatticeDiscretizationProperties` velocity contribution. + """ + discretization: LatticeDiscretization velocity_configurations: Set[Tuple[bool, ...]] mass: int - momentum: np.array + momentum: np.typing.NDArray def __init__( self, @@ -34,7 +65,7 @@ def __init__( ) self.discretization = discretization - self.velocity_configuration = velocity_configurations + self.velocity_configurations = velocity_configurations self.mass = sum(list(velocity_configurations)[0]) velocity_vectors = LatticeDiscretizationProperties.get_velocity_vectors( @@ -68,19 +99,74 @@ def __init__( raise LatticeException("Velocity configurations have different momenta.") def size(self) -> int: - return len(self.velocity_configuration) + """ + The number of velocity configurations in the equivalence class. + + Returns + ------- + int + The number of velocity configurations in the equivalence class. + """ + return len(self.velocity_configurations) + + def id(self) -> Tuple[int, np.typing.NDArray]: + """ + The identifier of the equivalence class. - def id(self) -> Tuple[int, np.array]: + For a given discretization, an equivalence class can be uniquely identified by the common mass and momentum of its velocity configurations. + + Returns + ------- + Tuple[int, np.typing.NDArray] + The mass and momentum of the equivalence class. + """ return (self.mass, self.momentum) + @override + def __eq__(self, value): + if not isinstance(value, EquivalenceClass): + return False + return ( + self.discretization == value.discretization + and self.velocity_configurations == value.velocity_configurations + and self.mass == value.mass + and np.array_equal(self.momentum, value.momentum) + ) + + @override + def __hash__(self): + return hash((self.discretization, tuple(self.velocity_configurations))) + class EquivalenceClassGenerator: + """ + A class that generates equivalence classes for a given lattice discretization. + + .. list-table:: Constructor Attributes + :widths: 25 50 + :header-rows: 1 + + * - Attribute + - Description + * - :attr:`discretization` + - The :class:`.LatticeDiscretization` that the equivalence class belongs to. + + """ + discretization: LatticeDiscretization def __init__(self, discretization): self.discretization = discretization def generate_equivalence_classes(self) -> Set[EquivalenceClass]: + """ + Generates equivalence classes for the given lattice discretization. + + Returns + ------- + Set[EquivalenceClass] + All equivalence classes of the discretization. + """ equivalence_classes: Dict = {} for state in product( [0, 1], @@ -101,7 +187,7 @@ def generate_equivalence_classes(self) -> Set[EquivalenceClass]: equivalence_classes[key].append(state) return { - EquivalenceClass(self.discretization, v) + EquivalenceClass(self.discretization, set(tuple(cfg.tolist()) for cfg in v)) for _, v in equivalence_classes.items() if len(v) > 1 } diff --git a/qlbm/components/spacetime/collision/eqc_permutation.py b/qlbm/components/spacetime/collision/eqc_permutation.py index e69de29..2c26880 100644 --- a/qlbm/components/spacetime/collision/eqc_permutation.py +++ b/qlbm/components/spacetime/collision/eqc_permutation.py @@ -0,0 +1 @@ +"""TODO.""" \ No newline at end of file diff --git a/qlbm/lattice/spacetime/properties_base.py b/qlbm/lattice/spacetime/properties_base.py index 1bec944..df93eb4 100644 --- a/qlbm/lattice/spacetime/properties_base.py +++ b/qlbm/lattice/spacetime/properties_base.py @@ -10,6 +10,7 @@ from logging import Logger, getLogger from typing import Dict, List, Tuple, cast +import numpy as np from qiskit import QuantumRegister @@ -22,6 +23,79 @@ class LatticeDiscretization(Enum): D1Q2 = (1,) D2Q4 = (2,) + D3Q6 = (3,) + + +class LatticeDiscretizationProperties: + """ + Class containing properties of the lattice discretization in the :math:`D_dQ_q` taxonomy. + + .. list-table:: Attributes + :widths: 25 50 + :header-rows: 1 + + * - Attribute + - Description + * - :attr:`num_velocities` + - The number of velocities in the discretization. Stored as a ``Dict[LatticeDiscretization, int]``. + * - :attr:`velocity_vectors` + - The velocity profile each of the :math:`q` velocity channels. Each vector is :math:`d-`dimensional and each entry represents the velocity component of the channel in a particular dimension. Stored as a ``Dict[LatticeDiscretization, numpy.ndarray]``. + """ + + velocity_vectors: Dict[LatticeDiscretization, np.ndarray] = { + LatticeDiscretization.D1Q2: np.array([[1], [-1]]), + LatticeDiscretization.D2Q4: np.array([[1, 0], [0, 1], [-1, 0], [0, -1]]), + LatticeDiscretization.D3Q6: np.array( + [[1, 0, 0], [0, 1, 0], [0, 0, 1], [-1, 0, 0], [0, -1, 0], [0, 0, -1]] + ), + } + + num_velocities: Dict[LatticeDiscretization, int] = { + LatticeDiscretization.D1Q2: 2, + LatticeDiscretization.D2Q4: 4, + LatticeDiscretization.D3Q6: 6, + } + + @staticmethod + def get_velocity_vectors( + discretization: LatticeDiscretization, + ) -> np.ndarray: + """ + Get the velocity vectors for a given discretization. + + Parameters + ---------- + discretization : LatticeDiscretization + The discretization for which to get the velocity vectors. + + Returns + ------- + np.ndarray + The velocity vectors corresponding to the discretization. + """ + return LatticeDiscretizationProperties.velocity_vectors[discretization] + + @staticmethod + def get_num_velocities( + discretization: LatticeDiscretization, + ) -> int: + """ + Get the number of velocities for a given discretization. + + Parameters + ---------- + discretization : LatticeDiscretization + The discretization for which to get the number of velocities. + + Returns + ------- + int + The number of velocities corresponding to the discretization. + """ + if discretization not in LatticeDiscretizationProperties.num_velocities: + raise ValueError(f"Discretization {discretization} is not supported.") + + return LatticeDiscretizationProperties.num_velocities[discretization] class VonNeumannNeighborType(Enum): From ddecc9a8a218720127c5ce396f8b6116b66d6296 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 30 Jun 2025 11:43:24 +0200 Subject: [PATCH 13/92] Add equivalence class generator tests --- test/unit/spacetime/circuits/mcswap_test.py | 6 +- .../spacetime/collision/eqc_generator_test.py | 97 +++++++++++++++++++ .../eqc_velocity_discretization_test.py | 13 ++- 3 files changed, 108 insertions(+), 8 deletions(-) create mode 100644 test/unit/spacetime/collision/eqc_generator_test.py diff --git a/test/unit/spacetime/circuits/mcswap_test.py b/test/unit/spacetime/circuits/mcswap_test.py index 2b86c8d..e6ac124 100644 --- a/test/unit/spacetime/circuits/mcswap_test.py +++ b/test/unit/spacetime/circuits/mcswap_test.py @@ -1,4 +1,4 @@ -from qiskit.circuit.library import MCXGate +from qiskit.circuit.library import MCMTGate, XGate from qlbm.components.common.primitives import MCSwap from qlbm.lattice.lattices.spacetime_lattice import SpaceTimeLattice @@ -21,7 +21,7 @@ def test_mcswap_13ctrl(): expected_circuit.cx(6, 5) expected_circuit.compose( - MCXGate(4), + MCMTGate(XGate(), 4, 1), qubits=[0, 2, 3, 5, 6], inplace=True, ) @@ -51,7 +51,7 @@ def test_mcswap_grid_ctrl(): expected_circuit.cx(11, 9) expected_circuit.compose( - MCXGate(9), + MCMTGate(XGate(), 9, 1), qubits=list(range(8)) + [9, 11], inplace=True, ) diff --git a/test/unit/spacetime/collision/eqc_generator_test.py b/test/unit/spacetime/collision/eqc_generator_test.py new file mode 100644 index 0000000..e857d8a --- /dev/null +++ b/test/unit/spacetime/collision/eqc_generator_test.py @@ -0,0 +1,97 @@ +import numpy as np + +from qlbm.components.spacetime.collision.eqc_discretizations import ( + EquivalenceClass, + EquivalenceClassGenerator, +) +from qlbm.lattice.spacetime.properties_base import LatticeDiscretization + + +def test_eqc_generator_d1q2(): + generator = EquivalenceClassGenerator(LatticeDiscretization.D1Q2) + eqcs = generator.generate_equivalence_classes() + assert len(eqcs) == 0 + + +def test_eqc_generator_d2q4(): + generator = EquivalenceClassGenerator(LatticeDiscretization.D2Q4) + eqcs = generator.generate_equivalence_classes() + eqcs_expected = set( + [ + EquivalenceClass( + LatticeDiscretization.D2Q4, + {(True, False, True, False), (False, True, False, True)}, + ) + ] + ) + assert len(eqcs) == 1 + assert eqcs_expected == eqcs + + +def test_eqc_generator_d3q6(): + generator = EquivalenceClassGenerator(LatticeDiscretization.D3Q6) + eqcs = generator.generate_equivalence_classes() + eqcs_expected = set( + [ + EquivalenceClass( + LatticeDiscretization.D3Q6, + { + (True, False, False, True, False, False), # 100100 + (False, True, False, False, True, False), # 010010 + (False, False, True, False, False, True), # 001001 + }, + ), + EquivalenceClass( + LatticeDiscretization.D3Q6, + { + (True, True, False, True, True, False), # 110110 + (True, False, True, True, False, True), # 101101 + (False, True, True, False, True, True), # 011011 + }, + ), + EquivalenceClass( + LatticeDiscretization.D3Q6, # 3 + { + (True, True, False, False, True, False), # 110010 + (True, False, True, False, False, True), # 101001 + }, + ), + EquivalenceClass( + LatticeDiscretization.D3Q6, # 4 + { + (False, True, False, True, True, False), # 010110 + (False, False, True, True, False, True), # 001101 + }, + ), + EquivalenceClass( + LatticeDiscretization.D3Q6, # 5 + { + (True, True, False, True, False, False), # 110100 + (False, True, True, False, False, True), # 011001 + }, + ), + EquivalenceClass( + LatticeDiscretization.D3Q6, # 6 + { + (True, False, False, True, True, False), # 100110 + (False, False, True, False, True, True), # 001011 + }, + ), + EquivalenceClass( + LatticeDiscretization.D3Q6, # 7 + { + (True, False, True, True, False, False), # 101100 + (False, True, True, False, True, False), # 011010 + }, + ), + EquivalenceClass( + LatticeDiscretization.D3Q6, # 8 + { + (False, True, False, False, True, True), # 010011 + (True, False, False, True, False, True), # 100101 + }, + ), + ] + ) + assert len(eqcs) == 8 + assert all(eqc in eqcs_expected for eqc in eqcs) diff --git a/test/unit/spacetime/collision/eqc_velocity_discretization_test.py b/test/unit/spacetime/collision/eqc_velocity_discretization_test.py index f5c331d..b572919 100644 --- a/test/unit/spacetime/collision/eqc_velocity_discretization_test.py +++ b/test/unit/spacetime/collision/eqc_velocity_discretization_test.py @@ -1,3 +1,5 @@ +from typing import Set, Tuple + import pytest from qlbm.components.spacetime.collision import ( @@ -9,7 +11,7 @@ def test_equivalence_class_bad_number_of_velocity_configurations(): - velocity_profile = {(True, True, True, True)} + velocity_profile: Set[Tuple[bool, ...]] = {(True, True, True, True)} with pytest.raises(LatticeException) as excinfo: EquivalenceClass(LatticeDiscretization.D2Q4, velocity_profile) assert ( @@ -19,7 +21,7 @@ def test_equivalence_class_bad_number_of_velocity_configurations(): def test_equivalence_class_bad_number_velocity_configurations(): - velocity_profile = {(True, True, True), (True, True, True, False)} + velocity_profile: Set[Tuple[bool, ...]] = {(True, True, True), (True, True, True, False)} with pytest.raises(LatticeException) as excinfo: EquivalenceClass(LatticeDiscretization.D2Q4, velocity_profile) assert ( @@ -29,20 +31,21 @@ def test_equivalence_class_bad_number_velocity_configurations(): def test_equivalence_class_bad_mass(): - velocity_profile = {(True, False, True, False), (True, True, True, False)} + velocity_profile: Set[Tuple[bool, ...]] = {(True, False, True, False), (True, True, True, False)} with pytest.raises(LatticeException) as excinfo: EquivalenceClass(LatticeDiscretization.D2Q4, velocity_profile) assert "Velocity configurations have different masses." == str(excinfo.value) def test_equivalence_class_bad_momentum(): - velocity_profile = {(True, False, True, False), (True, True, False, False)} + velocity_profile: Set[Tuple[bool, ...]] = {(True, False, True, False), (True, True, False, False)} with pytest.raises(LatticeException) as excinfo: EquivalenceClass(LatticeDiscretization.D2Q4, velocity_profile) assert "Velocity configurations have different momenta." == str(excinfo.value) + def test_equivalence_class_ok(): - velocity_profile = {(True, False, True, False), (False, True, False, True)} + velocity_profile: Set[Tuple[bool, ...]] = {(True, False, True, False), (False, True, False, True)} eqc = EquivalenceClass(LatticeDiscretization.D2Q4, velocity_profile) assert eqc.mass == 2 assert eqc.momentum.tolist() == [0, 0] From f334231de49911639696552cd015b2add28d68c2 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 30 Jun 2025 14:18:02 +0200 Subject: [PATCH 14/92] Update website documentation --- docs/source/code/stqlbm_comps.rst | 17 +++++++++++++++++ docs/source/index.rst | 4 ++-- docs/source/refs.bib | 21 +++++++++++++++++---- qlbm/lattice/spacetime/properties_base.py | 2 +- 4 files changed, 37 insertions(+), 7 deletions(-) diff --git a/docs/source/code/stqlbm_comps.rst b/docs/source/code/stqlbm_comps.rst index 9851a60..389f57f 100644 --- a/docs/source/code/stqlbm_comps.rst +++ b/docs/source/code/stqlbm_comps.rst @@ -65,8 +65,25 @@ Streaming Collision ---------------------------------- +The collision module contains collision operators and adjacent logic classes. +The former implements the circuits that perform collision in computational basis state encodings, +while the latter contains useful abstractions that circuits build on top of. + +Collision Operators +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + .. autoclass:: qlbm.components.spacetime.collision.SpaceTimeCollisionOperator +Collision Logic Classes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: qlbm.lattice.spacetime.properties_base.LatticeDiscretization + +.. autoclass:: qlbm.lattice.spacetime.properties_base.LatticeDiscretizationProperties + +.. autoclass:: qlbm.components.spacetime.collision.EquivalenceClass + +.. autoclass:: qlbm.components.spacetime.collision.EquivalenceClassGenerator .. _stqlbm_others: diff --git a/docs/source/index.rst b/docs/source/index.rst index b80fd1d..83e7865 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -6,7 +6,7 @@ quantum computers running on a heterogeneous quantum-classical high-performance Currently, the primary aim of ``qlbm`` is to accelerate and improve the research surrounding **Q**\ uantum **L**\ attice **B**\ oltzmann **M**\ ethods (QLBMs). On this website, you can find the :ref:`internal_docs` of the source code components that make up ``qlbm``. -A preprint describing `qlbm` in detail is available on `arXiv `_ :cite:p:`qlbm`. +A paper describing `qlbm` in detail is available on `here `_ :cite:p:`qlbm`. ``qlbm`` is made up of 4 main modules. Together, the :ref:`base_components`, :ref:`cqlbm_components`, and :ref:`stqlbm_components` @@ -21,7 +21,7 @@ The :ref:`tools` module contains miscellaneous utilities. #. The **C**\ ollisionless **QLBM** (CQLBM) first described in :cite:p:`collisionless` and later expanded in :cite:p:`qmem`. -#. **S**\ pace-\ **T**\ ime **QLBM** (STQLBM) described in :cite:p:`spacetime`. +#. **S**\ pace-\ **T**\ ime **QLBM** (STQLBM) described in :cite:p:`spacetime` and :cite:p:`spacetime2`. .. card:: Internal Documentation :link: internal_docs diff --git a/docs/source/refs.bib b/docs/source/refs.bib index 61a4313..cbf2ae7 100644 --- a/docs/source/refs.bib +++ b/docs/source/refs.bib @@ -30,12 +30,25 @@ @article{qmem } @article{qlbm, - title={qlbm -- A Quantum Lattice Boltzmann Software Framework}, - author={Georgescu, C{\u{a}}lin A. and Schalkers, Merel A. and M{\"o}ller, Matthias}, - journal={arXiv preprint arXiv:2411.19439}, - year={2024} +title = {qlbm – A quantum lattice Boltzmann software framework}, +journal = {Computer Physics Communications}, +volume = {315}, +pages = {109699}, +year = {2025}, +issn = {0010-4655}, +doi = {https://doi.org/10.1016/j.cpc.2025.109699}, +url = {https://www.sciencedirect.com/science/article/pii/S0010465525002012}, +author = {C{\u{a}}lin A. Georgescu and Merel A. Schalkers and Matthias M{\"o}ller}, } +@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} +} + + @article{mcswap, title={Representation of binary classification trees with binary features by quantum circuits}, author={Heese, Raoul and Bickert, Patricia and Niederle, Astrid Elisa}, diff --git a/qlbm/lattice/spacetime/properties_base.py b/qlbm/lattice/spacetime/properties_base.py index df93eb4..8def8fb 100644 --- a/qlbm/lattice/spacetime/properties_base.py +++ b/qlbm/lattice/spacetime/properties_base.py @@ -39,7 +39,7 @@ class LatticeDiscretizationProperties: * - :attr:`num_velocities` - The number of velocities in the discretization. Stored as a ``Dict[LatticeDiscretization, int]``. * - :attr:`velocity_vectors` - - The velocity profile each of the :math:`q` velocity channels. Each vector is :math:`d-`dimensional and each entry represents the velocity component of the channel in a particular dimension. Stored as a ``Dict[LatticeDiscretization, numpy.ndarray]``. + - The velocity profile each of the :math:`q` velocity channels. Each vector is :math:`d`-dimensional and each entry represents the velocity component of the channel in a particular dimension. Stored as a ``Dict[LatticeDiscretization, numpy.ndarray]``. """ velocity_vectors: Dict[LatticeDiscretization, np.ndarray] = { From a4ac1bd9b81045086374f77867cc1fda6838ded6 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 30 Jun 2025 14:18:14 +0200 Subject: [PATCH 15/92] Update mypy dependency version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2d617b9..9f8de33 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,7 @@ readme = "README.md" [project.optional-dependencies] dev = [ - "mypy", + "mypy==1.16.1", "pytest", "pytest-cov", "coverage", From fcd85868d356a3def46d80bf45ae5024fe5b0751 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Wed, 2 Jul 2025 07:38:14 +0200 Subject: [PATCH 16/92] Add D3Q6 collision permutation step --- .../spacetime/collision/eqc_permutation.py | 124 +++++++++++++++++- 1 file changed, 123 insertions(+), 1 deletion(-) diff --git a/qlbm/components/spacetime/collision/eqc_permutation.py b/qlbm/components/spacetime/collision/eqc_permutation.py index 2c26880..58e23da 100644 --- a/qlbm/components/spacetime/collision/eqc_permutation.py +++ b/qlbm/components/spacetime/collision/eqc_permutation.py @@ -1 +1,123 @@ -"""TODO.""" \ No newline at end of file +"""Permutation step in QLGA collision.""" + +from logging import Logger, getLogger +from typing import override + +from qiskit import QuantumCircuit + +from qlbm.components.base import LBMPrimitive +from qlbm.components.spacetime.collision.eqc_discretizations import EquivalenceClass +from qlbm.lattice.spacetime.properties_base import LatticeDiscretization +from qlbm.tools.exceptions import CircuitException + + +class SpaceTimeEQCPermutation(LBMPrimitive): + """Permutes states belonging to equivalence classes onto pre-determined basis states. + + Precedes redistribution. + + WIP. + """ + + def __init__( + self, + equivalence_class: EquivalenceClass, + inverse: bool = False, + logger: Logger = getLogger("qlbm"), + ): + super().__init__(logger) + self.equivalence_class = equivalence_class + self.inverse = inverse + + self.circuit = self.create_circuit() + + @override + def create_circuit(self): + if self.equivalence_class.discretization == LatticeDiscretization.D2Q4: + return self.__create_circuit_d2q4() + elif self.equivalence_class.discretization == LatticeDiscretization.D3Q6: + return self.__create_circuit_d3q6() + else: + raise CircuitException( + f"Collision not yet supported for discretization {self.equivalence_class.discretization}." + ) + + def __create_circuit_d2q4(self): + circuit = QuantumCircuit(4) + + if not self.inverse: + circuit.cx(1, 2) + circuit.cx(0, 1) + circuit.cx(0, 3) + else: + circuit.cx(0, 3) + circuit.cx(0, 1) + circuit.cx(1, 2) + + return circuit + + def __create_circuit_d3q6(self): + circuit = QuantumCircuit(6) + + match self.equivalence_class.id(): + case (2, [0, 0, 0]): + circuit.cx(2, 3) + circuit.cx(5, 4) + + circuit.cx(1, 2) + circuit.cx(1, 3) + circuit.cx(1, 5) + + circuit.cx(0, 2) + circuit.cx(0, 4) + circuit.cx(0, 5) + case (4, [0, 0, 0]): + circuit.ccx(4, 5, 3) + circuit.ccx(0, 2, 4) + circuit.ccx(0, 1, 2) + circuit.ccx(0, 1, 5) + circuit.x(1) + circuit.cx(1, 0) + case (3, [1, 0, 0]): + circuit.cx(0, 3) + circuit.cx(1, 2) + circuit.ccx(0, 5, 4) + circuit.cx(1, 5) + circuit.swap(0, 1) + case (3, [-1, 0, 0]): + circuit.cx(1, 2) + circuit.cx(5, 4) + circuit.cx(1, 5) + circuit.x(0) + circuit.swap(0, 1) + case (3, [0, 1, 0]): + circuit.cx(0, 2) + circuit.cx(5, 3) + circuit.cx(0, 5) + circuit.x(4) + case (3, [0, -1, 0]): + circuit.cx(5, 3) + circuit.cx(0, 2) + circuit.cx(0, 5) + circuit.x(1) + case (3, [0, 0, 1]): + circuit.cx(1, 3) + circuit.cx(0, 1) + circuit.cx(0, 4) + circuit.x(5) + case (3, [0, 0, -1]): + circuit.cx(0, 1) + circuit.cx(4, 3) + circuit.cx(0, 4) + circuit.x(2) + case _: + raise CircuitException( + f"Collision not yet supported for discretization {self.equivalence_class.discretization} and equivalence class {self.equivalence_class.id()}." + ) + + return circuit if not self.inverse else circuit.inverse() + + @override + def __str__(self) -> str: + # TODO: Implement + return "Space Time Collision Operator" From 3f0554372d1b4d4ff934f548fc4f15d1f94e900d Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Wed, 2 Jul 2025 07:38:59 +0200 Subject: [PATCH 17/92] Add bitstring functionality to EQC generator --- .../collision/eqc_discretizations.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/qlbm/components/spacetime/collision/eqc_discretizations.py b/qlbm/components/spacetime/collision/eqc_discretizations.py index 1240e8d..c61440a 100644 --- a/qlbm/components/spacetime/collision/eqc_discretizations.py +++ b/qlbm/components/spacetime/collision/eqc_discretizations.py @@ -1,7 +1,7 @@ """Equivalence class utility functions. For a discussion of equivalence classes, we refer the reader to Section 4 of :cite:`spacetime2`.""" from itertools import product -from typing import Dict, Set, Tuple, override +from typing import Dict, List, Set, Tuple, override import numpy as np @@ -109,7 +109,7 @@ def size(self) -> int: """ return len(self.velocity_configurations) - def id(self) -> Tuple[int, np.typing.NDArray]: + def id(self) -> Tuple[int, List[float]]: """ The identifier of the equivalence class. @@ -117,10 +117,21 @@ def id(self) -> Tuple[int, np.typing.NDArray]: Returns ------- - Tuple[int, np.typing.NDArray] + Tuple[int, List[float]] The mass and momentum of the equivalence class. """ - return (self.mass, self.momentum) + return (self.mass, self.momentum.tolist()) + + def get_bitstrings(self) -> List[str]: + """ + Returns the velocity configurations as bitstrings. + + Returns + ------- + List[str] + The velocity configurations of the equivalence class as bitstrings. + """ + return [''.join(['1' if x else '0' for x in cfg]) for cfg in self.velocity_configurations] @override def __eq__(self, value): From e1acf127a71d06bb7de9c7ce92f2dfcbdd3c4875 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Wed, 2 Jul 2025 07:39:30 +0200 Subject: [PATCH 18/92] Add support for 3D STQBM lattice parsing --- qlbm/lattice/lattices/spacetime_lattice.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/qlbm/lattice/lattices/spacetime_lattice.py b/qlbm/lattice/lattices/spacetime_lattice.py index 8c8d052..266c87a 100644 --- a/qlbm/lattice/lattices/spacetime_lattice.py +++ b/qlbm/lattice/lattices/spacetime_lattice.py @@ -9,6 +9,7 @@ from qlbm.lattice.lattices.base import Lattice from qlbm.lattice.spacetime.d1q2 import D1Q2SpaceTimeLatticeBuilder from qlbm.lattice.spacetime.d2q4 import D2Q4SpaceTimeLatticeBuilder +from qlbm.lattice.spacetime.d3q6 import D3Q6SpaceTimeLatticeBuilder from qlbm.lattice.spacetime.properties_base import SpaceTimeLatticeBuilder from qlbm.tools.exceptions import LatticeException from qlbm.tools.utils import flatten @@ -177,9 +178,24 @@ def __get_builder(self) -> SpaceTimeLatticeBuilder: f"Unsupported number of velocities for 2D: {(self.num_velocities[0] + 1, self.num_velocities[1] + 1)}. Only D2Q4 is supported at the moment." ) - raise LatticeException( - "Only 1D and 2D discretizations are currently available." - ) + if self.num_dims == 3: + if ( + self.num_velocities[0] == 1 + and self.num_velocities[1] == 1 + and self.num_velocities[2] == 1 + ): + return D3Q6SpaceTimeLatticeBuilder( + self.num_timesteps, + self.num_gridpoints, + include_measurement_qubit=self.include_measurement_qubit, + use_volumetric_ops=self.use_volumetric_ops, + logger=self.logger, + ) + raise LatticeException( + f"Unsupported number of velocities for 3D: {(self.num_velocities[0] + 1, self.num_velocities[1] + 1)}. Only D3Q6 is supported at the moment." + ) + + raise LatticeException("Only 1-3D discretizations are currently available.") 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. From 6c22a4d185659d1ade4da2c75a7859acf6456264 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Wed, 2 Jul 2025 07:39:58 +0200 Subject: [PATCH 19/92] Add generic PRP collision opeator --- .../spacetime/collision/eqc_collision.py | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/qlbm/components/spacetime/collision/eqc_collision.py b/qlbm/components/spacetime/collision/eqc_collision.py index 135ccd3..b2778b0 100644 --- a/qlbm/components/spacetime/collision/eqc_collision.py +++ b/qlbm/components/spacetime/collision/eqc_collision.py @@ -10,6 +10,14 @@ from typing_extensions import override from qlbm.components.base import SpaceTimeOperator +from qlbm.components.spacetime.collision.eqc_discretizations import ( + EquivalenceClass, + EquivalenceClassGenerator, +) +from qlbm.components.spacetime.collision.eqc_permutation import SpaceTimeEQCPermutation +from qlbm.components.spacetime.collision.eqc_redistribution import ( + SpaceTimeEQCRedistribution, +) from qlbm.lattice.lattices.spacetime_lattice import SpaceTimeLattice @@ -160,3 +168,115 @@ def local_collision_circuit(self, reset_state: bool) -> QuantumCircuit: def __str__(self) -> str: # TODO: Implement return "Space Time Collision Operator" + + +class GenericSpaceTimeCollisionOperator(SpaceTimeOperator): + """ + A generic space-time collision operator that can be used to apply any gate to the velocities of a grid location. + + ========================= ====================================================================== + Attribute Summary + ========================= ====================================================================== + :attr:`lattice` The :class:`.SpaceTimeLattice` based on which the properties of the operator are inferred. + :attr:`gate` The gate to apply to the velocities. + :attr:`logger` The performance logger, by default ``getLogger("qlbm")``. + ========================= ====================================================================== + """ + + def __init__( + self, + lattice: SpaceTimeLattice, + timestep: int, + logger: Logger = getLogger("qlbm"), + ) -> None: + super().__init__(lattice, logger) + self.lattice = lattice + self.timestep = timestep + + 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: + local_collision_circuit = self.create_circuit_one_register() + circuit = self.lattice.circuit.copy() + + for velocity_qubit_indices in range( + self.lattice.properties.get_num_grid_qubits(), + self.lattice.properties.get_num_grid_qubits() + + self.lattice.properties.get_num_velocity_qubits(self.timestep), + self.lattice.properties.get_num_velocities_per_point(), + ): + circuit.compose( + local_collision_circuit, + inplace=True, + qubits=range( + velocity_qubit_indices, + velocity_qubit_indices + + self.lattice.properties.get_num_velocities_per_point(), + ), + ) + return circuit + + def create_circuit_one_register(self) -> QuantumCircuit: + """ + Applies the collision operator of all equivalence classes onto one velocity register. + + Returns + ------- + QuantumCircuit + The circuit performing the complete collision operator. + """ + circuit = QuantumCircuit(self.lattice.properties.get_num_velocities_per_point()) + + for eqc in EquivalenceClassGenerator( + self.lattice.properties.get_discretization() + ).generate_equivalence_classes(): + circuit.compose(self.create_circuit_one_eqc(eqc), inplace=True) + + return circuit + + def create_circuit_one_eqc( + self, equivalence_class: EquivalenceClass + ) -> QuantumCircuit: + """ + Creates the PRP-based collision operator for one equivalence class. + + Parameters + ---------- + equivalence_class : EquivalenceClass + The equivalence class to collide. + + Returns + ------- + QuantumCircuit + The circuit performing the collision. + """ + circuit = QuantumCircuit(self.lattice.properties.get_num_velocities_per_point()) + # if equivalence_class.id() in [(4, [0, 0, 0]), (3, [1, 0, 0]), (2, [0, 0, 0])]: + circuit.compose( + SpaceTimeEQCPermutation(equivalence_class, logger=self.logger).circuit, + inplace=True, + ) + + circuit.compose( + SpaceTimeEQCRedistribution(equivalence_class, logger=self.logger).circuit, + inplace=True, + ) + circuit.compose( + SpaceTimeEQCPermutation( + equivalence_class, inverse=True, logger=self.logger + ).circuit, + inplace=True, + ) + + return circuit + + @override + def __str__(self) -> str: + # TODO + return "Generic Space Time Collision Operator" From 0b096d5b64ddedc52ddebe57984d5f21ef6a687a Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Wed, 2 Jul 2025 07:40:35 +0200 Subject: [PATCH 20/92] Add D3Q6 STQBM discretization --- qlbm/lattice/spacetime/d2q4.py | 5 +- qlbm/lattice/spacetime/d3q6.py | 158 +++++++++++++++++++++++++++++++++ 2 files changed, 160 insertions(+), 3 deletions(-) create mode 100644 qlbm/lattice/spacetime/d3q6.py diff --git a/qlbm/lattice/spacetime/d2q4.py b/qlbm/lattice/spacetime/d2q4.py index 6a16103..6643c50 100644 --- a/qlbm/lattice/spacetime/d2q4.py +++ b/qlbm/lattice/spacetime/d2q4.py @@ -1,6 +1,5 @@ """:math:`D_2Q_4` STQBM builder.""" - from itertools import product from logging import Logger, getLogger from typing import Dict, List, Tuple, cast @@ -367,7 +366,7 @@ def coordinates_to_quadrant(self, distance: Tuple[int, int]) -> int: def get_reflected_index_of_velocity(self, velocity_index: int) -> int: if velocity_index not in list(range(4)): raise LatticeException( - f"D1Q2 discretization only supports velocities with indices {list(range(4))}. Index {velocity_index} unsupported." + f"D2Q4 discretization only supports velocities with indices {list(range(4))}. Index {velocity_index} unsupported." ) return self.velocity_reflection[velocity_index] @@ -376,7 +375,7 @@ def get_reflected_index_of_velocity(self, velocity_index: int) -> int: def get_reflection_increments(self, velocity_index: int) -> Tuple[int, ...]: if velocity_index not in list(range(4)): raise LatticeException( - f"D1Q2 discretization only supports velocities with indices {list(range(4))}. Index {velocity_index} unsupported." + f"D2Q4 discretization only supports velocities with indices {list(range(4))}. Index {velocity_index} unsupported." ) return self.extreme_point_classes[velocity_index][1] diff --git a/qlbm/lattice/spacetime/d3q6.py b/qlbm/lattice/spacetime/d3q6.py new file mode 100644 index 0000000..c6baf84 --- /dev/null +++ b/qlbm/lattice/spacetime/d3q6.py @@ -0,0 +1,158 @@ +""":math:`D_3Q_6` STQBM builder.""" + +from itertools import product +from logging import Logger, getLogger +from typing import Dict, List, Tuple + +from qiskit import QuantumRegister +from typing_extensions import override + +from qlbm.lattice.spacetime.properties_base import ( + LatticeDiscretization, + SpaceTimeLatticeBuilder, +) +from qlbm.tools.exceptions import LatticeException +from qlbm.tools.utils import dimension_letter + + +class D3Q6SpaceTimeLatticeBuilder(SpaceTimeLatticeBuilder): + """ + :math:`D_3Q_6` lattice builder. + + WIP. + """ + + velocity_reflection: Dict[int, int] = {0: 3, 1: 4, 2: 5, 3: 0, 4: 1, 5: 2} + + def __init__( + self, + num_timesteps: int, + num_gridpoints: List[int], + include_measurement_qubit: bool = False, + use_volumetric_ops: bool = False, + logger: Logger = getLogger("qlbm"), + ) -> None: + super().__init__( + num_timesteps, + include_measurement_qubit=include_measurement_qubit, + use_volumetric_ops=use_volumetric_ops, + logger=logger, + ) + self.num_gridpoints = num_gridpoints + + @override + def get_discretization(self) -> LatticeDiscretization: + return LatticeDiscretization.D3Q6 + + @override + def get_num_velocities_per_point(self) -> int: + return 6 + + @override + def get_num_ancilla_qubits(self) -> int: + return (6 if self.use_volumetric_ops else 0) + ( + 1 if self.include_measurement_qubit else 0 + ) + + @override + def get_num_grid_qubits(self) -> int: + return sum( + num_gridpoints_in_dim.bit_length() + for num_gridpoints_in_dim in self.num_gridpoints + ) + + @override + def get_num_previous_grid_qubits(self, dim: int) -> int: + # ! TODO add exception + return sum(self.num_gridpoints[i].bit_length() for i in range(dim)) + + @override + def get_num_velocity_qubits(self, num_timesteps: int | None = None) -> int: + total_gridpoints = ( + (sum(self.num_gridpoints) + len(self.num_gridpoints)) + * (sum(self.num_gridpoints) + len(self.num_gridpoints)) + * (sum(self.num_gridpoints) + len(self.num_gridpoints)) + ) + velocities_per_gp = self.get_num_velocities_per_point() + + if num_timesteps is None: + num_timesteps = self.num_timesteps + + return min( + total_gridpoints * velocities_per_gp, + int( + 8 * velocities_per_gp * velocities_per_gp * velocities_per_gp + + 12 * velocities_per_gp * velocities_per_gp + + 16 * velocities_per_gp + + velocities_per_gp + ), + ) + + @override + def get_registers(self) -> Tuple[List[QuantumRegister], ...]: + # Grid qubits + grid_registers = [ + QuantumRegister(gp.bit_length(), name=f"g_{dimension_letter(c)}") + for c, gp in enumerate(self.num_gridpoints) + ] + + # Velocity qubits + velocity_registers = [ + QuantumRegister( + self.get_num_velocity_qubits( + self.num_timesteps, + ), # The number of velocity qubits required at time t + name="v", + ) + ] + + ancilla_measurement_register = ( + [QuantumRegister(1, "a_m")] if self.include_measurement_qubit else [] + ) + + ancilla_comparator_registers = ( + [ + QuantumRegister(1, f"a_{bound}{dimension_letter(dim)}") + for dim, bound in product([0, 1], ["l", "u"]) + ] + if self.use_volumetric_ops + else [] + ) + + # Ancilla qubits + ancilla_registers = ancilla_measurement_register + ancilla_comparator_registers + + return (grid_registers, velocity_registers, ancilla_registers) + + @override + def get_index_of_neighbor(self, distance: Tuple[int, ...]) -> int: + raise LatticeException("Not implemented") + + @override + def get_streaming_lines( + self, dimension: int, direction: bool, timestep: int | None = None + ) -> List[List[int]]: + raise LatticeException("Not implemented") + + @override + def get_neighbor_indices(self): + # ! TODO!!! + return [], [] + + @override + def get_reflected_index_of_velocity(self, velocity_index: int) -> int: + if velocity_index not in list(range(4)): + raise LatticeException( + f"D3Q6 discretization only supports velocities with indices 0-5. Index {velocity_index} unsupported." + ) + + return self.velocity_reflection[velocity_index] + + @override + def get_reflection_increments(self, velocity_index: int) -> Tuple[int, ...]: + if velocity_index not in list(range(4)): + raise LatticeException( + f"D3Q6 discretization only supports velocities with indices 0-5. Index {velocity_index} unsupported." + ) + + raise LatticeException("Not implemented") From f5339f39899b0cfe72d225a5092c9bda5ec63da4 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Wed, 2 Jul 2025 07:41:09 +0200 Subject: [PATCH 21/92] Update D2Q4 lattice tests --- test/unit/spacetime/d2q4_lattice_test.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/test/unit/spacetime/d2q4_lattice_test.py b/test/unit/spacetime/d2q4_lattice_test.py index 37448b5..ef5e8cc 100644 --- a/test/unit/spacetime/d2q4_lattice_test.py +++ b/test/unit/spacetime/d2q4_lattice_test.py @@ -1128,20 +1128,21 @@ def test_bad_lattice_specification_velocities(): ) -def test_bad_lattice_specification_velocities_3d(): +def test_bad_lattice_specification_velocities_4d(): with pytest.raises(LatticeException) as excinfo_measure: SpaceTimeLattice( 2, { "lattice": { - "dim": {"x": 16, "y": 16, "z": 16}, - "velocities": {"x": 2, "y": 2, "z": 2}, + "dim": {"x": 16, "y": 16, "z": 16, "a": 32}, + "velocities": {"x": 2, "y": 2, "z": 2, "a": 2}, }, }, ) - assert "Only 1D and 2D discretizations are currently available." == str( - excinfo_measure.value + assert ( + "Only 1, 2, and 3-dimensional lattices are supported. Provided lattice has 4 dimensions." + == str(excinfo_measure.value) ) From 0d65a423694be4b1019fad1100dfb04312593ef5 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Wed, 2 Jul 2025 07:41:34 +0200 Subject: [PATCH 22/92] Add PRP collision property tests --- .../spacetime/collision/eqc_collision_test.py | 177 ++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 test/unit/spacetime/collision/eqc_collision_test.py diff --git a/test/unit/spacetime/collision/eqc_collision_test.py b/test/unit/spacetime/collision/eqc_collision_test.py new file mode 100644 index 0000000..b377589 --- /dev/null +++ b/test/unit/spacetime/collision/eqc_collision_test.py @@ -0,0 +1,177 @@ +from typing import List + +import pytest +from qiskit import QuantumCircuit, transpile +from qiskit_aer import AerSimulator + +from qlbm.components.spacetime.collision.eqc_collision import ( + GenericSpaceTimeCollisionOperator, +) +from qlbm.components.spacetime.collision.eqc_discretizations import ( + EquivalenceClassGenerator, +) +from qlbm.lattice.lattices.spacetime_lattice import SpaceTimeLattice +from qlbm.lattice.spacetime.properties_base import LatticeDiscretization +from qlbm.tools.utils import flatten + + +def int_to_bool_list(num, num_bits): + return bit_string_to_bool_list(format(num, f"0{num_bits}b")) + + +def bit_string_to_bool_list(bitstring): + return [x == "1" for x in bitstring] + + +@pytest.fixture +def d2q4_equivalence_class_bitstrings() -> List[List[str]]: + return [ + eqc.get_bitstrings() + for eqc in EquivalenceClassGenerator( + LatticeDiscretization.D2Q4 + ).generate_equivalence_classes() + ] + + +@pytest.fixture +def d3q6_equivalence_class_bitstrings() -> List[List[str]]: + return sorted( + [ + eqc.get_bitstrings() + for eqc in EquivalenceClassGenerator( + LatticeDiscretization.D3Q6 + ).generate_equivalence_classes() + ], + key=lambda x: x[0], + ) # Sort by first element + + +def verify_simulation_outcome( + input_state: str, + expected_outcomes: List[str], + collision_circuit: QuantumCircuit, + num_shots=128, + verify_negative_cases: bool = True, +): + init_circuit = QuantumCircuit(collision_circuit.num_qubits) + # for c, b in enumerate(bit_string_to_bool_list(input_state[::-1])): + # if b: + # init_circuit.x(collision_circuit.num_qubits - c - 1) + for c, b in enumerate(bit_string_to_bool_list(input_state)): + if b: + init_circuit.x(c) + qc_final = init_circuit.compose(collision_circuit) + qc_final.measure_all() + sim = AerSimulator() + counts = sim.run(transpile(qc_final, sim), shots=num_shots).result().get_counts() + + assert all(b[::-1] in counts for b in expected_outcomes), ( + f"{expected_outcomes} not covered by counts {counts}" + ) + + if verify_negative_cases: + all_biststrings = [ + format(i, f"0{collision_circuit.num_qubits}b") + for i in range(2**collision_circuit.num_qubits) + ] + assert all( + b[::-1] not in counts for b in all_biststrings if b not in expected_outcomes + ) + + +@pytest.mark.parametrize( + "equivalence_class_index", + list( + range( + len( + EquivalenceClassGenerator( + LatticeDiscretization.D2Q4 + ).generate_equivalence_classes() + ) + ) + ), +) +def test_d2q4_collision_positive_cases( + d2q4_equivalence_class_bitstrings, equivalence_class_index +): + lattice = SpaceTimeLattice( + 1, + { + "lattice": {"dim": {"x": 4, "y": 4}, "velocities": {"x": 2, "y": 2}}, + "geometry": [], + }, + ) + + collision_operator = GenericSpaceTimeCollisionOperator(lattice, 0) + local_circuit = collision_operator.create_circuit_one_register() + assert local_circuit.num_qubits == 4 + eqc = d2q4_equivalence_class_bitstrings[equivalence_class_index] + for velocity_cfg in eqc: + verify_simulation_outcome( + velocity_cfg, eqc, local_circuit, verify_negative_cases=True + ) + + +def test_d2q4_collision_negative_cases(d2q4_equivalence_class_bitstrings): + lattice = SpaceTimeLattice( + 1, + { + "lattice": {"dim": {"x": 4, "y": 4}, "velocities": {"x": 2, "y": 2}}, + "geometry": [], + }, + ) + + collision_operator = GenericSpaceTimeCollisionOperator(lattice, 0) + local_circuit = collision_operator.create_circuit_one_register() + assert local_circuit.num_qubits == 4 + + for b in [ + format(i, f"0{local_circuit.num_qubits}b") + for i in range(2**local_circuit.num_qubits) + if format(i, f"0{local_circuit.num_qubits}b") + not in flatten(d2q4_equivalence_class_bitstrings) + ]: + verify_simulation_outcome( + b, + [b], + local_circuit, + verify_negative_cases=True, + ) + + +@pytest.mark.parametrize( + "equivalence_class_index", + list( + range( + len( + EquivalenceClassGenerator( + LatticeDiscretization.D3Q6 + ).generate_equivalence_classes() + ) + ) + ), +) +def test_d3q6_collision_positive_cases( + d3q6_equivalence_class_bitstrings, equivalence_class_index +): + lattice = SpaceTimeLattice( + 1, + { + "lattice": { + "dim": {"x": 2, "y": 2, "z": 2}, + "velocities": {"x": 2, "y": 2, "z": 2}, + }, + "geometry": [], + }, + ) + + collision_operator = GenericSpaceTimeCollisionOperator(lattice, 0) + local_circuit = collision_operator.create_circuit_one_register() + assert local_circuit.num_qubits == 6 + eqc = d3q6_equivalence_class_bitstrings[equivalence_class_index] + for velocity_cfg in eqc: + verify_simulation_outcome( + velocity_cfg, eqc, local_circuit, verify_negative_cases=True + ) + + # [['010011', '100101'], ['010110', '001101'], ['011011', '110110', '101101'], ['100100', '001001', '010010'], ['100110', '001011'], ['101100', '011010'], ['110010', '101001'], ['110100', '011001']] From ea65033c5236f82e74e229bcd1159b4b98ac1633 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Wed, 2 Jul 2025 07:42:22 +0200 Subject: [PATCH 23/92] Add PRP collision redistribution step --- .../spacetime/collision/eqc_redistribution.py | 65 +++++++++++++++++++ .../collision/eqc_permutation_test.py | 0 2 files changed, 65 insertions(+) create mode 100644 qlbm/components/spacetime/collision/eqc_redistribution.py create mode 100644 test/unit/spacetime/collision/eqc_permutation_test.py diff --git a/qlbm/components/spacetime/collision/eqc_redistribution.py b/qlbm/components/spacetime/collision/eqc_redistribution.py new file mode 100644 index 0000000..944e861 --- /dev/null +++ b/qlbm/components/spacetime/collision/eqc_redistribution.py @@ -0,0 +1,65 @@ +"""Redistribution step in QLGA collision.""" + +from logging import Logger, getLogger +from typing import override + +import numpy as np +from qiskit import QuantumCircuit +from qiskit.quantum_info import Operator + +from qlbm.components.base import LBMPrimitive +from qlbm.components.spacetime.collision.eqc_discretizations import EquivalenceClass +from qlbm.lattice.spacetime.properties_base import LatticeDiscretizationProperties + + +class SpaceTimeEQCRedistribution(LBMPrimitive): + """ + Redistributes the amplitudes of the states belonging to an equivalence class evenly across all other equivalent states. + + WIP. + """ + + def __init__( + self, equivalence_class: EquivalenceClass, logger: Logger = getLogger("qlbm") + ): + super().__init__(logger) + self.equivalence_class = equivalence_class + + self.circuit = self.create_circuit() + + @override + def create_circuit(self): + nv = LatticeDiscretizationProperties.get_num_velocities( + self.equivalence_class.discretization + ) + circuit = QuantumCircuit(nv) + + n = self.equivalence_class.size() + nq = np.ceil(np.log2(n)).astype(int) + + QFT = np.array( + [ + [np.exp(2j * np.pi * i * j / n) / np.sqrt(n) for j in range(n)] + for i in range(n) + ] + ) + + U = np.eye(2**nq, dtype=complex) + U[:n, :n] = QFT + op = Operator(U) + assert op.is_unitary() + + qft_block_circ = QuantumCircuit(nq) + qft_block_circ.append(op, list(range(nq))) + + circuit.compose( + qft_block_circ.control(nv - int(nq), label=rf"Coll({n}, {nq})"), + qubits=list(range(nv - 1, -1, -1)), + inplace=True, + ) + + return circuit.decompose() + + @override + def __str__(self): + return super().__str__() diff --git a/test/unit/spacetime/collision/eqc_permutation_test.py b/test/unit/spacetime/collision/eqc_permutation_test.py new file mode 100644 index 0000000..e69de29 From 032fbe1dfe7b31f1abc66f547e720aadcfaabea8 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Wed, 2 Jul 2025 16:06:41 +0200 Subject: [PATCH 24/92] Add missing str implementations --- qlbm/components/spacetime/collision/eqc_collision.py | 5 +---- qlbm/components/spacetime/collision/eqc_permutation.py | 3 +-- qlbm/components/spacetime/measurement.py | 6 ++---- qlbm/components/spacetime/reflection/pointwise.py | 3 +-- qlbm/components/spacetime/streaming.py | 3 +-- test/unit/blocks_3d_test.py | 2 -- 6 files changed, 6 insertions(+), 16 deletions(-) diff --git a/qlbm/components/spacetime/collision/eqc_collision.py b/qlbm/components/spacetime/collision/eqc_collision.py index b2778b0..f25dd41 100644 --- a/qlbm/components/spacetime/collision/eqc_collision.py +++ b/qlbm/components/spacetime/collision/eqc_collision.py @@ -166,7 +166,6 @@ def local_collision_circuit(self, reset_state: bool) -> QuantumCircuit: @override def __str__(self) -> str: - # TODO: Implement return "Space Time Collision Operator" @@ -257,7 +256,6 @@ def create_circuit_one_eqc( The circuit performing the collision. """ circuit = QuantumCircuit(self.lattice.properties.get_num_velocities_per_point()) - # if equivalence_class.id() in [(4, [0, 0, 0]), (3, [1, 0, 0]), (2, [0, 0, 0])]: circuit.compose( SpaceTimeEQCPermutation(equivalence_class, logger=self.logger).circuit, inplace=True, @@ -278,5 +276,4 @@ def create_circuit_one_eqc( @override def __str__(self) -> str: - # TODO - return "Generic Space Time Collision Operator" + return f"[GenericSpaceTimeCollisionOperator for discretization {self.lattice.properties.get_discretization()}]" diff --git a/qlbm/components/spacetime/collision/eqc_permutation.py b/qlbm/components/spacetime/collision/eqc_permutation.py index 58e23da..6a364fa 100644 --- a/qlbm/components/spacetime/collision/eqc_permutation.py +++ b/qlbm/components/spacetime/collision/eqc_permutation.py @@ -119,5 +119,4 @@ def __create_circuit_d3q6(self): @override def __str__(self) -> str: - # TODO: Implement - return "Space Time Collision Operator" + return f"[SpaceTimeEQCPermutation for eqc {self.equivalence_class}]" diff --git a/qlbm/components/spacetime/measurement.py b/qlbm/components/spacetime/measurement.py index a6da9a1..8e34bca 100644 --- a/qlbm/components/spacetime/measurement.py +++ b/qlbm/components/spacetime/measurement.py @@ -79,8 +79,7 @@ def create_circuit(self): @override def __str__(self) -> str: - # TODO: Implement - return "Space Gird Measurement" + return f"[SpaceTimeGridVelocityMeasurement for lattice {self.lattice}]" class SpaceTimePointWiseMassMeasurement(SpaceTimeOperator): @@ -150,5 +149,4 @@ def create_circuit(self): @override def __str__(self) -> str: - # TODO: Implement - return "SpaceTimePointWiseMassMeasurement" + return f"[SpaceTimePointWiseMassMeasurement for lattice {self.lattice}, gridpoint {self.gridpoint}, velocity {self.velocity_index_to_measure}]" diff --git a/qlbm/components/spacetime/reflection/pointwise.py b/qlbm/components/spacetime/reflection/pointwise.py index 0be4024..fdd6fde 100644 --- a/qlbm/components/spacetime/reflection/pointwise.py +++ b/qlbm/components/spacetime/reflection/pointwise.py @@ -169,5 +169,4 @@ def __create_circuit_d2q4(self) -> QuantumCircuit: @override def __str__(self) -> str: - # TODO: Implement - return "Space Time Reflection Operator" + return f"[PointWiseSpaceTimeReflectionOperator for lattice {self.lattice}, blocks {self.blocks}, filtered {self.filter_inside_blocks}]" diff --git a/qlbm/components/spacetime/streaming.py b/qlbm/components/spacetime/streaming.py index b4be9a5..cd7e32f 100644 --- a/qlbm/components/spacetime/streaming.py +++ b/qlbm/components/spacetime/streaming.py @@ -165,5 +165,4 @@ def stream_lines( @override def __str__(self) -> str: - # TODO: Implement - return "Space Time Streaming Operator" + return f"[SpaceTimeStreamingOperator for lattice {self.lattice}]" diff --git a/test/unit/blocks_3d_test.py b/test/unit/blocks_3d_test.py index babb025..d3353eb 100644 --- a/test/unit/blocks_3d_test.py +++ b/test/unit/blocks_3d_test.py @@ -38,8 +38,6 @@ def test_3d_mesh(simple_3d_block): assert all(a == b for a, b in zip(vertex_ull, mesh.vectors[1][1])) assert all(a == b for a, b in zip(vertex_ulu, mesh.vectors[1][2])) - # TODO: complete the remaining 5 faces - def test_3d_inner_x_wall_points(simple_3d_block): assert len(simple_3d_block.walls_inside) == 3 From 913257c6cedd1e691385cb894070e650cbc6597c Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 7 Jul 2025 14:16:19 +0200 Subject: [PATCH 25/92] Update lattice documentation --- qlbm/lattice/lattices/base.py | 94 ++++++++++++++----- .../lattice/lattices/collisionless_lattice.py | 6 +- qlbm/lattice/lattices/spacetime_lattice.py | 4 +- 3 files changed, 78 insertions(+), 26 deletions(-) diff --git a/qlbm/lattice/lattices/base.py b/qlbm/lattice/lattices/base.py index fcc843b..e820c81 100644 --- a/qlbm/lattice/lattices/base.py +++ b/qlbm/lattice/lattices/base.py @@ -31,22 +31,16 @@ class Lattice(ABC): Attribute Summary =========================== ====================================================================== :attr:`num_dims` The number of dimensions of the lattice. - :attr:`num_gridpoints` A ``List[int]`` of the number of gridpoints of the lattice in each dimension. - **Important**\ : for easier compatibility with binary arithmetic, the number of gridpoints - specified in the input dicitionary is one larger than the one held in the ``Lattice``. - That is, for a ``16x64`` lattice, the ``num_gridpoints`` attribute will have the value ``[15, 63]``. - :attr:`num_grid_qubits` The total number of qubits required to encode the lattice grid. + :attr:`num_gridpoints` The number of gridpoints in each dimension. + :attr:`num_grid_qubits` The total number of qubits required to encode the grid. + :attr:`num_velocities` The number of discrete velocities in each dimension. :attr:`num_velocity_qubits` The total number of qubits required to encode the velocity discretization of the lattice. - :attr:`num_ancilla_qubits` The total number of ancilla (non-velocity, non-grid) qubits required for the quantum circuit to simulate this lattice. + :attr:`num_ancilla_qubits` The total number of ancllary qubits required for the quantum circuit to simulate this lattice. :attr:`num_total_qubits` The total number of qubits required for the quantum circuit to simulate the lattice. - This is the sum of the number of grid, velocity, and ancilla qubits. - :attr:`registers` A ``Tuple[qiskit.QuantumRegister, ...]`` that holds registers responsible for specific operations of the QLBM algorithm. - :attr:`circuit` An empty ``qiskit.QuantumCircuit`` with labeled registers that quantum components use as a base. - Each quantum component that is parameterized by a ``Lattice`` makes a copy of this quantum circuit - to which it appends its designated logic. - :attr:`blocks` A ``Dict[str, List[Block]]`` that contains all of the :class:`.Block`\ s encoding the solid geometry of the lattice. - The key of the dictionary is the specific kind of boundary condition of the obstacle (i.e., ``"bounceback"`` or ``"specular"``). - :attr:`logger` The performance logger, by default ``getLogger("qlbm")``. + :attr:`registers` The qubit registers of the quantum algorithm. + :attr:`circuit` The blueprint quantum circuit for all components of the algorithm. + :attr:`shapes` A list of the solid geometry objects. + :attr:`logger` The performance logger. =========================== ====================================================================== A lattice can be constructed from from either an input file or a Python dictionary. @@ -81,13 +75,71 @@ class Lattice(ABC): """ num_dims: int + """ + The number of dimensions of the system. + """ num_gridpoints: List[int] + """ + The number of gridpoints of the lattice in each dimension. + + .. warning:: + + For easier compatibility with binary arithmetic, the number of gridpoints + specified in the input dictionary is one larger than the one held in the :class:`.Lattice` s. + That is, for a :math:`16 \\times 64` lattice, the ``num_gridpoints`` attribute will have the value ``[15, 63]``. + """ + num_velocities: List[int] + """ + The number of velocities in each dimension. This will be refactored in the future to support :math:`D_dQ_q` discretizations. + + .. warning:: + + For easier compatibility with binary arithmetic, the number of velocities + specified in the input dictionary is one larger than the one held in the :class:`.Lattice` s. + If the numbers of discrete velocities are :math:`4` and :math:`2`, the ``num_velocities`` attribute will have the value ``[3, 1]``. + """ + + num_grid_qubits: int + """ + The number of qubits required to encode the grid. + """ + + num_velocity_qubits: int + """ + The number of qubits required to encode the velocity discretization of the lattice. + """ + + num_ancilla_qubits: int + """ + The number of ancilla (non-velocity, non-grid) qubits required for the quantum circuit to simulate this lattice. + """ + num_total_qubits: int + """ + The total number of qubits required for the quantum circuit to simulate the lattice. This is the sum of the number of grid, velocity, and ancilla qubits. + """ + registers: Tuple[QuantumRegister, ...] - logger: Logger + """ + A tuple that holds registers responsible for specific operations of the QLBM algorithm. + """ circuit: QuantumCircuit - blocks: Dict[str, List[Block]] + """ + An empty ``qiskit.QuantumCircuit`` with labeled registers that quantum components use as a base. + Each quantum component that is parameterized by a :class:`.Lattice` makes a copy of this quantum circuit + to which it appends its designated logic. + """ + + shapes: Dict[str, List[Shape]] + """ + Contains all of the :class:`.Shape`s encoding the solid geometry of the lattice. The key of the dictionary is the specific kind of boundary condition of the obstacle (i.e., ``"bounceback"`` or ``"specular"``). + """ + + logger: Logger + """ + The performance logger, by default ``getLogger("qlbm")``. + """ def __init__( self, @@ -100,7 +152,7 @@ def __init__( def parse_input_data( self, lattice_data: str | Dict, # type: ignore - ) -> Tuple[List[int], List[int], Dict[str, List[Block]]]: + ) -> Tuple[List[int], List[int], Dict[str, List[Shape]]]: r""" Parses the lattice input data, provided in either a file path or a dictionary. @@ -112,11 +164,11 @@ def parse_input_data( Returns ------- - Tuple[List[int], List[int], Dict[str, List[Block]]] + Tuple[List[int], List[int], Dict[str, List[Shape]]] A tuple containing (i) a list of the number of gridpoints per dimension, (ii) a list of the number of velicities per dimension, - and (iii) a dictionary containing the solid :class:`.Block`\ s. + and (iii) a dictionary containing the solid :class:`.Shape`\ s. The key of the dictionary is the specific kind of boundary condition of the obstacle (i.e., ``"bounceback"`` or ``"specular"``). @@ -341,8 +393,8 @@ def to_json(self) -> str: lattice_dict["geometry"] = flatten( [ - [block.to_dict() for block in self.blocks[boundary_type]] - for boundary_type in self.blocks + [block.to_dict() for block in self.shapes[boundary_type]] + for boundary_type in self.shapes ] ) diff --git a/qlbm/lattice/lattices/collisionless_lattice.py b/qlbm/lattice/lattices/collisionless_lattice.py index 0c04974..6b3ebfb 100644 --- a/qlbm/lattice/lattices/collisionless_lattice.py +++ b/qlbm/lattice/lattices/collisionless_lattice.py @@ -23,7 +23,7 @@ class CollisionlessLattice(Lattice): :attr:`num_dims` The number of dimensions of the lattice. :attr:`num_gridpoints` A ``List[int]`` of the number of gridpoints of the lattice in each dimension. **Important**\ : for easier compatibility with binary arithmetic, the number of gridpoints - specified in the input dicitionary is one larger than the one held in the ``Lattice``. + specified in the input dictionary is one larger than the one held in the ``Lattice``. That is, for a ``16x64`` lattice, the ``num_gridpoints`` attribute will have the value ``[15, 63]``. :attr:`num_grid_qubits` The total number of qubits required to encode the lattice grid. :attr:`num_velocity_qubits` The total number of qubits required to encode the velocity discretization of the lattice. @@ -160,7 +160,7 @@ def __init__( self.num_dims = len(dimensions) self.num_gridpoints = dimensions self.num_velocities = velocities - self.blocks: Dict[str, List[Block]] = blocks + self.blocks: Dict[str, List[Block]] = blocks # type: ignore self.block_list: List[Block] = flatten(list(blocks.values())) self.num_comparator_qubits = 2 * (self.num_dims - 1) self.num_obstacle_qubits = self.__num_obstacle_qubits() @@ -514,7 +514,7 @@ def __str__(self) -> str: def logger_name(self) -> str: gp_string = "" for c, gp in enumerate(self.num_gridpoints): - gp_string += f"{gp+1}" + gp_string += f"{gp + 1}" if c < len(self.num_gridpoints) - 1: gp_string += "x" return f"{self.num_dims}d-{gp_string}-{len(self.block_list)}-obstacle" diff --git a/qlbm/lattice/lattices/spacetime_lattice.py b/qlbm/lattice/lattices/spacetime_lattice.py index 266c87a..86f1e0d 100644 --- a/qlbm/lattice/lattices/spacetime_lattice.py +++ b/qlbm/lattice/lattices/spacetime_lattice.py @@ -123,7 +123,7 @@ def __init__( self.include_measurement_qubit = include_measurement_qubit self.use_volumetric_ops = use_volumetric_ops - self.num_gridpoints, self.num_velocities, self.blocks = self.parse_input_data( + self.num_gridpoints, self.num_velocities, self.shapes = self.parse_input_data( lattice_data ) # type: ignore self.num_dims = len(self.num_gridpoints) @@ -416,7 +416,7 @@ def is_inside_an_obstacle(self, gridpoint: Tuple[int, ...]) -> bool: """ return any( block.contains_gridpoint(gridpoint) - for block in flatten(self.blocks.values()) + for block in flatten(self.shapes.values()) ) @override From c0d9c5ad727fdc26c2ef14a0d4f0e277e658eeb6 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 7 Jul 2025 14:16:53 +0200 Subject: [PATCH 26/92] Update base result documentation --- qlbm/infra/result/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qlbm/infra/result/base.py b/qlbm/infra/result/base.py index d4b258b..ff3b39d 100644 --- a/qlbm/infra/result/base.py +++ b/qlbm/infra/result/base.py @@ -62,9 +62,9 @@ def visualize_geometry(self): Creates ``stl`` files for each block in the lattice. Output files are formatted as ``output_dir/paraview_dir/cube_.stl``. - The output is created through the :class:`.Block`'s :meth:`.Block.stl_mesh` method. + The output is created through the :class:`.Shape`'s :meth:`.Shape.stl_mesh` method. """ - for c, shape in enumerate(flatten(self.lattice.blocks.values())): + for c, shape in enumerate(flatten(self.lattice.shapes.values())): shape.stl_mesh().save(f"{self.paraview_dir}/{shape.name()}_{c}.stl") def save_timestep_array( From 498de709ec1ca67c9e533cbe020c8c3059aacb7a Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 7 Jul 2025 14:17:19 +0200 Subject: [PATCH 27/92] Update STQBM components documentation --- qlbm/components/spacetime/reflection/pointwise.py | 14 +++++++------- qlbm/components/spacetime/spacetime.py | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/qlbm/components/spacetime/reflection/pointwise.py b/qlbm/components/spacetime/reflection/pointwise.py index fdd6fde..8dad761 100644 --- a/qlbm/components/spacetime/reflection/pointwise.py +++ b/qlbm/components/spacetime/reflection/pointwise.py @@ -9,7 +9,7 @@ from qlbm.components.base import SpaceTimeOperator from qlbm.components.common.primitives import MCSwap -from qlbm.lattice.geometry.shapes.block import Block +from qlbm.lattice.geometry.shapes.base import Shape, SpaceTimeShape from qlbm.lattice.lattices.spacetime_lattice import SpaceTimeLattice from qlbm.lattice.spacetime.properties_base import LatticeDiscretization from qlbm.tools.exceptions import CircuitException @@ -25,7 +25,7 @@ class PointWiseSpaceTimeReflectionOperator(SpaceTimeOperator): ============================ ====================================================================== :attr:`lattice` The :class:`.SpaceTimeLattice` based on which the properties of the operator are inferred. :attr:`timestep` The timestep for to which to perform reflection. - :attr:`blocks` A list of :class:`.Block` objects for which to generate the BB boundary condition circuits. + :attr:`shapes` A list of :class:`.Shape` objects for which to generate the BB boundary condition circuits. :attr:`filter_inside_blocks` A ``bool`` that, when enabled, disregards operations that would have happened inside blocks. :attr:`logger` The performance logger, by default ``getLogger("qlbm")``. ============================ ====================================================================== @@ -36,7 +36,7 @@ def __init__( self, lattice: SpaceTimeLattice, timestep: int, - blocks: List[Block], + shapes: List[Shape], filter_inside_blocks: bool = True, logger: Logger = getLogger("qlbm"), ) -> None: @@ -48,7 +48,7 @@ def __init__( f"Invalid time step {timestep}, select a value between 1 and {lattice.num_timesteps}" ) - self.blocks = blocks + self.shapes = cast(List[SpaceTimeShape], shapes) self.filter_inside_blocks = filter_inside_blocks self.logger.info(f"Creating circuit {str(self)}...") @@ -71,7 +71,7 @@ def create_circuit(self) -> QuantumCircuit: def __create_circuit_d1q2(self) -> QuantumCircuit: circuit = self.lattice.circuit.copy() - for block in self.blocks: + for block in self.shapes: for reflection_data in block.get_spacetime_reflection_data_d1q2( self.lattice.properties, self.timestep ): @@ -116,7 +116,7 @@ def __create_circuit_d1q2(self) -> QuantumCircuit: def __create_circuit_d2q4(self) -> QuantumCircuit: circuit = self.lattice.circuit.copy() - for block in self.blocks: + for block in self.shapes: reflection_data_points = block.get_spacetime_reflection_data_d2q4( self.lattice.properties, self.timestep ) @@ -169,4 +169,4 @@ def __create_circuit_d2q4(self) -> QuantumCircuit: @override def __str__(self) -> str: - return f"[PointWiseSpaceTimeReflectionOperator for lattice {self.lattice}, blocks {self.blocks}, filtered {self.filter_inside_blocks}]" + return f"[PointWiseSpaceTimeReflectionOperator for lattice {self.lattice}, blocks {self.shapes}, filtered {self.filter_inside_blocks}]" diff --git a/qlbm/components/spacetime/spacetime.py b/qlbm/components/spacetime/spacetime.py index 508d8c8..145bb02 100644 --- a/qlbm/components/spacetime/spacetime.py +++ b/qlbm/components/spacetime/spacetime.py @@ -81,7 +81,7 @@ def create_circuit(self) -> QuantumCircuit: PointWiseSpaceTimeReflectionOperator( self.lattice, timestep, - self.lattice.blocks["bounceback"], + self.lattice.shapes["bounceback"], self.filter_inside_blocks, self.logger, ).circuit, From 8f87fa1b1405f9f35097464b1d4a9eede822bd7e Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Wed, 9 Jul 2025 11:58:20 +0200 Subject: [PATCH 28/92] Add LQLGA lattice --- qlbm/lattice/lattices/lqlga_lattice.py | 142 +++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 qlbm/lattice/lattices/lqlga_lattice.py diff --git a/qlbm/lattice/lattices/lqlga_lattice.py b/qlbm/lattice/lattices/lqlga_lattice.py new file mode 100644 index 0000000..ab3a637 --- /dev/null +++ b/qlbm/lattice/lattices/lqlga_lattice.py @@ -0,0 +1,142 @@ +from itertools import product +from math import prod +from typing import Dict, List, Tuple, cast, override + +from qiskit import QuantumCircuit, QuantumRegister + +from qlbm.lattice.geometry.shapes.base import Shape +from qlbm.lattice.lattices.base import Lattice +from qlbm.lattice.spacetime.properties_base import ( + LatticeDiscretization, + LatticeDiscretizationProperties, +) +from qlbm.tools.exceptions import LatticeException + + +class LQLGALattice(Lattice): + discretization: LatticeDiscretization + + num_gridpoints: List[int] + + shapes: Dict[str, List[Shape]] + + def __init__(self, lattice_data, logger=...): + super().__init__(lattice_data, logger) + + self.num_gridpoints, self.num_velocities, self.shapes = self.parse_input_data( + lattice_data + ) # type: ignore + self.num_dims = len(self.num_gridpoints) + self.discretization = self.__get_discretization() + self.num_velocities_per_point = ( + LatticeDiscretizationProperties.get_num_velocities(self.discretization) + ) + + self.num_base_qubits = ( + prod(map(lambda x: x + 1, self.num_gridpoints)) + * self.num_velocities_per_point + ) + + self.num_total_qubits = self.num_base_qubits + + self.registers = self.get_registers() + + self.circuit = QuantumCircuit(*self.registers) + + def __get_discretization(self) -> LatticeDiscretization: + if self.num_dims == 1: + if self.num_velocities[0] == 1: + return LatticeDiscretization.D1Q2 + raise LatticeException( + f"Unsupported number of velocities for 1D: {self.num_velocities[0] + 1}. Only D1Q2 is supported at the moment." + ) + + if self.num_dims == 2: + if self.num_velocities[0] == 1 and self.num_velocities[1] == 1: + return LatticeDiscretization.D2Q4 + raise LatticeException( + f"Unsupported number of velocities for 2D: {(self.num_velocities[0] + 1, self.num_velocities[1] + 1)}. Only D2Q4 is supported at the moment." + ) + + if self.num_dims == 3: + if ( + self.num_velocities[0] == 1 + and self.num_velocities[1] == 1 + and self.num_velocities[2] == 1 + ): + return LatticeDiscretization.D3Q6 + raise LatticeException( + f"Unsupported number of velocities for 3D: {(self.num_velocities[0] + 1, self.num_velocities[1] + 1)}. Only D3Q6 is supported at the moment." + ) + + raise LatticeException("Only 1-3D discretizations are currently available.") + + @override + def get_registers(self) -> Tuple[List[QuantumRegister], ...]: + velocity_registers = [ + QuantumRegister( + LatticeDiscretizationProperties.get_num_velocities(self.discretization), + name=rf"v^{{{gp_tuple}}}", + ) + for gp_tuple in list( + product(*[range(ng + 1) for ng in self.num_gridpoints]) + ) + ] + + return velocity_registers + + def gridpoint_index_tuple(self, gridpoint: Tuple[int, ...]) -> int: + flat_index = 0 + multiplier = 1 + num_dims = len(self.num_gridpoints) + + for dim in reversed(range(num_dims)): + flat_index += gridpoint[dim] * multiplier + multiplier *= self.num_gridpoints[dim] + 1 + + return flat_index + + def gridpoint_index_flat(self, gridpoint: int) -> Tuple[int, ...]: + if ( + gridpoint < 0 + or gridpoint >= self.num_total_qubits // self.num_velocities_per_point + ): + raise LatticeException( + f"Gridpoint {gridpoint} is out of bounds for the lattice with {self.num_total_qubits // self.num_velocities_per_point} gridpoints." + ) + indices = [] + for size in reversed(self.num_gridpoints): + indices.append(gridpoint % (size + 1)) + gridpoint //= size + 1 + return cast(Tuple[int, ...], tuple(reversed(indices))) + + def velocity_index_flat(self, gridpoint: int, velocity: int) -> int: + if velocity < 0 or velocity >= self.num_velocities_per_point: + raise LatticeException( + f"Velocity {velocity} is out of bounds for the lattice with {self.num_velocities_per_point} velocities per point." + ) + + if ( + gridpoint < 0 + or gridpoint >= self.num_total_qubits // self.num_velocities_per_point + ): + raise LatticeException( + f"Gridpoint {gridpoint} is out of bounds for the lattice with {self.num_total_qubits // self.num_velocities_per_point} gridpoints." + ) + return gridpoint * self.num_velocities_per_point + velocity + + def velocity_index_tuple(self, gridpoint: Tuple[int, ...], velocity: int) -> int: + if velocity < 0 or velocity >= self.num_velocities_per_point: + raise LatticeException( + f"Velocity {velocity} is out of bounds for the lattice with {self.num_velocities_per_point} velocities per point." + ) + return self.gridpoint_index_tuple(gridpoint) + velocity + + @override + def logger_name(self) -> str: + gp_string = "" + for c, gp in enumerate(self.num_gridpoints): + gp_string += f"{gp + 1}" + if c < len(self.num_gridpoints) - 1: + gp_string += "x" + return f"lqlga-d{self.num_dims}-q{LatticeDiscretizationProperties.get_num_velocities(self.discretization)}-{gp_string}" From f19efbae83c1cdbf554b8bc5803910290f7ca33a Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Wed, 9 Jul 2025 11:59:01 +0200 Subject: [PATCH 29/92] Refactor eqc collision utilities for reuse --- .../spacetime/collision/__init__.py | 8 +- .../spacetime/collision/d2q4_old.py | 159 +++++++++++++ .../spacetime/collision/eqc_collision.py | 223 +----------------- .../spacetime/collision/eqc_permutation.py | 122 ---------- .../spacetime/collision/eqc_redistribution.py | 65 ----- qlbm/lattice/eqc/__init__.py | 0 .../eqc/eqc.py} | 71 +----- qlbm/lattice/eqc/eqc_generator.py | 65 +++++ 8 files changed, 246 insertions(+), 467 deletions(-) create mode 100644 qlbm/components/spacetime/collision/d2q4_old.py delete mode 100644 qlbm/components/spacetime/collision/eqc_permutation.py delete mode 100644 qlbm/components/spacetime/collision/eqc_redistribution.py create mode 100644 qlbm/lattice/eqc/__init__.py rename qlbm/{components/spacetime/collision/eqc_discretizations.py => lattice/eqc/eqc.py} (70%) create mode 100644 qlbm/lattice/eqc/eqc_generator.py diff --git a/qlbm/components/spacetime/collision/__init__.py b/qlbm/components/spacetime/collision/__init__.py index 553e792..7f5a082 100644 --- a/qlbm/components/spacetime/collision/__init__.py +++ b/qlbm/components/spacetime/collision/__init__.py @@ -1,13 +1,13 @@ """Classes implementing the logic of the collision operator in the Space-Time Data encoding.""" -from .eqc_collision import SpaceTimeCollisionOperator -from .eqc_discretizations import ( - EquivalenceClass, +from ....lattice.eqc.eqc import EquivalenceClass +from .d2q4_old import SpaceTimeD2Q4CollisionOperator +from ....lattice.eqc.eqc_generator import ( EquivalenceClassGenerator, ) __all__ = [ - "SpaceTimeCollisionOperator", + "SpaceTimeD2Q4CollisionOperator", "EquivalenceClass", "EquivalenceClassGenerator", ] diff --git a/qlbm/components/spacetime/collision/d2q4_old.py b/qlbm/components/spacetime/collision/d2q4_old.py new file mode 100644 index 0000000..89d4eed --- /dev/null +++ b/qlbm/components/spacetime/collision/d2q4_old.py @@ -0,0 +1,159 @@ +from logging import Logger, getLogger +from math import pi +from time import perf_counter_ns + +from qiskit import QuantumCircuit +from qiskit.circuit import Gate +from qiskit.circuit.library import MCMTGate, RYGate +from typing_extensions import override + +from qlbm.components.base import SpaceTimeOperator +from qlbm.lattice.lattices.spacetime_lattice import SpaceTimeLattice + + +class SpaceTimeD2Q4CollisionOperator(SpaceTimeOperator): + r"""An operator that performs collision part of the :class:`.SpaceTimeQLBM` algorithm. + + Collision is a local operation that is performed simultaneously on all velocity qubits corresponding to a grid location. + In practice, this means the same circuit is repeated across all "local" qubit register chunks. + Collision can be understood as follows: + + #. For each group of qubits, the states encoding velocities belonging to a particular equivalence class are first isolated with a series of :math:`X` and :math:`CX` gates. This leaves qubits not affected by the rotation in :math:`\ket{1}^{\otimes n_v-1}` state. + #. A rotation gate is applied to the qubit(s) relevant to the equivalence class shift, controlled on the qubits set in the previous step. + #. The operation performed in Step 1 is undone. + + The register setup of the :class:`.SpaceTimeLattice` is such that following each + time step, an additional "layer" neighboring velocity qubits can be discarded, + since the information they encode can never reach the relative origin in the remaining number of time steps. + As such, the complexity of the collision operator decreases with the number of steps (still) to be simulated. + For an in-depth mathematical explanation of the procedure, consult pages 11-15 of :cite:t:`spacetime`. + + + ========================= ====================================================================== + Attribute Summary + ========================= ====================================================================== + :attr:`lattice` The :class:`.SpaceTimeLattice` based on which the properties of the operator are inferred. + :attr:`timestep` The time step for which to perform streaming. + :attr:`gate_to_apply` The gate to apply to the velocities matching equivalence classes. Defaults to :math:`R_y(\frac{\pi}{2})`. + :attr:`logger` The performance logger, by default ``getLogger("qlbm")``. + ========================= ====================================================================== + + + Example usage: + + .. plot:: + :include-source: + + from qlbm.components.spacetime import SpaceTimeCollisionOperator + from qlbm.lattice import SpaceTimeLattice + + # Build an example lattice + lattice = SpaceTimeLattice( + num_timesteps=1, + lattice_data={ + "lattice": {"dim": {"x": 4, "y": 8}, "velocities": {"x": 2, "y": 2}}, + "geometry": [], + }, + ) + + # Draw the collision operator for 1 time step + SpaceTimeCollisionOperator(lattice=lattice, timestep=1).draw("mpl") + """ + + def __init__( + self, + lattice: SpaceTimeLattice, + timestep: int, + gate_to_apply: Gate = RYGate(pi / 2), + logger: Logger = getLogger("qlbm"), + ) -> None: + super().__init__(lattice, logger) + self.lattice = lattice + self.timestep = timestep + self.gate_to_apply = gate_to_apply + + 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 = self.lattice.circuit.copy() + + collision_circuit = self.local_collision_circuit(reset_state=False) + collision_circuit.compose( + MCMTGate( + self.gate_to_apply, + self.lattice.properties.get_num_velocities_per_point() - 1, + 1, + ), + qubits=list( + range(1, self.lattice.properties.get_num_velocities_per_point()) + ) + + [0], + inplace=True, + ) + + # Create the local circuit once + collision_circuit.compose( + self.local_collision_circuit(reset_state=True), inplace=True + ) + + # Append the collision circuit at each step + for velocity_qubit_indices in range( + self.lattice.properties.get_num_grid_qubits(), + self.lattice.properties.get_num_grid_qubits() + + self.lattice.properties.get_num_velocity_qubits(self.timestep), + self.lattice.properties.get_num_velocities_per_point(), + ): + circuit.compose( + collision_circuit, + inplace=True, + qubits=range(velocity_qubit_indices, velocity_qubit_indices + 4), + ) + return circuit + + def local_collision_circuit(self, reset_state: bool) -> QuantumCircuit: + """ + Sets the state for the collision circuit one local gridpoint and its corresponding velocity qubits. + + Parameters + ---------- + reset_state : bool + Whether the circuit sets or re-sets the state past the application of the rotation operator. + + Returns + ------- + QuantumCircuit + The collision (re-)set circuit for a single gridpoint. + """ + circuit = QuantumCircuit(self.lattice.properties.get_num_velocities_per_point()) + + # circuit.cx(1, 2) + # circuit.cx(0, 1) + # circuit.cx(0, 3) + + # return circuit if not reset_state else circuit.inverse() + + if not reset_state: + circuit.cx(control_qubit=0, target_qubit=2) + circuit.x(0) + circuit.cx(control_qubit=1, target_qubit=3) + circuit.cx(control_qubit=0, target_qubit=1) + circuit.x(list(range(circuit.num_qubits))) + # Same circuit, but mirrored + else: + circuit.x(list(range(circuit.num_qubits))) + circuit.cx(control_qubit=0, target_qubit=1) + circuit.cx(control_qubit=1, target_qubit=3) + circuit.x(0) + circuit.cx(control_qubit=0, target_qubit=2) + + return circuit + + @override + def __str__(self) -> str: + return "Space Time Collision Operator" \ No newline at end of file diff --git a/qlbm/components/spacetime/collision/eqc_collision.py b/qlbm/components/spacetime/collision/eqc_collision.py index f25dd41..26bda60 100644 --- a/qlbm/components/spacetime/collision/eqc_collision.py +++ b/qlbm/components/spacetime/collision/eqc_collision.py @@ -1,174 +1,26 @@ """Collision operators for the :class:`.SpaceTimeQLBM` algorithm :cite:`spacetime`.""" from logging import Logger, getLogger -from math import pi from time import perf_counter_ns from qiskit import QuantumCircuit -from qiskit.circuit import Gate -from qiskit.circuit.library import MCMTGate, RYGate from typing_extensions import override from qlbm.components.base import SpaceTimeOperator -from qlbm.components.spacetime.collision.eqc_discretizations import ( - EquivalenceClass, - EquivalenceClassGenerator, +from qlbm.components.common.cbse_collision.cbse_collision import EQCCollisionOperator +from qlbm.components.common.cbse_collision.cbse_permutation import ( + EQCPermutation, +) +from qlbm.components.common.cbse_collision.cbse_redistribution import ( + EQCRedistribution, ) -from qlbm.components.spacetime.collision.eqc_permutation import SpaceTimeEQCPermutation -from qlbm.components.spacetime.collision.eqc_redistribution import ( - SpaceTimeEQCRedistribution, +from qlbm.lattice.eqc.eqc import EquivalenceClass +from qlbm.lattice.eqc.eqc_generator import ( + EquivalenceClassGenerator, ) from qlbm.lattice.lattices.spacetime_lattice import SpaceTimeLattice -class SpaceTimeCollisionOperator(SpaceTimeOperator): - r"""An operator that performs collision part of the :class:`.SpaceTimeQLBM` algorithm. - - Collision is a local operation that is performed simultaneously on all velocity qubits corresponding to a grid location. - In practice, this means the same circuit is repeated across all "local" qubit register chunks. - Collision can be understood as follows: - - #. For each group of qubits, the states encoding velocities belonging to a particular equivalence class are first isolated with a series of :math:`X` and :math:`CX` gates. This leaves qubits not affected by the rotation in :math:`\ket{1}^{\otimes n_v-1}` state. - #. A rotation gate is applied to the qubit(s) relevant to the equivalence class shift, controlled on the qubits set in the previous step. - #. The operation performed in Step 1 is undone. - - The register setup of the :class:`.SpaceTimeLattice` is such that following each - time step, an additional "layer" neighboring velocity qubits can be discarded, - since the information they encode can never reach the relative origin in the remaining number of time steps. - As such, the complexity of the collision operator decreases with the number of steps (still) to be simulated. - For an in-depth mathematical explanation of the procedure, consult pages 11-15 of :cite:t:`spacetime`. - - - ========================= ====================================================================== - Attribute Summary - ========================= ====================================================================== - :attr:`lattice` The :class:`.SpaceTimeLattice` based on which the properties of the operator are inferred. - :attr:`timestep` The time step for which to perform streaming. - :attr:`gate_to_apply` The gate to apply to the velocities matching equivalence classes. Defaults to :math:`R_y(\frac{\pi}{2})`. - :attr:`logger` The performance logger, by default ``getLogger("qlbm")``. - ========================= ====================================================================== - - - Example usage: - - .. plot:: - :include-source: - - from qlbm.components.spacetime import SpaceTimeCollisionOperator - from qlbm.lattice import SpaceTimeLattice - - # Build an example lattice - lattice = SpaceTimeLattice( - num_timesteps=1, - lattice_data={ - "lattice": {"dim": {"x": 4, "y": 8}, "velocities": {"x": 2, "y": 2}}, - "geometry": [], - }, - ) - - # Draw the collision operator for 1 time step - SpaceTimeCollisionOperator(lattice=lattice, timestep=1).draw("mpl") - """ - - def __init__( - self, - lattice: SpaceTimeLattice, - timestep: int, - gate_to_apply: Gate = RYGate(pi / 2), - logger: Logger = getLogger("qlbm"), - ) -> None: - super().__init__(lattice, logger) - self.lattice = lattice - self.timestep = timestep - self.gate_to_apply = gate_to_apply - - 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 = self.lattice.circuit.copy() - - collision_circuit = self.local_collision_circuit(reset_state=False) - collision_circuit.compose( - MCMTGate( - self.gate_to_apply, - self.lattice.properties.get_num_velocities_per_point() - 1, - 1, - ), - qubits=list( - range(1, self.lattice.properties.get_num_velocities_per_point()) - ) - + [0], - inplace=True, - ) - - # Create the local circuit once - collision_circuit.compose( - self.local_collision_circuit(reset_state=True), inplace=True - ) - - # Append the collision circuit at each step - for velocity_qubit_indices in range( - self.lattice.properties.get_num_grid_qubits(), - self.lattice.properties.get_num_grid_qubits() - + self.lattice.properties.get_num_velocity_qubits(self.timestep), - self.lattice.properties.get_num_velocities_per_point(), - ): - circuit.compose( - collision_circuit, - inplace=True, - qubits=range(velocity_qubit_indices, velocity_qubit_indices + 4), - ) - return circuit - - def local_collision_circuit(self, reset_state: bool) -> QuantumCircuit: - """ - Sets the state for the collision circuit one local gridpoint and its corresponding velocity qubits. - - Parameters - ---------- - reset_state : bool - Whether the circuit sets or re-sets the state past the application of the rotation operator. - - Returns - ------- - QuantumCircuit - The collision (re-)set circuit for a single gridpoint. - """ - circuit = QuantumCircuit(self.lattice.properties.get_num_velocities_per_point()) - - # circuit.cx(1, 2) - # circuit.cx(0, 1) - # circuit.cx(0, 3) - - # return circuit if not reset_state else circuit.inverse() - - if not reset_state: - circuit.cx(control_qubit=0, target_qubit=2) - circuit.x(0) - circuit.cx(control_qubit=1, target_qubit=3) - circuit.cx(control_qubit=0, target_qubit=1) - circuit.x(list(range(circuit.num_qubits))) - # Same circuit, but mirrored - else: - circuit.x(list(range(circuit.num_qubits))) - circuit.cx(control_qubit=0, target_qubit=1) - circuit.cx(control_qubit=1, target_qubit=3) - circuit.x(0) - circuit.cx(control_qubit=0, target_qubit=2) - - return circuit - - @override - def __str__(self) -> str: - return "Space Time Collision Operator" - - class GenericSpaceTimeCollisionOperator(SpaceTimeOperator): """ A generic space-time collision operator that can be used to apply any gate to the velocities of a grid location. @@ -201,7 +53,9 @@ def __init__( @override def create_circuit(self) -> QuantumCircuit: - local_collision_circuit = self.create_circuit_one_register() + local_collision_circuit = EQCCollisionOperator( + self.lattice.properties.get_discretization() + ).circuit circuit = self.lattice.circuit.copy() for velocity_qubit_indices in range( @@ -221,59 +75,6 @@ def create_circuit(self) -> QuantumCircuit: ) return circuit - def create_circuit_one_register(self) -> QuantumCircuit: - """ - Applies the collision operator of all equivalence classes onto one velocity register. - - Returns - ------- - QuantumCircuit - The circuit performing the complete collision operator. - """ - circuit = QuantumCircuit(self.lattice.properties.get_num_velocities_per_point()) - - for eqc in EquivalenceClassGenerator( - self.lattice.properties.get_discretization() - ).generate_equivalence_classes(): - circuit.compose(self.create_circuit_one_eqc(eqc), inplace=True) - - return circuit - - def create_circuit_one_eqc( - self, equivalence_class: EquivalenceClass - ) -> QuantumCircuit: - """ - Creates the PRP-based collision operator for one equivalence class. - - Parameters - ---------- - equivalence_class : EquivalenceClass - The equivalence class to collide. - - Returns - ------- - QuantumCircuit - The circuit performing the collision. - """ - circuit = QuantumCircuit(self.lattice.properties.get_num_velocities_per_point()) - circuit.compose( - SpaceTimeEQCPermutation(equivalence_class, logger=self.logger).circuit, - inplace=True, - ) - - circuit.compose( - SpaceTimeEQCRedistribution(equivalence_class, logger=self.logger).circuit, - inplace=True, - ) - circuit.compose( - SpaceTimeEQCPermutation( - equivalence_class, inverse=True, logger=self.logger - ).circuit, - inplace=True, - ) - - return circuit - @override def __str__(self) -> str: return f"[GenericSpaceTimeCollisionOperator for discretization {self.lattice.properties.get_discretization()}]" diff --git a/qlbm/components/spacetime/collision/eqc_permutation.py b/qlbm/components/spacetime/collision/eqc_permutation.py deleted file mode 100644 index 6a364fa..0000000 --- a/qlbm/components/spacetime/collision/eqc_permutation.py +++ /dev/null @@ -1,122 +0,0 @@ -"""Permutation step in QLGA collision.""" - -from logging import Logger, getLogger -from typing import override - -from qiskit import QuantumCircuit - -from qlbm.components.base import LBMPrimitive -from qlbm.components.spacetime.collision.eqc_discretizations import EquivalenceClass -from qlbm.lattice.spacetime.properties_base import LatticeDiscretization -from qlbm.tools.exceptions import CircuitException - - -class SpaceTimeEQCPermutation(LBMPrimitive): - """Permutes states belonging to equivalence classes onto pre-determined basis states. - - Precedes redistribution. - - WIP. - """ - - def __init__( - self, - equivalence_class: EquivalenceClass, - inverse: bool = False, - logger: Logger = getLogger("qlbm"), - ): - super().__init__(logger) - self.equivalence_class = equivalence_class - self.inverse = inverse - - self.circuit = self.create_circuit() - - @override - def create_circuit(self): - if self.equivalence_class.discretization == LatticeDiscretization.D2Q4: - return self.__create_circuit_d2q4() - elif self.equivalence_class.discretization == LatticeDiscretization.D3Q6: - return self.__create_circuit_d3q6() - else: - raise CircuitException( - f"Collision not yet supported for discretization {self.equivalence_class.discretization}." - ) - - def __create_circuit_d2q4(self): - circuit = QuantumCircuit(4) - - if not self.inverse: - circuit.cx(1, 2) - circuit.cx(0, 1) - circuit.cx(0, 3) - else: - circuit.cx(0, 3) - circuit.cx(0, 1) - circuit.cx(1, 2) - - return circuit - - def __create_circuit_d3q6(self): - circuit = QuantumCircuit(6) - - match self.equivalence_class.id(): - case (2, [0, 0, 0]): - circuit.cx(2, 3) - circuit.cx(5, 4) - - circuit.cx(1, 2) - circuit.cx(1, 3) - circuit.cx(1, 5) - - circuit.cx(0, 2) - circuit.cx(0, 4) - circuit.cx(0, 5) - case (4, [0, 0, 0]): - circuit.ccx(4, 5, 3) - circuit.ccx(0, 2, 4) - circuit.ccx(0, 1, 2) - circuit.ccx(0, 1, 5) - circuit.x(1) - circuit.cx(1, 0) - case (3, [1, 0, 0]): - circuit.cx(0, 3) - circuit.cx(1, 2) - circuit.ccx(0, 5, 4) - circuit.cx(1, 5) - circuit.swap(0, 1) - case (3, [-1, 0, 0]): - circuit.cx(1, 2) - circuit.cx(5, 4) - circuit.cx(1, 5) - circuit.x(0) - circuit.swap(0, 1) - case (3, [0, 1, 0]): - circuit.cx(0, 2) - circuit.cx(5, 3) - circuit.cx(0, 5) - circuit.x(4) - case (3, [0, -1, 0]): - circuit.cx(5, 3) - circuit.cx(0, 2) - circuit.cx(0, 5) - circuit.x(1) - case (3, [0, 0, 1]): - circuit.cx(1, 3) - circuit.cx(0, 1) - circuit.cx(0, 4) - circuit.x(5) - case (3, [0, 0, -1]): - circuit.cx(0, 1) - circuit.cx(4, 3) - circuit.cx(0, 4) - circuit.x(2) - case _: - raise CircuitException( - f"Collision not yet supported for discretization {self.equivalence_class.discretization} and equivalence class {self.equivalence_class.id()}." - ) - - return circuit if not self.inverse else circuit.inverse() - - @override - def __str__(self) -> str: - return f"[SpaceTimeEQCPermutation for eqc {self.equivalence_class}]" diff --git a/qlbm/components/spacetime/collision/eqc_redistribution.py b/qlbm/components/spacetime/collision/eqc_redistribution.py deleted file mode 100644 index 944e861..0000000 --- a/qlbm/components/spacetime/collision/eqc_redistribution.py +++ /dev/null @@ -1,65 +0,0 @@ -"""Redistribution step in QLGA collision.""" - -from logging import Logger, getLogger -from typing import override - -import numpy as np -from qiskit import QuantumCircuit -from qiskit.quantum_info import Operator - -from qlbm.components.base import LBMPrimitive -from qlbm.components.spacetime.collision.eqc_discretizations import EquivalenceClass -from qlbm.lattice.spacetime.properties_base import LatticeDiscretizationProperties - - -class SpaceTimeEQCRedistribution(LBMPrimitive): - """ - Redistributes the amplitudes of the states belonging to an equivalence class evenly across all other equivalent states. - - WIP. - """ - - def __init__( - self, equivalence_class: EquivalenceClass, logger: Logger = getLogger("qlbm") - ): - super().__init__(logger) - self.equivalence_class = equivalence_class - - self.circuit = self.create_circuit() - - @override - def create_circuit(self): - nv = LatticeDiscretizationProperties.get_num_velocities( - self.equivalence_class.discretization - ) - circuit = QuantumCircuit(nv) - - n = self.equivalence_class.size() - nq = np.ceil(np.log2(n)).astype(int) - - QFT = np.array( - [ - [np.exp(2j * np.pi * i * j / n) / np.sqrt(n) for j in range(n)] - for i in range(n) - ] - ) - - U = np.eye(2**nq, dtype=complex) - U[:n, :n] = QFT - op = Operator(U) - assert op.is_unitary() - - qft_block_circ = QuantumCircuit(nq) - qft_block_circ.append(op, list(range(nq))) - - circuit.compose( - qft_block_circ.control(nv - int(nq), label=rf"Coll({n}, {nq})"), - qubits=list(range(nv - 1, -1, -1)), - inplace=True, - ) - - return circuit.decompose() - - @override - def __str__(self): - return super().__str__() diff --git a/qlbm/lattice/eqc/__init__.py b/qlbm/lattice/eqc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/qlbm/components/spacetime/collision/eqc_discretizations.py b/qlbm/lattice/eqc/eqc.py similarity index 70% rename from qlbm/components/spacetime/collision/eqc_discretizations.py rename to qlbm/lattice/eqc/eqc.py index c61440a..371b863 100644 --- a/qlbm/components/spacetime/collision/eqc_discretizations.py +++ b/qlbm/lattice/eqc/eqc.py @@ -1,15 +1,11 @@ -"""Equivalence class utility functions. For a discussion of equivalence classes, we refer the reader to Section 4 of :cite:`spacetime2`.""" +from qlbm.lattice.spacetime.properties_base import LatticeDiscretization, LatticeDiscretizationProperties +from qlbm.tools.exceptions import LatticeException -from itertools import product -from typing import Dict, List, Set, Tuple, override import numpy as np -from qlbm.lattice.spacetime.properties_base import ( - LatticeDiscretization, - LatticeDiscretizationProperties, -) -from qlbm.tools.exceptions import LatticeException + +from typing import List, Set, Tuple, override class EquivalenceClass: @@ -121,7 +117,7 @@ def id(self) -> Tuple[int, List[float]]: The mass and momentum of the equivalence class. """ return (self.mass, self.momentum.tolist()) - + def get_bitstrings(self) -> List[str]: """ Returns the velocity configurations as bitstrings. @@ -146,59 +142,4 @@ def __eq__(self, value): @override def __hash__(self): - return hash((self.discretization, tuple(self.velocity_configurations))) - - -class EquivalenceClassGenerator: - """ - A class that generates equivalence classes for a given lattice discretization. - - .. list-table:: Constructor Attributes - :widths: 25 50 - :header-rows: 1 - - * - Attribute - - Description - * - :attr:`discretization` - - The :class:`.LatticeDiscretization` that the equivalence class belongs to. - - """ - - discretization: LatticeDiscretization - - def __init__(self, discretization): - self.discretization = discretization - - def generate_equivalence_classes(self) -> Set[EquivalenceClass]: - """ - Generates equivalence classes for the given lattice discretization. - - Returns - ------- - Set[EquivalenceClass] - All equivalence classes of the discretization. - """ - equivalence_classes: Dict = {} - for state in product( - [0, 1], - repeat=LatticeDiscretizationProperties.get_num_velocities( - self.discretization - ), - ): - velocity_vectors = LatticeDiscretizationProperties.get_velocity_vectors( - self.discretization - ) - state = np.array(state) # type: ignore - mass = np.sum(state) - momentum = np.sum(state[:, None] * velocity_vectors, axis=0) # type: ignore - - key = (mass, tuple(momentum)) - if key not in equivalence_classes: - equivalence_classes[key] = [] - equivalence_classes[key].append(state) - - return { - EquivalenceClass(self.discretization, set(tuple(cfg.tolist()) for cfg in v)) - for _, v in equivalence_classes.items() - if len(v) > 1 - } + return hash((self.discretization, tuple(self.velocity_configurations))) \ No newline at end of file diff --git a/qlbm/lattice/eqc/eqc_generator.py b/qlbm/lattice/eqc/eqc_generator.py new file mode 100644 index 0000000..00e9e59 --- /dev/null +++ b/qlbm/lattice/eqc/eqc_generator.py @@ -0,0 +1,65 @@ +from itertools import product +from typing import Dict, Set + +import numpy as np + +from qlbm.lattice.eqc.eqc import EquivalenceClass +from qlbm.lattice.spacetime.properties_base import ( + LatticeDiscretization, + LatticeDiscretizationProperties, +) + + +class EquivalenceClassGenerator: + """ + A class that generates equivalence classes for a given lattice discretization. + + .. list-table:: Constructor Attributes + :widths: 25 50 + :header-rows: 1 + + * - Attribute + - Description + * - :attr:`discretization` + - The :class:`.LatticeDiscretization` that the equivalence class belongs to. + + """ + + discretization: LatticeDiscretization + + def __init__(self, discretization): + self.discretization = discretization + + def generate_equivalence_classes(self) -> Set[EquivalenceClass]: + """ + Generates equivalence classes for the given lattice discretization. + + Returns + ------- + Set[EquivalenceClass] + All equivalence classes of the discretization. + """ + equivalence_classes: Dict = {} + for state in product( + [0, 1], + repeat=LatticeDiscretizationProperties.get_num_velocities( + self.discretization + ), + ): + velocity_vectors = LatticeDiscretizationProperties.get_velocity_vectors( + self.discretization + ) + state = np.array(state) # type: ignore + mass = np.sum(state) + momentum = np.sum(state[:, None] * velocity_vectors, axis=0) # type: ignore + + key = (mass, tuple(momentum)) + if key not in equivalence_classes: + equivalence_classes[key] = [] + equivalence_classes[key].append(state) + + return { + EquivalenceClass(self.discretization, set(tuple(cfg.tolist()) for cfg in v)) + for _, v in equivalence_classes.items() + if len(v) > 1 + } \ No newline at end of file From 6db39c1adb7d2f3fe7260eb685a54c5c05885148 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Wed, 9 Jul 2025 11:59:29 +0200 Subject: [PATCH 30/92] Add base LQLGA components --- qlbm/components/base.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/qlbm/components/base.py b/qlbm/components/base.py index 4d70670..fc3cf73 100644 --- a/qlbm/components/base.py +++ b/qlbm/components/base.py @@ -10,6 +10,7 @@ from typing_extensions import override from qlbm.lattice import CollisionlessLattice, Lattice +from qlbm.lattice.lattices.lqlga_lattice import LQLGALattice from qlbm.lattice.lattices.spacetime_lattice import SpaceTimeLattice @@ -237,6 +238,33 @@ def __init__( self.lattice = lattice +class LQLGAOperator(LBMOperator): + """ + Specialization of the :class:`.LBMOperator` operator class for the LQLGA algorithm. + + Specializaitons of this class infer their properties + based on a :class:`.LQLGALattice`. + + ========================= ====================================================================== + Attribute Summary + ========================= ====================================================================== + :attr:`circuit` The :class:`.qiskit.QuantumCircuit` of the operator. + :attr:`lattice` The :class:`.LQLGALattice` based on which the properties of the operator are inferred. + :attr:`logger` The performance logger, by default ``getLogger("qlbm")`` + ========================= ====================================================================== + """ + + lattice: LQLGALattice + + def __init__( + self, + lattice: LQLGALattice, + logger: Logger = getLogger("qlbm"), + ) -> None: + super().__init__(lattice, logger) + self.lattice = lattice + + class LBMAlgorithm(QuantumComponent): """ Base class for all end-to-end Quantum Boltzmann Methods. From 07e9f0b31566aa3b9ad84dab38a5981faca4f42e Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Wed, 9 Jul 2025 12:02:20 +0200 Subject: [PATCH 31/92] Adjust STQBM names --- qlbm/components/spacetime/__init__.py | 4 ++-- qlbm/components/spacetime/spacetime.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/qlbm/components/spacetime/__init__.py b/qlbm/components/spacetime/__init__.py index f743f4f..7aca5cc 100644 --- a/qlbm/components/spacetime/__init__.py +++ b/qlbm/components/spacetime/__init__.py @@ -1,6 +1,6 @@ """Modular qlbm quantum circuit components for the CQLBM algorithm :cite:p:`spacetime`.""" -from .collision import SpaceTimeCollisionOperator +from .collision.d2q4_old import SpaceTimeD2Q4CollisionOperator from .initial.pointwise import PointWiseSpaceTimeInitialConditions from .initial.volumetric import VolumetricSpaceTimeInitialConditions from .measurement import SpaceTimeGridVelocityMeasurement @@ -9,7 +9,7 @@ from .streaming import SpaceTimeStreamingOperator __all__ = [ - "SpaceTimeCollisionOperator", + "SpaceTimeD2Q4CollisionOperator", "PointWiseSpaceTimeInitialConditions", "VolumetricSpaceTimeInitialConditions", "SpaceTimeStreamingOperator", diff --git a/qlbm/components/spacetime/spacetime.py b/qlbm/components/spacetime/spacetime.py index 145bb02..ac12f4b 100644 --- a/qlbm/components/spacetime/spacetime.py +++ b/qlbm/components/spacetime/spacetime.py @@ -9,7 +9,7 @@ from qlbm.components.spacetime.reflection import PointWiseSpaceTimeReflectionOperator from qlbm.lattice.lattices.spacetime_lattice import SpaceTimeLattice -from .collision import SpaceTimeCollisionOperator +from .collision.d2q4_old import SpaceTimeD2Q4CollisionOperator from .streaming import SpaceTimeStreamingOperator @@ -91,7 +91,7 @@ def create_circuit(self) -> QuantumCircuit: # There is no collision in 1D if self.lattice.num_dims > 1: circuit.compose( - SpaceTimeCollisionOperator( + SpaceTimeD2Q4CollisionOperator( self.lattice, timestep, logger=self.logger ).circuit, inplace=True, From 3a8ab3b258d44c850602a84461607a0cd077fb2b Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Wed, 9 Jul 2025 12:02:42 +0200 Subject: [PATCH 32/92] Add LQLGA initialization, streaming, collision --- qlbm/components/lqlga/__init__.py | 0 qlbm/components/lqlga/collision.py | 55 ++++++++++++++++++++ qlbm/components/lqlga/initial.py | 56 ++++++++++++++++++++ qlbm/components/lqlga/lqlga.py | 45 ++++++++++++++++ qlbm/components/lqlga/measurement.py | 42 +++++++++++++++ qlbm/components/lqlga/streaming.py | 76 ++++++++++++++++++++++++++++ 6 files changed, 274 insertions(+) create mode 100644 qlbm/components/lqlga/__init__.py create mode 100644 qlbm/components/lqlga/collision.py create mode 100644 qlbm/components/lqlga/initial.py create mode 100644 qlbm/components/lqlga/lqlga.py create mode 100644 qlbm/components/lqlga/measurement.py create mode 100644 qlbm/components/lqlga/streaming.py diff --git a/qlbm/components/lqlga/__init__.py b/qlbm/components/lqlga/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/qlbm/components/lqlga/collision.py b/qlbm/components/lqlga/collision.py new file mode 100644 index 0000000..ba963fc --- /dev/null +++ b/qlbm/components/lqlga/collision.py @@ -0,0 +1,55 @@ +"""Collision operators for the :class:`.SpaceTimeQLBM` algorithm :cite:`spacetime`.""" + +from logging import Logger, getLogger +from time import perf_counter_ns + +from qiskit import QuantumCircuit +from typing_extensions import override + +from qlbm.components.base import LQLGAOperator, SpaceTimeOperator +from qlbm.components.common.cbse_collision.cbse_collision import EQCCollisionOperator +from qlbm.lattice.lattices.lqlga_lattice import LQLGALattice + + +class GenericLQLGACollisionOperator(LQLGAOperator): + def __init__( + self, + lattice: LQLGALattice, + logger: Logger = getLogger("qlbm"), + ) -> None: + super().__init__(lattice, 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: + local_collision_circuit = EQCCollisionOperator( + self.lattice.discretization + ).circuit + circuit = self.lattice.circuit.copy() + + for velocity_qubit_indices in range( + 0, + self.lattice.num_base_qubits, + self.lattice.num_velocities_per_point, + ): + circuit.compose( + local_collision_circuit, + inplace=True, + qubits=range( + velocity_qubit_indices, + velocity_qubit_indices + self.lattice.num_velocities_per_point, + ), + ) + + return circuit + + @override + def __str__(self) -> str: + return f"[GenericSpaceTimeCollisionOperator for discretization {self.lattice.discretization}]" diff --git a/qlbm/components/lqlga/initial.py b/qlbm/components/lqlga/initial.py new file mode 100644 index 0000000..2e0e328 --- /dev/null +++ b/qlbm/components/lqlga/initial.py @@ -0,0 +1,56 @@ +from logging import Logger, getLogger +from time import perf_counter_ns +from typing import List, Tuple + +from qiskit import QuantumCircuit +from qiskit.circuit.library import MCXGate +from typing_extensions import override + +from qlbm.components.base import LBMPrimitive +from qlbm.lattice.lattices.lqlga_lattice import LQLGALattice +from qlbm.lattice.lattices.spacetime_lattice import SpaceTimeLattice +from qlbm.lattice.spacetime.properties_base import VonNeumannNeighbor +from qlbm.tools.utils import bit_value, flatten + + +class LQGLAInitialConditions(LBMPrimitive): + def __init__( + self, + lattice: LQLGALattice, + grid_data: List[Tuple[Tuple[int, ...], Tuple[bool, ...]]], + logger: Logger = getLogger("qlbm"), + ): + super().__init__(logger) + + self.lattice = lattice + self.grid_data = grid_data + + 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)" + ) + + def create_circuit(self): + circuit = self.lattice.circuit.copy() + + for tup in self.grid_data: + gp, vel_profile = tup[0], tup[1] + + if not any(vel_profile): + continue + + circuit.x( + self.lattice.gridpoint_index_tuple(gp) + * self.lattice.num_velocities_per_point + + v + for v, is_enabled in enumerate(vel_profile) + if is_enabled + ) + + return circuit + + @override + def __str__(self): + return f"[Primitive LQGLAInitialConditions on lattice={self.lattice}, grid_data={self.grid_data})]" diff --git a/qlbm/components/lqlga/lqlga.py b/qlbm/components/lqlga/lqlga.py new file mode 100644 index 0000000..5573900 --- /dev/null +++ b/qlbm/components/lqlga/lqlga.py @@ -0,0 +1,45 @@ +"""The end-to-end algorithm of the Space-Time Quantum Lattice Boltzmann Algorithm described in :cite:`spacetime`.""" + +from logging import Logger, getLogger + +from qiskit import QuantumCircuit +from typing_extensions import override + +from qlbm.components.base import LBMAlgorithm +from qlbm.components.lqlga.collision import GenericLQLGACollisionOperator +from qlbm.components.lqlga.streaming import LQLGAStreamingOperator +from qlbm.lattice.lattices.lqlga_lattice import LQLGALattice + + +class LQLGA(LBMAlgorithm): + lattice: LQLGALattice + + def __init__( + self, + lattice: LQLGALattice, + filter_inside_blocks: bool = True, + logger: Logger = getLogger("qlbm"), + ): + super().__init__(lattice, logger) + + self.lattice = lattice + self.filter_inside_blocks = filter_inside_blocks + + self.circuit = self.create_circuit() + + @override + def create_circuit(self) -> QuantumCircuit: + circuit = self.lattice.circuit.copy() + + circuit.compose( + LQLGAStreamingOperator(self.lattice, self.logger).circuit, inplace=True + ) + + circuit.compose( + GenericLQLGACollisionOperator(self.lattice, self.logger).circuit, + inplace=True, + ) + + @override + def __str__(self) -> str: + return f"[LQLGA on lattice={self.lattice}]" diff --git a/qlbm/components/lqlga/measurement.py b/qlbm/components/lqlga/measurement.py new file mode 100644 index 0000000..b9af2d7 --- /dev/null +++ b/qlbm/components/lqlga/measurement.py @@ -0,0 +1,42 @@ +"""Measurement operator for the :class:`.SpaceTimeQLBM` algorithm :cite:`spacetime`.""" + +from logging import Logger, getLogger +from typing import Tuple + +from qiskit import ClassicalRegister +from qiskit.circuit.library import MCMTGate, XGate +from typing_extensions import override + +from qlbm.components.base import LQLGAOperator, SpaceTimeOperator +from qlbm.lattice.lattices.lqlga_lattice import LQLGALattice +from qlbm.tools.utils import flatten, get_qubits_to_invert + + +class LQLGAGridVelocityMeasurement(LQLGAOperator): + def __init__( + self, + lattice: LQLGALattice, + logger: Logger = getLogger("qlbm"), + ) -> None: + super().__init__(lattice, logger) + self.lattice = lattice + + self.circuit = self.create_circuit() + + @override + def create_circuit(self): + circuit = self.lattice.circuit.copy() + + qubits_to_measure = list(range(self.lattice.num_base_qubits)) + circuit.add_register(ClassicalRegister(self.lattice.num_base_qubits)) + + circuit.measure( + qubits_to_measure, + list(range(len(qubits_to_measure))), + ) + + return circuit + + @override + def __str__(self) -> str: + return f"[LQLGAGridVelocityMeasurement for lattice {self.lattice}]" diff --git a/qlbm/components/lqlga/streaming.py b/qlbm/components/lqlga/streaming.py new file mode 100644 index 0000000..134f3f2 --- /dev/null +++ b/qlbm/components/lqlga/streaming.py @@ -0,0 +1,76 @@ +"""Streaming operators for the :class:`.SpaceTimeQLBM` algorithm :cite:`spacetime`.""" + +import math +from logging import Logger, getLogger +from time import perf_counter_ns +from typing import List, Tuple + +from qiskit import QuantumCircuit +from typing_extensions import override + +from qlbm.components.base import LQLGAOperator, SpaceTimeOperator +from qlbm.lattice.lattices.lqlga_lattice import LQLGALattice +from qlbm.lattice.lattices.spacetime_lattice import SpaceTimeLattice +from qlbm.lattice.spacetime.properties_base import LatticeDiscretization +from qlbm.tools.exceptions import CircuitException + + +class LQLGAStreamingOperator(LQLGAOperator): + def __init__( + self, + lattice: LQLGALattice, + logger: Logger = getLogger("qlbm"), + ) -> None: + super().__init__(lattice, 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): + circuit = self.lattice.circuit.copy() + # ! TODO Generalize in 2 and 3D + num_gps = self.lattice.num_gridpoints[0] + 1 + + gridpoints_to_swap = self.logarithmic_depth_streaming_line_swaps(num_gps) + + for layer in gridpoints_to_swap: + for i, j in layer: + circuit.swap( + self.lattice.velocity_index_flat(i, 0), + self.lattice.velocity_index_flat(j, 0), + ) + circuit.swap( + self.lattice.velocity_index_flat(i, 1), + self.lattice.velocity_index_flat(j, 1), + ) + + return circuit + + def logarithmic_depth_streaming_line_swaps( + self, num_gridpoints: int + ) -> List[List[Tuple[int, int]]]: + if num_gridpoints < 2: + return [] + + layers: List[List[Tuple[int, int]]] = [] + stride = 1 + + while stride < num_gridpoints: + layer: List[Tuple[int, int]] = [] + for i in range(0, num_gridpoints, 2 * stride): + if i + stride < num_gridpoints: + layer.append((i, i + stride)) + layers.append(layer) + stride *= 2 + + return layers + + @override + def __str__(self): + return f"[LQLGAStreamingOperator on lattice={self.lattice}]" From 233457c949b8ba120a88d2b8bed4b4607ace5adc Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Wed, 9 Jul 2025 12:03:03 +0200 Subject: [PATCH 33/92] Add CBSE collision module for STQBM, LQLGA --- .../common/cbse_collision/__init__.py | 0 .../common/cbse_collision/cbse_collision.py | 101 ++++++++++++++ .../common/cbse_collision/cbse_permutation.py | 126 ++++++++++++++++++ .../cbse_collision/cbse_redistribution.py | 69 ++++++++++ 4 files changed, 296 insertions(+) create mode 100644 qlbm/components/common/cbse_collision/__init__.py create mode 100644 qlbm/components/common/cbse_collision/cbse_collision.py create mode 100644 qlbm/components/common/cbse_collision/cbse_permutation.py create mode 100644 qlbm/components/common/cbse_collision/cbse_redistribution.py diff --git a/qlbm/components/common/cbse_collision/__init__.py b/qlbm/components/common/cbse_collision/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/qlbm/components/common/cbse_collision/cbse_collision.py b/qlbm/components/common/cbse_collision/cbse_collision.py new file mode 100644 index 0000000..afc6ef2 --- /dev/null +++ b/qlbm/components/common/cbse_collision/cbse_collision.py @@ -0,0 +1,101 @@ +"""Collision operators for the :class:`.SpaceTimeQLBM` algorithm :cite:`spacetime`.""" + +from logging import Logger, getLogger +from math import pi +from time import perf_counter_ns + +from qiskit import QuantumCircuit +from qiskit.circuit import Gate +from qiskit.circuit.library import MCMTGate, RYGate +from typing_extensions import override + +from qlbm.components.base import LBMPrimitive, SpaceTimeOperator +from qlbm.components.common.cbse_collision.cbse_permutation import ( + EQCPermutation, +) +from qlbm.components.common.cbse_collision.cbse_redistribution import ( + EQCRedistribution, +) +from qlbm.lattice.eqc.eqc import EquivalenceClass +from qlbm.lattice.eqc.eqc_generator import ( + EquivalenceClassGenerator, +) +from qlbm.lattice.spacetime.properties_base import ( + LatticeDiscretization, + LatticeDiscretizationProperties, +) + + +class EQCCollisionOperator(LBMPrimitive): + def __init__( + self, + discretization: LatticeDiscretization, + ) -> None: + super().__init__() + self.discretization = discretization + self.num_velocities = LatticeDiscretizationProperties.get_num_velocities( + self.discretization + ) + + 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: + """ + Applies the collision operator of all equivalence classes onto one velocity register. + + Returns + ------- + QuantumCircuit + The circuit performing the complete collision operator. + """ + circuit = QuantumCircuit(self.num_velocities) + + for eqc in EquivalenceClassGenerator( + self.discretization + ).generate_equivalence_classes(): + circuit.compose(self.create_circuit_one_eqc(eqc), inplace=True) + + return circuit + + def create_circuit_one_eqc( + self, equivalence_class: EquivalenceClass + ) -> QuantumCircuit: + """ + Creates the PRP-based collision operator for one equivalence class. + + Parameters + ---------- + equivalence_class : EquivalenceClass + The equivalence class to collide. + + Returns + ------- + QuantumCircuit + The circuit performing the collision. + """ + circuit = QuantumCircuit(self.num_velocities) + circuit.compose( + EQCPermutation(equivalence_class, logger=self.logger).circuit, + inplace=True, + ) + + circuit.compose( + EQCRedistribution(equivalence_class, logger=self.logger).circuit, + inplace=True, + ) + circuit.compose( + EQCPermutation(equivalence_class, inverse=True, logger=self.logger).circuit, + inplace=True, + ) + + return circuit + + @override + def __str__(self) -> str: + return f"[EQCCollisionOperator for equi {self.discretization}]" diff --git a/qlbm/components/common/cbse_collision/cbse_permutation.py b/qlbm/components/common/cbse_collision/cbse_permutation.py new file mode 100644 index 0000000..d3ca106 --- /dev/null +++ b/qlbm/components/common/cbse_collision/cbse_permutation.py @@ -0,0 +1,126 @@ +from logging import Logger, getLogger +from time import perf_counter_ns +from typing import override + +from qiskit import QuantumCircuit + +from qlbm.components.base import LBMPrimitive +from qlbm.lattice.eqc.eqc import EquivalenceClass +from qlbm.lattice.spacetime.properties_base import LatticeDiscretization +from qlbm.tools.exceptions import CircuitException + + +class EQCPermutation(LBMPrimitive): + """Permutes states belonging to equivalence classes onto pre-determined basis states. + + Precedes redistribution. + + WIP. + """ + + def __init__( + self, + equivalence_class: EquivalenceClass, + inverse: bool = False, + logger: Logger = getLogger("qlbm"), + ): + super().__init__(logger) + self.equivalence_class = equivalence_class + self.inverse = inverse + + 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): + if self.equivalence_class.discretization == LatticeDiscretization.D2Q4: + return self.__create_circuit_d2q4() + elif self.equivalence_class.discretization == LatticeDiscretization.D3Q6: + return self.__create_circuit_d3q6() + else: + raise CircuitException( + f"Collision not yet supported for discretization {self.equivalence_class.discretization}." + ) + + def __create_circuit_d2q4(self): + circuit = QuantumCircuit(4) + + if not self.inverse: + circuit.cx(1, 2) + circuit.cx(0, 1) + circuit.cx(0, 3) + else: + circuit.cx(0, 3) + circuit.cx(0, 1) + circuit.cx(1, 2) + + return circuit + + def __create_circuit_d3q6(self): + circuit = QuantumCircuit(6) + + match self.equivalence_class.id(): + case (2, [0, 0, 0]): + circuit.cx(2, 3) + circuit.cx(5, 4) + + circuit.cx(1, 2) + circuit.cx(1, 3) + circuit.cx(1, 5) + + circuit.cx(0, 2) + circuit.cx(0, 4) + circuit.cx(0, 5) + case (4, [0, 0, 0]): + circuit.ccx(4, 5, 3) + circuit.ccx(0, 2, 4) + circuit.ccx(0, 1, 2) + circuit.ccx(0, 1, 5) + circuit.x(1) + circuit.cx(1, 0) + case (3, [1, 0, 0]): + circuit.cx(0, 3) + circuit.cx(1, 2) + circuit.ccx(0, 5, 4) + circuit.cx(1, 5) + circuit.swap(0, 1) + case (3, [-1, 0, 0]): + circuit.cx(1, 2) + circuit.cx(5, 4) + circuit.cx(1, 5) + circuit.x(0) + circuit.swap(0, 1) + case (3, [0, 1, 0]): + circuit.cx(0, 2) + circuit.cx(5, 3) + circuit.cx(0, 5) + circuit.x(4) + case (3, [0, -1, 0]): + circuit.cx(5, 3) + circuit.cx(0, 2) + circuit.cx(0, 5) + circuit.x(1) + case (3, [0, 0, 1]): + circuit.cx(1, 3) + circuit.cx(0, 1) + circuit.cx(0, 4) + circuit.x(5) + case (3, [0, 0, -1]): + circuit.cx(0, 1) + circuit.cx(4, 3) + circuit.cx(0, 4) + circuit.x(2) + case _: + raise CircuitException( + f"Collision not yet supported for discretization {self.equivalence_class.discretization} and equivalence class {self.equivalence_class.id()}." + ) + + return circuit if not self.inverse else circuit.inverse() + + @override + def __str__(self) -> str: + return f"[EQCPermutation for eqc {self.equivalence_class}]" \ No newline at end of file diff --git a/qlbm/components/common/cbse_collision/cbse_redistribution.py b/qlbm/components/common/cbse_collision/cbse_redistribution.py new file mode 100644 index 0000000..74208c2 --- /dev/null +++ b/qlbm/components/common/cbse_collision/cbse_redistribution.py @@ -0,0 +1,69 @@ +from logging import Logger, getLogger +from time import perf_counter_ns +from typing import override + +import numpy as np +from qiskit import QuantumCircuit +from qiskit.quantum_info import Operator + +from qlbm.components.base import LBMPrimitive +from qlbm.lattice.eqc.eqc import EquivalenceClass +from qlbm.lattice.spacetime.properties_base import LatticeDiscretizationProperties + + +class EQCRedistribution(LBMPrimitive): + """ + Redistributes the amplitudes of the states belonging to an equivalence class evenly across all other equivalent states. + + WIP. + """ + + def __init__( + self, equivalence_class: EquivalenceClass, logger: Logger = getLogger("qlbm") + ): + super().__init__(logger) + self.equivalence_class = equivalence_class + + 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): + nv = LatticeDiscretizationProperties.get_num_velocities( + self.equivalence_class.discretization + ) + circuit = QuantumCircuit(nv) + + n = self.equivalence_class.size() + nq = np.ceil(np.log2(n)).astype(int) + + QFT = np.array( + [ + [np.exp(2j * np.pi * i * j / n) / np.sqrt(n) for j in range(n)] + for i in range(n) + ] + ) + + U = np.eye(2**nq, dtype=complex) + U[:n, :n] = QFT + op = Operator(U) + assert op.is_unitary() + + qft_block_circ = QuantumCircuit(nq) + qft_block_circ.append(op, list(range(nq))) + + circuit.compose( + qft_block_circ.control(nv - int(nq), label=rf"Coll({n}, {nq})"), + qubits=list(range(nv - 1, -1, -1)), + inplace=True, + ) + + return circuit.decompose() + + @override + def __str__(self): + return f"[EQCRedistribution({self.equivalence_class})]" From 379638c83bcf52ccb644ceebfe2415846aeff77b Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Wed, 9 Jul 2025 12:03:27 +0200 Subject: [PATCH 34/92] Update STQBM collision tests --- test/unit/spacetime/collision/eqc_collision_test.py | 13 +++++-------- test/unit/spacetime/collision/eqc_generator_test.py | 4 ++-- .../collision/eqc_velocity_discretization_test.py | 4 ++-- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/test/unit/spacetime/collision/eqc_collision_test.py b/test/unit/spacetime/collision/eqc_collision_test.py index b377589..6e9447b 100644 --- a/test/unit/spacetime/collision/eqc_collision_test.py +++ b/test/unit/spacetime/collision/eqc_collision_test.py @@ -5,9 +5,9 @@ from qiskit_aer import AerSimulator from qlbm.components.spacetime.collision.eqc_collision import ( - GenericSpaceTimeCollisionOperator, + EQCCollisionOperator, ) -from qlbm.components.spacetime.collision.eqc_discretizations import ( +from qlbm.lattice.eqc.eqc_generator import ( EquivalenceClassGenerator, ) from qlbm.lattice.lattices.spacetime_lattice import SpaceTimeLattice @@ -102,8 +102,7 @@ def test_d2q4_collision_positive_cases( }, ) - collision_operator = GenericSpaceTimeCollisionOperator(lattice, 0) - local_circuit = collision_operator.create_circuit_one_register() + local_circuit = EQCCollisionOperator(lattice.properties.get_discretization()).circuit assert local_circuit.num_qubits == 4 eqc = d2q4_equivalence_class_bitstrings[equivalence_class_index] for velocity_cfg in eqc: @@ -121,8 +120,7 @@ def test_d2q4_collision_negative_cases(d2q4_equivalence_class_bitstrings): }, ) - collision_operator = GenericSpaceTimeCollisionOperator(lattice, 0) - local_circuit = collision_operator.create_circuit_one_register() + local_circuit = EQCCollisionOperator(lattice.properties.get_discretization()).circuit assert local_circuit.num_qubits == 4 for b in [ @@ -165,8 +163,7 @@ def test_d3q6_collision_positive_cases( }, ) - collision_operator = GenericSpaceTimeCollisionOperator(lattice, 0) - local_circuit = collision_operator.create_circuit_one_register() + local_circuit = EQCCollisionOperator(lattice.properties.get_discretization()).circuit assert local_circuit.num_qubits == 6 eqc = d3q6_equivalence_class_bitstrings[equivalence_class_index] for velocity_cfg in eqc: diff --git a/test/unit/spacetime/collision/eqc_generator_test.py b/test/unit/spacetime/collision/eqc_generator_test.py index e857d8a..66e52e2 100644 --- a/test/unit/spacetime/collision/eqc_generator_test.py +++ b/test/unit/spacetime/collision/eqc_generator_test.py @@ -1,9 +1,9 @@ import numpy as np -from qlbm.components.spacetime.collision.eqc_discretizations import ( - EquivalenceClass, +from qlbm.lattice.eqc.eqc_generator import ( EquivalenceClassGenerator, ) +from qlbm.lattice.eqc.eqc import EquivalenceClass from qlbm.lattice.spacetime.properties_base import LatticeDiscretization diff --git a/test/unit/spacetime/collision/eqc_velocity_discretization_test.py b/test/unit/spacetime/collision/eqc_velocity_discretization_test.py index b572919..3470a4f 100644 --- a/test/unit/spacetime/collision/eqc_velocity_discretization_test.py +++ b/test/unit/spacetime/collision/eqc_velocity_discretization_test.py @@ -2,10 +2,10 @@ import pytest -from qlbm.components.spacetime.collision import ( - EquivalenceClass, +from qlbm.lattice.eqc.eqc_generator import ( EquivalenceClassGenerator, ) +from qlbm.lattice.eqc.eqc import EquivalenceClass from qlbm.lattice.spacetime.properties_base import LatticeDiscretization from qlbm.tools.exceptions import LatticeException From 8a75f2437dcbd81f488a8f0498fb48f4b92cdc8b Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Wed, 9 Jul 2025 12:03:39 +0200 Subject: [PATCH 35/92] Add lqlga tests --- test/unit/lqlga/__init__.py | 0 test/unit/lqlga/circuits/__init__.py | 0 test/unit/lqlga/circuits/conftest.py | 39 +++++++++++++++++ test/unit/lqlga/circuits/streaming_test.py | 49 +++++++++++++++++++++ test/unit/lqlga/conftest.py | 27 ++++++++++++ test/unit/lqlga/lqlga_lattice_test.py | 51 ++++++++++++++++++++++ 6 files changed, 166 insertions(+) create mode 100644 test/unit/lqlga/__init__.py create mode 100644 test/unit/lqlga/circuits/__init__.py create mode 100644 test/unit/lqlga/circuits/conftest.py create mode 100644 test/unit/lqlga/circuits/streaming_test.py create mode 100644 test/unit/lqlga/conftest.py create mode 100644 test/unit/lqlga/lqlga_lattice_test.py diff --git a/test/unit/lqlga/__init__.py b/test/unit/lqlga/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/unit/lqlga/circuits/__init__.py b/test/unit/lqlga/circuits/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/unit/lqlga/circuits/conftest.py b/test/unit/lqlga/circuits/conftest.py new file mode 100644 index 0000000..b8c5710 --- /dev/null +++ b/test/unit/lqlga/circuits/conftest.py @@ -0,0 +1,39 @@ +import pytest + +from qlbm.lattice.lattices.lqlga_lattice import LQLGALattice + + +@pytest.fixture +def lattice_d2q4_256_8() -> LQLGALattice: + return LQLGALattice( + { + "lattice": { + "dim": {"x": 256, "y": 8}, + "velocities": {"x": 2, "y": 2}, + }, + }, + ) + + +@pytest.fixture +def lattice_d1q2_256() -> LQLGALattice: + return LQLGALattice( + { + "lattice": { + "dim": {"x": 256}, + "velocities": {"x": 2}, + }, + }, + ) + +@pytest.fixture +def lattice_d1q2_8() -> LQLGALattice: + return LQLGALattice( + { + "lattice": { + "dim": {"x": 8}, + "velocities": {"x": 2}, + }, + }, + ) + diff --git a/test/unit/lqlga/circuits/streaming_test.py b/test/unit/lqlga/circuits/streaming_test.py new file mode 100644 index 0000000..7ecf0e8 --- /dev/null +++ b/test/unit/lqlga/circuits/streaming_test.py @@ -0,0 +1,49 @@ +from typing import List, Tuple + +import pytest +from qlbm.components.lqlga.streaming import LQLGAStreamingOperator + + +def verify_streaming_line_correctness( + size: int, swaps: List[List[Tuple[int, int]]] +) -> bool: + configuration = list(range(size)) + for layer in swaps: + for i, j in layer: + configuration[i], configuration[j] = ( + configuration[j], + configuration[i], + ) + + return configuration == [size - 1] + list(range(size - 1)) + + +@pytest.mark.parametrize("size", list(map(lambda x: 2**x, list(range(10))))) +def test_streaming_line_creation_power_of_2(lattice_d1q2_8, size): + operator = LQLGAStreamingOperator(lattice_d1q2_8) + swaps = operator.logarithmic_depth_streaming_line_swaps(size) + + assert verify_streaming_line_correctness(size, swaps) + + +@pytest.mark.parametrize("size", list(map(lambda x: 2**x + 1, list(range(1, 10))))) +def test_streaming_line_creation_off_power_of_2_positive(lattice_d1q2_8, size): + operator = LQLGAStreamingOperator(lattice_d1q2_8) + swaps = operator.logarithmic_depth_streaming_line_swaps(size) + + assert verify_streaming_line_correctness(size, swaps) + + +@pytest.mark.parametrize("size", list(map(lambda x: 2**x - 1, list(range(3, 10))))) +def test_streaming_line_creation__off_power_2_negative(lattice_d1q2_8, size): + operator = LQLGAStreamingOperator(lattice_d1q2_8) + swaps = operator.logarithmic_depth_streaming_line_swaps(size) + + assert verify_streaming_line_correctness(size, swaps) + +@pytest.mark.parametrize("size", list(range(7, 16))) +def test_streaming_line_creation_intermediate(lattice_d1q2_8, size): + operator = LQLGAStreamingOperator(lattice_d1q2_8) + swaps = operator.logarithmic_depth_streaming_line_swaps(size) + + assert verify_streaming_line_correctness(size, swaps) \ No newline at end of file diff --git a/test/unit/lqlga/conftest.py b/test/unit/lqlga/conftest.py new file mode 100644 index 0000000..245a555 --- /dev/null +++ b/test/unit/lqlga/conftest.py @@ -0,0 +1,27 @@ +import pytest + +from qlbm.lattice.lattices.lqlga_lattice import LQLGALattice + + +@pytest.fixture +def lattice_d2q4_256_8() -> LQLGALattice: + return LQLGALattice( + { + "lattice": { + "dim": {"x": 256, "y": 8}, + "velocities": {"x": 2, "y": 2}, + }, + }, + ) + + +@pytest.fixture +def lattice_d1q2_256() -> LQLGALattice: + return LQLGALattice( + { + "lattice": { + "dim": {"x": 256}, + "velocities": {"x": 2}, + }, + }, + ) diff --git a/test/unit/lqlga/lqlga_lattice_test.py b/test/unit/lqlga/lqlga_lattice_test.py new file mode 100644 index 0000000..39f115e --- /dev/null +++ b/test/unit/lqlga/lqlga_lattice_test.py @@ -0,0 +1,51 @@ +import pytest + +from qlbm.lattice.lattices.lqlga_lattice import LQLGALattice +from qlbm.lattice.spacetime.properties_base import LatticeDiscretizationProperties + + +def test_lqlga_lattice_num_registers_d1q2(lattice_d1q2_256): + assert len(lattice_d1q2_256.registers) == 256 + assert all(reg.size == 2 for reg in lattice_d1q2_256.registers) + assert lattice_d1q2_256.circuit.num_qubits == 512 + + +def test_lqlga_lattice_num_registers_d2q4(lattice_d2q4_256_8): + assert len(lattice_d2q4_256_8.registers) == 256 * 8 + assert all(reg.size == 4 for reg in lattice_d2q4_256_8.registers) + assert lattice_d2q4_256_8.circuit.num_qubits == 256 * 8 * 4 + + +def test_lqlga_grid_index_mapping_edge(): + lattice = LQLGALattice( + { + "lattice": { + "dim": {"x": 64, "y": 8}, + "velocities": {"x": 2, "y": 2}, + }, + }, + ) + + assert lattice.gridpoint_index_flat(0) == (0, 0) + assert lattice.gridpoint_index_flat(1) == (0, 1) + assert lattice.gridpoint_index_flat(7) == (0, 7) + assert lattice.gridpoint_index_flat(8) == (1, 0) + assert lattice.gridpoint_index_flat(9) == (1, 1) + assert lattice.gridpoint_index_flat(511) == (63, 7) + + +@pytest.mark.parametrize( + "lattice_name", + ["lattice_d2q4_256_8", "lattice_d1q2_256"], +) +def test_lqlga_grid_index_mapping_general(lattice_name, request): + lattice = request.getfixturevalue(lattice_name) + assert all( + lattice.gridpoint_index_tuple(lattice.gridpoint_index_flat(gp)) == gp + for gp in range( + lattice.num_total_qubits + // LatticeDiscretizationProperties.get_num_velocities( + lattice.discretization + ) + ) + ) From d88e81318d52284a10f6b69fb86f2d7fa204d4e0 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Wed, 9 Jul 2025 16:59:54 +0200 Subject: [PATCH 36/92] Generalize CQLBM data for shapes --- .../collisionless/bounceback_reflection.py | 2 +- qlbm/components/collisionless/cqlbm.py | 25 ++++++++++++++++--- .../collisionless/specular_reflection.py | 2 +- .../lattice/lattices/collisionless_lattice.py | 18 ++++++------- 4 files changed, 32 insertions(+), 15 deletions(-) diff --git a/qlbm/components/collisionless/bounceback_reflection.py b/qlbm/components/collisionless/bounceback_reflection.py index c0098e5..8f9c700 100644 --- a/qlbm/components/collisionless/bounceback_reflection.py +++ b/qlbm/components/collisionless/bounceback_reflection.py @@ -529,4 +529,4 @@ def flip_and_stream( @override def __str__(self) -> str: - return f"[Operator BounceBackReflectionOperator against block {self.lattice.blocks}]" + return f"[Operator BounceBackReflectionOperator against shapes {self.lattice.shapes}]" diff --git a/qlbm/components/collisionless/cqlbm.py b/qlbm/components/collisionless/cqlbm.py index 579fad8..b9b54b5 100644 --- a/qlbm/components/collisionless/cqlbm.py +++ b/qlbm/components/collisionless/cqlbm.py @@ -8,6 +8,8 @@ from qlbm.components.base import LBMAlgorithm from qlbm.lattice import CollisionlessLattice +from qlbm.lattice.geometry.shapes.block import Block +from qlbm.tools.exceptions import LatticeException from qlbm.tools.utils import get_time_series from .bounceback_reflection import BounceBackReflectionOperator @@ -68,21 +70,36 @@ def create_circuit(self): ).circuit, inplace=True, ) - if self.lattice.blocks["specular"]: + if self.lattice.shapes["specular"]: + if not all( + isinstance(shape, Block) + for shape in self.lattice.shapes["specular"] + ): + raise LatticeException( + "All shapes with the 'specular' boundary condition must be of type Block for the CQLBM algorithm. " + ) circuit.compose( SpecularReflectionOperator( self.lattice, - self.lattice.blocks["specular"], + self.lattice.shapes["specular"], # type: ignore logger=self.logger, ).circuit, inplace=True, ) - if self.lattice.blocks["bounceback"]: + if self.lattice.shapes["bounceback"]: + if self.lattice.shapes["specular"]: + if not all( + isinstance(shape, Block) + for shape in self.lattice.shapes["specular"] + ): + raise LatticeException( + "All shapes with the 'bounceback' boundary condition must be of type Block for the CQLBM algorithm. " + ) circuit.compose( BounceBackReflectionOperator( self.lattice, - self.lattice.blocks["bounceback"], + self.lattice.shapes["bounceback"], # type: ignore logger=self.logger, ).circuit, inplace=True, diff --git a/qlbm/components/collisionless/specular_reflection.py b/qlbm/components/collisionless/specular_reflection.py index ab4b978..a29fcda 100644 --- a/qlbm/components/collisionless/specular_reflection.py +++ b/qlbm/components/collisionless/specular_reflection.py @@ -519,5 +519,5 @@ def flip_and_stream( @override def __str__(self) -> str: return ( - f"[Operator SpecularReflectionOperator against block {self.lattice.blocks}]" + f"[Operator SpecularReflectionOperator against shapes {self.lattice.shapes}]" ) diff --git a/qlbm/lattice/lattices/collisionless_lattice.py b/qlbm/lattice/lattices/collisionless_lattice.py index 6b3ebfb..58558e1 100644 --- a/qlbm/lattice/lattices/collisionless_lattice.py +++ b/qlbm/lattice/lattices/collisionless_lattice.py @@ -6,7 +6,7 @@ from qiskit import QuantumCircuit, QuantumRegister from typing_extensions import override -from qlbm.lattice.geometry.shapes.block import Block +from qlbm.lattice.geometry.shapes.base import Shape from qlbm.tools.exceptions import LatticeException from qlbm.tools.utils import dimension_letter, flatten @@ -34,7 +34,7 @@ class CollisionlessLattice(Lattice): :attr:`circuit` An empty ``qiskit.QuantumCircuit`` with labeled registers that quantum components use as a base. Each quantum component that is parameterized by a ``Lattice`` makes a copy of this quantum circuit to which it appends its designated logic. - :attr:`blocks` A ``Dict[str, List[Block]]`` that contains all of the :class:`.Block`\ s encoding the solid geometry of the lattice. + :attr:`shapes` A ``Dict[str, List[Shape]]`` that contains all of the :class:`.Shape`\ s encoding the solid geometry of the lattice. The key of the dictionary is the specific kind of boundary condition of the obstacle (i.e., ``"bounceback"`` or ``"specular"``). :attr:`logger` The performance logger, by default ``getLogger("qlbm")``. =========================== ====================================================================== @@ -155,13 +155,13 @@ def __init__( logger: Logger = getLogger("qlbm"), ) -> None: super().__init__(lattice_data, logger) - dimensions, velocities, blocks = self.parse_input_data(lattice_data) # type: ignore + dimensions, velocities, shapes = self.parse_input_data(lattice_data) # type: ignore self.num_dims = len(dimensions) self.num_gridpoints = dimensions self.num_velocities = velocities - self.blocks: Dict[str, List[Block]] = blocks # type: ignore - self.block_list: List[Block] = flatten(list(blocks.values())) + self.shapes: Dict[str, List[Shape]] = shapes # type: ignore + self.shape_list: List[Shape] = flatten(list(shapes.values())) self.num_comparator_qubits = 2 * (self.num_dims - 1) self.num_obstacle_qubits = self.__num_obstacle_qubits() self.num_ancilla_qubits = ( @@ -495,8 +495,8 @@ def get_registers(self) -> Tuple[List[QuantumRegister], ...]: def __num_obstacle_qubits(self) -> int: all_obstacle_bounceback: bool = len( - [b for b in self.block_list if b.boundary_condition == "bounceback"] - ) == len(self.block_list) + [b for b in self.shape_list if b.boundary_condition == "bounceback"] + ) == len(self.shape_list) if all_obstacle_bounceback: # A single qubit suffices to determine # Whether particles have streamed inside the object @@ -508,7 +508,7 @@ def __num_obstacle_qubits(self) -> int: @override def __str__(self) -> str: - return f"[Lattice with {self.num_gridpoints} gps, {self.num_velocities} vels, and {str(self.blocks)} blocks with {self.num_total_qubits} qubits]" + return f"[Lattice with {self.num_gridpoints} gps, {self.num_velocities} vels, and {str(self.shapes)} shapes with {self.num_total_qubits} qubits]" @override def logger_name(self) -> str: @@ -517,4 +517,4 @@ def logger_name(self) -> str: gp_string += f"{gp + 1}" if c < len(self.num_gridpoints) - 1: gp_string += "x" - return f"{self.num_dims}d-{gp_string}-{len(self.block_list)}-obstacle" + return f"{self.num_dims}d-{gp_string}-{len(self.shape_list)}-obstacle" From 0b868260bbf82bf633b9e538a3467fa915836313 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Wed, 9 Jul 2025 17:00:35 +0200 Subject: [PATCH 37/92] Add warning for STQBM specular reflection BCs --- qlbm/components/spacetime/spacetime.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/qlbm/components/spacetime/spacetime.py b/qlbm/components/spacetime/spacetime.py index ac12f4b..c756de8 100644 --- a/qlbm/components/spacetime/spacetime.py +++ b/qlbm/components/spacetime/spacetime.py @@ -8,6 +8,7 @@ from qlbm.components.base import LBMAlgorithm from qlbm.components.spacetime.reflection import PointWiseSpaceTimeReflectionOperator from qlbm.lattice.lattices.spacetime_lattice import SpaceTimeLattice +from qlbm.tools.exceptions import LatticeException from .collision.d2q4_old import SpaceTimeD2Q4CollisionOperator from .streaming import SpaceTimeStreamingOperator @@ -72,6 +73,12 @@ def create_circuit(self) -> QuantumCircuit: circuit = self.lattice.circuit.copy() for timestep in range(self.lattice.num_timesteps, 0, -1): + # Warn the user if there are any shapes that are NOT bounceback boundary conditions. + if self.lattice.shapes["specular"]: + raise LatticeException( + "Currently, the Space-Time QLBM algorithm only supports bounceback boundary conditions." + ) + circuit.compose( SpaceTimeStreamingOperator(self.lattice, timestep, self.logger).circuit, inplace=True, @@ -101,4 +108,4 @@ def create_circuit(self) -> QuantumCircuit: @override def __str__(self) -> str: - return f"[Space Time QLBM on lattice={self.lattice}]" + return f"[SpaceTimes QLBM on lattice={self.lattice}]" From a5fcea72da8d2865589eb59757bf02880e59720c Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Wed, 9 Jul 2025 17:01:41 +0200 Subject: [PATCH 38/92] Add option for algorithm recompilation due to new qiskit transpiler --- qlbm/infra/runner/qiskit_runner.py | 14 +++++++++++++- qlbm/infra/runner/simulation_config.py | 5 +++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/qlbm/infra/runner/qiskit_runner.py b/qlbm/infra/runner/qiskit_runner.py index 2e643b9..ec12b8b 100644 --- a/qlbm/infra/runner/qiskit_runner.py +++ b/qlbm/infra/runner/qiskit_runner.py @@ -4,6 +4,7 @@ from time import perf_counter_ns from qiskit import QuantumCircuit as QiskitQC +from qiskit import transpile from qiskit.circuit.library import Initialize from qiskit.quantum_info import Statevector from typing_extensions import override @@ -62,6 +63,7 @@ def run( output_directory: str, output_file_name: str = "step", statevector_snapshots: bool = False, + recompile_each_step: bool = False, # ! Document ) -> QBMResult: if (self.sampling_backend is None) and self.config.statevector_sampling: raise ExecutionException( @@ -89,6 +91,7 @@ def run( num_shots, initial_conditions, simulation_result, + recompile_each_step, ) if statevector_snapshots else self._run_time_loop( @@ -111,6 +114,7 @@ def _run_snapshot_time_loop( num_shots: int, initial_conditions: QiskitQC, simulation_result: QBMResult, + recompile_each_step: bool = False, # ! Document ) -> QBMResult: for step in range(num_steps + 1): step_start_time = perf_counter_ns() @@ -124,7 +128,15 @@ def _run_snapshot_time_loop( ) # The first step consists of just initial conditions if step > 0: - circuit.compose(self.config.algorithm, inplace=True) + if recompile_each_step: + circuit.compose(self.config.algorithm_copy, inplace=True) + circuit = transpile( + circuit, + backend=self.execution_backend, + optimization_level=0, + ) + else: + circuit.compose(self.config.algorithm, inplace=True) if ( self.reinitializer.requires_statevector() diff --git a/qlbm/infra/runner/simulation_config.py b/qlbm/infra/runner/simulation_config.py index 6df6a91..00d4dde 100644 --- a/qlbm/infra/runner/simulation_config.py +++ b/qlbm/infra/runner/simulation_config.py @@ -230,6 +230,11 @@ def __init__( # Circuits self.initial_conditions = initial_conditions self.algorithm = algorithm + self.algorithm_copy = ( + algorithm.circuit.copy() + if isinstance(algorithm, QuantumComponent) + else algorithm.copy() + ) self.postprocessing = postprocessing self.measurement = measurement From a3bb1af4f706b279219d7c0e2501f95a765b4956 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Wed, 9 Jul 2025 17:02:54 +0200 Subject: [PATCH 39/92] Update STQBM simulation demo --- demos/simulation/spacetime_simulation.ipynb | 1 + 1 file changed, 1 insertion(+) diff --git a/demos/simulation/spacetime_simulation.ipynb b/demos/simulation/spacetime_simulation.ipynb index 3359f2d..a5455f9 100644 --- a/demos/simulation/spacetime_simulation.ipynb +++ b/demos/simulation/spacetime_simulation.ipynb @@ -116,6 +116,7 @@ " NUM_SHOTS, # Number of shots per time step\n", " output_dir,\n", " statevector_snapshots=True,\n", + " recompile_each_step=True, # This is necessary due to Qiskit transpiler settings. WIP.\n", ")" ] }, From bc5248c01bef70fdf5672ef4caaa63c945aaa3f3 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Wed, 9 Jul 2025 12:05:20 +0200 Subject: [PATCH 40/92] Remove unused imports --- .../common/cbse_collision/cbse_collision.py | 6 +----- qlbm/components/lqlga/collision.py | 2 +- qlbm/components/lqlga/initial.py | 5 ----- qlbm/components/lqlga/measurement.py | 5 +---- qlbm/components/lqlga/streaming.py | 7 +------ qlbm/components/spacetime/collision/__init__.py | 2 +- qlbm/components/spacetime/collision/eqc_collision.py | 10 ---------- qlbm/lattice/eqc/eqc.py | 11 ++++++----- 8 files changed, 11 insertions(+), 37 deletions(-) diff --git a/qlbm/components/common/cbse_collision/cbse_collision.py b/qlbm/components/common/cbse_collision/cbse_collision.py index afc6ef2..5ceadc9 100644 --- a/qlbm/components/common/cbse_collision/cbse_collision.py +++ b/qlbm/components/common/cbse_collision/cbse_collision.py @@ -1,15 +1,11 @@ """Collision operators for the :class:`.SpaceTimeQLBM` algorithm :cite:`spacetime`.""" -from logging import Logger, getLogger -from math import pi from time import perf_counter_ns from qiskit import QuantumCircuit -from qiskit.circuit import Gate -from qiskit.circuit.library import MCMTGate, RYGate from typing_extensions import override -from qlbm.components.base import LBMPrimitive, SpaceTimeOperator +from qlbm.components.base import LBMPrimitive from qlbm.components.common.cbse_collision.cbse_permutation import ( EQCPermutation, ) diff --git a/qlbm/components/lqlga/collision.py b/qlbm/components/lqlga/collision.py index ba963fc..6aa2ac7 100644 --- a/qlbm/components/lqlga/collision.py +++ b/qlbm/components/lqlga/collision.py @@ -6,7 +6,7 @@ from qiskit import QuantumCircuit from typing_extensions import override -from qlbm.components.base import LQLGAOperator, SpaceTimeOperator +from qlbm.components.base import LQLGAOperator from qlbm.components.common.cbse_collision.cbse_collision import EQCCollisionOperator from qlbm.lattice.lattices.lqlga_lattice import LQLGALattice diff --git a/qlbm/components/lqlga/initial.py b/qlbm/components/lqlga/initial.py index 2e0e328..b15754b 100644 --- a/qlbm/components/lqlga/initial.py +++ b/qlbm/components/lqlga/initial.py @@ -2,15 +2,10 @@ from time import perf_counter_ns from typing import List, Tuple -from qiskit import QuantumCircuit -from qiskit.circuit.library import MCXGate from typing_extensions import override from qlbm.components.base import LBMPrimitive from qlbm.lattice.lattices.lqlga_lattice import LQLGALattice -from qlbm.lattice.lattices.spacetime_lattice import SpaceTimeLattice -from qlbm.lattice.spacetime.properties_base import VonNeumannNeighbor -from qlbm.tools.utils import bit_value, flatten class LQGLAInitialConditions(LBMPrimitive): diff --git a/qlbm/components/lqlga/measurement.py b/qlbm/components/lqlga/measurement.py index b9af2d7..e9b6b65 100644 --- a/qlbm/components/lqlga/measurement.py +++ b/qlbm/components/lqlga/measurement.py @@ -1,15 +1,12 @@ """Measurement operator for the :class:`.SpaceTimeQLBM` algorithm :cite:`spacetime`.""" from logging import Logger, getLogger -from typing import Tuple from qiskit import ClassicalRegister -from qiskit.circuit.library import MCMTGate, XGate from typing_extensions import override -from qlbm.components.base import LQLGAOperator, SpaceTimeOperator +from qlbm.components.base import LQLGAOperator from qlbm.lattice.lattices.lqlga_lattice import LQLGALattice -from qlbm.tools.utils import flatten, get_qubits_to_invert class LQLGAGridVelocityMeasurement(LQLGAOperator): diff --git a/qlbm/components/lqlga/streaming.py b/qlbm/components/lqlga/streaming.py index 134f3f2..9700782 100644 --- a/qlbm/components/lqlga/streaming.py +++ b/qlbm/components/lqlga/streaming.py @@ -1,18 +1,13 @@ """Streaming operators for the :class:`.SpaceTimeQLBM` algorithm :cite:`spacetime`.""" -import math from logging import Logger, getLogger from time import perf_counter_ns from typing import List, Tuple -from qiskit import QuantumCircuit from typing_extensions import override -from qlbm.components.base import LQLGAOperator, SpaceTimeOperator +from qlbm.components.base import LQLGAOperator from qlbm.lattice.lattices.lqlga_lattice import LQLGALattice -from qlbm.lattice.lattices.spacetime_lattice import SpaceTimeLattice -from qlbm.lattice.spacetime.properties_base import LatticeDiscretization -from qlbm.tools.exceptions import CircuitException class LQLGAStreamingOperator(LQLGAOperator): diff --git a/qlbm/components/spacetime/collision/__init__.py b/qlbm/components/spacetime/collision/__init__.py index 7f5a082..7073d50 100644 --- a/qlbm/components/spacetime/collision/__init__.py +++ b/qlbm/components/spacetime/collision/__init__.py @@ -1,10 +1,10 @@ """Classes implementing the logic of the collision operator in the Space-Time Data encoding.""" from ....lattice.eqc.eqc import EquivalenceClass -from .d2q4_old import SpaceTimeD2Q4CollisionOperator from ....lattice.eqc.eqc_generator import ( EquivalenceClassGenerator, ) +from .d2q4_old import SpaceTimeD2Q4CollisionOperator __all__ = [ "SpaceTimeD2Q4CollisionOperator", diff --git a/qlbm/components/spacetime/collision/eqc_collision.py b/qlbm/components/spacetime/collision/eqc_collision.py index 26bda60..5ac8cd4 100644 --- a/qlbm/components/spacetime/collision/eqc_collision.py +++ b/qlbm/components/spacetime/collision/eqc_collision.py @@ -8,16 +8,6 @@ from qlbm.components.base import SpaceTimeOperator from qlbm.components.common.cbse_collision.cbse_collision import EQCCollisionOperator -from qlbm.components.common.cbse_collision.cbse_permutation import ( - EQCPermutation, -) -from qlbm.components.common.cbse_collision.cbse_redistribution import ( - EQCRedistribution, -) -from qlbm.lattice.eqc.eqc import EquivalenceClass -from qlbm.lattice.eqc.eqc_generator import ( - EquivalenceClassGenerator, -) from qlbm.lattice.lattices.spacetime_lattice import SpaceTimeLattice diff --git a/qlbm/lattice/eqc/eqc.py b/qlbm/lattice/eqc/eqc.py index 371b863..889a25b 100644 --- a/qlbm/lattice/eqc/eqc.py +++ b/qlbm/lattice/eqc/eqc.py @@ -1,11 +1,12 @@ -from qlbm.lattice.spacetime.properties_base import LatticeDiscretization, LatticeDiscretizationProperties -from qlbm.tools.exceptions import LatticeException - +from typing import List, Set, Tuple, override import numpy as np - -from typing import List, Set, Tuple, override +from qlbm.lattice.spacetime.properties_base import ( + LatticeDiscretization, + LatticeDiscretizationProperties, +) +from qlbm.tools.exceptions import LatticeException class EquivalenceClass: From 63fea03b3f1d072b24937b89a79aaf813cb7bb66 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Thu, 10 Jul 2025 00:33:48 +0200 Subject: [PATCH 41/92] Add CBSE collision module info --- qlbm/components/common/cbse_collision/__init__.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/qlbm/components/common/cbse_collision/__init__.py b/qlbm/components/common/cbse_collision/__init__.py index e69de29..d835c7c 100644 --- a/qlbm/components/common/cbse_collision/__init__.py +++ b/qlbm/components/common/cbse_collision/__init__.py @@ -0,0 +1,11 @@ +"""Quantum circuits used for computational basis state encodings.""" + +from .cbse_collision import EQCCollisionOperator +from .cbse_permutation import EQCPermutation +from .cbse_redistribution import EQCRedistribution + +__all__ = [ + "EQCCollisionOperator", + "EQCPermutation", + "EQCRedistribution", +] From 5fbf1de88ae2b4b77422c5d99efc27abd2a7b0a5 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Thu, 10 Jul 2025 00:35:42 +0200 Subject: [PATCH 42/92] Include shape in web documentation --- docs/source/code/base_comps.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/source/code/base_comps.rst b/docs/source/code/base_comps.rst index 196441e..370c4f5 100644 --- a/docs/source/code/base_comps.rst +++ b/docs/source/code/base_comps.rst @@ -20,6 +20,9 @@ Lattice Base .. autoclass:: qlbm.lattice.lattices.base.Lattice :members: +.. autoclass:: qlbm.lattice.geometry.shapes.base.Shape + :members: + .. _components_bases: Components Base From 12807bc2dd1e8e69acac8bea6b3cfc340bb63d44 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Fri, 11 Jul 2025 15:41:07 +0200 Subject: [PATCH 43/92] Add lqlga collision operator --- qlbm/components/lqlga/lqlga.py | 12 +++++- qlbm/components/lqlga/reflection.py | 62 +++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 qlbm/components/lqlga/reflection.py diff --git a/qlbm/components/lqlga/lqlga.py b/qlbm/components/lqlga/lqlga.py index 5573900..30aeb23 100644 --- a/qlbm/components/lqlga/lqlga.py +++ b/qlbm/components/lqlga/lqlga.py @@ -7,6 +7,7 @@ from qlbm.components.base import LBMAlgorithm from qlbm.components.lqlga.collision import GenericLQLGACollisionOperator +from qlbm.components.lqlga.reflection import LQLGAReflectionOperator from qlbm.components.lqlga.streaming import LQLGAStreamingOperator from qlbm.lattice.lattices.lqlga_lattice import LQLGALattice @@ -17,13 +18,11 @@ class LQLGA(LBMAlgorithm): def __init__( self, lattice: LQLGALattice, - filter_inside_blocks: bool = True, logger: Logger = getLogger("qlbm"), ): super().__init__(lattice, logger) self.lattice = lattice - self.filter_inside_blocks = filter_inside_blocks self.circuit = self.create_circuit() @@ -35,11 +34,20 @@ def create_circuit(self) -> QuantumCircuit: LQLGAStreamingOperator(self.lattice, self.logger).circuit, inplace=True ) + circuit.compose( + LQLGAReflectionOperator( + self.lattice, self.lattice.shapes["bounceback"], self.logger + ).circuit, + inplace=True, + ) + circuit.compose( GenericLQLGACollisionOperator(self.lattice, self.logger).circuit, inplace=True, ) + return circuit + @override def __str__(self) -> str: return f"[LQLGA on lattice={self.lattice}]" diff --git a/qlbm/components/lqlga/reflection.py b/qlbm/components/lqlga/reflection.py new file mode 100644 index 0000000..cb8df58 --- /dev/null +++ b/qlbm/components/lqlga/reflection.py @@ -0,0 +1,62 @@ +"""Reflection operator for the :class:`.LQLGA` algorithm :cite:`spacetime` that swaps particles one gridpoint at a time.""" + +from logging import Logger, getLogger +from time import perf_counter_ns +from typing import List, Tuple, cast + +from qiskit import QuantumCircuit +from typing_extensions import override + +from qlbm.components.base import LQLGAOperator +from qlbm.components.common.primitives import MCSwap +from qlbm.lattice.geometry.shapes.base import LQLGAShape, Shape +from qlbm.lattice.lattices.lqlga_lattice import LQLGALattice +from qlbm.lattice.spacetime.properties_base import LatticeDiscretization +from qlbm.tools.exceptions import CircuitException + + +class LQLGAReflectionOperator(LQLGAOperator): + def __init__( + self, + lattice: LQLGALattice, + shapes: List[Shape], + logger: Logger = getLogger("qlbm"), + ) -> None: + super().__init__(lattice, logger) + self.shapes = cast(List[LQLGAShape], shapes) + + 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: + discretization = self.lattice.discretization + if discretization == LatticeDiscretization.D1Q2: + return self.__create_circuit_d1q2() + + raise CircuitException(f"Reflection Operator unsupported for {discretization}.") + + def __create_circuit_d1q2(self) -> QuantumCircuit: + circuit = self.lattice.circuit.copy() + + for shape in self.shapes: + for reflection_data in shape.get_lqlga_reflection_data_d1q2(): + circuit.swap( + self.lattice.velocity_index_tuple( + reflection_data.gridpoints[0], + reflection_data.velocity_indices_to_swap[0], + ), + self.lattice.velocity_index_tuple( + reflection_data.gridpoints[1], + reflection_data.velocity_indices_to_swap[1], + ), + ) + return circuit + + @override + def __str__(self) -> str: + return f"[PointWiseLQLGAReflectionOperator for lattice {self.lattice}, shapes {self.shapes}]" From a24fe9b5d28c0c49eb21e82bb1090f0d870743e2 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Fri, 11 Jul 2025 15:41:41 +0200 Subject: [PATCH 44/92] Fix bug in LQLGA negative direction streaming --- qlbm/components/lqlga/streaming.py | 32 +++++++++++++++++------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/qlbm/components/lqlga/streaming.py b/qlbm/components/lqlga/streaming.py index 9700782..d79be95 100644 --- a/qlbm/components/lqlga/streaming.py +++ b/qlbm/components/lqlga/streaming.py @@ -32,23 +32,23 @@ def create_circuit(self): # ! TODO Generalize in 2 and 3D num_gps = self.lattice.num_gridpoints[0] + 1 - gridpoints_to_swap = self.logarithmic_depth_streaming_line_swaps(num_gps) - - for layer in gridpoints_to_swap: - for i, j in layer: - circuit.swap( - self.lattice.velocity_index_flat(i, 0), - self.lattice.velocity_index_flat(j, 0), - ) - circuit.swap( - self.lattice.velocity_index_flat(i, 1), - self.lattice.velocity_index_flat(j, 1), - ) + for direction, velocity_qubit_of_line in enumerate( + self.lattice.get_velocity_qubits_of_line(0) + ): + gridpoints_to_swap = self.logarithmic_depth_streaming_line_swaps( + num_gps, negative_direction=bool(direction) + ) + for layer in gridpoints_to_swap: + for i, j in layer: + circuit.swap( + self.lattice.velocity_index_flat(i, velocity_qubit_of_line), + self.lattice.velocity_index_flat(j, velocity_qubit_of_line), + ) return circuit def logarithmic_depth_streaming_line_swaps( - self, num_gridpoints: int + self, num_gridpoints: int, negative_direction: bool ) -> List[List[Tuple[int, int]]]: if num_gridpoints < 2: return [] @@ -60,7 +60,11 @@ def logarithmic_depth_streaming_line_swaps( layer: List[Tuple[int, int]] = [] for i in range(0, num_gridpoints, 2 * stride): if i + stride < num_gridpoints: - layer.append((i, i + stride)) + layer.append( + (i, i + stride) + if not negative_direction + else (num_gridpoints - 1 - i, num_gridpoints - 1 - i - stride) + ) layers.append(layer) stride *= 2 From b009462ce7b43780aac284475eee8a815a6f418a Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Fri, 11 Jul 2025 15:42:10 +0200 Subject: [PATCH 45/92] Fix typo --- qlbm/lattice/geometry/encodings/spacetime.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qlbm/lattice/geometry/encodings/spacetime.py b/qlbm/lattice/geometry/encodings/spacetime.py index e437053..56cd294 100644 --- a/qlbm/lattice/geometry/encodings/spacetime.py +++ b/qlbm/lattice/geometry/encodings/spacetime.py @@ -7,7 +7,7 @@ class SpaceTimeReflectionData(ABC): - """Base class for all space-time reflectiopn data structures.""" + """Base class for all space-time reflection data structures.""" pass From ef6f27d51b69b830c40f30f9b7c0c64e085f3113 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Fri, 11 Jul 2025 15:42:52 +0200 Subject: [PATCH 46/92] Refactor former collisionless reinitializer to more general form --- qlbm/infra/reinitialize/__init__.py | 4 ++-- .../reinitialize/collisionless_reinitializer.py | 13 +++++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/qlbm/infra/reinitialize/__init__.py b/qlbm/infra/reinitialize/__init__.py index a834c09..abeda1a 100644 --- a/qlbm/infra/reinitialize/__init__.py +++ b/qlbm/infra/reinitialize/__init__.py @@ -1,7 +1,7 @@ """Reinitialize objects for transitioning between time steps of the QLBM algorithm.""" from .base import Reinitializer -from .collisionless_reinitializer import CollisionlessReinitializer +from .collisionless_reinitializer import IdentityReinitializer from .spacetime_reinitializer import SpaceTimeReinitializer -__all__ = ["Reinitializer", "CollisionlessReinitializer", "SpaceTimeReinitializer"] +__all__ = ["Reinitializer", "IdentityReinitializer", "SpaceTimeReinitializer"] diff --git a/qlbm/infra/reinitialize/collisionless_reinitializer.py b/qlbm/infra/reinitialize/collisionless_reinitializer.py index dfb4086..83e6166 100644 --- a/qlbm/infra/reinitialize/collisionless_reinitializer.py +++ b/qlbm/infra/reinitialize/collisionless_reinitializer.py @@ -11,15 +11,16 @@ from typing_extensions import override from qlbm.infra.compiler import CircuitCompiler -from qlbm.lattice import CollisionlessLattice +from qlbm.lattice.lattices.base import Lattice from .base import Reinitializer -class CollisionlessReinitializer(Reinitializer): +class IdentityReinitializer(Reinitializer): r""" - :class:`.CQLBM`-specific implementation of the :class:`.Reinitializer`. + Implementation of the :class:`.Reinitializer` that passes along the statevector to the following time step. + Useful for the :class:`.CQLBM` and :class:`.LQLGA` algorithms. Compatible with both :class:`.QiskitRunner`\ s and :class:`.QulacsRunner`\ s. To generate a new set of initial conditions for the CQLBM algorithm, the reinitializer simply returns the quantum state computed @@ -31,19 +32,19 @@ class CollisionlessReinitializer(Reinitializer): =========================== ====================================================================== Attribute Summary =========================== ====================================================================== - :attr:`lattice` The :class:`.CollisionlessLattice` of the simulated system. + :attr:`lattice` The :class:`.Lattice` of the simulated system. :attr:`compiler` The compiler that converts the novel initial conditions circuits. :attr:`logger` The performance logger, by default ``getLogger("qlbm")`` =========================== ====================================================================== """ - lattice: CollisionlessLattice + lattice: Lattice statevector: Statevector counts: Counts def __init__( self, - lattice: CollisionlessLattice, + lattice: Lattice, compiler: CircuitCompiler, logger: Logger = getLogger("qlbm"), ): From 6dc7187f5454def1a363f2a97c6e33b08c550959 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Fri, 11 Jul 2025 15:43:45 +0200 Subject: [PATCH 47/92] Add LQLGA result class --- qlbm/infra/result/__init__.py | 7 +- qlbm/infra/result/lqlga_result.py | 103 ++++++++++++++++++++++++++++++ qlbm/infra/runner/base.py | 22 +++++-- 3 files changed, 122 insertions(+), 10 deletions(-) create mode 100644 qlbm/infra/result/lqlga_result.py diff --git a/qlbm/infra/result/__init__.py b/qlbm/infra/result/__init__.py index bc012b2..78cfce1 100644 --- a/qlbm/infra/result/__init__.py +++ b/qlbm/infra/result/__init__.py @@ -2,10 +2,7 @@ from .base import QBMResult from .collisionless_result import CollisionlessResult +from .lqlga_result import LQLGAResult from .spacetime_result import SpaceTimeResult -__all__ = [ - "QBMResult", - "CollisionlessResult", - "SpaceTimeResult", -] +__all__ = ["QBMResult", "CollisionlessResult", "SpaceTimeResult", "LQLGAResult"] diff --git a/qlbm/infra/result/lqlga_result.py b/qlbm/infra/result/lqlga_result.py new file mode 100644 index 0000000..f8cd332 --- /dev/null +++ b/qlbm/infra/result/lqlga_result.py @@ -0,0 +1,103 @@ +""":class:`.SpaceTimeQLBM`-specific implementation of the :class:`.QBMResult`.""" + +import re +from os import listdir +from typing import Dict + +import numpy as np +import vtk +from typing_extensions import override +from vtkmodules.util import numpy_support + +from qlbm.lattice.lattices.lqlga_lattice import LQLGALattice +from qlbm.lattice.lattices.spacetime_lattice import SpaceTimeLattice + +from .base import QBMResult + + +class LQLGAResult(QBMResult): + num_steps: int + directory: str + output_file_name: str + lattice: LQLGALattice + + def __init__( + self, + lattice: LQLGALattice, + directory: str, + output_file_name: str = "step", + ) -> None: + super().__init__(lattice, directory, output_file_name) + + @override + def save_timestep_counts( + self, + counts: Dict[str, float], + timestep: int, + create_vis: bool = True, + save_array: bool = False, + ): + dimension_bit_counts = ( + self.lattice.num_gridpoints[0].bit_length(), + self.lattice.num_gridpoints[1].bit_length() + if self.lattice.num_dims > 1 + else 0, + self.lattice.num_gridpoints[2].bit_length() + if self.lattice.num_dims > 2 + else 0, + ) + + if self.lattice.num_dims == 1: + # The second dimension is a dirty rendering trick for VTK and Paraview + count_history = np.zeros((self.lattice.num_gridpoints[0] + 1, 2)) + for count in counts: + count_inverse = count[::-1] + num_vel = self.lattice.num_velocities_per_point + for gp in range( + self.lattice.num_base_qubits + // self.lattice.num_velocities_per_point + ): + num_populations = int( + count_inverse[gp * num_vel : (gp + 1) * num_vel][::-1].count( + "1" # The number of 1s is the number of populations + ) + ) + # Another dirty rendering trick for VTK and Paraview + count_history[gp][0] = count_history[gp][1] = ( + counts[count] * num_populations + ) + self.save_timestep_array( + np.transpose(count_history), + timestep, + create_vis=create_vis, + save_counts_array=save_array, + ) + + @override + def visualize_all_numpy_data(self): + # Filter the algorithm output files + r = re.compile("[a-zA-Z0-9]+_[0-9]+.csv") + for data_file_name in filter(r.match, listdir(self.directory)): + data = np.genfromtxt( + f"{self.directory}/{data_file_name}", + dtype=None, + delimiter=",", + autostrip=True, + ) + vtk_data = numpy_support.numpy_to_vtk( + num_array=data.flatten, deep=True, array_type=vtk.VTK_FLOAT + ) + img = vtk.vtkImageData() + img.SetDimensions( + self.lattice.num_gridpoints[0] + 1, + self.lattice.num_gridpoints[1] + 1 if self.lattice.num_dims > 1 else 1, + self.lattice.num_gridpoints[2] + 1 if self.lattice.num_dims > 2 else 1, + ) + img.GetPointData().SetScalars(vtk_data) + + writer = vtk.vtkXMLImageDataWriter() + writer.SetFileName( + f"{self.paraview_dir}/{data_file_name.split('.')[0]}.vti" + ) + writer.SetInputData(img) + writer.Write() diff --git a/qlbm/infra/runner/base.py b/qlbm/infra/runner/base.py index acb84d2..dae7959 100644 --- a/qlbm/infra/runner/base.py +++ b/qlbm/infra/runner/base.py @@ -10,12 +10,18 @@ from qiskit_aer import AerSimulator from qlbm.infra.reinitialize import ( - CollisionlessReinitializer, + IdentityReinitializer, Reinitializer, SpaceTimeReinitializer, ) -from qlbm.infra.result import CollisionlessResult, QBMResult, SpaceTimeResult +from qlbm.infra.result import ( + CollisionlessResult, + LQLGAResult, + QBMResult, + SpaceTimeResult, +) from qlbm.lattice import CollisionlessLattice, Lattice +from qlbm.lattice.lattices.lqlga_lattice import LQLGALattice from qlbm.lattice.lattices.spacetime_lattice import SpaceTimeLattice from qlbm.tools.exceptions import CircuitException, ResultsException @@ -127,6 +133,10 @@ def new_result(self, output_directory: str, output_file_name: str) -> QBMResult: 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}.") @@ -144,9 +154,11 @@ def new_reinitializer(self) -> Reinitializer: ResultsException If the underlying algorithm does not support reinitialization. """ - if isinstance(self.lattice, CollisionlessLattice): - return CollisionlessReinitializer( - cast(CollisionlessLattice, self.lattice), + if isinstance(self.lattice, CollisionlessLattice) or isinstance( + self.lattice, LQLGALattice + ): + return IdentityReinitializer( + self.lattice, self.config.get_execution_compiler(), self.logger, ) From 46a6b9d5ad0a8aa8be15742bff5b1caf90d5ccb8 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Fri, 11 Jul 2025 15:44:20 +0200 Subject: [PATCH 48/92] Add support for LQLGA geometry parsing --- qlbm/lattice/geometry/encodings/lqlga.py | 30 +++++++++++ qlbm/lattice/geometry/shapes/base.py | 63 ++++++++++++++++++++++++ qlbm/lattice/geometry/shapes/block.py | 16 +++++- qlbm/lattice/lattices/base.py | 1 + qlbm/lattice/lattices/lqlga_lattice.py | 35 ++++++++++++- 5 files changed, 141 insertions(+), 4 deletions(-) create mode 100644 qlbm/lattice/geometry/encodings/lqlga.py diff --git a/qlbm/lattice/geometry/encodings/lqlga.py b/qlbm/lattice/geometry/encodings/lqlga.py new file mode 100644 index 0000000..5fe1de4 --- /dev/null +++ b/qlbm/lattice/geometry/encodings/lqlga.py @@ -0,0 +1,30 @@ +from abc import ABC +from typing import Tuple + + +class LQLGAReflectionData(ABC): + """Base class for all space-time reflectiopn data structures.""" + + pass + + +class LQLGAPointwiseReflectionData(LQLGAReflectionData): + """ + Data structure representing pointwise space-time reflection information for LQLGA geometries. + + Attributes: + gridpoints (Tuple[Tuple[int, ...], Tuple[int, ...]]): + A tuple containing two grid points (as tuples of integers) that are + related by a reflection operation. + velocity_indices_to_swap (Tuple[int, int]): + A tuple of two velocity indices that should be swapped during the + reflection operation. + """ + + def __init__( + self, + gridpoints: Tuple[Tuple[int, ...], Tuple[int, ...]], + velocity_indices_to_swap: Tuple[int, int], + ) -> None: + self.gridpoints = gridpoints + self.velocity_indices_to_swap = velocity_indices_to_swap diff --git a/qlbm/lattice/geometry/shapes/base.py b/qlbm/lattice/geometry/shapes/base.py index 94b4369..74a88a7 100644 --- a/qlbm/lattice/geometry/shapes/base.py +++ b/qlbm/lattice/geometry/shapes/base.py @@ -5,6 +5,7 @@ from stl import mesh +from qlbm.lattice.geometry.encodings.lqlga import LQLGAPointwiseReflectionData from qlbm.lattice.geometry.encodings.spacetime import ( SpaceTimePWReflectionData, SpaceTimeVolumetricReflectionData, @@ -65,6 +66,68 @@ def to_dict(self): pass +class LQLGAShape(Shape): + """Base class for all shapes compatible with the :class:`.LQLGA` algorithm.""" + + def __init__(self, num_grid_qubits: List[int], boundary_condition: str): + super().__init__(num_grid_qubits, boundary_condition) + + def get_lqlga_reflection_data_d1q2_from_points( + self, + gridpoints: List[Tuple[int, ...]], + before_reflection_velocity_index: int, + after_reflection_velocity_index: int, + reflection_increment_from_boundary: Tuple[int, ...], + max_grid_size: int, + ) -> List[LQLGAPointwiseReflectionData]: + """Calculate LQLGA reflection data for D1Q2 lattice from a list of points. + + Parameters + ---------- + gridpoints : List[Tuple[int, ...]] + List of grid points where reflections occur, inside the solid domain. + before_reflection_velocity_index : int + Velocity index before reflection. + after_reflection_velocity_index : int + Velocity index after reflection. + reflection_increment_from_boundary : Tuple[int, ...] + Increment vector from boundary point to reflection destination. + max_grid_size: int + The size of the grid on which reflection is performed, to account for periodicity. + + Returns + ------- + List[LQLGAPointwiseReflectionData] + List of reflection data for each point. + """ + return [ + LQLGAPointwiseReflectionData( + tuple( + [ + gridpoint, + tuple( + (a + b) % max_grid_size + for a, b in zip( + gridpoint, reflection_increment_from_boundary + ) + ), + ], + ), + tuple( + [before_reflection_velocity_index, after_reflection_velocity_index], + ), + ) + for gridpoint in gridpoints + ] + + @abstractmethod + def get_lqlga_reflection_data_d1q2( + self, + ) -> List[LQLGAPointwiseReflectionData]: + """Calculate space-time reflection data for :math:`D_1Q_2` :class:`.LQLGA`.""" + pass + + class SpaceTimeShape(Shape): """Base class for all shapes compatible with the :class:`.SpaceTimeQLBM` algorithm.""" diff --git a/qlbm/lattice/geometry/shapes/block.py b/qlbm/lattice/geometry/shapes/block.py index e860ddc..81a3c3f 100644 --- a/qlbm/lattice/geometry/shapes/block.py +++ b/qlbm/lattice/geometry/shapes/block.py @@ -18,12 +18,12 @@ SpaceTimePWReflectionData, SpaceTimeVolumetricReflectionData, ) -from qlbm.lattice.geometry.shapes.base import SpaceTimeShape +from qlbm.lattice.geometry.shapes.base import LQLGAShape, SpaceTimeShape from qlbm.lattice.spacetime.properties_base import SpaceTimeLatticeBuilder from qlbm.tools.utils import bit_value, dimension_letter, flatten, get_qubits_to_invert -class Block(SpaceTimeShape): +class Block(SpaceTimeShape, LQLGAShape): r""" Contains information required for the generation of boundary conditions for an axis-parallel cuboid obstacle. @@ -707,6 +707,18 @@ def get_d2q4_surfaces(self) -> List[List[List[Tuple[int, ...]]]]: return surfaces + @override + def get_lqlga_reflection_data_d1q2(self): + return self.get_lqlga_reflection_data_d1q2_from_points( + [tuple([self.bounds[0][0]])], + 0, + 1, + tuple([-1]), + 2 ** self.num_grid_qubits[0], + ) + self.get_lqlga_reflection_data_d1q2_from_points( + [tuple([self.bounds[0][1]])], 1, 0, tuple([1]), 2 ** self.num_grid_qubits[0] + ) + @override def contains_gridpoint(self, gridpoint: Tuple[int, ...]) -> bool: return all( diff --git a/qlbm/lattice/lattices/base.py b/qlbm/lattice/lattices/base.py index e820c81..4e4405f 100644 --- a/qlbm/lattice/lattices/base.py +++ b/qlbm/lattice/lattices/base.py @@ -243,6 +243,7 @@ def __parse_input_dict( for dim in range(num_dimensions): dim_index = dimension_letter(dim) + # ! TODO move to after parsing if not is_two_pow(lattice_dict["dim"][dim_index]): # type: ignore raise LatticeException( f"Lattice {dim_index}-dimension has a number of grid points that is not divisible by 2." diff --git a/qlbm/lattice/lattices/lqlga_lattice.py b/qlbm/lattice/lattices/lqlga_lattice.py index ab3a637..ca00d51 100644 --- a/qlbm/lattice/lattices/lqlga_lattice.py +++ b/qlbm/lattice/lattices/lqlga_lattice.py @@ -130,7 +130,38 @@ def velocity_index_tuple(self, gridpoint: Tuple[int, ...], velocity: int) -> int raise LatticeException( f"Velocity {velocity} is out of bounds for the lattice with {self.num_velocities_per_point} velocities per point." ) - return self.gridpoint_index_tuple(gridpoint) + velocity + return ( + self.gridpoint_index_tuple(gridpoint) * self.num_velocities_per_point + + velocity + ) + + def get_velocity_qubits_of_line(self, line_index: int) -> Tuple[int, int]: + r""" + Returns the velocity qubits of the positive and negative directions of a streaming line. + + Assumes that the lattice follows a :math:`D_{d}Q_{q}` discretization, where if :math:`q` is even, there are + :math:`\lceil \frac{q}{2} \rceil` streaming lines. This is to be generalized in the future. + + Parameters + ---------- + line_index : int + The index of the line to get the velocity qubits for. + + Returns + ------- + List[int] + The list of velocity qubits for the specified line. + """ + if line_index < 0 or line_index > self.num_velocities_per_point // 2: + raise LatticeException( + f"Streaming Line index {line_index} is out of bounds for the lattice with {self.num_velocities_per_point // 2} lines." + ) + return ( + (self.num_velocities_per_point % 2) + line_index, + (self.num_velocities_per_point % 2) + + line_index + + self.num_velocities_per_point // 2, + ) @override def logger_name(self) -> str: @@ -139,4 +170,4 @@ def logger_name(self) -> str: gp_string += f"{gp + 1}" if c < len(self.num_gridpoints) - 1: gp_string += "x" - return f"lqlga-d{self.num_dims}-q{LatticeDiscretizationProperties.get_num_velocities(self.discretization)}-{gp_string}" + return f"lqlga-d{self.num_dims}-q{self.num_velocities_per_point}-{gp_string}" From 9b2948686188361cce4cae610d66ee8692158c61 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Fri, 11 Jul 2025 18:34:27 +0200 Subject: [PATCH 49/92] Add missing parameter in LQLGA streaming tests --- test/unit/lqlga/circuits/streaming_test.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/test/unit/lqlga/circuits/streaming_test.py b/test/unit/lqlga/circuits/streaming_test.py index 7ecf0e8..444d474 100644 --- a/test/unit/lqlga/circuits/streaming_test.py +++ b/test/unit/lqlga/circuits/streaming_test.py @@ -21,7 +21,7 @@ def verify_streaming_line_correctness( @pytest.mark.parametrize("size", list(map(lambda x: 2**x, list(range(10))))) def test_streaming_line_creation_power_of_2(lattice_d1q2_8, size): operator = LQLGAStreamingOperator(lattice_d1q2_8) - swaps = operator.logarithmic_depth_streaming_line_swaps(size) + swaps = operator.logarithmic_depth_streaming_line_swaps(size, False) assert verify_streaming_line_correctness(size, swaps) @@ -29,7 +29,7 @@ def test_streaming_line_creation_power_of_2(lattice_d1q2_8, size): @pytest.mark.parametrize("size", list(map(lambda x: 2**x + 1, list(range(1, 10))))) def test_streaming_line_creation_off_power_of_2_positive(lattice_d1q2_8, size): operator = LQLGAStreamingOperator(lattice_d1q2_8) - swaps = operator.logarithmic_depth_streaming_line_swaps(size) + swaps = operator.logarithmic_depth_streaming_line_swaps(size, False) assert verify_streaming_line_correctness(size, swaps) @@ -37,13 +37,14 @@ def test_streaming_line_creation_off_power_of_2_positive(lattice_d1q2_8, size): @pytest.mark.parametrize("size", list(map(lambda x: 2**x - 1, list(range(3, 10))))) def test_streaming_line_creation__off_power_2_negative(lattice_d1q2_8, size): operator = LQLGAStreamingOperator(lattice_d1q2_8) - swaps = operator.logarithmic_depth_streaming_line_swaps(size) + swaps = operator.logarithmic_depth_streaming_line_swaps(size, False) assert verify_streaming_line_correctness(size, swaps) + @pytest.mark.parametrize("size", list(range(7, 16))) def test_streaming_line_creation_intermediate(lattice_d1q2_8, size): operator = LQLGAStreamingOperator(lattice_d1q2_8) - swaps = operator.logarithmic_depth_streaming_line_swaps(size) + swaps = operator.logarithmic_depth_streaming_line_swaps(size, False) - assert verify_streaming_line_correctness(size, swaps) \ No newline at end of file + assert verify_streaming_line_correctness(size, swaps) From 22a65a65ce1c2004132099739406983fd2405f27 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Fri, 11 Jul 2025 23:02:54 +0200 Subject: [PATCH 50/92] Remove unused import --- qlbm/components/lqlga/reflection.py | 3 +-- qlbm/infra/result/lqlga_result.py | 1 - qlbm/lattice/geometry/encodings/lqlga.py | 3 ++- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/qlbm/components/lqlga/reflection.py b/qlbm/components/lqlga/reflection.py index cb8df58..0ec7786 100644 --- a/qlbm/components/lqlga/reflection.py +++ b/qlbm/components/lqlga/reflection.py @@ -2,13 +2,12 @@ from logging import Logger, getLogger from time import perf_counter_ns -from typing import List, Tuple, cast +from typing import List, cast from qiskit import QuantumCircuit from typing_extensions import override from qlbm.components.base import LQLGAOperator -from qlbm.components.common.primitives import MCSwap from qlbm.lattice.geometry.shapes.base import LQLGAShape, Shape from qlbm.lattice.lattices.lqlga_lattice import LQLGALattice from qlbm.lattice.spacetime.properties_base import LatticeDiscretization diff --git a/qlbm/infra/result/lqlga_result.py b/qlbm/infra/result/lqlga_result.py index f8cd332..1acda3e 100644 --- a/qlbm/infra/result/lqlga_result.py +++ b/qlbm/infra/result/lqlga_result.py @@ -10,7 +10,6 @@ from vtkmodules.util import numpy_support from qlbm.lattice.lattices.lqlga_lattice import LQLGALattice -from qlbm.lattice.lattices.spacetime_lattice import SpaceTimeLattice from .base import QBMResult diff --git a/qlbm/lattice/geometry/encodings/lqlga.py b/qlbm/lattice/geometry/encodings/lqlga.py index 5fe1de4..2c5e2bd 100644 --- a/qlbm/lattice/geometry/encodings/lqlga.py +++ b/qlbm/lattice/geometry/encodings/lqlga.py @@ -12,7 +12,8 @@ class LQLGAPointwiseReflectionData(LQLGAReflectionData): """ Data structure representing pointwise space-time reflection information for LQLGA geometries. - Attributes: + Attributes + ---------- gridpoints (Tuple[Tuple[int, ...], Tuple[int, ...]]): A tuple containing two grid points (as tuples of integers) that are related by a reflection operation. From 44ff086794e7662d010c1b8c861e045da1874b86 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 14 Jul 2025 14:31:48 +0200 Subject: [PATCH 51/92] Remove deprecated files --- .../collisionless_reinitializer.py | 91 ------------------- 1 file changed, 91 deletions(-) delete mode 100644 qlbm/infra/reinitialize/collisionless_reinitializer.py diff --git a/qlbm/infra/reinitialize/collisionless_reinitializer.py b/qlbm/infra/reinitialize/collisionless_reinitializer.py deleted file mode 100644 index 83e6166..0000000 --- a/qlbm/infra/reinitialize/collisionless_reinitializer.py +++ /dev/null @@ -1,91 +0,0 @@ -""":class:`.CQLBM`-specific implementation of the :class:`.Reinitializer`.""" - -from logging import Logger, getLogger - -from qiskit import QuantumCircuit as QiskitQC -from qiskit.circuit.library import Initialize -from qiskit.quantum_info import Statevector -from qiskit.result import Counts -from qiskit_aer.backends.aer_simulator import AerBackend -from qulacs import QuantumCircuit as QulacsQC -from typing_extensions import override - -from qlbm.infra.compiler import CircuitCompiler -from qlbm.lattice.lattices.base import Lattice - -from .base import Reinitializer - - -class IdentityReinitializer(Reinitializer): - r""" - Implementation of the :class:`.Reinitializer` that passes along the statevector to the following time step. - - Useful for the :class:`.CQLBM` and :class:`.LQLGA` algorithms. - Compatible with both :class:`.QiskitRunner`\ s and :class:`.QulacsRunner`\ s. - To generate a new set of initial conditions for the CQLBM algorithm, - the reinitializer simply returns the quantum state computed - at the end of the previous simulation. - This allows the reuse of a single quantum circuit for the simulation - of arbitrarily many time steps. - No copy of the statevector is required. - - =========================== ====================================================================== - Attribute Summary - =========================== ====================================================================== - :attr:`lattice` The :class:`.Lattice` of the simulated system. - :attr:`compiler` The compiler that converts the novel initial conditions circuits. - :attr:`logger` The performance logger, by default ``getLogger("qlbm")`` - =========================== ====================================================================== - """ - - lattice: Lattice - statevector: Statevector - counts: Counts - - def __init__( - self, - lattice: Lattice, - compiler: CircuitCompiler, - logger: Logger = getLogger("qlbm"), - ): - super().__init__(lattice, compiler, logger) - self.lattice = lattice - self.logger = logger - - def reinitialize( - self, - statevector: Statevector, - counts: Counts, - backend: AerBackend | None = None, - optimization_level: int = 0, - ) -> QiskitQC | QulacsQC: - """ - Returns the provided ``statevector`` as a new Qiskit ``Initialize`` object that can be prepended to the time step circuit to resume simulation. - - Parameters - ---------- - statevector : Statevector - The statevector at the end of the simulation. - counts : Counts - Ignored. - backend : AerBackend | None - Ignored. - optimization_level : int, optional - Ignored. - - Returns - ------- - QiskitQC | QulacsQC - A Qiskit ``Initialize`` object. - """ - circuit = self.lattice.circuit.copy() - circuit.compose( - Initialize(statevector), - inplace=True, - qubits=range(circuit.num_qubits), - ) - return circuit - - @override - def requires_statevector(self) -> bool: - return True From 263a91f803e5ad2f274e7972996fcae24947674d Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 14 Jul 2025 14:32:36 +0200 Subject: [PATCH 52/92] Update component module header files --- qlbm/components/__init__.py | 21 +++++++++++++++++++++ qlbm/components/common/__init__.py | 4 ++++ qlbm/components/lqlga/__init__.py | 17 +++++++++++++++++ 3 files changed, 42 insertions(+) diff --git a/qlbm/components/__init__.py b/qlbm/components/__init__.py index 56a2846..3c79cd7 100644 --- a/qlbm/components/__init__.py +++ b/qlbm/components/__init__.py @@ -25,11 +25,23 @@ ) from .common import ( EmptyPrimitive, + EQCCollisionOperator, + EQCPermutation, + EQCRedistribution, +) +from .lqlga import ( + LQLGA, + GenericLQLGACollisionOperator, + LQGLAInitialConditions, + LQLGAGridVelocityMeasurement, + LQLGAReflectionOperator, + LQLGAStreamingOperator, ) __all__ = [ "QuantumComponent", "LBMPrimitive", + "GenericLQLGACollisionOperator", "LBMOperator", "CQLBMOperator", "SpaceTimeOperator", @@ -48,4 +60,13 @@ "SpecularReflectionOperator", "BounceBackReflectionOperator", "CQLBM", + "GenericLQLGACollisionOperator", + "LQGLAInitialConditions", + "LQLGA", + "LQLGAGridVelocityMeasurement", + "LQLGAReflectionOperator", + "LQLGAStreamingOperator", + "EQCCollisionOperator", + "EQCPermutation", + "EQCRedistribution", ] diff --git a/qlbm/components/common/__init__.py b/qlbm/components/common/__init__.py index 5ca19c9..75ca482 100644 --- a/qlbm/components/common/__init__.py +++ b/qlbm/components/common/__init__.py @@ -1,9 +1,13 @@ """Common primitives used for multiple encodings.""" +from .cbse_collision import EQCCollisionOperator, EQCPermutation, EQCRedistribution from .primitives import ( EmptyPrimitive, ) __all__ = [ "EmptyPrimitive", + "EQCCollisionOperator", + "EQCPermutation", + "EQCRedistribution", ] diff --git a/qlbm/components/lqlga/__init__.py b/qlbm/components/lqlga/__init__.py index e69de29..11a630a 100644 --- a/qlbm/components/lqlga/__init__.py +++ b/qlbm/components/lqlga/__init__.py @@ -0,0 +1,17 @@ +"""Implementation of the Linear Quantum Lattice Gas Algorithm (LQLGA) algorithm.""" + +from .collision import GenericLQLGACollisionOperator +from .initial import LQGLAInitialConditions +from .lqlga import LQLGA +from .measurement import LQLGAGridVelocityMeasurement +from .reflection import LQLGAReflectionOperator +from .streaming import LQLGAStreamingOperator + +__all__ = [ + "GenericLQLGACollisionOperator", + "LQGLAInitialConditions", + "LQLGA", + "LQLGAGridVelocityMeasurement", + "LQLGAReflectionOperator", + "LQLGAStreamingOperator", +] From 5176a7d1bbf99c935d69a34fd411af73b313db99 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 14 Jul 2025 14:33:22 +0200 Subject: [PATCH 53/92] Update lattice module header files --- qlbm/lattice/__init__.py | 12 ++++++++++++ qlbm/lattice/eqc/__init__.py | 9 +++++++++ qlbm/lattice/geometry/__init__.py | 4 ++++ qlbm/lattice/geometry/encodings/__init__.py | 3 +++ qlbm/lattice/lattices/__init__.py | 3 ++- 5 files changed, 30 insertions(+), 1 deletion(-) diff --git a/qlbm/lattice/__init__.py b/qlbm/lattice/__init__.py index 368b435..b709602 100644 --- a/qlbm/lattice/__init__.py +++ b/qlbm/lattice/__init__.py @@ -9,16 +9,28 @@ from .geometry.shapes.block import ( Block, ) +from .geometry.shapes.circle import ( + Circle, +) from .lattices import CollisionlessLattice, Lattice +from .lattices.lqlga_lattice import LQLGALattice from .lattices.spacetime_lattice import SpaceTimeLattice +from .spacetime.properties_base import ( + LatticeDiscretization, + LatticeDiscretizationProperties, +) __all__ = [ "Lattice", "CollisionlessLattice", "SpaceTimeLattice", + "LQLGALattice", "DimensionalReflectionData", "ReflectionWall", "ReflectionPoint", "ReflectionResetEdge", "Block", + "Circle", + "LatticeDiscretization", + "LatticeDiscretizationProperties", ] diff --git a/qlbm/lattice/eqc/__init__.py b/qlbm/lattice/eqc/__init__.py index e69de29..fc8ac86 100644 --- a/qlbm/lattice/eqc/__init__.py +++ b/qlbm/lattice/eqc/__init__.py @@ -0,0 +1,9 @@ +"""Utilities for the equivalence class logic handling in LGA-based algorithms.""" + +from .eqc import EquivalenceClass +from .eqc_generator import EquivalenceClassGenerator + +__all__ = [ + "EquivalenceClass", + "EquivalenceClassGenerator", +] diff --git a/qlbm/lattice/geometry/__init__.py b/qlbm/lattice/geometry/__init__.py index 54d9e72..d78094e 100644 --- a/qlbm/lattice/geometry/__init__.py +++ b/qlbm/lattice/geometry/__init__.py @@ -2,6 +2,8 @@ from .encodings import ( DimensionalReflectionData, + LQLGAPointwiseReflectionData, + LQLGAReflectionData, ReflectionPoint, ReflectionResetEdge, ReflectionWall, @@ -21,4 +23,6 @@ "SpaceTimeVolumetricReflectionData", "Block", "Circle", + "LQLGAPointwiseReflectionData", + "LQLGAReflectionData", ] diff --git a/qlbm/lattice/geometry/encodings/__init__.py b/qlbm/lattice/geometry/encodings/__init__.py index 293aa9d..462e32c 100644 --- a/qlbm/lattice/geometry/encodings/__init__.py +++ b/qlbm/lattice/geometry/encodings/__init__.py @@ -6,6 +6,7 @@ ReflectionResetEdge, ReflectionWall, ) +from .lqlga import LQLGAPointwiseReflectionData, LQLGAReflectionData from .spacetime import ( SpaceTimeDiagonalReflectionData, SpaceTimePWReflectionData, @@ -20,4 +21,6 @@ "SpaceTimePWReflectionData", "SpaceTimeVolumetricReflectionData", "SpaceTimeDiagonalReflectionData", + "LQLGAPointwiseReflectionData", + "LQLGAReflectionData", ] diff --git a/qlbm/lattice/lattices/__init__.py b/qlbm/lattice/lattices/__init__.py index 9e74f3d..7a92929 100644 --- a/qlbm/lattice/lattices/__init__.py +++ b/qlbm/lattice/lattices/__init__.py @@ -2,6 +2,7 @@ from .base import Lattice from .collisionless_lattice import CollisionlessLattice +from .lqlga_lattice import LQLGALattice from .spacetime_lattice import SpaceTimeLattice -__all__ = ["Lattice", "CollisionlessLattice", "SpaceTimeLattice"] +__all__ = ["Lattice", "CollisionlessLattice", "SpaceTimeLattice", "LQLGALattice"] From eda0947b437ec0cbaaec548011ef85c13267dca4 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 14 Jul 2025 14:34:48 +0200 Subject: [PATCH 54/92] Update result documentation --- qlbm/infra/result/collisionless_result.py | 8 +++++- qlbm/infra/result/lqlga_result.py | 33 +++++++++++++++-------- qlbm/infra/result/spacetime_result.py | 8 +++++- 3 files changed, 36 insertions(+), 13 deletions(-) diff --git a/qlbm/infra/result/collisionless_result.py b/qlbm/infra/result/collisionless_result.py index 070546c..ea9b88c 100644 --- a/qlbm/infra/result/collisionless_result.py +++ b/qlbm/infra/result/collisionless_result.py @@ -25,15 +25,21 @@ class CollisionlessResult(QBMResult): =========================== ====================================================================== :attr:`lattice` The :class:`.CollisionlessLattice` of the simulated system. :attr:`directory` The directory to which the results outputs data to. - :attr:`paraview_dir` The subdirectory under ``directory`` which stores the Paraview files. :attr:`output_file_name` The root name for files containing time step artifacts, by default "step". =========================== ====================================================================== """ num_steps: int + """The time step to which this result corresponds.""" + directory: str + """The output directory for the results.""" + output_file_name: str + """The name of the file to output the artifacts to.""" + lattice: CollisionlessLattice + """The lattice the result corresponds to.""" def __init__( self, diff --git a/qlbm/infra/result/lqlga_result.py b/qlbm/infra/result/lqlga_result.py index 1acda3e..cd6613b 100644 --- a/qlbm/infra/result/lqlga_result.py +++ b/qlbm/infra/result/lqlga_result.py @@ -1,4 +1,4 @@ -""":class:`.SpaceTimeQLBM`-specific implementation of the :class:`.QBMResult`.""" +""":class:`.LQLGA`-specific implementation of the :class:`.QBMResult`.""" import re from os import listdir @@ -15,10 +15,31 @@ class LQLGAResult(QBMResult): + """ + :class:`.LQLGA`-specific implementation of the :class:`.QBMResult`. + + Processes counts sampled from :class:`.LQLGAGridVelocityMeasurement` primitives. + + =========================== ====================================================================== + Attribute Summary + =========================== ====================================================================== + :attr:`lattice` The :class:`.LQLGA` of the simulated system. + :attr:`directory` The directory to which the results outputs data to. + :attr:`output_file_name` The root name for files containing time step artifacts, by default "step". + =========================== ====================================================================== + """ + num_steps: int + """The time step to which this result corresponds.""" + directory: str + """The output directory for the results.""" + output_file_name: str + """The name of the file to output the artifacts to.""" + lattice: LQLGALattice + """The lattice the result corresponds to.""" def __init__( self, @@ -36,16 +57,6 @@ def save_timestep_counts( create_vis: bool = True, save_array: bool = False, ): - dimension_bit_counts = ( - self.lattice.num_gridpoints[0].bit_length(), - self.lattice.num_gridpoints[1].bit_length() - if self.lattice.num_dims > 1 - else 0, - self.lattice.num_gridpoints[2].bit_length() - if self.lattice.num_dims > 2 - else 0, - ) - if self.lattice.num_dims == 1: # The second dimension is a dirty rendering trick for VTK and Paraview count_history = np.zeros((self.lattice.num_gridpoints[0] + 1, 2)) diff --git a/qlbm/infra/result/spacetime_result.py b/qlbm/infra/result/spacetime_result.py index bed4691..60a3506 100644 --- a/qlbm/infra/result/spacetime_result.py +++ b/qlbm/infra/result/spacetime_result.py @@ -25,15 +25,21 @@ class SpaceTimeResult(QBMResult): =========================== ====================================================================== :attr:`lattice` The :class:`.SpaceTimeLattice` of the simulated system. :attr:`directory` The directory to which the results outputs data to. - :attr:`paraview_dir` The subdirectory under ``directory`` which stores the Paraview files. :attr:`output_file_name` The root name for files containing time step artifacts, by default "step". =========================== ====================================================================== """ num_steps: int + """The time step to which this result corresponds.""" + directory: str + """The output directory for the results.""" + output_file_name: str + """The name of the file to output the artifacts to.""" + lattice: SpaceTimeLattice + """The lattice the result corresponds to.""" def __init__( self, From c576fa5eb947ef211ded7e4f2e791359f3454f76 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 14 Jul 2025 14:35:34 +0200 Subject: [PATCH 55/92] Update LQLGA component documentation --- qlbm/components/lqlga/collision.py | 8 +++++++- qlbm/components/lqlga/initial.py | 18 ++++++++++++++++++ qlbm/components/lqlga/lqlga.py | 12 ++++++++++++ qlbm/components/lqlga/measurement.py | 6 ++++++ qlbm/components/lqlga/reflection.py | 21 +++++++++++++++++++++ qlbm/components/lqlga/streaming.py | 24 ++++++++++++++++++++++++ 6 files changed, 88 insertions(+), 1 deletion(-) diff --git a/qlbm/components/lqlga/collision.py b/qlbm/components/lqlga/collision.py index 6aa2ac7..3efbcf6 100644 --- a/qlbm/components/lqlga/collision.py +++ b/qlbm/components/lqlga/collision.py @@ -1,4 +1,4 @@ -"""Collision operators for the :class:`.SpaceTimeQLBM` algorithm :cite:`spacetime`.""" +"""Collision operators for the :class:`.LQLGA` algorithm.""" from logging import Logger, getLogger from time import perf_counter_ns @@ -12,6 +12,12 @@ class GenericLQLGACollisionOperator(LQLGAOperator): + """ + Equivalence class-based LGA collision operator for the :class:`.LQLGA` algorithm. + + This operator applies the :class:`.EQCCollisionOperator` operator to all velocity qubits at each grid point. + """ + def __init__( self, lattice: LQLGALattice, diff --git a/qlbm/components/lqlga/initial.py b/qlbm/components/lqlga/initial.py index b15754b..ae91b0a 100644 --- a/qlbm/components/lqlga/initial.py +++ b/qlbm/components/lqlga/initial.py @@ -1,3 +1,5 @@ +"""Initial conditions for the :class:`.LQLGA` algorithm.""" + from logging import Logger, getLogger from time import perf_counter_ns from typing import List, Tuple @@ -9,6 +11,21 @@ class LQGLAInitialConditions(LBMPrimitive): + """ + Primitive for setting initial conditions in the :class:`.LQLGA` algorithm. + + This operator allows the construction of arbitrary deterministic initial conditions for the LQLGA algorithm. + The number of gates required by this operator is equal to the number of enabled velocity qubits across all grid points. + The depth of the circuit is 1, as all gates are applied in parallel at each grid point. + """ + + grid_data: List[Tuple[Tuple[int, ...], Tuple[bool, ...]]] + """ + Grid data for the initial conditions, where each tuple contains: + #. A tuple of grid point indices (e.g., `(x, y, z)`). + #. A tuple of booleans indicating which velocity qubits are enabled at that grid point. + """ + def __init__( self, lattice: LQLGALattice, @@ -27,6 +44,7 @@ def __init__( f"Creating circuit {str(self)} took {perf_counter_ns() - circuit_creation_start_time} (ns)" ) + @override def create_circuit(self): circuit = self.lattice.circuit.copy() diff --git a/qlbm/components/lqlga/lqlga.py b/qlbm/components/lqlga/lqlga.py index 30aeb23..20ed5aa 100644 --- a/qlbm/components/lqlga/lqlga.py +++ b/qlbm/components/lqlga/lqlga.py @@ -13,6 +13,18 @@ class LQLGA(LBMAlgorithm): + r""" + Implementation of the Linear Quantum Lattice Gas Algorithm (LQLGA). + + For a lattice with :math:`N_g` gridpoints and :math:`q` discrete velocities, + LQLGA requires exactly :math:`N_g \cdot q` qubits. + + That is exactly equal to the number of classical bits required for one + deterministic run of the classical LGA algorithm. + + More information about this algorithm can be found in :cite:t:`lqlga1`, :cite:t:`lqlga2`, and :cite:t:`spacetime2`. + """ + lattice: LQLGALattice def __init__( diff --git a/qlbm/components/lqlga/measurement.py b/qlbm/components/lqlga/measurement.py index e9b6b65..5be72d9 100644 --- a/qlbm/components/lqlga/measurement.py +++ b/qlbm/components/lqlga/measurement.py @@ -10,6 +10,12 @@ class LQLGAGridVelocityMeasurement(LQLGAOperator): + """ + Measurement operator for the :class:`.LQLGA` algorithm. + + This operator measures the velocity qubits at each grid point in the LQLGA lattice. + """ + def __init__( self, lattice: LQLGALattice, diff --git a/qlbm/components/lqlga/reflection.py b/qlbm/components/lqlga/reflection.py index 0ec7786..96cb589 100644 --- a/qlbm/components/lqlga/reflection.py +++ b/qlbm/components/lqlga/reflection.py @@ -15,6 +15,27 @@ class LQLGAReflectionOperator(LQLGAOperator): + """ + Operator implementing reflection in the :class:`.LQLGA` algorithm. + + Reflections in this algorithm can be entirely implemented by swap gates. + The number of gates scales with the number of grridpoints of the solid geometry. + The depth of the operator is 1. + + ============================ ====================================================================== + Attribute Summary + ============================ ====================================================================== + :attr:`lattice` The lattice the operator acts on. + :attr:`shapes` A list of boundary-conditioned shapes. + :attr:`logger` The performance logger, by default ``getLogger("qlbm")``. + ============================ ====================================================================== + """ + + shapes: List[LQLGAShape] + """ + A list of shapes that require reflection at the boundaries. + """ + def __init__( self, lattice: LQLGALattice, diff --git a/qlbm/components/lqlga/streaming.py b/qlbm/components/lqlga/streaming.py index d79be95..488e9f0 100644 --- a/qlbm/components/lqlga/streaming.py +++ b/qlbm/components/lqlga/streaming.py @@ -11,6 +11,14 @@ class LQLGAStreamingOperator(LQLGAOperator): + """ + Streaming operator for the :class:`.LQLGA` algorithm. + + Streaming is implemented by a series of swap gates as described in :cite:`spacetime`. + The number of gates scales linearly with size of the grid, + while the depth scales logarithmically. + """ + def __init__( self, lattice: LQLGALattice, @@ -50,6 +58,22 @@ def create_circuit(self): def logarithmic_depth_streaming_line_swaps( self, num_gridpoints: int, negative_direction: bool ) -> List[List[Tuple[int, int]]]: + """ + + Implements the logarithmic depth streaming line permuation as described in Section 4 of :cite:`spacetime`. + + Parameters + ---------- + num_gridpoints : int + The number of gridpoints in the streaming line. + negative_direction : bool + Whether streaming occurs in the negative direction (i.e., from high to low indices). + + Returns + ------- + List[List[Tuple[int, int]]] + A list of layers, where each layer is a list of tuples representing pairs of gridpoints to swap. + """ if num_gridpoints < 2: return [] From b596eedc4346ae83616556d4a43a75a062d1dbbb Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 14 Jul 2025 14:36:12 +0200 Subject: [PATCH 56/92] Update CBSE collision documentation --- .../common/cbse_collision/cbse_collision.py | 24 +++++++ .../common/cbse_collision/cbse_permutation.py | 44 ++++++++++-- .../cbse_collision/cbse_redistribution.py | 71 +++++++++++++++++-- 3 files changed, 131 insertions(+), 8 deletions(-) diff --git a/qlbm/components/common/cbse_collision/cbse_collision.py b/qlbm/components/common/cbse_collision/cbse_collision.py index 5ceadc9..947f94e 100644 --- a/qlbm/components/common/cbse_collision/cbse_collision.py +++ b/qlbm/components/common/cbse_collision/cbse_collision.py @@ -23,6 +23,30 @@ class EQCCollisionOperator(LBMPrimitive): + """ + Collision operator based on the equivalence class abstraction described in section 5 of :cite:`spacetime2`. + + Consists of a permutation, redistribution, and inverse permutation of the velocity qubits. + This operator is designed to be applied to a single velocity register, which can be repeated depending on the encoding. + Used in the :class:`.GenericSpaceTimeCollisionOperator` and :class:`.GenericLQLGACollisionOperator`. + + ========================= ====================================================================== + Attribute Summary + ========================= ====================================================================== + :attr:`discretization` The discretization for which this collision operator is defined. + :attr:`num_velocities` The number of velocities in the discretization. + ========================= ====================================================================== + """ + + discretization: LatticeDiscretization + """ + The discretization of the lattice for which this collision operator is defined. + """ + + num_velocities: int + """ + The number of velocities in the discretization.""" + def __init__( self, discretization: LatticeDiscretization, diff --git a/qlbm/components/common/cbse_collision/cbse_permutation.py b/qlbm/components/common/cbse_collision/cbse_permutation.py index d3ca106..9543e33 100644 --- a/qlbm/components/common/cbse_collision/cbse_permutation.py +++ b/qlbm/components/common/cbse_collision/cbse_permutation.py @@ -1,3 +1,5 @@ +"""Permutations of states belonging to equivalence classes, based on the computational basis state encoding.""" + from logging import Logger, getLogger from time import perf_counter_ns from typing import override @@ -11,11 +13,45 @@ class EQCPermutation(LBMPrimitive): - """Permutes states belonging to equivalence classes onto pre-determined basis states. + """Applies a permutation to the velocity qubits of an equivalence Sclass in the CBSE encoding. + + This is used as part of the PRP collision operator described in section 5 of :cite:`spacetime2`. + Utilized in the :class:`.EQCCollisionOperator`. + + ============================== ================================================================= + Attribute Summary + ============================== ================================================================= + :attr:`equivalence_class` The equivalence class of the operator. + :attr:`inverse` Whether to apply the inverse permutation. + ============================== ================================================================= + + Example usage: + + .. plot:: + :include-source: + + from qlbm.components.common import EQCPermutation + from qlbm.lattice import LatticeDiscretization + from qlbm.lattice.eqc import EquivalenceClassGenerator + + # Generate some equivalence classes + eqcs = EquivalenceClassGenerator( + LatticeDiscretization.D3Q6 + ).generate_equivalence_classes() - Precedes redistribution. + # Select one at random and draw its circuit + EQCPermutation(eqcs.pop(), inverse=False).circuit.draw("mpl") - WIP. + """ + + equivalence_class: EquivalenceClass + """ + The equivalence class for which the permutation is defined. + """ + + inverse: bool + """ + Whether to apply the inverse permutation. """ def __init__( @@ -123,4 +159,4 @@ def __create_circuit_d3q6(self): @override def __str__(self) -> str: - return f"[EQCPermutation for eqc {self.equivalence_class}]" \ No newline at end of file + return f"[EQCPermutation for eqc {self.equivalence_class}]" diff --git a/qlbm/components/common/cbse_collision/cbse_redistribution.py b/qlbm/components/common/cbse_collision/cbse_redistribution.py index 74208c2..4c0a0ac 100644 --- a/qlbm/components/common/cbse_collision/cbse_redistribution.py +++ b/qlbm/components/common/cbse_collision/cbse_redistribution.py @@ -1,3 +1,5 @@ +"""Permutations of states belonging to equivalence classes, based on the computational basis state encoding.""" + from logging import Logger, getLogger from time import perf_counter_ns from typing import override @@ -13,16 +15,77 @@ class EQCRedistribution(LBMPrimitive): """ - Redistributes the amplitudes of the states belonging to an equivalence class evenly across all other equivalent states. + Redistribution operator for equivalence classes in the CBSE encoding. + + The operator is mathematically described in section 4 of :cite:`spacetime2`. + Redistribution is applied before and after permutations, and consists of a controlled + unitary operator composed of a discrete Fourier transform (DFT)-block matrix. + + + ========================= ====================================================================== + Attribute Summary + ========================= ====================================================================== + :attr:`equivalence_class` The equivalence class of the operator. + :attr:`decompose_block` Whether to decompose the DFT block into a circuit. + ========================= ====================================================================== + + Example usage: + + .. plot:: + :include-source: + + from qlbm.components.common import EQCRedistribution + from qlbm.lattice import LatticeDiscretization + from qlbm.lattice.eqc import EquivalenceClassGenerator + + # Generate some equivalence classes + eqcs = EquivalenceClassGenerator( + LatticeDiscretization.D3Q6 + ).generate_equivalence_classes() + + # Select one at random and draw its circuit in the schematic form + EQCRedistribution(eqcs.pop(), decompose_block=False).circuit.draw("mpl") + + The `decompose_block` parameter can be set to ``True`` to decompose the DFT block into a circuit: + + .. plot:: + :include-source: + + from qlbm.components.common import EQCRedistribution + from qlbm.lattice import LatticeDiscretization + from qlbm.lattice.eqc import EquivalenceClassGenerator + + # Generate some equivalence classes + eqcs = EquivalenceClassGenerator( + LatticeDiscretization.D3Q6 + ).generate_equivalence_classes() + + # Select one at random and draw its decomposed circuit + EQCRedistribution(eqcs.pop(), decompose_block=True).circuit.draw("mpl") + + """ - WIP. + equivalence_class: EquivalenceClass + """ + The equivalence class for which the redistribution is defined. + """ + + decompose_block: bool + """ + Whether to decompose the DFT block into a circuit. + If set to ``False``, the block is returned as a matrix. Otherwise, it is decomposed into a circuit. + Defaults to ``True``. """ def __init__( - self, equivalence_class: EquivalenceClass, logger: Logger = getLogger("qlbm") + self, + equivalence_class: EquivalenceClass, + decompose_block: bool = True, + logger: Logger = getLogger("qlbm"), ): super().__init__(logger) self.equivalence_class = equivalence_class + self.decompose_block = decompose_block self.logger.info(f"Creating circuit {str(self)}...") circuit_creation_start_time = perf_counter_ns() @@ -62,7 +125,7 @@ def create_circuit(self): inplace=True, ) - return circuit.decompose() + return circuit.decompose() if self.decompose_block else circuit @override def __str__(self): From c1ba2fe165960033653e18376003348486d1f5ac Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 14 Jul 2025 14:36:49 +0200 Subject: [PATCH 57/92] Update CQLBM component documentation --- qlbm/components/collisionless/bounceback_reflection.py | 4 ++-- qlbm/components/collisionless/primitives.py | 2 +- qlbm/components/collisionless/specular_reflection.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/qlbm/components/collisionless/bounceback_reflection.py b/qlbm/components/collisionless/bounceback_reflection.py index 8f9c700..7ca3571 100644 --- a/qlbm/components/collisionless/bounceback_reflection.py +++ b/qlbm/components/collisionless/bounceback_reflection.py @@ -61,7 +61,7 @@ class BounceBackWallComparator(LBMPrimitive): # Comparing on the indices of the inside x-wall on the lower-bound of the obstacle BounceBackWallComparator( - lattice=lattice, wall=lattice.block_list[0].walls_inside[0][0] + lattice=lattice, wall=lattice.shape_list[0].walls_inside[0][0] ).draw("mpl") """ @@ -171,7 +171,7 @@ class BounceBackReflectionOperator(CQLBMOperator): } ) - BounceBackReflectionOperator(lattice=lattice, blocks=lattice.block_list) + BounceBackReflectionOperator(lattice=lattice, blocks=lattice.shape_list) """ def __init__( diff --git a/qlbm/components/collisionless/primitives.py b/qlbm/components/collisionless/primitives.py index ddc708c..d9147ee 100644 --- a/qlbm/components/collisionless/primitives.py +++ b/qlbm/components/collisionless/primitives.py @@ -488,7 +488,7 @@ class EdgeComparator(LBMPrimitive): ) # Draw the edge comparator circuit for one specific corner edge - EdgeComparator(lattice, lattice.block_list[0].corner_edges_3d[0]).draw("mpl") + EdgeComparator(lattice, lattice.shape_list[0].corner_edges_3d[0]).draw("mpl") """ def __init__( diff --git a/qlbm/components/collisionless/specular_reflection.py b/qlbm/components/collisionless/specular_reflection.py index a29fcda..b0bfb07 100644 --- a/qlbm/components/collisionless/specular_reflection.py +++ b/qlbm/components/collisionless/specular_reflection.py @@ -62,7 +62,7 @@ class SpecularWallComparator(LBMPrimitive): # Comparing on the indices of the inside x-wall on the lower-bound of the obstacle SpecularWallComparator( - lattice=lattice, wall=lattice.block_list[0].walls_inside[0][0] + lattice=lattice, wall=lattice.shape_list[0].walls_inside[0][0] ).draw("mpl") """ @@ -166,7 +166,7 @@ class SpecularReflectionOperator(CQLBMOperator): } ) - SpecularReflectionOperator(lattice=lattice, blocks=lattice.block_list) + SpecularReflectionOperator(lattice=lattice, blocks=lattice.shape_list) """ def __init__( From 059e4404c0b7594593ba0d596be5011501f411fb Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 14 Jul 2025 14:37:09 +0200 Subject: [PATCH 58/92] Update STQLBM component documentation --- qlbm/components/spacetime/collision/__init__.py | 2 ++ qlbm/components/spacetime/collision/d2q4_old.py | 8 +++++--- qlbm/components/spacetime/spacetime.py | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/qlbm/components/spacetime/collision/__init__.py b/qlbm/components/spacetime/collision/__init__.py index 7073d50..db8a47d 100644 --- a/qlbm/components/spacetime/collision/__init__.py +++ b/qlbm/components/spacetime/collision/__init__.py @@ -5,9 +5,11 @@ EquivalenceClassGenerator, ) from .d2q4_old import SpaceTimeD2Q4CollisionOperator +from .eqc_collision import GenericSpaceTimeCollisionOperator __all__ = [ "SpaceTimeD2Q4CollisionOperator", "EquivalenceClass", "EquivalenceClassGenerator", + "GenericSpaceTimeCollisionOperator", ] diff --git a/qlbm/components/spacetime/collision/d2q4_old.py b/qlbm/components/spacetime/collision/d2q4_old.py index 89d4eed..81f3a9f 100644 --- a/qlbm/components/spacetime/collision/d2q4_old.py +++ b/qlbm/components/spacetime/collision/d2q4_old.py @@ -1,3 +1,5 @@ +"""Collision operator for the :math:`D_2Q_4` discretization :class:`.SpaceTimeQLBM` algorithm as described in :cite:`spacetime`.""" + from logging import Logger, getLogger from math import pi from time import perf_counter_ns @@ -44,7 +46,7 @@ class SpaceTimeD2Q4CollisionOperator(SpaceTimeOperator): .. plot:: :include-source: - from qlbm.components.spacetime import SpaceTimeCollisionOperator + from qlbm.components.spacetime.collision.d2q4_old import SpaceTimeD2Q4CollisionOperator from qlbm.lattice import SpaceTimeLattice # Build an example lattice @@ -57,7 +59,7 @@ class SpaceTimeD2Q4CollisionOperator(SpaceTimeOperator): ) # Draw the collision operator for 1 time step - SpaceTimeCollisionOperator(lattice=lattice, timestep=1).draw("mpl") + SpaceTimeD2Q4CollisionOperator(lattice=lattice, timestep=1).draw("mpl") """ def __init__( @@ -156,4 +158,4 @@ def local_collision_circuit(self, reset_state: bool) -> QuantumCircuit: @override def __str__(self) -> str: - return "Space Time Collision Operator" \ No newline at end of file + return "Space Time Collision Operator" diff --git a/qlbm/components/spacetime/spacetime.py b/qlbm/components/spacetime/spacetime.py index c756de8..bf4a6fd 100644 --- a/qlbm/components/spacetime/spacetime.py +++ b/qlbm/components/spacetime/spacetime.py @@ -24,7 +24,7 @@ class SpaceTimeQLBM(LBMAlgorithm): The algorithm is composed of two main steps, the implementation of which (in general) varies per individual time step: #. Streaming performed by the :class:`.SpaceTimeStreamingOperator` moves the particles on the grid by means of swap gates over velocity qubits. - #. Collision performed by the :class:`.SpaceTimeCollisionOperator` does not move particles on the grid, but locally alters the velocity qubits at each grid point, if applicable. + #. Collision performed by the :class:`.GenericSpaceTimeCollisionOperator` does not move particles on the grid, but locally alters the velocity qubits at each grid point, if applicable. ========================= ====================================================================== Attribute Summary From fa4820655b9ef1c7088a2ec4429cee1b76a9e770 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 14 Jul 2025 14:37:51 +0200 Subject: [PATCH 59/92] Update lattice, geometry, eqc documentation --- qlbm/lattice/eqc/eqc.py | 16 ++- qlbm/lattice/eqc/eqc_generator.py | 18 ++- qlbm/lattice/geometry/encodings/lqlga.py | 2 + qlbm/lattice/geometry/shapes/base.py | 7 +- qlbm/lattice/lattices/base.py | 2 +- qlbm/lattice/lattices/lqlga_lattice.py | 141 ++++++++++++++++++++++- 6 files changed, 178 insertions(+), 8 deletions(-) diff --git a/qlbm/lattice/eqc/eqc.py b/qlbm/lattice/eqc/eqc.py index 889a25b..b238e43 100644 --- a/qlbm/lattice/eqc/eqc.py +++ b/qlbm/lattice/eqc/eqc.py @@ -1,3 +1,5 @@ +"""Implementation of the equivalence class abstraction described in Section 4 of :cite:p:`spacetime2`.""" + from typing import List, Set, Tuple, override import numpy as np @@ -40,9 +42,16 @@ class EquivalenceClass: """ discretization: LatticeDiscretization + """The discretization of the equivalence class.""" + velocity_configurations: Set[Tuple[bool, ...]] + """The velocity configurations of the equivalence class, represented as a set of tuples of boolean corresponding to velocity channel occupancies that all have the same mass and momentum.""" + mass: int + """The total mass of the equivalence class, which is the sum of all occupied velocity channels.""" + momentum: np.typing.NDArray + """The total momentum of the equivalence class, which is the vector sum of all occupied velocity channels multiplied by their :class:`.LatticeDiscretizationProperties` velocity contribution.""" def __init__( self, @@ -128,7 +137,10 @@ def get_bitstrings(self) -> List[str]: List[str] The velocity configurations of the equivalence class as bitstrings. """ - return [''.join(['1' if x else '0' for x in cfg]) for cfg in self.velocity_configurations] + return [ + "".join(["1" if x else "0" for x in cfg]) + for cfg in self.velocity_configurations + ] @override def __eq__(self, value): @@ -143,4 +155,4 @@ def __eq__(self, value): @override def __hash__(self): - return hash((self.discretization, tuple(self.velocity_configurations))) \ No newline at end of file + return hash((self.discretization, tuple(self.velocity_configurations))) diff --git a/qlbm/lattice/eqc/eqc_generator.py b/qlbm/lattice/eqc/eqc_generator.py index 00e9e59..943a83b 100644 --- a/qlbm/lattice/eqc/eqc_generator.py +++ b/qlbm/lattice/eqc/eqc_generator.py @@ -1,3 +1,5 @@ +"""Generator class for equivalence classes in LGA-based algorithms.""" + from itertools import product from typing import Dict, Set @@ -23,6 +25,20 @@ class EquivalenceClassGenerator: * - :attr:`discretization` - The :class:`.LatticeDiscretization` that the equivalence class belongs to. + Example usage: + + .. code-block:: python + :linenos: + + from qlbm.lattice import LatticeDiscretization + from qlbm.lattice.eqc import EquivalenceClassGenerator + + # Generate some equivalence classes + eqcs = EquivalenceClassGenerator( + LatticeDiscretization.D3Q6 + ).generate_equivalence_classes() + + print(eqcs.pop().get_bitstrings()) """ discretization: LatticeDiscretization @@ -62,4 +78,4 @@ def generate_equivalence_classes(self) -> Set[EquivalenceClass]: EquivalenceClass(self.discretization, set(tuple(cfg.tolist()) for cfg in v)) for _, v in equivalence_classes.items() if len(v) > 1 - } \ No newline at end of file + } diff --git a/qlbm/lattice/geometry/encodings/lqlga.py b/qlbm/lattice/geometry/encodings/lqlga.py index 2c5e2bd..fb5ecdb 100644 --- a/qlbm/lattice/geometry/encodings/lqlga.py +++ b/qlbm/lattice/geometry/encodings/lqlga.py @@ -1,3 +1,5 @@ +"""Geometrical data encodings specific to the :class:`.LQLGA` algorithm.""" + from abc import ABC from typing import Tuple diff --git a/qlbm/lattice/geometry/shapes/base.py b/qlbm/lattice/geometry/shapes/base.py index 74a88a7..9d8cb33 100644 --- a/qlbm/lattice/geometry/shapes/base.py +++ b/qlbm/lattice/geometry/shapes/base.py @@ -102,18 +102,19 @@ def get_lqlga_reflection_data_d1q2_from_points( """ return [ LQLGAPointwiseReflectionData( - tuple( + tuple( # type: ignore [ gridpoint, tuple( (a + b) % max_grid_size for a, b in zip( - gridpoint, reflection_increment_from_boundary + gridpoint, + reflection_increment_from_boundary, ) ), ], ), - tuple( + tuple( # type: ignore [before_reflection_velocity_index, after_reflection_velocity_index], ), ) diff --git a/qlbm/lattice/lattices/base.py b/qlbm/lattice/lattices/base.py index 4e4405f..6bd49d4 100644 --- a/qlbm/lattice/lattices/base.py +++ b/qlbm/lattice/lattices/base.py @@ -120,7 +120,7 @@ class Lattice(ABC): The total number of qubits required for the quantum circuit to simulate the lattice. This is the sum of the number of grid, velocity, and ancilla qubits. """ - registers: Tuple[QuantumRegister, ...] + velocity_register: Tuple[QuantumRegister, ...] """ A tuple that holds registers responsible for specific operations of the QLBM algorithm. """ diff --git a/qlbm/lattice/lattices/lqlga_lattice.py b/qlbm/lattice/lattices/lqlga_lattice.py index ca00d51..e399fe3 100644 --- a/qlbm/lattice/lattices/lqlga_lattice.py +++ b/qlbm/lattice/lattices/lqlga_lattice.py @@ -1,3 +1,5 @@ +"""Implementation of the :class:`.Lattice` base specific to the 2D and 3D :class:`.LQLGA` algorithm.""" + from itertools import product from math import prod from typing import Dict, List, Tuple, cast, override @@ -14,11 +16,59 @@ class LQLGALattice(Lattice): + r""" + Lattice class for the :class:`.LQLGA` algorithm. + + ================================= ======================================================================================== + Attribute Summary + ================================= ======================================================================================== + :attr:`num_gridpoints` The number of gridpoints in each dimension of the lattice. + :attr:`num_velocities` The number of discrete velocities in each dimension of the lattice. + :attr:`num_dims` The number of dimensions of the lattice. + :attr:`discretization` The discretization of the lattice. + :attr:`num_velocities_per_point` The number of discrete velocities per gridpoint. + :attr:`num_base_qubits` The number of qubits required to represent the lattice without velocities. + :attr:`num_total_qubits` The total number of qubits required to represent the lattice, including velocities. + :attr:`registers` The list of quantum registers for the lattice, one for each gridpoint. + :attr:`circuit` The quantum circuit representing the lattice, initialized with the registers. + ================================ ======================================================================================== + + The registers encoded in the lattice and their accessors are given below. + For the size of each register, + and :math:`d` is the total number of dimensions: 2 or 3. + + .. list-table:: Register allocation + :widths: 25 25 25 50 + :header-rows: 1 + + * - Register + - Size + - Access Method + - Description + * - :attr:`velocity_register` + - :math:`N_g \cdot q` + - :meth:`velocity_index_flat` and `:meth:`velocity_index_tuple` + - :math:`N_g` registers sized according to the number of discrete velocities of the lattice. + """ + discretization: LatticeDiscretization + """The discretization of the lattice, one of :class:`.LatticeDiscretization`.""" num_gridpoints: List[int] + """The number of gridpoints in each dimension of the lattice. + **Important** : for easier compatibility with binary arithmetic, the number of gridpoints + specified in the input dictionary is one larger than the one held in the ``Lattice``.""" shapes: Dict[str, List[Shape]] + """The shapes of the lattice, which are used to define the geometry of the lattice. + The key consists of the type of the shape and the name of the shape, e.g. "bounceback" or "specular". + """ + + num_base_qubits: int + """The number of qubits required to represent the lattice.""" + + velocity_register: QuantumRegister + """The quantum register representing the velocities of the lattice.""" def __init__(self, lattice_data, logger=...): super().__init__(lattice_data, logger) @@ -41,6 +91,8 @@ def __init__(self, lattice_data, logger=...): self.registers = self.get_registers() + self.velocity_register = self.registers + self.circuit = QuantumCircuit(*self.registers) def __get_discretization(self) -> LatticeDiscretization: @@ -83,9 +135,38 @@ def get_registers(self) -> Tuple[List[QuantumRegister], ...]: ) ] - return velocity_registers + return velocity_registers # type: ignore def gridpoint_index_tuple(self, gridpoint: Tuple[int, ...]) -> int: + """ + Get the lexicographic index of a gridpoint in the lattice. + + Parameters + ---------- + gridpoint : Tuple[int, ...] + The gridpoint formatted as a tuple of indices for each dimension. + + Returns + ------- + int + The lexicographic index of the gridpoint in the lattice. + + Raises + ------ + LatticeException + If the gridpoint index is out of bounds for the lattice. + """ + if len(gridpoint) != self.num_dims: + raise LatticeException( + f"Gridpoint {gridpoint} has incorrect number of dimensions. Expected {self.num_dims}, got {len(gridpoint)}." + ) + if any( + gp < 0 or gp >= ng + 1 for gp, ng in zip(gridpoint, self.num_gridpoints) + ): + raise LatticeException( + f"Gridpoint {gridpoint} is out of bounds for the lattice with gridpoints {self.num_gridpoints}." + ) + flat_index = 0 multiplier = 1 num_dims = len(self.num_gridpoints) @@ -97,6 +178,24 @@ def gridpoint_index_tuple(self, gridpoint: Tuple[int, ...]) -> int: return flat_index def gridpoint_index_flat(self, gridpoint: int) -> Tuple[int, ...]: + """ + Get the tuple representation of a gridpoint index in the lattice. + + Parameters + ---------- + gridpoint : int + The lexicographic index of the gridpoint in the lattice. + + Returns + ------- + Tuple[int, ...] + The tuple representation of the gridpoint index in the lattice. + + Raises + ------ + LatticeException + If the gridpoint index is out of bounds for the lattice. + """ if ( gridpoint < 0 or gridpoint >= self.num_total_qubits // self.num_velocities_per_point @@ -111,6 +210,26 @@ def gridpoint_index_flat(self, gridpoint: int) -> Tuple[int, ...]: return cast(Tuple[int, ...], tuple(reversed(indices))) def velocity_index_flat(self, gridpoint: int, velocity: int) -> int: + """ + Get the index of a qubit representing a particular velocity channel of a given gridpoint. + + Parameters + ---------- + gridpoint : int + The lexicographic index of the gridpoint in the lattice. + velocity : int + The index of the velocity channel (0-indexed). + + Returns + ------- + int + The index of the qubit representing the velocity channel at the specified gridpoint. + + Raises + ------ + LatticeException + If the gridpoint or velocity indices are out of bounds for the lattice. + """ if velocity < 0 or velocity >= self.num_velocities_per_point: raise LatticeException( f"Velocity {velocity} is out of bounds for the lattice with {self.num_velocities_per_point} velocities per point." @@ -126,6 +245,26 @@ def velocity_index_flat(self, gridpoint: int, velocity: int) -> int: return gridpoint * self.num_velocities_per_point + velocity def velocity_index_tuple(self, gridpoint: Tuple[int, ...], velocity: int) -> int: + """ + Get the index of a qubit representing a particular velocity channel of a given gridpoint. + + Parameters + ---------- + gridpoint : Tuple[int, ...] + The gridpoint formatted as a tuple of indices for each dimension. + velocity : int + The index of the velocity channel (0-indexed). + + Returns + ------- + int + The index of the qubit representing the velocity channel at the specified gridpoint. + + Raises + ------ + LatticeException + If the gridpoint or velocity indices are out of bounds for the lattice. + """ if velocity < 0 or velocity >= self.num_velocities_per_point: raise LatticeException( f"Velocity {velocity} is out of bounds for the lattice with {self.num_velocities_per_point} velocities per point." From 832b218a1b003c4d1fe8f274096aeef2afb4e4a7 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 14 Jul 2025 14:38:43 +0200 Subject: [PATCH 60/92] Update infrastructure documentation --- qlbm/infra/reinitialize/__init__.py | 2 +- .../reinitialize/identity_reinitializer.py | 90 +++++++++++++++++++ qlbm/infra/runner/base.py | 2 +- 3 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 qlbm/infra/reinitialize/identity_reinitializer.py diff --git a/qlbm/infra/reinitialize/__init__.py b/qlbm/infra/reinitialize/__init__.py index abeda1a..1c06f5a 100644 --- a/qlbm/infra/reinitialize/__init__.py +++ b/qlbm/infra/reinitialize/__init__.py @@ -1,7 +1,7 @@ """Reinitialize objects for transitioning between time steps of the QLBM algorithm.""" from .base import Reinitializer -from .collisionless_reinitializer import IdentityReinitializer +from .identity_reinitializer import IdentityReinitializer from .spacetime_reinitializer import SpaceTimeReinitializer __all__ = ["Reinitializer", "IdentityReinitializer", "SpaceTimeReinitializer"] diff --git a/qlbm/infra/reinitialize/identity_reinitializer.py b/qlbm/infra/reinitialize/identity_reinitializer.py new file mode 100644 index 0000000..f38c434 --- /dev/null +++ b/qlbm/infra/reinitialize/identity_reinitializer.py @@ -0,0 +1,90 @@ +"""Identity reinitializer used for the :class:`.CQLBM` and :class:`.LQLGA` algorithms.""" + +from logging import Logger, getLogger + +from qiskit import QuantumCircuit as QiskitQC +from qiskit.circuit.library import Initialize +from qiskit.quantum_info import Statevector +from qiskit.result import Counts +from qiskit_aer.backends.aer_simulator import AerBackend +from qulacs import QuantumCircuit as QulacsQC +from typing_extensions import override + +from qlbm.infra.compiler import CircuitCompiler +from qlbm.infra.reinitialize.base import Reinitializer +from qlbm.lattice.lattices.base import Lattice + + +class IdentityReinitializer(Reinitializer): + r""" + Implementation of the :class:`.Reinitializer` that passes along the statevector to the following time step. + + Useful for the :class:`.CQLBM` and :class:`.LQLGA` algorithms. + Compatible with both :class:`.QiskitRunner`\ s and :class:`.QulacsRunner`\ s. + To generate a new set of initial conditions for the CQLBM algorithm, + the reinitializer simply returns the quantum state computed + at the end of the previous simulation. + This allows the reuse of a single quantum circuit for the simulation + of arbitrarily many time steps. + No copy of the statevector is required. + + =========================== ====================================================================== + Attribute Summary + =========================== ====================================================================== + :attr:`lattice` The :class:`.Lattice` of the simulated system. + :attr:`compiler` The compiler that converts the novel initial conditions circuits. + :attr:`logger` The performance logger, by default ``getLogger("qlbm")`` + =========================== ====================================================================== + """ + + lattice: Lattice + statevector: Statevector + counts: Counts + + def __init__( + self, + lattice: Lattice, + compiler: CircuitCompiler, + logger: Logger = getLogger("qlbm"), + ): + super().__init__(lattice, compiler, logger) + self.lattice = lattice + self.logger = logger + + def reinitialize( + self, + statevector: Statevector, + counts: Counts, + backend: AerBackend | None = None, + optimization_level: int = 0, + ) -> QiskitQC | QulacsQC: + """ + Returns the provided ``statevector`` as a new Qiskit ``Initialize`` object that can be prepended to the time step circuit to resume simulation. + + Parameters + ---------- + statevector : Statevector + The statevector at the end of the simulation. + counts : Counts + Ignored. + backend : AerBackend | None + Ignored. + optimization_level : int, optional + Ignored. + + Returns + ------- + QiskitQC | QulacsQC + A Qiskit ``Initialize`` object. + """ + circuit = self.lattice.circuit.copy() + circuit.compose( + Initialize(statevector), + inplace=True, + qubits=range(circuit.num_qubits), + ) + return circuit + + @override + def requires_statevector(self) -> bool: + return True \ No newline at end of file diff --git a/qlbm/infra/runner/base.py b/qlbm/infra/runner/base.py index dae7959..0de08ca 100644 --- a/qlbm/infra/runner/base.py +++ b/qlbm/infra/runner/base.py @@ -10,10 +10,10 @@ from qiskit_aer import AerSimulator from qlbm.infra.reinitialize import ( - IdentityReinitializer, Reinitializer, SpaceTimeReinitializer, ) +from qlbm.infra.reinitialize.identity_reinitializer import IdentityReinitializer from qlbm.infra.result import ( CollisionlessResult, LQLGAResult, From 13c293f69656f4531a05c03b5d1fd787d714e81d Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 14 Jul 2025 14:50:16 +0200 Subject: [PATCH 61/92] Update empty primitive circuit creation --- qlbm/components/common/primitives.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qlbm/components/common/primitives.py b/qlbm/components/common/primitives.py index 456e2a7..ef629fc 100644 --- a/qlbm/components/common/primitives.py +++ b/qlbm/components/common/primitives.py @@ -44,7 +44,7 @@ def __init__( @override def create_circuit(self) -> QuantumCircuit: - return QuantumCircuit(*self.lattice.registers) + return self.lattice.circuit.copy() @override def __str__(self) -> str: From 8ec78daf74d571581f774a67d5ee5295dabc1060 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 14 Jul 2025 14:50:38 +0200 Subject: [PATCH 62/92] Add missing parameter documentation in lattice base class --- qlbm/lattice/lattices/base.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/qlbm/lattice/lattices/base.py b/qlbm/lattice/lattices/base.py index 6bd49d4..55dfc7a 100644 --- a/qlbm/lattice/lattices/base.py +++ b/qlbm/lattice/lattices/base.py @@ -141,6 +141,11 @@ class Lattice(ABC): The performance logger, by default ``getLogger("qlbm")``. """ + register: Tuple[List[QuantumRegister], ...] + """ + A tuple of lists of :class:`qiskit.QuantumRegister` s that are used to store the quantum information of the lattice. + """ + def __init__( self, lattice_data: str | Dict, # type: ignore From d6cca322d221357d3db2bdb87b417f34cb1aa967 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 14 Jul 2025 15:15:04 +0200 Subject: [PATCH 63/92] Update documentation landing page and references --- docs/source/index.rst | 18 ++++++++++-------- docs/source/refs.bib | 21 +++++++++++++++++++++ 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index 83e7865..0d07a9f 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -9,31 +9,33 @@ On this website, you can find the :ref:`internal_docs` of the source code compon A paper describing `qlbm` in detail is available on `here `_ :cite:p:`qlbm`. ``qlbm`` is made up of 4 main modules. -Together, the :ref:`base_components`, :ref:`cqlbm_components`, and :ref:`stqlbm_components` -module handle the parameterized creation of quantum circuits that compose QBMs. +Together, the :ref:`base_components`, :ref:`cqlbm_components`, :ref:`stqlbm_components`, and :ref:`lqlga_components` +modules 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. The :ref:`infra` module integrates the quantum components with Tket, Qiskit, and Qulacs transpilers and runners. The :ref:`tools` module contains miscellaneous utilities. -``qlbm`` currently supports two algorithms: +``qlbm`` currently supports three algorithms: #. The **C**\ ollisionless **QLBM** (CQLBM) first described in :cite:p:`collisionless` and later expanded in :cite:p:`qmem`. #. **S**\ pace-\ **T**\ ime **QLBM** (STQLBM) described in :cite:p:`spacetime` and :cite:p:`spacetime2`. +#. **L**\ inear \ **Q**\ uantum **L**\ attice **G**\ as **A**\ utomata (LQLGA) described in :cite:p:`spacetime2`, :cite:p:`lqlga1`, and :cite:p:`lqlga2`. + .. card:: Internal Documentation :link: internal_docs :link-type: ref :fas:`book;sd-text-primary` Detailed documentation of ``qlbm``. -.. card:: Tutorials - :link: tutorials - :link-type: ref +.. .. card:: Tutorials +.. :link: tutorials +.. :link-type: ref - :fas:`flask;sd-text-primary` Hands-on examples. +.. :fas:`flask;sd-text-primary` Hands-on examples. .. toctree:: @@ -41,7 +43,7 @@ The :ref:`tools` module contains miscellaneous utilities. :maxdepth: 2 code/index - examples/index +.. examples/index References ----------------------------------- diff --git a/docs/source/refs.bib b/docs/source/refs.bib index cbf2ae7..ab56d73 100644 --- a/docs/source/refs.bib +++ b/docs/source/refs.bib @@ -58,3 +58,24 @@ @article{mcswap year={2022}, publisher={Verein zur F{\"o}rderung des Open Access Publizierens in den Quantenwissenschaften} } + +@article{lqlga2, + title={Fully quantum algorithm for mesoscale fluid simulations with application to partial differential equations}, + author={Kocherla, Sriharsha and Song, Zhixin and Chrit, Fatima Ezahra and Gard, Bryan and Dumitrescu, Eugene F and Alexeev, Alexander and Bryngelson, Spencer H}, + journal={AVS Quantum Science}, + volume={6}, + number={3}, + year={2024}, + publisher={AIP Publishing} +} + +@article{lqlga1, + title={On quantum extensions of hydrodynamic lattice gas automata}, + author={Love, Peter}, + journal={Condensed Matter}, + volume={4}, + number={2}, + pages={48}, + year={2019}, + publisher={MDPI} +} From e672b345cfc5349c34392b5e0a0c74cfd820c44b Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 14 Jul 2025 16:18:05 +0200 Subject: [PATCH 64/92] Add contribution guidelines --- CONTRIBUTING.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..979ea1d --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,31 @@ +# Contributing + +Pull requests are welcome. To make a PR, first fork the repo, make your proposed changes on the main branch, and open a PR from your fork. If it passes tests and is accepted after review, it will be merged. For discussions about feature requests and contributions, please contact `c.a.georgescu@tudelft.nl`. + +## Code style + +### Formatting and Linting + +All code should be formatted using `ruff` with default options. You should verify that your changes adhere to the `ruff` standards before submitting a PR. The exact version used in the CI pipeline can be found in the `pyproject.toml` file. Both the source code `ruff check qlbm` and the demos `ruff check demos` should adhere to these standards. + +### Type annotation + +`mypy` is used as a static type checker and all submissions must pass its checks. YYou should verify that your changes adhere to the `mypy` standards before submitting a PR. The exact version used in the CI pipeline can be found in the `pyproject.toml` file. There are some custom rules used for type checking: you can check whether your submission adheres to them by running `mypy qlbm test --config-file pyproject.toml`. +Linting + + +### Tests + +We encourage each contribution to include unit tests for the added functionality. Tests reside in the `test` directory and can be executed using `pytest test/unit`. +Please sure all tests are passing before submitting a PR, and verify that simple flow simulations work as intended. +When fixing a bug, please add a test that demonstrates the fix. + +### Documentation + +We encourage all contributions to contain an adequate and thorough documentation of the changes. All contributed classes, methods, and attributes should be documented using the established style. Where appropriate, please include code-block examples and references to the scientific literature. + +We use `sphinx` to automatically generate our documentation website. To make sure your contribution fits the standards of the code base, you can execute `make docs` in the root directory. To check the documentation website locally, you can first `cd docs` and then `make clean html` to build the website locally. You can view the updated web pages by opening `docs/build/html/index.html`. + +### Easy verification + +To verify that your changes comply with our checks, you can simply run `make check-ci` to run all checks. \ No newline at end of file From 0bb407e9b68dd98ecec638349aaf157cd9e639f0 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 14 Jul 2025 16:18:36 +0200 Subject: [PATCH 65/92] Update website internal documentation --- .../code/{base_comps.rst => comps_base.rst} | 0 docs/source/code/comps_collision.rst | 63 ++++++++++++++ .../code/{cqlbm_comps.rst => comps_cqlbm.rst} | 0 docs/source/code/comps_lqlga.rst | 84 +++++++++++++++++++ .../{stqlbm_comps.rst => comps_stqbm.rst} | 16 +--- docs/source/code/index.rst | 8 +- docs/source/code/infra.rst | 2 +- 7 files changed, 155 insertions(+), 18 deletions(-) rename docs/source/code/{base_comps.rst => comps_base.rst} (100%) create mode 100644 docs/source/code/comps_collision.rst rename docs/source/code/{cqlbm_comps.rst => comps_cqlbm.rst} (100%) create mode 100644 docs/source/code/comps_lqlga.rst rename docs/source/code/{stqlbm_comps.rst => comps_stqbm.rst} (84%) diff --git a/docs/source/code/base_comps.rst b/docs/source/code/comps_base.rst similarity index 100% rename from docs/source/code/base_comps.rst rename to docs/source/code/comps_base.rst diff --git a/docs/source/code/comps_collision.rst b/docs/source/code/comps_collision.rst new file mode 100644 index 0000000..b862f85 --- /dev/null +++ b/docs/source/code/comps_collision.rst @@ -0,0 +1,63 @@ +.. _comps_collision: + +==================================== +Collision Logic Classes +==================================== + +.. testcode:: + :hide: + + from qlbm.components import ( + EQCPermutation, + EQCRedistribution, + ) + from qlbm.lattice.spacetime.properties_base import ( + LatticeDiscretization, + LatticeDiscretizationProperties, + ) + + from qlbm.lattice.eqc import ( + EquivalenceClass, + EquivalenceClassGenerator, + ) + print("ok") + +.. testoutput:: + :hide: + + ok + + +This page contain collision-related components shared +between the :ref:`stqlbm_components` and :ref:`lqlga_components` algorithms. +Collision in these algorithms is based on the concept of equivalence classes +described in Section 4 of :cite:p:`spacetime2`, and follows a PRP (permute-redistribute-unpermute) approach. +All components of this module may be used for different variations of the Computational Basis State Encoding (CBSE) +of the velocity register. +The components of this module consist of: + +#. :ref:`collision_logic` classes that provide information about the lattice discretization and equivalence classes. +#. :ref:`collision_components` that implement the small parts of the collision operator. + +.. _collision_logic: + +Collision Logic Classes +----------------------------------- + +.. autoclass:: qlbm.lattice.spacetime.properties_base.LatticeDiscretization + +.. autoclass:: qlbm.lattice.spacetime.properties_base.LatticeDiscretizationProperties + +.. autoclass:: qlbm.lattice.eqc.EquivalenceClass + +.. autoclass:: qlbm.lattice.eqc.EquivalenceClassGenerator + +.. _collision_components: + +Collision Components +----------------------------------- + +.. autoclass:: qlbm.components.common.EQCPermutation + +.. autoclass:: qlbm.components.common.EQCRedistribution + diff --git a/docs/source/code/cqlbm_comps.rst b/docs/source/code/comps_cqlbm.rst similarity index 100% rename from docs/source/code/cqlbm_comps.rst rename to docs/source/code/comps_cqlbm.rst diff --git a/docs/source/code/comps_lqlga.rst b/docs/source/code/comps_lqlga.rst new file mode 100644 index 0000000..da2f7b4 --- /dev/null +++ b/docs/source/code/comps_lqlga.rst @@ -0,0 +1,84 @@ +.. _lqlga_components: + +==================================== +LQLGA Circuits +==================================== + +.. testcode:: + :hide: + + from qlbm.components import ( + LQLGA, + LQGLAInitialConditions, + LQLGAGridVelocityMeasurement, + LQLGAReflectionOperator, + LQLGAStreamingOperator, + ) + from qlbm.lattice import LQLGALattice + print("ok") + +.. testoutput:: + :hide: + + ok + + +This page contains documentation about the quantum circuits that make up the +**L**\ inear **Q**\ uantum **L**\ attice **G**\ as **A**\ utomata (LQLGA). +For a more in-depth depth description of the LQLGA algorithm, +we suggest :cite:p:`lqlga1`, :cite:p:`lqlga2`, and :cite:p:`spacetime2`. +The LQGLA encodes a lattice of :math:`N_g` gridpoints with :math:`q` discrete velocities +each into :math:`N_g \cdot q` qubits. +The time-evolution of the system consists of the following steps: + +#. :ref:`lqlga_initial` prepare the starting state of the flow field. +#. :ref:`lqlga_streaming` move particles across gridpoints according to the velocity discretization. +#. :ref:`lqlga_reflection` circuits apply boundary conditions that affect particles that come in contact with solid obstacles. Reflection places those particles back in the appropriate position of the fluid domain. +#. :ref:`lqlga_collision` operators create superposed local configurations of velocity profiles. +#. :ref:`lqlga_measurement` operations extract information out of the quantum state, which can later be post-processed classically. + +This page documents the individual components that make up the CQLBM algorithm. +Subsections follow a top-down approach, where end-to-end operators are introduced first, +before being broken down into their constituent parts. + +.. _lqlga_e2e: + +End-to-end algorithms +---------------------------------- + +.. autoclass:: qlbm.components.LQLGA + +.. _lqlga_initial: + +Initial Conditions +---------------------------------- + +.. autoclass:: qlbm.components.LQGLAInitialConditions + +.. _lqlga_streaming: + +Streaming +---------------------------------- + +.. autoclass:: qlbm.components.LQLGAStreamingOperator + +.. _lqlga_reflection: + +Reflection +---------------------------------- + +.. autoclass:: qlbm.components.LQLGAReflectionOperator + +.. _lqlga_collision: + +Collision +----------------------------------- + +.. autoclass:: qlbm.components.GenericLQLGACollisionOperator + +.. _lqlga_measurement: + +Measurement +---------------------------------- + +.. autoclass:: qlbm.components.LQLGAGridVelocityMeasurement \ No newline at end of file diff --git a/docs/source/code/stqlbm_comps.rst b/docs/source/code/comps_stqbm.rst similarity index 84% rename from docs/source/code/stqlbm_comps.rst rename to docs/source/code/comps_stqbm.rst index 389f57f..a37faa3 100644 --- a/docs/source/code/stqlbm_comps.rst +++ b/docs/source/code/comps_stqbm.rst @@ -69,21 +69,9 @@ The collision module contains collision operators and adjacent logic classes. The former implements the circuits that perform collision in computational basis state encodings, while the latter contains useful abstractions that circuits build on top of. -Collision Operators -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. autoclass:: qlbm.components.spacetime.collision.GenericSpaceTimeCollisionOperator -.. autoclass:: qlbm.components.spacetime.collision.SpaceTimeCollisionOperator - -Collision Logic Classes -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. autoclass:: qlbm.lattice.spacetime.properties_base.LatticeDiscretization - -.. autoclass:: qlbm.lattice.spacetime.properties_base.LatticeDiscretizationProperties - -.. autoclass:: qlbm.components.spacetime.collision.EquivalenceClass - -.. autoclass:: qlbm.components.spacetime.collision.EquivalenceClassGenerator +.. autoclass:: qlbm.components.spacetime.collision.SpaceTimeD2Q4CollisionOperator .. _stqlbm_others: diff --git a/docs/source/code/index.rst b/docs/source/code/index.rst index ca25609..f869ba8 100644 --- a/docs/source/code/index.rst +++ b/docs/source/code/index.rst @@ -14,9 +14,11 @@ The :ref:`tools` module contains miscellaneous utilities. .. toctree:: - base_comps - cqlbm_comps - stqlbm_comps + comps_base + comps_cqlbm + comps_collision + comps_stqbm + comps_lqlga lattice infra tools diff --git a/docs/source/code/infra.rst b/docs/source/code/infra.rst index 1e4e119..a25f96f 100644 --- a/docs/source/code/infra.rst +++ b/docs/source/code/infra.rst @@ -40,7 +40,7 @@ Performance .. autoclass:: qlbm.infra.reinitialize.base.Reinitializer :members: -.. autoclass:: qlbm.infra.reinitialize.collisionless_reinitializer.CollisionlessReinitializer +.. autoclass:: qlbm.infra.reinitialize.identity_reinitializer.IdentityReinitializer :members: .. autoclass:: qlbm.infra.reinitialize.spacetime_reinitializer.SpaceTimeReinitializer From aff293fa5a71f0ab2077a00a1123323547577240 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 14 Jul 2025 16:19:20 +0200 Subject: [PATCH 66/92] Update qlbm article citation --- CITATION.bib | 12 +++++++----- README.md | 16 +++++++++++++++- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/CITATION.bib b/CITATION.bib index e6d3ef5..a370309 100644 --- a/CITATION.bib +++ b/CITATION.bib @@ -1,6 +1,8 @@ -@article{georgescu2024qlbm, - title={qlbm -- A Quantum Lattice Boltzmann Software Framework}, - author={Georgescu, C{\u{a}}lin A. and Schalkers, Merel A. and M{\"o}ller, Matthias}, - journal={arXiv preprint arXiv:2411.19439}, - year={2024} +@article{georgescu2025qlbm, + title={qlbm--A Quantum Lattice Boltzmann Software Framework}, + author={Georgescu, C{\u{a}}lin A and Schalkers, Merel A and M{\"o}ller, Matthias}, + journal={Computer Physics Communications}, + pages={109699}, + year={2025}, + publisher={Elsevier} } diff --git a/README.md b/README.md index 622e710..6d4b749 100644 --- a/README.md +++ b/README.md @@ -110,4 +110,18 @@ The `demos` directory contains several use cases for simulating and analyzing th ## Citation -A preprint describing `qlbm` in detail is currently available on [arXiv](https://arxiv.org/abs/2411.19439). If you use `qlbm`, you can cite it as per the [CITATION.bib file](CITATION.bib). \ No newline at end of file +An open access peer-reviewed article describing `qlbm` is available [here](https://doi.org/10.1016/j.cpc.2025.109699). If you use `qlbm`, you can cite it as per the [CITATION.bib](CITATION.bib) file: + +``` +@article{georgescu2025qlbm, +title = {qlbm – A quantum lattice Boltzmann software framework}, +journal = {Computer Physics Communications}, +volume = {315}, +pages = {109699}, +year = {2025}, +issn = {0010-4655}, +doi = {https://doi.org/10.1016/j.cpc.2025.109699}, +url = {https://www.sciencedirect.com/science/article/pii/S0010465525002012}, +author = {C\u{{a}}lin A. Georgescu and Merel A. Schalkers and Matthias M\"{o}ller}, +} +``` \ No newline at end of file From 91cae3c8c9074e9882eb6827fc5ceb04b7941fe0 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 14 Jul 2025 16:19:39 +0200 Subject: [PATCH 67/92] Update python version checking --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 07eece2..19b4430 100644 --- a/Makefile +++ b/Makefile @@ -13,8 +13,8 @@ check-python-version: @PYTHON_VERSION=$$($(PYTHON) --version 2>&1 | awk '{print $$2}'); \ MAJOR_VERSION=$$(echo $$PYTHON_VERSION | cut -d. -f1); \ MINOR_VERSION=$$(echo $$PYTHON_VERSION | cut -d. -f2); \ - if [ "$$MAJOR_VERSION" -ne 3 ] || [ "$$MINOR_VERSION" -lt 8 ] || [ "$$MINOR_VERSION" -gt 13 ]; then \ - echo "Python version must be between 3.8 and 3.13"; \ + if [ "$$MAJOR_VERSION" -ne 3 ] || [ "$$MINOR_VERSION" -lt 12 ] || [ "$$MINOR_VERSION" -gt 13 ]; then \ + echo "Python version must be between 3.12 and 3.13"; \ exit 1; \ fi From 04c8e0d43a64e108dfe71e85097e2b8074876bd6 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 14 Jul 2025 16:23:42 +0200 Subject: [PATCH 68/92] Update citation format --- CITATION.bib | 8 -------- CITATION.cff | 11 +++++++++++ README.md | 2 +- 3 files changed, 12 insertions(+), 9 deletions(-) delete mode 100644 CITATION.bib create mode 100644 CITATION.cff diff --git a/CITATION.bib b/CITATION.bib deleted file mode 100644 index a370309..0000000 --- a/CITATION.bib +++ /dev/null @@ -1,8 +0,0 @@ -@article{georgescu2025qlbm, - title={qlbm--A Quantum Lattice Boltzmann Software Framework}, - author={Georgescu, C{\u{a}}lin A and Schalkers, Merel A and M{\"o}ller, Matthias}, - journal={Computer Physics Communications}, - pages={109699}, - year={2025}, - publisher={Elsevier} -} diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 0000000..4e36f46 --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,11 @@ +@article{georgescu2025qlbm, + title = {qlbm – A quantum lattice Boltzmann software framework}, + journal = {Computer Physics Communications}, + volume = {315}, + pages = {109699}, + year = {2025}, + issn = {0010-4655}, + doi = {https://doi.org/10.1016/j.cpc.2025.109699}, + url = {https://www.sciencedirect.com/science/article/pii/S0010465525002012}, + author = {C\u{{a}}lin A. Georgescu and Merel A. Schalkers and Matthias M\"{o}ller}, +} \ No newline at end of file diff --git a/README.md b/README.md index 6d4b749..93eb0ac 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ The `demos` directory contains several use cases for simulating and analyzing th ## Citation -An open access peer-reviewed article describing `qlbm` is available [here](https://doi.org/10.1016/j.cpc.2025.109699). If you use `qlbm`, you can cite it as per the [CITATION.bib](CITATION.bib) file: +An open access peer-reviewed article describing `qlbm` is available [here](https://doi.org/10.1016/j.cpc.2025.109699). If you use `qlbm`, you can cite it as: ``` @article{georgescu2025qlbm, From 0b9ab0171eff7cc7d898df687e4521036cdf4e4e Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 14 Jul 2025 16:27:11 +0200 Subject: [PATCH 69/92] Update citation format --- CITATION.cff | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/CITATION.cff b/CITATION.cff index 4e36f46..b703e2d 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -1,11 +1,17 @@ -@article{georgescu2025qlbm, - title = {qlbm – A quantum lattice Boltzmann software framework}, - journal = {Computer Physics Communications}, - volume = {315}, - pages = {109699}, - year = {2025}, - issn = {0010-4655}, - doi = {https://doi.org/10.1016/j.cpc.2025.109699}, - url = {https://www.sciencedirect.com/science/article/pii/S0010465525002012}, - author = {C\u{{a}}lin A. Georgescu and Merel A. Schalkers and Matthias M\"{o}ller}, -} \ No newline at end of file +cff-version: 1.2.0 +message: "If you use this software, please cite it as below." +authors: +- family-names: "Georgescu" + given-names: "Calin A." + orcid: "https://orcid.org/0000-0002-8102-6389" +- family-names: "Schalkers" + given-names: "Merel A." + orcid: "https://orcid.org/0000-0001-7751-9060" +- family-names: "Möller" + given-names: "Matthias" + orcid: "https://orcid.org/0000-0003-0802-945X" +title: "qlbm – A Qantum Lattice Boltzmann Software Framework" +version: 0.0.5 +doi: 10.1016/j.cpc.2025.109699 +date-released: 2017-12-18 +url: "doi: 10.1016/j.cpc.2025.109699 \ No newline at end of file From 735c15ea84b23298a4a3147958053c62dbb9d4e0 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 14 Jul 2025 16:33:36 +0200 Subject: [PATCH 70/92] Update citation format --- CITATION.cff | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/CITATION.cff b/CITATION.cff index b703e2d..6822df5 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -12,6 +12,25 @@ authors: orcid: "https://orcid.org/0000-0003-0802-945X" title: "qlbm – A Qantum Lattice Boltzmann Software Framework" version: 0.0.5 -doi: 10.1016/j.cpc.2025.109699 -date-released: 2017-12-18 -url: "doi: 10.1016/j.cpc.2025.109699 \ No newline at end of file +doi: "10.1016/j.cpc.2025.109699" +date-released: 2025-03-24 +url: "https://doi.org/10.1016/j.cpc.2025.109699" +preferred-citation: + type: article + authors: + - family-names: "Georgescu" + given-names: "Calin A." + orcid: "https://orcid.org/0000-0002-8102-6389" + - family-names: "Schalkers" + given-names: "Merel A." + orcid: "https://orcid.org/0000-0001-7751-9060" + - family-names: "Möller" + given-names: "Matthias" + orcid: "https://orcid.org/0000-0003-0802-945X" + doi: "10.1016/j.cpc.2025.109699" + journal: "Computer Physics Communications" + title: "qlbm – A Qantum Lattice Boltzmann Software Framework" + issue: 1 + volume: 315 + year: 2025 + pages: 109699 \ No newline at end of file From bfd467a5295f87e19d9a8d4ea2fbaaa2d9f59f3f Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 14 Jul 2025 16:36:02 +0200 Subject: [PATCH 71/92] Update citation format --- CITATION.cff | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CITATION.cff b/CITATION.cff index 6822df5..c78dfc3 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -30,7 +30,6 @@ preferred-citation: doi: "10.1016/j.cpc.2025.109699" journal: "Computer Physics Communications" title: "qlbm – A Qantum Lattice Boltzmann Software Framework" - issue: 1 volume: 315 year: 2025 - pages: 109699 \ No newline at end of file + start: 109699 \ No newline at end of file From 1a11c14bb8ccd288bcc306ece8f9140c2b3ca58b Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 14 Jul 2025 16:37:37 +0200 Subject: [PATCH 72/92] Update citation format --- CITATION.cff | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CITATION.cff b/CITATION.cff index c78dfc3..17b415c 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -32,4 +32,4 @@ preferred-citation: title: "qlbm – A Qantum Lattice Boltzmann Software Framework" volume: 315 year: 2025 - start: 109699 \ No newline at end of file + issue: 109699 \ No newline at end of file From fa6cefb5d8a0bb85f26376a719610a1378e8c30b Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 21 Jul 2025 13:51:48 +0200 Subject: [PATCH 73/92] Add d1q3 CBSE permutation --- .../common/cbse_collision/cbse_permutation.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/qlbm/components/common/cbse_collision/cbse_permutation.py b/qlbm/components/common/cbse_collision/cbse_permutation.py index 9543e33..c50ee53 100644 --- a/qlbm/components/common/cbse_collision/cbse_permutation.py +++ b/qlbm/components/common/cbse_collision/cbse_permutation.py @@ -73,7 +73,9 @@ def __init__( @override def create_circuit(self): - if self.equivalence_class.discretization == LatticeDiscretization.D2Q4: + if self.equivalence_class.discretization == LatticeDiscretization.D1Q3: + return self.__create_circuit_d1q3() + elif self.equivalence_class.discretization == LatticeDiscretization.D2Q4: return self.__create_circuit_d2q4() elif self.equivalence_class.discretization == LatticeDiscretization.D3Q6: return self.__create_circuit_d3q6() @@ -82,6 +84,18 @@ def create_circuit(self): f"Collision not yet supported for discretization {self.equivalence_class.discretization}." ) + def __create_circuit_d1q3(self): + circuit = QuantumCircuit(3) + + if not self.inverse: + circuit.cx(0, 1) + circuit.cx(0, 2) + else: + circuit.cx(0, 2) + circuit.cx(0, 1) + + return circuit + def __create_circuit_d2q4(self): circuit = QuantumCircuit(4) From 49bb6462d0b9e6933b1d77375f28545ae6bc55b8 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 21 Jul 2025 13:52:27 +0200 Subject: [PATCH 74/92] Make CBSE redistribution adaptable for powers of 2 --- .../cbse_collision/cbse_redistribution.py | 39 ++++++++++++------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/qlbm/components/common/cbse_collision/cbse_redistribution.py b/qlbm/components/common/cbse_collision/cbse_redistribution.py index 4c0a0ac..8a4f408 100644 --- a/qlbm/components/common/cbse_collision/cbse_redistribution.py +++ b/qlbm/components/common/cbse_collision/cbse_redistribution.py @@ -11,6 +11,7 @@ from qlbm.components.base import LBMPrimitive from qlbm.lattice.eqc.eqc import EquivalenceClass from qlbm.lattice.spacetime.properties_base import LatticeDiscretizationProperties +from qlbm.tools.utils import is_two_pow class EQCRedistribution(LBMPrimitive): @@ -100,32 +101,40 @@ def create_circuit(self): self.equivalence_class.discretization ) circuit = QuantumCircuit(nv) - n = self.equivalence_class.size() nq = np.ceil(np.log2(n)).astype(int) - QFT = np.array( - [ - [np.exp(2j * np.pi * i * j / n) / np.sqrt(n) for j in range(n)] - for i in range(n) - ] - ) + redistribution_circuit = QuantumCircuit(nq) + if is_two_pow(n): + redistribution_circuit.ry(np.pi / 2, list(range(nq)), label="RY(π/2)") + else: + QFT = np.array( + [ + [np.exp(2j * np.pi * i * j / n) / np.sqrt(n) for j in range(n)] + for i in range(n) + ] + ) - U = np.eye(2**nq, dtype=complex) - U[:n, :n] = QFT - op = Operator(U) - assert op.is_unitary() + U = np.eye(2**nq, dtype=complex) + U[:n, :n] = QFT + op = Operator(U) + assert op.is_unitary() - qft_block_circ = QuantumCircuit(nq) - qft_block_circ.append(op, list(range(nq))) + redistribution_circuit.append(op, list(range(nq))) circuit.compose( - qft_block_circ.control(nv - int(nq), label=rf"Coll({n}, {nq})"), + redistribution_circuit.control( + nv - int(nq), + label=rf"MCRY(π/2, nq)" if is_two_pow(n) else rf"Coll({n}, {nq})", + ), qubits=list(range(nv - 1, -1, -1)), inplace=True, ) - return circuit.decompose() if self.decompose_block else circuit + if not is_two_pow(n): + return circuit.decompose() if self.decompose_block else circuit + else: + return circuit @override def __str__(self): From 5e403d7bd64853818983992d5ed2f03ad4ca042b Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 21 Jul 2025 13:53:24 +0200 Subject: [PATCH 75/92] Add LQLGA D1Q3 reflection operator --- qlbm/components/lqlga/reflection.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/qlbm/components/lqlga/reflection.py b/qlbm/components/lqlga/reflection.py index 96cb589..2ad7882 100644 --- a/qlbm/components/lqlga/reflection.py +++ b/qlbm/components/lqlga/reflection.py @@ -58,6 +58,9 @@ def create_circuit(self) -> QuantumCircuit: if discretization == LatticeDiscretization.D1Q2: return self.__create_circuit_d1q2() + elif discretization == LatticeDiscretization.D1Q3: + return self.__create_circuit_d1q3() + raise CircuitException(f"Reflection Operator unsupported for {discretization}.") def __create_circuit_d1q2(self) -> QuantumCircuit: @@ -77,6 +80,23 @@ def __create_circuit_d1q2(self) -> QuantumCircuit: ) return circuit + def __create_circuit_d1q3(self) -> QuantumCircuit: + circuit = self.lattice.circuit.copy() + + for shape in self.shapes: + for reflection_data in shape.get_lqlga_reflection_data_d1q3(): + circuit.swap( + self.lattice.velocity_index_tuple( + reflection_data.gridpoints[0], + reflection_data.velocity_indices_to_swap[0], + ), + self.lattice.velocity_index_tuple( + reflection_data.gridpoints[1], + reflection_data.velocity_indices_to_swap[1], + ), + ) + return circuit + @override def __str__(self) -> str: return f"[PointWiseLQLGAReflectionOperator for lattice {self.lattice}, shapes {self.shapes}]" From 630f4834880749e33ab4dcd1e547ce5740be429d Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 21 Jul 2025 13:54:03 +0200 Subject: [PATCH 76/92] Generalize EQC semantics to support arbitrarily heavy velocity channels --- qlbm/lattice/eqc/eqc.py | 9 +++++++-- qlbm/lattice/eqc/eqc_generator.py | 6 ++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/qlbm/lattice/eqc/eqc.py b/qlbm/lattice/eqc/eqc.py index b238e43..84c8d85 100644 --- a/qlbm/lattice/eqc/eqc.py +++ b/qlbm/lattice/eqc/eqc.py @@ -72,13 +72,18 @@ def __init__( self.discretization = discretization self.velocity_configurations = velocity_configurations - self.mass = sum(list(velocity_configurations)[0]) + channel_masses = LatticeDiscretizationProperties.get_channel_masses( + discretization + ) + self.mass = np.dot(channel_masses, list(velocity_configurations)[0]) velocity_vectors = LatticeDiscretizationProperties.get_velocity_vectors( discretization ) + if not all( - (sum(velocity_cfg) == self.mass) for velocity_cfg in velocity_configurations + (np.dot(velocity_cfg, channel_masses) == self.mass) + for velocity_cfg in velocity_configurations ): raise LatticeException("Velocity configurations have different masses.") self.momentum = sum( diff --git a/qlbm/lattice/eqc/eqc_generator.py b/qlbm/lattice/eqc/eqc_generator.py index 943a83b..d9df93c 100644 --- a/qlbm/lattice/eqc/eqc_generator.py +++ b/qlbm/lattice/eqc/eqc_generator.py @@ -29,7 +29,7 @@ class EquivalenceClassGenerator: .. code-block:: python :linenos: - + from qlbm.lattice import LatticeDiscretization from qlbm.lattice.eqc import EquivalenceClassGenerator @@ -66,7 +66,9 @@ def generate_equivalence_classes(self) -> Set[EquivalenceClass]: self.discretization ) state = np.array(state) # type: ignore - mass = np.sum(state) + mass = state @ LatticeDiscretizationProperties.get_channel_masses( + self.discretization + ) momentum = np.sum(state[:, None] * velocity_vectors, axis=0) # type: ignore key = (mass, tuple(momentum)) From 27f68ac340f3c6350e9dd669d51e21dac88f5860 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 21 Jul 2025 13:57:04 +0200 Subject: [PATCH 77/92] Make collision occur first in LQLGA loop --- qlbm/components/lqlga/lqlga.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/qlbm/components/lqlga/lqlga.py b/qlbm/components/lqlga/lqlga.py index 20ed5aa..2cd2d3b 100644 --- a/qlbm/components/lqlga/lqlga.py +++ b/qlbm/components/lqlga/lqlga.py @@ -42,6 +42,11 @@ def __init__( def create_circuit(self) -> QuantumCircuit: circuit = self.lattice.circuit.copy() + circuit.compose( + GenericLQLGACollisionOperator(self.lattice, self.logger).circuit, + inplace=True, + ) + circuit.compose( LQLGAStreamingOperator(self.lattice, self.logger).circuit, inplace=True ) @@ -53,11 +58,6 @@ def create_circuit(self) -> QuantumCircuit: inplace=True, ) - circuit.compose( - GenericLQLGACollisionOperator(self.lattice, self.logger).circuit, - inplace=True, - ) - return circuit @override From 4058d13d432769666adafe2743c2dd1810008619 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 21 Jul 2025 13:57:49 +0200 Subject: [PATCH 78/92] Change lattice parsing to allow DdQq discretizations directly --- qlbm/lattice/lattices/base.py | 87 ++++++++++++------- .../lattice/lattices/collisionless_lattice.py | 17 +++- qlbm/lattice/lattices/lqlga_lattice.py | 36 +------- qlbm/lattice/lattices/spacetime_lattice.py | 80 ++++++++--------- 4 files changed, 114 insertions(+), 106 deletions(-) diff --git a/qlbm/lattice/lattices/base.py b/qlbm/lattice/lattices/base.py index 55dfc7a..3562413 100644 --- a/qlbm/lattice/lattices/base.py +++ b/qlbm/lattice/lattices/base.py @@ -10,6 +10,10 @@ 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.spacetime.properties_base import ( + LatticeDiscretization, + LatticeDiscretizationProperties, +) from qlbm.tools.exceptions import LatticeException from qlbm.tools.utils import dimension_letter, flatten, is_two_pow @@ -157,7 +161,7 @@ def __init__( def parse_input_data( self, lattice_data: str | Dict, # type: ignore - ) -> Tuple[List[int], List[int], Dict[str, List[Shape]]]: + ) -> Tuple[List[int], List[int], Dict[str, List[Shape]], LatticeDiscretization]: r""" Parses the lattice input data, provided in either a file path or a dictionary. @@ -169,11 +173,12 @@ def parse_input_data( Returns ------- - Tuple[List[int], List[int], Dict[str, List[Shape]]] + Tuple[List[int], List[int], Dict[str, List[Shape]], LatticeDiscretization] A tuple containing (i) a list of the number of gridpoints per dimension, (ii) a list of the number of velicities per dimension, - and (iii) a dictionary containing the solid :class:`.Shape`\ s. + (iii) a dictionary containing the solid :class:`.Shape`\ s, + and (iv) the discretization enum of the lattice. The key of the dictionary is the specific kind of boundary condition of the obstacle (i.e., ``"bounceback"`` or ``"specular"``). @@ -232,11 +237,6 @@ def __parse_input_dict( 'Lattice configuration missing "velocities" properties.' ) - if len(lattice_dict["dim"]) != len(lattice_dict["velocities"]): # type: ignore - raise LatticeException( - "Lattice configuration dimensionality is inconsistent." - ) - num_dimensions = len(lattice_dict["dim"]) # type: ignore if num_dimensions not in [1, 2, 3]: @@ -244,37 +244,60 @@ def __parse_input_dict( f"Only 1, 2, and 3-dimensional lattices are supported. Provided lattice has {len(lattice_dict['dim'])} dimensions." # type: ignore ) - # Check whether the number of grid points and velocities is compatible - for dim in range(num_dimensions): - dim_index = dimension_letter(dim) + grid_list: 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) + ] - # ! TODO move to after parsing - if not is_two_pow(lattice_dict["dim"][dim_index]): # type: ignore + discretization: LatticeDiscretization = LatticeDiscretization.CFLDISCRETIZATION + velocity_list: List[int] = [] + + # Check if velocities is a string (DdQq format) or dict + if isinstance(lattice_dict["velocities"], str): # type: ignore + # Parse DdQq format (e.g., "D2Q4" means 2 dimensions, 4 velocities total) + velocity_spec = lattice_dict["velocities"] # type: ignore + if not velocity_spec.startswith("D") or "Q" not in velocity_spec: raise LatticeException( - f"Lattice {dim_index}-dimension has a number of grid points that is not divisible by 2." + f"Invalid velocity specification format: {lattice_dict['velocities']}. Expected format like 'd2q4'." ) - if not is_two_pow(lattice_dict["velocities"][dim_index]): # type: ignore + try: + parts = velocity_spec[1:].split("Q") + spec_dims = int(parts[0]) + total_velocities = int(parts[1]) + velocity_list = [] + + if spec_dims != num_dimensions: + raise LatticeException( + f"Velocity specification dimensions ({spec_dims}) do not match lattice dimensions ({num_dimensions})." + ) + + discretization = LatticeDiscretizationProperties.get_discretization( + num_dimensions, total_velocities + ) + + except (ValueError, IndexError): raise LatticeException( - f"Lattice {dim_index}-dimension has a number of velocities that is not divisible by 2." + f"Invalid velocity specification format: {lattice_dict['velocities']}. Expected format like 'DQ'." ) - # The lattice properties are ok - grid_list: 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) - ] - velocity_list: List[int] = [ - # -1 because the bit_length() would "overshoot" for powers of 2 - lattice_dict["velocities"][dimension_letter(dim)] - 1 - for dim in range(num_dimensions) - ] + else: + if len(lattice_dict["dim"]) != len(lattice_dict["velocities"]): # type: ignore + raise LatticeException( + "Lattice configuration dimensionality is inconsistent." + ) + + velocity_list = [ + # -1 because the bit_length() would "overshoot" for powers of 2 + lattice_dict["velocities"][dimension_letter(dim)] - 1 + for dim in range(num_dimensions) + ] parsed_obstacles: Dict[str, List[Shape]] = {"specular": [], "bounceback": []} if "geometry" not in input_dict: - return grid_list, velocity_list, parsed_obstacles + return grid_list, velocity_list, parsed_obstacles, discretization geometry_list: List[Dict[str, List[int]]] = input_dict["geometry"] # type: ignore @@ -373,7 +396,7 @@ def __parse_input_dict( ) ) - return grid_list, velocity_list, parsed_obstacles + return grid_list, velocity_list, parsed_obstacles, discretization def to_json(self) -> str: """ @@ -393,7 +416,11 @@ def to_json(self) -> str: "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 }, } diff --git a/qlbm/lattice/lattices/collisionless_lattice.py b/qlbm/lattice/lattices/collisionless_lattice.py index 58558e1..d02724e 100644 --- a/qlbm/lattice/lattices/collisionless_lattice.py +++ b/qlbm/lattice/lattices/collisionless_lattice.py @@ -8,7 +8,7 @@ from qlbm.lattice.geometry.shapes.base import Shape from qlbm.tools.exceptions import LatticeException -from qlbm.tools.utils import dimension_letter, flatten +from qlbm.tools.utils import dimension_letter, flatten, is_two_pow from .base import Lattice @@ -155,10 +155,23 @@ def __init__( logger: Logger = getLogger("qlbm"), ) -> None: super().__init__(lattice_data, logger) - dimensions, velocities, shapes = self.parse_input_data(lattice_data) # type: ignore + dimensions, velocities, shapes, self.discretization = self.parse_input_data( + lattice_data + ) # type: ignore self.num_dims = len(dimensions) self.num_gridpoints = dimensions + + for dim in range(self.num_dims): + if not is_two_pow(self.num_gridpoints[dim] + 1): # type: ignore + raise LatticeException( + f"Lattice has a number of grid points that is not divisible by 2 in dimension {dimension_letter(dim)}." + ) + if not is_two_pow(velocities[dim] + 1): # type: ignore + raise LatticeException( + f"Lattice has a number of velocities that is not divisible by 2 in dimension {dimension_letter(dim)}." + ) + self.num_velocities = velocities self.shapes: Dict[str, List[Shape]] = shapes # type: ignore self.shape_list: List[Shape] = flatten(list(shapes.values())) diff --git a/qlbm/lattice/lattices/lqlga_lattice.py b/qlbm/lattice/lattices/lqlga_lattice.py index e399fe3..41743c2 100644 --- a/qlbm/lattice/lattices/lqlga_lattice.py +++ b/qlbm/lattice/lattices/lqlga_lattice.py @@ -73,11 +73,10 @@ class LQLGALattice(Lattice): def __init__(self, lattice_data, logger=...): super().__init__(lattice_data, logger) - self.num_gridpoints, self.num_velocities, self.shapes = self.parse_input_data( - lattice_data + self.num_gridpoints, self.num_velocities, self.shapes, self.discretization = ( + self.parse_input_data(lattice_data) ) # type: ignore self.num_dims = len(self.num_gridpoints) - self.discretization = self.__get_discretization() self.num_velocities_per_point = ( LatticeDiscretizationProperties.get_num_velocities(self.discretization) ) @@ -95,34 +94,6 @@ def __init__(self, lattice_data, logger=...): self.circuit = QuantumCircuit(*self.registers) - def __get_discretization(self) -> LatticeDiscretization: - if self.num_dims == 1: - if self.num_velocities[0] == 1: - return LatticeDiscretization.D1Q2 - raise LatticeException( - f"Unsupported number of velocities for 1D: {self.num_velocities[0] + 1}. Only D1Q2 is supported at the moment." - ) - - if self.num_dims == 2: - if self.num_velocities[0] == 1 and self.num_velocities[1] == 1: - return LatticeDiscretization.D2Q4 - raise LatticeException( - f"Unsupported number of velocities for 2D: {(self.num_velocities[0] + 1, self.num_velocities[1] + 1)}. Only D2Q4 is supported at the moment." - ) - - if self.num_dims == 3: - if ( - self.num_velocities[0] == 1 - and self.num_velocities[1] == 1 - and self.num_velocities[2] == 1 - ): - return LatticeDiscretization.D3Q6 - raise LatticeException( - f"Unsupported number of velocities for 3D: {(self.num_velocities[0] + 1, self.num_velocities[1] + 1)}. Only D3Q6 is supported at the moment." - ) - - raise LatticeException("Only 1-3D discretizations are currently available.") - @override def get_registers(self) -> Tuple[List[QuantumRegister], ...]: velocity_registers = [ @@ -284,7 +255,7 @@ def get_velocity_qubits_of_line(self, line_index: int) -> Tuple[int, int]: Parameters ---------- line_index : int - The index of the line to get the velocity qubits for. + The index of the line to get the velocity qubits for. Counted from 0 according to the regular discretization taxonomy. Returns ------- @@ -295,6 +266,7 @@ def get_velocity_qubits_of_line(self, line_index: int) -> Tuple[int, int]: raise LatticeException( f"Streaming Line index {line_index} is out of bounds for the lattice with {self.num_velocities_per_point // 2} lines." ) + return ( (self.num_velocities_per_point % 2) + line_index, (self.num_velocities_per_point % 2) diff --git a/qlbm/lattice/lattices/spacetime_lattice.py b/qlbm/lattice/lattices/spacetime_lattice.py index 86f1e0d..d7c3e6e 100644 --- a/qlbm/lattice/lattices/spacetime_lattice.py +++ b/qlbm/lattice/lattices/spacetime_lattice.py @@ -10,9 +10,12 @@ from qlbm.lattice.spacetime.d1q2 import D1Q2SpaceTimeLatticeBuilder from qlbm.lattice.spacetime.d2q4 import D2Q4SpaceTimeLatticeBuilder from qlbm.lattice.spacetime.d3q6 import D3Q6SpaceTimeLatticeBuilder -from qlbm.lattice.spacetime.properties_base import SpaceTimeLatticeBuilder +from qlbm.lattice.spacetime.properties_base import ( + LatticeDiscretization, + SpaceTimeLatticeBuilder, +) from qlbm.tools.exceptions import LatticeException -from qlbm.tools.utils import flatten +from qlbm.tools.utils import flatten, is_two_pow class SpaceTimeLattice(Lattice): @@ -123,10 +126,17 @@ def __init__( self.include_measurement_qubit = include_measurement_qubit self.use_volumetric_ops = use_volumetric_ops - self.num_gridpoints, self.num_velocities, self.shapes = self.parse_input_data( - lattice_data + self.num_gridpoints, self.num_velocities, self.shapes, self.discretization = ( + self.parse_input_data(lattice_data) ) # type: ignore self.num_dims = len(self.num_gridpoints) + + for dim in range(self.num_dims): + if not is_two_pow(self.num_gridpoints[dim] + 1): # type: ignore + raise LatticeException( + f"Lattice has a number of grid points that is not divisible by 2 in dimension {dim}." + ) + self.num_timesteps = num_timesteps self.properties: SpaceTimeLatticeBuilder = self.__get_builder() @@ -152,50 +162,36 @@ def __init__( logger.info(self.__str__()) def __get_builder(self) -> SpaceTimeLatticeBuilder: - if self.num_dims == 1: - if self.num_velocities[0] == 1: - return D1Q2SpaceTimeLatticeBuilder( - self.num_timesteps, - self.num_gridpoints, - include_measurement_qubit=self.include_measurement_qubit, - use_volumetric_ops=self.use_volumetric_ops, - logger=self.logger, - ) - raise LatticeException( - f"Unsupported number of velocities for 1D: {self.num_velocities[0] + 1}. Only D1Q2 is supported at the moment." + if self.discretization == LatticeDiscretization.D1Q2: + return D1Q2SpaceTimeLatticeBuilder( + self.num_timesteps, + self.num_gridpoints, + include_measurement_qubit=self.include_measurement_qubit, + use_volumetric_ops=self.use_volumetric_ops, + logger=self.logger, ) - if self.num_dims == 2: - if self.num_velocities[0] == 1 and self.num_velocities[1] == 1: - return D2Q4SpaceTimeLatticeBuilder( - self.num_timesteps, - self.num_gridpoints, - include_measurement_qubit=self.include_measurement_qubit, - use_volumetric_ops=self.use_volumetric_ops, - logger=self.logger, - ) - raise LatticeException( - f"Unsupported number of velocities for 2D: {(self.num_velocities[0] + 1, self.num_velocities[1] + 1)}. Only D2Q4 is supported at the moment." + if self.discretization == LatticeDiscretization.D2Q4: + return D2Q4SpaceTimeLatticeBuilder( + self.num_timesteps, + self.num_gridpoints, + include_measurement_qubit=self.include_measurement_qubit, + use_volumetric_ops=self.use_volumetric_ops, + logger=self.logger, ) - if self.num_dims == 3: - if ( - self.num_velocities[0] == 1 - and self.num_velocities[1] == 1 - and self.num_velocities[2] == 1 - ): - return D3Q6SpaceTimeLatticeBuilder( - self.num_timesteps, - self.num_gridpoints, - include_measurement_qubit=self.include_measurement_qubit, - use_volumetric_ops=self.use_volumetric_ops, - logger=self.logger, - ) - raise LatticeException( - f"Unsupported number of velocities for 3D: {(self.num_velocities[0] + 1, self.num_velocities[1] + 1)}. Only D3Q6 is supported at the moment." + if self.discretization == LatticeDiscretization.D3Q6: + return D3Q6SpaceTimeLatticeBuilder( + self.num_timesteps, + self.num_gridpoints, + include_measurement_qubit=self.include_measurement_qubit, + use_volumetric_ops=self.use_volumetric_ops, + logger=self.logger, ) - raise LatticeException("Only 1-3D discretizations are currently available.") + raise LatticeException( + f"{self.discretization} not currently implemented for the Space-Time method. Only D1Q2 and D2Q4 are fully at the moment. D3Q6 only supports collision." + ) 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. From dabbe0f3fd2eac13ba01396b9d5d36c9d37521c5 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 21 Jul 2025 13:59:15 +0200 Subject: [PATCH 79/92] Add support for D1Q3 geometry --- qlbm/lattice/geometry/shapes/base.py | 9 +- qlbm/lattice/geometry/shapes/block.py | 16 +++- qlbm/lattice/spacetime/d1q2.py | 4 +- qlbm/lattice/spacetime/properties_base.py | 107 ++++++++++++++++++++++ 4 files changed, 131 insertions(+), 5 deletions(-) diff --git a/qlbm/lattice/geometry/shapes/base.py b/qlbm/lattice/geometry/shapes/base.py index 9d8cb33..0dd18f9 100644 --- a/qlbm/lattice/geometry/shapes/base.py +++ b/qlbm/lattice/geometry/shapes/base.py @@ -72,7 +72,7 @@ class LQLGAShape(Shape): def __init__(self, num_grid_qubits: List[int], boundary_condition: str): super().__init__(num_grid_qubits, boundary_condition) - def get_lqlga_reflection_data_d1q2_from_points( + def get_lqlga_reflection_data_1d_from_points( self, gridpoints: List[Tuple[int, ...]], before_reflection_velocity_index: int, @@ -128,6 +128,13 @@ def get_lqlga_reflection_data_d1q2( """Calculate space-time reflection data for :math:`D_1Q_2` :class:`.LQLGA`.""" pass + @abstractmethod + def get_lqlga_reflection_data_d1q3( + self, + ) -> List[LQLGAPointwiseReflectionData]: + """Calculate space-time reflection data for :math:`D_1Q_2` :class:`.LQLGA`.""" + pass + class SpaceTimeShape(Shape): """Base class for all shapes compatible with the :class:`.SpaceTimeQLBM` algorithm.""" diff --git a/qlbm/lattice/geometry/shapes/block.py b/qlbm/lattice/geometry/shapes/block.py index 81a3c3f..3704c1c 100644 --- a/qlbm/lattice/geometry/shapes/block.py +++ b/qlbm/lattice/geometry/shapes/block.py @@ -709,16 +709,28 @@ def get_d2q4_surfaces(self) -> List[List[List[Tuple[int, ...]]]]: @override def get_lqlga_reflection_data_d1q2(self): - return self.get_lqlga_reflection_data_d1q2_from_points( + return self.get_lqlga_reflection_data_1d_from_points( [tuple([self.bounds[0][0]])], 0, 1, tuple([-1]), 2 ** self.num_grid_qubits[0], - ) + self.get_lqlga_reflection_data_d1q2_from_points( + ) + self.get_lqlga_reflection_data_1d_from_points( [tuple([self.bounds[0][1]])], 1, 0, tuple([1]), 2 ** self.num_grid_qubits[0] ) + @override + def get_lqlga_reflection_data_d1q3(self): + return self.get_lqlga_reflection_data_1d_from_points( + [tuple([self.bounds[0][0]])], + 1, + 2, + tuple([-1]), + 2 ** self.num_grid_qubits[0], + ) + self.get_lqlga_reflection_data_1d_from_points( + [tuple([self.bounds[0][1]])], 2, 1, tuple([1]), 2 ** self.num_grid_qubits[0] + ) + @override def contains_gridpoint(self, gridpoint: Tuple[int, ...]) -> bool: return all( diff --git a/qlbm/lattice/spacetime/d1q2.py b/qlbm/lattice/spacetime/d1q2.py index 5244be4..546da08 100644 --- a/qlbm/lattice/spacetime/d1q2.py +++ b/qlbm/lattice/spacetime/d1q2.py @@ -1,4 +1,4 @@ -""":math:`D_2Q_4` STQBM builder.""" +""":math:`D_1Q_2` STQBM builder.""" from logging import Logger, getLogger from typing import Dict, List, Tuple, cast @@ -16,7 +16,7 @@ class D1Q2SpaceTimeLatticeBuilder(SpaceTimeLatticeBuilder): - """:math:`D_2Q_4` STQBM builder.""" + """:math:`D_1Q_2` STQBM builder.""" # Points to the right of the origin are marked as 0 # And points to the left are marked as 1 diff --git a/qlbm/lattice/spacetime/properties_base.py b/qlbm/lattice/spacetime/properties_base.py index 8def8fb..0d7d29a 100644 --- a/qlbm/lattice/spacetime/properties_base.py +++ b/qlbm/lattice/spacetime/properties_base.py @@ -21,9 +21,11 @@ class LatticeDiscretization(Enum): The only supported discretizations currently are D1Q2 and D2Q4. """ + CFLDISCRETIZATION = (0,) D1Q2 = (1,) D2Q4 = (2,) D3Q6 = (3,) + D1Q3 = (4,) class LatticeDiscretizationProperties: @@ -48,12 +50,35 @@ class LatticeDiscretizationProperties: LatticeDiscretization.D3Q6: np.array( [[1, 0, 0], [0, 1, 0], [0, 0, 1], [-1, 0, 0], [0, -1, 0], [0, 0, -1]] ), + LatticeDiscretization.D1Q3: np.array([[0], [1], [-1]]), + } + + channel_masses: Dict[LatticeDiscretization, np.ndarray] = { + LatticeDiscretization.D1Q2: np.ones(2), + LatticeDiscretization.D2Q4: np.ones(4), + LatticeDiscretization.D3Q6: np.ones(6), + LatticeDiscretization.D1Q3: np.concatenate((np.array([2]), np.ones(2))), } num_velocities: Dict[LatticeDiscretization, int] = { LatticeDiscretization.D1Q2: 2, LatticeDiscretization.D2Q4: 4, LatticeDiscretization.D3Q6: 6, + LatticeDiscretization.D1Q3: 3, + } + + num_dimensions: Dict[LatticeDiscretization, int] = { + LatticeDiscretization.D1Q2: 1, + LatticeDiscretization.D2Q4: 2, + LatticeDiscretization.D3Q6: 3, + LatticeDiscretization.D1Q3: 1, + } + + string_representation: Dict[LatticeDiscretization, str] = { + LatticeDiscretization.D1Q2: "D1Q2", + LatticeDiscretization.D2Q4: "D2Q4", + LatticeDiscretization.D3Q6: "D3Q6", + LatticeDiscretization.D1Q3: "D1Q3", } @staticmethod @@ -97,6 +122,88 @@ def get_num_velocities( return LatticeDiscretizationProperties.num_velocities[discretization] + @staticmethod + def get_channel_masses( + discretization: LatticeDiscretization, + ) -> np.ndarray: + """ + Get the channel masses for a given discretization. + + Parameters + ---------- + discretization : LatticeDiscretization + The discretization for which to get the channel masses. + + Returns + ------- + np.ndarray + The channel masses corresponding to the discretization. + """ + return LatticeDiscretizationProperties.channel_masses[discretization] + + @staticmethod + def get_discretizations_of_dimensionality( + num_dimensions: int, + ) -> List[LatticeDiscretization]: + """ + Get all discretizations with a given number of dimensions. + + Parameters + ---------- + num_dimensions : int + The number of dimensions to filter by. + + Returns + ------- + List[LatticeDiscretization] + A list of discretizations with the specified number of dimensions. + """ + return [ + d + for d in LatticeDiscretization + if d != LatticeDiscretization.CFLDISCRETIZATION + and LatticeDiscretizationProperties.num_dimensions[d] == num_dimensions + ] + + @staticmethod + def get_discretization( + num_dimensions: int, num_velocities: int + ) -> LatticeDiscretization: + """ + Get the discretization for a given number of dimensions and velocities. + + Parameters + ---------- + num_dimensions : int + The number of dimensions. + num_velocities : int + The number of velocities. + + Returns + ------- + LatticeDiscretization + The discretization corresponding to the given parameters. + """ + compatible_discretizations = ( + LatticeDiscretizationProperties.get_discretizations_of_dimensionality( + num_dimensions + ) + ) + + compatible_discretizations = [ + discretization + for discretization in compatible_discretizations + if LatticeDiscretizationProperties.get_num_velocities(discretization) + == num_velocities + ] + + if not compatible_discretizations: + raise ValueError( + f"No discretization found for {num_dimensions} dimensions and {num_velocities} velocities." + ) + + return compatible_discretizations[0] + class VonNeumannNeighborType(Enum): """ From 5189549ebb53d0c374eff562c850c0a9fdcc4a77 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 21 Jul 2025 14:02:37 +0200 Subject: [PATCH 80/92] Normalize density in LQGLA result --- qlbm/infra/result/lqlga_result.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/qlbm/infra/result/lqlga_result.py b/qlbm/infra/result/lqlga_result.py index cd6613b..5d45326 100644 --- a/qlbm/infra/result/lqlga_result.py +++ b/qlbm/infra/result/lqlga_result.py @@ -10,6 +10,7 @@ from vtkmodules.util import numpy_support from qlbm.lattice.lattices.lqlga_lattice import LQLGALattice +from qlbm.lattice.spacetime.properties_base import LatticeDiscretizationProperties from .base import QBMResult @@ -57,9 +58,13 @@ def save_timestep_counts( create_vis: bool = True, save_array: bool = False, ): + total_counts = sum(counts.values()) if self.lattice.num_dims == 1: # The second dimension is a dirty rendering trick for VTK and Paraview count_history = np.zeros((self.lattice.num_gridpoints[0] + 1, 2)) + channel_masses = LatticeDiscretizationProperties.get_channel_masses( + self.lattice.discretization + ) for count in counts: count_inverse = count[::-1] num_vel = self.lattice.num_velocities_per_point @@ -67,15 +72,20 @@ def save_timestep_counts( self.lattice.num_base_qubits // self.lattice.num_velocities_per_point ): - num_populations = int( - count_inverse[gp * num_vel : (gp + 1) * num_vel][::-1].count( - "1" # The number of 1s is the number of populations - ) - ) - # Another dirty rendering trick for VTK and Paraview - count_history[gp][0] = count_history[gp][1] = ( - counts[count] * num_populations + mass = np.dot( + np.array( + list( + map( + lambda x: float(x), + count_inverse[gp * num_vel : (gp + 1) * num_vel], + ) + ) + ), + channel_masses, ) + pops = counts[count] * mass / total_counts + count_history[gp][0] += pops + count_history[gp][1] += pops self.save_timestep_array( np.transpose(count_history), timestep, From 15780aa93c5d5d236ad76628d730a1f9ac6ec3db Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 21 Jul 2025 14:03:12 +0200 Subject: [PATCH 81/92] Update tests for new discretization parsing --- .../{spacetime => }/collision/__init__.py | 0 .../collision/eqc_collision_test.py | 18 ++++++++++----- .../collision/eqc_generator_test.py | 6 ++++- .../collision/eqc_permutation_test.py | 0 .../eqc_velocity_discretization_test.py | 2 +- test/unit/collisionles_lattice_test.py | 4 ++-- test/unit/lqlga/circuits/conftest.py | 6 ++--- test/unit/lqlga/conftest.py | 4 ++-- test/unit/lqlga/lqlga_lattice_test.py | 2 +- test/unit/spacetime/circuits/mcswap_test.py | 4 ++-- test/unit/spacetime/conftest.py | 22 +++++++++---------- test/unit/spacetime/d1q2_lattice_test.py | 2 +- test/unit/spacetime/d2q4_lattice_test.py | 16 +++++++------- 13 files changed, 48 insertions(+), 38 deletions(-) rename test/unit/{spacetime => }/collision/__init__.py (100%) rename test/unit/{spacetime => }/collision/eqc_collision_test.py (90%) rename test/unit/{spacetime => }/collision/eqc_generator_test.py (95%) rename test/unit/{spacetime => }/collision/eqc_permutation_test.py (100%) rename test/unit/{spacetime => }/collision/eqc_velocity_discretization_test.py (100%) diff --git a/test/unit/spacetime/collision/__init__.py b/test/unit/collision/__init__.py similarity index 100% rename from test/unit/spacetime/collision/__init__.py rename to test/unit/collision/__init__.py diff --git a/test/unit/spacetime/collision/eqc_collision_test.py b/test/unit/collision/eqc_collision_test.py similarity index 90% rename from test/unit/spacetime/collision/eqc_collision_test.py rename to test/unit/collision/eqc_collision_test.py index 6e9447b..2e09a7c 100644 --- a/test/unit/spacetime/collision/eqc_collision_test.py +++ b/test/unit/collision/eqc_collision_test.py @@ -97,12 +97,14 @@ def test_d2q4_collision_positive_cases( lattice = SpaceTimeLattice( 1, { - "lattice": {"dim": {"x": 4, "y": 4}, "velocities": {"x": 2, "y": 2}}, + "lattice": {"dim": {"x": 4, "y": 4}, "velocities": "D2Q4"}, "geometry": [], }, ) - local_circuit = EQCCollisionOperator(lattice.properties.get_discretization()).circuit + local_circuit = EQCCollisionOperator( + lattice.properties.get_discretization() + ).circuit assert local_circuit.num_qubits == 4 eqc = d2q4_equivalence_class_bitstrings[equivalence_class_index] for velocity_cfg in eqc: @@ -115,12 +117,14 @@ def test_d2q4_collision_negative_cases(d2q4_equivalence_class_bitstrings): lattice = SpaceTimeLattice( 1, { - "lattice": {"dim": {"x": 4, "y": 4}, "velocities": {"x": 2, "y": 2}}, + "lattice": {"dim": {"x": 4, "y": 4}, "velocities": "D2Q4"}, "geometry": [], }, ) - local_circuit = EQCCollisionOperator(lattice.properties.get_discretization()).circuit + local_circuit = EQCCollisionOperator( + lattice.properties.get_discretization() + ).circuit assert local_circuit.num_qubits == 4 for b in [ @@ -157,13 +161,15 @@ def test_d3q6_collision_positive_cases( { "lattice": { "dim": {"x": 2, "y": 2, "z": 2}, - "velocities": {"x": 2, "y": 2, "z": 2}, + "velocities": "D3Q6", }, "geometry": [], }, ) - local_circuit = EQCCollisionOperator(lattice.properties.get_discretization()).circuit + local_circuit = EQCCollisionOperator( + lattice.properties.get_discretization() + ).circuit assert local_circuit.num_qubits == 6 eqc = d3q6_equivalence_class_bitstrings[equivalence_class_index] for velocity_cfg in eqc: diff --git a/test/unit/spacetime/collision/eqc_generator_test.py b/test/unit/collision/eqc_generator_test.py similarity index 95% rename from test/unit/spacetime/collision/eqc_generator_test.py rename to test/unit/collision/eqc_generator_test.py index 66e52e2..66934e3 100644 --- a/test/unit/spacetime/collision/eqc_generator_test.py +++ b/test/unit/collision/eqc_generator_test.py @@ -1,9 +1,9 @@ import numpy as np +from qlbm.lattice.eqc.eqc import EquivalenceClass from qlbm.lattice.eqc.eqc_generator import ( EquivalenceClassGenerator, ) -from qlbm.lattice.eqc.eqc import EquivalenceClass from qlbm.lattice.spacetime.properties_base import LatticeDiscretization @@ -12,6 +12,10 @@ def test_eqc_generator_d1q2(): eqcs = generator.generate_equivalence_classes() assert len(eqcs) == 0 +def test_eqc_generator_d1q3(): + generator = EquivalenceClassGenerator(LatticeDiscretization.D1Q3) + eqcs = generator.generate_equivalence_classes() + assert len(eqcs) == 1 def test_eqc_generator_d2q4(): generator = EquivalenceClassGenerator(LatticeDiscretization.D2Q4) diff --git a/test/unit/spacetime/collision/eqc_permutation_test.py b/test/unit/collision/eqc_permutation_test.py similarity index 100% rename from test/unit/spacetime/collision/eqc_permutation_test.py rename to test/unit/collision/eqc_permutation_test.py diff --git a/test/unit/spacetime/collision/eqc_velocity_discretization_test.py b/test/unit/collision/eqc_velocity_discretization_test.py similarity index 100% rename from test/unit/spacetime/collision/eqc_velocity_discretization_test.py rename to test/unit/collision/eqc_velocity_discretization_test.py index 3470a4f..4f78d9e 100644 --- a/test/unit/spacetime/collision/eqc_velocity_discretization_test.py +++ b/test/unit/collision/eqc_velocity_discretization_test.py @@ -2,10 +2,10 @@ import pytest +from qlbm.lattice.eqc.eqc import EquivalenceClass from qlbm.lattice.eqc.eqc_generator import ( EquivalenceClassGenerator, ) -from qlbm.lattice.eqc.eqc import EquivalenceClass from qlbm.lattice.spacetime.properties_base import LatticeDiscretization from qlbm.tools.exceptions import LatticeException diff --git a/test/unit/collisionles_lattice_test.py b/test/unit/collisionles_lattice_test.py index 7e92a67..f69fa66 100644 --- a/test/unit/collisionles_lattice_test.py +++ b/test/unit/collisionles_lattice_test.py @@ -183,7 +183,7 @@ def test_lattice_exception_mismatched_bad_dimensions(): ) assert ( - "Lattice y-dimension has a number of grid points that is not divisible by 2." + "Lattice has a number of grid points that is not divisible by 2 in dimension y." == str(excinfo.value) ) @@ -200,7 +200,7 @@ def test_lattice_exception_mismatched_bad_velocities(): ) assert ( - "Lattice y-dimension has a number of velocities that is not divisible by 2." + "Lattice has a number of velocities that is not divisible by 2 in dimension y." == str(excinfo.value) ) diff --git a/test/unit/lqlga/circuits/conftest.py b/test/unit/lqlga/circuits/conftest.py index b8c5710..e94a2e1 100644 --- a/test/unit/lqlga/circuits/conftest.py +++ b/test/unit/lqlga/circuits/conftest.py @@ -9,7 +9,7 @@ def lattice_d2q4_256_8() -> LQLGALattice: { "lattice": { "dim": {"x": 256, "y": 8}, - "velocities": {"x": 2, "y": 2}, + "velocities": "D2Q4", }, }, ) @@ -21,7 +21,7 @@ def lattice_d1q2_256() -> LQLGALattice: { "lattice": { "dim": {"x": 256}, - "velocities": {"x": 2}, + "velocities": "D1Q2", }, }, ) @@ -32,7 +32,7 @@ def lattice_d1q2_8() -> LQLGALattice: { "lattice": { "dim": {"x": 8}, - "velocities": {"x": 2}, + "velocities": "D1Q2", }, }, ) diff --git a/test/unit/lqlga/conftest.py b/test/unit/lqlga/conftest.py index 245a555..0bb459e 100644 --- a/test/unit/lqlga/conftest.py +++ b/test/unit/lqlga/conftest.py @@ -9,7 +9,7 @@ def lattice_d2q4_256_8() -> LQLGALattice: { "lattice": { "dim": {"x": 256, "y": 8}, - "velocities": {"x": 2, "y": 2}, + "velocities": "D2Q4", }, }, ) @@ -21,7 +21,7 @@ def lattice_d1q2_256() -> LQLGALattice: { "lattice": { "dim": {"x": 256}, - "velocities": {"x": 2}, + "velocities": "D1Q2", }, }, ) diff --git a/test/unit/lqlga/lqlga_lattice_test.py b/test/unit/lqlga/lqlga_lattice_test.py index 39f115e..15daded 100644 --- a/test/unit/lqlga/lqlga_lattice_test.py +++ b/test/unit/lqlga/lqlga_lattice_test.py @@ -21,7 +21,7 @@ def test_lqlga_grid_index_mapping_edge(): { "lattice": { "dim": {"x": 64, "y": 8}, - "velocities": {"x": 2, "y": 2}, + "velocities": "D2Q4", }, }, ) diff --git a/test/unit/spacetime/circuits/mcswap_test.py b/test/unit/spacetime/circuits/mcswap_test.py index e6ac124..27fdd2e 100644 --- a/test/unit/spacetime/circuits/mcswap_test.py +++ b/test/unit/spacetime/circuits/mcswap_test.py @@ -10,7 +10,7 @@ def test_mcswap_13ctrl(): { "lattice": { "dim": {"x": 16, "y": 16}, - "velocities": {"x": 2, "y": 2}, + "velocities": "D2Q4", }, "geometry": [], }, @@ -36,7 +36,7 @@ def test_mcswap_grid_ctrl(): { "lattice": { "dim": {"x": 16, "y": 16}, - "velocities": {"x": 2, "y": 2}, + "velocities": "D2Q4", }, "geometry": [], }, diff --git a/test/unit/spacetime/conftest.py b/test/unit/spacetime/conftest.py index c098e93..52a7c34 100644 --- a/test/unit/spacetime/conftest.py +++ b/test/unit/spacetime/conftest.py @@ -13,7 +13,7 @@ def dummy_1d_lattice() -> SpaceTimeLattice: { "lattice": { "dim": {"x": 256}, - "velocities": {"x": 2}, + "velocities": "D1Q2", }, }, ) @@ -26,7 +26,7 @@ def lattice_1d_16_1_obstacle_1_timestep() -> SpaceTimeLattice: { "lattice": { "dim": {"x": 16}, - "velocities": {"x": 2}, + "velocities": "D1Q2" }, "geometry": [ {"shape": "cuboid", "x": [4, 6], "boundary": "bounceback"}, @@ -42,7 +42,7 @@ def lattice_1d_16_1_obstacle_2_timesteps() -> SpaceTimeLattice: { "lattice": { "dim": {"x": 16}, - "velocities": {"x": 2}, + "velocities": "D1Q2" }, "geometry": [ {"shape": "cuboid", "x": [4, 6], "boundary": "bounceback"}, @@ -58,7 +58,7 @@ def lattice_1d_16_1_obstacle_5_timesteps() -> SpaceTimeLattice: { "lattice": { "dim": {"x": 16}, - "velocities": {"x": 2}, + "velocities": "D1Q2" }, }, ) @@ -71,7 +71,7 @@ def volumetric_lattice_1d_16_1_obstacle_1_timestep() -> SpaceTimeLattice: { "lattice": { "dim": {"x": 16}, - "velocities": {"x": 2}, + "velocities": "D1Q2" }, }, use_volumetric_ops=True, @@ -86,7 +86,7 @@ def dummy_lattice() -> SpaceTimeLattice: { "lattice": { "dim": {"x": 32, "y": 32}, - "velocities": {"x": 2, "y": 2}, + "velocities": "D2Q4" }, }, ) @@ -99,7 +99,7 @@ def lattice_2d_16x16_1_obstacle_1_timestep() -> SpaceTimeLattice: { "lattice": { "dim": {"x": 16, "y": 16}, - "velocities": {"x": 2, "y": 2}, + "velocities": "D2Q4" }, "geometry": [ { @@ -120,7 +120,7 @@ def lattice_2d_16x16_1_obstacle_2_timesteps() -> SpaceTimeLattice: { "lattice": { "dim": {"x": 16, "y": 16}, - "velocities": {"x": 2, "y": 2}, + "velocities": "D2Q4" }, "geometry": [ { @@ -141,7 +141,7 @@ def lattice_2d_16x16_1_obstacle_5_timesteps() -> SpaceTimeLattice: { "lattice": { "dim": {"x": 16, "y": 16}, - "velocities": {"x": 2, "y": 2}, + "velocities": "D2Q4" }, "geometry": [ { @@ -162,7 +162,7 @@ def lattice_1d_16_1_obstacle_5_timesteps_1_obstacle() -> SpaceTimeLattice: { "lattice": { "dim": {"x": 16}, - "velocities": {"x": 2}, + "velocities": "D1Q2" }, "geometry": [ {"shape": "cuboid", "x": [5, 11], "boundary": "bounceback"}, @@ -178,7 +178,7 @@ def lattice_1d_16_1_obstacle_5_timesteps_large_obstacle() -> SpaceTimeLattice: { "lattice": { "dim": {"x": 16}, - "velocities": {"x": 2}, + "velocities": "D1Q2" }, "geometry": [ {"shape": "cuboid", "x": [2, 14], "boundary": "bounceback"}, diff --git a/test/unit/spacetime/d1q2_lattice_test.py b/test/unit/spacetime/d1q2_lattice_test.py index 99bd5e3..3a64214 100644 --- a/test/unit/spacetime/d1q2_lattice_test.py +++ b/test/unit/spacetime/d1q2_lattice_test.py @@ -239,7 +239,7 @@ def test_bad_lattice_specification_velocities(): ) assert ( - "Unsupported number of velocities for 1D: 4. Only D1Q2 is supported at the moment." + "LatticeDiscretization.CFLDISCRETIZATION not currently implemented for the Space-Time method. Only D1Q2 and D2Q4 are fully at the moment. D3Q6 only supports collision." == str(excinfo_measure.value) ) diff --git a/test/unit/spacetime/d2q4_lattice_test.py b/test/unit/spacetime/d2q4_lattice_test.py index ef5e8cc..c685bea 100644 --- a/test/unit/spacetime/d2q4_lattice_test.py +++ b/test/unit/spacetime/d2q4_lattice_test.py @@ -15,7 +15,7 @@ def lattice_2d_16x16_1_obstacle_1_timestep() -> SpaceTimeLattice: { "lattice": { "dim": {"x": 16, "y": 16}, - "velocities": {"x": 2, "y": 2}, + "velocities": "D2Q4", }, "geometry": [ {"shape": "cuboid", "x": [4, 6], "y": [3, 12], "boundary": "specular"}, @@ -31,7 +31,7 @@ def lattice_2d_16x16_1_obstacle_2_timesteps() -> SpaceTimeLattice: { "lattice": { "dim": {"x": 16, "y": 16}, - "velocities": {"x": 2, "y": 2}, + "velocities": "D2Q4", }, "geometry": [ {"shape": "cuboid", "x": [4, 6], "y": [3, 12], "boundary": "specular"}, @@ -47,7 +47,7 @@ def volumetric_lattice_2d_16x16_1_obstacle_1_timestep() -> SpaceTimeLattice: { "lattice": { "dim": {"x": 16, "y": 16}, - "velocities": {"x": 2, "y": 2}, + "velocities": "D2Q4", }, }, use_volumetric_ops=True, @@ -207,7 +207,7 @@ def test_adaptable_qubit_register_indexing_measure_off_volume_off(): { "lattice": { "dim": {"x": 16, "y": 16}, - "velocities": {"x": 2, "y": 2}, + "velocities": "D2Q4", }, }, ) @@ -237,7 +237,7 @@ def test_adaptable_qubit_register_indexing_measure_on_volume_off(): { "lattice": { "dim": {"x": 16, "y": 16}, - "velocities": {"x": 2, "y": 2}, + "velocities": "D2Q4", }, }, include_measurement_qubit=True, @@ -262,7 +262,7 @@ def test_adaptable_qubit_register_indexing_measure_on_volume_on(): { "lattice": { "dim": {"x": 16, "y": 16}, - "velocities": {"x": 2, "y": 2}, + "velocities": "D2Q4", }, }, include_measurement_qubit=True, @@ -1123,7 +1123,7 @@ def test_bad_lattice_specification_velocities(): ) assert ( - "Unsupported number of velocities for 2D: (4, 2). Only D2Q4 is supported at the moment." + "LatticeDiscretization.CFLDISCRETIZATION not currently implemented for the Space-Time method. Only D1Q2 and D2Q4 are fully at the moment. D3Q6 only supports collision." == str(excinfo_measure.value) ) @@ -1152,7 +1152,7 @@ def test_2d_lattice_grid_register(): { "lattice": { "dim": {"x": 256, "y": 256}, - "velocities": {"x": 2, "y": 2}, + "velocities": "D2Q4", }, }, ) From 00112f6f7eef45347c4042d584a17bf1760fc447 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 21 Jul 2025 14:07:50 +0200 Subject: [PATCH 82/92] Fix linting --- qlbm/components/common/cbse_collision/cbse_redistribution.py | 2 +- qlbm/lattice/lattices/base.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/qlbm/components/common/cbse_collision/cbse_redistribution.py b/qlbm/components/common/cbse_collision/cbse_redistribution.py index 8a4f408..25d66fa 100644 --- a/qlbm/components/common/cbse_collision/cbse_redistribution.py +++ b/qlbm/components/common/cbse_collision/cbse_redistribution.py @@ -125,7 +125,7 @@ def create_circuit(self): circuit.compose( redistribution_circuit.control( nv - int(nq), - label=rf"MCRY(π/2, nq)" if is_two_pow(n) else rf"Coll({n}, {nq})", + label=rf"MCRY(π/2, {nq})" if is_two_pow(n) else rf"Coll({n}, {nq})", ), qubits=list(range(nv - 1, -1, -1)), inplace=True, diff --git a/qlbm/lattice/lattices/base.py b/qlbm/lattice/lattices/base.py index 3562413..4c640db 100644 --- a/qlbm/lattice/lattices/base.py +++ b/qlbm/lattice/lattices/base.py @@ -15,7 +15,7 @@ LatticeDiscretizationProperties, ) from qlbm.tools.exceptions import LatticeException -from qlbm.tools.utils import dimension_letter, flatten, is_two_pow +from qlbm.tools.utils import dimension_letter, flatten class Lattice(ABC): From 3f32468e436dd5c9279c032b3dbc71da5569a489 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 21 Jul 2025 15:59:49 +0200 Subject: [PATCH 83/92] Add discretization attribute to base lattice --- qlbm/lattice/lattices/base.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/qlbm/lattice/lattices/base.py b/qlbm/lattice/lattices/base.py index 4c640db..d717237 100644 --- a/qlbm/lattice/lattices/base.py +++ b/qlbm/lattice/lattices/base.py @@ -43,6 +43,7 @@ class Lattice(ABC): :attr:`num_total_qubits` The total number of qubits required for the quantum circuit to simulate the lattice. :attr:`registers` The qubit registers of the quantum algorithm. :attr:`circuit` The blueprint quantum circuit for all components of the algorithm. + :attr:`discretization` The discretization of the lattice, as an enum value of :class:`.LatticeDiscretization`. :attr:`shapes` A list of the solid geometry objects. :attr:`logger` The performance logger. =========================== ====================================================================== @@ -150,6 +151,11 @@ class Lattice(ABC): A tuple of lists of :class:`qiskit.QuantumRegister` s that are used to store the quantum information of the lattice. """ + discretization: LatticeDiscretization + """ + The discretization of the lattice, as an enum value of :class:`.LatticeDiscretization`. + """ + def __init__( self, lattice_data: str | Dict, # type: ignore @@ -199,7 +205,7 @@ def parse_input_data( def __parse_input_dict( self, input_dict: Dict, # type: ignore - ) -> Tuple[List[int], List[int], Dict[str, List[Shape]]]: + ) -> Tuple[List[int], List[int], Dict[str, List[Shape]], LatticeDiscretization]: r""" Parses the lattice input data, provided as a dictionary. @@ -210,7 +216,7 @@ def __parse_input_dict( Returns ------- - Tuple[List[int], List[int], Dict[str, List[Shape]]] + Tuple[List[int], List[int], Dict[str, List[Shape]], LatticeDiscretization] A tuple containing (i) a list of the number of gridpoints per dimension, (ii) a list of the number of velicities per dimension, From 882db5ed7fa3cf861fe1115ab2014669bc389971 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 4 Aug 2025 15:02:57 +0200 Subject: [PATCH 84/92] Improve LQLGA documentation and add examples --- qlbm/components/lqlga/initial.py | 20 ++++++++++++++++++++ qlbm/components/lqlga/lqlga.py | 21 +++++++++++++++++++++ qlbm/components/lqlga/measurement.py | 21 +++++++++++++++++++++ qlbm/components/lqlga/reflection.py | 23 +++++++++++++++++++++++ qlbm/components/lqlga/streaming.py | 21 +++++++++++++++++++++ 5 files changed, 106 insertions(+) diff --git a/qlbm/components/lqlga/initial.py b/qlbm/components/lqlga/initial.py index ae91b0a..e20c1da 100644 --- a/qlbm/components/lqlga/initial.py +++ b/qlbm/components/lqlga/initial.py @@ -17,6 +17,26 @@ class LQGLAInitialConditions(LBMPrimitive): This operator allows the construction of arbitrary deterministic initial conditions for the LQLGA algorithm. The number of gates required by this operator is equal to the number of enabled velocity qubits across all grid points. The depth of the circuit is 1, as all gates are applied in parallel at each grid point. + + Example usage: + + .. plot:: + :include-source: + + from qlbm.lattice import LQLGALattice + from qlbm.components.lqlga import LQGLAInitialConditions + + lattice = LQLGALattice( + { + "lattice": { + "dim": {"x": 4}, + "velocities": "D1Q3", + }, + "geometry": [], + }, + ) + initial_conditions = LQGLAInitialConditions(lattice, [(tuple([2]), (True, True, True))]) + initial_conditions.draw("mpl") """ grid_data: List[Tuple[Tuple[int, ...], Tuple[bool, ...]]] diff --git a/qlbm/components/lqlga/lqlga.py b/qlbm/components/lqlga/lqlga.py index 2cd2d3b..d4e6dc5 100644 --- a/qlbm/components/lqlga/lqlga.py +++ b/qlbm/components/lqlga/lqlga.py @@ -23,6 +23,27 @@ class LQLGA(LBMAlgorithm): deterministic run of the classical LGA algorithm. More information about this algorithm can be found in :cite:t:`lqlga1`, :cite:t:`lqlga2`, and :cite:t:`spacetime2`. + + Example usage: + + .. plot:: + :include-source: + + from qlbm.components.lqlga import LQLGA + from qlbm.lattice import LQLGALattice + + lattice = LQLGALattice( + { + "lattice": { + "dim": {"x": 7}, + "velocities": "D1Q3", + }, + "geometry": [{"shape": "cuboid", "x": [3, 5], "boundary": "bounceback"}], + }, + ) + + LQLGA(lattice=lattice).draw("mpl") + """ lattice: LQLGALattice diff --git a/qlbm/components/lqlga/measurement.py b/qlbm/components/lqlga/measurement.py index 5be72d9..f9f1568 100644 --- a/qlbm/components/lqlga/measurement.py +++ b/qlbm/components/lqlga/measurement.py @@ -10,10 +10,31 @@ class LQLGAGridVelocityMeasurement(LQLGAOperator): + # TODO: Improve documentation """ Measurement operator for the :class:`.LQLGA` algorithm. This operator measures the velocity qubits at each grid point in the LQLGA lattice. + + Example usage: + + .. plot:: + :include-source: + + from qlbm.components.lqlga import LQLGAGridVelocityMeasurement + from qlbm.lattice import LQLGALattice + + lattice = LQLGALattice( + { + "lattice": { + "dim": {"x": 5}, + "velocities": "D1Q3", + }, + "geometry": [], + }, + ) + + LQLGAGridVelocityMeasurement(lattice=lattice).draw("mpl") """ def __init__( diff --git a/qlbm/components/lqlga/reflection.py b/qlbm/components/lqlga/reflection.py index 2ad7882..d990d36 100644 --- a/qlbm/components/lqlga/reflection.py +++ b/qlbm/components/lqlga/reflection.py @@ -29,6 +29,29 @@ class LQLGAReflectionOperator(LQLGAOperator): :attr:`shapes` A list of boundary-conditioned shapes. :attr:`logger` The performance logger, by default ``getLogger("qlbm")``. ============================ ====================================================================== + + Example usage: + + .. plot:: + :include-source: + + from qlbm.components.lqlga import LQLGAReflectionOperator + from qlbm.lattice import LQLGALattice + + lattice = LQLGALattice( + { + "lattice": { + "dim": {"x": 7}, + "velocities": "D1Q3", + }, + "geometry": [{"shape": "cuboid", "x": [3, 5], "boundary": "bounceback"}], + }, + ) + reflection_operator = LQLGAReflectionOperator( + lattice, shapes=lattice.shapes["bounceback"] + ) + reflection_operator.draw("mpl") + """ shapes: List[LQLGAShape] diff --git a/qlbm/components/lqlga/streaming.py b/qlbm/components/lqlga/streaming.py index 488e9f0..373b7a8 100644 --- a/qlbm/components/lqlga/streaming.py +++ b/qlbm/components/lqlga/streaming.py @@ -11,12 +11,33 @@ class LQLGAStreamingOperator(LQLGAOperator): + # TODO: Improve documentation """ Streaming operator for the :class:`.LQLGA` algorithm. Streaming is implemented by a series of swap gates as described in :cite:`spacetime`. The number of gates scales linearly with size of the grid, while the depth scales logarithmically. + + Example usage: + + .. plot:: + :include-source: + + from qlbm.components.lqlga import LQLGAStreamingOperator + from qlbm.lattice import LQLGALattice + + lattice = LQLGALattice( + { + "lattice": { + "dim": {"x": 4}, + "velocities": "D1Q3", + }, + "geometry": [], + }, + ) + streaming_operator = LQLGAStreamingOperator(lattice) + streaming_operator.draw("mpl") """ def __init__( From d47bbd3c8063205b9e9fb8a037fbd376f70f6188 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 4 Aug 2025 15:03:37 +0200 Subject: [PATCH 85/92] Add BSE collision operator exmaple --- .../common/cbse_collision/cbse_collision.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/qlbm/components/common/cbse_collision/cbse_collision.py b/qlbm/components/common/cbse_collision/cbse_collision.py index 947f94e..0235660 100644 --- a/qlbm/components/common/cbse_collision/cbse_collision.py +++ b/qlbm/components/common/cbse_collision/cbse_collision.py @@ -36,6 +36,32 @@ class EQCCollisionOperator(LBMPrimitive): :attr:`discretization` The discretization for which this collision operator is defined. :attr:`num_velocities` The number of velocities in the discretization. ========================= ====================================================================== + + Simple D2Q4 example usage: + + .. plot:: + :include-source: + + from qlbm.components.common import EQCCollisionOperator + from qlbm.lattice import LatticeDiscretization + + # Select a discretization and draw its circuit + EQCCollisionOperator( + LatticeDiscretization.D2Q4 + ).draw("mpl") + + More complex D3Q6 example usage: + + .. plot:: + :include-source: + + from qlbm.components.common import EQCCollisionOperator + from qlbm.lattice import LatticeDiscretization + + # Select a discretization and draw its circuit + EQCCollisionOperator( + LatticeDiscretization.D3Q6 + ).draw("mpl") """ discretization: LatticeDiscretization From 0baae3840a71fd0b4e51d39aad2e28a39badc7a0 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 4 Aug 2025 15:04:08 +0200 Subject: [PATCH 86/92] Update STQBM documentation examples to use the DdQq velocity notation --- qlbm/components/spacetime/collision/d2q4_old.py | 2 +- qlbm/components/spacetime/initial/pointwise.py | 2 +- qlbm/components/spacetime/measurement.py | 2 +- qlbm/components/spacetime/spacetime.py | 2 +- qlbm/components/spacetime/streaming.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/qlbm/components/spacetime/collision/d2q4_old.py b/qlbm/components/spacetime/collision/d2q4_old.py index 81f3a9f..b70b6a2 100644 --- a/qlbm/components/spacetime/collision/d2q4_old.py +++ b/qlbm/components/spacetime/collision/d2q4_old.py @@ -53,7 +53,7 @@ class SpaceTimeD2Q4CollisionOperator(SpaceTimeOperator): lattice = SpaceTimeLattice( num_timesteps=1, lattice_data={ - "lattice": {"dim": {"x": 4, "y": 8}, "velocities": {"x": 2, "y": 2}}, + "lattice": {"dim": {"x": 4, "y": 8}, "velocities": "D2Q4"}, "geometry": [], }, ) diff --git a/qlbm/components/spacetime/initial/pointwise.py b/qlbm/components/spacetime/initial/pointwise.py index 63d604a..e23451a 100644 --- a/qlbm/components/spacetime/initial/pointwise.py +++ b/qlbm/components/spacetime/initial/pointwise.py @@ -55,7 +55,7 @@ class PointWiseSpaceTimeInitialConditions(LBMPrimitive): lattice = SpaceTimeLattice( num_timesteps=1, lattice_data={ - "lattice": {"dim": {"x": 4, "y": 8}, "velocities": {"x": 2, "y": 2}}, + "lattice": {"dim": {"x": 4, "y": 8}, "velocities": "D2Q4"}, "geometry": [], }, ) diff --git a/qlbm/components/spacetime/measurement.py b/qlbm/components/spacetime/measurement.py index 8e34bca..1095179 100644 --- a/qlbm/components/spacetime/measurement.py +++ b/qlbm/components/spacetime/measurement.py @@ -39,7 +39,7 @@ class SpaceTimeGridVelocityMeasurement(SpaceTimeOperator): lattice = SpaceTimeLattice( num_timesteps=1, lattice_data={ - "lattice": {"dim": {"x": 4, "y": 8}, "velocities": {"x": 2, "y": 2}}, + "lattice": {"dim": {"x": 4, "y": 8}, "velocities": "D2Q4"}, "geometry": [], }, ) diff --git a/qlbm/components/spacetime/spacetime.py b/qlbm/components/spacetime/spacetime.py index bf4a6fd..88771fd 100644 --- a/qlbm/components/spacetime/spacetime.py +++ b/qlbm/components/spacetime/spacetime.py @@ -45,7 +45,7 @@ class SpaceTimeQLBM(LBMAlgorithm): lattice = SpaceTimeLattice( num_timesteps=1, lattice_data={ - "lattice": {"dim": {"x": 4, "y": 8}, "velocities": {"x": 2, "y": 2}}, + "lattice": {"dim": {"x": 4, "y": 8}, "velocities": "D2Q4"}, "geometry": [], }, ) diff --git a/qlbm/components/spacetime/streaming.py b/qlbm/components/spacetime/streaming.py index cd7e32f..88fc302 100644 --- a/qlbm/components/spacetime/streaming.py +++ b/qlbm/components/spacetime/streaming.py @@ -44,7 +44,7 @@ class SpaceTimeStreamingOperator(SpaceTimeOperator): lattice = SpaceTimeLattice( num_timesteps=1, lattice_data={ - "lattice": {"dim": {"x": 4, "y": 8}, "velocities": {"x": 2, "y": 2}}, + "lattice": {"dim": {"x": 4, "y": 8}, "velocities": "D2Q4"}, "geometry": [], }, ) From 500e1f3c09271637b0045336b5025fe88d3e7e32 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 4 Aug 2025 15:11:14 +0200 Subject: [PATCH 87/92] Update references and include new algorithm in README --- README.md | 5 +++-- qlbm/infra/compiler.py | 2 +- qlbm/lattice/lattices/spacetime_lattice.py | 7 ++----- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 93eb0ac..f6b98fd 100644 --- a/README.md +++ b/README.md @@ -62,8 +62,9 @@ source qlbm-cpu-venv/bin/activate Currently, `qlbm` supports two algorithms: - - The 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 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. + - 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 `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/qlbm/infra/compiler.py b/qlbm/infra/compiler.py index e1de1b0..313610a 100644 --- a/qlbm/infra/compiler.py +++ b/qlbm/infra/compiler.py @@ -50,7 +50,7 @@ class CircuitCompiler: lattice = SpaceTimeLattice( num_timesteps=1, lattice_data={ - "lattice": {"dim": {"x": 4, "y": 8}, "velocities": {"x": 2, "y": 2}}, + "lattice": {"dim": {"x": 4, "y": 8}, "velocities": "D2Q4"}, "geometry": [], }, ) diff --git a/qlbm/lattice/lattices/spacetime_lattice.py b/qlbm/lattice/lattices/spacetime_lattice.py index d7c3e6e..e7099e2 100644 --- a/qlbm/lattice/lattices/spacetime_lattice.py +++ b/qlbm/lattice/lattices/spacetime_lattice.py @@ -88,10 +88,7 @@ class SpaceTimeLattice(Lattice): "x": 16, "y": 16 }, - "velocities": { - "x": 2, - "y": 2 - } + "velocities": "D2Q4" }, "geometry": [] } @@ -106,7 +103,7 @@ class SpaceTimeLattice(Lattice): SpaceTimeLattice( num_timesteps=1, lattice_data={ - "lattice": {"dim": {"x": 4, "y": 8}, "velocities": {"x": 2, "y": 2}}, + "lattice": {"dim": {"x": 4, "y": 8}, "velocities": "D2Q4"}, "geometry": [], } ).circuit.draw("mpl") From 06252e51b52d85aa14f605892e5288116f51186e Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 4 Aug 2025 15:50:14 +0200 Subject: [PATCH 88/92] Update STQBM demos with new DdQq velocity notation --- demos/simulation/spacetime_simulation.ipynb | 4 ++-- demos/visualization/spacetime_components.ipynb | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/demos/simulation/spacetime_simulation.ipynb b/demos/simulation/spacetime_simulation.ipynb index a5455f9..fdf50f7 100644 --- a/demos/simulation/spacetime_simulation.ipynb +++ b/demos/simulation/spacetime_simulation.ipynb @@ -38,7 +38,7 @@ "lattice = SpaceTimeLattice(\n", " num_timesteps=1,\n", " lattice_data={\n", - " \"lattice\": {\"dim\": {\"x\": 4, \"y\": 8}, \"velocities\": {\"x\": 2, \"y\": 2}},\n", + " \"lattice\": {\"dim\": {\"x\": 4, \"y\": 8}, \"velocities\": \"D2Q4\"},\n", " \"geometry\": [],\n", " },\n", ")\n", @@ -144,7 +144,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.12" + "version": "3.12.10" } }, "nbformat": 4, diff --git a/demos/visualization/spacetime_components.ipynb b/demos/visualization/spacetime_components.ipynb index 68f64ec..79fa0a4 100644 --- a/demos/visualization/spacetime_components.ipynb +++ b/demos/visualization/spacetime_components.ipynb @@ -17,7 +17,7 @@ "source": [ "from qlbm.components.spacetime import (\n", " PointWiseSpaceTimeInitialConditions,\n", - " SpaceTimeCollisionOperator,\n", + " SpaceTimeD2Q4CollisionOperator,\n", " SpaceTimeQLBM,\n", " SpaceTimeStreamingOperator,\n", ")\n", @@ -35,7 +35,7 @@ "example_lattice = SpaceTimeLattice(\n", " 1,\n", " {\n", - " \"lattice\": {\"dim\": {\"x\": 16, \"y\": 16}, \"velocities\": {\"x\": 2, \"y\": 2}},\n", + " \"lattice\": {\"dim\": {\"x\": 16, \"y\": 16}, \"velocities\": \"d2q4\"},\n", " \"geometry\": [],\n", " },\n", ")" @@ -68,7 +68,7 @@ "metadata": {}, "outputs": [], "source": [ - "SpaceTimeCollisionOperator(example_lattice, 1).draw(\"mpl\")" + "SpaceTimeD2Q4CollisionOperator(example_lattice, 1).draw(\"mpl\")" ] }, { @@ -92,7 +92,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "qiskit2-venv", "language": "python", "name": "python3" }, @@ -106,7 +106,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.5" + "version": "3.13.0" } }, "nbformat": 4, From d8c1795b399ebf2067835adb602b1fc84add77d2 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 4 Aug 2025 15:50:49 +0200 Subject: [PATCH 89/92] Add LQLGA simulation and visualization demos --- demos/simulation/lqlga_simulation.ipynb | 268 +++++++++++++++++++++ demos/visualization/lqlga_components.ipynb | 105 ++++++++ 2 files changed, 373 insertions(+) create mode 100644 demos/simulation/lqlga_simulation.ipynb create mode 100644 demos/visualization/lqlga_components.ipynb diff --git a/demos/simulation/lqlga_simulation.ipynb b/demos/simulation/lqlga_simulation.ipynb new file mode 100644 index 0000000..d92178b --- /dev/null +++ b/demos/simulation/lqlga_simulation.ipynb @@ -0,0 +1,268 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "70305ef3", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit_aer import AerSimulator\n", + "\n", + "from qlbm.components import EmptyPrimitive\n", + "from qlbm.components.lqlga.initial import LQGLAInitialConditions\n", + "from qlbm.components.lqlga.lqlga import LQLGA\n", + "from qlbm.components.lqlga.measurement import LQLGAGridVelocityMeasurement\n", + "from qlbm.infra import QiskitRunner, SimulationConfig\n", + "from qlbm.lattice.lattices.lqlga_lattice import LQLGALattice\n", + "from qlbm.tools.utils import create_directory_and_parents\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ec2f4062", + "metadata": {}, + "outputs": [], + "source": [ + "from qlbm.components.lqlga import LQGLAInitialConditions\n", + "from qlbm.lattice import LQLGALattice\n", + "\n", + "lattice = LQLGALattice(\n", + " {\n", + " \"lattice\": {\n", + " \"dim\": {\"x\": 7},\n", + " \"velocities\": \"D1Q3\",\n", + " },\n", + " \"geometry\": [{\"shape\": \"cuboid\", \"x\": [3, 5], \"boundary\": \"bounceback\"}],\n", + " },\n", + ")\n", + "initial_conditions = LQGLAInitialConditions(lattice, [(tuple([2]), (True, True, True))])\n", + "initial_conditions.draw(\"mpl\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "05e422b4", + "metadata": {}, + "outputs": [], + "source": [ + "from qlbm.components.lqlga import LQLGAStreamingOperator\n", + "from qlbm.lattice import LQLGALattice\n", + "\n", + "lattice = LQLGALattice(\n", + " {\n", + " \"lattice\": {\n", + " \"dim\": {\"x\": 4},\n", + " \"velocities\": \"D1Q3\",\n", + " },\n", + " \"geometry\": [],\n", + " # \"geometry\": [{\"shape\": \"cuboid\", \"x\": [3, 5], \"boundary\": \"bounceback\"}],\n", + " },\n", + ")\n", + "streaming_operator = LQLGAStreamingOperator(lattice)\n", + "streaming_operator.draw(\"mpl\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "020bb111", + "metadata": {}, + "outputs": [], + "source": [ + "from qlbm.components.lqlga import LQLGAReflectionOperator\n", + "from qlbm.lattice import LQLGALattice\n", + "\n", + "lattice = LQLGALattice(\n", + " {\n", + " \"lattice\": {\n", + " \"dim\": {\"x\": 7},\n", + " \"velocities\": \"D1Q3\",\n", + " },\n", + " \"geometry\": [{\"shape\": \"cuboid\", \"x\": [3, 5], \"boundary\": \"bounceback\"}],\n", + " },\n", + ")\n", + "reflection_operator = LQLGAReflectionOperator(\n", + " lattice, shapes=lattice.shapes[\"bounceback\"]\n", + ")\n", + "reflection_operator.draw(\"mpl\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "db7e5c99", + "metadata": {}, + "outputs": [], + "source": [ + "from qlbm.components.lqlga import LQLGA\n", + "from qlbm.lattice import LQLGALattice\n", + "\n", + "lattice = LQLGALattice(\n", + " {\n", + " \"lattice\": {\n", + " \"dim\": {\"x\": 7},\n", + " \"velocities\": \"D1Q3\",\n", + " },\n", + " \"geometry\": [{\"shape\": \"cuboid\", \"x\": [3, 5], \"boundary\": \"bounceback\"}],\n", + " },\n", + ")\n", + "\n", + "LQLGA(lattice=lattice).draw(\"mpl\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bb1f01f6", + "metadata": {}, + "outputs": [], + "source": [ + "from qlbm.components.lqlga import LQLGAGridVelocityMeasurement\n", + "from qlbm.lattice import LQLGALattice\n", + "\n", + "lattice = LQLGALattice(\n", + " {\n", + " \"lattice\": {\n", + " \"dim\": {\"x\": 5},\n", + " \"velocities\": \"D1Q3\",\n", + " },\n", + " \"geometry\": [],\n", + " },\n", + ")\n", + "\n", + "LQLGAGridVelocityMeasurement(lattice=lattice).draw(\"mpl\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "34715b68", + "metadata": {}, + "outputs": [], + "source": [ + "lattice = LQLGALattice(\n", + " {\n", + " \"lattice\": {\n", + " \"dim\": {\"x\": 4},\n", + " \"velocities\": \"D1Q3\",\n", + " },\n", + " \"geometry\": [],\n", + " # \"geometry\": [{\"shape\": \"cuboid\", \"x\": [3, 5], \"boundary\": \"bounceback\"}],\n", + " },\n", + ")\n", + "\n", + "output_dir = f\"qlbm-output/lqlga-{lattice.logger_name()}-qiskit\"\n", + "create_directory_and_parents(output_dir)\n", + "\n", + "lattice.circuit.draw(\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b084dda3", + "metadata": {}, + "outputs": [], + "source": [ + "LQLGA(lattice).circuit.draw(\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "86ec87a5", + "metadata": {}, + "outputs": [], + "source": [ + "cfg = SimulationConfig(\n", + " initial_conditions=LQGLAInitialConditions(\n", + " lattice, [(tuple([2]), (True, True, True))]\n", + " ),\n", + " algorithm=LQLGA(lattice),\n", + " postprocessing=EmptyPrimitive(lattice),\n", + " measurement=LQLGAGridVelocityMeasurement(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": "4d54ef63", + "metadata": {}, + "outputs": [], + "source": [ + "cfg.prepare_for_simulation()\n", + "\n", + "# Number of shots to simulate for each timestep when running the circuit\n", + "# NUM_SHOTS = 2**10\n", + "\n", + "NUM_SHOTS = 2**10\n", + "\n", + "# Number of timesteps to simulate\n", + "NUM_STEPS = 100\n", + "\n", + "# Create a runner object to simulate the circuit\n", + "runner = QiskitRunner(\n", + " cfg,\n", + " lattice,\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": "76799b9e", + "metadata": {}, + "outputs": [], + "source": [ + "cfg.initial_conditions.draw(\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "244ed487", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.12.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/demos/visualization/lqlga_components.ipynb b/demos/visualization/lqlga_components.ipynb new file mode 100644 index 0000000..4786403 --- /dev/null +++ b/demos/visualization/lqlga_components.ipynb @@ -0,0 +1,105 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "ddea9d2e", + "metadata": {}, + "outputs": [], + "source": [ + "from qlbm.components.lqlga.collision import GenericLQLGACollisionOperator\n", + "from qlbm.components.lqlga.initial import LQGLAInitialConditions\n", + "from qlbm.components.lqlga.lqlga import LQLGA\n", + "from qlbm.components.lqlga.streaming import LQLGAStreamingOperator\n", + "from qlbm.lattice.lattices.lqlga_lattice import LQLGALattice" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "311e7286", + "metadata": {}, + "outputs": [], + "source": [ + "lattice = LQLGALattice(\n", + " {\n", + " \"lattice\": {\n", + " \"dim\": {\"x\": 8},\n", + " \"velocities\": \"d1q3\",\n", + " },\n", + " \"geometry\": [{\"shape\": \"cuboid\", \"x\": [3, 5], \"boundary\": \"bounceback\"}],\n", + " },\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0f787330", + "metadata": {}, + "outputs": [], + "source": [ + "LQGLAInitialConditions(lattice, [(tuple([2]), (False, False))]).draw(\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a8967c83", + "metadata": {}, + "outputs": [], + "source": [ + "LQLGAStreamingOperator(lattice).draw(\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c32cccb3", + "metadata": {}, + "outputs": [], + "source": [ + "GenericLQLGACollisionOperator(lattice).draw(\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fcc13530", + "metadata": {}, + "outputs": [], + "source": [ + "LQLGA(lattice).circuit.draw(\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d31eb2aa", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.12.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From b706459d36feaf08c3b95cc4a06f2878acd0b246 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 4 Aug 2025 15:51:20 +0200 Subject: [PATCH 90/92] Ignore capitialization in DdQq specification --- qlbm/lattice/lattices/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qlbm/lattice/lattices/base.py b/qlbm/lattice/lattices/base.py index d717237..6ae96bf 100644 --- a/qlbm/lattice/lattices/base.py +++ b/qlbm/lattice/lattices/base.py @@ -262,7 +262,7 @@ def __parse_input_dict( # Check if velocities is a string (DdQq format) or dict if isinstance(lattice_dict["velocities"], str): # type: ignore # Parse DdQq format (e.g., "D2Q4" means 2 dimensions, 4 velocities total) - velocity_spec = lattice_dict["velocities"] # type: ignore + velocity_spec = lattice_dict["velocities"].upper() # type: ignore if not velocity_spec.startswith("D") or "Q" not in velocity_spec: raise LatticeException( f"Invalid velocity specification format: {lattice_dict['velocities']}. Expected format like 'd2q4'." From 97e520bad8026f608749907e67db80456584462b Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 4 Aug 2025 15:52:02 +0200 Subject: [PATCH 91/92] Add EQCCollision operator documentation to website --- docs/source/code/comps_collision.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/source/code/comps_collision.rst b/docs/source/code/comps_collision.rst index b862f85..1eb7a13 100644 --- a/docs/source/code/comps_collision.rst +++ b/docs/source/code/comps_collision.rst @@ -61,3 +61,5 @@ Collision Components .. autoclass:: qlbm.components.common.EQCRedistribution +.. autoclass:: qlbm.components.common.EQCCollisionOperator + From 893e2072db488b16d5d7dbd0ed21dcebb3c29023 Mon Sep 17 00:00:00 2001 From: Calin Georgescu Date: Mon, 4 Aug 2025 16:06:21 +0200 Subject: [PATCH 92/92] Update nblinks --- docs/source/index.rst | 10 +++++----- examples/index.rst | 20 ++++++++++++++++++++ examples/notebooks/collisionless_vis.nblink | 3 +++ examples/notebooks/flowfield_vis.nblink | 3 +++ examples/notebooks/geometry_vis.nblink | 3 +++ examples/notebooks/lqlga_vis.nblink | 3 +++ examples/notebooks/spacetime_vis.nblink | 3 +++ 7 files changed, 40 insertions(+), 5 deletions(-) create mode 100644 examples/index.rst create mode 100644 examples/notebooks/collisionless_vis.nblink create mode 100644 examples/notebooks/flowfield_vis.nblink create mode 100644 examples/notebooks/geometry_vis.nblink create mode 100644 examples/notebooks/lqlga_vis.nblink create mode 100644 examples/notebooks/spacetime_vis.nblink diff --git a/docs/source/index.rst b/docs/source/index.rst index 0d07a9f..5d0dfa1 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -31,11 +31,11 @@ The :ref:`tools` module contains miscellaneous utilities. :fas:`book;sd-text-primary` Detailed documentation of ``qlbm``. -.. .. card:: Tutorials -.. :link: tutorials -.. :link-type: ref +.. card:: Tutorials + :link: tutorials + :link-type: ref -.. :fas:`flask;sd-text-primary` Hands-on examples. + :fas:`flask;sd-text-primary` Hands-on examples. .. toctree:: @@ -43,7 +43,7 @@ The :ref:`tools` module contains miscellaneous utilities. :maxdepth: 2 code/index -.. examples/index + examples/index References ----------------------------------- diff --git a/examples/index.rst b/examples/index.rst new file mode 100644 index 0000000..4a34a27 --- /dev/null +++ b/examples/index.rst @@ -0,0 +1,20 @@ +.. _tutorials: + +Tutorials +================================ + +We are working on integrating more current tutorials into the web documentation. +More Jupyter notebooks that demonstrate the features of ``qlbm`` are available +`in the demos directory of the GitHub repository `_. +Check the ``README`` of the directory for an explanation of the different available demos. + +Currently, the following visualization tutorials are available online: + +.. toctree:: + :caption: Tutorials + :maxdepth: 2 + + notebooks/collisionless_vis + notebooks/spacetime_vis + notebooks/geometry_vis + notebooks/flowfield_vis diff --git a/examples/notebooks/collisionless_vis.nblink b/examples/notebooks/collisionless_vis.nblink new file mode 100644 index 0000000..35cee09 --- /dev/null +++ b/examples/notebooks/collisionless_vis.nblink @@ -0,0 +1,3 @@ +{ + "path": "../../../../demos/visualization/collisionless_components.ipynb" +} \ No newline at end of file diff --git a/examples/notebooks/flowfield_vis.nblink b/examples/notebooks/flowfield_vis.nblink new file mode 100644 index 0000000..6e10e1e --- /dev/null +++ b/examples/notebooks/flowfield_vis.nblink @@ -0,0 +1,3 @@ +{ + "path": "../../../../demos/visualization/flowfields.ipynb" +} \ No newline at end of file diff --git a/examples/notebooks/geometry_vis.nblink b/examples/notebooks/geometry_vis.nblink new file mode 100644 index 0000000..0da5790 --- /dev/null +++ b/examples/notebooks/geometry_vis.nblink @@ -0,0 +1,3 @@ +{ + "path": "../../../../demos/visualization/geometry.ipynb" +} \ No newline at end of file diff --git a/examples/notebooks/lqlga_vis.nblink b/examples/notebooks/lqlga_vis.nblink new file mode 100644 index 0000000..a8ca849 --- /dev/null +++ b/examples/notebooks/lqlga_vis.nblink @@ -0,0 +1,3 @@ +{ + "path": "../../../../demos/visualization/lqlga_components.ipynb" +} \ No newline at end of file diff --git a/examples/notebooks/spacetime_vis.nblink b/examples/notebooks/spacetime_vis.nblink new file mode 100644 index 0000000..68905b1 --- /dev/null +++ b/examples/notebooks/spacetime_vis.nblink @@ -0,0 +1,3 @@ +{ + "path": "../../../../demos/visualization/spacetime_components.ipynb" +} \ No newline at end of file