diff --git a/.github/workflows/build-wheel-linux-x86_64.yaml b/.github/workflows/build-wheel-linux-x86_64.yaml index e351b65ab2..c505d891ae 100644 --- a/.github/workflows/build-wheel-linux-x86_64.yaml +++ b/.github/workflows/build-wheel-linux-x86_64.yaml @@ -348,7 +348,7 @@ jobs: -DENABLE_OPENMP=OFF \ -DLQ_ENABLE_KERNEL_OMP=OFF - cmake --build runtime-build --target rt_capi rtd_lightning rtd_openqasm rtd_dummy + cmake --build runtime-build --target rt_capi rtd_lightning rtd_openqasm rtd_null_qubit # Build OQC-Runtime - name: Build OQC-Runtime diff --git a/.github/workflows/build-wheel-macos-arm64.yaml b/.github/workflows/build-wheel-macos-arm64.yaml index 64bc758a31..0f018e61d6 100644 --- a/.github/workflows/build-wheel-macos-arm64.yaml +++ b/.github/workflows/build-wheel-macos-arm64.yaml @@ -308,7 +308,7 @@ jobs: -DENABLE_OPENMP=OFF \ -DLQ_ENABLE_KERNEL_OMP=OFF - cmake --build runtime-build --target rt_capi rtd_lightning rtd_openqasm rtd_dummy + cmake --build runtime-build --target rt_capi rtd_lightning rtd_openqasm rtd_null_qubit - name: Test Catalyst-Runtime run: | diff --git a/.github/workflows/build-wheel-macos-x86_64.yaml b/.github/workflows/build-wheel-macos-x86_64.yaml index b499f7c888..e3c161745e 100644 --- a/.github/workflows/build-wheel-macos-x86_64.yaml +++ b/.github/workflows/build-wheel-macos-x86_64.yaml @@ -299,7 +299,7 @@ jobs: -DENABLE_OPENMP=OFF \ -DLQ_ENABLE_KERNEL_OMP=OFF - cmake --build runtime-build --target rt_capi rtd_lightning rtd_openqasm rtd_dummy + cmake --build runtime-build --target rt_capi rtd_lightning rtd_openqasm rtd_null_qubit # Build OQC-Runtime - name: Build OQC-Runtime diff --git a/.github/workflows/check-catalyst.yaml b/.github/workflows/check-catalyst.yaml index 17e7089070..51d447e6d9 100644 --- a/.github/workflows/check-catalyst.yaml +++ b/.github/workflows/check-catalyst.yaml @@ -57,15 +57,6 @@ jobs: ENABLE_ASAN=OFF \ make runtime - # This is needed in the artifact for the pytests - # Note the lack of sanitizers. - # Left other flags the same. - COMPILER_LAUNCHER="" \ - C_COMPILER=$(which ${{ needs.constants.outputs[format('c_compiler.{0}', matrix.compiler)] }}) \ - CXX_COMPILER=$(which ${{ needs.constants.outputs[format('cxx_compiler.{0}', matrix.compiler)] }}) \ - RT_BUILD_DIR="$(pwd)/runtime-build" \ - make dummy_device - COMPILER_LAUNCHER="" \ C_COMPILER=$(which ${{ needs.constants.outputs[format('c_compiler.{0}', matrix.compiler)] }}) \ CXX_COMPILER=$(which ${{ needs.constants.outputs[format('cxx_compiler.{0}', matrix.compiler)] }}) \ diff --git a/.github/workflows/scripts/linux_arm64/rh8/build_catalyst.sh b/.github/workflows/scripts/linux_arm64/rh8/build_catalyst.sh index 2408967e97..d8d0fe1a80 100644 --- a/.github/workflows/scripts/linux_arm64/rh8/build_catalyst.sh +++ b/.github/workflows/scripts/linux_arm64/rh8/build_catalyst.sh @@ -14,7 +14,7 @@ export PYTHON_PACKAGE=$4 export PYTHON_ALTERNATIVE_VERSION=$5 # Install system dependencies -dnf update -y +dnf update -y dnf install -y libzstd-devel gcc-toolset-${GCC_VERSION} if [ "$PYTHON_VERSION" != "3.10" ]; then dnf install -y ${PYTHON_PACKAGE} ${PYTHON_PACKAGE}-devel @@ -22,13 +22,13 @@ fi dnf clean all -y # Make GCC the default compiler -source /opt/rh/gcc-toolset-${GCC_VERSION}/enable -y -export C_COMPILER=/opt/rh/gcc-toolset-${GCC_VERSION}/root/usr/bin/gcc +source /opt/rh/gcc-toolset-${GCC_VERSION}/enable -y +export C_COMPILER=/opt/rh/gcc-toolset-${GCC_VERSION}/root/usr/bin/gcc export CXX_COMPILER=/opt/rh/gcc-toolset-${GCC_VERSION}/root/usr/bin/g++ # Set the right Python interpreter rm -rf /usr/bin/python3 -ln -s /opt/_internal/cpython-${PYTHON_VERSION}.${PYTHON_SUBVERSION}/bin/python3 /usr/bin/python3 +ln -s /opt/_internal/cpython-${PYTHON_VERSION}.${PYTHON_SUBVERSION}/bin/python3 /usr/bin/python3 export PYTHON=/usr/bin/python3 # Add LLVM, Python and GCC to the PATH env var @@ -51,7 +51,7 @@ cmake -S runtime -B runtime-build -G Ninja \ -DENABLE_OPENQASM=ON \ -DENABLE_OPENMP=OFF \ -DLQ_ENABLE_KERNEL_OMP=OFF -cmake --build runtime-build --target rt_capi rtd_lightning rtd_openqasm rtd_dummy +cmake --build runtime-build --target rt_capi rtd_lightning rtd_openqasm rtd_null_qubit # Build OQC export OQC_BUILD_DIR="/catalyst/oqc-build" diff --git a/.gitignore b/.gitignore index 6d43df32e5..5d17554e16 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,6 @@ frontend/mlir_quantum doc/_build doc/code/api .ipynb* + +# Development +venv diff --git a/Makefile b/Makefile index d8f785bc46..780e1ccc94 100644 --- a/Makefile +++ b/Makefile @@ -66,7 +66,6 @@ help: @echo " mlir to build MLIR and custom Catalyst dialects" @echo " runtime to build Catalyst Runtime" @echo " oqc to build Catalyst-OQC Runtime" - @echo " dummy_device needed for frontend tests" @echo " test to run the Catalyst test suites" @echo " docs to build the documentation for Catalyst" @echo " clean to uninstall Catalyst and delete all temporary and cache files" @@ -112,9 +111,6 @@ dialects: runtime: $(MAKE) -C runtime runtime -dummy_device: - $(MAKE) -C runtime dummy_device - oqc: $(MAKE) -C frontend/catalyst/third_party/oqc/src oqc @@ -268,11 +264,11 @@ format-frontend: ifdef check $(PYTHON) ./bin/format.py --check $(if $(version:-=),--cfversion $(version)) ./frontend/catalyst/utils black --check --verbose . - isort --check --diff . + isort --check --diff . else $(PYTHON) ./bin/format.py $(if $(version:-=),--cfversion $(version)) ./frontend/catalyst/utils black . - isort . + isort . endif .PHONY: docs clean-docs diff --git a/doc/dev/custom_devices.rst b/doc/dev/custom_devices.rst index 92db0ed2e1..3275c22d46 100644 --- a/doc/dev/custom_devices.rst +++ b/doc/dev/custom_devices.rst @@ -168,7 +168,7 @@ The Pennylane device API allows you to build a QJIT compatible device in a simpl .. code-block:: python class CustomDevice(qml.devices.Device): - """Dummy Device""" + """Custom Device""" config = pathlib.Path("absolute/path/to/configuration/file.toml") @@ -178,7 +178,7 @@ The Pennylane device API allows you to build a QJIT compatible device in a simpl the location to the shared object with the C/C++ device implementation. """ - return "CustomDevice", "absolute/path/to/librtd_dummy.so" + return "CustomDevice", "absolute/path/to/librtd_custom.so" def __init__(self, shots=None, wires=None): super().__init__(wires=wires, shots=shots) diff --git a/frontend/catalyst/device/qjit_device.py b/frontend/catalyst/device/qjit_device.py index 99cf6a4e04..b86ab1aee7 100644 --- a/frontend/catalyst/device/qjit_device.py +++ b/frontend/catalyst/device/qjit_device.py @@ -126,6 +126,7 @@ # TODO: This should be removed after implementing `get_c_interface` # for the following backend devices: SUPPORTED_RT_DEVICES = { + "null.qubit": ("NullQubit", "librtd_null_qubit"), "lightning.qubit": ("LightningSimulator", "librtd_lightning"), "braket.aws.qubit": ("OpenQasmDevice", "librtd_openqasm"), "braket.local.qubit": ("OpenQasmDevice", "librtd_openqasm"), diff --git a/frontend/test/conftest.py b/frontend/test/conftest.py index d78ce0dfc7..d6090cdd2d 100644 --- a/frontend/test/conftest.py +++ b/frontend/test/conftest.py @@ -23,9 +23,6 @@ # pylint: disable=unused-import,wrong-import-position import platform - -import numpy as np -import pennylane as qml import pytest diff --git a/runtime/lib/backend/dummy/dummy_device.toml b/frontend/test/custom_device/custom_device.toml similarity index 100% rename from runtime/lib/backend/dummy/dummy_device.toml rename to frontend/test/custom_device/custom_device.toml diff --git a/frontend/test/lit/test_decomposition.py b/frontend/test/lit/test_decomposition.py index 302675f33e..a84d375a04 100644 --- a/frontend/test/lit/test_decomposition.py +++ b/frontend/test/lit/test_decomposition.py @@ -70,9 +70,9 @@ def get_c_interface(): """ system_extension = ".dylib" if platform.system() == "Darwin" else ".so" lib_path = ( - get_lib_path("runtime", "RUNTIME_LIB_DIR") + "/librtd_dummy" + system_extension + get_lib_path("runtime", "RUNTIME_LIB_DIR") + "/librtd_null_qubit" + system_extension ) - return "dummy.remote", lib_path + return "NullQubit", lib_path def execute(self, circuits, execution_config): """Execution.""" diff --git a/frontend/test/lit/test_device_api.py b/frontend/test/lit/test_device_api.py index b2f9b6398c..e929bf82ab 100644 --- a/frontend/test/lit/test_device_api.py +++ b/frontend/test/lit/test_device_api.py @@ -16,6 +16,8 @@ """Test for the device API. """ +import os +import pathlib import platform from typing import Optional @@ -27,11 +29,14 @@ from catalyst import qjit from catalyst.compiler import get_lib_path +TEST_PATH = os.path.dirname(__file__) +CONFIG_CUSTOM_DEVICE = pathlib.Path(f"{TEST_PATH}/../custom_device/custom_device.toml") -class DummyDevice(Device): - """A dummy device from the device API.""" - config = get_lib_path("runtime", "RUNTIME_LIB_DIR") + "/backend/dummy_device.toml" +class CustomDevice(Device): + """A custom device that does nothing.""" + + config = CONFIG_CUSTOM_DEVICE def __init__(self, wires, shots=1024): super().__init__(wires=wires, shots=shots) @@ -42,11 +47,11 @@ def get_c_interface(): the location to the shared object with the C/C++ device implementation. """ system_extension = ".dylib" if platform.system() == "Darwin" else ".so" - lightning_lib_path = ( - get_lib_path("runtime", "RUNTIME_LIB_DIR") + "/librtd_lightning" + system_extension + null_qubit_lib_path = ( + get_lib_path("runtime", "RUNTIME_LIB_DIR") + "/librtd_null_qubit" + system_extension ) - return "dummy.remote", lightning_lib_path + return "Custom", null_qubit_lib_path def execute(self, circuits, execution_config): """Execute""" @@ -65,8 +70,8 @@ def preprocess(self, execution_config: Optional[ExecutionConfig] = None): def test_circuit(): """Test a circuit compilation to MLIR when using the new device API.""" - # CHECK: quantum.device["[[PATH:.*]]librtd_lightning.{{so|dylib}}", "dummy.remote", "{'shots': 2048}"] - dev = DummyDevice(wires=2, shots=2048) + # CHECK: quantum.device["[[PATH:.*]]librtd_null_qubit.{{so|dylib}}", "Custom", "{'shots': 2048}"] + dev = CustomDevice(wires=2, shots=2048) @qjit(target="mlir") @qml.qnode(device=dev) @@ -90,8 +95,8 @@ def test_preprocess(): using the new device API. TODO: we need to readd the two check-not once we accept the device preprocessing.""" - # CHECK: quantum.device["[[PATH:.*]]librtd_lightning.{{so|dylib}}", "dummy.remote", "{'shots': 2048}"] - dev = DummyDevice(wires=2, shots=2048) + # CHECK: quantum.device["[[PATH:.*]]librtd_null_qubit.{{so|dylib}}", "Custom", "{'shots': 2048}"] + dev = CustomDevice(wires=2, shots=2048) @qjit(target="mlir") @qml.qnode(device=dev) diff --git a/frontend/test/pytest/conftest.py b/frontend/test/pytest/conftest.py new file mode 100644 index 0000000000..3d757ae4ea --- /dev/null +++ b/frontend/test/pytest/conftest.py @@ -0,0 +1,22 @@ +# Copyright 2023 Xanadu Quantum Technologies Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Pytest configuration file for Catalyst test suite. +""" + +import os +import pathlib + +TEST_PATH = os.path.dirname(__file__) +CONFIG_CUSTOM_DEVICE = pathlib.Path(f"{TEST_PATH}/../custom_device/custom_device.toml") diff --git a/frontend/test/pytest/device/test_decomposition.py b/frontend/test/pytest/device/test_decomposition.py index 3946032982..4b0378ae55 100644 --- a/frontend/test/pytest/device/test_decomposition.py +++ b/frontend/test/pytest/device/test_decomposition.py @@ -172,9 +172,9 @@ def get_c_interface(): """ system_extension = ".dylib" if platform.system() == "Darwin" else ".so" lib_path = ( - get_lib_path("runtime", "RUNTIME_LIB_DIR") + "/librtd_dummy" + system_extension + get_lib_path("runtime", "RUNTIME_LIB_DIR") + "/librtd_null_qubit" + system_extension ) - return "dummy.remote", lib_path + return "NullQubit", lib_path def execute(self, circuits, execution_config): """Execution.""" diff --git a/frontend/test/pytest/test_config_functions.py b/frontend/test/pytest/test_config_functions.py index ea81b95a41..dca8f03f00 100644 --- a/frontend/test/pytest/test_config_functions.py +++ b/frontend/test/pytest/test_config_functions.py @@ -18,7 +18,6 @@ from tempfile import TemporaryDirectory from textwrap import dedent -import pennylane as qml import pytest from catalyst.utils.exceptions import CompileError @@ -32,23 +31,6 @@ ) -class DeviceToBeTested(qml.devices.QubitDevice): - """Test device""" - - name = "Dummy Device" - short_name = "dummy.device" - pennylane_requires = "0.33.0" - version = "0.0.1" - author = "Dummy" - - operations = [] - observables = [] - - def apply(self, operations, **kwargs): - """Unused""" - raise RuntimeError("Only C/C++ interface is defined") - - def get_test_config(config_text: str) -> TOMLDocument: """Parse test config into the TOMLDocument structure""" with TemporaryDirectory() as d: diff --git a/frontend/test/pytest/test_custom_devices.py b/frontend/test/pytest/test_custom_devices.py index f3b2e47956..bc2c9b9f4c 100644 --- a/frontend/test/pytest/test_custom_devices.py +++ b/frontend/test/pytest/test_custom_devices.py @@ -17,6 +17,7 @@ import pennylane as qml import pytest +from conftest import CONFIG_CUSTOM_DEVICE from catalyst import measure, qjit from catalyst.compiler import get_lib_path @@ -133,17 +134,18 @@ def test_custom_device_load(): """Test that custom device can run using Catalyst.""" - class DummyDevice(qml.devices.QubitDevice): - """Dummy Device""" + class CustomDevice(qml.devices.QubitDevice): + """Custom device""" - name = "Dummy Device" - short_name = "dummy.device" + name = "Custom Device" + short_name = "custom.device" pennylane_requires = "0.33.0" version = "0.0.1" author = "Dummy" operations = OPERATIONS observables = OBSERVABLES + config = CONFIG_CUSTOM_DEVICE def __init__(self, shots=None, wires=None): super().__init__(wires=wires, shots=shots) @@ -160,11 +162,11 @@ def get_c_interface(): """ system_extension = ".dylib" if platform.system() == "Darwin" else ".so" lib_path = ( - get_lib_path("runtime", "RUNTIME_LIB_DIR") + "/librtd_dummy" + system_extension + get_lib_path("runtime", "RUNTIME_LIB_DIR") + "/librtd_null_qubit" + system_extension ) - return "DummyDevice", lib_path + return "NullQubit", lib_path - device = DummyDevice(wires=1) + device = CustomDevice(wires=1) capabilities = get_device_capabilities(device) backend_info = extract_backend_info(device, capabilities) assert backend_info.kwargs["option1"] == 42 @@ -174,7 +176,7 @@ def get_c_interface(): @qml.qnode(device) def f(): """This function would normally return False. - However, DummyDevice as defined in librtd_dummy.so + However, NullQubit as defined in librtd_null_qubit.so has been implemented to always return True.""" return measure(0) @@ -184,17 +186,18 @@ def f(): def test_custom_device_bad_directory(): """Test that custom device error.""" - class DummyDevice(qml.devices.QubitDevice): - """Dummy Device""" + class CustomDevice(qml.devices.QubitDevice): + """Custom Device""" - name = "Dummy Device" - short_name = "dummy.device" + name = "Custom Qubit" + short_name = "custom.device" pennylane_requires = "0.33.0" version = "0.0.1" author = "Dummy" operations = OPERATIONS observables = OBSERVABLES + config = CONFIG_CUSTOM_DEVICE def __init__(self, shots=None, wires=None): super().__init__(wires=wires, shots=shots) @@ -209,14 +212,14 @@ def get_c_interface(): the location to the shared object with the C/C++ device implementation. """ - return "DummyDevice", "this-file-does-not-exist.so" + return "CustomDevice", "this-file-does-not-exist.so" with pytest.raises( CompileError, match="Device at this-file-does-not-exist.so cannot be found!" ): @qjit - @qml.qnode(DummyDevice(wires=1)) + @qml.qnode(CustomDevice(wires=1)) def f(): return measure(0) @@ -224,30 +227,31 @@ def f(): def test_custom_device_no_c_interface(): """Test that custom device error.""" - class DummyDevice(qml.devices.QubitDevice): - """Dummy Device""" + class CustomDevice(qml.devices.QubitDevice): + """Custom Device""" - name = "Dummy Device" - short_name = "dummy.device" + name = "Custom Qubit" + short_name = "custom.device" pennylane_requires = "0.33.0" version = "0.0.1" author = "Dummy" operations = OPERATIONS observables = OBSERVABLES + config = CONFIG_CUSTOM_DEVICE def __init__(self, shots=None, wires=None): super().__init__(wires=wires, shots=shots) def apply(self, operations, **kwargs): """Unused.""" - raise RuntimeError("Dummy device") + raise RuntimeError("Custom device") with pytest.raises( - CompileError, match="The dummy.device device does not provide C interface for compilation." + CompileError, match="The custom.device device does not provide C interface for compilation." ): @qjit - @qml.qnode(DummyDevice(wires=1)) + @qml.qnode(CustomDevice(wires=1)) def f(): return measure(0) diff --git a/frontend/test/pytest/test_device_api.py b/frontend/test/pytest/test_device_api.py index f0d799a80e..da442d17a6 100644 --- a/frontend/test/pytest/test_device_api.py +++ b/frontend/test/pytest/test_device_api.py @@ -13,19 +13,11 @@ # limitations under the License. """Test for the device API. """ -import pathlib -import platform -from typing import Optional - import pennylane as qml import pytest -from pennylane.devices import Device -from pennylane.devices.execution_config import ExecutionConfig -from pennylane.transforms import split_non_commuting -from pennylane.transforms.core import TransformProgram +from pennylane.devices import NullQubit from catalyst import qjit -from catalyst.compiler import get_lib_path from catalyst.device import ( QJITDevice, get_device_capabilities, @@ -37,64 +29,9 @@ # pylint:disable = protected-access,attribute-defined-outside-init -class DummyDevice(Device): - """A dummy device from the device API.""" - - config = get_lib_path("runtime", "RUNTIME_LIB_DIR") + "/backend/dummy_device.toml" - - def __init__(self, wires, shots=1024): - print(pathlib.Path(__file__).parent.parent.parent.parent) - super().__init__(wires=wires, shots=shots) - - @staticmethod - def get_c_interface(): - """Returns a tuple consisting of the device name, and - the location to the shared object with the C/C++ device implementation. - """ - system_extension = ".dylib" if platform.system() == "Darwin" else ".so" - lib_path = get_lib_path("runtime", "RUNTIME_LIB_DIR") + "/librtd_dummy" + system_extension - return "dummy.remote", lib_path - - def execute(self, circuits, execution_config): - """Execution.""" - return circuits, execution_config - - def preprocess(self, execution_config: Optional[ExecutionConfig] = None): - """Preprocessing.""" - if execution_config is None: - execution_config = ExecutionConfig() - - transform_program = TransformProgram() - transform_program.add_transform(split_non_commuting) - return transform_program, execution_config - - -class DummyDeviceNoWires(Device): - """A dummy device from the device API without wires.""" - - config = get_lib_path("runtime", "RUNTIME_LIB_DIR") + "/backend/dummy_device.toml" - - def __init__(self, shots=1024): - super().__init__(shots=shots) - - @staticmethod - def get_c_interface(): - """Returns a tuple consisting of the device name, and - the location to the shared object with the C/C++ device implementation. - """ - - system_extension = ".dylib" if platform.system() == "Darwin" else ".so" - lib_path = get_lib_path("runtime", "RUNTIME_LIB_DIR") + "/librtd_dummy" + system_extension - return "dummy.remote", lib_path - - def execute(self, circuits, execution_config): - """Execution.""" - return circuits, execution_config - - def test_qjit_device(): """Test the qjit device from a device using the new api.""" - device = DummyDevice(wires=10, shots=2032) + device = NullQubit(wires=10, shots=2032) # Create qjit device device_qjit = QJITDevice(device) @@ -125,7 +62,7 @@ def test_qjit_device(): def test_qjit_device_no_wires(): """Test the qjit device from a device using the new api without wires set.""" - device = DummyDeviceNoWires(shots=2032) + device = NullQubit(shots=2032) with pytest.raises( AttributeError, match="Catalyst does not support device instances without set wires." @@ -144,7 +81,7 @@ def test_qjit_device_no_wires(): ) def test_qjit_device_invalid_wires(wires): """Test the qjit device from a device using the new api without wires set.""" - device = DummyDeviceNoWires(shots=2032) + device = NullQubit(shots=2032) device._wires = wires with pytest.raises( @@ -198,7 +135,7 @@ def circuit(): def test_simple_circuit(): """Test that a circuit with the new device API is compiling to MLIR.""" - dev = DummyDevice(wires=2, shots=2048) + dev = NullQubit(wires=2, shots=2048) @qjit(target="mlir") @qml.qnode(device=dev) diff --git a/frontend/test/pytest/test_measurement_transforms.py b/frontend/test/pytest/test_measurement_transforms.py index 81bac53925..8611b61130 100644 --- a/frontend/test/pytest/test_measurement_transforms.py +++ b/frontend/test/pytest/test_measurement_transforms.py @@ -21,16 +21,14 @@ import platform import tempfile from functools import partial -from typing import Optional from unittest.mock import Mock, patch import numpy as np import pennylane as qml import pytest +from conftest import CONFIG_CUSTOM_DEVICE from pennylane.devices import Device -from pennylane.devices.execution_config import ExecutionConfig from pennylane.transforms import split_non_commuting, split_to_single_terms -from pennylane.transforms.core import TransformProgram from catalyst.compiler import get_lib_path from catalyst.device import QJITDevice, get_device_capabilities, get_device_toml_config @@ -44,10 +42,10 @@ # pylint: disable=attribute-defined-outside-init -class DummyDevice(Device): - """A dummy device from the device API.""" +class CustomDevice(Device): + """A Custom Device following the new API.""" - config = get_lib_path("runtime", "RUNTIME_LIB_DIR") + "/backend/dummy_device.toml" + config = CONFIG_CUSTOM_DEVICE def __init__(self, wires, shots=1024): print(pathlib.Path(__file__).parent.parent.parent.parent) @@ -63,27 +61,21 @@ def get_c_interface(): the location to the shared object with the C/C++ device implementation. """ system_extension = ".dylib" if platform.system() == "Darwin" else ".so" - lib_path = get_lib_path("runtime", "RUNTIME_LIB_DIR") + "/librtd_dummy" + system_extension - return "dummy.remote", lib_path + # Borrowing the NullQubit library: + lib_path = ( + get_lib_path("runtime", "RUNTIME_LIB_DIR") + "/librtd_null_qubit" + system_extension + ) + return "CustomQubit", lib_path def execute(self, circuits, execution_config): """Execution.""" return circuits, execution_config - def preprocess(self, execution_config: Optional[ExecutionConfig] = None): - """Preprocessing.""" - if execution_config is None: - execution_config = ExecutionConfig() - transform_program = TransformProgram() - transform_program.add_transform(split_non_commuting) - return transform_program, execution_config +class CustomDeviceLimitedMPs(Device): + """A Custom Device from the device API without wires.""" - -class DummyDeviceLimitedMPs(Device): - """A dummy device from the device API without wires.""" - - config = get_lib_path("runtime", "RUNTIME_LIB_DIR") + "/backend/dummy_device.toml" + config = CONFIG_CUSTOM_DEVICE def __init__(self, wires, shots=1024, allow_counts=False, allow_samples=False): self.allow_samples = allow_samples @@ -98,16 +90,19 @@ def get_c_interface(): """ system_extension = ".dylib" if platform.system() == "Darwin" else ".so" - lib_path = get_lib_path("runtime", "RUNTIME_LIB_DIR") + "/librtd_dummy" + system_extension - return "dummy.remote", lib_path + # Borrowing the NullQubit library: + lib_path = ( + get_lib_path("runtime", "RUNTIME_LIB_DIR") + "/librtd_null_qubit" + system_extension + ) + return "CustomDevice", lib_path def execute(self, circuits, execution_config): """Execution.""" return circuits, execution_config def __enter__(self, *args, **kwargs): - dummy_toml = self.config - with open(dummy_toml, mode="r", encoding="UTF-8") as f: + toml_file_path = self.config + with open(toml_file_path, mode="r", encoding="UTF-8") as f: toml_contents = f.readlines() updated_toml_contents = [] @@ -309,7 +304,7 @@ def test_measurement_from_readout_if_only_readout_measurements_supported( allow_sample = "sample" in device_measurements allow_counts = "counts" in device_measurements - with DummyDeviceLimitedMPs( + with CustomDeviceLimitedMPs( wires=4, shots=1000, allow_counts=allow_counts, allow_samples=allow_sample ) as dev: @@ -738,7 +733,7 @@ def test_measurements_are_split(self, mocker): are added to the transform program from preprocess as expected, based on the sum_observables_flag and the non_commuting_observables_flag""" - dev = DummyDevice(wires=4, shots=1000) + dev = CustomDevice(wires=4, shots=1000) # dev1 supports non-commuting observables and sum observables - no splitting qjit_dev1 = QJITDevice(dev) diff --git a/frontend/test/pytest/test_preprocess.py b/frontend/test/pytest/test_preprocess.py index e1b52c50d8..00f13bf0aa 100644 --- a/frontend/test/pytest/test_preprocess.py +++ b/frontend/test/pytest/test_preprocess.py @@ -13,23 +13,19 @@ # limitations under the License. """Test for the device preprocessing. """ - import pathlib import platform from dataclasses import replace from os.path import join from tempfile import TemporaryDirectory from textwrap import dedent -from typing import Optional import numpy as np import pennylane as qml import pytest -from pennylane.devices import Device -from pennylane.devices.execution_config import ExecutionConfig +from conftest import CONFIG_CUSTOM_DEVICE +from pennylane.devices import Device, NullQubit from pennylane.tape import QuantumScript -from pennylane.transforms import split_non_commuting -from pennylane.transforms.core import TransformProgram from catalyst import CompileError, ctrl from catalyst.api_extensions.control_flow import ( @@ -77,42 +73,6 @@ def get_test_device_capabilities( return device_capabilities -class DummyDevice(Device): - """A dummy device from the device API.""" - - config = get_lib_path("runtime", "RUNTIME_LIB_DIR") + "/backend/dummy_device.toml" - - def __init__(self, wires, shots=1024): - print(pathlib.Path(__file__).parent.parent.parent.parent) - super().__init__(wires=wires, shots=shots) - dummy_capabilities = get_device_capabilities(self) - dummy_capabilities.native_ops.pop("BlockEncode") - dummy_capabilities.to_matrix_ops["BlockEncode"] = OperationProperties(False, False, False) - self.qjit_capabilities = dummy_capabilities - - @staticmethod - def get_c_interface(): - """Returns a tuple consisting of the device name, and - the location to the shared object with the C/C++ device implementation. - """ - system_extension = ".dylib" if platform.system() == "Darwin" else ".so" - lib_path = get_lib_path("runtime", "RUNTIME_LIB_DIR") + "/librtd_dummy" + system_extension - return "dummy.remote", lib_path - - def execute(self, circuits, execution_config): - """Execution.""" - return circuits, execution_config - - def preprocess(self, execution_config: Optional[ExecutionConfig] = None): - """Preprocessing.""" - if execution_config is None: - execution_config = ExecutionConfig() - - transform_program = TransformProgram() - transform_program.add_transform(split_non_commuting) - return transform_program, execution_config - - class OtherHadamard(qml.Hadamard): """A version of the Hadamard operator that won't be recognized by the QJit device, and will need to be decomposed""" @@ -147,12 +107,41 @@ def decomposition(self): return [qml.RX(*self.parameters, self.wires)] +class CustomDevice(Device): + """A dummy device from the device API.""" + + config = CONFIG_CUSTOM_DEVICE + + def __init__(self, wires, shots=1024): + print(pathlib.Path(__file__).parent.parent.parent.parent) + super().__init__(wires=wires, shots=shots) + dummy_capabilities = get_device_capabilities(self) + dummy_capabilities.native_ops.pop("BlockEncode") + dummy_capabilities.to_matrix_ops["BlockEncode"] = OperationProperties(False, False, False) + self.qjit_capabilities = dummy_capabilities + + @staticmethod + def get_c_interface(): + """Returns a tuple consisting of the device name, and + the location to the shared object with the C/C++ device implementation. + """ + system_extension = ".dylib" if platform.system() == "Darwin" else ".so" + lib_path = ( + get_lib_path("runtime", "RUNTIME_LIB_DIR") + "/librtd_null_qubit" + system_extension + ) + return "dummy.remote", lib_path + + def execute(self, circuits, execution_config): + """Execution.""" + raise NotImplementedError + + class TestDecomposition: """Test the preprocessing transforms implemented in Catalyst.""" def test_decompose_integration(self): """Test the decompose transform as part of the Catalyst pipeline.""" - dev = DummyDevice(wires=4, shots=None) + dev = NullQubit(wires=4, shots=None) @qml.qjit @qml.qnode(dev) @@ -179,7 +168,7 @@ def test_decompose_ops_to_unitary(self): def test_decompose_ops_to_unitary_integration(self): """Test the decompose ops to unitary transform as part of the Catalyst pipeline.""" - dev = DummyDevice(wires=4, shots=None) + dev = CustomDevice(wires=4, shots=None) @qml.qjit @qml.qnode(dev) @@ -193,7 +182,7 @@ def circuit(): def test_no_matrix(self): """Test that controlling an operation without a matrix method raises an error.""" - dev = DummyDevice(wires=4) + dev = NullQubit(wires=4) class OpWithNoMatrix(qml.operation.Operation): """Op without matrix.""" diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 5e48d9a8ba..e44ed4191f 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -81,7 +81,8 @@ message(STATUS "ENABLE_LIGHTNING is ${ENABLE_LIGHTNING}.") message(STATUS "ENABLE_OPENQASM is ${ENABLE_OPENQASM}.") set(devices_list) -list(APPEND devices_list rtd_dummy) +list(APPEND devices_list rtd_null_qubit) +list(APPEND backend_includes "${PROJECT_SOURCE_DIR}/lib/backend/null_qubit") if(ENABLE_LIGHTNING) list(APPEND devices_list pennylane_lightning rtd_lightning) diff --git a/runtime/Makefile b/runtime/Makefile index a3a85672b4..22889a5386 100644 --- a/runtime/Makefile +++ b/runtime/Makefile @@ -17,8 +17,8 @@ ENABLE_ASAN?=OFF LIGHTNING_GIT_TAG_VALUE?=6f3e0d5d371ff9823a3177dd2c66052668883d42 ENABLE_LAPACK?=OFF -BUILD_TARGETS := rt_capi rtd_dummy -TEST_TARGETS := "" +BUILD_TARGETS := rt_capi rtd_null_qubit +TEST_TARGETS := runner_tests_qir_runtime ifeq ($(ENABLE_LIGHTNING), ON) BUILD_TARGETS += rtd_lightning @@ -119,19 +119,18 @@ runtime: configure $(RT_BUILD_DIR)/tests/runner_tests_lightning: configure cmake --build $(RT_BUILD_DIR) --target $(TEST_TARGETS) -j$(NPROC) -.PHONY: dummy_device -dummy_device: - cmake --build $(RT_BUILD_DIR) --target rtd_dummy -j$(NPROC) - .PHONY: test test: $(RT_BUILD_DIR)/tests/runner_tests_lightning - @echo "test the Catalyst runtime test suite" + @echo "Catalyst runtime test suite - NullQubit" + $(ASAN_COMMAND) $(RT_BUILD_DIR)/tests/runner_tests_qir_runtime ifeq ($(ENABLE_OPENQASM), ON) # Test the OpenQasm devices C++ tests $(ASAN_COMMAND) $(RT_BUILD_DIR)/tests/runner_tests_openqasm endif +ifeq ($(ENABLE_LIGHTNING), ON) # Test the Lightning suite C++ tests $(ASAN_COMMAND) $(RT_BUILD_DIR)/tests/runner_tests_lightning +endif .PHONY: coverage coverage: lq_target diff --git a/runtime/lib/backend/CMakeLists.txt b/runtime/lib/backend/CMakeLists.txt index f93c9321f2..a5825d3a8a 100644 --- a/runtime/lib/backend/CMakeLists.txt +++ b/runtime/lib/backend/CMakeLists.txt @@ -1,5 +1,5 @@ -add_subdirectory(dummy) -configure_file(dummy/dummy_device.toml dummy_device.toml) +add_subdirectory(null_qubit) +configure_file(null_qubit/null_qubit.toml null_qubit.toml) if(ENABLE_LIGHTNING) add_subdirectory(lightning) endif() diff --git a/runtime/lib/backend/dummy/CMakeLists.txt b/runtime/lib/backend/dummy/CMakeLists.txt deleted file mode 100644 index 7e61134064..0000000000 --- a/runtime/lib/backend/dummy/CMakeLists.txt +++ /dev/null @@ -1,8 +0,0 @@ -cmake_minimum_required(VERSION 3.20) - -set(CMAKE_CXX_STANDARD 20) -set(CMAKE_CXX_STANDARD_REQUIRED ON) - -add_library(rtd_dummy SHARED dummy_device.cpp) -target_include_directories(rtd_dummy PUBLIC ${runtime_includes}) -set_property(TARGET rtd_dummy PROPERTY POSITION_INDEPENDENT_CODE ON) \ No newline at end of file diff --git a/runtime/lib/backend/dummy/dummy_device.cpp b/runtime/lib/backend/dummy/dummy_device.cpp deleted file mode 100644 index 96d7a42e7f..0000000000 --- a/runtime/lib/backend/dummy/dummy_device.cpp +++ /dev/null @@ -1,77 +0,0 @@ -#include - -struct DummyDevice final : public Catalyst::Runtime::QuantumDevice { - DummyDevice([[maybe_unused]] const std::string &kwargs) {} - ~DummyDevice() = default; // LCOV_EXCL_LINE - - DummyDevice &operator=(const QuantumDevice &) = delete; - DummyDevice(const DummyDevice &) = delete; - DummyDevice(DummyDevice &&) = delete; - DummyDevice &operator=(QuantumDevice &&) = delete; - - auto AllocateQubit() -> QubitIdType override { return 0; } - auto AllocateQubits(size_t num_qubits) -> std::vector override - { - return std::vector(num_qubits); - } - [[nodiscard]] auto Zero() const -> Result override { return NULL; } - [[nodiscard]] auto One() const -> Result override { return NULL; } - auto Observable(ObsId, const std::vector> &, - const std::vector &) -> ObsIdType override - { - return 0; - } - auto TensorObservable(const std::vector &) -> ObsIdType override { return 0; } - auto HamiltonianObservable(const std::vector &, const std::vector &) - -> ObsIdType override - { - return 0; - } - auto Measure(QubitIdType, std::optional) -> Result override - { - bool *ret = (bool *)malloc(sizeof(bool)); - *ret = true; - return ret; - } - - void ReleaseQubit(QubitIdType) override {} - void ReleaseAllQubits() override {} - [[nodiscard]] auto GetNumQubits() const -> size_t override { return 0; } - void SetDeviceShots(size_t shots) override {} - [[nodiscard]] auto GetDeviceShots() const -> size_t override { return 0; } - void StartTapeRecording() override {} - void StopTapeRecording() override {} - void PrintState() override {} - - void NamedOperation(const std::string &name, const std::vector ¶ms, - const std::vector &wires, bool inverse, - const std::vector &controlled_wires, - const std::vector &controlled_values) override - { - } - - void MatrixOperation(const std::vector> &, - const std::vector &, bool, - const std::vector &controlled_wires, - const std::vector &controlled_values) override - { - } - - auto Expval(ObsIdType) -> double override { return 0.0; } - auto Var(ObsIdType) -> double override { return 0.0; } - void State(DataView, 1> &) override {} - void Probs(DataView &) override {} - void PartialProbs(DataView &, const std::vector &) override {} - void Sample(DataView &, size_t) override {} - void PartialSample(DataView &, const std::vector &, size_t) override {} - void Counts(DataView &, DataView &, size_t) override {} - - void PartialCounts(DataView &, DataView &, - const std::vector &, size_t) override - { - } - - void Gradient(std::vector> &, const std::vector &) override {} -}; - -GENERATE_DEVICE_FACTORY(DummyDevice, DummyDevice); diff --git a/runtime/lib/backend/dummy/.gitignore b/runtime/lib/backend/null_qubit/.gitignore similarity index 100% rename from runtime/lib/backend/dummy/.gitignore rename to runtime/lib/backend/null_qubit/.gitignore diff --git a/runtime/lib/backend/null_qubit/CMakeLists.txt b/runtime/lib/backend/null_qubit/CMakeLists.txt new file mode 100644 index 0000000000..fe815b8b9f --- /dev/null +++ b/runtime/lib/backend/null_qubit/CMakeLists.txt @@ -0,0 +1,14 @@ +cmake_minimum_required(VERSION 3.20) + +project(rtd_null_qubit LANGUAGES CXX) + +add_library(rtd_null_qubit SHARED NullQubit.cpp) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +target_include_directories(rtd_null_qubit PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} + ${runtime_includes} + ) + +set_property(TARGET rtd_null_qubit PROPERTY POSITION_INDEPENDENT_CODE ON) diff --git a/runtime/lib/backend/null_qubit/NullQubit.cpp b/runtime/lib/backend/null_qubit/NullQubit.cpp new file mode 100644 index 0000000000..e1cc7d7930 --- /dev/null +++ b/runtime/lib/backend/null_qubit/NullQubit.cpp @@ -0,0 +1,4 @@ +#include +#include + +GENERATE_DEVICE_FACTORY(NullQubit, Catalyst::Runtime::Devices::NullQubit); diff --git a/runtime/lib/backend/null_qubit/NullQubit.hpp b/runtime/lib/backend/null_qubit/NullQubit.hpp new file mode 100644 index 0000000000..2856182317 --- /dev/null +++ b/runtime/lib/backend/null_qubit/NullQubit.hpp @@ -0,0 +1,299 @@ +// Copyright 2022 Xanadu Quantum Technologies Inc. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include +#include +#include +#include + +#include "DataView.hpp" +#include "QuantumDevice.hpp" +#include "Types.h" + +namespace Catalyst::Runtime::Devices { + +/** + * @brief struct API for a null backend quantum device. + * + * This device API contains several NULL methods: + * - a set of methods to manage qubit allocations and deallocations, device shot + * noise, and quantum tape recording as well as reference values for the result + * data-type; these are used to implement Quantum Runtime (QR) instructions. + * + * - a set of methods for quantum operations, observables, measurements, and gradient + * of the device; these are used to implement Quantum Instruction Set (QIS) instructions. + */ +struct NullQubit final : public Catalyst::Runtime::QuantumDevice { + NullQubit(const std::string &kwargs = "{}") {} + ~NullQubit() = default; // LCOV_EXCL_LINE + + NullQubit &operator=(const NullQubit &) = delete; + NullQubit(const NullQubit &) = delete; + NullQubit(NullQubit &&) = delete; + NullQubit &operator=(NullQubit &&) = delete; + + /** + * @brief Doesn't Allocate a qubit. + * + * @return `QubitIdType` + */ + auto AllocateQubit() -> QubitIdType { return 0; } + + /** + * @brief Allocate a vector of qubits. + * + * @param num_qubits The number of qubits to allocate. + * + * @return `std::vector` + */ + auto AllocateQubits(size_t num_qubits) -> std::vector + { + return std::vector(num_qubits, 0); + } + + /** + * @brief Doesn't Release a qubit. + */ + void ReleaseQubit(QubitIdType) {} + + /** + * @brief Doesn't Release all qubits. + */ + void ReleaseAllQubits() {} + + /** + * @brief Doesn't Get the number of allocated qubits. + * + * @return `size_t` + */ + [[nodiscard]] auto GetNumQubits() const -> size_t { return 0; } + + /** + * @brief Doesn't Set the number of device shots. + * + * @param shots The number of noise shots + */ + void SetDeviceShots(size_t shots) {} + + /** + * @brief Doesn't Get the number of device shots. + * + * @return `size_t` + */ + [[nodiscard]] auto GetDeviceShots() const -> size_t { return 0; } + + /** + * @brief Doesn't Set the PRNG of the device. + * + * The Catalyst runtime enables seeded program execution on non-hardware devices. + * A random number generator instance is managed by the runtime to predictably + * generate results for non-deterministic programs, such as those involving `Measure` + * calls. + * Devices implementing support for this feature do not need to use the provided + * PRNG instance as their sole source of random numbers, but it is expected that the + * the same instance state will predictable and reproducibly generate the same + * program results. It is also expected that the provided PRNG state is evolved + * sufficiently so that two device executions sharing the same instance do not produce + * identical results. + * The provided PRNG instance is not thread-locked, and devices wishing to share it + * across threads will need to provide their own thread-safety. + */ + void SetDevicePRNG([[maybe_unused]] std::mt19937 *gen) {} + + /** + * @brief Doesn't Start recording a quantum tape if provided. + * + * @note This is backed by the `Catalyst::Runtime::CacheManager` property in + * the device implementation. + */ + void StartTapeRecording() {} + + /** + * @brief Doesn't Stop recording a quantum tape if provided. + * + * @note This is backed by the `Catalyst::Runtime::CacheManager` property in + * the device implementation. + */ + void StopTapeRecording() {} + + /** + * @brief Not the Result value for "Zero" used in the measurement process. + * + * @return `Result` + */ + [[nodiscard]] auto Zero() const -> Result { return NULL; } + + /** + * @brief Not the Result value for "One" used in the measurement process. + * + * @return `Result` + */ + [[nodiscard]] auto One() const -> Result { return NULL; } + + /** + * @brief Not A helper method to print the state vector of a device. + */ + void PrintState() {} + + /** + * @brief Doesn't Prepare subsystems using the given ket vector in the computational basis. + */ + void SetState(DataView, 1> &, std::vector &) {} + + /** + * @brief Doesn't Prepare a single computational basis state. + */ + void SetBasisState(DataView &, std::vector &) {} + + /** + * @brief Doesn't Apply a single gate to the state vector of a device with its name if this is + * supported. + */ + void NamedOperation(const std::string &name, const std::vector ¶ms, + const std::vector &wires, bool inverse, + const std::vector &controlled_wires = {}, + const std::vector &controlled_values = {}) + { + } + + /** + * @brief Doesn't Apply a given matrix directly to the state vector of a device. + * + */ + void MatrixOperation(const std::vector> &, + const std::vector &, bool, + const std::vector &controlled_wires = {}, + const std::vector &controlled_values = {}) + { + } + + /** + * @brief Doesn't Construct a named (Identity, PauliX, PauliY, PauliZ, and Hadamard) + * or Hermitian observable. + * + * @return `ObsIdType` Index of the constructed observable + */ + auto Observable(ObsId, const std::vector> &, + const std::vector &) -> ObsIdType + { + return 0.0; + } + + /** + * @brief Doesn't Construct a tensor product of observables. + * + * @return `ObsIdType` Index of the constructed observable + */ + auto TensorObservable(const std::vector &) -> ObsIdType { return 0.0; } + + /** + * @brief Doesn't Construct a Hamiltonian observable. + * + * @return `ObsIdType` Index of the constructed observable + */ + auto HamiltonianObservable(const std::vector &, const std::vector &) + -> ObsIdType + { + return 0.0; + } + + /** + * @brief Doesn't Compute the expected value of an observable. + * + * @return `double` The expected value + */ + auto Expval(ObsIdType) -> double { return 0.0; } + + /** + * @brief Doesn't Compute the variance of an observable. + * + * @return `double` The variance + */ + auto Var(ObsIdType) -> double { return 0.0; } + + /** + * @brief Doesn't Get the state-vector of a device. + */ + void State(DataView, 1> &) {} + + /** + * @brief Doesn't Compute the probabilities of each computational basis state. + */ + void Probs(DataView &) {} + + /** + * @brief Doesn't Compute the probabilities for a subset of the full system. + */ + void PartialProbs(DataView &, const std::vector &) {} + + /** + * @brief Doesn't Compute samples with the number of shots on the entire wires, + * returing raw samples. + */ + void Sample(DataView &, size_t) {} + + /** + * @brief Doesn't Compute partial samples with the number of shots on `wires`, + * returing raw samples. + * + * @param samples The pre-allocated `DataView`representing a matrix of + * shape `shots * numWires`. The built-in iterator in `DataView` + * iterates over all elements of `samples` row-wise. + */ + void PartialSample(DataView &, const std::vector &, size_t) {} + + /** + * @brief Doesn't Sample with the number of shots on the entire wires, returning the + * number of counts for each sample. + */ + void Counts(DataView &, DataView &, size_t) {} + + /** + * @brief Doesn't Partial sample with the number of shots on `wires`, returning the + * number of counts for each sample. + */ + void PartialCounts(DataView &, DataView &, + const std::vector &, size_t) + { + } + + /** + * @brief This is not A general measurement method that acts on a single wire. + * + * @return `Result` The measurement result + */ + auto Measure(QubitIdType, std::optional) -> Result + { + bool *ret = (bool *)malloc(sizeof(bool)); + *ret = true; + return ret; + } + + /** + * @brief Doesn't Compute the gradient of a quantum tape, that is cached using + * `Catalyst::Runtime::Simulator::CacheManager`, for a specific set of trainable + * parameters. + */ + void Gradient(std::vector> &, const std::vector &) {} + + auto CacheManagerInfo() + -> std::tuple, std::vector> + { + return {0, 0, 0, {}, {}}; + } +}; +} // namespace Catalyst::Runtime::Devices diff --git a/runtime/lib/backend/null_qubit/null_qubit.toml b/runtime/lib/backend/null_qubit/null_qubit.toml new file mode 100644 index 0000000000..b60af9048c --- /dev/null +++ b/runtime/lib/backend/null_qubit/null_qubit.toml @@ -0,0 +1,117 @@ +# Based on Lightning's lightning_qubit.toml. It is better if we keep it updated. +schema = 2 + +[operators.gates.native] + +CNOT = { properties = [ "invertible", "differentiable" ] } +ControlledPhaseShift = { properties = [ "invertible", "differentiable" ] } +CRot = { properties = [ "invertible" ] } +CRX = { properties = [ "invertible", "differentiable" ] } +CRY = { properties = [ "invertible", "differentiable" ] } +CRZ = { properties = [ "invertible", "differentiable" ] } +CSWAP = { properties = [ "invertible", "differentiable" ] } +CY = { properties = [ "invertible", "differentiable" ] } +CZ = { properties = [ "invertible", "differentiable" ] } +DoubleExcitationMinus = { properties = [ "invertible", "controllable", "differentiable" ] } +DoubleExcitationPlus = { properties = [ "invertible", "controllable", "differentiable" ] } +DoubleExcitation = { properties = [ "invertible", "controllable", "differentiable" ] } +GlobalPhase = { properties = [ "invertible", "controllable", "differentiable" ] } +Hadamard = { properties = [ "invertible", "controllable", "differentiable" ] } +Identity = { properties = [ "invertible", "differentiable" ] } +IsingXX = { properties = [ "invertible", "controllable", "differentiable" ] } +IsingXY = { properties = [ "invertible", "controllable", "differentiable" ] } +IsingYY = { properties = [ "invertible", "controllable", "differentiable" ] } +IsingZZ = { properties = [ "invertible", "controllable", "differentiable" ] } +MultiRZ = { properties = [ "invertible", "controllable", "differentiable" ] } +PauliX = { properties = [ "invertible", "controllable", "differentiable" ] } +PauliY = { properties = [ "invertible", "controllable", "differentiable" ] } +PauliZ = { properties = [ "invertible", "controllable", "differentiable" ] } +PhaseShift = { properties = [ "invertible", "controllable", "differentiable" ] } +QubitUnitary = { properties = [ "invertible", "controllable", ] } +Rot = { properties = [ "invertible", "controllable", ] } +RX = { properties = [ "invertible", "controllable", "differentiable" ] } +RY = { properties = [ "invertible", "controllable", "differentiable" ] } +RZ = { properties = [ "invertible", "controllable", "differentiable" ] } +SingleExcitationMinus = { properties = [ "invertible", "controllable", "differentiable" ] } +SingleExcitationPlus = { properties = [ "invertible", "controllable", "differentiable" ] } +SingleExcitation = { properties = [ "invertible", "controllable", "differentiable" ] } +S = { properties = [ "invertible", "controllable", "differentiable" ] } +SWAP = { properties = [ "invertible", "controllable", "differentiable" ] } +Toffoli = { properties = [ "invertible", "differentiable" ] } +T = { properties = [ "invertible", "controllable", "differentiable" ] } + +[operators.gates.decomp] + +# Operators that should be decomposed according to the algorithm used +# by PennyLane's device API. +# Optional, since gates not listed in this list will typically be decomposed by +# default, but can be useful to express a deviation from this device's regular +# strategy in PennyLane. +MultiControlledX = {} + +# Gates which should be translated to QubitUnitary +[operators.gates.matrix] + +BlockEncode = {properties = [ "controllable" ]} +DiagonalQubitUnitary = {} +ECR = {} +ISWAP = {} +OrbitalRotation = {} +PSWAP = {} +QubitCarry = {} +QubitSum = {} +SISWAP = {} +SQISW = {} +SX = {} + +# Observables supported by the device +[operators.observables] + +Identity = { properties = [ "differentiable" ] } +PauliX = { properties = [ "differentiable" ] } +PauliY = { properties = [ "differentiable" ] } +PauliZ = { properties = [ "differentiable" ] } +Hadamard = { properties = [ "differentiable" ] } +Hermitian = { properties = [ "differentiable" ] } +Hamiltonian = { properties = [ "differentiable" ] } +SparseHamiltonian = { properties = [ "differentiable" ] } +Projector = { properties = [ "differentiable" ] } +Sum = { properties = [ "differentiable" ] } +SProd = { properties = [ "differentiable" ] } +Prod = { properties = [ "differentiable" ] } +Exp = { properties = [ "differentiable" ] } +LinearCombination = { properties = [ "differentiable" ] } + +[measurement_processes] + +Expval = {} +Var = {} +Probs = {} +State = { condition = [ "analytic" ] } +Sample = { condition = [ "finiteshots" ] } +Counts = { condition = [ "finiteshots" ] } + +[compilation] + +# If the device is compatible with qjit +qjit_compatible = true +# If the device requires run time generation of the quantum circuit. +runtime_code_generation = false +# If the device supports mid circuit measurements natively +mid_circuit_measurement = true +# This field is currently unchecked but it is reserved for the purpose of +# determining if the device supports dynamic qubit allocation/deallocation. +dynamic_qubit_management = false + +# whether the device can support non-commuting measurements together +# in a single execution +non_commuting_observables = true + +# Whether the device supports (arbitrary) initial state preparation. +initial_state_prep = true + +[options] + +mcmc = "_mcmc" +num_burnin = "_num_burnin" +kernel_name = "_kernel_name" diff --git a/runtime/lib/capi/ExecutionContext.hpp b/runtime/lib/capi/ExecutionContext.hpp index 6131c0ee5b..68777ccc75 100644 --- a/runtime/lib/capi/ExecutionContext.hpp +++ b/runtime/lib/capi/ExecutionContext.hpp @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +#pragma once + #include #include @@ -182,9 +184,12 @@ class RTDevice { // Besides, this provides support for runtime device (RTD) libraries added to the system // path. This maintains backward compatibility for specifying a device using its name. // TODO: This support may need to be removed after updating the C++ unit tests. - if (rtd_lib == "lightning.qubit" || rtd_lib == "lightning.kokkos") { - rtd_name = - (rtd_lib == "lightning.qubit") ? "LightningSimulator" : "LightningKokkosSimulator"; + if (rtd_lib == "null.qubit") { + rtd_name = "NullQubit"; + _complete_dylib_os_extension(rtd_lib, "null_qubit"); + } + else if (rtd_lib == "lightning.qubit") { + rtd_name = "LightningSimulator"; _complete_dylib_os_extension(rtd_lib, "lightning"); } else if (rtd_lib == "braket.aws.qubit" || rtd_lib == "braket.local.qubit") { diff --git a/runtime/tests/CMakeLists.txt b/runtime/tests/CMakeLists.txt index 1a7bc84f1b..6e29781ede 100644 --- a/runtime/tests/CMakeLists.txt +++ b/runtime/tests/CMakeLists.txt @@ -24,6 +24,22 @@ list(APPEND CMAKE_MODULE_PATH ${catch2_SOURCE_DIR}/contrib) include(CTest) include(Catch) +add_executable(runner_tests_qir_runtime runner_main.cpp) + +# To avoid link to libpython, we use pybind11::module interface library. +target_link_libraries(runner_tests_qir_runtime PRIVATE + Catch2::Catch2 + pybind11::embed + catalyst_qir_runtime + rtd_null_qubit + ) + +target_sources(runner_tests_qir_runtime PRIVATE + Test_NullQubit.cpp + ) + +catch_discover_tests(runner_tests_qir_runtime) + if(ENABLE_LIGHTNING) add_executable(runner_tests_lightning runner_main.cpp) target_include_directories(runner_tests_lightning PRIVATE catalyst_python_interpreter) @@ -42,7 +58,7 @@ if(ENABLE_LIGHTNING) # runtime (see https://github.com/google/sanitizers/issues/611 for # details). If you want to run this-file-does-not-exist.so library under # sanitizers please remove RTLD_DEEPBIND from dlopen flags. - set(dl_manager_tests + set(dl_manager_tests Test_DLManager.cpp ) endif() diff --git a/runtime/tests/TestUtils.hpp b/runtime/tests/TestUtils.hpp index 3a89cde737..c620d0668b 100644 --- a/runtime/tests/TestUtils.hpp +++ b/runtime/tests/TestUtils.hpp @@ -24,16 +24,22 @@ #include "catch2/catch.hpp" -#include "LightningSimulator.hpp" +#include "ExecutionContext.hpp" +#include "QuantumDevice.hpp" + +#include + +/// @cond DEV +namespace { +using namespace Catalyst::Runtime; +} // namespace +/// @endcond /** * A tuple of available backend devices to be tested using TEMPLATE_LIST_TEST_CASE in Catch2 */ -#if __has_include("LightningKokkosSimulator.hpp") -#include "LightningKokkosSimulator.hpp" -using SimTypes = std::tuple; -#else +#if __has_include("LightningSimulator.hpp") +#include "LightningSimulator.hpp" using SimTypes = std::tuple; #endif @@ -48,9 +54,6 @@ static inline auto getDevices() -> std::vector> devices{ {"lightning.qubit", "lightning.qubit", "{shots: 0}"}}; -#ifdef __device_lightning_kokkos - devices.emplace_back("lightning.kokkos", "lightning.kokkos", "{shots: 0}"); -#endif return devices; } @@ -73,3 +76,15 @@ static inline MemRefT_CplxT_double_1d getState(size_t buffer_len) } static inline void freeState(MemRefT_CplxT_double_1d &result) { delete[] result.data_allocated; } + +static inline QuantumDevice *loadDevice(std::string device_name, std::string filename) +{ + std::unique_ptr init_rtd_dylib = + std::make_unique(filename); + std::string factory_name{device_name + "Factory"}; + void *f_ptr = init_rtd_dylib->getSymbol(factory_name); + + // LCOV_EXCL_START + return f_ptr ? reinterpret_cast(f_ptr)("") : nullptr; + // LCOV_EXCL_STOP +} diff --git a/runtime/tests/Test_DLManager.cpp b/runtime/tests/Test_DLManager.cpp index e66c6d79af..5a10e1a818 100644 --- a/runtime/tests/Test_DLManager.cpp +++ b/runtime/tests/Test_DLManager.cpp @@ -21,18 +21,6 @@ using namespace Catalyst::Runtime; -QuantumDevice *loadDevice(std::string device_name, std::string filename) -{ - std::unique_ptr init_rtd_dylib = - std::make_unique(filename); - std::string factory_name{device_name + "Factory"}; - void *f_ptr = init_rtd_dylib->getSymbol(factory_name); - - // LCOV_EXCL_START - return f_ptr ? reinterpret_cast(f_ptr)("") : nullptr; - // LCOV_EXCL_STOP -} - TEST_CASE("Test dummy", "[Third Party]") { std::string file("this-file-does-not-exist" + get_dylib_ext()); @@ -57,12 +45,6 @@ TEST_CASE("Test error message if init device fails", "[Third Party]") } #endif -TEST_CASE("Test success of loading dummy device", "[Third Party]") -{ - std::unique_ptr driver = std::make_unique(); - CHECK(loadDevice("DummyDevice", "librtd_dummy" + get_dylib_ext())); -} - TEST_CASE("Test __catalyst__rt__device_init registering a custom device with shots=500 and " "device=lightning.qubit", "[CoreQIS]") diff --git a/runtime/tests/Test_NullQubit.cpp b/runtime/tests/Test_NullQubit.cpp new file mode 100644 index 0000000000..9c138691fd --- /dev/null +++ b/runtime/tests/Test_NullQubit.cpp @@ -0,0 +1,298 @@ + +// Copyright 2023 Xanadu Quantum Technologies Inc. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ExecutionContext.hpp" +#include "NullQubit.hpp" +#include "QuantumDevice.hpp" +#include "RuntimeCAPI.h" + +#include "TestUtils.hpp" + +using namespace Catalyst::Runtime; +using namespace Catalyst::Runtime::Devices; + +TEST_CASE("Test success of loading a device", "[NullQubit]") +{ + std::unique_ptr driver = std::make_unique(); + CHECK(loadDevice("NullQubit", "librtd_null_qubit" + get_dylib_ext())); +} + +TEST_CASE("Test __catalyst__rt__device_init registering device=null.qubit", "[NullQubit]") +{ + __catalyst__rt__initialize(nullptr); + + char rtd_name[11] = "null.qubit"; + __catalyst__rt__device_init((int8_t *)rtd_name, nullptr, nullptr); + + __catalyst__rt__device_release(); + + __catalyst__rt__finalize(); +} + +TEST_CASE("Test NullQubit qubit allocation is successful.", "[NullQubit]") +{ + std::unique_ptr sim = std::make_unique(); + sim->AllocateQubit(); +} + +TEST_CASE("Test a NullQubit circuit with num_qubits=2 ", "[NullQubit]") +{ + std::unique_ptr sim = std::make_unique(); + + // state-vector with #qubits = n + constexpr size_t n = 2; + std::vector Qs; + Qs.reserve(n); + + for (size_t i = 0; i < n; i++) { + Qs[i] = sim->AllocateQubit(); + } + + sim->StartTapeRecording(); + sim->NamedOperation("PauliX", {}, {Qs[0]}, false); + sim->NamedOperation("CNOT", {}, {Qs[0], Qs[1]}, false); + + auto &&[num_ops, num_obs, num_params, op_names, obs_keys] = sim->CacheManagerInfo(); + CHECK((num_ops == 0 && num_obs == 0)); + CHECK(num_params == 0); + CHECK(op_names.empty()); + CHECK(obs_keys.empty()); +} + +TEST_CASE("Test a NullQubit circuit with num_qubits=4", "[NullQubit]") +{ + std::unique_ptr sim = std::make_unique(); + + // state-vector with #qubit = n + constexpr size_t n = 4; + std::vector Qs; + Qs.reserve(n); + + sim->StartTapeRecording(); + Qs[0] = sim->AllocateQubit(); + sim->NamedOperation("Hadamard", {}, {Qs[0]}, false); + + Qs[1] = sim->AllocateQubit(); + sim->NamedOperation("CRX", {0.123}, {Qs[0], Qs[1]}, false); + + Qs[2] = sim->AllocateQubit(); + sim->NamedOperation("CRY", {0.456}, {Qs[0], Qs[2]}, false); + + Qs[3] = sim->AllocateQubit(); + sim->NamedOperation("CRZ", {0.789}, {Qs[0], Qs[3]}, false); + sim->StopTapeRecording(); + + auto &&[num_ops, num_obs, num_params, op_names, obs_keys] = sim->CacheManagerInfo(); + CHECK((num_ops == 0 && num_obs == 0)); + CHECK(num_params == 0); + CHECK(op_names.empty()); + CHECK(obs_keys.empty()); +} + +TEST_CASE("Test a NullQubit circuit with num_qubits=4 and observables", "[NullQubit]") +{ + std::unique_ptr sim = std::make_unique(); + + // state-vector with #qubits = n + constexpr size_t n = 4; + std::vector Qs; + Qs.reserve(n); + for (size_t i = 0; i < n; i++) { + Qs[i] = sim->AllocateQubit(); + } + + sim->StartTapeRecording(); + sim->NamedOperation("PauliX", {}, {Qs[0]}, false); + sim->NamedOperation("PauliY", {}, {Qs[1]}, false); + sim->NamedOperation("Hadamard", {}, {Qs[2]}, false); + sim->NamedOperation("PauliZ", {}, {Qs[3]}, false); + + ObsIdType pz = sim->Observable(ObsId::PauliZ, {}, {Qs[0]}); + + ObsIdType px = sim->Observable(ObsId::PauliX, {}, {Qs[1]}); + ObsIdType h = sim->Observable(ObsId::Hadamard, {}, {Qs[0]}); + + sim->Var(h); + sim->Var(px); + sim->Expval(pz); + + auto &&[num_ops, num_obs, num_params, op_names, obs_keys] = sim->CacheManagerInfo(); + CHECK(num_ops == 0); + CHECK(num_obs == 0); + CHECK(num_params == 0); + CHECK(op_names.empty()); + CHECK(obs_keys.empty()); +} + +TEST_CASE("Test __catalyst__qis__Sample with num_qubits=2 and PartialSample calling Hadamard, " + "ControlledPhaseShift, IsingYY, and CRX quantum operations", + "[CoreQIS]") +{ + const auto [rtd_lib, rtd_name, rtd_kwargs] = + std::array{"null.qubit", "null_qubit", "{shots: 0}"}; + __catalyst__rt__initialize(nullptr); + __catalyst__rt__device_init((int8_t *)rtd_lib.c_str(), (int8_t *)rtd_name.c_str(), + (int8_t *)rtd_kwargs.c_str()); + + QirArray *qs = __catalyst__rt__qubit_allocate_array(2); + + QUBIT **target = (QUBIT **)__catalyst__rt__array_get_element_ptr_1d(qs, 0); + QUBIT **ctrls = (QUBIT **)__catalyst__rt__array_get_element_ptr_1d(qs, 1); + + // qml.Hadamard(wires=0) + __catalyst__qis__Hadamard(*target, NO_MODIFIERS); + // qml.ControlledPhaseShift(0.6, wires=[0,1]) + __catalyst__qis__ControlledPhaseShift(0.6, *target, *ctrls, NO_MODIFIERS); + // qml.IsingYY(0.2, wires=[0, 1]) + __catalyst__qis__IsingYY(0.2, *target, *ctrls, NO_MODIFIERS); + // qml.CRX(0.4, wires=[1,0]) + __catalyst__qis__CRX(0.4, *target, *ctrls, NO_MODIFIERS); + + constexpr size_t n = 1; + constexpr size_t shots = 1000; + + double *buffer = new double[shots * n]; + MemRefT_double_2d result = {buffer, buffer, 0, {shots, n}, {n, 1}}; + __catalyst__qis__Sample(&result, shots, 1, ctrls[0]); + + CHECK(shots == 1000); + + auto obs = __catalyst__qis__NamedObs(ObsId::PauliZ, *ctrls); + + CHECK(__catalyst__qis__Variance(obs) == Approx(0.0).margin(1e-5)); + + delete[] buffer; + + __catalyst__rt__qubit_release_array(qs); + __catalyst__rt__device_release(); + __catalyst__rt__finalize(); +} + +TEST_CASE("NullQubit (no) Basis vector", "[NullQubit]") +{ + std::unique_ptr sim = std::make_unique(); + + QubitIdType q = sim->AllocateQubit(); + q = sim->AllocateQubit(); + q = sim->AllocateQubit(); + + sim->ReleaseQubit(q); + + std::vector> state(1U << sim->GetNumQubits()); + DataView, 1> view(state); + sim->State(view); + + CHECK(view.size() == 1); + CHECK(view(0).real() == Approx(0.0).epsilon(1e-5)); + CHECK(view(0).imag() == Approx(0.0).epsilon(1e-5)); +} + +TEST_CASE("test AllocateQubits", "[NullQubit]") +{ + std::unique_ptr sim = std::make_unique(); + + CHECK(sim->AllocateQubits(0).size() == 0); + + auto &&q = sim->AllocateQubits(2); + + sim->ReleaseQubit(q[0]); + + std::vector> state(1U << sim->GetNumQubits()); + DataView, 1> view(state); + sim->State(view); + + CHECK(state.size() == 1); + CHECK(state[0].real() == Approx(0.0).epsilon(1e-5)); + CHECK(state[0].imag() == Approx(0.0).epsilon(1e-5)); +} + +TEST_CASE("Mix Gate test R(X,Y,Z) num_qubits=4", "[NullQubit]") +{ + std::unique_ptr sim = std::make_unique(); + + std::vector Qs = sim->AllocateQubits(4); + + sim->NamedOperation("PauliX", {}, {Qs[0]}, false); + + sim->NamedOperation("RX", {0.123}, {Qs[1]}, false); + sim->NamedOperation("RY", {0.456}, {Qs[2]}, false); + sim->NamedOperation("RZ", {0.789}, {Qs[3]}, false); + + std::vector> state(1U << sim->GetNumQubits()); + DataView, 1> view(state); + sim->State(view); + + CHECK(view.size() == 1); + CHECK(view(0).real() == Approx(0.0).epsilon(1e-5)); + CHECK(view(0).imag() == Approx(0.0).epsilon(1e-5)); +} + +TEST_CASE("Test __catalyst__qis__Gradient_params Op=[Hadamard,RZ,RY,RZ,S,T,ParamShift], " + "Obs=[X]", + "[Gradient]") +{ + const std::vector param{0.3, 0.7, 0.4}; + + std::vector trainParams{0, 1, 2}; + size_t J = trainParams.size(); + double *buffer = new double[J]; + MemRefT_double_1d result = {buffer, buffer, 0, {J}, {1}}; + double *buffer_tp = new double[J]; + MemRefT_double_1d result_tp = {buffer_tp, buffer_tp, 0, {J}, {1}}; + int64_t *buffer_tp_memref = trainParams.data(); + MemRefT_int64_1d tp_memref = {buffer_tp_memref, buffer_tp_memref, 0, {trainParams.size()}, {1}}; + + __catalyst__rt__initialize(nullptr); + + const auto [rtd_lib, rtd_name, rtd_kwargs] = + std::array{"null.qubit", "null_qubit", "{shots: 0}"}; + + __catalyst__rt__device_init((int8_t *)rtd_lib.c_str(), (int8_t *)rtd_name.c_str(), + (int8_t *)rtd_kwargs.c_str()); + + QUBIT *q0 = __catalyst__rt__qubit_allocate(); + QUBIT *q1 = __catalyst__rt__qubit_allocate(); + + __catalyst__rt__toggle_recorder(/* activate_cm */ true); + + __catalyst__qis__Hadamard(q0, NO_MODIFIERS); + __catalyst__qis__RZ(param[0], q0, NO_MODIFIERS); + __catalyst__qis__RY(param[1], q0, NO_MODIFIERS); + __catalyst__qis__RZ(param[2], q0, NO_MODIFIERS); + __catalyst__qis__S(q0, NO_MODIFIERS); + __catalyst__qis__T(q0, NO_MODIFIERS); + + auto obs_idx_0 = __catalyst__qis__NamedObs(ObsId::PauliX, q0); + + __catalyst__qis__Expval(obs_idx_0); + + __catalyst__qis__Gradient_params(&tp_memref, 1, &result_tp); + + __catalyst__qis__Gradient(1, &result); + + __catalyst__rt__toggle_recorder(/* activate_cm */ false); + + CHECK(result_tp.data_aligned[0] == Approx(0.0).margin(1e-5)); + CHECK(result_tp.data_aligned[1] == Approx(0.0).margin(1e-5)); + CHECK(result_tp.data_aligned[2] == Approx(0.0).margin(1e-5)); + + __catalyst__rt__qubit_release(q1); + __catalyst__rt__qubit_release(q0); + __catalyst__rt__device_release(); + __catalyst__rt__finalize(); + + delete[] buffer; + delete[] buffer_tp; +}