diff --git a/demos/simulation/ab_simulation.ipynb b/demos/simulation/ab_simulation.ipynb new file mode 100644 index 0000000..e8f839b --- /dev/null +++ b/demos/simulation/ab_simulation.ipynb @@ -0,0 +1,141 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "348de144", + "metadata": {}, + "source": [ + "# Simulating the Amplitude-Based Collisionless QLBM" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "181af34e", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit_aer import AerSimulator\n", + "\n", + "from qlbm.components import (\n", + " CQLBM,\n", + " ABGridMeasurement,\n", + " ABInitialConditions,\n", + " EmptyPrimitive,\n", + ")\n", + "from qlbm.infra import QiskitRunner, SimulationConfig\n", + "from qlbm.lattice import ABLattice\n", + "from qlbm.tools.utils import create_directory_and_parents\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "05e4915d", + "metadata": {}, + "outputs": [], + "source": [ + "lattice = ABLattice(\n", + " {\n", + " \"lattice\": {\"dim\": {\"x\": 64, \"y\": 32}, \"velocities\": \"d2q9\"},\n", + " \"geometry\": [\n", + " {\"shape\": \"cuboid\", \"x\": [10, 13], \"y\": [14, 17], \"boundary\": \"bounceback\"},\n", + " ],\n", + " }\n", + ")\n", + "\n", + "\n", + "output_dir = \"qlbm-output/ab-d2q9-64x32-1-obstacle-qiskit\"\n", + "create_directory_and_parents(output_dir)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "deb472a0", + "metadata": {}, + "outputs": [], + "source": [ + "cfg = SimulationConfig(\n", + " initial_conditions=ABInitialConditions(lattice),\n", + " algorithm=CQLBM(lattice),\n", + " postprocessing=EmptyPrimitive(lattice),\n", + " measurement=ABGridMeasurement(lattice),\n", + " target_platform=\"QISKIT\",\n", + " compiler_platform=\"QISKIT\",\n", + " optimization_level=0,\n", + " statevector_sampling=True,\n", + " execution_backend=AerSimulator(method=\"statevector\"),\n", + " sampling_backend=AerSimulator(method=\"statevector\"),\n", + ")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "57240678", + "metadata": {}, + "outputs": [], + "source": [ + "cfg.prepare_for_simulation()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1da18eb8", + "metadata": {}, + "outputs": [], + "source": [ + "# Number of shots to simulate for each timestep when running the circuit\n", + "NUM_SHOTS = 2**12\n", + "\n", + "# Number of timesteps to simulate\n", + "NUM_STEPS = 20" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5c8c7d1e", + "metadata": {}, + "outputs": [], + "source": [ + "runner = QiskitRunner(\n", + " cfg,\n", + " lattice,\n", + ")\n", + "\n", + "\n", + "# Simulate the circuits using both snapshots\n", + "runner.run(\n", + " NUM_STEPS, # Number of time steps\n", + " NUM_SHOTS, # Number of shots per time step\n", + " output_dir,\n", + " statevector_snapshots=True,\n", + ")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "qlbm-cpu-venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/demos/simulation/lqlga_simulation.ipynb b/demos/simulation/lqlga_simulation.ipynb index 2ac16f8..cd9cb26 100644 --- a/demos/simulation/lqlga_simulation.ipynb +++ b/demos/simulation/lqlga_simulation.ipynb @@ -1,5 +1,13 @@ { "cells": [ + { + "cell_type": "markdown", + "id": "4c316e35", + "metadata": {}, + "source": [ + "# Simulating the Linear Encoding Quantum Lattice Gas Automata" + ] + }, { "cell_type": "code", "execution_count": null, @@ -10,12 +18,14 @@ "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.components.lqlga import (\n", + " LQLGA,\n", + " LQGLAInitialConditions,\n", + " LQLGAGridVelocityMeasurement,\n", + ")\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" + "from qlbm.lattice import LQLGALattice\n", + "from qlbm.tools.utils import create_directory_and_parents" ] }, { @@ -25,9 +35,6 @@ "metadata": {}, "outputs": [], "source": [ - "from qlbm.components.lqlga import LQGLAInitialConditions\n", - "from qlbm.lattice import LQLGALattice\n", - "\n", "lattice = LQLGALattice(\n", " {\n", " \"lattice\": {\n", @@ -69,18 +76,34 @@ { "cell_type": "code", "execution_count": null, - "id": "4d54ef63", + "id": "65ec52e8", + "metadata": {}, + "outputs": [], + "source": [ + "cfg.prepare_for_simulation()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11c5dbb7", "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", "# Number of timesteps to simulate\n", - "NUM_STEPS = 20\n", - "\n", + "NUM_STEPS = 20" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4d54ef63", + "metadata": {}, + "outputs": [], + "source": [ "# Create a runner object to simulate the circuit\n", "runner = QiskitRunner(\n", " cfg,\n", @@ -121,7 +144,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.13.7" + "version": "3.13.9" } }, "nbformat": 4, diff --git a/demos/simulation/collisionless_simulation.ipynb b/demos/simulation/ms_simulation.ipynb similarity index 68% rename from demos/simulation/collisionless_simulation.ipynb rename to demos/simulation/ms_simulation.ipynb index 64490ad..9dc16b3 100644 --- a/demos/simulation/collisionless_simulation.ipynb +++ b/demos/simulation/ms_simulation.ipynb @@ -1,9 +1,17 @@ { "cells": [ + { + "cell_type": "markdown", + "id": "328661be", + "metadata": {}, + "source": [ + "# Simulation the Multi-Speed Collisionless QLBM" + ] + }, { "cell_type": "code", "execution_count": null, - "id": "9b1814b3", + "id": "be6f3d11", "metadata": {}, "outputs": [], "source": [ @@ -11,61 +19,51 @@ "\n", "from qlbm.components import (\n", " CQLBM,\n", - " CollisionlessInitialConditions,\n", " EmptyPrimitive,\n", " GridMeasurement,\n", + " MSInitialConditions,\n", ")\n", "from qlbm.infra import QiskitRunner, SimulationConfig\n", - "from qlbm.lattice import CollisionlessLattice\n", - "from qlbm.tools.utils import create_directory_and_parents" + "from qlbm.lattice import MSLattice\n", + "from qlbm.tools.utils import create_directory_and_parents\n" ] }, { "cell_type": "code", "execution_count": null, - "id": "e8e77d2f-a778-441c-b436-a61e1f3154cf", + "id": "e8d2b671", "metadata": {}, "outputs": [], "source": [ - "# Load example with mixed boundary conditions and create output directory\n", - "lattice = CollisionlessLattice(\n", + "lattice = MSLattice(\n", " {\n", - " \"lattice\": {\"dim\": {\"x\": 16, \"y\": 16}, \"velocities\": {\"x\": 4, \"y\": 4}},\n", + " \"lattice\": {\n", + " \"dim\": {\"x\": 64, \"y\": 32},\n", + " \"velocities\": {\n", + " \"x\": 4,\n", + " \"y\": 4,\n", + " },\n", + " },\n", " \"geometry\": [\n", - " {\"shape\": \"cuboid\", \"x\": [9, 12], \"y\": [3, 6], \"boundary\": \"specular\"},\n", - " {\"shape\": \"cuboid\", \"x\": [9, 12], \"y\": [9, 12], \"boundary\": \"bounceback\"},\n", + " {\"shape\": \"cuboid\", \"x\": [10, 13], \"y\": [14, 17], \"boundary\": \"bounceback\"},\n", " ],\n", " }\n", ")\n", "\n", "\n", - "output_dir = \"qlbm-output/collisionless-2d-16x16-2-obstacle-mixed-qiskit\"\n", + "output_dir = \"qlbm-output/ms-d2q9-64x32-1-obstacle-qiskit\"\n", "create_directory_and_parents(output_dir)" ] }, { "cell_type": "code", "execution_count": null, - "id": "b4df878f", - "metadata": {}, - "outputs": [], - "source": [ - "# Number of shots to simulate for each timestep when running the circuit\n", - "NUM_SHOTS = 2**12\n", - "# Number of timesteps to simulate\n", - "NUM_STEPS = 20" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "26ce07ff-55f9-4828-a9b3-4e9c6dc6a856", + "id": "da32a65c", "metadata": {}, "outputs": [], "source": [ - "# In the simulation configuration function the user can determine the specifications of the run\n", "cfg = SimulationConfig(\n", - " initial_conditions=CollisionlessInitialConditions(lattice),\n", + " initial_conditions=MSInitialConditions(lattice),\n", " algorithm=CQLBM(lattice),\n", " postprocessing=EmptyPrimitive(lattice),\n", " measurement=GridMeasurement(lattice),\n", @@ -75,13 +73,13 @@ " statevector_sampling=True,\n", " execution_backend=AerSimulator(method=\"statevector\"),\n", " sampling_backend=AerSimulator(method=\"statevector\"),\n", - ")" + ")\n" ] }, { "cell_type": "code", "execution_count": null, - "id": "b733d245-c344-46db-88bb-864e4cf07474", + "id": "92d4f9ab", "metadata": {}, "outputs": [], "source": [ @@ -91,16 +89,30 @@ { "cell_type": "code", "execution_count": null, - "id": "f1c02c70-8244-4a86-8357-9f8c80d2d632", + "id": "f2ba1f1f", + "metadata": {}, + "outputs": [], + "source": [ + "# Number of shots to simulate for each timestep when running the circuit\n", + "NUM_SHOTS = 2**12\n", + "\n", + "# Number of timesteps to simulate\n", + "NUM_STEPS = 20" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7f26c6d9", "metadata": {}, "outputs": [], "source": [ - "# Create a runner object to simulate the circuit\n", "runner = QiskitRunner(\n", " cfg,\n", " lattice,\n", ")\n", "\n", + "\n", "# Simulate the circuits using both snapshots\n", "runner.run(\n", " NUM_STEPS, # Number of time steps\n", @@ -113,7 +125,7 @@ { "cell_type": "code", "execution_count": null, - "id": "a4c15cbe-3766-4ea1-b3cb-f7caec41328c", + "id": "a1737979", "metadata": {}, "outputs": [], "source": [] @@ -135,7 +147,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.13.7" + "version": "3.13.9" } }, "nbformat": 4, diff --git a/demos/simulation/spacetime_simulation.ipynb b/demos/simulation/spacetime_simulation.ipynb index 2668488..e135c7c 100644 --- a/demos/simulation/spacetime_simulation.ipynb +++ b/demos/simulation/spacetime_simulation.ipynb @@ -1,5 +1,12 @@ { "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Simulating the Linear Space-Time QLBM" + ] + }, { "cell_type": "code", "execution_count": null, @@ -14,7 +21,7 @@ " SpaceTimeGridVelocityMeasurement,\n", " SpaceTimeQLBM,\n", ")\n", - "from qlbm.infra import QiskitRunner, SimulationConfig\n", + "from qlbm.infra import QiskitRunner, SimulationConfig, SpaceTimeResult\n", "from qlbm.lattice import SpaceTimeLattice\n", "from qlbm.tools.utils import create_directory_and_parents" ] @@ -29,12 +36,20 @@ "lattice = SpaceTimeLattice(\n", " num_timesteps=1,\n", " lattice_data={\n", - " \"lattice\": {\"dim\": {\"x\": 4, \"y\": 8}, \"velocities\": \"D2Q4\"},\n", - " \"geometry\": [],\n", + " \"lattice\": {\"dim\": {\"x\": 64, \"y\": 64}, \"velocities\": \"D2Q4\"},\n", + " \"geometry\": [\n", + " {\n", + " \"shape\": \"sphere\",\n", + " \"center\": [30, 30],\n", + " \"radius\": 15,\n", + " \"boundary\": \"bounceback\",\n", + " }\n", + " ],\n", " },\n", + " use_volumetric_ops=False,\n", ")\n", "\n", - "output_dir = f\"qlbm-output/spacetime-{lattice.logger_name()}-qiskit\"\n", + "output_dir = \"qlbm-output/spacetime-d2q4-64x64-1-sphere-qiskit\"\n", "create_directory_and_parents(output_dir)" ] }, @@ -135,7 +150,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.13.7" + "version": "3.13.9" } }, "nbformat": 4, diff --git a/demos/visualization/collisionless_components.ipynb b/demos/visualization/amplitude_components.ipynb similarity index 68% rename from demos/visualization/collisionless_components.ipynb rename to demos/visualization/amplitude_components.ipynb index 218f5a0..3a29926 100644 --- a/demos/visualization/collisionless_components.ipynb +++ b/demos/visualization/amplitude_components.ipynb @@ -5,7 +5,7 @@ "id": "ca7a55e2-5729-4a0c-b1bc-19ace7be8100", "metadata": {}, "source": [ - "# Visualizing Collisionless QLBM circuits" + "# Visualizing Amplitude-Based QLBM circuits" ] }, { @@ -18,12 +18,39 @@ "# Import the required packages from the qlbm framework\n", "from qlbm.components import (\n", " CQLBM,\n", - " CollisionlessStreamingOperator,\n", + " ABStreamingOperator,\n", " ControlledIncrementer,\n", - " SpecularReflectionOperator,\n", + " MSStreamingOperator,\n", " SpeedSensitivePhaseShift,\n", ")\n", - "from qlbm.lattice import CollisionlessLattice" + "from qlbm.components.ab import ABStreamingOperator\n", + "from qlbm.lattice import ABLattice, MSLattice" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3e99560c", + "metadata": {}, + "outputs": [], + "source": [ + "example_lattice_ms = MSLattice(\n", + " {\n", + " \"lattice\": {\"dim\": {\"x\": 32, \"y\": 8}, \"velocities\": {\"x\": 4, \"y\": 4}},\n", + " \"geometry\": [\n", + " {\"shape\": \"cuboid\", \"x\": [4, 7], \"y\": [1, 5], \"boundary\": \"bounceback\"}\n", + " ],\n", + " }\n", + ")\n", + "\n", + "example_lattice_ab = ABLattice(\n", + " {\n", + " \"lattice\": {\"dim\": {\"x\": 128, \"y\": 8}, \"velocities\": \"d2q9\"},\n", + " \"geometry\": [\n", + " {\"shape\": \"cuboid\", \"x\": [4, 7], \"y\": [1, 5], \"boundary\": \"bounceback\"}\n", + " ],\n", + " }\n", + ")\n" ] }, { @@ -34,7 +61,7 @@ "outputs": [], "source": [ "# Define an example which uses 4 velocity qubits and the qubits with speed 2 will stream\n", - "speed_shift_primitive = SpeedSensitivePhaseShift(4, 2, True)" + "speed_shift_primitive = SpeedSensitivePhaseShift(5, 1, True)" ] }, { @@ -70,25 +97,6 @@ "speed_shift_primitive.draw(\"latex_source\")" ] }, - { - "cell_type": "code", - "execution_count": null, - "id": "ad38ddb2-56fe-435d-b2db-17ed4a11efef", - "metadata": {}, - "outputs": [], - "source": [ - "# Define a lattice based on which we can construct\n", - "# Operators and algorithms\n", - "example_lattice = CollisionlessLattice(\n", - " {\n", - " \"lattice\": {\"dim\": {\"x\": 8, \"y\": 8}, \"velocities\": {\"x\": 4, \"y\": 4}},\n", - " \"geometry\": [\n", - " {\"shape\": \"cuboid\", \"x\": [5, 6], \"y\": [1, 2], \"boundary\": \"specular\"}\n", - " ],\n", - " }\n", - ")" - ] - }, { "cell_type": "code", "execution_count": null, @@ -97,7 +105,7 @@ "outputs": [], "source": [ "# All primitives can be drawn to the same interface\n", - "ControlledIncrementer(example_lattice, reflection=False).draw(\"mpl\")" + "ControlledIncrementer(example_lattice_ms, reflection=False).draw(\"mpl\")" ] }, { @@ -108,19 +116,10 @@ "outputs": [], "source": [ "# All operators can be drawn the same way\n", - "CollisionlessStreamingOperator(example_lattice, [0, 2, 3]).draw(\"mpl\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3cf55506-d0b0-4f26-832d-13e101ba725a", - "metadata": {}, - "outputs": [], - "source": [ - "SpecularReflectionOperator(example_lattice, example_lattice.shapes[\"bounceback\"]).draw(\n", - " \"mpl\"\n", - ")" + "MSStreamingOperator(\n", + " example_lattice_ms,\n", + " [0, 2, 3],\n", + ").draw(\"mpl\")" ] }, { @@ -130,14 +129,14 @@ "metadata": {}, "outputs": [], "source": [ - "# As can entire algorithms\n", - "CQLBM(example_lattice).draw(\"mpl\")" + "# This works for all quantum components in the library\n", + "ABStreamingOperator(example_lattice_ab).draw(\"mpl\")" ] }, { "cell_type": "code", "execution_count": null, - "id": "66036f08-5fb8-4955-8192-de03b708f340", + "id": "8e80f2af", "metadata": {}, "outputs": [], "source": [] @@ -145,7 +144,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "qlbm-cpu-venv", "language": "python", "name": "python3" }, @@ -159,7 +158,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.12" + "version": "3.13.9" } }, "nbformat": 4, diff --git a/demos/visualization/geometry.ipynb b/demos/visualization/geometry.ipynb index 3864e5a..2054778 100644 --- a/demos/visualization/geometry.ipynb +++ b/demos/visualization/geometry.ipynb @@ -27,8 +27,8 @@ "import pyvista as pv\n", "from pyvista import themes\n", "\n", - "from qlbm.infra import CollisionlessResult\n", - "from qlbm.lattice import CollisionlessLattice\n", + "from qlbm.infra import AmplitudeResult\n", + "from qlbm.lattice import MSLattice\n", "from qlbm.tools.utils import create_directory_and_parents\n", "\n", "pv.set_plot_theme(themes.ParaViewTheme())" @@ -40,7 +40,7 @@ "metadata": {}, "outputs": [], "source": [ - "lattice_2d = CollisionlessLattice(\n", + "lattice_2d = MSLattice(\n", " {\n", " \"lattice\": {\n", " \"dim\": {\"x\": 32, \"y\": 32},\n", @@ -57,7 +57,7 @@ " ],\n", " }\n", ")\n", - "lattice_3d = CollisionlessLattice(\n", + "lattice_3d = MSLattice(\n", " {\n", " \"lattice\": {\n", " \"dim\": {\"x\": 16, \"y\": 128, \"z\": 16},\n", @@ -95,7 +95,7 @@ "outputs": [], "source": [ "# Will output seven 2D stl files under `qlbm-output/visualization-components-2d/paraview`\n", - "CollisionlessResult(lattice_2d, root_directory_2d).visualize_geometry()" + "AmplitudeResult(lattice_2d, root_directory_2d).visualize_geometry()" ] }, { @@ -105,7 +105,7 @@ "outputs": [], "source": [ "# Will output one 3D stl files under `qlbm-output/visualization-components-3d/paraview`\n", - "CollisionlessResult(lattice_3d, root_directory_3d).visualize_geometry()" + "AmplitudeResult(lattice_3d, root_directory_3d).visualize_geometry()" ] }, { diff --git a/demos/visualization/lqlga_components.ipynb b/demos/visualization/lqlga_components.ipynb index 25719bd..3aedc8d 100644 --- a/demos/visualization/lqlga_components.ipynb +++ b/demos/visualization/lqlga_components.ipynb @@ -19,7 +19,7 @@ "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 " + "from qlbm.lattice.lattices.lqlga_lattice import LQLGALattice" ] }, { @@ -105,7 +105,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.13.7" + "version": "3.13.9" } }, "nbformat": 4, diff --git a/docs/source/code/comps_base.rst b/docs/source/code/comps_base.rst index 370c4f5..98949e4 100644 --- a/docs/source/code/comps_base.rst +++ b/docs/source/code/comps_base.rst @@ -20,6 +20,9 @@ Lattice Base .. autoclass:: qlbm.lattice.lattices.base.Lattice :members: +.. autoclass:: qlbm.lattice.lattices.base.AmplitudeLattice + :members: + .. autoclass:: qlbm.lattice.geometry.shapes.base.Shape :members: @@ -35,7 +38,7 @@ Components Base .. autoclass:: qlbm.components.base.LBMOperator -.. autoclass:: qlbm.components.base.CQLBMOperator +.. autoclass:: qlbm.components.base.MSOperator .. autoclass:: qlbm.components.base.SpaceTimeOperator diff --git a/docs/source/code/comps_collision.rst b/docs/source/code/comps_collision.rst deleted file mode 100644 index 1eb7a13..0000000 --- a/docs/source/code/comps_collision.rst +++ /dev/null @@ -1,65 +0,0 @@ -.. _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 - -.. autoclass:: qlbm.components.common.EQCCollisionOperator - diff --git a/docs/source/code/comps_cqlbm.rst b/docs/source/code/comps_cqlbm.rst index 4a2e80f..ac97ebe 100644 --- a/docs/source/code/comps_cqlbm.rst +++ b/docs/source/code/comps_cqlbm.rst @@ -1,7 +1,7 @@ -.. _cqlbm_components: +.. _amplitude_components: ==================================== -Collisionless Circuits +Amplitude-Based Circuits ==================================== .. testcode:: @@ -9,12 +9,13 @@ Collisionless Circuits from qlbm.components import ( CQLBM, - CollisionlessStreamingOperator, + MSQLBM, + MSStreamingOperator, ControlledIncrementer, SpecularReflectionOperator, SpeedSensitivePhaseShift, ) - from qlbm.lattice import CollisionlessLattice + from qlbm.lattice import MSLattice print("ok") .. testoutput:: @@ -23,74 +24,106 @@ Collisionless Circuits ok -This page contains documentation about the quantum circuits that make up the -**C**\ ollisionless **Q**\ uantum **L**\ attice **B**\ oltzmann **M**\ ethod (CQLBM) -first described in :cite:p:`collisionless` and later expanded in :cite:p:`qmem`. +This page documents the components that are used in algorithms +that use the **A** mplitude **B** ased (AB) Encoding. +At the moment, this includes two algorihtms: + +#. The "regular" Amplitude-Based Collisionless QLBM: ABQLBM, +#. The Multi-Speed (MS) Collisionless QLBM: MSQLBM. + +:class:`.ABQLBM` uses :class:`.ABLattice`\ s and :class:`.OHLattice`\ s, while :class:`.MSQLBM` is built from :class:`.MSLattice`\ s. +The general interface of :class:`.CQLBM` is built from all 3 lattice instances and delegeates to the appropriate implementation automatically. + +Both algorithms are instances of the Collisionless QLBM (:class:`.CQLBM`), also known as the +Quantum Transport Method (QTM). +Both algorithms compress the grid and the vnumber of discrete velocities +into :math:`N_g\cdot N_v \mapsto \lceil \log_2 N_g \rceil + \lceil \log_2 N_v \rceil` qubits. +The amplitude of each basis state is directly related to the populations in the classical LBM discretization. +The MSQLBM is a generalization of the ABQLBM. +The implementation of the algorithms was first described in :cite:p:`collisionless` and later expanded in :cite:p:`qmem`. + At its core, the CQLBM algorithm manipulates the particle probability distribution in an amplitude-based encoding of the quantum state. -This happens in several distinct steps: +The :ref:`cqlbm_e2e` can be broken down into several distinct steps: -#. Initial conditions prepare the starting state of the probability distribution function. +#. :ref:`cqlbm_initial` conditions prepare the starting state of the probability distribution function. #. :ref:`cqlbm_streaming` circuits increment or decrement the position of particles in physical space through QFT-based streaming. #. :ref:`cqlbm_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. -#. Measurement operations extract information out of the quantum state, which can later be post-processed classically. +#. :ref:`cqlbm_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. -.. _e2e: +.. _cqlbm_e2e: End-to-end algorithms ---------------------------------- -.. autoclass:: qlbm.components.collisionless.cqlbm.CQLBM +.. autoclass:: qlbm.components.CQLBM + +.. autoclass:: qlbm.components.ms.MSQLBM + +.. autoclass:: qlbm.components.ab.ABQLBM + +.. _cqlbm_initial: + +Initial Conditions +----------------------------------- + +.. autoclass:: qlbm.components.ms.primitives.MSInitialConditions + +.. autoclass:: qlbm.components.ms.primitives.MSInitialConditions3DSlim + +.. autoclass:: qlbm.components.ab.initial.ABInitialConditions .. _cqlbm_streaming: Streaming ---------------------------------- -.. autoclass:: qlbm.components.collisionless.streaming.CollisionlessStreamingOperator +.. autoclass:: qlbm.components.ms.streaming.MSStreamingOperator -.. autoclass:: qlbm.components.collisionless.streaming.StreamingAncillaPreparation +.. autoclass:: qlbm.components.ms.streaming.StreamingAncillaPreparation -.. autoclass:: qlbm.components.collisionless.streaming.ControlledIncrementer +.. autoclass:: qlbm.components.ms.streaming.ControlledIncrementer -.. autoclass:: qlbm.components.collisionless.primitives.SpeedSensitiveAdder +.. autoclass:: qlbm.components.ms.primitives.SpeedSensitiveAdder -.. autoclass:: qlbm.components.collisionless.streaming.SpeedSensitivePhaseShift +.. autoclass:: qlbm.components.ms.streaming.SpeedSensitivePhaseShift -.. autoclass:: qlbm.components.collisionless.streaming.PhaseShift +.. autoclass:: qlbm.components.ms.streaming.PhaseShift + +.. autoclass:: qlbm.components.ab.streaming.ABStreamingOperator .. _cqlbm_reflection: Reflection ---------------------------------- -.. autoclass:: qlbm.components.collisionless.bounceback_reflection.BounceBackReflectionOperator +.. autoclass:: qlbm.components.ms.bounceback_reflection.BounceBackReflectionOperator :members: -.. autoclass:: qlbm.components.collisionless.specular_reflection.SpecularReflectionOperator +.. autoclass:: qlbm.components.ms.specular_reflection.SpecularReflectionOperator :members: -.. autoclass:: qlbm.components.collisionless.bounceback_reflection.BounceBackWallComparator +.. autoclass:: qlbm.components.ms.bounceback_reflection.BounceBackWallComparator -.. autoclass:: qlbm.components.collisionless.specular_reflection.SpecularWallComparator +.. autoclass:: qlbm.components.ms.specular_reflection.SpecularWallComparator -.. autoclass:: qlbm.components.collisionless.primitives.EdgeComparator +.. autoclass:: qlbm.components.ms.primitives.EdgeComparator -.. autoclass:: qlbm.components.collisionless.primitives.Comparator +.. autoclass:: qlbm.components.ms.primitives.Comparator -.. autoclass:: qlbm.components.collisionless.primitives.ComparatorMode +.. autoclass:: qlbm.components.ms.primitives.ComparatorMode -.. _cqlbm_others: +.. autoclass:: qlbm.components.ab.reflection.ABReflectionOperator -Others ------------------------------------ +.. _cqlbm_measurement: -.. autoclass:: qlbm.components.collisionless.primitives.CollisionlessInitialConditions +Measurement +----------------------------------- -.. autoclass:: qlbm.components.collisionless.primitives.CollisionlessInitialConditions3DSlim +.. autoclass:: qlbm.components.ms.primitives.GridMeasurement -.. autoclass:: qlbm.components.collisionless.primitives.GridMeasurement \ No newline at end of file +.. autoclass:: qlbm.components.ab.measurement.ABGridMeasurement \ No newline at end of file diff --git a/docs/source/code/comps_lga.rst b/docs/source/code/comps_lga.rst new file mode 100644 index 0000000..71a16a2 --- /dev/null +++ b/docs/source/code/comps_lga.rst @@ -0,0 +1,144 @@ +.. _qlga_components: + +==================================== +QLGA Circuits +==================================== + +.. testcode:: + :hide: + + from qlbm.components import ( + LQLGA, + LQGLAInitialConditions, + LQLGAGridVelocityMeasurement, + LQLGAReflectionOperator, + LQLGAStreamingOperator, + ) + from qlbm.components import ( + CQLBM, + MSStreamingOperator, + ControlledIncrementer, + SpecularReflectionOperator, + SpeedSensitivePhaseShift, + ) + from qlbm.lattice import MSLattice, LQLGALattice + print("ok") + +.. testoutput:: + :hide: + + ok + + +This page contains documentation about the quantum circuits that make up the +**L**\ attice **G**\ as **A**\ utomata (LGA) algorithms of ``qlbm``. +This includes two aglorithms: + +#. **S**\ pace-\ **T**\ ime **Q**\ uantum **L**\ attice **B**\ oltzmann **M**\ ethod (STQLBM), described in :cite:p:`spacetime` and extended in :cite:p:`spacetime2`. +#. **L**\ inear **Q**\ uantum **L**\ attice **G**\ as **A**\ utomata (LQLGA), :cite:p:`lqlga1`, :cite:p:`lqlga2`. + +At its core, the Space-Time QLBM uses an extended computational basis state +encoding that that circumvents the non-locality of the streaming +step by including additional information from neighboring grid points. +This happens in several distinct steps: +The LQGLA encodes a lattice of :math:`N_g` gridpoints with :math:`q` discrete velocities +each into :math:`N_g \cdot q` qubits. +LQLGA can be seen as the "limit" of the extended computational basis state encoding that is the Space-Time encoding. +For both algorithms, 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. + +.. warning:: + STQLBM and LQLGA are a based on typical :math:`D_dQ_q` discretizations. + The current implementation only supports :math:`D_1Q_2`, :math:`D_1Q_3`, and :math:`D_2Q_4` for one time step + with inexact restarts through ``qlbm``\ 's reinitialization mechanism. + LQLGA only supports :math:`D_1Q_3`. + +.. note:: + Need to work with a different discretization or want to work together? Reach out at ``qcfd-ewi@tudelft.nl``. + +.. _lqlga_e2e: + +End-to-end algorithms +---------------------------------- + +.. autoclass:: qlbm.components.spacetime.spacetime.SpaceTimeQLBM + +.. autoclass:: qlbm.components.LQLGA + +.. _lqlga_initial: + +Initial Conditions +---------------------------------- + +.. autoclass:: qlbm.components.spacetime.initial.PointWiseSpaceTimeInitialConditions + +.. autoclass:: qlbm.components.LQGLAInitialConditions + +.. _lqlga_streaming: + +Streaming +---------------------------------- + +.. autoclass:: qlbm.components.spacetime.streaming.SpaceTimeStreamingOperator + +.. autoclass:: qlbm.components.LQLGAStreamingOperator + +.. _lqlga_reflection: + +Reflection +---------------------------------- + +.. autoclass:: qlbm.components.LQLGAReflectionOperator + +.. _lqlga_collision: + +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 in LGA algorithms is based on the concept of equivalence classes +described in Section 4 of :cite:p:`spacetime2`, and follows a permute-redistribute-unpermute (PRP) 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: + +.. autoclass:: qlbm.components.spacetime.collision.GenericSpaceTimeCollisionOperator + +.. autoclass:: qlbm.components.spacetime.collision.SpaceTimeD2Q4CollisionOperator + +.. autoclass:: qlbm.components.GenericLQLGACollisionOperator + +.. autoclass:: qlbm.components.common.EQCPermutation + +.. autoclass:: qlbm.components.common.EQCRedistribution + +.. autoclass:: qlbm.components.common.EQCCollisionOperator + +.. autoclass:: qlbm.lattice.spacetime.properties_base.LatticeDiscretization + +.. autoclass:: qlbm.lattice.spacetime.properties_base.LatticeDiscretizationProperties + +.. autoclass:: qlbm.lattice.eqc.EquivalenceClass + +.. autoclass:: qlbm.lattice.eqc.EquivalenceClassGenerator + + +.. _lqlga_measurement: + +Measurement +---------------------------------- + +.. autoclass:: qlbm.components.spacetime.measurement.SpaceTimeGridVelocityMeasurement + +.. autoclass:: qlbm.components.LQLGAGridVelocityMeasurement \ No newline at end of file diff --git a/docs/source/code/comps_lqlga.rst b/docs/source/code/comps_lqlga.rst deleted file mode 100644 index da2f7b4..0000000 --- a/docs/source/code/comps_lqlga.rst +++ /dev/null @@ -1,84 +0,0 @@ -.. _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/comps_stqbm.rst b/docs/source/code/comps_stqbm.rst deleted file mode 100644 index a37faa3..0000000 --- a/docs/source/code/comps_stqbm.rst +++ /dev/null @@ -1,83 +0,0 @@ -.. _stqlbm_components: - -==================================== -Space-Time Circuits -==================================== - -.. testcode:: - :hide: - - from qlbm.components import ( - CQLBM, - CollisionlessStreamingOperator, - ControlledIncrementer, - SpecularReflectionOperator, - SpeedSensitivePhaseShift, - ) - from qlbm.lattice import CollisionlessLattice - print("ok") - -.. testoutput:: - :hide: - - ok - - -This page contains documentation about the quantum circuits that make up the -**S**\ pace-\ **T**\ ime **Q**\ uantum **L**\ attice **B**\ oltzmann **M**\ ethod (STQLBM) -described in :cite:p:`spacetime`. -At its core, the Space-Time QLBM uses an extended computational basis state -encoding that that circumvents the non-locality of the streaming -step by including additional information from neighboring grid points. -This happens in several distinct steps: - -#. Initial conditions prepare the starting state of the probability distribution function. -#. :ref:`stqlbm_streaming` moves the position of particles to neighboring points according to their velocity. -#. :ref:`stqlbm_collision` locally changes the velocity profile of particles positioned at the same position in space. -#. 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 STQLBM algorithm. -Subsections follow a top-down approach, where end-to-end operators are introduced first, -before being broken down into their constituent parts. - -.. warning:: - The STQBLM algorithm is a based on typical :math:`D_dQ_q` discretizations. - The current implementation only supports :math:`D_2Q_4` for one time step. - This is work in progress. - Multiple steps are possible through ``qlbm``\ 's reinitialization mechanism. - -.. _stqlbm_e2e: - -End-to-end algorithms ----------------------------------- - -.. autoclass:: qlbm.components.spacetime.spacetime.SpaceTimeQLBM - -.. _stqlbm_streaming: - -Streaming ----------------------------------- - -.. autoclass:: qlbm.components.spacetime.streaming.SpaceTimeStreamingOperator - -.. _stqlbm_collision: - -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. - -.. autoclass:: qlbm.components.spacetime.collision.GenericSpaceTimeCollisionOperator - -.. autoclass:: qlbm.components.spacetime.collision.SpaceTimeD2Q4CollisionOperator - -.. _stqlbm_others: - -Others ------------------------------------ - -.. autoclass:: qlbm.components.spacetime.initial.PointWiseSpaceTimeInitialConditions - -.. autoclass:: qlbm.components.spacetime.measurement.SpaceTimeGridVelocityMeasurement \ No newline at end of file diff --git a/docs/source/code/index.rst b/docs/source/code/index.rst index f869ba8..5174384 100644 --- a/docs/source/code/index.rst +++ b/docs/source/code/index.rst @@ -4,7 +4,7 @@ Internal Documentation ================================ ``qlbm`` is made up of 4 main modules. -Together, the :ref:`base_components`, :ref:`cqlbm_components`, and :ref:`stqlbm_components` +Together, the :ref:`base_components`, :ref:`amplitude_components`, and :ref:`qlga_components` module handle the parameterized creation of quantum circuits that compose QBMs. The :ref:`lattice` module parses external information into quantum registers and provides uniform interfaces for underlying algorithms. @@ -14,12 +14,10 @@ The :ref:`tools` module contains miscellaneous utilities. .. toctree:: + lattice comps_base comps_cqlbm - comps_collision - comps_stqbm - comps_lqlga - lattice + comps_lga infra tools diff --git a/docs/source/code/infra.rst b/docs/source/code/infra.rst index a25f96f..760ec0a 100644 --- a/docs/source/code/infra.rst +++ b/docs/source/code/infra.rst @@ -62,7 +62,7 @@ Results .. autoclass:: qlbm.infra.result.base.QBMResult :members: -.. autoclass:: qlbm.infra.result.collisionless_result.CollisionlessResult +.. autoclass:: qlbm.infra.result.amplitude_result.AmplitudeResult :members: .. autoclass:: qlbm.infra.result.spacetime_result.SpaceTimeResult diff --git a/docs/source/code/lattice.rst b/docs/source/code/lattice.rst index bf5fbbb..b9bb399 100644 --- a/docs/source/code/lattice.rst +++ b/docs/source/code/lattice.rst @@ -7,6 +7,17 @@ This page contains documentation about :ref:`lattices` and :ref:`geometry` class Lattices and geometry go hand-in-hand in that they do not themselves contain quantum components, but instead provide a convenient interface for accessing the information that determines the structure and composition of quantum components. +``qlbm`` supports the following kinds of lattices: + +#. Amplitude-Based (AB) lattices. These are the most common encdoings in QLBM literature. All AB lattices compress the grid into logarithmically many qubits. + #. :class:`.AmpltiudeLattice` is the abstract base class for all amplitude-based lattices. + #. :class:`.ABLattice` is the "standard" amplitude-based lattice, where both the grid and the velocities are logarithmically compressed. It supports only :math:`D_dQ_q` discretization. + #. :class:`.MSLattice` is the multi-speed lattice for the algorithm described in :cite:t:`collisionless`. It is the same as the :class:`.ABLattice`, except it supports different velocity discretizations. + #. :class:`.OHLattice` is the amplitude-based lattice where the grid is logarithmically compressed, but the :math:`D_dQ_q` velocities are not. It assigns one basis state per discrete velocity. + +#. LGA lattices. These rely on the computational basis state encoding (CBSE) and are used for QLGA algorithms. + #. :class:`.SpaceTimeLattice` is the realization of the space-time data encoding described in :cite:`spacetime` and :cite:`spacetime2`. It uses an expanded CBSE to accomodate multiple time steps. + #. :class:`.LQLGALattice` is the entirely uncompressed CBSE, encoding all velocity channels in the system. .. _lattices: @@ -24,12 +35,21 @@ Concretely, each :class:`.Lattice` fulfills the following functionality: #. Provide convenient indexing methods methods to access individual (or groups of) qubits based on their purpose. #. Encode additional information required for the automatic assembly of large quantum circuits. -.. autoclass:: qlbm.lattice.lattices.collisionless_lattice.CollisionlessLattice +.. autoclass:: qlbm.lattice.lattices.ms_lattice.MSLattice + :members: + +.. autoclass:: qlbm.lattice.lattices.ab_lattice.ABLattice + :members: + +.. autoclass:: qlbm.lattice.lattices.oh_lattice.OHLattice :members: .. autoclass:: qlbm.lattice.lattices.spacetime_lattice.SpaceTimeLattice :members: +.. autoclass:: qlbm.lattice.lattices.lqlga_lattice.LQLGALattice + :members: + .. _geometry: Geometry diff --git a/docs/source/examples/notebooks/collisionless_vis.nblink b/docs/source/examples/notebooks/collisionless_vis.nblink index 35cee09..db3aa26 100644 --- a/docs/source/examples/notebooks/collisionless_vis.nblink +++ b/docs/source/examples/notebooks/collisionless_vis.nblink @@ -1,3 +1,3 @@ { - "path": "../../../../demos/visualization/collisionless_components.ipynb" + "path": "../../../../demos/visualization/amplitude_components.ipynb" } \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index 5d0dfa1..9664634 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -9,7 +9,7 @@ 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`, :ref:`stqlbm_components`, and :ref:`lqlga_components` +Together, the :ref:`base_components`, :ref:`amplitude_components`, and :ref:`qlga_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. @@ -25,6 +25,12 @@ The :ref:`tools` module contains miscellaneous utilities. #. **L**\ inear \ **Q**\ uantum **L**\ attice **G**\ as **A**\ utomata (LQLGA) described in :cite:p:`spacetime2`, :cite:p:`lqlga1`, and :cite:p:`lqlga2`. +.. .. card:: Intro to QLBM +.. :link: internal_docs +.. :link-type: ref + +.. :fas:`lightbulb;sd-text-primary` Check out the basics. + .. card:: Internal Documentation :link: internal_docs :link-type: ref diff --git a/qlbm/__init__.py b/qlbm/__init__.py index f2d7a69..7e98639 100644 --- a/qlbm/__init__.py +++ b/qlbm/__init__.py @@ -6,23 +6,24 @@ from .components import ( CQLBM, BounceBackReflectionOperator, - CollisionlessStreamingOperator, + MSStreamingOperator, SpecularReflectionOperator, ) -from .infra import CircuitCompiler, CollisionlessResult, QiskitRunner -from .lattice import CollisionlessLattice, Lattice -from .lattice.lattices.spacetime_lattice import SpaceTimeLattice +from .infra import AmplitudeResult, CircuitCompiler, QiskitRunner +from .lattice import ABLattice, Lattice, LQLGALattice, MSLattice, SpaceTimeLattice __all__ = [ + "ABLattice", + "LQLGALattice", "Lattice", - "CollisionlessLattice", + "MSLattice", "SpaceTimeLattice", - "CollisionlessStreamingOperator", + "MSStreamingOperator", "SpecularReflectionOperator", "BounceBackReflectionOperator", "CQLBM", "CircuitCompiler", "QiskitRunner", "QulacsRunner", - "CollisionlessResult", + "AmplitudeResult", ] diff --git a/qlbm/components/__init__.py b/qlbm/components/__init__.py index 0cd14be..f969bbc 100644 --- a/qlbm/components/__init__.py +++ b/qlbm/components/__init__.py @@ -1,34 +1,29 @@ """Modular and extendible quantum circuits that perform parts of the QLBM algorithm.""" +from .ab import ( + ABQLBM, + ABGridMeasurement, + ABInitialConditions, + ABReflectionOperator, + ABReflectionPermutation, + ABStreamingOperator, +) from .base import ( - CQLBMOperator, LBMAlgorithm, LBMOperator, LBMPrimitive, + MSOperator, QuantumComponent, SpaceTimeOperator, ) -from .collisionless import ( - CQLBM, - BounceBackReflectionOperator, - CollisionlessInitialConditions, - CollisionlessStreamingOperator, - GridMeasurement, - SpecularReflectionOperator, -) -from .collisionless.primitives import Comparator, ComparatorMode, SpeedSensitiveAdder -from .collisionless.streaming import ( - ControlledIncrementer, - PhaseShift, - SpeedSensitivePhaseShift, - StreamingAncillaPreparation, -) from .common import ( EmptyPrimitive, EQCCollisionOperator, EQCPermutation, EQCRedistribution, + HammingWeightAdder, ) +from .cqlbm import CQLBM from .lqlga import ( LQLGA, GenericLQLGACollisionOperator, @@ -38,13 +33,28 @@ LQLGAReflectionOperator, LQLGAStreamingOperator, ) +from .ms import ( + MSQLBM, + BounceBackReflectionOperator, + GridMeasurement, + MSInitialConditions, + MSStreamingOperator, + SpecularReflectionOperator, +) +from .ms.primitives import Comparator, ComparatorMode, SpeedSensitiveAdder +from .ms.streaming import ( + ControlledIncrementer, + PhaseShift, + SpeedSensitivePhaseShift, + StreamingAncillaPreparation, +) __all__ = [ "QuantumComponent", "LBMPrimitive", "GenericLQLGACollisionOperator", "LBMOperator", - "CQLBMOperator", + "MSOperator", "SpaceTimeOperator", "LBMAlgorithm", "ComparatorMode", @@ -56,14 +66,15 @@ "StreamingAncillaPreparation", "ControlledIncrementer", "GridMeasurement", - "CollisionlessInitialConditions", - "CollisionlessStreamingOperator", + "MSInitialConditions", + "MSStreamingOperator", "SpecularReflectionOperator", "BounceBackReflectionOperator", - "CQLBM", + "MSQLBM", "GenericLQLGACollisionOperator", "LQGLAInitialConditions", "LQLGA", + "CQLBM", "LQLGAGridVelocityMeasurement", "LQLGAMGReflectionOperator", "LQLGAReflectionOperator", @@ -71,4 +82,11 @@ "EQCCollisionOperator", "EQCPermutation", "EQCRedistribution", + "HammingWeightAdder", + "ABQLBM", + "ABInitialConditions", + "ABGridMeasurement", + "ABReflectionOperator", + "ABReflectionPermutation", + "ABStreamingOperator", ] diff --git a/qlbm/components/ab/__init__.py b/qlbm/components/ab/__init__.py new file mode 100644 index 0000000..c1f69b3 --- /dev/null +++ b/qlbm/components/ab/__init__.py @@ -0,0 +1,18 @@ +"""Primitives and operators for the Amplitude Based QLBM.""" + +from .ab import ABQLBM +from .encodings import ABEncodingType +from .initial import ABInitialConditions +from .measurement import ABGridMeasurement +from .reflection import ABReflectionOperator, ABReflectionPermutation +from .streaming import ABStreamingOperator + +__all__ = [ + "ABQLBM", + "ABInitialConditions", + "ABGridMeasurement", + "ABReflectionOperator", + "ABReflectionPermutation", + "ABStreamingOperator", + "ABEncodingType", +] diff --git a/qlbm/components/ab/ab.py b/qlbm/components/ab/ab.py new file mode 100644 index 0000000..a66b081 --- /dev/null +++ b/qlbm/components/ab/ab.py @@ -0,0 +1,99 @@ +"""The end-to-end algorithm of the Collisionless Quantum Lattice Boltzmann Algorithm first introduced in :cite:t:`collisionless` and later extended in :cite:t:`qmem`.""" + +from logging import Logger, getLogger +from time import perf_counter_ns + +from qiskit import QuantumCircuit +from typing_extensions import override + +from qlbm.components.ab.reflection import ABReflectionOperator +from qlbm.components.base import LBMAlgorithm +from qlbm.lattice.geometry.shapes.block import Block +from qlbm.lattice.lattices.ab_lattice import ABLattice +from qlbm.tools.exceptions import LatticeException +from qlbm.tools.utils import flatten + +from .streaming import ABStreamingOperator + + +class ABQLBM(LBMAlgorithm): + """ + Implementation of the **A** mplitude **B** ased QLBM (ABQLBM). + + The algorithm consists of interleaving steps of streaming and boundary conditions. + Note that there is **no** collision in this algorithm as of yet. + Details of the general framework can be found in :cite:`collisionless`. + The ABQLBM works with :math:`D_dQ_q` discretizations only. + For multi-speed alternatives, see :class:`.MSQLBM`. + + Eample usage: + + .. code-block:: python + + from qlbm.components.ab import ABQLBM + from qlbm.lattice import ABLattice + + # Example with streaming only for simplicity. + lattice = ABLattice( + { + "lattice": {"dim": {"x": 16, "y": 8}, "velocities": "d2q9"}, + "geometry": [], + } + ) + + ABQLBM(lattice).draw("mpl") + """ + + def __init__( + self, + lattice: ABLattice, + logger: Logger = getLogger("qlbm"), + ) -> None: + super().__init__(lattice, logger) + self.lattice: ABLattice = 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 = QuantumCircuit( + *self.lattice.registers, + ) + + circuit.compose( + ABStreamingOperator( + self.lattice, + logger=self.logger, + ).circuit, + inplace=True, + ) + + for bc in ["bounceback", "specular"]: + if self.lattice.shapes[bc]: + if not all( + isinstance(shape, Block) + for shape in self.lattice.shapes["specular"] + ): + raise LatticeException( + f"All shapes with the {bc} boundary condition must be cuboids for the ABQLBM algorithm. " + ) + + circuit.compose( + ABReflectionOperator( + self.lattice, + flatten(list(self.lattice.shapes.values())), # type: ignore + logger=self.logger, + ).circuit, + inplace=True, + ) + + return circuit + + @override + def __str__(self) -> str: + return f"[Algorithm ABQLBM with lattice {self.lattice}]" diff --git a/qlbm/components/ab/averaged_collision.py b/qlbm/components/ab/averaged_collision.py new file mode 100644 index 0000000..d817c0a --- /dev/null +++ b/qlbm/components/ab/averaged_collision.py @@ -0,0 +1,59 @@ +"""WIP.""" + +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 LBMOperator +from qlbm.components.common.primitives import TruncatedQFT +from qlbm.lattice.lattices.ab_lattice import ABLattice +from qlbm.lattice.spacetime.properties_base import LatticeDiscretization +from qlbm.tools.exceptions import LatticeException + + +class ABEAveragedCollisionOperator(LBMOperator): + """WIP.""" + + lattice: ABLattice + + def __init__( + self, + lattice: ABLattice, + logger: Logger = getLogger("qlbm"), + ) -> None: + super().__init__(lattice, logger) + + self.logger.info(f"Creating circuit {str(self)}...") + circuit_creation_start_time = perf_counter_ns() + self.circuit = self.create_circuit() + self.logger.info( + f"Creating circuit {str(self)} took {perf_counter_ns() - circuit_creation_start_time} (ns)" + ) + + @override + def create_circuit(self) -> QuantumCircuit: + if self.lattice.discretization == LatticeDiscretization.D1Q3: + return self.__create_circuit_d1q3() + + raise LatticeException("ABE only currently supported in D1Q3") + + def __create_circuit_d1q3(self): + circuit = self.lattice.circuit.copy() + + circuit.compose( + TruncatedQFT( + self.lattice.num_velocity_qubits, + self.lattice.num_velocities_per_point, + self.logger, + ).circuit, + qubits=self.lattice.velocity_index(), + inplace=True, + ) + + return circuit + + @override + def __str__(self) -> str: + return f"[Operator ABEAveragedCollision with lattice {self.lattice}]" diff --git a/qlbm/components/ab/encodings.py b/qlbm/components/ab/encodings.py new file mode 100644 index 0000000..0b0901b --- /dev/null +++ b/qlbm/components/ab/encodings.py @@ -0,0 +1,18 @@ +"""Encoding utilities for amplitude-based lattices and components.""" + +from matplotlib.pylab import Enum + + +class ABEncodingType(Enum): + r"""Enumerator for the kinds of encodings under the Amplitude-Based encoding umbrella. + + The modes are as follows: + + * (1, ``ABEncodingType.AB``, The regular AB encoding.); + * (2, ``ABEncodingType.OH``, The one-hot encoding.); + * (3, ``ABEncodingType.MS``, The multi-speed encoding.). + """ + + AB = (1, ) + OH = (2, ) + MS = (3, ) diff --git a/qlbm/components/ab/initial.py b/qlbm/components/ab/initial.py new file mode 100644 index 0000000..bf294bb --- /dev/null +++ b/qlbm/components/ab/initial.py @@ -0,0 +1,149 @@ +"""Quantum circuits used for setting the initial state in the :class:`ABQLBM` algorithm.""" + +from logging import Logger, getLogger +from time import perf_counter_ns + +import numpy as np +from qiskit import QuantumCircuit +from qiskit.quantum_info import Operator +from typing_extensions import override + +from qlbm.components.ab.encodings import ABEncodingType +from qlbm.components.base import LBMPrimitive +from qlbm.components.common.primitives import TruncatedQFT +from qlbm.lattice.lattices.ab_lattice import ABLattice +from qlbm.tools.exceptions import LatticeException + + +class ABInitialConditions(LBMPrimitive): + """ + Initial conditions for the :class:`ABQLBM` algorithm. + + This component creates an equal magnitude superposition of all velocity + basis states at position ``(0, 0)`` using the :class:`TruncatedQFT`. + + Example usage: + + .. plot:: + :include-source: + + from qlbm.components.ab import ABInitialConditions + from qlbm.lattice import ABLattice + + lattice = ABLattice( + { + "lattice": {"dim": {"x": 16, "y": 8}, "velocities": "d2q9"}, + "geometry": [], + } + ) + + ABInitialConditions(lattice).draw("mpl") + + You can also get the low-level decomposition of the circuit as: + + .. plot:: + :include-source: + + from qlbm.components.ab import ABInitialConditions + from qlbm.lattice import ABLattice + + lattice = ABLattice( + { + "lattice": {"dim": {"x": 4, "y": 4}, "velocities": "d2q9"}, + "geometry": [], + } + ) + + ABInitialConditions(lattice).circuit.decompose(reps=2).draw("mpl") + """ + + def __init__( + self, + lattice: ABLattice, + logger: Logger = getLogger("qlbm"), + ) -> None: + super().__init__(logger) + self.lattice = lattice + + self.logger.info(f"Creating circuit {str(self)}...") + circuit_creation_start_time = perf_counter_ns() + self.circuit = self.create_circuit() + self.logger.info( + f"Creating circuit {str(self)} took {perf_counter_ns() - circuit_creation_start_time} (ns)" + ) + + @override + def create_circuit(self) -> QuantumCircuit: + circuit = QuantumCircuit(*self.lattice.registers) + + match self.lattice.get_encoding(): + case ABEncodingType.AB: + circuit.compose( + TruncatedQFT( + self.lattice.num_velocity_qubits, + self.lattice.num_velocities_per_point, + self.logger, + ).circuit, + qubits=self.lattice.velocity_index(), + inplace=True, + ) + case ABEncodingType.OH: + nq = int(np.ceil(np.log2(self.lattice.num_velocity_qubits))) + circuit.compose( + TruncatedQFT( + nq, + self.lattice.num_velocity_qubits, + self.logger, + ).circuit, + qubits=self.lattice.velocity_index()[:nq], + inplace=True, + ) + + circuit.compose( + self.__oh_permutation(), + qubits=self.lattice.velocity_index(), + inplace=True, + ) + case _: + raise LatticeException( + f"Encoding {self.lattice.get_encoding()} not supported." + ) + + return circuit + + def __oh_permutation(self) -> QuantumCircuit: + circuit = QuantumCircuit(*self.lattice.registers) + + n = self.lattice.num_velocity_qubits + dim = 2**n + + perm = [-1] * dim + used_rows = set() + + for j in range(9): + row = 1 << j # 2^j + perm[j] = row + used_rows.add(row) + + # Fill in the rest of the permutation arbitrarily but bijectively. + remaining_rows = [r for r in range(dim) if r not in used_rows] + k = 0 + for col in range(9, dim): + perm[col] = remaining_rows[k] + k += 1 + + U = np.zeros((dim, dim), dtype=complex) + for col in range(dim): + row = perm[col] + U[row, col] = 1.0 + + op = Operator(U) + + circuit = QuantumCircuit(n) + circuit.unitary(op, range(n), label="binary_to_onehot") + + return circuit + + @override + def __str__(self) -> str: + return f"[Primitive ABEInitialConditions with lattice {self.lattice}]" diff --git a/qlbm/components/ab/measurement.py b/qlbm/components/ab/measurement.py new file mode 100644 index 0000000..7c6f58d --- /dev/null +++ b/qlbm/components/ab/measurement.py @@ -0,0 +1,86 @@ +"""Quantum circuits used for measurement in the :class:`ABQLBM` algorithm.""" + +from logging import Logger, getLogger +from time import perf_counter_ns + +from qiskit import ClassicalRegister, QuantumCircuit +from typing_extensions import override + +from qlbm.components.base import LBMPrimitive +from qlbm.lattice.lattices.ab_lattice import ABLattice + + +class ABGridMeasurement(LBMPrimitive): + """ + Grid measurement for the :class:`ABQLBM` algorithm. + + Example usage: + + .. plot:: + :include-source: + + from qlbm.components.ab import ABGridMeasurement + from qlbm.lattice import ABLattice + + lattice = ABLattice( + { + "lattice": {"dim": {"x": 32, "y": 8}, "velocities": "d2q9"}, + "geometry": [], + } + ) + + ABGridMeasurement(lattice).draw("mpl") + + """ + + def __init__( + self, + lattice: ABLattice, + measure_velocity_qubits: bool = False, + logger: Logger = getLogger("qlbm"), + ) -> None: + super().__init__(logger) + self.lattice = lattice + self.measure_velocity_qubits = measure_velocity_qubits + + self.logger.info(f"Creating circuit {str(self)}...") + circuit_creation_start_time = perf_counter_ns() + self.circuit = self.create_circuit() + self.logger.info( + f"Creating circuit {str(self)} took {perf_counter_ns() - circuit_creation_start_time} (ns)" + ) + + @override + def create_circuit(self) -> QuantumCircuit: + circuit = self.lattice.circuit.copy() + circuit.add_register( + ClassicalRegister( + self.lattice.num_grid_qubits + + ( + self.lattice.num_velocity_qubits + if self.measure_velocity_qubits + else 0 + ) + ) + ) + + circuit.measure( + self.lattice.grid_index() + + (self.lattice.velocity_index() if self.measure_velocity_qubits else []), + list( + range( + self.lattice.num_grid_qubits + + ( + self.lattice.num_velocity_qubits + if self.measure_velocity_qubits + else 0 + ) + ) + ), + ) + + return circuit + + @override + def __str__(self) -> str: + return f"[Primitive ABEGridMeasurement with lattice {self.lattice}]" diff --git a/qlbm/components/ab/reflection.py b/qlbm/components/ab/reflection.py new file mode 100644 index 0000000..92aa411 --- /dev/null +++ b/qlbm/components/ab/reflection.py @@ -0,0 +1,565 @@ +"""Quantum circuits used for reflection in the :class:`ABQLBM` algorithm.""" + +from itertools import product +from logging import Logger, getLogger +from time import perf_counter_ns +from typing import List, Tuple + +from qiskit import QuantumCircuit +from qiskit.circuit.library import MCMTGate, XGate +from typing_extensions import override + +from qlbm.components.ab.encodings import ABEncodingType +from qlbm.components.ab.streaming import ABStreamingOperator +from qlbm.components.base import LBMOperator, LBMPrimitive +from qlbm.components.ms.specular_reflection import SpecularWallComparator +from qlbm.lattice.geometry.encodings.ms import ReflectionPoint +from qlbm.lattice.geometry.shapes.block import Block +from qlbm.lattice.lattices.ab_lattice import ABLattice +from qlbm.lattice.lattices.base import AmplitudeLattice +from qlbm.lattice.spacetime.properties_base import LatticeDiscretization +from qlbm.tools.exceptions import LatticeException +from qlbm.tools.utils import flatten, get_qubits_to_invert + + +class ABReflectionOperator(LBMOperator): + """ + Implements bounceback reflection in the amplitude-based encoding of :class:`.ABQLBM` for :math:`D_dQ_q` discretizations. + + Example usage: + + .. code-block:: python + + from qlbm.components.ab import ABReflectionOperator + from qlbm.lattice import ABLattice + + lattice = ABLattice( + { + "lattice": {"dim": {"x": 4, "y": 4}, "velocities": "d2q9"}, + "geometry": [ + { + "shape": "cuboid", + "x": [1, 3], + "y": [1, 3], + "boundary": "bounceback", + } + ], + } + ) + + # Drawing the circuit might take a while + ABReflectionOperator(lattice, blocks=lattice.shapes["bounceback"]) + + """ + + lattice: AmplitudeLattice + + def __init__( + self, + lattice: ABLattice, + blocks: List[Block], + logger: Logger = getLogger("qlbm"), + ) -> None: + super().__init__(lattice, logger) + + self.blocks = blocks + + self.logger.info(f"Creating circuit {str(self)}...") + circuit_creation_start_time = perf_counter_ns() + self.circuit = self.create_circuit() + self.logger.info( + f"Creating circuit {str(self)} took {perf_counter_ns() - circuit_creation_start_time} (ns)" + ) + + @override + def create_circuit(self) -> QuantumCircuit: + if self.lattice.discretization == LatticeDiscretization.D2Q9: + return self.__create_circuit_d2q9() + + raise LatticeException("AB reflection only currently supported in D2Q9") + + def __create_circuit_d2q9(self): + circuit = self.lattice.circuit.copy() + + # Mark populations inside the object + for block in self.blocks: + circuit.compose(self.set_inside_wall_ancilla_state(block), inplace=True) + + circuit.compose( + self.set_ancilla_of_point_state( + flatten( + [[(p, None) for p in block.corners_inside] for block in self.blocks] + ), + ignore_velocity_data=True, + ), + inplace=True, + ) + + # Stream the particles back + circuit.compose(self.permute_and_stream(), inplace=True) + + # Reset the ancilla state of reflected populations + for block in self.blocks: + circuit.compose(self.reset_outside_wall_ancilla_state(block), inplace=True) + + # Re-reset near corner point ancillas + point_data: List[Tuple[ReflectionPoint, List[int]]] = [] + + for block in self.blocks: + for dim in range(self.lattice.num_dims): + for c, bounds in enumerate( + product(*[[False, True]] * self.lattice.num_dims) + ): + point_data.append( + ( + block.near_corner_points_2d[dim * 4 + c], + block.get_lbm_near_corner_velocity_indices_to_reflect( + self.lattice.discretization, dim, bounds + ), + ) + ) + for c, bounds in enumerate( + product(*[[False, True]] * self.lattice.num_dims) + ): + point_data.append( + ( + block.corners_outside[c], + block.get_lbm_outside_corner_indices_to_reflect( + self.lattice.discretization, bounds + ), + ) + ) + # Re-reset the ancilla state of the populations that + # Shouldn't have been flipped in the previous step + circuit.compose( + self.set_ancilla_of_point_state(point_data, ignore_velocity_data=False), + inplace=True, + ) + + return circuit + + def set_inside_wall_ancilla_state(self, block: Block) -> QuantumCircuit: + """ + Sets the state of the ancilla qubit for all the gridpoints lying inside the walls of the block. + + This is done using the :class:`.SpecularWallComparator` originally designed + for the :class:`MSQLBM` algorithm. + The inside corner points are not addressed. + + Parameters + ---------- + block : Block + The solid object to address. + + Returns + ------- + QuantumCircuit + The circuit that sets the appropriate state on the object ancilla qubit. + """ + circuit = self.lattice.circuit.copy() + + for dim in range(self.lattice.num_dims): + for wall in block.walls_inside[dim]: + comparator_circuit = SpecularWallComparator( + self.lattice, wall, self.logger + ).circuit + + grid_qubit_indices_to_invert = [ + self.lattice.grid_index(0)[0] + qubit + for qubit in wall.data.qubits_to_invert + ] + + circuit.compose(comparator_circuit, inplace=True) + + if grid_qubit_indices_to_invert: + circuit.x(grid_qubit_indices_to_invert) + + control_qubits = ( + self.lattice.grid_index(wall.dim) + + self.lattice.ancillae_comparator_index() + ) + + target_qubits = self.lattice.ancillae_obstacle_index(0) + + circuit.compose( + MCMTGate( + XGate(), + len(control_qubits), + len(target_qubits), + ), + qubits=control_qubits + target_qubits, + inplace=True, + ) + + if grid_qubit_indices_to_invert: + circuit.x(grid_qubit_indices_to_invert) + + circuit.compose(comparator_circuit, inplace=True) + + return circuit + + def reset_outside_wall_ancilla_state(self, block: Block) -> QuantumCircuit: + """ + Resets the state of the obstacle ancilla qubit for all the gridpoints that are directly adjacent to the object, but in the fluid domain. + + This is done using the :class:`.SpecularWallComparator` originally designed + for the :class:`MSQLBM` algorithm. The state of obstacle ancilla of the + the near-corner gridpoints will be incorrect following the application of this primitive + and needs to be corrected. + The outside corner points are not addressed. + + Parameters + ---------- + block : Block + The solid object to address. + + Returns + ------- + QuantumCircuit + The circuit that sets the appropriate state on the object ancilla qubit. + """ + circuit = self.lattice.circuit.copy() + + for dim in range(self.lattice.num_dims): + for bound, wall in enumerate(block.walls_outside[dim]): + comparator_circuit = SpecularWallComparator( + self.lattice, wall, self.logger + ).circuit + + grid_qubit_indices_to_invert = [ + self.lattice.grid_index(0)[0] + qubit + for qubit in wall.data.qubits_to_invert + ] + + circuit.compose(comparator_circuit, inplace=True) + + if grid_qubit_indices_to_invert: + circuit.x(grid_qubit_indices_to_invert) + + # Reset the state for each velocity we care about: + for v in block.get_lbm_wall_velocity_indices_to_reflect( + self.lattice.discretization, dim, bool(bound) + ): + match self.lattice.get_encoding(): + case ABEncodingType.AB: + qs = [ + self.lattice.velocity_index()[0] + q + for q in get_qubits_to_invert( + v, self.lattice.num_velocity_qubits + ) + ] + + if qs: + circuit.x(qs) + + control_qubits = ( + self.lattice.grid_index(wall.dim) + + self.lattice.ancillae_comparator_index() + + self.lattice.velocity_index() # The reset step is additionally controlled on the velocity register + ) + + target_qubits = self.lattice.ancillae_obstacle_index(0) + + circuit.compose( + MCMTGate( + XGate(), + len(control_qubits), + len(target_qubits), + ), + qubits=control_qubits + target_qubits, + inplace=True, + ) + + if qs: + circuit.x(qs) + case ABEncodingType.OH: + control_qubits = ( + self.lattice.grid_index(wall.dim) + + self.lattice.ancillae_comparator_index() + + [ + self.lattice.velocity_index()[ + v + ] # Only one velocity index required here + ] + ) + + target_qubits = self.lattice.ancillae_obstacle_index(0) + + circuit.compose( + MCMTGate( + XGate(), + len(control_qubits), + len(target_qubits), + ), + qubits=control_qubits + target_qubits, + inplace=True, + ) + + case _: + raise LatticeException( + f"Unsupported lattice encoding: {self.lattice.get_encoding()}" + ) + + if grid_qubit_indices_to_invert: + circuit.x(grid_qubit_indices_to_invert) + + circuit.compose(comparator_circuit, inplace=True) + + return circuit + + def set_ancilla_of_point_state( + self, + points_data: List[Tuple[ReflectionPoint, List[int]]], + ignore_velocity_data: bool, + ) -> QuantumCircuit: + """ + Sets the state of the obstacle ancilla qubit of a given gridpoint, conditioned on the velocity profile. + + Parameters + ---------- + points_data : List[Tuple[ReflectionPoint, List[int]]] + The tuple of gridpoint and velocity profile to set the ancilla state for. + ignore_velocity_data : bool + Whether to ignore the velocity data. Setting this to ``True`` will flip the state of the ancilla qubit based on position alone. + + Returns + ------- + QuantumCircuit + The circuit that sets the appropriate state on the object ancilla qubit. + """ + circuit = self.lattice.circuit.copy() + + for point, velocities in points_data: + grid_qubit_indices_to_invert = [ + self.lattice.grid_index(0)[0] + qubit + for qubit in point.qubits_to_invert + ] + if grid_qubit_indices_to_invert: + circuit.x(grid_qubit_indices_to_invert) + + match self.lattice.get_encoding(): + case ABEncodingType.AB: + velocity_data = ( + [ + [ + self.lattice.velocity_index()[0] + qubit + for qubit in get_qubits_to_invert( + velocity_index, self.lattice.num_velocity_qubits + ) + ] + for velocity_index in velocities + ] + if not ignore_velocity_data + else [[]] + ) + + # Reset the state for each velocity we care about: + for velocity_qubit_indices_to_invert in velocity_data: + if velocity_qubit_indices_to_invert: + circuit.x(velocity_qubit_indices_to_invert) + + control_qubits = ( + self.lattice.grid_index() + + ( + self.lattice.velocity_index() + if not ignore_velocity_data + else [] + ) # The reset step is additionally controlled on the velocity register + ) + + target_qubits = self.lattice.ancillae_obstacle_index(0) + + circuit.compose( + MCMTGate( + XGate(), + len(control_qubits), + len(target_qubits), + ), + qubits=control_qubits + target_qubits, + inplace=True, + ) + if velocity_qubit_indices_to_invert: + circuit.x(velocity_qubit_indices_to_invert) + case ABEncodingType.OH: + if ignore_velocity_data: + circuit.compose( + MCMTGate( + XGate(), + len(self.lattice.grid_index()), + len(self.lattice.ancillae_obstacle_index(0)), + ), + qubits=self.lattice.grid_index() + + self.lattice.ancillae_obstacle_index(0), + inplace=True, + ) + else: + for v in velocities: + control_qubits = ( + self.lattice.grid_index() + + ( + [self.lattice.velocity_index()[v]] + ) # Only one velocity control + ) + + target_qubits = self.lattice.ancillae_obstacle_index(0) + + circuit.compose( + MCMTGate( + XGate(), + len(control_qubits), + len(target_qubits), + ), + qubits=control_qubits + target_qubits, + inplace=True, + ) + case _: + raise LatticeException( + f"Unsupported lattice encoding: {self.lattice.get_encoding()}" + ) + if grid_qubit_indices_to_invert: + circuit.x(grid_qubit_indices_to_invert) + + return circuit + + def permute_and_stream(self) -> QuantumCircuit: + """ + Performs the permutation of basis states that implements bounceback reflection in the amplitude-based encoding. + + Returns + ------- + QuantumCircuit + The permutation acting on only the velocity register. + """ + circuit = self.lattice.circuit.copy() + + # Permute the velocities according to reflection rules + circuit.compose( + ABReflectionPermutation( + self.lattice.num_velocity_qubits, + self.lattice.discretization, + self.lattice.get_encoding(), + self.logger, + ) + .circuit.control(1) + .decompose(), + qubits=self.lattice.ancillae_obstacle_index() + + self.lattice.velocity_index(), + inplace=True, + ) + + circuit.compose( + ABStreamingOperator( + self.lattice, self.lattice.ancillae_obstacle_index(), self.logger + ).circuit, + inplace=True, + ) + + return circuit + + @override + def __str__(self) -> str: + return f"[Operator ABStreaming with lattice {self.lattice}]" + + +class ABReflectionPermutation(LBMPrimitive): + """ + Permutes velocity state to implement reflection in the amplitude-based encoding for :math:`D_dQ_q` discretizations. + + Example usage: + + .. plot:: + :include-source: + + from qlbm.components.ab import ABEncodingType, ABReflectionPermutation + from qlbm.lattice import LatticeDiscretization + + ABReflectionPermutation(4, LatticeDiscretization.D2Q9, ABEncodingType.AB).draw("mpl") + + """ + + num_qubits: int + """ + The number of qubits that encode the velocity state. + """ + + discretization: LatticeDiscretization + """ + The lattice discretization the permutation adheres to. + """ + + encoding: ABEncodingType + """ + The type of encoding to permute for. + """ + + def __init__( + self, + num_qubits: int, + discretization: LatticeDiscretization, + encoding: ABEncodingType, + logger: Logger = getLogger("qlbm"), + ) -> None: + super().__init__(logger) + + self.num_qubits = num_qubits + self.discretization = discretization + self.encoding = encoding + + self.logger.info(f"Creating circuit {str(self)}...") + circuit_creation_start_time = perf_counter_ns() + self.circuit = self.create_circuit() + self.logger.info( + f"Creating circuit {str(self)} took {perf_counter_ns() - circuit_creation_start_time} (ns)" + ) + + @override + def create_circuit(self) -> QuantumCircuit: + if self.discretization == LatticeDiscretization.D2Q9: + return self.__create_circuit_d2q9() + + raise LatticeException("AB reflection only currently supported in D2Q9") + + def __create_circuit_d2q9(self): + circuit = QuantumCircuit(self.num_qubits) + match self.encoding: + case ABEncodingType.OH: + circuit.swap(1, 3) + circuit.swap(2, 4) + circuit.swap(5, 7) + circuit.swap(6, 8) + + case ABEncodingType.AB: + # 1 <-> 3 + circuit.x([0, 1]) + circuit.mcx([0, 1, 3], 2) + circuit.x([0, 1]) + + # 2 <-> 4 + circuit.x([0, 3]) + circuit.cx(1, 2) + circuit.mcx([0, 2, 3], 1) + circuit.cx(1, 2) + circuit.x([0, 3]) + + # 5 <-> 7 + circuit.x(0) + circuit.mcx([0, 1, 3], 2) + circuit.x(0) + + # 6 <-> 8 + circuit.cx(0, 1) + circuit.cx(0, 2) + circuit.x(3) + circuit.mcx([1, 2, 3], 0) + circuit.cx(0, 2) + circuit.cx(0, 1) + circuit.x(3) + + case _: + raise LatticeException(f"Unsupported lattice encoding: {self.encoding}") + + return circuit.reverse_bits() if self.encoding == ABEncodingType.AB else circuit + + @override + def __str__(self) -> str: + return f"[Primitive ABReflectionPermutation with {self.num_qubits} qubits on {self.discretization}]" diff --git a/qlbm/components/ab/streaming.py b/qlbm/components/ab/streaming.py new file mode 100644 index 0000000..745606e --- /dev/null +++ b/qlbm/components/ab/streaming.py @@ -0,0 +1,227 @@ +"""Quantum circuits used for streaming in the :class:`ABQLBM` algorithm.""" + +from logging import Logger, getLogger +from time import perf_counter_ns +from typing import List + +from qiskit import QuantumCircuit +from qiskit.synthesis import synth_qft_full as QFT +from typing_extensions import override + +from qlbm.components.ab.encodings import ABEncodingType +from qlbm.components.base import LBMOperator +from qlbm.components.ms.streaming import PhaseShift +from qlbm.lattice.lattices.base import AmplitudeLattice +from qlbm.lattice.spacetime.properties_base import LatticeDiscretization +from qlbm.tools.exceptions import LatticeException +from qlbm.tools.utils import get_qubits_to_invert + + +class ABStreamingOperator(LBMOperator): + """ + Streaming operator for the :class:`ABQLBM` algorithm. + + Uses a variant of the Draper adder described in :cite:`collisionless`. + The operator works by applying QFTs in parallel to each dimension of the grid, + followed by phase gates that perform incrementation in the Fourier basis, and, finally, + by applying an inverse QFT mapping the qubits back to the computational basis. + + Populations are streamed one after the other in Fourier space + by controlling phase gates on the state of the velocity qubits. + Additional controls qubits can be specified to restrict this operation. + + Example usage: + + .. plot:: + :include-source: + + from qlbm.components.ab import ABStreamingOperator + from qlbm.lattice import ABLattice + + lattice = ABLattice( + { + "lattice": {"dim": {"x": 4, "y": 8}, "velocities": "d2q9"}, + "geometry": [], + } + ) + + ABStreamingOperator(lattice).draw("mpl") + + """ + + lattice: AmplitudeLattice + """The lattice to construct the component for.""" + + additional_control_qubit_indices: List[int] + """The qubits (if any) that streaming should be controlled over. + This makes the operator useful for the application of boundary conditions. + Controls need only be applied to the phase gates and not the QFT blocks.""" + + def __init__( + self, + lattice: AmplitudeLattice, + additional_control_qubit_indices: List[int] = [], + logger: Logger = getLogger("qlbm"), + ) -> None: + super().__init__(lattice, logger) + + self.additional_control_qubit_indices = additional_control_qubit_indices + + self.logger.info(f"Creating circuit {str(self)}...") + circuit_creation_start_time = perf_counter_ns() + self.circuit = self.create_circuit() + self.logger.info( + f"Creating circuit {str(self)} took {perf_counter_ns() - circuit_creation_start_time} (ns)" + ) + + @override + def create_circuit(self) -> QuantumCircuit: + if self.lattice.discretization == LatticeDiscretization.D1Q3: + return self.__create_circuit_d1q3() + + if self.lattice.discretization == LatticeDiscretization.D2Q9: + return self.__create_circuit_d2q9() + + raise LatticeException("ABE only currently supported in D1Q3 and D2Q9") + + def __create_circuit_d1q3(self): + # TODO Remove? + # TODO add geometry + circuit = self.lattice.circuit.copy() + + circuit.compose( + QFT(self.lattice.num_grid_qubits), + qubits=self.lattice.grid_index(), + inplace=True, + ) + + # 01 streaming in the positive direction + circuit.x(self.lattice.velocity_index()[0]) + + # Controlled Phase Gates for the positive direction + circuit.compose( + PhaseShift( + num_qubits=len(self.lattice.grid_index()), + positive=True, + logger=self.logger, + ) + .circuit.control(2) + .decompose(), + qubits=self.lattice.velocity_index() + self.lattice.grid_index(), + inplace=True, + ) + + # 10 Streaming in the negative direction (and resetting the previous state prep) + circuit.x(self.lattice.velocity_index()) + + circuit.compose( + PhaseShift( + num_qubits=len(self.lattice.grid_index()), + positive=False, # Negative this time + logger=self.logger, + ) + .circuit.control(2) + .decompose(), + qubits=self.lattice.velocity_index() + self.lattice.grid_index(), + inplace=True, + ) + + # Undo the second state prep + circuit.x(self.lattice.velocity_index()[1]) + + # Inverse QFT to return the grid to the computational basis + circuit.compose( + QFT(self.lattice.num_grid_qubits, inverse=True), + qubits=self.lattice.grid_index(), + inplace=True, + ) + + return circuit + + def __create_circuit_d2q9(self): + circuit = self.lattice.circuit.copy() + + dim_indices = [ + [ + [1, 5, 8], # f1, f5, f8 x <- x + 1 + [3, 6, 7], # f3, f6, f7 x <- x - 1 + ], + [ + [2, 5, 6], # f2, f5, f6 y <- y + 1 + [4, 7, 8], # f4, f7, f8 y <- y + 1 + ], + ] + + for dim, dim_population_to_update in enumerate(dim_indices): + circuit.compose( + QFT(len(self.lattice.grid_index(dim))), + qubits=self.lattice.grid_index(dim), + inplace=True, + ) + + for direction, indices in enumerate(dim_population_to_update): + positive = bool(1 - direction) + + for index in indices: + match self.lattice.get_encoding(): + case ABEncodingType.OH: + circuit.compose( + PhaseShift( + num_qubits=len(self.lattice.grid_index(dim)), + positive=positive, + logger=self.logger, + ) + .circuit.control( + 1 + len(self.additional_control_qubit_indices) + ) + .decompose(), + qubits=self.additional_control_qubit_indices + + [self.lattice.velocity_index()[index]] + + self.lattice.grid_index(dim), + inplace=True, + ) + case ABEncodingType.AB: + velocity_inversion_qubits = [ + self.lattice.num_grid_qubits + q + for q in get_qubits_to_invert( + index, self.lattice.num_velocity_qubits + ) + ] + if velocity_inversion_qubits: + circuit.x(velocity_inversion_qubits) + + circuit.compose( + PhaseShift( + num_qubits=len(self.lattice.grid_index(dim)), + positive=positive, + logger=self.logger, + ) + .circuit.control( + self.lattice.num_velocity_qubits + + len(self.additional_control_qubit_indices) + ) + .decompose(), + qubits=self.additional_control_qubit_indices + + self.lattice.velocity_index() + + self.lattice.grid_index(dim), + inplace=True, + ) + + if velocity_inversion_qubits: + circuit.x(velocity_inversion_qubits) + case _: + raise LatticeException( + f"Unsupported lattice encoding: {self.lattice.get_encoding()}" + ) + + circuit.compose( + QFT(len(self.lattice.grid_index(dim)), inverse=True), + qubits=self.lattice.grid_index(dim), + inplace=True, + ) + + return circuit + + @override + def __str__(self) -> str: + return f"[Operator ABStreaming with lattice {self.lattice}]" diff --git a/qlbm/components/base.py b/qlbm/components/base.py index fc3cf73..ca34b48 100644 --- a/qlbm/components/base.py +++ b/qlbm/components/base.py @@ -9,7 +9,7 @@ from qiskit.qasm3 import dump as dump_qasm3 from typing_extensions import override -from qlbm.lattice import CollisionlessLattice, Lattice +from qlbm.lattice import Lattice, MSLattice from qlbm.lattice.lattices.lqlga_lattice import LQLGALattice from qlbm.lattice.lattices.spacetime_lattice import SpaceTimeLattice @@ -184,27 +184,27 @@ def __init__( self.lattice = lattice -class CQLBMOperator(LBMOperator): +class MSOperator(LBMOperator): """ - Specialization of the :class:`.LBMOperator` operator class for the Collisionless Quantum Transport Method algorithm by :cite:t:`collisionless`. + Specialization of the :class:`.LBMOperator` operator class for the Multi-Speed Collisionless Quantum Lattice Boltzmann Method algorithm by :cite:t:`collisionless`. Specializaitons of this class infer their properties - based on a :class:`.CollisionlessLattice`. + based on a :class:`.MSLattice`. ========================= ====================================================================== Attribute Summary ========================= ====================================================================== :attr:`circuit` The :class:`.qiskit.QuantumCircuit` of the operator. - :attr:`lattice` The :class:`.CollisionlessLattice` based on which the properties of the operator are inferred. + :attr:`lattice` The :class:`.MSLattice` based on which the properties of the operator are inferred. :attr:`logger` The performance logger, by default ``getLogger("qlbm")`` ========================= ====================================================================== """ - lattice: CollisionlessLattice + lattice: MSLattice def __init__( self, - lattice: CollisionlessLattice, + lattice: MSLattice, logger: Logger = getLogger("qlbm"), ) -> None: super().__init__(lattice, logger) diff --git a/qlbm/components/common/__init__.py b/qlbm/components/common/__init__.py index 75ca482..139a116 100644 --- a/qlbm/components/common/__init__.py +++ b/qlbm/components/common/__init__.py @@ -1,13 +1,12 @@ """Common primitives used for multiple encodings.""" from .cbse_collision import EQCCollisionOperator, EQCPermutation, EQCRedistribution -from .primitives import ( - EmptyPrimitive, -) +from .primitives import EmptyPrimitive, HammingWeightAdder __all__ = [ "EmptyPrimitive", "EQCCollisionOperator", "EQCPermutation", "EQCRedistribution", + "HammingWeightAdder", ] diff --git a/qlbm/components/common/cbse_collision/cbse_redistribution.py b/qlbm/components/common/cbse_collision/cbse_redistribution.py index 25d66fa..6aeab84 100644 --- a/qlbm/components/common/cbse_collision/cbse_redistribution.py +++ b/qlbm/components/common/cbse_collision/cbse_redistribution.py @@ -6,9 +6,9 @@ import numpy as np from qiskit import QuantumCircuit -from qiskit.quantum_info import Operator from qlbm.components.base import LBMPrimitive +from qlbm.components.common.primitives import TruncatedQFT from qlbm.lattice.eqc.eqc import EquivalenceClass from qlbm.lattice.spacetime.properties_base import LatticeDiscretizationProperties from qlbm.tools.utils import is_two_pow @@ -108,19 +108,7 @@ def create_circuit(self): 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() - - redistribution_circuit.append(op, list(range(nq))) + redistribution_circuit = TruncatedQFT(nq, n).circuit circuit.compose( redistribution_circuit.control( diff --git a/qlbm/components/common/primitives.py b/qlbm/components/common/primitives.py index 79901a1..cb2a4cf 100644 --- a/qlbm/components/common/primitives.py +++ b/qlbm/components/common/primitives.py @@ -4,8 +4,12 @@ from time import perf_counter_ns from typing import List, Tuple +import numpy as np +from numpy import pi from qiskit import QuantumCircuit from qiskit.circuit.library import MCMTGate, XGate +from qiskit.quantum_info import Operator +from qiskit.synthesis import synth_qft_full as QFT from typing_extensions import override from qlbm.components.base import LBMPrimitive @@ -22,7 +26,7 @@ class EmptyPrimitive(LBMPrimitive): ========================= ====================================================================== Attribute Summary ========================= ====================================================================== - :attr:`lattice` The :class:`.CollisionlessLattice` or :class:`.SpaceTimeLattice` based on which the number of qubits is inferred. + :attr:`lattice` The :class:`.MSLattice` or :class:`.SpaceTimeLattice` based on which the number of qubits is inferred. :attr:`logger` The performance logger, by default ``getLogger("qlbm")``. ========================= ====================================================================== """ @@ -106,3 +110,145 @@ def create_circuit(self) -> QuantumCircuit: @override def __str__(self) -> str: return f"[Primitive MCSwap with lattice {self.lattice}]" + + +class HammingWeightAdder(LBMPrimitive): + """ + QFT-based Hamming Weight adder. + + This primitive adds the hamming weight (number of 1s) in a given register :math:`x` + to the binary-encoded value of a second register :math:`y`. + """ + + x_register_size: int + """ + The size of the register encoding the hamming weight value to add. + """ + + y_register_size: int + """ + The size of the register to which the hamming weight is added. + """ + + def __init__( + self, + x_register_size: int, + y_register_size: int, + logger: Logger = getLogger("qlbm"), + ): + super().__init__(logger) + self.x_register_size = x_register_size + self.y_register_size = y_register_size + + self.logger.info(f"Creating circuit {str(self)}...") + circuit_creation_start_time = perf_counter_ns() + self.circuit = self.create_circuit() + self.logger.info( + f"Creating circuit {str(self)} took {perf_counter_ns() - circuit_creation_start_time} (ns)" + ) + + @override + def create_circuit(self) -> QuantumCircuit: + circuit = QuantumCircuit(self.x_register_size + self.y_register_size) + + circuit.compose( + QFT(self.y_register_size), + inplace=True, + qubits=list( + range(self.x_register_size, self.x_register_size + self.y_register_size) + ), + ) + + angles = np.zeros(self.y_register_size) + for i in range(self.y_register_size): + angles[i] = 2 * pi / (2 ** (self.y_register_size - i)) + + for xi in range(self.x_register_size): + for k, yi in enumerate(range(self.y_register_size)): + circuit.cp(angles[k], xi, self.x_register_size + yi) + + circuit.compose( + QFT(self.y_register_size, inverse=True), + inplace=True, + qubits=list( + range(self.x_register_size, self.x_register_size + self.y_register_size) + ), + ) + + return circuit + + @override + def __str__(self): + return f"[Primitive HWAdder with with register size {self.x_register_size} and {self.y_register_size}]" + + +class TruncatedQFT(LBMPrimitive): + r"""Truncated Quantum Fourier Transform primitive used to create an equal magnitude superposition. + + For a superposition of the first :math:`k` basis states encoded in :math:`n` qubits, + the operator consists of discrete fourier transform block of size :math:`k\times k`, + padded with :math:`2^n - k` :math:`1`s on the main diagonal. + The rationale and properties of this operator are described in :cite:`spacetime2`. + This primitive is used in both amplitude-based and computational basis state encodings. + In the :class:`ABInitialConditions`, it creates an equal magnitude superposition over the velocity space. + In the :class:`EQCRedistribution`, the superposition is over all basis states with an equivalent mass and momenta. + + Example usage: + + .. plot:: + :include-source: + + from qlbm.components.common import TruncatedQFT + + TruncatedQFT(4, 7).decompose(reps=2).draw("mpl") + """ + + num_qubits: int + """The number of qubits the operator acts on.""" + + dft_size: int + """The size of the discrete Fourier transform block.""" + + def __init__( + self, + num_qubits: int, + dft_size: int, + logger: Logger = getLogger("qlbm"), + ): + super().__init__(logger) + self.num_qubits = num_qubits + self.dft_size = dft_size + + self.logger.info(f"Creating circuit {str(self)}...") + circuit_creation_start_time = perf_counter_ns() + self.circuit = self.create_circuit() + self.logger.info( + f"Creating circuit {str(self)} took {perf_counter_ns() - circuit_creation_start_time} (ns)" + ) + + @override + def create_circuit(self): + circuit = QuantumCircuit(self.num_qubits) + + QFT = np.array( + [ + [ + np.exp(2j * np.pi * i * j / self.dft_size) / np.sqrt(self.dft_size) + for j in range(self.dft_size) + ] + for i in range(self.dft_size) + ] + ) + + U = np.eye(2**self.num_qubits, dtype=complex) + U[: self.dft_size, : self.dft_size] = QFT + op = Operator(U) + assert op.is_unitary() + + circuit.append(op, list(range(self.num_qubits))) + + return circuit + + @override + def __str__(self): + return f"[Primitive TuncatedQFT({self.num_qubits}, {self.dft_size})]" diff --git a/qlbm/components/cqlbm.py b/qlbm/components/cqlbm.py new file mode 100644 index 0000000..4ad4290 --- /dev/null +++ b/qlbm/components/cqlbm.py @@ -0,0 +1,90 @@ +"""The end-to-end algorithm of the Collisionless Quantum Lattice Boltzmann Algorithm, or Quantum Transport Method first introduced in :cite:t:`collisionless` and later extended in :cite:t:`qmem`. + +This is a common entrypoint that supports implementations based on the :class:`.MSLattice` and :class:`.ABLattice`. + +Implementations can be found in the :class:`MSQLBM` and :class:`.ABQLBM`, respectively. +""" + +from logging import Logger, getLogger +from time import perf_counter_ns +from typing import cast + +from typing_extensions import override + +from qlbm.components.ab.ab import ABQLBM +from qlbm.components.base import LBMAlgorithm +from qlbm.components.ms.msqlbm import MSQLBM +from qlbm.lattice import MSLattice +from qlbm.lattice.lattices.ab_lattice import ABLattice +from qlbm.lattice.lattices.base import AmplitudeLattice +from qlbm.tools.exceptions import LatticeException + + +class CQLBM(LBMAlgorithm): + """The end-to-end algorithm of the Collisionless Quantum Lattice Boltzmann Algorithm first introduced in :cite:t:`collisionless` and later extended in :cite:t:`qmem`. + + Implementations based on lattices with the DdQq discretization use the :class:`.ABQLBM`: + + .. plot:: + :include-source: + + from qlbm.components import CQLBM + from qlbm.lattice import ABLattice + + lattice = ABLattice( + { + "lattice": {"dim": {"x": 4, "y": 4}, "velocities": "d2q9"}, + "geometry": [], + } + ) + + CQLBM(lattice).draw("mpl") + + Implementations where the number of velocities is defined per dimension delegate to the :class:`.MSQLBM`. + + .. plot:: + :include-source: + + from qlbm.components import CQLBM + from qlbm.lattice import MSLattice + + lattice = MSLattice( + { + "lattice": {"dim": {"x": 4, "y": 4}, "velocities": {"x": 4, "y": 4}}, + "geometry": [], + } + ) + + CQLBM(lattice).draw("mpl") + + """ + + def __init__( + self, + lattice: AmplitudeLattice, + logger: Logger = getLogger("qlbm"), + ) -> None: + super().__init__(lattice, logger) + self.lattice: AmplitudeLattice = 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): + if isinstance(self.lattice, MSLattice): + return MSQLBM(cast(MSLattice, self.lattice), self.logger).circuit + elif isinstance(self.lattice, ABLattice): + return ABQLBM(cast(ABLattice, self.lattice), self.logger).circuit + else: + raise LatticeException( + f"CQLBM does not support lattices of type {type(self.lattice)}" + ) + + @override + def __str__(self) -> str: + return f"[Algorithm CQLBM with lattice {self.lattice}]" diff --git a/qlbm/components/lqlga/initial.py b/qlbm/components/lqlga/initial.py index c73f5c0..835e47d 100644 --- a/qlbm/components/lqlga/initial.py +++ b/qlbm/components/lqlga/initial.py @@ -8,6 +8,7 @@ from qlbm.components.base import LBMPrimitive from qlbm.lattice.lattices.lqlga_lattice import LQLGALattice +from qlbm.tools.utils import flatten class LQGLAInitialConditions(LBMPrimitive): @@ -90,3 +91,81 @@ def create_circuit(self): @override def __str__(self): return f"[Primitive LQGLAInitialConditions on lattice={self.lattice}, grid_data={self.grid_data})]" + + +class LQGLAAveragedInitialConditions(LBMPrimitive): + """ + Primitive for setting initial conditions in the :class:`.LQLGA` algorithm. + + This operator creates an equal magnitude superposition over a set of gridpoints. + This is equivalent to starting the QLGA algorithm in all possible configurations + over the given set of gridpoints. + + + Example usage: + + .. plot:: + :include-source: + + from qlbm.lattice import LQLGALattice + from qlbm.components.lqlga import LQGLAAveragedInitialConditions + + lattice = LQLGALattice( + { + "lattice": { + "dim": {"x": 5}, + "velocities": "D1Q3", + }, + "geometry": [], + }, + ) + initial_conditions = LQGLAAveragedInitialConditions(lattice, [0, 2, 3]) + initial_conditions.draw("mpl") + + """ + + gridpoints: List[int] + """The gridpoints to create the uniform superposition over.""" + + def __init__( + self, + lattice: LQLGALattice, + gridpoints: List[int], + logger: Logger = getLogger("qlbm"), + ): + super().__init__(logger) + + self.lattice = lattice + self.gridpoints = gridpoints + + 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() + + circuit.h( + flatten( + [ + list( + range( + gp * self.lattice.num_velocities_per_point, + gp * self.lattice.num_velocities_per_point + + self.lattice.num_velocities_per_point, + ) + ) + for gp in self.gridpoints + ] + ) + ) + + return circuit + + @override + def __str__(self): + return f"[Primitive LQGLAAveragedInitialConditions on lattice={self.lattice}, gps={self.gridpoints})]" diff --git a/qlbm/components/collisionless/__init__.py b/qlbm/components/ms/__init__.py similarity index 69% rename from qlbm/components/collisionless/__init__.py rename to qlbm/components/ms/__init__.py index fd04d41..8652182 100644 --- a/qlbm/components/collisionless/__init__.py +++ b/qlbm/components/ms/__init__.py @@ -1,23 +1,23 @@ -"""Modular qlbm quantum circuit components for the CQLBM algorithm :cite:p:`collisionless`.""" +"""Modular qlbm quantum circuit components for the MSQLBM algorithm :cite:p:`collisionless`.""" from .bounceback_reflection import ( BounceBackReflectionOperator, BounceBackWallComparator, ) -from .cqlbm import CQLBM +from .msqlbm import MSQLBM from .primitives import ( - CollisionlessInitialConditions, - CollisionlessInitialConditions3DSlim, Comparator, ComparatorMode, EdgeComparator, GridMeasurement, + MSInitialConditions, + MSInitialConditions3DSlim, SpeedSensitiveAdder, ) from .specular_reflection import SpecularReflectionOperator, SpecularWallComparator from .streaming import ( - CollisionlessStreamingOperator, ControlledIncrementer, + MSStreamingOperator, PhaseShift, SpeedSensitivePhaseShift, StreamingAncillaPreparation, @@ -31,14 +31,14 @@ "ControlledIncrementer", "GridMeasurement", "EdgeComparator", - "CollisionlessInitialConditions", - "CollisionlessInitialConditions3DSlim", + "MSInitialConditions", + "MSInitialConditions3DSlim", "PhaseShift", "SpeedSensitivePhaseShift", - "CollisionlessStreamingOperator", + "MSStreamingOperator", "SpecularReflectionOperator", "SpecularWallComparator", "BounceBackReflectionOperator", "BounceBackWallComparator", - "CQLBM", + "MSQLBM", ] diff --git a/qlbm/components/collisionless/bounceback_reflection.py b/qlbm/components/ms/bounceback_reflection.py similarity index 94% rename from qlbm/components/collisionless/bounceback_reflection.py rename to qlbm/components/ms/bounceback_reflection.py index 7ca3571..5acc32b 100644 --- a/qlbm/components/collisionless/bounceback_reflection.py +++ b/qlbm/components/ms/bounceback_reflection.py @@ -8,14 +8,14 @@ from qiskit.circuit.library import MCMTGate, XGate from typing_extensions import override -from qlbm.components.base import CQLBMOperator, LBMPrimitive -from qlbm.components.collisionless.primitives import ( +from qlbm.components.base import LBMPrimitive, MSOperator +from qlbm.components.ms.primitives import ( Comparator, ComparatorMode, ) -from qlbm.components.collisionless.specular_reflection import SpecularWallComparator -from qlbm.lattice import CollisionlessLattice -from qlbm.lattice.geometry.encodings.collisionless import ( +from qlbm.components.ms.specular_reflection import SpecularWallComparator +from qlbm.lattice import MSLattice +from qlbm.lattice.geometry.encodings.ms import ( ReflectionPoint, ReflectionResetEdge, ReflectionWall, @@ -38,7 +38,7 @@ class BounceBackWallComparator(LBMPrimitive): ========================= ====================================================================== Attribute Summary ========================= ====================================================================== - :attr:`lattice` The :class:`.CollisionlessLattice` based on which the properties of the operator are inferred. + :attr:`lattice` The :class:`.MSLattice` based on which the properties of the operator are inferred. :attr:`wall` The :class:`.ReflectionWall` encoding the range spanned by the wall. :attr:`logger` The performance logger, by default ``getLogger("qlbm")``. ========================= ====================================================================== @@ -48,11 +48,11 @@ class BounceBackWallComparator(LBMPrimitive): .. plot:: :include-source: - from qlbm.components.collisionless import BounceBackWallComparator - from qlbm.lattice import CollisionlessLattice + from qlbm.components.ms import BounceBackWallComparator + from qlbm.lattice import MSLattice # Build an example lattice - lattice = CollisionlessLattice( + lattice = MSLattice( { "lattice": {"dim": {"x": 8, "y": 8}, "velocities": {"x": 4, "y": 4}}, "geometry": [{"shape":"cuboid", "x": [5, 6], "y": [1, 2], "boundary": "bounceback"}], @@ -67,7 +67,7 @@ class BounceBackWallComparator(LBMPrimitive): def __init__( self, - lattice: CollisionlessLattice, + lattice: MSLattice, wall: ReflectionWall, logger: Logger = getLogger("qlbm"), ) -> None: @@ -133,7 +133,7 @@ def __str__(self) -> str: return f"[Primitive BounceBackWallComparator on wall={self.wall}]" -class BounceBackReflectionOperator(CQLBMOperator): +class BounceBackReflectionOperator(MSOperator): """ Operator implementing the 2D and 3D Bounce-Back (BB) boundary conditions as described in :cite:t:`qmem`. @@ -150,7 +150,7 @@ class BounceBackReflectionOperator(CQLBMOperator): ========================= ====================================================================== Attribute Summary ========================= ====================================================================== - :attr:`lattice` The :class:`.CollisionlessLattice` based on which the properties of the operator are inferred. + :attr:`lattice` The :class:`.MSLattice` based on which the properties of the operator are inferred. :attr:`blocks` A list of :class:`.Block` objects for which to generate the BB boundary condition circuits. :attr:`logger` The performance logger, by default ``getLogger("qlbm")``. ========================= ====================================================================== @@ -160,11 +160,11 @@ class BounceBackReflectionOperator(CQLBMOperator): .. plot:: :include-source: - from qlbm.components.collisionless import BounceBackReflectionOperator - from qlbm.lattice import CollisionlessLattice + from qlbm.components.ms import BounceBackReflectionOperator + from qlbm.lattice import MSLattice # Build an example lattice - lattice = CollisionlessLattice( + lattice = MSLattice( { "lattice": {"dim": {"x": 8, "y": 8}, "velocities": {"x": 4, "y": 4}}, "geometry": [{"shape":"cuboid", "x": [5, 6], "y": [1, 2], "boundary": "bounceback"}], @@ -176,7 +176,7 @@ class BounceBackReflectionOperator(CQLBMOperator): def __init__( self, - lattice: CollisionlessLattice, + lattice: MSLattice, blocks: List[Block], logger: Logger = getLogger("qlbm"), ) -> None: @@ -494,7 +494,7 @@ def flip_and_stream( ): """Flips the velocity direction qubit controlled on the ancilla obstacle qubit, before performing streaming. - Unlike in the regular :class:`.CollisionlessStreamingOperator`, the :class:`.ControlledIncrementer` + Unlike in the regular :class:`.MSStreamingOperator`, the :class:`.ControlledIncrementer` phase shift circuit is additionally controlled on the ancilla obstacle qubit, which ensures that only particles whose grid position gets incremented (decremented) are those that have streamed inside the solid domain in this CFL time step. diff --git a/qlbm/components/collisionless/cqlbm.py b/qlbm/components/ms/msqlbm.py similarity index 78% rename from qlbm/components/collisionless/cqlbm.py rename to qlbm/components/ms/msqlbm.py index b9b54b5..52b02b0 100644 --- a/qlbm/components/collisionless/cqlbm.py +++ b/qlbm/components/ms/msqlbm.py @@ -7,43 +7,43 @@ from typing_extensions import override from qlbm.components.base import LBMAlgorithm -from qlbm.lattice import CollisionlessLattice +from qlbm.lattice import MSLattice 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 from .specular_reflection import SpecularReflectionOperator -from .streaming import CollisionlessStreamingOperator, StreamingAncillaPreparation +from .streaming import MSStreamingOperator, StreamingAncillaPreparation -class CQLBM(LBMAlgorithm): - """The end-to-end algorithm of the Collisionless Quantum Lattice Boltzmann Algorithm first introduced in :cite:t:`collisionless` and later extended in :cite:t:`qmem`. +class MSQLBM(LBMAlgorithm): + """The end-to-end algorithm of the Multi-Speed Collisionless Quantum Lattice Boltzmann Algorithm first introduced in :cite:t:`collisionless` and later extended in :cite:t:`qmem`. This implementation supports 2D and 3D simulations with with cuboid objects with either bounce-back or specular reflection boundary conditions. The algorithm is composed of three steps that are repeated according to a CFL counter: - #. Streaming performed by the :class:`.CollisionlessStreamingOperator` increments or decrements the positions of particles on the grid. - #. :class:`.BounceBackReflectionOperator` and :class:`.SpecularReflectionOperator` reflect the particles that come in contact with :class:`.Block` obstacles encoded in the :class:`.CollisionlessLattice`. + #. Streaming performed by the :class:`.MSStreamingOperator` increments or decrements the positions of particles on the grid. + #. :class:`.BounceBackReflectionOperator` and :class:`.SpecularReflectionOperator` reflect the particles that come in contact with :class:`.Block` obstacles encoded in the :class:`.MSLattice`. #. The :class:`.StreamingAncillaPreparation` resets the state of the ancilla qubits for the next CFL counter substep. ========================= ====================================================================== Attribute Summary ========================= ====================================================================== - :attr:`lattice` The :class:`.CollisionlessLattice` based on which the properties of the operator are inferred. + :attr:`lattice` The :class:`.MSLattice` based on which the properties of the operator are inferred. :attr:`logger` The performance logger, by default ``getLogger("qlbm")``. ========================= ====================================================================== """ def __init__( self, - lattice: CollisionlessLattice, + lattice: MSLattice, logger: Logger = getLogger("qlbm"), ) -> None: super().__init__(lattice, logger) - self.lattice: CollisionlessLattice = lattice + self.lattice: MSLattice = lattice self.logger.info(f"Creating circuit {str(self)}...") circuit_creation_start_time = perf_counter_ns() @@ -63,7 +63,7 @@ def create_circuit(self): for velocities_to_increment in time_series: circuit.compose( - CollisionlessStreamingOperator( + MSStreamingOperator( self.lattice, velocities_to_increment, logger=self.logger, @@ -76,7 +76,7 @@ def create_circuit(self): 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. " + "All shapes with the 'specular' boundary condition must be of type Block for the MSQLBM algorithm. " ) circuit.compose( SpecularReflectionOperator( @@ -87,14 +87,14 @@ def create_circuit(self): inplace=True, ) - if self.lattice.shapes["bounceback"]: - if self.lattice.shapes["specular"]: + for bc in ["bounceback", "specular"]: + if self.lattice.shapes[bc]: if not all( isinstance(shape, Block) for shape in self.lattice.shapes["specular"] ): raise LatticeException( - "All shapes with the 'bounceback' boundary condition must be of type Block for the CQLBM algorithm. " + f"All shapes with the {bc} boundary condition must be cuboids for the MSQLBM algorithm. " ) circuit.compose( BounceBackReflectionOperator( @@ -119,4 +119,4 @@ def create_circuit(self): @override def __str__(self) -> str: - return f"[Algorithm CQLBM with lattice {self.lattice}]" + return f"[Algorithm MSQLBM with lattice {self.lattice}]" diff --git a/qlbm/components/collisionless/primitives.py b/qlbm/components/ms/primitives.py similarity index 89% rename from qlbm/components/collisionless/primitives.py rename to qlbm/components/ms/primitives.py index d9147ee..07a3491 100644 --- a/qlbm/components/collisionless/primitives.py +++ b/qlbm/components/ms/primitives.py @@ -10,9 +10,9 @@ from typing_extensions import override from qlbm.components.base import LBMPrimitive -from qlbm.components.collisionless.streaming import SpeedSensitivePhaseShift -from qlbm.lattice import CollisionlessLattice -from qlbm.lattice.geometry.encodings.collisionless import ReflectionResetEdge +from qlbm.components.ms.streaming import SpeedSensitivePhaseShift +from qlbm.lattice import MSLattice +from qlbm.lattice.geometry.encodings.ms import ReflectionResetEdge from qlbm.tools import flatten @@ -24,7 +24,7 @@ class GridMeasurement(LBMPrimitive): ========================= ====================================================================== Attribute Summary ========================= ====================================================================== - :attr:`lattice` The :class:`.CollisionlessLattice` based on which the properties of the operator are inferred. + :attr:`lattice` The :class:`.MSLattice` based on which the properties of the operator are inferred. :attr:`logger` The performance logger, by default ``getLogger("qlbm")``. ========================= ====================================================================== @@ -33,11 +33,11 @@ class GridMeasurement(LBMPrimitive): .. plot:: :include-source: - from qlbm.components.collisionless import GridMeasurement - from qlbm.lattice import CollisionlessLattice + from qlbm.components.ms import GridMeasurement + from qlbm.lattice import MSLattice # Build an example lattice - lattice = CollisionlessLattice({ + lattice = MSLattice({ "lattice": { "dim": { "x": 8, @@ -64,7 +64,7 @@ class GridMeasurement(LBMPrimitive): def __init__( self, - lattice: CollisionlessLattice, + lattice: MSLattice, logger: Logger = getLogger("qlbm"), ) -> None: super().__init__(logger) @@ -94,11 +94,11 @@ def create_circuit(self) -> QuantumCircuit: @override def __str__(self) -> str: - return f"[Primitive InitialConditions with lattice {self.lattice}]" + return f"[Primitive DVGridMeasurement with lattice {self.lattice}]" -class CollisionlessInitialConditions(LBMPrimitive): - """A primitive that creates the quantum circuit to prepare the flow field in its initial conditions. +class MSInitialConditions(LBMPrimitive): + """A primitive that creates the quantum circuit to prepare the flow field in its initial conditions for the :class:`.MSLattice`. The initial conditions create a quantum state spanning half the grid in the x-axis, and the entirety of the y (and z)-axes (if 3D). @@ -107,7 +107,7 @@ class CollisionlessInitialConditions(LBMPrimitive): ========================= ====================================================================== Attribute Summary ========================= ====================================================================== - :attr:`lattice` The :class:`.CollisionlessLattice` based on which the properties of the operator are inferred. + :attr:`lattice` The :class:`.MSLattice` based on which the properties of the operator are inferred. :attr:`logger` The performance logger, by default ``getLogger("qlbm")``. ========================= ====================================================================== @@ -116,11 +116,11 @@ class CollisionlessInitialConditions(LBMPrimitive): .. plot:: :include-source: - from qlbm.components.collisionless import CollisionlessInitialConditions - from qlbm.lattice import CollisionlessLattice + from qlbm.components.ms import MSInitialConditions + from qlbm.lattice import MSLattice # Build an example lattice - lattice = CollisionlessLattice({ + lattice = MSLattice({ "lattice": { "dim": { "x": 8, @@ -142,12 +142,12 @@ class CollisionlessInitialConditions(LBMPrimitive): }) # Draw the initial conditions circuit - CollisionlessInitialConditions(lattice).draw("mpl") + MSInitialConditions(lattice).draw("mpl") """ def __init__( self, - lattice: CollisionlessLattice, + lattice: MSLattice, logger: Logger = getLogger("qlbm"), ) -> None: super().__init__(logger) @@ -183,7 +183,7 @@ def __str__(self) -> str: return f"[Primitive InitialConditions with lattice {self.lattice}]" -class CollisionlessInitialConditions3DSlim(LBMPrimitive): +class MSInitialConditions3DSlim(LBMPrimitive): r""" A primitive that creates the quantum circuit to prepare the flow field in its initial conditions for 3 dimensions. @@ -196,7 +196,7 @@ class CollisionlessInitialConditions3DSlim(LBMPrimitive): ========================= ====================================================================== Attribute Summary ========================= ====================================================================== - :attr:`lattice` The :class:`.CollisionlessLattice` based on which the properties of the operator are inferred. + :attr:`lattice` The :class:`.MSLattice` based on which the properties of the operator are inferred. :attr:`logger` The performance logger, by default ``getLogger("qlbm")``. ========================= ====================================================================== @@ -205,11 +205,11 @@ class CollisionlessInitialConditions3DSlim(LBMPrimitive): .. plot:: :include-source: - from qlbm.components.collisionless import CollisionlessInitialConditions3DSlim - from qlbm.lattice import CollisionlessLattice + from qlbm.components.ms import MSInitialConditions3DSlim + from qlbm.lattice import MSLattice # Build an example lattice - lattice = CollisionlessLattice({ + lattice = MSLattice({ "lattice": { "dim": { "x": 8, @@ -226,12 +226,12 @@ class CollisionlessInitialConditions3DSlim(LBMPrimitive): }) # Draw the initial conditions circuit - CollisionlessInitialConditions3DSlim(lattice).draw("mpl") + MSInitialConditions3DSlim(lattice).draw("mpl") """ def __init__( self, - lattice: CollisionlessLattice, + lattice: MSLattice, logger: Logger = getLogger("qlbm"), ) -> None: super().__init__(logger) @@ -286,7 +286,7 @@ class ComparatorMode(Enum): class SpeedSensitiveAdder(LBMPrimitive): - r"""A QFT-based incrementer used to perform streaming in the CQLBM algorithm. + r"""A QFT-based incrementer used to perform streaming in the algorithms based on amplitude encodings. Incrementation and decerementation are performed as rotations on grid qubits that have been previously mapped to the Fourier basis. @@ -307,7 +307,7 @@ class SpeedSensitiveAdder(LBMPrimitive): .. plot:: :include-source: - from qlbm.components.collisionless import SpeedSensitiveAdder + from qlbm.components.ms import SpeedSensitiveAdder SpeedSensitiveAdder(4, 1, True).draw("mpl") """ @@ -372,7 +372,7 @@ class Comparator(LBMPrimitive): .. plot:: :include-source: - from qlbm.components.collisionless import Comparator, ComparatorMode + from qlbm.components.ms import Comparator, ComparatorMode # On a 5 qubit register, compare the number 3 Comparator(num_qubits=5, @@ -463,7 +463,7 @@ class EdgeComparator(LBMPrimitive): ========================= ====================================================================== Attribute Summary ========================= ====================================================================== - :attr:`lattice` The :class:`.CollisionlessLattice` based on which the properties of the operator are inferred. + :attr:`lattice` The :class:`.MSLattice` based on which the properties of the operator are inferred. :attr:`logger` The performance logger, by default ``getLogger("qlbm")``. :attr:`edge` The coordinates of the edge within the grid. ========================= ====================================================================== @@ -473,11 +473,11 @@ class EdgeComparator(LBMPrimitive): .. plot:: :include-source: - from qlbm.components.collisionless import EdgeComparator - from qlbm.lattice import CollisionlessLattice + from qlbm.components.ms import EdgeComparator + from qlbm.lattice import MSLattice # Build an example lattice - lattice = CollisionlessLattice( + lattice = MSLattice( { "lattice": { "dim": {"x": 8, "y": 8, "z": 8}, @@ -493,7 +493,7 @@ class EdgeComparator(LBMPrimitive): def __init__( self, - lattice: CollisionlessLattice, + lattice: MSLattice, edge: ReflectionResetEdge, logger: Logger = getLogger("qlbm"), ) -> None: diff --git a/qlbm/components/collisionless/specular_reflection.py b/qlbm/components/ms/specular_reflection.py similarity index 95% rename from qlbm/components/collisionless/specular_reflection.py rename to qlbm/components/ms/specular_reflection.py index b0bfb07..1930ad3 100644 --- a/qlbm/components/collisionless/specular_reflection.py +++ b/qlbm/components/ms/specular_reflection.py @@ -8,20 +8,21 @@ from qiskit.circuit.library import MCMTGate, XGate from typing_extensions import override -from qlbm.components.base import CQLBMOperator, LBMPrimitive -from qlbm.components.collisionless.primitives import ( +from qlbm.components.base import LBMPrimitive, MSOperator +from qlbm.components.ms.primitives import ( Comparator, ComparatorMode, ) from qlbm.lattice import ( - CollisionlessLattice, + MSLattice, ) -from qlbm.lattice.geometry.encodings.collisionless import ( +from qlbm.lattice.geometry.encodings.ms import ( ReflectionPoint, ReflectionResetEdge, ReflectionWall, ) from qlbm.lattice.geometry.shapes.block import Block +from qlbm.lattice.lattices.base import AmplitudeLattice from qlbm.tools.exceptions import CircuitException from qlbm.tools.utils import flatten @@ -39,7 +40,7 @@ class SpecularWallComparator(LBMPrimitive): ========================= ====================================================================== Attribute Summary ========================= ====================================================================== - :attr:`lattice` The :class:`.CollisionlessLattice` based on which the properties of the operator are inferred. + :attr:`lattice` The :class:`.AmplitudeLattice` based on which the properties of the operator are inferred. :attr:`wall` The :class:`.ReflectionWall` encoding the range spanned by the wall. :attr:`logger` The performance logger, by default ``getLogger("qlbm")``. ========================= ====================================================================== @@ -49,11 +50,11 @@ class SpecularWallComparator(LBMPrimitive): .. plot:: :include-source: - from qlbm.components.collisionless import SpecularWallComparator - from qlbm.lattice import CollisionlessLattice + from qlbm.components.ms import SpecularWallComparator + from qlbm.lattice import MSLattice # Build an example lattice - lattice = CollisionlessLattice( + lattice = MSLattice( { "lattice": {"dim": {"x": 8, "y": 8}, "velocities": {"x": 4, "y": 4}}, "geometry": [{"shape":"cuboid", "x": [5, 6], "y": [1, 2], "boundary": "specular"}], @@ -68,7 +69,7 @@ class SpecularWallComparator(LBMPrimitive): def __init__( self, - lattice: CollisionlessLattice, + lattice: AmplitudeLattice, wall: ReflectionWall, logger: Logger = getLogger("qlbm"), ) -> None: @@ -128,7 +129,7 @@ def __str__(self) -> str: return f"[Primitive SpecularWallComparator on wall={self.wall}]" -class SpecularReflectionOperator(CQLBMOperator): +class SpecularReflectionOperator(MSOperator): r""" Operator implementing the 2D and 3D Specular Reflection (SR) boundary conditions as described :cite:t:`collisionless`. @@ -145,7 +146,7 @@ class SpecularReflectionOperator(CQLBMOperator): ========================= ====================================================================== Attribute Summary ========================= ====================================================================== - :attr:`lattice` The :class:`.CollisionlessLattice` based on which the properties of the operator are inferred. + :attr:`lattice` The :class:`.MSLattice` based on which the properties of the operator are inferred. :attr:`blocks` A list of :class:`.Block` objects for which to generate the BB boundary condition circuits. :attr:`logger` The performance logger, by default ``getLogger("qlbm")``. ========================= ====================================================================== @@ -155,11 +156,11 @@ class SpecularReflectionOperator(CQLBMOperator): .. plot:: :include-source: - from qlbm.components.collisionless import SpecularReflectionOperator - from qlbm.lattice import CollisionlessLattice + from qlbm.components.ms import SpecularReflectionOperator + from qlbm.lattice import MSLattice # Build an example lattice - lattice = CollisionlessLattice( + lattice = MSLattice( { "lattice": {"dim": {"x": 8, "y": 8}, "velocities": {"x": 4, "y": 4}}, "geometry": [{"shape":"cuboid", "x": [5, 6], "y": [1, 2], "boundary": "specular"}], @@ -171,7 +172,7 @@ class SpecularReflectionOperator(CQLBMOperator): def __init__( self, - lattice: CollisionlessLattice, + lattice: MSLattice, blocks: List[Block], logger: Logger = getLogger("qlbm"), ) -> None: @@ -490,7 +491,7 @@ def flip_and_stream( ): """Flips the velocity direction qubit controlled on the ancilla obstacle qubit, before performing streaming. - Unlike in the regular :class:`.CollisionlessStreamingOperator`, the :class:`.ControlledIncrementer` + Unlike in the regular :class:`.MSStreamingOperator`, the :class:`.ControlledIncrementer` phase shift circuit is additionally controlled on the ancilla obstacle qubit of the streaming dimension, which ensures that only particles whose grid position gets incremented (decremented) are those that have streamed inside the solid domain in this CFL time step. diff --git a/qlbm/components/collisionless/streaming.py b/qlbm/components/ms/streaming.py similarity index 90% rename from qlbm/components/collisionless/streaming.py rename to qlbm/components/ms/streaming.py index 8e58210..984a87c 100644 --- a/qlbm/components/collisionless/streaming.py +++ b/qlbm/components/ms/streaming.py @@ -11,14 +11,14 @@ from qiskit.synthesis import synth_qft_full as QFT from typing_extensions import override -from qlbm.components.base import CQLBMOperator, LBMPrimitive -from qlbm.lattice import CollisionlessLattice +from qlbm.components.base import LBMPrimitive, MSOperator +from qlbm.lattice import MSLattice from qlbm.tools import CircuitException, bit_value class StreamingAncillaPreparation(LBMPrimitive): r""" - A primitive used in :class:`.CollisionlessStreamingOperator` that implements the preparatory step of streaming necessary for the :class:`.CQLBM` method. + A primitive used in :class:`.MSStreamingOperator` that implements the preparatory step of streaming necessary for the :class:`.MSQLBM` method. This operator sets the ancilla qubits to :math:`\ket{1}` for the velocities that will be streamed in the next CFL time step. @@ -26,7 +26,7 @@ class StreamingAncillaPreparation(LBMPrimitive): ========================= ====================================================================== Attribute Summary ========================= ====================================================================== - :attr:`lattice` The :class:`.CollisionlessLattice` based on which the properties of the operator are inferred. + :attr:`lattice` The :class:`.MSLattice` based on which the properties of the operator are inferred. :attr:`velocities` The velocities that need to be streamed within the next time step. :attr:`dim` The dimension to which the velocities correspond. :attr:`logger` The performance logger, by default ``getLogger("qlbm")``. @@ -37,11 +37,11 @@ class StreamingAncillaPreparation(LBMPrimitive): .. plot:: :include-source: - from qlbm.components.collisionless import StreamingAncillaPreparation - from qlbm.lattice import CollisionlessLattice + from qlbm.components.ms import StreamingAncillaPreparation + from qlbm.lattice import MSLattice # Build an example lattice - lattice = CollisionlessLattice( + lattice = MSLattice( { "lattice": {"dim": {"x": 8, "y": 8}, "velocities": {"x": 4, "y": 4}}, "geometry": [], @@ -54,7 +54,7 @@ class StreamingAncillaPreparation(LBMPrimitive): def __init__( self, - lattice: CollisionlessLattice, + lattice: MSLattice, velocities: List[int], dim: int, logger: Logger = getLogger("qlbm"), @@ -114,14 +114,14 @@ def __str__(self) -> str: class ControlledIncrementer(LBMPrimitive): r""" - A primitive used in :class:`.CollisionlessStreamingOperator` that implements the streaming operation on the states for which the ancilla qubits are in the state :math:`\ket{1}`. + A primitive used in :class:`.MSStreamingOperator` that implements the streaming operation on the states for which the ancilla qubits are in the state :math:`\ket{1}`. This primitive is applied after the primitive :class:`.StreamingAncillaPreparation` to compose the streaming operator. ========================= ====================================================================== Attribute Summary ========================= ====================================================================== - :attr:`lattice` The :class:`.CollisionlessLattice` based on which the properties of the operator are inferred. + :attr:`lattice` The :class:`.MSLattice` based on which the properties of the operator are inferred. :attr:`reflection` The reflection attribute decides the type of reflection that will take place. This should be either "specular", "bounceback", or ``None``, and defaults to None. This parameter governs which qubits are used as controls for the Fourier space phase shifts. @@ -133,11 +133,11 @@ class ControlledIncrementer(LBMPrimitive): .. plot:: :include-source: - from qlbm.components.collisionless import ControlledIncrementer - from qlbm.lattice import CollisionlessLattice + from qlbm.components.ms import ControlledIncrementer + from qlbm.lattice import MSLattice # Build an example lattice - lattice = CollisionlessLattice( + lattice = MSLattice( { "lattice": {"dim": {"x": 8, "y": 8}, "velocities": {"x": 4, "y": 4}}, "geometry": [], @@ -152,7 +152,7 @@ class ControlledIncrementer(LBMPrimitive): def __init__( self, - lattice: CollisionlessLattice, + lattice: MSLattice, reflection: str | None = None, logger: Logger = getLogger("qlbm"), ) -> None: @@ -239,8 +239,8 @@ def __str__(self) -> str: return f"[Primitive ControlledIncrementer with reflection {self.reflection}]" -class CollisionlessStreamingOperator(CQLBMOperator): - """An operator that performs streaming in Fourier space as part of the :class:`.CQLBM` algorithm. +class MSStreamingOperator(MSOperator): + """An operator that performs streaming in Fourier space as part of the :class:`.MSQLBM` algorithm. Streaming is broken down into the following steps: @@ -253,7 +253,7 @@ class CollisionlessStreamingOperator(CQLBMOperator): ========================= ====================================================================== Attribute Summary ========================= ====================================================================== - :attr:`lattice` The :class:`.CollisionlessLattice` based on which the properties of the operator are inferred. + :attr:`lattice` The :class:`.MSLattice` based on which the properties of the operator are inferred. :attr:`velocities` A list of velocities to increment. This is computed according to CFL counter. :attr:`logger` The performance logger, by default ``getLogger("qlbm")``. ========================= ====================================================================== @@ -263,11 +263,11 @@ class CollisionlessStreamingOperator(CQLBMOperator): .. plot:: :include-source: - from qlbm.components.collisionless import CollisionlessStreamingOperator - from qlbm.lattice import CollisionlessLattice + from qlbm.components.ms import MSStreamingOperator + from qlbm.lattice import MSLattice # Build an example lattice - lattice = CollisionlessLattice( + lattice = MSLattice( { "lattice": {"dim": {"x": 8, "y": 8}, "velocities": {"x": 4, "y": 4}}, "geometry": [], @@ -275,14 +275,14 @@ class CollisionlessStreamingOperator(CQLBMOperator): ) # Streaming the velocity with index 2 - CollisionlessStreamingOperator(lattice=lattice, velocities=[2]).draw("mpl") + MSStreamingOperator(lattice=lattice, velocities=[2]).draw("mpl") """ circuit: QuantumCircuit def __init__( self, - lattice: CollisionlessLattice, + lattice: MSLattice, velocities: List[int], logger: Logger = getLogger("qlbm"), ) -> None: @@ -330,7 +330,7 @@ def __str__(self) -> str: class PhaseShift(LBMPrimitive): r""" - A primitive that applies the phase-shift as part of the :class:`.ControlledIncrementer` used in the :class:`.CollisionlessStreamingOperator`. + A primitive that applies the phase-shift as part of the :class:`.ControlledIncrementer` used in the :class:`.MSStreamingOperator`. The rotation applied is :math:`\pm\frac{\pi}{2^{n_q - 1 - j}}`, with :math:`j` the position of the qubit (indexed starting with 0). For an in-depth mathematical explanation of the procedure, consult Section 4 of :cite:t:`collisionless`. @@ -350,7 +350,7 @@ class PhaseShift(LBMPrimitive): .. plot:: :include-source: - from qlbm.components.collisionless import PhaseShift + from qlbm.components.ms import PhaseShift # A phase shift of 5 qubits PhaseShift(num_qubits=5, positive=False).draw("mpl") @@ -414,7 +414,7 @@ class SpeedSensitivePhaseShift(LBMPrimitive): .. plot:: :include-source: - from qlbm.components.collisionless import SpeedSensitivePhaseShift + from qlbm.components.ms import SpeedSensitivePhaseShift # A phase shift of 5 qubits, controlled on speed index 2 SpeedSensitivePhaseShift(num_qubits=5, speed=2, positive=True).draw("mpl") diff --git a/qlbm/components/spacetime/__init__.py b/qlbm/components/spacetime/__init__.py index 7aca5cc..7835a9e 100644 --- a/qlbm/components/spacetime/__init__.py +++ b/qlbm/components/spacetime/__init__.py @@ -1,4 +1,4 @@ -"""Modular qlbm quantum circuit components for the CQLBM algorithm :cite:p:`spacetime`.""" +"""Modular qlbm quantum circuit components for the Space-Time QLBM algorithm :cite:p:`spacetime`.""" from .collision.d2q4_old import SpaceTimeD2Q4CollisionOperator from .initial.pointwise import PointWiseSpaceTimeInitialConditions diff --git a/qlbm/components/spacetime/initial/volumetric.py b/qlbm/components/spacetime/initial/volumetric.py index 9d28bc8..b8f8868 100644 --- a/qlbm/components/spacetime/initial/volumetric.py +++ b/qlbm/components/spacetime/initial/volumetric.py @@ -9,7 +9,7 @@ from typing_extensions import override from qlbm.components.base import LBMPrimitive -from qlbm.components.collisionless.primitives import Comparator, ComparatorMode +from qlbm.components.ms.primitives import Comparator, ComparatorMode from qlbm.lattice.lattices.spacetime_lattice import SpaceTimeLattice from qlbm.tools.utils import flatten diff --git a/qlbm/components/spacetime/reflection/volumetric.py b/qlbm/components/spacetime/reflection/volumetric.py index 5f01686..f0bcd86 100644 --- a/qlbm/components/spacetime/reflection/volumetric.py +++ b/qlbm/components/spacetime/reflection/volumetric.py @@ -8,7 +8,7 @@ from typing_extensions import override from qlbm.components.base import SpaceTimeOperator -from qlbm.components.collisionless.primitives import Comparator, ComparatorMode +from qlbm.components.ms.primitives import Comparator, ComparatorMode from qlbm.lattice.geometry.shapes.block import Block from qlbm.lattice.lattices.spacetime_lattice import SpaceTimeLattice from qlbm.tools.exceptions import CircuitException diff --git a/qlbm/infra/__init__.py b/qlbm/infra/__init__.py index f7f9839..3c32022 100644 --- a/qlbm/infra/__init__.py +++ b/qlbm/infra/__init__.py @@ -6,14 +6,14 @@ """ from .compiler import CircuitCompiler -from .result import CollisionlessResult, SpaceTimeResult +from .result import AmplitudeResult, SpaceTimeResult from .runner import CircuitRunner, QiskitRunner, SimulationConfig __all__ = [ "CircuitCompiler", "CircuitRunner", # "MPIQulacsRunner", - "CollisionlessResult", + "AmplitudeResult", "SpaceTimeResult", "QiskitRunner", "SimulationConfig", diff --git a/qlbm/infra/compiler.py b/qlbm/infra/compiler.py index 313610a..412d443 100644 --- a/qlbm/infra/compiler.py +++ b/qlbm/infra/compiler.py @@ -35,7 +35,7 @@ class CircuitCompiler: =========================== ====================================================================== Example usage: we will construct an end-to-end QLBM algorithm and compile it to a qiskit simulator using Tket. - We begin by constructing a :class:`.SpaceTimeQLBM` algorithm for a :math:`4 \\times 8` lattice, with one time step. + We begin by constructing a :class:`.SpaceTimeQLBM` algorithm for a :math:`4 \times 8` lattice, with one time step. .. plot:: :include-source: diff --git a/qlbm/infra/reinitialize/spacetime_reinitializer.py b/qlbm/infra/reinitialize/spacetime_reinitializer.py index 20199e6..866487b 100644 --- a/qlbm/infra/reinitialize/spacetime_reinitializer.py +++ b/qlbm/infra/reinitialize/spacetime_reinitializer.py @@ -25,7 +25,7 @@ class SpaceTimeReinitializer(Reinitializer): :class:`.SpaceTimeQLBM`-specific implementation of the :class:`.Reinitializer`. Compatible with both :class:`.QiskitRunner`\ s and :class:`.QulacsRunner`\ s. - To generate a new set of initial conditions for the CQLBM algorithm, + To generate a new set of initial conditions for the Space-Time encoding, 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 diff --git a/qlbm/infra/result/__init__.py b/qlbm/infra/result/__init__.py index 78cfce1..2f2a3f6 100644 --- a/qlbm/infra/result/__init__.py +++ b/qlbm/infra/result/__init__.py @@ -1,8 +1,8 @@ """Result objects for processing measurement data into visualizations.""" +from .amplitude_result import AmplitudeResult from .base import QBMResult -from .collisionless_result import CollisionlessResult from .lqlga_result import LQLGAResult from .spacetime_result import SpaceTimeResult -__all__ = ["QBMResult", "CollisionlessResult", "SpaceTimeResult", "LQLGAResult"] +__all__ = ["QBMResult", "AmplitudeResult", "SpaceTimeResult", "LQLGAResult"] diff --git a/qlbm/infra/result/collisionless_result.py b/qlbm/infra/result/amplitude_result.py similarity index 75% rename from qlbm/infra/result/collisionless_result.py rename to qlbm/infra/result/amplitude_result.py index ea9b88c..ababae2 100644 --- a/qlbm/infra/result/collisionless_result.py +++ b/qlbm/infra/result/amplitude_result.py @@ -1,4 +1,4 @@ -""":class:`.CQLBM`-specific implementation of the :class:`.QBMResult`.""" +"""Implementation of the :class:`.QBMResult`. for :class:`AmplitudeLattice`-based algorithms.""" import re from os import listdir @@ -9,21 +9,21 @@ from typing_extensions import override from vtkmodules.util import numpy_support -from qlbm.lattice import CollisionlessLattice +from qlbm.lattice.lattices.base import AmplitudeLattice from .base import QBMResult -class CollisionlessResult(QBMResult): +class AmplitudeResult(QBMResult): """ - :class:`.CQLBM`-specific implementation of the :class:`.QBMResult`. + Implementation of the :class:`.QBMResult` for lattices inheriting from :class:`.AmplitudeLattice`. Processes counts sampled from :class:`.GridMeasurement` primitives. =========================== ====================================================================== Attribute Summary =========================== ====================================================================== - :attr:`lattice` The :class:`.CollisionlessLattice` of the simulated system. + :attr:`lattice` The :class:`.AmplitudeLattice` 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". =========================== ====================================================================== @@ -38,12 +38,12 @@ class CollisionlessResult(QBMResult): output_file_name: str """The name of the file to output the artifacts to.""" - lattice: CollisionlessLattice + lattice: AmplitudeLattice """The lattice the result corresponds to.""" def __init__( self, - lattice: CollisionlessLattice, + lattice: AmplitudeLattice, directory: str, output_file_name: str = "step", ) -> None: @@ -70,7 +70,23 @@ def save_timestep_counts( else 0, ) - if self.lattice.num_dims == 2: + 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: + x = int( + count[self.lattice.num_velocity_qubits :], + 2, + ) + # Another dirty rendering trick for VTK and Paraview + count_history[x][0] += counts[count] * ( + 1 + (int(count[: self.lattice.num_velocity_qubits] == "00")) + ) + count_history[x][1] += counts[count] * ( + 1 + (int(count[: self.lattice.num_velocity_qubits] == "00")) + ) + + elif self.lattice.num_dims == 2: count_history = np.zeros( (self.lattice.num_gridpoints[0] + 1, self.lattice.num_gridpoints[1] + 1) ) @@ -105,7 +121,10 @@ def save_timestep_counts( count_history[x][y][z] = counts[count] self.save_timestep_array( - count_history, timestep, create_vis=create_vis, save_counts_array=save_array + count_history if self.lattice.num_dims > 1 else np.transpose(count_history), + timestep, + create_vis=create_vis, + save_counts_array=save_array, ) @override diff --git a/qlbm/infra/result/base.py b/qlbm/infra/result/base.py index 8987f5a..fce9140 100644 --- a/qlbm/infra/result/base.py +++ b/qlbm/infra/result/base.py @@ -2,10 +2,12 @@ from abc import ABC, abstractmethod from os.path import isdir +from pathlib import Path from typing import Dict import numpy as np import vtk +from qiskit.quantum_info import Statevector from vtkmodules.util import numpy_support from qlbm.lattice import Lattice @@ -167,3 +169,24 @@ def save_timestep_counts( def visualize_all_numpy_data(self): """Converts all numpy data saved to disk to ``vti`` files.""" pass + + def save_statevector(self, statevector: Statevector, step: int): + """ + Save a given statevector to disk. + + The statevector is saved as a numpy array in the ``statevector`` subdirectory of the result's root directory. + + Parameters + ---------- + statevector : Statevector + The statevector to save. + step : int + The step this statevector corresponds to, for naming purposes. + """ + statevector_dir = f"{self.directory}/statevectors" + if not isdir(statevector_dir): + create_directory_and_parents(statevector_dir) + state = np.asarray(statevector, dtype=np.complex128) # shape (2**n,) + + out_path = Path(f"{statevector_dir}/step_{step}.npy") + np.save(out_path, state) diff --git a/qlbm/infra/runner/base.py b/qlbm/infra/runner/base.py index 0de08ca..1263312 100644 --- a/qlbm/infra/runner/base.py +++ b/qlbm/infra/runner/base.py @@ -15,12 +15,13 @@ ) from qlbm.infra.reinitialize.identity_reinitializer import IdentityReinitializer from qlbm.infra.result import ( - CollisionlessResult, + AmplitudeResult, LQLGAResult, QBMResult, SpaceTimeResult, ) -from qlbm.lattice import CollisionlessLattice, Lattice +from qlbm.lattice import Lattice, MSLattice +from qlbm.lattice.lattices.ab_lattice import ABLattice from qlbm.lattice.lattices.lqlga_lattice import LQLGALattice from qlbm.lattice.lattices.spacetime_lattice import SpaceTimeLattice from qlbm.tools.exceptions import CircuitException, ResultsException @@ -123,9 +124,11 @@ def new_result(self, output_directory: str, output_file_name: str) -> QBMResult: ResultsException If there is no matching result object for the runner's lattice. """ - if isinstance(self.lattice, CollisionlessLattice): - return CollisionlessResult( - cast(CollisionlessLattice, self.lattice), + if isinstance(self.lattice, MSLattice) or isinstance( + self.lattice, ABLattice + ): + return AmplitudeResult( + self.lattice, # type: ignore output_directory, output_file_name, ) @@ -154,8 +157,10 @@ def new_reinitializer(self) -> Reinitializer: ResultsException If the underlying algorithm does not support reinitialization. """ - if isinstance(self.lattice, CollisionlessLattice) or isinstance( - self.lattice, LQLGALattice + if ( + isinstance(self.lattice, MSLattice) + or isinstance(self.lattice, LQLGALattice) + or isinstance(self.lattice, ABLattice) ): return IdentityReinitializer( self.lattice, diff --git a/qlbm/infra/runner/mpi_qulacs.py b/qlbm/infra/runner/mpi_qulacs.py index 16a814d..0f59797 100644 --- a/qlbm/infra/runner/mpi_qulacs.py +++ b/qlbm/infra/runner/mpi_qulacs.py @@ -19,8 +19,8 @@ from qlbm.components.base import LBMPrimitive from qlbm.infra.compiler import CircuitCompiler -from qlbm.infra.result import CollisionlessResult -from qlbm.lattice import CollisionlessLattice +from qlbm.infra.result import AmplitudeResult +from qlbm.lattice import MSLattice from qlbm.tools.utils import get_circuit_properties, qiskit_to_qulacs @@ -29,7 +29,7 @@ class MPIQulacsRunner: def __init__( self, - lattice: CollisionlessLattice, + lattice: MSLattice, output_directory: str, output_file_name: str = "step", sampling_backend: AerSimulator = QasmSimulator(), @@ -52,7 +52,7 @@ def run( num_shots: int, snapshot_execution: bool = False, statevector_sampling: bool = False, - ) -> CollisionlessResult: + ) -> AmplitudeResult: """Simualtes the provided algorithm configuration. Parameters @@ -76,7 +76,7 @@ def run( Returns ------- - CollisionlessResult + AmplitudeResult The result of the simulation. """ measurement = CircuitCompiler("QISKIT", "QISKIT").compile( @@ -104,7 +104,7 @@ def run_mpi_qulacs( num_shots: int, snapshot_execution: bool = False, statevector_sampling: bool = False, - ) -> CollisionlessResult: + ) -> AmplitudeResult: """ Simualtes the provided algorithm configuration. @@ -129,10 +129,10 @@ def run_mpi_qulacs( Returns ------- - CollisionlessResult + AmplitudeResult The result of the simulation. """ - simulation_result = CollisionlessResult( + simulation_result = AmplitudeResult( self.lattice, self.output_directory, self.output_file_name ) diff --git a/qlbm/infra/runner/qiskit_runner.py b/qlbm/infra/runner/qiskit_runner.py index ec12b8b..5064e65 100644 --- a/qlbm/infra/runner/qiskit_runner.py +++ b/qlbm/infra/runner/qiskit_runner.py @@ -49,11 +49,13 @@ def __init__( lattice: Lattice, logger: Logger = getLogger("qlbm"), device: str = "CPU", # ! TODO reimplement + save_statevector_to_disk: bool = False, ) -> None: super().__init__(config, lattice, logger, device) self.execution_backend = self.config.execution_backend self.sampling_backend = self.config.sampling_backend + self.statevector_to_disk = save_statevector_to_disk @override def run( @@ -183,6 +185,11 @@ def _run_snapshot_time_loop( qiskit_execution_result.get_counts(), step ) + if self.statevector_to_disk: + simulation_result.save_statevector( + qiskit_execution_result.data(0)["step"], step=step + ) + # Update the initial conditions for the next step initial_conditions = self.reinitializer.reinitialize( statevector=qiskit_execution_result.data(0)["step"] diff --git a/qlbm/infra/runner/simulation_config.py b/qlbm/infra/runner/simulation_config.py index 00d4dde..0c96262 100644 --- a/qlbm/infra/runner/simulation_config.py +++ b/qlbm/infra/runner/simulation_config.py @@ -35,7 +35,7 @@ class SimulationConfig: * - Attribute - Description * - :attr:`initial_conditions` - - The initial conditions of the simulations. For example, :class:`.CollisionlessInitialConditions` or :class:`.PointWiseSpaceTimeInitialConditions`. + - The initial conditions of the simulations. For example, :class:`.MSInitialConditions` or :class:`.PointWiseSpaceTimeInitialConditions`. * - :attr:`algorithm` - The algorithm that performs the QLBM time step computation. For example, :class:`.CQLBM` or :class:`.SpaceTimeQLBM`. * - :attr:`postprocessing` @@ -132,7 +132,7 @@ class SimulationConfig: .. code-block:: python cfg = SimulationConfig( - initial_conditions=CollisionlessInitialConditions(lattice, logger), + initial_conditions=MSInitialConditions(lattice, logger), algorithm=CQLBM(lattice, logger), postprocessing=EmptyPrimitive(lattice, logger), measurement=GridMeasurement(lattice, logger), diff --git a/qlbm/lattice/__init__.py b/qlbm/lattice/__init__.py index b709602..5d5620e 100644 --- a/qlbm/lattice/__init__.py +++ b/qlbm/lattice/__init__.py @@ -1,6 +1,6 @@ """Lattice and Block utilitites.""" -from .geometry.encodings.collisionless import ( +from .geometry.encodings.ms import ( DimensionalReflectionData, ReflectionPoint, ReflectionResetEdge, @@ -12,8 +12,10 @@ from .geometry.shapes.circle import ( Circle, ) -from .lattices import CollisionlessLattice, Lattice +from .lattices import Lattice, MSLattice +from .lattices.ab_lattice import ABLattice from .lattices.lqlga_lattice import LQLGALattice +from .lattices.oh_lattice import OHLattice from .lattices.spacetime_lattice import SpaceTimeLattice from .spacetime.properties_base import ( LatticeDiscretization, @@ -22,7 +24,9 @@ __all__ = [ "Lattice", - "CollisionlessLattice", + "ABLattice", + "MSLattice", + "OHLattice", "SpaceTimeLattice", "LQLGALattice", "DimensionalReflectionData", diff --git a/qlbm/lattice/geometry/encodings/__init__.py b/qlbm/lattice/geometry/encodings/__init__.py index 462e32c..aff8bdb 100644 --- a/qlbm/lattice/geometry/encodings/__init__.py +++ b/qlbm/lattice/geometry/encodings/__init__.py @@ -1,12 +1,12 @@ """Algorithm-specific geometrical data encodings.""" -from .collisionless import ( +from .lqlga import LQLGAPointwiseReflectionData, LQLGAReflectionData +from .ms import ( DimensionalReflectionData, ReflectionPoint, ReflectionResetEdge, ReflectionWall, ) -from .lqlga import LQLGAPointwiseReflectionData, LQLGAReflectionData from .spacetime import ( SpaceTimeDiagonalReflectionData, SpaceTimePWReflectionData, diff --git a/qlbm/lattice/geometry/encodings/collisionless.py b/qlbm/lattice/geometry/encodings/ms.py similarity index 99% rename from qlbm/lattice/geometry/encodings/collisionless.py rename to qlbm/lattice/geometry/encodings/ms.py index dcbe2e5..d232f6a 100644 --- a/qlbm/lattice/geometry/encodings/collisionless.py +++ b/qlbm/lattice/geometry/encodings/ms.py @@ -253,4 +253,4 @@ def __get_bounceback_wall_loose_bounds(self): if self.num_dims == 2: return [[True], [False]] else: - return [[True, True], [False, False], [False, True]] \ No newline at end of file + return [[True, True], [False, False], [False, True]] diff --git a/qlbm/lattice/geometry/shapes/block.py b/qlbm/lattice/geometry/shapes/block.py index 2fcece0..c3a0f62 100644 --- a/qlbm/lattice/geometry/shapes/block.py +++ b/qlbm/lattice/geometry/shapes/block.py @@ -8,7 +8,7 @@ import numpy as np from stl import mesh -from qlbm.lattice.geometry.encodings.collisionless import ( +from qlbm.lattice.geometry.encodings.ms import ( DimensionalReflectionData, ReflectionPoint, ReflectionResetEdge, @@ -19,7 +19,11 @@ SpaceTimeVolumetricReflectionData, ) from qlbm.lattice.geometry.shapes.base import LQLGAShape, SpaceTimeShape -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 bit_value, dimension_letter, flatten, get_qubits_to_invert @@ -97,6 +101,47 @@ class Block(SpaceTimeShape, LQLGAShape): ), ] + ab_wall_indices_to_reset: Dict[ + LatticeDiscretization, Dict[Tuple[int, bool], List[int]] + ] = { + LatticeDiscretization.D2Q9: { + (0, False): [3, 6, 7], + (0, True): [1, 5, 8], + (1, False): [4, 7, 8], + (1, True): [2, 5, 6], + } + } + + ab_near_corner_indices_to_reset: Dict[ + LatticeDiscretization, Dict[int, Dict[Tuple[bool, ...], List[int]]] + ] = { + LatticeDiscretization.D2Q9: { + 0: { + (False, False): [6], + (False, True): [7], + (True, False): [5], + (True, True): [8], + }, + 1: { + (False, False): [8], + (False, True): [5], + (True, False): [7], + (True, True): [6], + }, + } + } + + ab_corner_indices_to_reset: Dict[ + LatticeDiscretization, Dict[Tuple[bool, ...], List[int]] + ] = { + LatticeDiscretization.D2Q9: { + (False, False): [7], + (False, True): [6], + (True, False): [8], + (True, True): [5], + } + } + def __init__( self, bounds: List[Tuple[int, int]], @@ -712,6 +757,120 @@ def get_d2q4_surfaces(self) -> List[List[List[Tuple[int, ...]]]]: return surfaces + def get_lbm_wall_velocity_indices_to_reflect( + self, discretization: LatticeDiscretization, dim: int, bound: bool + ) -> List[int]: + """ + Get the indices of the velocities that need to be reflected at gridpoints outside the wall of the object. + + Parameters + ---------- + discretization : LatticeDiscretization + The discretization of the lattice. + dim : int + The dimension that the wall reflects. + bounds : Tuple[bool, ...] + The bounds of the corner to address. + + Returns + ------- + List[int] + The list of velocity indices to reflect. + + Raises + ------ + LatticeException + If the discretization is not supported or if the dimension or bounds are inconsistent. + """ + if discretization not in self.ab_wall_indices_to_reset: + raise LatticeException( + f"Discretization {discretization} not supported. Supported discretizations are: {self.ab_wall_indices_to_reset.keys()}" + ) + + if (dim, bound) not in self.ab_wall_indices_to_reset[discretization]: + raise LatticeException( + f"{(dim, bound)} is not a valid description of a wall for {discretization}." + ) + + return self.ab_wall_indices_to_reset[discretization][(dim, bound)] + + def get_lbm_near_corner_velocity_indices_to_reflect( + self, discretization: LatticeDiscretization, dim: int, bounds: Tuple[bool, ...] + ) -> List[int]: + """ + Get the indices of the velocities that need to be reflected at gridpoints near the corners of the object. + + Parameters + ---------- + discretization : LatticeDiscretization + The discretization of the lattice. + dim : int + The dimension outside of obstacle bounds. + bounds : Tuple[bool, ...] + The bounds of the corner to address. + + Returns + ------- + List[int] + The list of velocity indices to reflect. + + Raises + ------ + LatticeException + If the discretization is not supported or if the dimension or bounds are inconsistent. + """ + if discretization not in self.ab_near_corner_indices_to_reset: + raise LatticeException( + f"Discretization {discretization} not supported. Supported discretizations are: {self.ab_wall_indices_to_reset.keys()}" + ) + + if dim not in self.ab_near_corner_indices_to_reset[discretization]: + raise LatticeException( + f"{dim} is not a valid dimension for {discretization}." + ) + + if bounds not in self.ab_near_corner_indices_to_reset[discretization][dim]: + raise LatticeException( + f"{bounds} is not a valid description of a near corner gridpoint of {discretization}." + ) + + return self.ab_near_corner_indices_to_reset[discretization][dim][bounds] + + def get_lbm_outside_corner_indices_to_reflect( + self, discretization: LatticeDiscretization, bounds: Tuple[bool, ...] + ) -> List[int]: + """ + Get the indices of the velocities that need to be reflected at the outside corners of the object. + + Parameters + ---------- + discretization : LatticeDiscretization + The discretization of the lattice. + bounds : Tuple[bool, ...] + The bounds of the corner to address. + + Returns + ------- + List[int] + The list of velocity indices to reflect. + + Raises + ------ + LatticeException + If the discretization is not supported or if the bounds are inconsistent. + """ + if discretization not in self.ab_corner_indices_to_reset: + raise LatticeException( + f"Discretization {discretization} not supported. Supported discretizations are: {self.ab_wall_indices_to_reset.keys()}" + ) + + if bounds not in self.ab_corner_indices_to_reset[discretization]: + raise LatticeException( + f"{bounds} is not a valid description of a wall for {discretization}." + ) + + return self.ab_corner_indices_to_reset[discretization][bounds] + @override def get_lqlga_reflection_data_d1q2(self): print(self.num_grid_qubits[0]) diff --git a/qlbm/lattice/lattices/__init__.py b/qlbm/lattice/lattices/__init__.py index 7a92929..11f49b3 100644 --- a/qlbm/lattice/lattices/__init__.py +++ b/qlbm/lattice/lattices/__init__.py @@ -1,8 +1,9 @@ """Base Lattice class and algorithm-specific implementations.""" from .base import Lattice -from .collisionless_lattice import CollisionlessLattice from .lqlga_lattice import LQLGALattice +from .ms_lattice import MSLattice +from .oh_lattice import OHLattice from .spacetime_lattice import SpaceTimeLattice -__all__ = ["Lattice", "CollisionlessLattice", "SpaceTimeLattice", "LQLGALattice"] +__all__ = ["Lattice", "MSLattice", "SpaceTimeLattice", "LQLGALattice", "OHLattice"] diff --git a/qlbm/lattice/lattices/ab_lattice.py b/qlbm/lattice/lattices/ab_lattice.py new file mode 100644 index 0000000..db8e601 --- /dev/null +++ b/qlbm/lattice/lattices/ab_lattice.py @@ -0,0 +1,311 @@ +"""Implementation of the Amplitude-Based (AB) encoding lattice for generic DdQq discretizations.""" + +from logging import getLogger +from typing import Dict, List, Tuple + +from numpy import ceil, log2 +from qiskit import QuantumCircuit, QuantumRegister +from typing_extensions import override + +from qlbm.components.ab.encodings import ABEncodingType +from qlbm.lattice.geometry.shapes.base import Shape +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 + +from .base import AmplitudeLattice + + +class ABLattice(AmplitudeLattice): + r""" + Implementation of the :class:`.Lattice` base specific to the 2D and 3D :class:`.ABQLBM` algorithm developed in :cite:t:`collisionless`. + + This lattice is only built from :math:`D_dQ_q` specifications. + For multi-speed implementations, see :class:`.MSQLBM`. + + The registers encoded in the lattice and their accessors are given below. + For the size of each register, + :math:`N_{g_j}` is the number of grid points of dimension :math:`j` (i.e., 64, 128), + :math:`q` is the number of discrete velocities, for instance, 9. + + .. list-table:: Register allocation + :widths: 25 25 25 50 + :header-rows: 1 + + * - Register + - Size + - Access Method + - Description + * - :attr:`grid_registers` + - :math:`\Sigma_{1\leq j \leq d} \left \lceil{\log N_{g_j}} \right \rceil` + - :meth:`grid_index` + - The qubits encoding the physical grid. + * - :attr:`velocity_registers` + - :math:`\lceil\log_2 q \rceil` + - :meth:`velocity_index` + - The qubits encoding the :math:`q` discrete velocities. + * - :attr:`ancilla_obstacle_register` + - :math:`1` + - :meth:`ancillae_obstacle_index` + - The qubits used to detect whether particles have streamed into obstacles. Used for reflection. + * - :attr:`ancilla_comparator_register` + - :math:`2(d-1)` + - :meth:`ancillae_comparator_index` + - The qubits used to for :class:`.Comparator`\ s. Used for reflection. + + A lattice can be constructed from from either an input file or a Python dictionary: + + .. code-block:: json + + { + "lattice": { + "dim": { + "x": 16, + "y": 16 + }, + "velocities": "d2q9" + }, + "geometry": [ + { + "x": [9, 12], + "y": [3, 6], + "boundary": "bounceback" + }, + { + "x": [9, 12], + "y": [9, 12], + "boundary": "bounceback" + } + ] + } + + The register setup can be visualized by constructing a lattice object: + + .. plot:: + :include-source: + + from qlbm.lattice import ABLattice + + ABLattice( + { + "lattice": {"dim": {"x": 8, "y": 8}, "velocities": "D2Q9"}, + "geometry": [], + } + ).circuit.draw("mpl") + """ + + 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.""" + + registers: Tuple[QuantumRegister, ...] + """The registers of the lattice.""" + + def __init__( + self, + lattice_data, + logger=getLogger("qlbm"), + ): + super().__init__(lattice_data, logger) + self.num_gridpoints, self.num_velocities, self.shapes, self.discretization = ( + self.parse_input_data(lattice_data) + ) # type: ignore + self.geometries: List[Dict[str, List[Shape]]] = [self.shapes] + self.num_dims = len(self.num_gridpoints) + self.num_velocities_per_point = ( + LatticeDiscretizationProperties.get_num_velocities(self.discretization) + ) + + 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)}." + ) + + self.num_grid_qubits = int( + sum(map(lambda x: ceil(log2(x)), self.num_gridpoints)) + ) + self.num_velocity_qubits = int(ceil(log2(self.num_velocities_per_point))) + self.num_base_qubits = self.num_grid_qubits + self.num_velocity_qubits + + self.num_obstacle_qubits = self.__num_obstacle_qubits() + self.num_comparator_qubits = 2 * (self.num_dims - 1) + self.num_ancilla_qubits = self.num_comparator_qubits + self.num_obstacle_qubits + + self.num_total_qubits = self.num_base_qubits + self.num_ancilla_qubits + + temporary_registers = self.get_registers() + ( + self.grid_registers, + self.velocity_registers, + self.ancilla_comparator_register, + self.ancilla_object_register, + ) = temporary_registers + + self.registers = tuple(flatten(temporary_registers)) + self.circuit = QuantumCircuit(*self.registers) + + @override + def grid_index(self, dim: int | None = None) -> List[int]: + if dim is None: + return list( + range( + self.num_grid_qubits, + ) + ) + + if dim >= self.num_dims or dim < 0: + raise LatticeException( + f"Cannot index grid register for dimension {dim} in {self.num_dims}-dimensional lattice." + ) + + previous_qubits = sum([self.num_gridpoints[d].bit_length() for d in range(dim)]) + + return list( + range( + previous_qubits, previous_qubits + self.num_gridpoints[dim].bit_length() + ) + ) + + @override + def velocity_index(self, dim: int | None = None) -> List[int]: + if dim is not None: + raise LatticeException( + "ABLattice does not support a dimensional breakdown of velocities." + ) + return list( + range( + self.num_grid_qubits, + self.num_grid_qubits + self.num_velocity_qubits, + ) + ) + + @override + def ancillae_comparator_index(self, index: int | None = None) -> List[int]: + if index is None: + return list( + range( + self.num_base_qubits, + self.num_base_qubits + 2 * (self.num_dims - 1), + ) + ) + + if index >= self.num_dims - 1 or index < 0: + raise LatticeException( + f"Cannot index ancilla comparator register for index {index} in {self.num_dims}-dimensional lattice. Maximum is {self.num_dims - 2}." + ) + + return list( + range( + self.num_base_qubits, self.num_base_qubits + self.num_comparator_qubits + ) + ) + + @override + def ancillae_obstacle_index(self, index: int | None = None) -> List[int]: + if index is None: + return list( + range( + self.num_base_qubits + self.num_comparator_qubits, + self.num_base_qubits + + self.num_comparator_qubits + + self.num_obstacle_qubits, + ) + ) + + if index >= self.num_obstacle_qubits or index < 0: + raise LatticeException( + f"Cannot index ancilla obstacle register for index {index}. Maximum index for this lattice is {self.num_obstacle_qubits - 1}." + ) + + return [self.num_base_qubits + self.num_comparator_qubits + index] + + def __num_obstacle_qubits(self) -> int: + all_obstacle_bounceback: bool = len( + [ + b + for b in flatten(list(self.shapes.values())) + if b.boundary_condition == "bounceback" + ] + ) == len(flatten(list(self.shapes.values()))) + if all_obstacle_bounceback: + # A single qubit suffices to determine + # Whether particles have streamed inside the object + return 1 + # If there is at least one object with specular reflection + # 2 ancilla qubits are required for velocity inversion + else: + return self.num_dims + + @override + def get_registers(self) -> Tuple[List[QuantumRegister], ...]: + """Generates the encoding-specific register required for the streaming step. + + For this encoding, different registers encode + (i) the logarithmically compressed grid, + (ii) the logarithmically compressed discrete velocities, + (iii) the comparator qubits, + (iv) the object qubits. + + Returns + ------- + List[int] + Tuple[QuantumRegister]: The 4-tuple of qubit registers encoding the streaming step. + """ + # d ancilla qubits used to conditionally reflect velocities + ancilla_object_register = [ + QuantumRegister(self.num_obstacle_qubits, name="a_o") + ] + + # 2(d-1) ancilla qubits + ancilla_comparator_register = [ + QuantumRegister(self.num_comparator_qubits, name="a_c") + ] + + # Velocity qubits + velocity_registers = [QuantumRegister(self.num_velocity_qubits, name="v")] + + # Grid qubits + grid_registers = [ + QuantumRegister(gp.bit_length(), name=f"g_{dimension_letter(c)}") + for c, gp in enumerate(self.num_gridpoints) + ] + + return ( + grid_registers, + velocity_registers, + ancilla_comparator_register, + ancilla_object_register, + ) + + @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"ablattice-{self.num_dims}d-{gp_string}-{len(flatten(list(self.shapes.values())))}-obstacle" + + @override + def has_multiple_geometries(self): + return False # multiple geometries unsupported for ABQLBM right now + + @override + def get_encoding(self) -> ABEncodingType: + return ABEncodingType.AB diff --git a/qlbm/lattice/lattices/base.py b/qlbm/lattice/lattices/base.py index 1d91b8f..cc4ad15 100644 --- a/qlbm/lattice/lattices/base.py +++ b/qlbm/lattice/lattices/base.py @@ -7,6 +7,7 @@ from qiskit import QuantumCircuit, QuantumRegister +from qlbm.components.ab.encodings import ABEncodingType from qlbm.lattice.geometry.shapes.base import Shape from qlbm.lattice.geometry.shapes.block import Block from qlbm.lattice.geometry.shapes.circle import Circle @@ -138,7 +139,7 @@ class Lattice(ABC): 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"``). + 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 @@ -507,3 +508,130 @@ def has_multiple_geometries(self) -> bool: """ pass + +class AmplitudeLattice(Lattice, ABC): + r""" + Abstract Lattice class for QLBM algorithms that use the ampltiude-based encoding. + + The amplitude-based encoding generally maps LBM populations :math:`f_i` onto basis states as :math:`\sqrt{f_i}\ket{x}\ket{v}`, + with :math:`x` the position and :math:`v` the velocity. + Amplitude-based encdoings generally compress both the grid register and the velocity register into logarithmically + many qubits. + + ``qlbm`` currently has 2 amplitude-based lattices: the :class:`.MSLattice` and :class:`.ABLattice` used in the :class:`.MSQLBM` and :class:`.ABQLBM`, respectively. + """ + + def __init__( + self, + lattice_data, + logger=getLogger("qlbm"), + ): + super(AmplitudeLattice, self).__init__(lattice_data, logger) + + @abstractmethod + def grid_index(self, dim: int | None = None) -> List[int]: + """Get the indices of the qubits used that encode the grid values for the specified dimension. + + Parameters + ---------- + dim : int | None, optional + The dimension of the grid for which to retrieve the grid qubit indices, by default ``None``. + When ``dim`` is ``None``, the indices of all grid qubits for all dimensions are returned. + + Returns + ------- + List[int] + A list of indices of the qubits used to encode the grid values for the given dimension. + + Raises + ------ + LatticeException + If the dimension does not exist. + """ + pass + + @abstractmethod + def ancillae_comparator_index(self, index: int | None = None) -> List[int]: + """Get the indices of the qubits used as comparator ancillae for the specified index. + + Parameters + ---------- + index : int | None, optional + The index for which to retrieve the comparator qubit indices, by default ``None``. + There are `num_dims-1` available indices (i.e., 1 for 2D and 2 for 3D). + When `index` is ``None``, the indices of ancillae qubits for all dimensions are returned. + + Returns + ------- + List[int] + A list of indices of the qubits used as obstacle ancilla for the given dimension. + By convention, the 0th qubit in the returned list is used + for lower bound comparison and the 1st is used for upper bound comparisons. + + Raises + ------ + LatticeException + If the dimension does not exist. + """ + pass + + @abstractmethod + def ancillae_obstacle_index(self, index: int | None = None) -> List[int]: + """Get the indices of the qubits used as obstacle ancilla for the specified dimension. + + Parameters + ---------- + index : int | None, optional + The index of the grid for which to retrieve the obstacle qubit index, by default ``None``. + When ``index`` is ``None``, the indices of ancillae qubits for all dimensions are returned. + For 2D lattices with only bounce-back boundary-conditions, only one obstacle + qubit is required. + For all other configurations, the algorithm uses ``2d-2`` obstacle qubits. + + Returns + ------- + List[int] + A list of indices of the qubits used as obstacle ancilla for the given dimension. + + Raises + ------ + LatticeException + If the dimension does not exist. + """ + pass + + @abstractmethod + def velocity_index(self, dim: int | None = None) -> List[int]: + """Get the indices of the qubits used that encode the velocity magnitude values for the specified dimension. + + Parameters + ---------- + dim : int | None, optional + The dimension of the grid for which to retrieve the velocity qubit indices, by default ``None``. + When ``dim`` is ``None``, the indices of all velocity qubits for all dimensions are returned. + Note: ``dim`` should only only be supplied to the MSLattice. + Other amplitude lattices do not support a dimensional breakdown, and ``dim`` should not be passed as an argument. + + Returns + ------- + List[int] + A list of indices of the qubits used to encode the velocity magnitude values for the given dimension. + + Raises + ------ + LatticeException + If the dimension does not exist. + """ + pass + + @abstractmethod + def get_encoding(self) -> ABEncodingType: + """ + Get the type of encoding this lattice implements. + + Returns + ------- + ABEncodingType + The encoding of this lattice. + """ + pass diff --git a/qlbm/lattice/lattices/lqlga_lattice.py b/qlbm/lattice/lattices/lqlga_lattice.py index 893043d..a8ba755 100644 --- a/qlbm/lattice/lattices/lqlga_lattice.py +++ b/qlbm/lattice/lattices/lqlga_lattice.py @@ -1,6 +1,7 @@ """Implementation of the :class:`.Lattice` base specific to the 2D and 3D :class:`.LQLGA` algorithm.""" from itertools import product +from logging import getLogger from math import prod from typing import Dict, List, Tuple, cast, override @@ -21,19 +22,19 @@ 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. - ================================ ======================================================================================== + ==================================== ======================================================================================== + 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, @@ -75,7 +76,11 @@ class LQLGALattice(Lattice): velocity_register: QuantumRegister """The quantum register representing the velocities of the lattice.""" - def __init__(self, lattice_data, logger=...): + def __init__( + self, + lattice_data, + logger=getLogger("qlbm"), + ): super().__init__(lattice_data, logger) self.num_gridpoints, self.num_velocities, self.shapes, self.discretization = ( @@ -97,11 +102,19 @@ def __init__(self, lattice_data, logger=...): else 0 ) + self.num_accumulation_qubits = 0 + self.indices_to_accumulate = [] + + self.__update_registers() + + def __update_registers(self): self.num_total_qubits = self.num_base_qubits + self.num_marker_qubits temp_registers = self.get_registers() - self.velocity_register, self.marker_register = temp_registers + self.velocity_register, self.marker_register, self.accumulation_register = ( + temp_registers + ) self.registers = tuple(flatten(temp_registers)) self.circuit = QuantumCircuit(*self.registers) @@ -129,7 +142,13 @@ def get_registers(self) -> Tuple[List[QuantumRegister], ...]: else [] ) - return (velocity_registers, marker_register) + accumulation_register = ( + [QuantumRegister(self.num_accumulation_qubits, name="acc")] + if self.has_accumulation_register() + else [] + ) + + return (velocity_registers, marker_register, accumulation_register) def gridpoint_index_tuple(self, gridpoint: Tuple[int, ...]) -> int: """ @@ -328,7 +347,9 @@ def set_geometries(self, geometries): For a given lattice (set number of gridpoints and velocity discretization), set multiple geometry configurations to simulate simultaneously. - .. code-block:: python + .. plot:: + :include-source: + from qlbm.lattice import LQLGALattice lattice = LQLGALattice( @@ -356,21 +377,73 @@ def set_geometries(self, geometries): A list of geometries to simulate on the same lattice. """ self.geometries = [self.parse_geometry_dict(g) for g in geometries] - - # Update the class attribute that depend on the register setup - temp_registers = self.get_registers() - - self.velocity_register, self.marker_register = temp_registers - self.registers = tuple(flatten(temp_registers)) self.num_marker_qubits = ( int(ceil(log2(len(self.geometries)))) if self.has_multiple_geometries() else 0 ) - - self.num_total_qubits = self.num_base_qubits + self.num_marker_qubits - self.circuit = QuantumCircuit(*self.registers) + self.__update_registers() @override def has_multiple_geometries(self) -> bool: return len(self.geometries) > 1 + + def has_accumulation_register(self) -> bool: + """ + Whether the lattice has a register that accumulates quantities at each step. + + Returns + ------- + bool + Whether the lattice has a register that accumulates quantities at each step. + """ + return self.num_accumulation_qubits > 0 + + def use_accumulation_register(self, size: int, indices_to_accumulate: List[int]): + """ + Sets up the accumulation register of the lattice. + + This register has a weighted value added to it at the end of each time step. + The value is a linear combination of the occupancy of several velocity channels. + This in turn allows for time-averaged calculations for values related to pressure, + mass, or drag/lift coefficients. + + Parameters + ---------- + size : int + The size of the register that accumulates the value. + indices_to_accumulate : List[int] + The qubit indices that contribute to the accumulated value. + """ + if size <= 0: + raise LatticeException( + f"Accumulation register size has to be positive. Provided value: {size}" + ) + + if any(i < 0 or i >= self.num_base_qubits for i in indices_to_accumulate): + raise LatticeException( + f"Accumulation indices have to be between 0 and {self.num_base_qubits} for this lattice." + ) + + self.num_accumulation_qubits = size + self.indices_to_accumulate = indices_to_accumulate + + self.__update_registers() + + def accumulation_index(self) -> List[int]: + """ + Get the indices of the qubits used for the accumulation register. + + Returns + ------- + List[int] + The absolute indices of the accumulation qubits. + """ + return list( + range( + self.num_base_qubits + self.num_marker_qubits, + self.num_base_qubits + + self.num_marker_qubits + + self.num_accumulation_qubits, + ) + ) diff --git a/qlbm/lattice/lattices/collisionless_lattice.py b/qlbm/lattice/lattices/ms_lattice.py similarity index 85% rename from qlbm/lattice/lattices/collisionless_lattice.py rename to qlbm/lattice/lattices/ms_lattice.py index efe3dcc..93530f0 100644 --- a/qlbm/lattice/lattices/collisionless_lattice.py +++ b/qlbm/lattice/lattices/ms_lattice.py @@ -6,16 +6,20 @@ from qiskit import QuantumCircuit, QuantumRegister from typing_extensions import override +from qlbm.components.ab.encodings import ABEncodingType from qlbm.lattice.geometry.shapes.base import Shape from qlbm.tools.exceptions import LatticeException from qlbm.tools.utils import dimension_letter, flatten, is_two_pow -from .base import Lattice +from .base import AmplitudeLattice -class CollisionlessLattice(Lattice): +class MSLattice(AmplitudeLattice): r""" - Implementation of the :class:`.Lattice` base specific to the 2D and 3D :class:`.CQLBM` algorithm developed by :cite:t:`collisionless`. + Implementation of the :class:`.Lattice` base specific to the 2D and 3D :class:`.MSQLBM` algorithm developed by :cite:t:`collisionless`. + + This lattice is only built from multi-speed specifications. + For :math:`D_dQ_q` implementations, see :class:`.ABQLBM`. =========================== ====================================================================== Attribute Summary @@ -132,9 +136,9 @@ class CollisionlessLattice(Lattice): .. plot:: :include-source: - from qlbm.lattice import CollisionlessLattice + from qlbm.lattice import MSLattice - CollisionlessLattice( + MSLattice( { "lattice": {"dim": {"x": 8, "y": 8}, "velocities": {"x": 4, "y": 4}}, "geometry": [{"shape":"cuboid", "x": [5, 6], "y": [1, 2], "boundary": "bounceback"}], @@ -230,28 +234,8 @@ def ancillae_velocity_index(self, dim: int | None = None) -> List[int]: # The velocity ancillas are on the first register, so no offset return [dim] + @override def ancillae_obstacle_index(self, index: int | None = None) -> List[int]: - """Get the indices of the qubits used as obstacle ancilla for the specified dimension. - - Parameters - ---------- - index : int | None, optional - The index of the grid for which to retrieve the obstacle qubit index, by default ``None``. - When ``index`` is ``None``, the indices of ancillae qubits for all dimensions are returned. - For 2D lattices with only bounce-back boundary-conditions, only one obstacle - qubit is required. - For all other configurations, the algorithm uses ``2d-2`` obstacle qubits. - - Returns - ------- - List[int] - A list of indices of the qubits used as obstacle ancilla for the given dimension. - - Raises - ------ - LatticeException - If the dimension does not exist. - """ if index is None: return list(range(self.num_dims, self.num_dims + self.num_obstacle_qubits)) @@ -263,28 +247,8 @@ def ancillae_obstacle_index(self, index: int | None = None) -> List[int]: # There are `d` ancillae velocity qubits "ahead" of this register return [self.num_dims + index] + @override def ancillae_comparator_index(self, index: int | None = None) -> List[int]: - """Get the indices of the qubits used as comparator ancillae for the specified index. - - Parameters - ---------- - index : int | None, optional - The index for which to retrieve the comparator qubit indices, by default ``None``. - There are `num_dims-1` available indices (i.e., 1 for 2D and 2 for 3D). - When `index` is ``None``, the indices of ancillae qubits for all dimensions are returned. - - Returns - ------- - List[int] - A list of indices of the qubits used as obstacle ancilla for the given dimension. - By convention, the 0th qubit in the returned list is used - for lower bound comparison and the 1st is used for upper bound comparisons. - - Raises - ------ - LatticeException - If the dimension does not exist. - """ # Ahead of this register # `d` ancillae velocity qubits # `num_obstacle_qubits` ancillae obstacle qubits @@ -305,25 +269,8 @@ def ancillae_comparator_index(self, index: int | None = None) -> List[int]: previous_qubits = self.num_dims + self.num_obstacle_qubits + 2 * index return list(range(previous_qubits, previous_qubits + 2)) + @override 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. - - Parameters - ---------- - dim : int | None, optional - The dimension of the grid for which to retrieve the grid qubit indices, by default ``None``. - When ``dim`` is ``None``, the indices of all grid qubits for all dimensions are returned. - - Returns - ------- - List[int] - A list of indices of the qubits used to encode the grid values for the given dimension. - - Raises - ------ - LatticeException - If the dimension does not exist. - """ if dim is None: return list( range( @@ -350,25 +297,8 @@ def grid_index(self, dim: int | None = None) -> List[int]: ) ) + @override def velocity_index(self, dim: int | None = None) -> List[int]: - """Get the indices of the qubits used that encode the velocity magnitude values for the specified dimension. - - Parameters - ---------- - dim : int | None, optional - The dimension of the grid for which to retrieve the velocity qubit indices, by default ``None``. - When ``dim`` is ``None``, the indices of all velocity magnitude qubits for all dimensions are returned. - - Returns - ------- - List[int] - A list of indices of the qubits used to encode the velocity magnitude values for the given dimension. - - Raises - ------ - LatticeException - If the dimension does not exist. - """ if dim is None: return list( range( @@ -531,7 +461,11 @@ def logger_name(self) -> str: if c < len(self.num_gridpoints) - 1: gp_string += "x" return f"{self.num_dims}d-{gp_string}-{len(self.shape_list)}-obstacle" - + @override def has_multiple_geometries(self): - return False # multiple geometries unsupported for CQBM + return False # multiple geometries unsupported for MS + + @override + def get_encoding(self): + return ABEncodingType.MS diff --git a/qlbm/lattice/lattices/oh_lattice.py b/qlbm/lattice/lattices/oh_lattice.py new file mode 100644 index 0000000..1583b65 --- /dev/null +++ b/qlbm/lattice/lattices/oh_lattice.py @@ -0,0 +1,245 @@ +"""Implementation of the One-Hot (OH) encoding lattice for generic DdQq discretizations.""" + +from logging import getLogger +from typing import Dict, List, Tuple + +from numpy import ceil, log2 +from qiskit import QuantumCircuit, QuantumRegister +from typing_extensions import override + +from qlbm.components.ab.encodings import ABEncodingType +from qlbm.lattice.geometry.shapes.base import Shape +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 + +from .ab_lattice import ABLattice + + +class OHLattice(ABLattice): + r""" + Implementation of the :class:`.Lattice` base specific to the 2D and 3D :class:`.ABQLBM` for the One-Hot (OH) encoding. + + In the OH encoding, the grid is compressed into logarithmically many qubits, + while the the velocity register is not. + For a :math:`1024 \times 1024` lattice with a :math:`D_2Q_9` discretization, the + OH encoding requires :math:`2\log_2 1024 + 9 = 29` qubits. + + Each of the discrete velocities is assigned a vector :math:`\ket{\mathbf{e}_j}`, + with entry :math:`1` at index :math:`j` and :math:`0` everywhere else. + + This lattice is only built from :math:`D_dQ_q` specifications. + For multi-speed implementations, see :class:`.MSQLBM` and :class:`.MSLattice`. + For the fully compressed velocity register counterpart, see :class:`.ABLattice`. + + The registers encoded in the lattice and their accessors are given below. + For the size of each register, + :math:`N_{g_j}` is the number of grid points of dimension :math:`j` (i.e., 64, 128), + :math:`q` is the number of discrete velocities, for instance, 9. + + .. list-table:: Register allocation + :widths: 25 25 25 50 + :header-rows: 1 + + * - Register + - Size + - Access Method + - Description + * - :attr:`grid_registers` + - :math:`\Sigma_{1\leq j \leq d} \left \lceil{\log N_{g_j}} \right \rceil` + - :meth:`grid_index` + - The qubits encoding the physical grid. + * - :attr:`velocity_registers` + - :math:`q` + - :meth:`velocity_index` + - The qubits encoding the :math:`q` discrete velocities. + * - :attr:`ancilla_obstacle_register` + - :math:`1` + - :meth:`ancillae_obstacle_index` + - The qubits used to detect whether particles have streamed into obstacles. Used for reflection. + * - :attr:`ancilla_comparator_register` + - :math:`2(d-1)` + - :meth:`ancillae_comparator_index` + - The qubits used to for :class:`.Comparator`\ s. Used for reflection. + + A lattice can be constructed from from either an input file or a Python dictionary: + + .. code-block:: json + + { + "lattice": { + "dim": { + "x": 16, + "y": 16 + }, + "velocities": "d2q9" + }, + "geometry": [ + { + "x": [9, 12], + "y": [3, 6], + "boundary": "bounceback" + }, + { + "x": [9, 12], + "y": [9, 12], + "boundary": "bounceback" + } + ] + } + + The register setup can be visualized by constructing a lattice object: + + .. plot:: + :include-source: + + from qlbm.lattice import OHLattice + + OHLattice( + { + "lattice": {"dim": {"x": 8, "y": 8}, "velocities": "D2Q9"}, + "geometry": [], + } + ).circuit.draw("mpl") + """ + + 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.""" + + registers: Tuple[QuantumRegister, ...] + """The registers of the lattice.""" + + def __init__( + self, + lattice_data, + logger=getLogger("qlbm"), + ): + super().__init__(lattice_data, logger) + self.num_gridpoints, self.num_velocities, self.shapes, self.discretization = ( + self.parse_input_data(lattice_data) + ) # type: ignore + self.geometries: List[Dict[str, List[Shape]]] = [self.shapes] + self.num_dims = len(self.num_gridpoints) + self.num_velocities_per_point = ( + LatticeDiscretizationProperties.get_num_velocities(self.discretization) + ) + + 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)}." + ) + + self.num_grid_qubits = int( + sum(map(lambda x: ceil(log2(x)), self.num_gridpoints)) + ) + self.num_velocity_qubits = self.num_velocities_per_point + self.num_base_qubits = self.num_grid_qubits + self.num_velocity_qubits + + self.num_obstacle_qubits = self.__num_obstacle_qubits() + self.num_comparator_qubits = 2 * (self.num_dims - 1) + self.num_ancilla_qubits = self.num_comparator_qubits + self.num_obstacle_qubits + + self.num_total_qubits = self.num_base_qubits + self.num_ancilla_qubits + + temporary_registers = self.get_registers() + ( + self.grid_registers, + self.velocity_registers, + self.ancilla_comparator_register, + self.ancilla_object_register, + ) = temporary_registers + + self.registers = tuple(flatten(temporary_registers)) + self.circuit = QuantumCircuit(*self.registers) + + @override + def get_registers(self) -> Tuple[List[QuantumRegister], ...]: + """Generates the encoding-specific register required for the streaming step. + + For this encoding, different registers encode + (i) the logarithmically compressed grid, + (ii) the uncompressed discrete velocities, + (iii) the comparator qubits, + (iv) the object qubits. + + Returns + ------- + List[int] + Tuple[QuantumRegister]: The 4-tuple of qubit registers encoding the streaming step. + """ + # d ancilla qubits used to conditionally reflect velocities + ancilla_object_register = [ + QuantumRegister(self.num_obstacle_qubits, name="a_o") + ] + + # 2(d-1) ancilla qubits + ancilla_comparator_register = [ + QuantumRegister(self.num_comparator_qubits, name="a_c") + ] + + # Velocity qubits + velocity_registers = [QuantumRegister(self.num_velocity_qubits, name="v")] + + # Grid qubits + grid_registers = [ + QuantumRegister(gp.bit_length(), name=f"g_{dimension_letter(c)}") + for c, gp in enumerate(self.num_gridpoints) + ] + + return ( + grid_registers, + velocity_registers, + ancilla_comparator_register, + ancilla_object_register, + ) + + @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"ohlattice-{self.num_dims}d-{gp_string}-{len(flatten(list(self.shapes.values())))}-obstacle" + + @override + def has_multiple_geometries(self): + return False # multiple geometries unsupported for ABQLBM right now + + @override + def get_encoding(self) -> ABEncodingType: + return ABEncodingType.OH + + def __num_obstacle_qubits(self) -> int: + all_obstacle_bounceback: bool = len( + [ + b + for b in flatten(list(self.shapes.values())) + if b.boundary_condition == "bounceback" + ] + ) == len(flatten(list(self.shapes.values()))) + if all_obstacle_bounceback: + # A single qubit suffices to determine + # Whether particles have streamed inside the object + return 1 + # If there is at least one object with specular reflection + # 2 ancilla qubits are required for velocity inversion + else: + return self.num_dims diff --git a/qlbm/lattice/spacetime/properties_base.py b/qlbm/lattice/spacetime/properties_base.py index 0d7d29a..e3e2b42 100644 --- a/qlbm/lattice/spacetime/properties_base.py +++ b/qlbm/lattice/spacetime/properties_base.py @@ -13,6 +13,8 @@ import numpy as np from qiskit import QuantumRegister +from qlbm.tools.exceptions import LatticeException + class LatticeDiscretization(Enum): """ @@ -26,6 +28,7 @@ class LatticeDiscretization(Enum): D2Q4 = (2,) D3Q6 = (3,) D1Q3 = (4,) + D2Q9 = (5,) class LatticeDiscretizationProperties: @@ -51,6 +54,19 @@ class LatticeDiscretizationProperties: [[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]]), + LatticeDiscretization.D2Q9: np.array( + [ + [0, 0], + [1, 0], + [0, 1], + [-1, 0], + [0, -1], + [1, 1], + [-1, 1], + [-1, -1], + [1, -1], + ] + ), } channel_masses: Dict[LatticeDiscretization, np.ndarray] = { @@ -58,6 +74,7 @@ class LatticeDiscretizationProperties: LatticeDiscretization.D2Q4: np.ones(4), LatticeDiscretization.D3Q6: np.ones(6), LatticeDiscretization.D1Q3: np.concatenate((np.array([2]), np.ones(2))), + LatticeDiscretization.D2Q9: np.concatenate((np.array([2]), np.ones(8))), } num_velocities: Dict[LatticeDiscretization, int] = { @@ -65,6 +82,7 @@ class LatticeDiscretizationProperties: LatticeDiscretization.D2Q4: 4, LatticeDiscretization.D3Q6: 6, LatticeDiscretization.D1Q3: 3, + LatticeDiscretization.D2Q9: 9, } num_dimensions: Dict[LatticeDiscretization, int] = { @@ -72,6 +90,7 @@ class LatticeDiscretizationProperties: LatticeDiscretization.D2Q4: 2, LatticeDiscretization.D3Q6: 3, LatticeDiscretization.D1Q3: 1, + LatticeDiscretization.D2Q9: 2, } string_representation: Dict[LatticeDiscretization, str] = { @@ -79,6 +98,7 @@ class LatticeDiscretizationProperties: LatticeDiscretization.D2Q4: "D2Q4", LatticeDiscretization.D3Q6: "D3Q6", LatticeDiscretization.D1Q3: "D1Q3", + LatticeDiscretization.D2Q9: "D2Q9", } @staticmethod @@ -118,7 +138,7 @@ def get_num_velocities( The number of velocities corresponding to the discretization. """ if discretization not in LatticeDiscretizationProperties.num_velocities: - raise ValueError(f"Discretization {discretization} is not supported.") + raise LatticeException(f"Discretization {discretization} is not supported.") return LatticeDiscretizationProperties.num_velocities[discretization] diff --git a/test/integration/compiler_test.py b/test/integration/compiler_test.py index 39ef697..be11dac 100644 --- a/test/integration/compiler_test.py +++ b/test/integration/compiler_test.py @@ -5,22 +5,22 @@ from qiskit_aer import AerSimulator from qulacs import QuantumCircuit as QulacsQC -from qlbm.components import CollisionlessStreamingOperator +from qlbm.components import MSStreamingOperator from qlbm.infra import ( CircuitCompiler, ) -from qlbm.lattice import CollisionlessLattice +from qlbm.lattice import MSLattice from qlbm.tools.utils import get_time_series @pytest.fixture def lattice_asymmetric_medium_3d(): - return CollisionlessLattice("test/resources/asymmetric_3d_no_obstacles.json") + return MSLattice("test/resources/asymmetric_3d_no_obstacles.json") @pytest.fixture def lattice_symmetric_small_2d(): - return CollisionlessLattice("test/resources/symmetric_2d_no_obstacles.json") + return MSLattice("test/resources/symmetric_2d_no_obstacles.json") @pytest.mark.parametrize( @@ -43,7 +43,7 @@ def test_qiskit_target_compilation( lattice.num_velocities[0] + 1 ) # +1 because velocities are between 0 and n_vi velocities = get_time_series(num_velocities)[velocity] - op = CollisionlessStreamingOperator(lattice, velocities) + op = MSStreamingOperator(lattice, velocities) compiler = CircuitCompiler(compiler_platform, "QISKIT") compiled_circuit = compiler.compile(op, backend, 0) @@ -69,7 +69,7 @@ def test_qiskit_target_compilation( # lattice.num_velocities[0] + 1 # ) # +1 because velocities are between 0 and n_vi # velocities = get_time_series(num_velocities)[velocity] -# op = CollisionlessStreamingOperator(lattice, velocities) +# op = MSStreamingOperator(lattice, velocities) # compiler = CircuitCompiler(compiler_platform, "QULACS") # # Qulacs backend is determined automatically diff --git a/test/integration/execution_test.py b/test/integration/execution_test.py index 23218c2..97e05fa 100644 --- a/test/integration/execution_test.py +++ b/test/integration/execution_test.py @@ -1,12 +1,12 @@ import pytest from qiskit_aer import AerSimulator -from qlbm.components.collisionless import ( - CQLBM, - CollisionlessInitialConditions, +from qlbm.components.common import EmptyPrimitive +from qlbm.components.ms import ( + MSQLBM, GridMeasurement, + MSInitialConditions, ) -from qlbm.components.common import EmptyPrimitive from qlbm.components.spacetime import ( SpaceTimeGridVelocityMeasurement, SpaceTimeQLBM, @@ -16,7 +16,7 @@ ) from qlbm.infra.runner import QiskitRunner from qlbm.infra.runner.simulation_config import SimulationConfig -from qlbm.lattice import CollisionlessLattice +from qlbm.lattice import MSLattice from qlbm.lattice.lattices.spacetime_lattice import SpaceTimeLattice OUTPUT_DIR = "test/artifacts" @@ -24,11 +24,11 @@ @pytest.fixture def collisionless_circuits(): - lattice = CollisionlessLattice("test/resources/symmetric_2d_1_obstacle.json") + lattice = MSLattice("test/resources/symmetric_2d_1_obstacle.json") return { - "initial_conditions": CollisionlessInitialConditions(lattice), - "algorithm": CQLBM(lattice), + "initial_conditions": MSInitialConditions(lattice), + "algorithm": MSQLBM(lattice), "postprocessing": EmptyPrimitive(lattice), "measurement": GridMeasurement(lattice), "lattice": lattice, diff --git a/test/integration/simulation_config_test.py b/test/integration/simulation_config_test.py index e720aed..f7b49dd 100644 --- a/test/integration/simulation_config_test.py +++ b/test/integration/simulation_config_test.py @@ -4,24 +4,24 @@ from qiskit import QuantumCircuit as QiskitQC from qiskit_aer import AerSimulator -from qlbm.components.collisionless import ( - CQLBM, - CollisionlessInitialConditions, +from qlbm.components.common import EmptyPrimitive +from qlbm.components.ms import ( + MSQLBM, GridMeasurement, + MSInitialConditions, ) -from qlbm.components.common import EmptyPrimitive from qlbm.infra.runner.simulation_config import SimulationConfig -from qlbm.lattice import CollisionlessLattice +from qlbm.lattice import MSLattice from qlbm.tools.exceptions import ExecutionException @pytest.fixture def symmetric_2d_no_osbtacle_circuits(): - lattice = CollisionlessLattice("test/resources/symmetric_2d_no_obstacles.json") + lattice = MSLattice("test/resources/symmetric_2d_no_obstacles.json") return { - "initial_conditions": CollisionlessInitialConditions(lattice), - "algorithm": CQLBM(lattice), + "initial_conditions": MSInitialConditions(lattice), + "algorithm": MSQLBM(lattice), "postprocessing": EmptyPrimitive(lattice), "measurement": GridMeasurement(lattice), } @@ -29,11 +29,11 @@ def symmetric_2d_no_osbtacle_circuits(): @pytest.fixture def symmetric_2d_one_osbtacle_circuits(): - lattice = CollisionlessLattice("test/resources/symmetric_2d_1_obstacle.json") + lattice = MSLattice("test/resources/symmetric_2d_1_obstacle.json") return { - "initial_conditions": CollisionlessInitialConditions(lattice), - "algorithm": CQLBM(lattice), + "initial_conditions": MSInitialConditions(lattice), + "algorithm": MSQLBM(lattice), "postprocessing": EmptyPrimitive(lattice), "measurement": GridMeasurement(lattice), } diff --git a/test/unit/ab/__init__.py b/test/unit/ab/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/unit/ab/ab_reflection_permutation_test.py b/test/unit/ab/ab_reflection_permutation_test.py new file mode 100644 index 0000000..26dc143 --- /dev/null +++ b/test/unit/ab/ab_reflection_permutation_test.py @@ -0,0 +1,38 @@ +import pytest +from qiskit import QuantumCircuit, transpile +from qiskit_aer import AerSimulator + +from qlbm.components.ab.encodings import ABEncodingType +from qlbm.components.ab.reflection import ABReflectionPermutation +from qlbm.lattice.spacetime.properties_base import LatticeDiscretization +from qlbm.tools.utils import bit_value + + +@pytest.mark.parametrize( + "permutation_outcome_pairs", + [(0, 0), (1, 3), (2, 4), (3, 1), (4, 2), (5, 7), (6, 8), (7, 5), (8, 6)] + + [(i, i) for i in range(9, 16)], +) +def test_reflectionpermutation_outcomes_d2q9(permutation_outcome_pairs): + nq = 4 + sim = AerSimulator() + + qc = QuantumCircuit(nq) + for q in range(nq): + if bit_value(permutation_outcome_pairs[0], q): + qc.x(q) + + qc.compose( + ABReflectionPermutation( + nq, LatticeDiscretization.D2Q9, ABEncodingType.AB + ).circuit, + inplace=True, + ) + qc.measure_all() + tqc = transpile(qc, sim, optimization_level=0) + + counts = sim.run(tqc, shots=128).result().get_counts() + + assert all(int(c, 2) == permutation_outcome_pairs[1] for c in counts.keys()), ( + f"{permutation_outcome_pairs} handled incorrectly. Expected {permutation_outcome_pairs}, got {counts}." + ) diff --git a/test/unit/blocks_2d_test.py b/test/unit/blocks_2d_test.py index 1888ae3..c862add 100644 --- a/test/unit/blocks_2d_test.py +++ b/test/unit/blocks_2d_test.py @@ -2,7 +2,7 @@ import pytest -from qlbm.lattice.geometry.encodings.collisionless import ReflectionPoint +from qlbm.lattice.geometry.encodings.ms import ReflectionPoint from qlbm.lattice.geometry.shapes.block import Block from qlbm.tools.utils import flatten from test.regression.blocks import Block2D as RegressionBlock2D diff --git a/test/unit/blocks_3d_test.py b/test/unit/blocks_3d_test.py index d3353eb..89adb4b 100644 --- a/test/unit/blocks_3d_test.py +++ b/test/unit/blocks_3d_test.py @@ -2,7 +2,7 @@ import pytest -from qlbm.lattice.geometry.encodings.collisionless import ReflectionWall +from qlbm.lattice.geometry.encodings.ms import ReflectionWall from qlbm.lattice.geometry.shapes.block import Block diff --git a/test/unit/collisionles_lattice_test.py b/test/unit/collisionles_lattice_test.py index f69fa66..fd4d850 100644 --- a/test/unit/collisionles_lattice_test.py +++ b/test/unit/collisionles_lattice_test.py @@ -1,12 +1,12 @@ import pytest -from qlbm.lattice import CollisionlessLattice +from qlbm.lattice import MSLattice from qlbm.tools.exceptions import LatticeException @pytest.fixture -def lattice_2d_16x16_1_obstacle() -> CollisionlessLattice: - return CollisionlessLattice( +def lattice_2d_16x16_1_obstacle() -> MSLattice: + return MSLattice( { "lattice": { "dim": {"x": 16, "y": 16}, @@ -20,8 +20,8 @@ def lattice_2d_16x16_1_obstacle() -> CollisionlessLattice: @pytest.fixture -def lattice_2d_16x16_1_obstacle_asymmetric() -> CollisionlessLattice: - return CollisionlessLattice( +def lattice_2d_16x16_1_obstacle_asymmetric() -> MSLattice: + return MSLattice( { "lattice": { "dim": {"x": 16, "y": 64}, @@ -35,8 +35,8 @@ def lattice_2d_16x16_1_obstacle_asymmetric() -> CollisionlessLattice: @pytest.fixture -def lattice_2d_16x16_1_obstacle_bounceback() -> CollisionlessLattice: - return CollisionlessLattice( +def lattice_2d_16x16_1_obstacle_bounceback() -> MSLattice: + return MSLattice( { "lattice": { "dim": {"x": 16, "y": 16}, @@ -55,8 +55,8 @@ def lattice_2d_16x16_1_obstacle_bounceback() -> CollisionlessLattice: @pytest.fixture -def lattice_2d_16x16_2_obstacle_mixed() -> CollisionlessLattice: - return CollisionlessLattice( +def lattice_2d_16x16_2_obstacle_mixed() -> MSLattice: + return MSLattice( { "lattice": { "dim": {"x": 16, "y": 16}, @@ -76,8 +76,8 @@ def lattice_2d_16x16_2_obstacle_mixed() -> CollisionlessLattice: @pytest.fixture -def lattice_3d_8x8x8_2_obstacle_mixed() -> CollisionlessLattice: - return CollisionlessLattice( +def lattice_3d_8x8x8_2_obstacle_mixed() -> MSLattice: + return MSLattice( { "lattice": { "dim": {"x": 8, "y": 8, "z": 8}, @@ -104,8 +104,8 @@ def lattice_3d_8x8x8_2_obstacle_mixed() -> CollisionlessLattice: @pytest.fixture -def lattice_3d_8x8x8_1_obstacle_bounceback() -> CollisionlessLattice: - return CollisionlessLattice( +def lattice_3d_8x8x8_1_obstacle_bounceback() -> MSLattice: + return MSLattice( { "lattice": { "dim": {"x": 8, "y": 8, "z": 8}, @@ -126,21 +126,21 @@ def lattice_3d_8x8x8_1_obstacle_bounceback() -> CollisionlessLattice: def test_lattice_exception_empty_dict(): with pytest.raises(LatticeException) as excinfo: - CollisionlessLattice({}) + MSLattice({}) assert 'Input configuration missing "lattice" properties.' == str(excinfo.value) def test_lattice_exception_no_dims(): with pytest.raises(LatticeException) as excinfo: - CollisionlessLattice({"lattice": {}}) + MSLattice({"lattice": {}}) assert 'Lattice configuration missing "dim" properties.' == str(excinfo.value) def test_lattice_exception_no_velocities(): with pytest.raises(LatticeException) as excinfo: - CollisionlessLattice({"lattice": {"dim": {}}}) + MSLattice({"lattice": {"dim": {}}}) assert 'Lattice configuration missing "velocities" properties.' == str( excinfo.value @@ -149,14 +149,14 @@ def test_lattice_exception_no_velocities(): def test_lattice_exception_mismatched_velocities_and_dims(): with pytest.raises(LatticeException) as excinfo: - CollisionlessLattice({"lattice": {"dim": {"x": 64}, "velocities": [4, 4]}}) + MSLattice({"lattice": {"dim": {"x": 64}, "velocities": [4, 4]}}) assert "Lattice configuration dimensionality is inconsistent." == str(excinfo.value) def test_lattice_exception_mismatched_too_many_dimensions(): with pytest.raises(LatticeException) as excinfo: - CollisionlessLattice( + MSLattice( { "lattice": { "dim": {"x": 64, "y": 64, "z": 128, "w": 64}, @@ -173,7 +173,7 @@ def test_lattice_exception_mismatched_too_many_dimensions(): def test_lattice_exception_mismatched_bad_dimensions(): with pytest.raises(LatticeException) as excinfo: - CollisionlessLattice( + MSLattice( { "lattice": { "dim": {"x": 64, "y": 127}, @@ -190,7 +190,7 @@ def test_lattice_exception_mismatched_bad_dimensions(): def test_lattice_exception_mismatched_bad_velocities(): with pytest.raises(LatticeException) as excinfo: - CollisionlessLattice( + MSLattice( { "lattice": { "dim": {"x": 64, "y": 64}, @@ -207,7 +207,7 @@ def test_lattice_exception_mismatched_bad_velocities(): def test_lattice_exception_mismatched_bad_object_dimensions(): with pytest.raises(LatticeException) as excinfo: - CollisionlessLattice( + MSLattice( { "lattice": { "dim": {"x": 64, "y": 64}, @@ -232,7 +232,7 @@ def test_lattice_exception_mismatched_bad_object_dimensions(): def test_lattice_exception_mismatched_bad_object_bound_specification(): with pytest.raises(LatticeException) as excinfo: - CollisionlessLattice( + MSLattice( { "lattice": { "dim": {"x": 64, "y": 64}, @@ -254,7 +254,7 @@ def test_lattice_exception_mismatched_bad_object_bound_specification(): def test_lattice_exception_mismatched_bad_object_bound_decreasing(): with pytest.raises(LatticeException) as excinfo: - CollisionlessLattice( + MSLattice( { "lattice": { "dim": {"x": 64, "y": 64}, @@ -276,7 +276,7 @@ def test_lattice_exception_mismatched_bad_object_bound_decreasing(): def test_lattice_exception_mismatched_bad_object_out_of_bounds(): with pytest.raises(LatticeException) as excinfo: - CollisionlessLattice( + MSLattice( { "lattice": { "dim": {"x": 64, "y": 64}, @@ -296,7 +296,7 @@ def test_lattice_exception_mismatched_bad_object_out_of_bounds(): assert "Obstacle 1 is out of bounds in the x-dimension." == str(excinfo.value) with pytest.raises(LatticeException) as excinfo: - CollisionlessLattice( + MSLattice( { "lattice": { "dim": {"x": 64, "y": 64}, @@ -318,7 +318,7 @@ def test_lattice_exception_mismatched_bad_object_out_of_bounds(): def test_lattice_exception_bad_object_boundary_conditions(): with pytest.raises(LatticeException) as excinfo: - CollisionlessLattice( + MSLattice( { "lattice": { "dim": {"x": 64, "y": 64}, @@ -342,7 +342,7 @@ def test_lattice_exception_bad_object_boundary_conditions(): def test_lattice_exception_no_object_boundary_conditions(): with pytest.raises(LatticeException) as excinfo: - CollisionlessLattice( + MSLattice( { "lattice": { "dim": {"x": 64, "y": 64}, @@ -364,7 +364,7 @@ def test_lattice_exception_no_object_boundary_conditions(): def test_lattice_exception_missing_shape(): with pytest.raises(LatticeException) as excinfo: - CollisionlessLattice( + MSLattice( { "lattice": { "dim": {"x": 64, "y": 64}, @@ -381,7 +381,7 @@ def test_lattice_exception_missing_shape(): def test_lattice_exception_unsupported_shape(): with pytest.raises(LatticeException) as excinfo: - CollisionlessLattice( + MSLattice( { "lattice": { "dim": {"x": 64, "y": 64}, @@ -404,7 +404,7 @@ def test_lattice_exception_unsupported_shape(): ) -def test_2d_lattice_basic_properties(lattice_2d_16x16_1_obstacle: CollisionlessLattice): +def test_2d_lattice_basic_properties(lattice_2d_16x16_1_obstacle: MSLattice): assert lattice_2d_16x16_1_obstacle.num_dims == 2 assert lattice_2d_16x16_1_obstacle.num_gridpoints == [15, 15] assert lattice_2d_16x16_1_obstacle.num_velocities == [3, 3] @@ -415,7 +415,7 @@ def test_2d_lattice_basic_properties(lattice_2d_16x16_1_obstacle: CollisionlessL def test_2d_lattice_basic_qubit_register_size( - lattice_2d_16x16_1_obstacle: CollisionlessLattice, + lattice_2d_16x16_1_obstacle: MSLattice, ): # Ancilla velocity (1r2q) # ancilla obstacle (1r2q) @@ -428,7 +428,7 @@ def test_2d_lattice_basic_qubit_register_size( def test_2d_lattice_ancilla_velocity_register( - lattice_2d_16x16_1_obstacle: CollisionlessLattice, + lattice_2d_16x16_1_obstacle: MSLattice, ): # Ancilla velocity (1r2q) # ancilla obstacle (1r2q) @@ -450,7 +450,7 @@ def test_2d_lattice_ancilla_velocity_register( def test_2d_lattice_ancilla_obstacle_register( - lattice_2d_16x16_1_obstacle: CollisionlessLattice, + lattice_2d_16x16_1_obstacle: MSLattice, ): # Ancilla velocity (1r2q) # ancilla obstacle (1r2q) @@ -472,7 +472,7 @@ def test_2d_lattice_ancilla_obstacle_register( def test_2d_lattice_ancilla_comparator_register( - lattice_2d_16x16_1_obstacle: CollisionlessLattice, + lattice_2d_16x16_1_obstacle: MSLattice, ): assert lattice_2d_16x16_1_obstacle.ancillae_comparator_index(0) == [4, 5] assert lattice_2d_16x16_1_obstacle.ancillae_comparator_index() == [4, 5] @@ -485,7 +485,7 @@ def test_2d_lattice_ancilla_comparator_register( ) -def test_2d_lattice_grid_register(lattice_2d_16x16_1_obstacle: CollisionlessLattice): +def test_2d_lattice_grid_register(lattice_2d_16x16_1_obstacle: MSLattice): assert lattice_2d_16x16_1_obstacle.grid_index(0) == [6, 7, 8, 9] assert lattice_2d_16x16_1_obstacle.grid_index(1) == [10, 11, 12, 13] assert lattice_2d_16x16_1_obstacle.grid_index() == list(range(6, 14)) @@ -499,7 +499,7 @@ def test_2d_lattice_grid_register(lattice_2d_16x16_1_obstacle: CollisionlessLatt def test_2d_lattice_velocity_register( - lattice_2d_16x16_1_obstacle: CollisionlessLattice, + lattice_2d_16x16_1_obstacle: MSLattice, ): assert lattice_2d_16x16_1_obstacle.velocity_index(0) == [14] assert lattice_2d_16x16_1_obstacle.velocity_index(1) == [15] @@ -514,7 +514,7 @@ def test_2d_lattice_velocity_register( def test_2d_lattice_velocity_dir_register( - lattice_2d_16x16_1_obstacle: CollisionlessLattice, + lattice_2d_16x16_1_obstacle: MSLattice, ): assert lattice_2d_16x16_1_obstacle.velocity_dir_index(0) == [16] assert lattice_2d_16x16_1_obstacle.velocity_dir_index(1) == [17] @@ -529,8 +529,8 @@ def test_2d_lattice_velocity_dir_register( def test_2d_asymmetric_lattice_ancialle_registers( - lattice_2d_16x16_1_obstacle: CollisionlessLattice, - lattice_2d_16x16_1_obstacle_asymmetric: CollisionlessLattice, + lattice_2d_16x16_1_obstacle: MSLattice, + lattice_2d_16x16_1_obstacle_asymmetric: MSLattice, ): assert lattice_2d_16x16_1_obstacle_asymmetric.num_gridpoints == [15, 63] assert lattice_2d_16x16_1_obstacle_asymmetric.num_velocities == [15, 3] @@ -563,7 +563,7 @@ def test_2d_asymmetric_lattice_ancialle_registers( def test_2d_asymmetric_lattice_grid_registers( - lattice_2d_16x16_1_obstacle_asymmetric: CollisionlessLattice, + lattice_2d_16x16_1_obstacle_asymmetric: MSLattice, ): assert lattice_2d_16x16_1_obstacle_asymmetric.grid_index(0) == [6, 7, 8, 9] assert lattice_2d_16x16_1_obstacle_asymmetric.grid_index(1) == list(range(10, 16)) @@ -578,7 +578,7 @@ def test_2d_asymmetric_lattice_grid_registers( def test_2d_asymmetric_lattice_velocity_register( - lattice_2d_16x16_1_obstacle_asymmetric: CollisionlessLattice, + lattice_2d_16x16_1_obstacle_asymmetric: MSLattice, ): assert lattice_2d_16x16_1_obstacle_asymmetric.velocity_index(0) == [16, 17, 18] assert lattice_2d_16x16_1_obstacle_asymmetric.velocity_index(1) == [19] @@ -595,7 +595,7 @@ def test_2d_asymmetric_lattice_velocity_register( def test_2d_asymmetric_lattice_velocity_dir_register( - lattice_2d_16x16_1_obstacle_asymmetric: CollisionlessLattice, + lattice_2d_16x16_1_obstacle_asymmetric: MSLattice, ): assert lattice_2d_16x16_1_obstacle_asymmetric.velocity_dir_index(0) == [20] assert lattice_2d_16x16_1_obstacle_asymmetric.velocity_dir_index(1) == [21] diff --git a/test/unit/cqlbm_test.py b/test/unit/cqlbm_test.py index 81388f7..3ce9baa 100644 --- a/test/unit/cqlbm_test.py +++ b/test/unit/cqlbm_test.py @@ -1,12 +1,12 @@ import pytest from qlbm.components import CQLBM -from qlbm.lattice import CollisionlessLattice +from qlbm.lattice import MSLattice @pytest.fixture -def lattice_2d_16x16_1_object() -> CollisionlessLattice: - return CollisionlessLattice( +def lattice_2d_16x16_1_object() -> MSLattice: + return MSLattice( { "lattice": { "dim": {"x": 16, "y": 16}, @@ -19,5 +19,5 @@ def lattice_2d_16x16_1_object() -> CollisionlessLattice: ) -def test_construction(lattice_2d_16x16_1_object: CollisionlessLattice): +def test_construction(lattice_2d_16x16_1_object: MSLattice): CQLBM(lattice=lattice_2d_16x16_1_object) diff --git a/test/unit/incrementer_test.py b/test/unit/incrementer_test.py index 5d012ff..3af3abc 100644 --- a/test/unit/incrementer_test.py +++ b/test/unit/incrementer_test.py @@ -1,17 +1,17 @@ import pytest -from qlbm.components.collisionless.streaming import ControlledIncrementer -from qlbm.lattice import CollisionlessLattice +from qlbm.components.ms.streaming import ControlledIncrementer +from qlbm.lattice import MSLattice @pytest.fixture def lattice_asymmetric_medium_3d(): - return CollisionlessLattice("test/resources/asymmetric_3d_no_obstacles.json") + return MSLattice("test/resources/asymmetric_3d_no_obstacles.json") @pytest.fixture def lattice_symmetric_small_2d(): - return CollisionlessLattice("test/resources/symmetric_2d_no_obstacles.json") + return MSLattice("test/resources/symmetric_2d_no_obstacles.json") def test_3d_incrementer_size(lattice_asymmetric_medium_3d): diff --git a/test/unit/lattice/__init__.py b/test/unit/lattice/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/unit/lattice/abe_lattice_exception_test.py b/test/unit/lattice/abe_lattice_exception_test.py new file mode 100644 index 0000000..ca3032e --- /dev/null +++ b/test/unit/lattice/abe_lattice_exception_test.py @@ -0,0 +1,83 @@ +import pytest + +from qlbm.lattice import ABLattice +from qlbm.tools.exceptions import LatticeException + + +def test_lattice_exception_empty_dict(): + with pytest.raises(LatticeException) as excinfo: + ABLattice({}) + + assert 'Input configuration missing "lattice" properties.' == str(excinfo.value) + + +def test_lattice_exception_no_dims(): + with pytest.raises(LatticeException) as excinfo: + ABLattice({"lattice": {}}) + + assert 'Lattice configuration missing "dim" properties.' == str(excinfo.value) + + +def test_lattice_exception_no_velocities(): + with pytest.raises(LatticeException) as excinfo: + ABLattice({"lattice": {"dim": {}}}) + + assert 'Lattice configuration missing "velocities" properties.' == str( + excinfo.value + ) + +def test_lattice_exception_mismatched_velocities_and_dims(): + with pytest.raises(LatticeException) as excinfo: + ABLattice({"lattice": {"dim": {"x": 64}, "velocities": "D2Q4"}}) + + assert "Velocity specification dimensions (2) do not match lattice dimensions (1)." == str(excinfo.value) + + +def test_lattice_exception_unsupported_discretization(): + with pytest.raises(LatticeException) as excinfo: + ABLattice({"lattice": {"dim": {"x": 64}, "velocities": {"x": 4}}}) + + assert 'Discretization LatticeDiscretization.CFLDISCRETIZATION is not supported.' == str( + excinfo.value + ) + +def test_lattice_exception_mismatched_bad_dimensions(): + with pytest.raises(LatticeException) as excinfo: + ABLattice( + { + "lattice": { + "dim": {"x": 64, "y": 127}, + "velocities": "D2Q4", + } + } + ) + + assert ( + "Lattice has a number of grid points that is not divisible by 2 in dimension y." + == str(excinfo.value) + ) + +def test_lattice_exception_mismatched_bad_object_dimensions(): + with pytest.raises(LatticeException) as excinfo: + ABLattice( + { + "lattice": { + "dim": {"x": 64, "y": 64}, + "velocities": "D2Q4", + }, + "geometry": [ + { + "shape": "cuboid", + "x": [5, 6], + "y": [1, 2], + "z": [1, 2], + "boundary": "specular", + }, + ], + } + ) + + assert "Obstacle 1 has 3 dimensions whereas the lattice has 2." == str( + excinfo.value + ) + \ No newline at end of file diff --git a/test/unit/lattice/abe_lattice_properties_test.py b/test/unit/lattice/abe_lattice_properties_test.py new file mode 100644 index 0000000..278e1d1 --- /dev/null +++ b/test/unit/lattice/abe_lattice_properties_test.py @@ -0,0 +1,58 @@ +import pytest + +from qlbm.lattice import ABLattice +from qlbm.tools.exceptions import LatticeException + + +def test_2d_abe_lattice_basic_properties(lattice_2d_16x16_1_obstacle: ABLattice): + assert lattice_2d_16x16_1_obstacle.num_dims == 2 + assert lattice_2d_16x16_1_obstacle.num_gridpoints == [15, 15] + assert lattice_2d_16x16_1_obstacle.num_ancilla_qubits == 3 + assert lattice_2d_16x16_1_obstacle.num_grid_qubits == 8 + assert lattice_2d_16x16_1_obstacle.num_velocity_qubits == 2 + assert lattice_2d_16x16_1_obstacle.num_total_qubits == 13 + + +def test_2d_lattice_grid_register(lattice_2d_16x16_1_obstacle: ABLattice): + assert lattice_2d_16x16_1_obstacle.grid_index(0) == list(range(4)) + assert lattice_2d_16x16_1_obstacle.grid_index(1) == list(range(4, 8)) + assert lattice_2d_16x16_1_obstacle.grid_index() == list(range(8)) + + with pytest.raises(LatticeException) as excinfo: + lattice_2d_16x16_1_obstacle.grid_index(2) + assert ( + "Cannot index grid register for dimension 2 in 2-dimensional lattice." + == str(excinfo.value) + ) + + +def test_2d_lattice_velocity_register( + lattice_2d_16x16_1_obstacle: ABLattice, +): + assert lattice_2d_16x16_1_obstacle.velocity_index() == [8, 9] + +def test_2d_lattice_ancilla_comparator_register( + lattice_2d_16x16_1_obstacle: ABLattice, +): + assert lattice_2d_16x16_1_obstacle.ancillae_comparator_index(0) == [10, 11] + assert lattice_2d_16x16_1_obstacle.ancillae_comparator_index() == [10, 11] + + with pytest.raises(LatticeException) as excinfo: + lattice_2d_16x16_1_obstacle.ancillae_comparator_index(1) + assert ( + "Cannot index ancilla comparator register for index 1 in 2-dimensional lattice. Maximum is 0." + == str(excinfo.value) + ) + + +def test_2d_lattice_ancilla_obstacle_register( + lattice_2d_16x16_1_obstacle: ABLattice, +): + assert lattice_2d_16x16_1_obstacle.ancillae_obstacle_index() == [8 + 2 + 2] + + with pytest.raises(LatticeException) as excinfo: + lattice_2d_16x16_1_obstacle.ancillae_obstacle_index(2) + assert ( + "Cannot index ancilla obstacle register for index 2. Maximum index for this lattice is 0." + == str(excinfo.value) + ) diff --git a/test/unit/lattice/conftest.py b/test/unit/lattice/conftest.py new file mode 100644 index 0000000..1c58d5b --- /dev/null +++ b/test/unit/lattice/conftest.py @@ -0,0 +1,79 @@ +import pytest + +from qlbm.lattice.geometry.shapes.block import Block +from qlbm.lattice.lattices.ab_lattice import ABLattice + + +# 1D Lattices +@pytest.fixture +def dummy_1d_lattice() -> ABLattice: + return ABLattice( + 0, + { + "lattice": { + "dim": {"x": 256}, + "velocities": "D1Q3", + }, + }, + ) + + +@pytest.fixture +def lattice_1d_16_1_obstacle() -> ABLattice: + return ABLattice( + { + "lattice": { + "dim": {"x": 16}, + "velocities": "D1Q2" + }, + "geometry": [ + {"shape": "cuboid", "x": [4, 6], "boundary": "bounceback"}, + ], + }, + ) + + + +# 2D Lattices +@pytest.fixture +def dummy_2d_lattice() -> ABLattice: + return ABLattice( + 0, + { + "lattice": { + "dim": {"x": 32, "y": 32}, + "velocities": "D2Q4" + }, + }, + ) + + +@pytest.fixture +def lattice_2d_16x16_1_obstacle() -> ABLattice: + return ABLattice( + { + "lattice": { + "dim": {"x": 16, "y": 16}, + "velocities": "D2Q4" + }, + "geometry": [ + { + "shape": "cuboid", + "x": [2, 6], + "y": [5, 10], + "boundary": "bounceback", + }, + ], + }, + ) + + +# Shapes +@pytest.fixture +def simple_1d_block() -> Block: + return Block([(5, 11)], [4], "bounceback") + + +@pytest.fixture +def simple_large_1d_block() -> Block: + return Block([(2, 14)], [4], "bounceback") diff --git a/test/unit/lqlga/circuits/hamming_adder_test.py b/test/unit/lqlga/circuits/hamming_adder_test.py new file mode 100644 index 0000000..7eba82a --- /dev/null +++ b/test/unit/lqlga/circuits/hamming_adder_test.py @@ -0,0 +1,105 @@ +import pytest +from qiskit import QuantumCircuit, transpile +from qiskit_aer import AerSimulator +from qiskit.result import Counts + +from qlbm.components.common import HammingWeightAdder + + +def bit_string_to_bool_list(bitstring): + return [x == "1" for x in bitstring] + + +def hamming_weight(bitstring): + return len([True for x in bitstring if x == "1"]) + + +def get_count_from_circuit(circuit, num_shots=128) -> Counts: + sim = AerSimulator() + tqc = transpile(circuit, sim) + + res = sim.run(tqc, num_shots=num_shots).result() + return res.get_counts() + + +def test_hamming_adder_all_0s(): + adder = HammingWeightAdder(3, 5).circuit + adder.measure_all() + counts = get_count_from_circuit(adder) + + assert len(counts) == 1 + assert (int(s, 2) == 0 for s in counts) + + +def test_hamming_adder_1plus0(): + circuit = QuantumCircuit(8) + circuit.x(3) + adder = HammingWeightAdder(4, 4).circuit + adder.measure_all() + circuit.compose(adder, inplace=True) + counts = get_count_from_circuit(circuit) + + assert len(counts) == 1 + assert all(int(s[4:][::-1], 2) == 1 for s in counts) + + +def test_hamming_adder_2plus0(): + # Hamming weight 2 in the x register + # Number 0 in the y register + circuit = QuantumCircuit(8) + circuit.x(0) + circuit.x(3) + adder = HammingWeightAdder(4, 4).circuit + adder.measure_all() + circuit.compose(adder, inplace=True) + counts = get_count_from_circuit(circuit) + + assert len(counts) == 1 + print(counts) + assert all(int(s[:4], 2) == 2 for s in counts) + + +def test_hamming_adder_2plus4(): + # Hamming weight 2 in the x register + # Number 4 in the y register + circuit = QuantumCircuit(8) + circuit.x(0) + circuit.x(3) + circuit.x(6) + adder = HammingWeightAdder(4, 4).circuit + adder.measure_all() + circuit.compose(adder, inplace=True) + counts = get_count_from_circuit(circuit) + + assert len(counts) == 1 + print(counts) + assert all(int(s[:4], 2) == 6 for s in counts) + + +def test_hamming_adder_twice(): + # Hamming weight 2 in the x register + # Number 0 in the y register + circuit = QuantumCircuit(8) + circuit.x(0) + circuit.x(3) + adder = HammingWeightAdder(4, 4).circuit + circuit.compose(adder, inplace=True) + circuit.compose(adder, inplace=True) + circuit.measure_all() + counts = get_count_from_circuit(circuit) + + assert len(counts) == 1 + assert all(int(s[:4], 2) == 4 for s in counts) + + +def test_hamming_adder_superposition_x(): + circuit = QuantumCircuit(8) + circuit.h([0, 1, 2, 3]) + circuit.x(6) + adder = HammingWeightAdder(4, 4).circuit + adder.measure_all() + circuit.compose(adder, inplace=True) + counts = get_count_from_circuit(circuit) + print(counts) + assert len(counts) == 16 + assert all(int(s[:4], 2) == (hamming_weight(s[4:]) + 4) for s in counts) diff --git a/test/unit/lqlga/lqlga_lattice_test.py b/test/unit/lqlga/lqlga_lattice_test.py index d25fa2c..931be65 100644 --- a/test/unit/lqlga/lqlga_lattice_test.py +++ b/test/unit/lqlga/lqlga_lattice_test.py @@ -5,7 +5,7 @@ def test_lqlga_lattice_num_registers_d1q2(lattice_d1q2_256): - assert len(lattice_d1q2_256.registers) == 256 # One empty register + assert len(lattice_d1q2_256.registers) == 256 # assert all(reg.size == 2 for reg in lattice_d1q2_256.velocity_register) assert lattice_d1q2_256.circuit.num_qubits == 512