diff --git a/.agent/plans/qdmi-client-runtime-c1.md b/.agent/plans/qdmi-client-runtime-c1.md new file mode 100644 index 0000000000..db4ccbc6ae --- /dev/null +++ b/.agent/plans/qdmi-client-runtime-c1.md @@ -0,0 +1,38 @@ +# Replaceable QDMI Client driver + +Status: independent rebase validated locally. + +## Scope and decisions + +Core's Client wrappers load a validated implementation of the standard QDMI +Client ABI rather than link to Core's packaged driver. Driver selection is +process-wide; failed validation or allocation must leave retry possible. Owning +wrappers retain their originating session and the loaded function table. + +This change depends only on QDMI #511. Keep the existing program-format enum, +single-program APIs, calibration submission, and current compiler and SDK +behavior. Multi-program adoption and payload capabilities are independent work. +The optional private discovery/configuration extension is in Core PR #2230. +Installed deployment is in Core PR #2231. Standardizing that extension belongs +to QDMI v2, not this Client ABI change. + +Target Core 4.1 / QDMI 1.4, never Core 4.0. Development uses the isolated QDMI +driver branch; published artifacts require a released dependency version. + +## Implementation boundary + +The runtime is in `src/qdmi/Client.cpp` and `include/mqt-core/qdmi/Client.hpp`. +The packaged driver reports stable catalogue IDs through the standard property. +Bindings, SDK entry points, Slurm selection, and compiler device opening route +through the Client session; they must not call the packaged registry directly. +Existing compiler target inference still rejects unknown topology and gate sets. + +## Validation + +Validate ABI/symbol rejection, retry after failed allocation, process-wide +selection, session lifetime, malformed results, and packaged-driver loading. +Retain current optional-device builds and Slurm status semantics. Run +independent release build/CTest, QDMI and SDK Python suites, generated stubs, +repository lint, and C++ lint before publication. The release suite passed 3,869 +tests with one existing skip; all 455 selected Python tests passed. Stub +generation, repository lint, and C++ lint passed. diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e2cc9f384..b4bcb1359f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -106,6 +106,10 @@ releases may include breaking changes. - 💥 Replace the QDMI-specific primitives with native Qiskit primitives and typed backend factories. Sampler and `memory=True` require genuine QDMI `SHOTS` ([#2358]) ([**@burgholzer**]) +- 💥 Load one replaceable QDMI 1.4 Client driver through a validated function + table, split `MQT::CoreQDMI` from the packaged `MQT::CoreQDMIDriver`, and use + stable Client device IDs across C++, Python, MLIR, Qiskit, PennyLane, and + Slurm ([#2229]) ([**@burgholzer**]) - 💥 Drop support for x86 macOS and stop publishing the respective wheels ([#2259]) ([**@denialhaag**]) - ⬆️ Raise the macOS deployment target to 13.3 to enable `std::format` in libc++ @@ -907,6 +911,7 @@ for previous changelogs._ [#2257]: https://github.com/munich-quantum-toolkit/core/pull/2257 [#2246]: https://github.com/munich-quantum-toolkit/core/pull/2246 [#2240]: https://github.com/munich-quantum-toolkit/core/pull/2240 +[#2229]: https://github.com/munich-quantum-toolkit/core/pull/2229 [#2232]: https://github.com/munich-quantum-toolkit/core/pull/2232 [#2228]: https://github.com/munich-quantum-toolkit/core/pull/2228 [#2224]: https://github.com/munich-quantum-toolkit/core/pull/2224 diff --git a/UPGRADING.md b/UPGRADING.md index 2d05eae44f..8194107c89 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -6,6 +6,38 @@ of changes including minor and patch releases, please refer to the ## [Unreleased] +### QDMI Client driver boundary + +`MQT::CoreQDMI` and `MQT::CoreQDMIDriver` are now separate shared libraries. +`MQT::CoreQDMI` loads one implementation of the standard QDMI 1.4 Client +interface. It no longer links to MQT Core's packaged Driver. Set +`qdmi::SessionConfig::driverPath`, pass Python `driver_path`, or set the UTF-8 +`MQT_CORE_QDMI_DRIVER` environment value to select another Driver. The first +Driver that passes validation and allocates a raw session fixes the selection +for the process. A failed load or raw-session allocation can be retried. + +Python no longer exposes the `mqt.core.qdmi.driver` registry submodule. Import +`ClientSession` and `open_device` from `mqt.core.qdmi`: + +```python +from mqt.core.qdmi import ClientSession, open_device + +device_ids = [device.id for device in ClientSession().devices] +device = open_device(device_ids[0], token="access-token") +``` + +`Device.id` and `Device::getId()` return the stable ID reported by the Driver. +The generic session parameters are `token`, `auth_file`, `auth_url`, `username`, +`password`, `project_id`, and `custom1` through `custom5`. The selected Driver +owns their validation and meaning. The former Python `base_url`, +`device_config`, and `device_config_file` keywords were tied to MQT Core's +Driver and are not part of the generic Client boundary. Use persistent Driver +configuration when that Driver supports these settings. + +The MLIR `from_device_id` helpers and the Qiskit and PennyLane adapters accept +the same generic session parameters. Device, site, operation, and job wrappers +keep their originating Client session alive. + ### Removal of the classic circuit representation MQT Core 4 removes the complete classic circuit surface. This includes the C++ @@ -210,13 +242,11 @@ MQT Core removed the following names: MQT Core 4 removes the deprecated FoMaC names that MQT Core 3.9 kept as compatibility aliases. Replace Python imports of QDMI entities from -`mqt.core.fomac` with imports from `mqt.core.qdmi`. Import registry functions -and `DeviceDefinition` from `mqt.core.qdmi.driver`. +`mqt.core.fomac` with imports from `mqt.core.qdmi`. -MQT Core 4 also removes `mqt.core.qdmi.driver.Session`. Use -`registered_device_ids()` to discover devices and `open_device()` to open a -fresh device session. Pass provider configuration overrides to `open_device()` -when a device needs per-open configuration. +MQT Core 4 also removes `mqt.core.qdmi.driver.Session` and the remaining +`mqt.core.qdmi.driver` registry API. Use `ClientSession().devices` to discover +devices and top-level `open_device()` to open a fresh Client session. Apply these replacements to C++ and MLIR code: diff --git a/bindings/mlir/register_mlir.cpp b/bindings/mlir/register_mlir.cpp index 9d86673eb3..86fd9d86a8 100644 --- a/bindings/mlir/register_mlir.cpp +++ b/bindings/mlir/register_mlir.cpp @@ -19,7 +19,6 @@ #include "mlir/Dialect/QCO/Utils/DDFunctionality.h" #include "mlir/bench/Generate.h" #include "qdmi/Client.hpp" -#include "qdmi/driver/SessionConfig.hpp" #include "qiskit/Qiskit.h" #include @@ -189,6 +188,33 @@ template driverPath, + std::optional token, + std::optional authFile, + std::optional authUrl, std::optional username, + std::optional password, std::optional projectId, + std::optional custom1, std::optional custom2, + std::optional custom3, std::optional custom4, + std::optional custom5) { + return qdmi::Session::openDevice(deviceId, + { + .driverPath = std::move(driverPath), + .token = std::move(token), + .authFile = std::move(authFile), + .authUrl = std::move(authUrl), + .username = std::move(username), + .password = std::move(password), + .projectId = std::move(projectId), + .custom1 = std::move(custom1), + .custom2 = std::move(custom2), + .custom3 = std::move(custom3), + .custom4 = std::move(custom4), + .custom5 = std::move(custom5), + }); +} + template [[nodiscard]] static ProgramType copiedOrConsumed(ProgramType& program, const bool copy) { @@ -826,44 +852,35 @@ either unrestricted or explicitly enumerated native-operation support.)pb"); "device"_a, "Snapshot a circuit-model QDMI device.") .def_static( "from_device_id", - [](const std::string& deviceId, std::optional baseUrl, + [](const std::string& deviceId, + std::optional driverPath, std::optional token, std::optional authFile, std::optional authUrl, std::optional username, std::optional password, - std::optional deviceConfig, - std::optional deviceConfigFile, + std::optional projectId, std::optional custom1, std::optional custom2, std::optional custom3, std::optional custom4, std::optional custom5) { - // Keep this preflight at the Python boundary so the public - // ValueError does not depend on cross-extension exception - // translation. - if (deviceConfig && deviceConfigFile) { - throw nb::value_error( - "device_config and device_config_file are mutually " - "exclusive"); - } - const auto overrides = qdmi::makeDeviceSessionConfig( - std::move(baseUrl), std::move(token), std::move(authFile), - std::move(authUrl), std::move(username), std::move(password), - std::move(deviceConfig), std::move(deviceConfigFile), - std::move(custom1), std::move(custom2), std::move(custom3), - std::move(custom4), std::move(custom5)); - auto device = qdmi::Session::openDevice(deviceId, overrides); + auto device = openQDMIDevice( + deviceId, std::move(driverPath), std::move(token), + std::move(authFile), std::move(authUrl), std::move(username), + std::move(password), std::move(projectId), std::move(custom1), + std::move(custom2), std::move(custom3), std::move(custom4), + std::move(custom5)); return takeResult(mlir::compilerTargetFromDevice(device)); }, - "device_id"_a, nb::kw_only(), "base_url"_a = std::nullopt, + "device_id"_a, nb::kw_only(), "driver_path"_a = std::nullopt, "token"_a = std::nullopt, "auth_file"_a = std::nullopt, "auth_url"_a = std::nullopt, "username"_a = std::nullopt, - "password"_a = std::nullopt, "device_config"_a = std::nullopt, - "device_config_file"_a = std::nullopt, "custom1"_a = std::nullopt, - "custom2"_a = std::nullopt, "custom3"_a = std::nullopt, - "custom4"_a = std::nullopt, "custom5"_a = std::nullopt, - "Open a registered device and snapshot its compiler target.") + "password"_a = std::nullopt, "project_id"_a = std::nullopt, + "custom1"_a = std::nullopt, "custom2"_a = std::nullopt, + "custom3"_a = std::nullopt, "custom4"_a = std::nullopt, + "custom5"_a = std::nullopt, + "Open a Client-visible device and snapshot its compiler target.") .def_prop_ro( "name", [](const mlir::CompilerTarget& target) { diff --git a/bindings/qdmi/qdmi.cpp b/bindings/qdmi/qdmi.cpp index 1339bd6b06..08fdb93ad2 100644 --- a/bindings/qdmi/qdmi.cpp +++ b/bindings/qdmi/qdmi.cpp @@ -9,8 +9,6 @@ */ #include "qdmi/Client.hpp" -#include "qdmi/driver/Driver.hpp" -#include "qdmi/driver/SessionConfig.hpp" #include #include @@ -43,6 +41,31 @@ void registerSlurm(nb::module_& qdmiModule); } namespace { +qdmi::SessionConfig makeClientSessionConfig( + std::optional driverPath, + std::optional token, + std::optional authFile, + std::optional authUrl, std::optional username, + std::optional password, std::optional projectId, + std::optional custom1, std::optional custom2, + std::optional custom3, std::optional custom4, + std::optional custom5) { + return { + .driverPath = std::move(driverPath), + .token = std::move(token), + .authFile = std::move(authFile), + .authUrl = std::move(authUrl), + .username = std::move(username), + .password = std::move(password), + .projectId = std::move(projectId), + .custom1 = std::move(custom1), + .custom2 = std::move(custom2), + .custom3 = std::move(custom3), + .custom4 = std::move(custom4), + .custom5 = std::move(custom5), + }; +} + template [[nodiscard]] nb::object queryCustomValue(Query query, const nb::handle valueType) { @@ -82,11 +105,72 @@ template } // namespace NB_MODULE(MQT_CORE_MODULE_NAME, qdmiModule) { - qdmiModule.doc() = "QDMI entities and access to MQT Core's QDMI driver."; - auto driver = qdmiModule.def_submodule( - "driver", "Register, discover, and open QDMI devices through MQT Core."); + qdmiModule.doc() = "QDMI Client entities."; bindings::registerSlurm(qdmiModule); + nb::class_(qdmiModule, "ClientSession", + "One initialized QDMI Client session.") + .def( + "__init__", + [](qdmi::Session* self, + std::optional driverPath, + std::optional token, + std::optional authFile, + std::optional authUrl, + std::optional username, + std::optional password, + std::optional projectId, + std::optional custom1, + std::optional custom2, + std::optional custom3, + std::optional custom4, + std::optional custom5) { + new (self) qdmi::Session(makeClientSessionConfig( + std::move(driverPath), std::move(token), std::move(authFile), + std::move(authUrl), std::move(username), std::move(password), + std::move(projectId), std::move(custom1), std::move(custom2), + std::move(custom3), std::move(custom4), std::move(custom5))); + }, + nb::kw_only(), "driver_path"_a = std::nullopt, + "token"_a = std::nullopt, "auth_file"_a = std::nullopt, + "auth_url"_a = std::nullopt, "username"_a = std::nullopt, + "password"_a = std::nullopt, "project_id"_a = std::nullopt, + "custom1"_a = std::nullopt, "custom2"_a = std::nullopt, + "custom3"_a = std::nullopt, "custom4"_a = std::nullopt, + "custom5"_a = std::nullopt) + .def_prop_ro("devices", &qdmi::Session::getDevices, + "The devices visible to this authenticated session."); + + qdmiModule.def( + "open_device", + [](const std::string& deviceId, + std::optional driverPath, + std::optional token, + std::optional authFile, + std::optional authUrl, + std::optional username, + std::optional password, + std::optional projectId, + std::optional custom1, std::optional custom2, + std::optional custom3, std::optional custom4, + std::optional custom5) { + return qdmi::Session::openDevice( + deviceId, + makeClientSessionConfig( + std::move(driverPath), std::move(token), std::move(authFile), + std::move(authUrl), std::move(username), std::move(password), + std::move(projectId), std::move(custom1), std::move(custom2), + std::move(custom3), std::move(custom4), std::move(custom5))); + }, + "device_id"_a, nb::kw_only(), "driver_path"_a = std::nullopt, + "token"_a = std::nullopt, "auth_file"_a = std::nullopt, + "auth_url"_a = std::nullopt, "username"_a = std::nullopt, + "password"_a = std::nullopt, "project_id"_a = std::nullopt, + "custom1"_a = std::nullopt, "custom2"_a = std::nullopt, + "custom3"_a = std::nullopt, "custom4"_a = std::nullopt, + "custom5"_a = std::nullopt, + "Open a Client-visible device by stable ID in a fresh session."); + // Job class auto job = nb::class_( qdmiModule, "Job", @@ -268,6 +352,9 @@ text, so the device must receive it as exact bytes. Pass ``bytes`` to device.def("name", &qdmi::Device::getName, "Returns the name of the device."); + device.def_prop_ro("id", &qdmi::Device::getId, + "The stable Client-visible device ID."); + device.def("version", &qdmi::Device::getVersion, "Returns the version of the device."); @@ -620,172 +707,6 @@ when the custom slot is unsupported.)pb"); nb::sig("def __eq__(self, arg: object, /) -> bool")); operation.def(nb::self != nb::self, nb::sig("def __ne__(self, arg: object, /) -> bool")); - nb::class_( - driver, "DeviceDefinition", - R"pb(A stable QDMI device registration that can be stored before loading.)pb") - .def( - "__init__", - [](qdmi::DeviceDefinition* self, std::string deviceId, - std::filesystem::path libraryPath, std::string prefix, - const std::optional& baseUrl = std::nullopt, - const std::optional& token = std::nullopt, - const std::optional& authFile = - std::nullopt, - const std::optional& authUrl = std::nullopt, - const std::optional& username = std::nullopt, - const std::optional& password = std::nullopt, - const std::optional& deviceConfig = std::nullopt, - const std::optional& deviceConfigFile = - std::nullopt, - const std::optional& custom1 = std::nullopt, - const std::optional& custom2 = std::nullopt, - const std::optional& custom3 = std::nullopt, - const std::optional& custom4 = std::nullopt, - const std::optional& custom5 = std::nullopt) { - new (self) qdmi::DeviceDefinition{ - .id = std::move(deviceId), - .library = std::move(libraryPath), - .prefix = std::move(prefix), - .session = qdmi::makeDeviceSessionConfig( - baseUrl, token, authFile, authUrl, username, password, - deviceConfig, deviceConfigFile, custom1, custom2, custom3, - custom4, custom5), - }; - }, - "device_id"_a, "library_path"_a, "prefix"_a, nb::kw_only(), - "base_url"_a = std::nullopt, "token"_a = std::nullopt, - "auth_file"_a = std::nullopt, "auth_url"_a = std::nullopt, - "username"_a = std::nullopt, "password"_a = std::nullopt, - "device_config"_a = std::nullopt, - "device_config_file"_a = std::nullopt, "custom1"_a = std::nullopt, - "custom2"_a = std::nullopt, "custom3"_a = std::nullopt, - "custom4"_a = std::nullopt, "custom5"_a = std::nullopt, - R"pb(Create a device definition without loading its native library. - -Args: - device_id: Stable identifier used by :func:`open_device`. - library_path: Path to the shared QDMI device library. - prefix: Function prefix used by the library (for example, ``MY_DEVICE``). - base_url: Optional base URL for the device API endpoint. - token: Optional authentication token. - auth_file: Optional path to an authentication file. - auth_url: Optional authentication server URL. - username: Optional authentication username. - password: Optional authentication password. - device_config: Optional inline JSON device description. - device_config_file: Optional device-description JSON file. - custom1: Optional custom configuration parameter 1. - custom2: Optional custom configuration parameter 2. - custom3: Optional custom configuration parameter 3. - custom4: Optional custom configuration parameter 4. - custom5: Optional custom configuration parameter 5.)pb") - .def_ro("device_id", &qdmi::DeviceDefinition::id, - R"pb(Stable identifier used to open the device.)pb") - .def_ro("library_path", &qdmi::DeviceDefinition::library, - R"pb(Path to the native QDMI device library.)pb") - .def_ro("prefix", &qdmi::DeviceDefinition::prefix, - R"pb(Prefix used for the QDMI device interface functions.)pb"); - - driver.def( - "register_device", - [](qdmi::DeviceDefinition definition, const bool replace) { - qdmi::Driver::get().registerDevice(std::move(definition), replace); - }, - "definition"_a, nb::kw_only(), "replace"_a = false, - R"pb(Register a QDMI device definition without loading its library. - -Args: - definition: Definition to validate and store. - replace: Replace an existing definition if it has not been opened. - -Raises: - ValueError: If the definition is invalid or its ID is already registered. - RuntimeError: If replacing an already opened ID.)pb"); - - driver.def( - "register_device_if_absent", - [](qdmi::DeviceDefinition definition) { - return qdmi::Driver::get().registerDeviceIfAbsent( - std::move(definition)); - }, - "definition"_a, - R"pb(Register a valid QDMI device definition if its ID is absent. - -Existing and explicitly disabled IDs are not inserted. Invalid definitions -still raise. - -Args: - definition: Definition to validate and store. - -Returns: - bool: Whether the definition was inserted. - -Raises: - ValueError: If the definition is invalid.)pb"); - - driver.def( - "registered_device_ids", - [] { return qdmi::Driver::get().registeredDeviceIds(); }, - R"pb(Return registered, enabled QDMI device IDs in registration order. - -This includes devices registered at runtime and does not load native device -libraries or expose their definitions.)pb"); - - driver.def( - "open_device", - [](const std::string& deviceId, std::optional baseUrl, - std::optional token, - std::optional authFile, - std::optional authUrl, - std::optional username, - std::optional password, - std::optional deviceConfig, - std::optional deviceConfigFile, - std::optional custom1, std::optional custom2, - std::optional custom3, std::optional custom4, - std::optional custom5) { - const auto overrides = qdmi::makeDeviceSessionConfig( - std::move(baseUrl), std::move(token), std::move(authFile), - std::move(authUrl), std::move(username), std::move(password), - std::move(deviceConfig), std::move(deviceConfigFile), - std::move(custom1), std::move(custom2), std::move(custom3), - std::move(custom4), std::move(custom5)); - return qdmi::Session::openDevice(deviceId, overrides); - }, - "device_id"_a, nb::kw_only(), "base_url"_a = std::nullopt, - "token"_a = std::nullopt, "auth_file"_a = std::nullopt, - "auth_url"_a = std::nullopt, "username"_a = std::nullopt, - "password"_a = std::nullopt, "device_config"_a = std::nullopt, - "device_config_file"_a = std::nullopt, "custom1"_a = std::nullopt, - "custom2"_a = std::nullopt, "custom3"_a = std::nullopt, - "custom4"_a = std::nullopt, "custom5"_a = std::nullopt, - R"pb(Open a registered QDMI device by stable ID. - -Every call creates a fresh device session while keeping the stable registration -unchanged. Opening the device loads trusted native device code. - -Args: - device_id: Stable ID of a registered device. - base_url: Optional base URL override for the device API endpoint. - token: Optional authentication token override. - auth_file: Optional authentication-file override. - auth_url: Optional authentication server URL override. - username: Optional authentication username override. - password: Optional authentication password override. - device_config: Optional inline JSON device-description override. - device_config_file: Optional device-description JSON file override. - custom1: Optional custom configuration parameter 1 override. - custom2: Optional custom configuration parameter 2 override. - custom3: Optional custom configuration parameter 3 override. - custom4: Optional custom configuration parameter 4 override. - custom5: Optional custom configuration parameter 5 override. - -Returns: - mqt.core.qdmi.Device: The opened device, ready for direct backend construction. - -Raises: - IndexError: If the ID is not registered. - RuntimeError: If the device library cannot be loaded or initialized.)pb"); } } // namespace mqt diff --git a/bindings/qdmi/slurm.cpp b/bindings/qdmi/slurm.cpp index c3b2e4b3d2..1ecd2a53fb 100644 --- a/bindings/qdmi/slurm.cpp +++ b/bindings/qdmi/slurm.cpp @@ -23,11 +23,10 @@ void registerSlurm(nb::module_& qdmiModule) { slurm.def("open_device_from_license", &qdmi::slurm::openDeviceFromLicense, R"pb(Open the QDMI device named by the Slurm license environment. -``SLURM_JOB_LICENSES`` must contain one local license whose name equals a -registered QDMI device ID. The optional count must be one. The function opens a -fresh device session from the persistent definition and accepts device status -``IDLE`` or ``BUSY``. It does not apply job-specific QDMI configuration or -credentials. +``SLURM_JOB_LICENSES`` must contain one local license whose name equals a stable +ID visible to the selected QDMI Driver. The optional count must be one. The +function opens a fresh Client session and accepts device status ``IDLE`` or +``BUSY``. It does not apply job-specific QDMI configuration or credentials. Warning: ``SLURM_JOB_LICENSES`` is process-mutable. This function uses it only for diff --git a/cmake/AddMQTQDMIDevice.cmake b/cmake/AddMQTQDMIDevice.cmake index 951975090f..710a0f4fdd 100644 --- a/cmake/AddMQTQDMIDevice.cmake +++ b/cmake/AddMQTQDMIDevice.cmake @@ -147,16 +147,43 @@ function(mqt_get_qdmi_device_targets result) PARENT_SCOPE) endfunction() -# Copy QDMI device libraries and their manifests beside a static consumer executable. +# Copy in-tree QDMI runtime libraries and manifests beside a runtime consumer. function(mqt_copy_qdmi_runtime target) if(NOT TARGET ${target}) message(FATAL_ERROR "Unknown QDMI runtime consumer target: ${target}") endif() + set_property(TARGET ${target} PROPERTY BUILD_WITH_INSTALL_RPATH FALSE) + get_target_property(consumer_target ${target} ALIASED_TARGET) + if(NOT consumer_target) + set(consumer_target ${target}) + endif() + foreach(runtime_target IN ITEMS MQT::CoreQDMI MQT::CoreQDMIDriver) + if(TARGET ${runtime_target}) + get_target_property(runtime_concrete_target ${runtime_target} ALIASED_TARGET) + if(NOT runtime_concrete_target) + set(runtime_concrete_target ${runtime_target}) + endif() + get_target_property(runtime_imported ${runtime_concrete_target} IMPORTED) + if(NOT runtime_imported AND NOT consumer_target STREQUAL runtime_concrete_target) + add_dependencies(${consumer_target} ${runtime_concrete_target}) + set(runtime_files "$") + if(WIN32) + list(APPEND runtime_files "$") + endif() + add_custom_command( + TARGET ${consumer_target} + POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different ${runtime_files} + "$" + COMMAND_EXPAND_LISTS) + endif() + endif() + endforeach() set(devices ${ARGN}) if(NOT devices) mqt_get_qdmi_device_targets(devices) endif() - if(NOT devices) + if(NOT devices AND NOT TARGET MQT::CoreQDMIDriver) message(FATAL_ERROR "mqt_copy_qdmi_runtime requires at least one QDMI device target") endif() foreach(device IN LISTS devices) @@ -195,13 +222,17 @@ function(mqt_copy_qdmi_runtime target) if(NOT device_imported) add_dependencies(${target} ${device}) endif() + set(device_files "$") + if(WIN32 AND NOT device_imported) + list(APPEND device_files "$") + endif() add_custom_command( TARGET ${target} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different "$" - "$" + COMMAND ${CMAKE_COMMAND} -E copy_if_different ${device_files} "$" COMMAND ${CMAKE_COMMAND} -E copy_if_different "${manifest}" - "$/${manifest_name}") + "$/${manifest_name}" + COMMAND_EXPAND_LISTS) get_target_property(runtime_files ${device_target} QDMI_RUNTIME_FILES) if(runtime_files) foreach(runtime_file IN LISTS runtime_files) diff --git a/cmake/ExternalDependencies.cmake b/cmake/ExternalDependencies.cmake index a1d267880f..fb05e0cb3f 100644 --- a/cmake/ExternalDependencies.cmake +++ b/cmake/ExternalDependencies.cmake @@ -67,11 +67,11 @@ if(BUILD_MQT_CORE_TESTS) endif() # cmake-format: off -set(QDMI_MINIMUM_VERSION 1.3.3 +set(QDMI_MINIMUM_VERSION 1.4.0 CACHE STRING "Minimum QDMI version") -set(QDMI_VERSION 1.3.3 +set(QDMI_VERSION 1.4.0 CACHE STRING "QDMI version") -set(QDMI_REV "18cfb67fd9042761d3005c2f8655751c1758f9c5" # v1.3.3 +set(QDMI_REV "c7494d6b6e7b0104ccf30fe079e88adeeb8e2d00" # PR #511 CACHE STRING "QDMI identifier (tag, branch or commit hash)") set(QDMI_REPO_OWNER "Munich-Quantum-Software-Stack" CACHE STRING "QDMI repository owner (change when using a fork)") diff --git a/docs/Doxyfile b/docs/Doxyfile index e3ec51ebf9..d0d1cb1ce3 100644 --- a/docs/Doxyfile +++ b/docs/Doxyfile @@ -2101,7 +2101,7 @@ SKIP_FUNCTION_MACROS = YES # the path). If a tag file is not located in the directory in which Doxygen is # run, you must also specify the path to the tagfile here. -TAGFILES = _build/qdmi.tag=https://munich-quantum-software-stack.github.io/QDMI/v1.3.2/ +TAGFILES = _build/qdmi.tag=https://munich-quantum-software-stack.github.io/QDMI/pr-preview/pr-511/ # When a file name is specified after GENERATE_TAGFILE, Doxygen will create a # tag file that is based on the input files it reads. See section "Linking to diff --git a/docs/conf.py b/docs/conf.py index 8743d4ba7c..0c5bc6e2ac 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -176,10 +176,12 @@ def format_url(self, _e: Entry) -> HRef: # ruff:ignore[no-self-use] cpp_api_tagfile = ("_build/doxygen/mqt-core.tag", "cpp/", "_build/doxygen/xml") +_qdmi_api_base = "https://munich-quantum-software-stack.github.io/QDMI/pr-preview/pr-511/" qdmi_api_tagfile = ( "_build/qdmi.tag", - "https://munich-quantum-software-stack.github.io/QDMI/v1.3.3/", + _qdmi_api_base, ) +qdmi_api_tagfile_url = f"{_qdmi_api_base}qdmi.tag" # -- Options for HTML output ------------------------------------------------- diff --git a/docs/glossary.md b/docs/glossary.md index c2ecdb889b..4aca704515 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -99,6 +99,17 @@ Quantum Device Management Interface discovering quantum-device properties and submitting and controlling work without coupling software to one device implementation. +QDMI Client interface + **Preferred term:** QDMI Client interface. The standard C interface consumed by + applications to open sessions, query devices, and manage jobs. Core's C++ and + Python Client wrappers consume this interface; they are not a driver. + +QDMI driver + **Preferred term:** QDMI driver. An implementation of the QDMI Client interface. + Core's packaged driver loads QDMI device libraries. A replacement driver owns + its own discovery, configuration, and device access; applications must not + assume it provides Core's private driver extension. + QIR Quantum Intermediate Representation **Preferred term:** Quantum Intermediate Representation. **Accepted diff --git a/docs/qdmi/configuration.md b/docs/qdmi/configuration.md index 9134dbcb8a..1eb711e75d 100644 --- a/docs/qdmi/configuration.md +++ b/docs/qdmi/configuration.md @@ -103,67 +103,29 @@ retaining packaged built-ins. ## Using configured devices -When the QDMI driver initializes a client session, it opens the configured -definitions. A failure to load one definition does not hide the remaining -devices. Stable-ID registration does not initialize device libraries. +When the packaged QDMI Driver initializes a Client session, it opens the +configured definitions. A failure to load one definition does not hide the +remaining devices. ```python -from mqt.core.qdmi.driver import open_device, registered_device_ids +from mqt.core.qdmi import ClientSession, open_device -for device_id in registered_device_ids(): - print(open_device(device_id).name()) +for discovered in ClientSession().devices: + print(discovered.id, open_device(discovered.id).name()) ``` Set `MQT_CORE_QDMI_CONFIG_FILE` or `MQT_CORE_QDMI_CONFIG_JSON` before the first -driver call. Applications can also register a definition without loading its -library and open it later by stable ID: - -```python -from mqt.core.qdmi.driver import DeviceDefinition, open_device, register_device - -register_device( - DeviceDefinition( - "example.device", - "/path/to/libexample-device.so", - "EXAMPLE", - base_url="https://device.example", - device_config_file="/path/to/device.json", - ) -) -device = open_device("example.device") -``` - -{py:class}`~mqt.core.qdmi.driver.DeviceDefinition` and -{py:func}`~mqt.core.qdmi.driver.open_device` also accept -`device_config=""` for inline configuration. `device_config` and -`device_config_file` are mutually exclusive. - -Every {py:func}`~mqt.core.qdmi.driver.open_device` call creates a fresh device -session while preserving the registered defaults and stable ID. The returned -{py:class}`~mqt.core.qdmi.Device` and any +Driver call. Every {py:func}`~mqt.core.qdmi.open_device` call creates a fresh +Client session and finds the stable ID in the standard Client device list. The +returned {py:class}`~mqt.core.qdmi.Device` and any {py:class}`~mqt.core.qdmi.Device.Site`, {py:class}`~mqt.core.qdmi.Device.Operation`, or {py:class}`~mqt.core.qdmi.Job` -wrapper derived from it keeps that fresh device session alive. The session is -released after the last such wrapper is destroyed. - -Code paths that may be imported more than once can use -{py:func}`~mqt.core.qdmi.driver.register_device_if_absent`. It returns whether -the definition was inserted and ignores an existing or explicitly disabled -stable ID; malformed definitions still raise an error. - -Use {py:func}`~mqt.core.qdmi.driver.registered_device_ids` to inspect the -enabled stable IDs in deterministic registration order. This includes runtime -registrations without loading native device libraries or exposing their paths, -prefixes, or session configuration. - -The equivalent C++ registration operation is -{cpp-api:func}`qdmi::Driver::registerDevice`. Duplicate IDs are rejected unless -`replace` is true, and an opened definition cannot be replaced. -{cpp-api:func}`qdmi::Driver::registeredDeviceIds` provides the same load-free -enumeration, and {cpp-api:func}`qdmi::Driver::open` returns the cached device. -{cpp-api:func}`qdmi::Session::openDevice` returns a fresh device session and -does not add it to the QDMI client catalog. Runtime registrations and explicit -opens are not added to that catalog. +wrapper derived from it keeps that Client session alive. The session is released +after the last such wrapper is destroyed. + +The equivalent C++ API is {cpp-api:class}`qdmi::Session`. `getDevices()` +enumerates one authenticated session. {cpp-api:func}`qdmi::Session::openDevice` +creates a fresh session and opens one enumerated ID. Multiple definitions may refer to the same library and prefix. MQT Core reuses the initialized library while creating a fresh QDMI device session, with its own @@ -172,9 +134,9 @@ session parameters, for every definition. ## Selecting a device from a Slurm license environment MQT Core provides a mechanism-specific adapter for jobs that use local Slurm -licenses for cluster-wide admission. The license name must equal one registered -QDMI device ID. Register one definition per separately licensed machine. Each -job must request one license. For example: +licenses for cluster-wide admission. The license name must equal one stable ID +reported by the selected QDMI Driver. Each job must request one license. For +example: ```bash sbatch --licenses=mqt.ddsim.default:1 simulation.sh @@ -190,8 +152,8 @@ device = slurm.open_device_from_license() The equivalent C++ function is `qdmi::slurm::openDeviceFromLicense()` from `qdmi/Slurm.hpp`. Both functions read `SLURM_JOB_LICENSES`. They accept only -`` or `:1`. They reject remote, -compound, and non-unit license values. +`` or `:1`. They reject remote, compound, and non-unit +license values. The adapter opens a fresh device session from the persistent definition. It does not replace configuration or inject credentials. Each provider defines its own @@ -203,9 +165,9 @@ device selection. It does not verify that Slurm allocated the license. It does not authenticate the caller or authorize access to the device. Provider credentials must authorize remote devices. The operating system must isolate a local device when access requires enforcement. A caller can also bypass this -adapter and call {py:func}`~mqt.core.qdmi.driver.open_device` with a stable -device ID. A different Slurm lookup would therefore not make MQT Core an access -control boundary. +adapter and call {py:func}`~mqt.core.qdmi.open_device` with a stable device ID. +A different Slurm lookup would therefore not make MQT Core an access control +boundary. A cluster can configure more than one license for a device. For example, `mqt.ddsim.default:2` permits two independent jobs to request one license each. diff --git a/docs/qdmi/ddsim_device.md b/docs/qdmi/ddsim_device.md index 2c598ed26d..eeb0f25c84 100644 --- a/docs/qdmi/ddsim_device.md +++ b/docs/qdmi/ddsim_device.md @@ -59,8 +59,7 @@ program to QIR, and submit the resulting bitcode to the same device: ```python from mqt.core.mlir import CompilerTarget, OutputFormat, compile_program -from mqt.core.qdmi import ProgramFormat -from mqt.core.qdmi.driver import open_device +from mqt.core.qdmi import ProgramFormat, open_device device = open_device("mqt.ddsim.default") target = CompilerTarget.from_device(device) diff --git a/docs/qdmi/driver.md b/docs/qdmi/driver.md index 7b15418d9b..fa99bfb4b3 100644 --- a/docs/qdmi/driver.md +++ b/docs/qdmi/driver.md @@ -6,22 +6,35 @@ mystnb: number_source_lines: true --- -# MQT Core's QDMI Driver Implementation +# QDMI Client and Driver Runtime ## Objective -A QDMI Driver manages the communication between QDMI devices, such as -[MQT Core's SC QDMI Device](sc_device.md) or -[MQT Core's DDSIM QDMI Device](ddsim_device.md), and QDMI clients, see the -[QDMI specification](https://munich-quantum-software-stack.github.io/QDMI/). -It is responsible for loading the device, forwarding requests from the client to -the device, and sending back the results. MQT Core's QDMI Driver, -{cpp-api:class}`qdmi::Driver`, comes with several preloaded devices when the -bundled devices are enabled. Other devices can be loaded dynamically at runtime -via {cpp-api:func}`qdmi::Driver::registerDevice` and -{cpp-api:func}`qdmi::Driver::open`. Built-in and external devices can also be -registered through -[versioned QDMI device configuration](configuration.md). +MQT Core consumes the standard QDMI 1.4 Client interface. `MQT::CoreQDMI` owns +the C++ wrappers and loads one Client driver at runtime. It does not link to a +specific Driver implementation. `MQT::CoreQDMIDriver` is the packaged shared +Driver. It loads devices such as [the SC QDMI Device](sc_device.md) and +[the DDSIM QDMI Device](ddsim_device.md). + +This boundary lets another QDMI 1.4 Driver implement the Client interface +without linking to MQT Core's Driver. MQT Core checks the complete Client +function table and the QDMI Client ABI major and minor versions before it +allocates a session. + +## Driver Selection + +MQT Core selects the Client driver for the process after the Driver passes ABI +and function-table validation and allocates the first raw session. The selection +order is: + +1. `qdmi::SessionConfig::driverPath` or Python `driver_path`; +2. the UTF-8 `MQT_CORE_QDMI_DRIVER` environment value; +3. the packaged `MQT::CoreQDMIDriver` library. + +The selection remains active until process exit. A later explicit request for a +different Driver fails. A failed load, ABI check, symbol check, or raw-session +allocation does not select a Driver, so a later call can retry. MQT Core keeps +the selected shared library loaded while its function pointers can be used. ## Building the Bundled Devices @@ -42,28 +55,34 @@ For example, an embedded simulator consumer can enable only the DDSIM device, while CUDA-Q can enable the DDSIM and superconducting devices used by its integration tests. -The QDMI driver and QDMI libraries are available independently. Device-free -builds can register external device libraries through -[QDMI device configuration](configuration.md). C++ test builds require every -bundled device available in the selected build configuration. +The Client and Driver libraries are separate shared libraries. Device-free +builds can use another QDMI 1.4 Driver through `driver_path` or +`MQT_CORE_QDMI_DRIVER`. The packaged Driver can load external device libraries +through [QDMI device configuration](configuration.md). C++ test builds require +the bundled devices available in the selected build configuration. ## Python Bindings -The QDMI interface is the low-level contract implemented by a QDMI device. The -MQT Core QDMI driver loads device libraries and implements the QDMI client -interface. The C++ QDMI library adds owning wrappers for QDMI devices, sites, -operations, and jobs. The Python module exposes these QDMI entities through -{py:mod}`mqt.core.qdmi`. Its {py:mod}`mqt.core.qdmi.driver` submodule provides -device discovery, registration, and opening. +The C++ QDMI library adds owning wrappers for Client sessions, devices, sites, +operations, and jobs. Each wrapper retains the Client session that owns its raw +handle. The Python module exposes the same entities through +{py:mod}`mqt.core.qdmi`. ## Usage -The following example opens each registered device by its stable ID. +The following example enumerates the devices visible to one authenticated Client +session. Each Driver supplies a stable `id` property. `open_device` starts a +fresh session and finds that ID in the standard Client device list. ```{code-cell} ipython3 -from mqt.core.qdmi.driver import open_device, registered_device_ids +from mqt.core.qdmi import ClientSession, open_device -for device_id in registered_device_ids(): - device = open_device(device_id) +for discovered in ClientSession().devices: + device = open_device(discovered.id) print(device.name()) ``` + +All session keywords map to standard QDMI parameters. They are `token`, +`auth_file`, `auth_url`, `username`, `password`, `project_id`, and `custom1` +through `custom5`. The selected Driver defines validation, precedence, and the +meaning of these values. diff --git a/docs/qdmi/index.md b/docs/qdmi/index.md index d222a1267b..3fa719723c 100644 --- a/docs/qdmi/index.md +++ b/docs/qdmi/index.md @@ -14,7 +14,7 @@ components, such as a [QDMI driver](driver.md), a SC QDMI Device DDSIM QDMI Device -QDMI Driver +QDMI Client and Driver runtime QDMI device configuration Slurm integration QDMI-Qiskit Backend diff --git a/docs/qdmi/pennylane_device.md b/docs/qdmi/pennylane_device.md index f34e7c6d57..db3266ce80 100644 --- a/docs/qdmi/pennylane_device.md +++ b/docs/qdmi/pennylane_device.md @@ -302,14 +302,14 @@ import pennylane as qp from mqt.core.plugins.pennylane import QDMIDevice -device_id = "stable ID returned by the QDMI device registration" +device_id = "stable ID reported by the QDMI Driver" device = QDMIDevice( device_id=device_id, wires=["a", "b", "c", "d"], shots=[(100, 2), 500], session_parameters={ - "base_url": "device endpoint or selector", "token": "...", + "project_id": "project", }, job_parameters={ "custom1": "device-specific job value", diff --git a/docs/qdmi/qdmi_backend.md b/docs/qdmi/qdmi_backend.md index 8129026a8f..6081acf2ae 100644 --- a/docs/qdmi/qdmi_backend.md +++ b/docs/qdmi/qdmi_backend.md @@ -68,7 +68,7 @@ print(f"Results: {counts}") ### Using the Provider The {py:class}`~mqt.core.plugins.qiskit.provider.QDMIProvider` discovers -registered QDMI devices. Use it when an application must enumerate backends. +Client-visible QDMI devices. Use it when an application must enumerate backends. ```{code-cell} ipython3 from mqt.core.plugins.qiskit import QDMIProvider @@ -93,10 +93,9 @@ print(f"Backend: {backend.name}") print(f"Qubits: {backend.target.num_qubits}") ``` -Optional session keywords apply explicit overrides to this fresh device session. -Their names and value types are described by -{py:class}`mqt.core.typing.QDMISessionParameters`; persistent configuration -remains the default: +Optional session keywords configure this fresh Client session. Their names and +value types are described by {py:class}`mqt.core.typing.QDMISessionParameters`. +The selected Driver defines their meaning and precedence: ```python backend = QDMIBackend.from_device_id( @@ -119,12 +118,12 @@ exact = provider.backends(name="MQT Core DDSIM QDMI Device") ## Authentication -{py:class}`~mqt.core.plugins.qiskit.provider.QDMIProvider` does not define a -generic credential interface. It opens each registered device with its -persistent definition. Configure credentials through the selected QDMI device -implementation. For example, a provider can use a credential file, an -environment variable, or a platform credential-provider chain. See -[QDMI device configuration](configuration.md) for persistent session settings. +`QDMIBackend.from_device_id` and `QDMIProvider.get_backend_by_device_id` accept +the standard QDMI Client authentication parameters: `token`, `auth_file`, +`auth_url`, `username`, `password`, and `project_id`. The selected Driver owns +validation and can also use environment variables or a platform credential +provider. `QDMIProvider.backends()` uses a fresh session without explicit +authentication parameters. ## Device Capabilities and Target diff --git a/docs/qdmi/slurm.md b/docs/qdmi/slurm.md index 7cb690b0a3..5785d29ca7 100644 --- a/docs/qdmi/slurm.md +++ b/docs/qdmi/slurm.md @@ -15,8 +15,8 @@ cluster. It does not make a Slurm license an access-control credential. The controls are independent: - Slurm admits jobs and accounts for the configured license count. -- The MQT Core adapter uses the license environment to select a registered QDMI - device. +- The MQT Core adapter uses the license environment to select a Client-visible + QDMI device. - The QDMI provider reports device availability and queue data. - The provider or the operating system authorizes access to the device. @@ -25,7 +25,7 @@ controls are independent: allocated the named license. It does not authenticate the user. It does not authorize access. A lookup through another Slurm interface would not make MQT Core an access-control boundary because a program can also call -`driver.open_device(device_id)` directly. +`mqt.core.qdmi.open_device(device_id)` directly. ## Install the software @@ -74,7 +74,7 @@ devices need no further registry file. Verify their stable IDs before you configure Slurm: ```console -python -c "from mqt.core.qdmi import driver; print(*driver.registered_device_ids(), sep='\n')" +python -c "from mqt.core.qdmi import ClientSession; print(*(device.id for device in ClientSession().devices), sep='\n')" ``` For an external provider, install its shared library and QDMI manifest. You can @@ -215,10 +215,10 @@ on that node. Check that `/sys/fs/cgroup/cgroup.controllers` exists. Check that all nodes use the same Munge key and the same `slurm.conf`. If MQT Core cannot select a device, print `SLURM_JOB_LICENSES` inside the batch -job and list the registered QDMI IDs. Use this value only to diagnose selection. -It is not proof of the Slurm allocation. The license name and stable ID must -match exactly. Do not add a generic device license. Do not use a Slurm OR -license expression for device selection because the environment does not +job and list the IDs visible to `ClientSession`. Use this value only to diagnose +selection. It is not proof of the Slurm allocation. The license name and stable +ID must match exactly. Do not add a generic device license. Do not use a Slurm +OR license expression for device selection because the environment does not identify a single selected device in that case. [Slurm GRES configuration]: https://slurm.schedmd.com/gres.conf.html diff --git a/include/mqt-core/qdmi/Client.hpp b/include/mqt-core/qdmi/Client.hpp index 71fa9681f3..07eb57e79f 100644 --- a/include/mqt-core/qdmi/Client.hpp +++ b/include/mqt-core/qdmi/Client.hpp @@ -15,7 +15,6 @@ #pragma once #include "qdmi/common/Common.hpp" -#include "qdmi/driver/Driver.hpp" #include "qdmi/types.h" #include @@ -69,6 +68,73 @@ concept custom_property_value = std::same_as>; namespace detail { +struct ClientApi { + decltype(&::QDMI_driver_get_client_abi_version) driverGetClientAbiVersion{}; + decltype(&::QDMI_session_alloc) sessionAlloc{}; + decltype(&::QDMI_session_init) sessionInit{}; + decltype(&::QDMI_session_free) sessionFree{}; + decltype(&::QDMI_session_set_parameter) sessionSetParameter{}; + decltype(&::QDMI_session_query_session_property) sessionQueryProperty{}; + decltype(&::QDMI_device_create_job) deviceCreateJob{}; + decltype(&::QDMI_session_retrieve_job_by_id) sessionRetrieveJobById{}; + decltype(&::QDMI_job_free) jobFree{}; + decltype(&::QDMI_job_set_parameter) jobSetParameter{}; + decltype(&::QDMI_job_query_property) jobQueryProperty{}; + decltype(&::QDMI_job_submit) jobSubmit{}; + decltype(&::QDMI_job_cancel) jobCancel{}; + decltype(&::QDMI_job_check) jobCheck{}; + decltype(&::QDMI_job_wait) jobWait{}; + decltype(&::QDMI_job_get_results) jobGetResults{}; + decltype(&::QDMI_device_query_device_property) deviceQueryProperty{}; + decltype(&::QDMI_device_query_site_property) deviceQuerySiteProperty{}; + decltype(&::QDMI_device_query_operation_property) + deviceQueryOperationProperty{}; +}; + +struct ClientSession { + ClientSession(std::shared_ptr selectedApi, + QDMI_Session selectedSession) + : api(std::move(selectedApi)), handle(selectedSession) {} + ~ClientSession(); + + ClientSession(const ClientSession&) = delete; + ClientSession& operator=(const ClientSession&) = delete; + + std::shared_ptr api; + QDMI_Session handle; +}; + +struct JobDeleter { + void operator()(QDMI_Job_impl_d* job) const; + std::shared_ptr session; +}; + +[[nodiscard]] inline std::string +decodeText(std::string value, const std::string_view description) { + if (value.empty() || value.back() != '\0') { + throw std::invalid_argument(std::string(description) + + " is not null-terminated"); + } + if (value.find('\0') != value.size() - 1U) { + throw std::invalid_argument(std::string(description) + + " contains an embedded null byte"); + } + value.pop_back(); + return value; +} + +[[nodiscard]] inline std::string +decodeText(const std::span value, + const std::string_view description) { + if (value.empty()) { + throw std::invalid_argument(std::string(description) + + " is not null-terminated"); + } + return decodeText( + std::string{reinterpret_cast(value.data()), value.size()}, + description); +} + [[nodiscard]] inline std::optional queuePositionFromResult(const int result, const size_t queuePosition) { if (result == QDMI_ERROR_NOTSUPPORTED || result == QDMI_ERROR_BADSTATE) { @@ -143,6 +209,16 @@ queryCustomValue(Query query, const std::string_view description) { } } +template +void validateArraySize(const size_t size, const std::string_view description) { + if (size % sizeof(Element) != 0U) { + throw std::invalid_argument( + "Cannot decode " + std::string(description) + ": the device reported " + + std::to_string(size) + " bytes, which is not a multiple of " + + std::to_string(sizeof(Element))); + } +} + template [[nodiscard]] std::optional> queryHandleArray(Query query, const std::string_view description) { @@ -153,12 +229,7 @@ queryHandleArray(Query query, const std::string_view description) { } qdmi::throwIfError(sizeResult, "Querying " + std::string(description) + " size"); - if (size % sizeof(Handle) != 0) { - throw std::invalid_argument( - "Cannot decode " + std::string(description) + ": the device reported " + - std::to_string(size) + " bytes, which is not a multiple of " + - std::to_string(sizeof(Handle))); - } + validateArraySize(size, description); std::vector handles(size / sizeof(Handle)); if (size != 0) { @@ -330,6 +401,8 @@ concept string_or_optional_string = (is_optional && std::same_as); /// @see remove_optional_t +/// The name follows the standard-library type-trait convention. +/// NOLINTNEXTLINE(readability-identifier-naming) template struct remove_optional { using type = T; }; @@ -389,6 +462,9 @@ concept maybe_optional_value_or_string_or_vector = * constructed. */ struct SessionConfig { + /// QDMI Client driver library. Uses the environment or packaged driver when + /// omitted. + std::optional driverPath; /// Authentication token std::optional token; /// Path to file containing authentication information @@ -427,23 +503,13 @@ class Operation; class Session { public: /** - * @brief Creates a Device object from a QDMI_Device handle. - * @param device The QDMI_Device handle to wrap. - * @return A Device object wrapping the given handle. - * @note This is a factory method for use in bindings where a - * session is not accessible. - */ - [[nodiscard]] static Device createSessionlessDevice(QDMI_Device device); - - /** - * @brief Opens a registered QDMI device as a fresh device session. - * @param id Stable registered device ID. - * @param overrides Session values that replace registered defaults. - * @return An owning device wrapper for the new session. + * @brief Opens a Client-visible QDMI device in a fresh session. + * @param id Stable device ID. + * @param config Client driver and authentication configuration. + * @return A device wrapper that retains the fresh session. */ - [[nodiscard]] static Device - openDevice(std::string_view id, - const qdmi::DeviceSessionConfig& overrides = {}); + [[nodiscard]] static Device openDevice(std::string_view id, + const SessionConfig& config = {}); /** * @brief Constructs a new QDMI Session with optional authentication. @@ -453,28 +519,36 @@ class Session { */ explicit Session(const SessionConfig& config = {}); + Session(const Session&) = delete; + Session& operator=(const Session&) = delete; + Session(Session&&) noexcept = default; + Session& operator=(Session&&) noexcept = default; + /// @see QDMI_SESSION_PROPERTY_DEVICES [[nodiscard]] std::vector getDevices(); private: + [[nodiscard]] const detail::ClientApi& api() const { return *session_->api; } + /// Query a session property. template [[nodiscard]] T queryProperty(const QDMI_Session_Property prop) const { using StrippedValueType = remove_optional_t::value_type; size_t size = 0; - qdmi::throwIfError(QDMI_session_query_session_property(session_.get(), prop, - 0, nullptr, &size), + qdmi::throwIfError(session_->api->sessionQueryProperty( + session_->handle, prop, 0, nullptr, &size), std::string("Querying size ") + qdmi::toString(prop)); + detail::validateArraySize(size, qdmi::toString(prop)); remove_optional_t value(size / sizeof(StrippedValueType)); - qdmi::throwIfError(QDMI_session_query_session_property( - session_.get(), prop, size, value.data(), nullptr), + qdmi::throwIfError(session_->api->sessionQueryProperty( + session_->handle, prop, size, + static_cast(value.data()), nullptr), std::string("Querying ") + qdmi::toString(prop)); return value; } - std::unique_ptr session_{ - nullptr, QDMI_session_free}; + std::shared_ptr session_; }; static_assert(!std::is_copy_constructible()); @@ -494,8 +568,11 @@ static_assert(std::is_move_assignable()); */ class Device { public: - // NOLINTNEXTLINE(misc-explicit-constructor, *-explicit-conversions) - operator QDMI_Device() const { return device_.get(); } + // NOLINTNEXTLINE(google-explicit-constructor, *-explicit-conversions) + operator QDMI_Device() const { return device_; } + + /// @see QDMI_DEVICE_PROPERTY_ID + [[nodiscard]] std::string getId() const; /// @see QDMI_DEVICE_PROPERTY_NAME [[nodiscard]] std::string getName() const; @@ -591,8 +668,8 @@ class Device { const auto qdmiProperty = detail::toDeviceProperty(property); return detail::queryCustomValue( [this, qdmiProperty](const size_t size, void* value, size_t* sizeRet) { - return QDMI_device_query_device_property(device_.get(), qdmiProperty, - size, value, sizeRet); + return session_->api->deviceQueryProperty(device_, qdmiProperty, size, + value, sizeRet); }, "custom device property " + std::to_string(static_cast(property))); @@ -717,19 +794,15 @@ class Device { auto operator<=>(const Device&) const noexcept = default; private: + [[nodiscard]] const detail::ClientApi& api() const { return *session_->api; } + /** * @brief Constructs a Device object from a QDMI_Device handle. * @param device The QDMI_Device handle to wrap. + * @param session The Client session that owns the handle. */ - explicit Device(QDMI_Device device) - : device_(device, [](QDMI_Device_impl_d*) {}) {} - - /** - * @brief Constructs a wrapper that retains an owning session. - * @param device The QDMI device handle to wrap. - */ - explicit Device(std::shared_ptr device) - : device_(std::move(device)) {} + Device(QDMI_Device device, std::shared_ptr session) + : device_(device), session_(std::move(session)) {} /// Wrap operation handles while retaining their owning device session. [[nodiscard]] std::vector @@ -743,8 +816,8 @@ class Device { if constexpr (string_or_optional_string) { size_t size = 0; - auto result = QDMI_device_query_device_property(device_.get(), prop, 0, - nullptr, &size); + auto result = + session_->api->deviceQueryProperty(device_, prop, 0, nullptr, &size); if constexpr (is_optional) { if (result == QDMI_ERROR_NOTSUPPORTED) { @@ -753,16 +826,16 @@ class Device { } qdmi::throwIfError(result, msg); - std::string value(size - 1, '\0'); - result = QDMI_device_query_device_property(device_.get(), prop, size, - value.data(), nullptr); + std::string value(size, '\0'); + result = session_->api->deviceQueryProperty(device_, prop, size, + value.data(), nullptr); qdmi::throwIfError(result, msg); - return value; + return detail::decodeText(std::move(value), msg); } else if constexpr (maybe_optional_size_constructible_contiguous_range< T>) { size_t size = 0; - auto result = QDMI_device_query_device_property(device_.get(), prop, 0, - nullptr, &size); + auto result = + session_->api->deviceQueryProperty(device_, prop, 0, nullptr, &size); if constexpr (is_optional) { if (result == QDMI_ERROR_NOTSUPPORTED) { @@ -771,16 +844,18 @@ class Device { } qdmi::throwIfError(result, msg); + detail::validateArraySize::value_type>( + size, qdmi::toString(prop)); remove_optional_t value( size / sizeof(typename remove_optional_t::value_type)); - result = QDMI_device_query_device_property(device_.get(), prop, size, - value.data(), nullptr); + result = session_->api->deviceQueryProperty( + device_, prop, size, static_cast(value.data()), nullptr); qdmi::throwIfError(result, msg); return value; } else { remove_optional_t value{}; - const auto result = QDMI_device_query_device_property( - device_.get(), prop, sizeof(remove_optional_t), &value, nullptr); + const auto result = session_->api->deviceQueryProperty( + device_, prop, sizeof(remove_optional_t), &value, nullptr); if constexpr (is_optional) { if (result == QDMI_ERROR_NOTSUPPORTED) { @@ -803,11 +878,12 @@ class Device { const std::optional& custom4, const std::optional& custom5) const; - static void setCustomJobParam(QDMI_Job job, QDMI_Job_Parameter param, - const CustomJobParameter& value); + void setCustomJobParam(QDMI_Job job, QDMI_Job_Parameter param, + const CustomJobParameter& value) const; /// @brief The underlying device pointer. - std::shared_ptr device_; + QDMI_Device device_{}; + std::shared_ptr session_; friend class Session; }; @@ -825,8 +901,7 @@ class Device { class Job { public: Job(Job&&) noexcept = default; - - auto operator=(Job&& other) noexcept -> Job&; + Job& operator=(Job&&) noexcept = default; // NOLINTNEXTLINE(misc-explicit-constructor, *-explicit-conversions) operator QDMI_Job() const { return job_.get(); } @@ -890,8 +965,8 @@ class Job { const auto qdmiProperty = detail::toJobProperty(property); return detail::queryCustomValue( [this, qdmiProperty](const size_t size, void* value, size_t* sizeRet) { - return QDMI_job_query_property(job_.get(), qdmiProperty, size, value, - sizeRet); + return job_.get_deleter().session->api->jobQueryProperty( + job_.get(), qdmiProperty, size, value, sizeRet); }, "custom job property " + std::to_string(static_cast(property))); @@ -911,8 +986,8 @@ class Job { const auto qdmiResult = detail::toJobResult(property); return detail::queryCustomValue( [this, qdmiResult](const size_t size, void* value, size_t* sizeRet) { - return QDMI_job_get_results(job_.get(), qdmiResult, size, value, - sizeRet); + return job_.get_deleter().session->api->jobGetResults( + job_.get(), qdmiResult, size, value, sizeRet); }, "custom job result " + std::to_string(static_cast(property))); } @@ -962,22 +1037,19 @@ class Job { auto operator<=>(const Job&) const noexcept = default; private: + [[nodiscard]] const detail::ClientApi& api() const { + return *job_.get_deleter().session->api; + } + /** * @brief Constructs a Job object from a QDMI_Job handle. * @param job The QDMI_Job handle to wrap. - * @param device The device that owns the job. - */ - explicit Job(QDMI_Job job, std::shared_ptr device) - : device_(std::move(device)), job_(job, QDMI_job_free) {} - - /** - * @brief Ownership of the device session that owns the job. - * @note Declared before `job_` so the job is freed before its device. + * @param session The Client session that owns the handle. */ - std::shared_ptr device_; + Job(QDMI_Job job, std::shared_ptr session) + : job_(job, detail::JobDeleter{std::move(session)}) {} - std::unique_ptr job_{ - nullptr, QDMI_job_free}; + std::unique_ptr job_; friend class Device; }; @@ -1054,8 +1126,8 @@ class Site { const auto qdmiProperty = detail::toSiteProperty(property); return detail::queryCustomValue( [this, qdmiProperty](const size_t size, void* value, size_t* sizeRet) { - return QDMI_device_query_site_property( - device_.get(), site_, qdmiProperty, size, value, sizeRet); + return session_->api->deviceQuerySiteProperty( + device_, site_, qdmiProperty, size, value, sizeRet); }, "custom site property " + std::to_string(static_cast(property))); @@ -1064,21 +1136,25 @@ class Site { auto operator<=>(const Site&) const noexcept = default; private: + [[nodiscard]] const detail::ClientApi& api() const { return *session_->api; } + /** * @brief Constructs a Site object from a QDMI_Site handle. * @param device The QDMI device handle that owns the site. + * @param session The Client session that owns the handle. * @param site The QDMI_Site handle to wrap. */ - Site(std::shared_ptr device, QDMI_Site site) - : device_(std::move(device)), site_(site) {} + Site(QDMI_Device device, std::shared_ptr session, + QDMI_Site site) + : device_(device), session_(std::move(session)), site_(site) {} /// Query a site property. template [[nodiscard]] T queryProperty(const QDMI_Site_Property prop) const { if constexpr (string_or_optional_string) { size_t size = 0; - const auto result = QDMI_device_query_site_property( - device_.get(), site_, prop, 0, nullptr, &size); + const auto result = session_->api->deviceQuerySiteProperty( + device_, site_, prop, 0, nullptr, &size); if constexpr (is_optional) { if (result == QDMI_ERROR_NOTSUPPORTED) { return std::nullopt; @@ -1086,17 +1162,15 @@ class Site { } qdmi::throwIfError(result, std::string("Querying size") + qdmi::toString(prop)); - std::string value(size - 1, '\0'); - qdmi::throwIfError(QDMI_device_query_site_property(device_.get(), site_, - prop, size, - value.data(), nullptr), + std::string value(size, '\0'); + qdmi::throwIfError(session_->api->deviceQuerySiteProperty( + device_, site_, prop, size, value.data(), nullptr), std::string("Querying ") + qdmi::toString(prop)); - return value; + return detail::decodeText(std::move(value), qdmi::toString(prop)); } else { remove_optional_t value{}; - const auto result = QDMI_device_query_site_property( - device_.get(), site_, prop, sizeof(remove_optional_t), &value, - nullptr); + const auto result = session_->api->deviceQuerySiteProperty( + device_, site_, prop, sizeof(remove_optional_t), &value, nullptr); if constexpr (is_optional) { if (result == QDMI_ERROR_NOTSUPPORTED) { return std::nullopt; @@ -1109,7 +1183,8 @@ class Site { } /// @brief The QDMI device handle that owns the site. - std::shared_ptr device_; + QDMI_Device device_{}; + std::shared_ptr session_; /// @brief The underlying QDMI_Site object. QDMI_Site site_; @@ -1221,8 +1296,8 @@ class Operation { return detail::queryCustomValue( [this, qdmiProperty, &qdmiSites, ¶ms](const size_t size, void* value, size_t* sizeRet) { - return QDMI_device_query_operation_property( - device_.get(), operation_, qdmiSites.size(), qdmiSites.data(), + return session_->api->deviceQueryOperationProperty( + device_, operation_, qdmiSites.size(), qdmiSites.data(), params.size(), params.data(), qdmiProperty, size, value, sizeRet); }, "custom operation property " + @@ -1232,14 +1307,17 @@ class Operation { auto operator<=>(const Operation&) const noexcept = default; private: + [[nodiscard]] const detail::ClientApi& api() const { return *session_->api; } + /** * @brief Constructs an Operation object from a QDMI_Operation handle. * @param device The QDMI device handle that owns the operation. + * @param session The Client session that owns the handle. * @param operation The QDMI_Operation handle to wrap. */ - Operation(std::shared_ptr device, + Operation(QDMI_Device device, std::shared_ptr session, QDMI_Operation operation) - : device_(std::move(device)), operation_(operation) {} + : device_(device), session_(std::move(session)), operation_(operation) {} /// Query an operation property. template @@ -1254,46 +1332,47 @@ class Operation { [](const Site& site) -> QDMI_Site { return site; }); if constexpr (string_or_optional_string) { size_t size = 0; - auto result = QDMI_device_query_operation_property( - device_.get(), operation_, sites.size(), qdmiSites.data(), - params.size(), params.data(), prop, 0, nullptr, &size); + auto result = session_->api->deviceQueryOperationProperty( + device_, operation_, sites.size(), qdmiSites.data(), params.size(), + params.data(), prop, 0, nullptr, &size); if constexpr (is_optional) { if (result == QDMI_ERROR_NOTSUPPORTED) { return std::nullopt; } } qdmi::throwIfError(result, msg); - std::string value(size - 1, '\0'); - result = QDMI_device_query_operation_property( - device_.get(), operation_, sites.size(), qdmiSites.data(), - params.size(), params.data(), prop, size, value.data(), nullptr); + std::string value(size, '\0'); + result = session_->api->deviceQueryOperationProperty( + device_, operation_, sites.size(), qdmiSites.data(), params.size(), + params.data(), prop, size, value.data(), nullptr); qdmi::throwIfError(result, msg); - return value; + return detail::decodeText(std::move(value), msg); } else if constexpr (maybe_optional_size_constructible_contiguous_range< T>) { size_t size = 0; - auto result = QDMI_device_query_operation_property( - device_.get(), operation_, sites.size(), qdmiSites.data(), - params.size(), params.data(), prop, 0, nullptr, &size); + auto result = session_->api->deviceQueryOperationProperty( + device_, operation_, sites.size(), qdmiSites.data(), params.size(), + params.data(), prop, 0, nullptr, &size); if constexpr (is_optional) { if (result == QDMI_ERROR_NOTSUPPORTED) { return std::nullopt; } } qdmi::throwIfError(result, msg); + detail::validateArraySize::value_type>( + size, qdmi::toString(prop)); remove_optional_t value( size / sizeof(typename remove_optional_t::value_type)); - result = QDMI_device_query_operation_property( - device_.get(), operation_, sites.size(), qdmiSites.data(), - params.size(), params.data(), prop, size, value.data(), nullptr); + result = session_->api->deviceQueryOperationProperty( + device_, operation_, sites.size(), qdmiSites.data(), params.size(), + params.data(), prop, size, static_cast(value.data()), nullptr); qdmi::throwIfError(result, msg); return value; } else { remove_optional_t value{}; - const auto result = QDMI_device_query_operation_property( - device_.get(), operation_, sites.size(), qdmiSites.data(), - params.size(), params.data(), prop, sizeof(remove_optional_t), - &value, nullptr); + const auto result = session_->api->deviceQueryOperationProperty( + device_, operation_, sites.size(), qdmiSites.data(), params.size(), + params.data(), prop, sizeof(remove_optional_t), &value, nullptr); if constexpr (is_optional) { if (result == QDMI_ERROR_NOTSUPPORTED) { return std::nullopt; @@ -1305,7 +1384,8 @@ class Operation { } /// @brief The QDMI device handle that owns the operation. - std::shared_ptr device_; + QDMI_Device device_{}; + std::shared_ptr session_; /// @brief The underlying QDMI_Operation object. QDMI_Operation operation_; diff --git a/include/mqt-core/qdmi/Slurm.hpp b/include/mqt-core/qdmi/Slurm.hpp index 0dcafaf86a..0dbbc90fa0 100644 --- a/include/mqt-core/qdmi/Slurm.hpp +++ b/include/mqt-core/qdmi/Slurm.hpp @@ -21,11 +21,11 @@ namespace qdmi::slurm { /** * @brief Opens the QDMI device named by the Slurm license environment. - * @return A fresh device session using the registered device definition. + * @return A fresh Client session for the selected device. * @details The @c SLURM_JOB_LICENSES value must contain exactly one local - * license. Its name must equal a registered QDMI device ID. The optional - * license count must be one. The device must report @c QDMI_DEVICE_STATUS_IDLE - * or @c QDMI_DEVICE_STATUS_BUSY. + * license. Its name must equal a stable ID visible to the selected QDMI Driver. + * The optional license count must be one. The device must report + * @c QDMI_DEVICE_STATUS_IDLE or @c QDMI_DEVICE_STATUS_BUSY. * @warning This function uses process-mutable environment data for device * selection. It does not verify a Slurm allocation, authenticate the caller, * or authorize access to the device. The provider or operating system must diff --git a/include/mqt-core/qdmi/common/Common.hpp b/include/mqt-core/qdmi/common/Common.hpp index d68eb8d275..67721a4954 100644 --- a/include/mqt-core/qdmi/common/Common.hpp +++ b/include/mqt-core/qdmi/common/Common.hpp @@ -16,9 +16,41 @@ #include +#include +#include +#include +#include #include +#include namespace qdmi { +namespace detail { +/// Encode a native filesystem path as UTF-8. +[[nodiscard]] inline auto pathToUtf8(const std::filesystem::path& path) + -> std::string { + const auto utf8 = path.u8string(); + std::string value(utf8.size(), '\0'); + if (!utf8.empty()) { + std::memcpy(value.data(), utf8.data(), utf8.size()); + } + return value; +} + +/// Decode a UTF-8 filesystem path into the native representation. +[[nodiscard]] inline auto pathFromUtf8(const std::string_view value) + -> std::filesystem::path { + std::u8string utf8(value.size(), u8'\0'); + if (!value.empty()) { + std::memcpy(utf8.data(), value.data(), value.size()); + } + return {utf8}; +} + +/// Read an environment value as UTF-8. +[[nodiscard]] auto environmentUtf8(std::string_view name) + -> std::optional; +} // namespace detail + template class Singleton { protected: /// @brief Protected constructor to enforce the singleton pattern. @@ -356,6 +388,8 @@ constexpr auto toString(const QDMI_Operation_Property prop) -> const char* { /// Returns the string representation of the given device property @p prop. constexpr auto toString(const QDMI_Device_Property prop) -> const char* { switch (prop) { + case QDMI_DEVICE_PROPERTY_ID: + return "ID"; case QDMI_DEVICE_PROPERTY_NAME: return "NAME"; case QDMI_DEVICE_PROPERTY_VERSION: diff --git a/include/mqt-core/qdmi/driver/Driver.hpp b/include/mqt-core/qdmi/driver/Driver.hpp index 3e3d740bfb..78d1a9580b 100644 --- a/include/mqt-core/qdmi/driver/Driver.hpp +++ b/include/mqt-core/qdmi/driver/Driver.hpp @@ -209,6 +209,8 @@ enum class SessionStatus : uint8_t { */ struct QDMI_Device_impl_d { private: + /// Stable ID assigned by the Client driver. + std::string id_; /** * @brief The device library that provides the device interface functions. * @note This must be a pointer type as we need access to dynamic and static @@ -235,8 +237,10 @@ struct QDMI_Device_impl_d { * @param config is the configuration for device session parameters. */ explicit QDMI_Device_impl_d(std::unique_ptr&& lib, - const qdmi::DeviceSessionConfig& config = {}) - : QDMI_Device_impl_d(std::shared_ptr(std::move(lib)), config) {} + const qdmi::DeviceSessionConfig& config = {}, + std::string id = {}) + : QDMI_Device_impl_d(std::shared_ptr(std::move(lib)), config, + std::move(id)) {} /** * @brief Constructor for the QDMI device. @@ -245,10 +249,12 @@ struct QDMI_Device_impl_d { * @param lib is a shared pointer to the device library that provides the * device interface functions. * @param config is the configuration for device session parameters. + * @param id Stable Client-visible device ID. * @param childDevice optionally selects a child device for this wrapper. */ explicit QDMI_Device_impl_d(std::shared_ptr lib, const qdmi::DeviceSessionConfig& config = {}, + std::string id = {}, QDMI_Child_Device childDevice = nullptr); /** diff --git a/mlir/include/mlir/Compiler/QDMIAdapter.h b/mlir/include/mlir/Compiler/QDMIAdapter.h index ab3577e6b0..01e9d1223d 100644 --- a/mlir/include/mlir/Compiler/QDMIAdapter.h +++ b/mlir/include/mlir/Compiler/QDMIAdapter.h @@ -38,7 +38,8 @@ namespace mlir { compilerTargetFromDevice(const qdmi::Device& device); /** - * @brief Open a registered QDMI device and snapshot it as a compiler target. + * @brief Open a Client-visible QDMI device and snapshot it as a compiler + * target. * * @details This adapter contains exceptions from the QDMI C++ API and returns * them as LLVM errors. The returned target owns all queried metadata. @@ -47,9 +48,9 @@ compilerTargetFromDevice(const qdmi::Device& device); compilerTargetFromDeviceId(std::string_view deviceId); /** - * @brief List the stable IDs of registered QDMI devices. + * @brief List the stable IDs visible to a fresh QDMI Client session. * - * @details This adapter contains exceptions from QDMI registry discovery and + * @details This adapter contains exceptions from QDMI Client discovery and * returns them as LLVM errors. */ [[nodiscard]] llvm::Expected> diff --git a/mlir/lib/Compiler/QDMIAdapter.cpp b/mlir/lib/Compiler/QDMIAdapter.cpp index 98f495c135..b654dc07c8 100644 --- a/mlir/lib/Compiler/QDMIAdapter.cpp +++ b/mlir/lib/Compiler/QDMIAdapter.cpp @@ -12,7 +12,6 @@ #include "mlir/Compiler/Target.h" #include "qdmi/Client.hpp" -#include "qdmi/driver/Driver.hpp" #include #include @@ -510,9 +509,16 @@ compilerTargetFromDeviceId(const std::string_view deviceId) { llvm::Expected> registeredQDMIDeviceIds() { try { - return qdmi::Driver::get().registeredDeviceIds(); + auto session = qdmi::Session{}; + auto devices = session.getDevices(); + std::vector ids; + ids.reserve(devices.size()); + std::ranges::transform( + devices, std::back_inserter(ids), + [](const qdmi::Device& device) { return device.getId(); }); + return ids; } catch (...) { - return qdmiError("Failed to discover registered QDMI devices", + return qdmiError("Failed to discover QDMI devices", std::current_exception()); } } diff --git a/mlir/unittests/Compiler/CMakeLists.txt b/mlir/unittests/Compiler/CMakeLists.txt index 398f6489b3..9509162639 100644 --- a/mlir/unittests/Compiler/CMakeLists.txt +++ b/mlir/unittests/Compiler/CMakeLists.txt @@ -28,14 +28,15 @@ target_link_libraries( mqt_copy_qdmi_runtime(mqt-core-mlir-unittests-compiler MQT::CoreQDMIScDevice MQT::CoreQDMI_DDSIM_Device) -target_compile_definitions( - mqt-core-mlir-unittests-compiler - PRIVATE - MQT_CORE_MLIR_HETEROGENEOUS_SC_CONFIG="${CMAKE_CURRENT_SOURCE_DIR}/Inputs/heterogeneous-sc.json" - MQT_CORE_MLIR_HIGHER_ARITY_SC_CONFIG="${CMAKE_CURRENT_SOURCE_DIR}/Inputs/higher-arity-sc.json" - MQT_CORE_MLIR_DIRECTIONAL_ONE_WAY_SC_CONFIG="${CMAKE_CURRENT_SOURCE_DIR}/Inputs/directional-one-way-sc.json" - MQT_CORE_MLIR_DIRECTIONAL_TWO_WAY_SC_CONFIG="${CMAKE_CURRENT_SOURCE_DIR}/Inputs/directional-two-way-sc.json" +set(qdmi_test_config "${CMAKE_CURRENT_BINARY_DIR}/$/qdmi-adapter-devices.json") +file( + GENERATE + OUTPUT "${qdmi_test_config}" + CONTENT + "{\n \"schema-version\": 1,\n \"qdmi\": {\n \"devices\": [\n {\"id\": \"test.mlir.heterogeneous\", \"library\": \"$\", \"prefix\": \"MQT_SC\", \"session\": {\"device-config\": {\"file\": \"${CMAKE_CURRENT_SOURCE_DIR}/Inputs/heterogeneous-sc.json\"}}},\n {\"id\": \"test.mlir.higher-arity\", \"library\": \"$\", \"prefix\": \"MQT_SC\", \"session\": {\"device-config\": {\"file\": \"${CMAKE_CURRENT_SOURCE_DIR}/Inputs/higher-arity-sc.json\"}}},\n {\"id\": \"test.mlir.directional-one-way\", \"library\": \"$\", \"prefix\": \"MQT_SC\", \"session\": {\"device-config\": {\"file\": \"${CMAKE_CURRENT_SOURCE_DIR}/Inputs/directional-one-way-sc.json\"}}},\n {\"id\": \"test.mlir.directional-two-way\", \"library\": \"$\", \"prefix\": \"MQT_SC\", \"session\": {\"device-config\": {\"file\": \"${CMAKE_CURRENT_SOURCE_DIR}/Inputs/directional-two-way-sc.json\"}}},\n {\"id\": \"test.mlir.unavailable-operation\", \"library\": \"$\", \"prefix\": \"MQT_SC\", \"session\": {\"device-config\": {\"file\": \"${CMAKE_CURRENT_SOURCE_DIR}/Inputs/unavailable-operation-sc.json\"}}}\n ]\n }\n}\n" ) +target_compile_definitions(mqt-core-mlir-unittests-compiler + PRIVATE MQT_CORE_MLIR_QDMI_TEST_CONFIG="${qdmi_test_config}") mqt_mlir_configure_unittest_target(mqt-core-mlir-unittests-compiler REQUIRES_EH) diff --git a/mlir/unittests/Compiler/Inputs/unavailable-operation-sc.json b/mlir/unittests/Compiler/Inputs/unavailable-operation-sc.json new file mode 100644 index 0000000000..4d02159291 --- /dev/null +++ b/mlir/unittests/Compiler/Inputs/unavailable-operation-sc.json @@ -0,0 +1,22 @@ +{ + "schema-version": 1, + "name": "Unavailable operation", + "numQubits": 1, + "durationUnit": { + "unit": "ns", + "scaleFactor": 1 + }, + "qubitProperties": { + "defaults": {}, + "overrides": [] + }, + "couplings": [], + "operations": [ + { + "name": "x", + "numQubits": 1, + "numParameters": 0, + "sites": [] + } + ] +} diff --git a/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp b/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp index 73ea2af25f..75082e2f18 100644 --- a/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp +++ b/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp @@ -11,7 +11,6 @@ #include "mlir/Compiler/QDMIAdapter.h" #include "mlir/Compiler/Target.h" #include "qdmi/Client.hpp" -#include "qdmi/driver/Driver.hpp" #include #include @@ -20,6 +19,7 @@ #include #include +#include #include #include #include @@ -28,6 +28,26 @@ using mlir::CompilerTarget; +namespace { +struct ConfiguredClientEnvironment { + ConfiguredClientEnvironment() noexcept { +#ifdef _WIN32 + if (_putenv_s("MQT_CORE_QDMI_CONFIG_FILE", + MQT_CORE_MLIR_QDMI_TEST_CONFIG) != 0) { + std::abort(); + } +#else + if (setenv("MQT_CORE_QDMI_CONFIG_FILE", MQT_CORE_MLIR_QDMI_TEST_CONFIG, + 1) != 0) { + std::abort(); + } +#endif + } +}; + +const ConfiguredClientEnvironment CONFIGURED_CLIENT_ENVIRONMENT; +} // namespace + [[nodiscard]] static const CompilerTarget::Operation& findOperation(const CompilerTarget& target, const llvm::StringRef name) { const auto* const found = @@ -144,14 +164,10 @@ TEST(CompilerQDMIAdapterTest, ConvertsUnknownDeviceFailureToError) { ASSERT_FALSE(target); const auto message = llvm::toString(target.takeError()); EXPECT_NE(message.find("mqt.unknown.device"), std::string::npos); - EXPECT_NE(message.find("Unknown QDMI device ID"), std::string::npos); } TEST(CompilerQDMIAdapterTest, RejectsNonhomogeneousOperationSupport) { - qdmi::DeviceSessionConfig overrides; - overrides.deviceConfiguration = - qdmi::FileDeviceConfiguration{MQT_CORE_MLIR_HETEROGENEOUS_SC_CONFIG}; - const auto device = qdmi::Session::openDevice("mqt.sc.default", overrides); + const auto device = qdmi::Session::openDevice("test.mlir.heterogeneous"); auto target = mlir::compilerTargetFromDevice(device); ASSERT_FALSE(target); const auto message = llvm::toString(target.takeError()); @@ -160,10 +176,7 @@ TEST(CompilerQDMIAdapterTest, RejectsNonhomogeneousOperationSupport) { } TEST(CompilerQDMIAdapterTest, SnapshotsHomogeneousHigherArityOperation) { - qdmi::DeviceSessionConfig overrides; - overrides.deviceConfiguration = - qdmi::FileDeviceConfiguration{MQT_CORE_MLIR_HIGHER_ARITY_SC_CONFIG}; - const auto device = qdmi::Session::openDevice("mqt.sc.default", overrides); + const auto device = qdmi::Session::openDevice("test.mlir.higher-arity"); const auto target = llvm::cantFail(mlir::compilerTargetFromDevice(device)); EXPECT_TRUE(target.supportsOperation("ccnot", 3, 0)); @@ -173,11 +186,8 @@ TEST(CompilerQDMIAdapterTest, SnapshotsHomogeneousHigherArityOperation) { } TEST(CompilerQDMIAdapterTest, PreservesOneWayDirectionalOperationSupport) { - qdmi::DeviceSessionConfig overrides; - overrides.deviceConfiguration = qdmi::FileDeviceConfiguration{ - MQT_CORE_MLIR_DIRECTIONAL_ONE_WAY_SC_CONFIG, - }; - const auto device = qdmi::Session::openDevice("mqt.sc.default", overrides); + const auto device = + qdmi::Session::openDevice("test.mlir.directional-one-way"); const auto target = llvm::cantFail(mlir::compilerTargetFromDevice(device)); ASSERT_EQ(target.couplings().size(), 1U); @@ -194,21 +204,8 @@ TEST(CompilerQDMIAdapterTest, PreservesOneWayDirectionalOperationSupport) { } TEST(CompilerQDMIAdapterTest, OmitsOperationsWithNoSupportedPlacements) { - qdmi::DeviceSessionConfig overrides; - overrides.deviceConfiguration = qdmi::InlineDeviceConfiguration{ - .json = R"({ - "schema-version": 1, - "name": "Unavailable operation", - "numQubits": 1, - "durationUnit": {"unit": "ns", "scaleFactor": 1}, - "qubitProperties": {"defaults": {}, "overrides": []}, - "couplings": [], - "operations": [ - {"name": "x", "numQubits": 1, "numParameters": 0, "sites": []} - ] - })", - }; - const auto device = qdmi::Session::openDevice("mqt.sc.default", overrides); + const auto device = + qdmi::Session::openDevice("test.mlir.unavailable-operation"); const auto target = llvm::cantFail(mlir::compilerTargetFromDevice(device)); EXPECT_EQ(target.nativeOperationsKind(), CompilerTarget::NativeOperations::Kind::Explicit); @@ -218,11 +215,8 @@ TEST(CompilerQDMIAdapterTest, OmitsOperationsWithNoSupportedPlacements) { TEST(CompilerQDMIAdapterTest, PreservesDirectionalCalibrationWhenBothOrientationsExist) { - qdmi::DeviceSessionConfig overrides; - overrides.deviceConfiguration = qdmi::FileDeviceConfiguration{ - MQT_CORE_MLIR_DIRECTIONAL_TWO_WAY_SC_CONFIG, - }; - const auto device = qdmi::Session::openDevice("mqt.sc.default", overrides); + const auto device = + qdmi::Session::openDevice("test.mlir.directional-two-way"); const auto target = llvm::cantFail(mlir::compilerTargetFromDevice(device)); ASSERT_EQ(target.couplings().size(), 1); diff --git a/python/mqt/core/mlir.pyi b/python/mqt/core/mlir.pyi index 7487b315ff..563aa06b50 100644 --- a/python/mqt/core/mlir.pyi +++ b/python/mqt/core/mlir.pyi @@ -343,7 +343,7 @@ class CompilerTarget: @staticmethod def from_device_id(device_id: str, **session_parameters: Unpack[QDMISessionParameters]) -> CompilerTarget: - """Open a registered device and snapshot its compiler target.""" + """Open a Client-visible device and snapshot its compiler target.""" @property def name(self) -> str | None: diff --git a/python/mqt/core/plugins/pennylane/device.py b/python/mqt/core/plugins/pennylane/device.py index e25c17d252..db71fafdba 100644 --- a/python/mqt/core/plugins/pennylane/device.py +++ b/python/mqt/core/plugins/pennylane/device.py @@ -30,8 +30,7 @@ from mqt.core.qdmi import Device as QDMIDeviceHandle from mqt.core.qdmi import Job as QDMIJobHandle -from mqt.core.qdmi import ProgramFormat -from mqt.core.qdmi.driver import open_device +from mqt.core.qdmi import ProgramFormat, open_device from .converter import _ConvertedProgram, _ProgramConverter from .exceptions import ( @@ -61,14 +60,13 @@ __all__ = ["DDSIMDevice", "QDMIDevice"] _SESSION_PARAMETERS = frozenset({ - "base_url", + "driver_path", "token", "auth_file", "auth_url", "username", "password", - "device_config", - "device_config_file", + "project_id", "custom1", "custom2", "custom3", @@ -111,14 +109,14 @@ class QDMIDevice(Device): """Execute PennyLane programs on a gate-based QDMI device. Args: - device_id: Stable ID from the QDMI device registry. Use either this + device_id: Stable ID reported by the QDMI Driver. Use either this argument or ``device``. wires: PennyLane wire labels or number of wires. By default all QDMI qubits are exposed as consecutive integer wires. shots: Finite default shot configuration. device: An already-open QDMI device. Use this for a session selected by an integration such as Slurm. - session_parameters: QDMI device-session keyword arguments. + session_parameters: QDMI Client-session keyword arguments. job_parameters: QDMI custom job keyword arguments. """ diff --git a/python/mqt/core/plugins/qiskit/backend.py b/python/mqt/core/plugins/qiskit/backend.py index 6b98b5e2e2..bc9e0206a9 100644 --- a/python/mqt/core/plugins/qiskit/backend.py +++ b/python/mqt/core/plugins/qiskit/backend.py @@ -31,8 +31,7 @@ from ...qdmi import Device as QDMIDevice from ...qdmi import Job as QDMIJobHandle -from ...qdmi import ProgramFormat, is_binary_program_format -from ...qdmi.driver import open_device +from ...qdmi import ProgramFormat, is_binary_program_format, open_device from .exceptions import ( CircuitValidationError, JobSubmissionError, @@ -227,9 +226,9 @@ class QDMIBackend(BackendV2): It automatically introspects device capabilities and constructs a :class:`~qiskit.transpiler.Target` object with supported operations. - Use :meth:`from_device_id` to open one registered device. Use + Use :meth:`from_device_id` to open one Client-visible device. Use :class:`~mqt.core.plugins.qiskit.provider.QDMIProvider` to enumerate - registered devices. + Client-visible devices. Args: device: QDMI device wrapper. @@ -307,7 +306,7 @@ def __init__( Args: device: QDMI device wrapper. provider: Provider instance that created this backend. - device_id: Stable registry ID for the opened device, if known. + device_id: Stable ID for the opened device, if known. Raises: UnsupportedDeviceError: If the device cannot be represented in Qiskit's Target model. @@ -331,12 +330,12 @@ def from_device_id( provider: QDMIProvider | None = None, **session_parameters: Unpack[QDMISessionParameters], ) -> QDMIBackend: - """Open a registered QDMI device and adapt it for Qiskit. + """Open a Client-visible QDMI device and adapt it for Qiskit. Args: - device_id: Stable ID from the QDMI device registry. + device_id: Stable ID reported by the QDMI Driver. provider: Provider to associate with the backend. - session_parameters: Optional overrides for this device session. + session_parameters: Optional parameters for this Client session. Returns: A Qiskit backend for a fresh QDMI device session. diff --git a/python/mqt/core/plugins/qiskit/provider.py b/python/mqt/core/plugins/qiskit/provider.py index 2ee8de3c49..d155d13fe6 100644 --- a/python/mqt/core/plugins/qiskit/provider.py +++ b/python/mqt/core/plugins/qiskit/provider.py @@ -17,7 +17,7 @@ import warnings from typing import TYPE_CHECKING -from ...qdmi.driver import registered_device_ids +from ...qdmi import ClientSession from .backend import QDMIBackend from .exceptions import UnsupportedDeviceError @@ -35,9 +35,9 @@ def __dir__() -> list[str]: class QDMIProvider: - """Provider for registered QDMI devices. + """Provider for Client-visible QDMI devices. - This provider discovers registered QDMI devices lazily and adapts + This provider discovers QDMI devices lazily and adapts Qiskit-compatible devices as backends. Examples: @@ -58,8 +58,8 @@ class QDMIProvider: @staticmethod def device_ids() -> list[str]: - """Return the current registered QDMI device IDs without opening them.""" - return registered_device_ids() + """Return the devices visible to a fresh QDMI Client session.""" + return [device.id for device in ClientSession().devices] def backends(self, name: str | None = None) -> list[QDMIBackend]: """Return all available backends, optionally filtered by name substring. @@ -84,13 +84,13 @@ def backends(self, name: str | None = None) -> list[QDMIBackend]: return list(self._iter_backends(name)) def _iter_backends(self, name: str | None = None) -> Iterator[QDMIBackend]: - """Open available registered backends one at a time. + """Open available backends one at a time. Args: name: If provided, yield only backends whose name contains this substring. Yields: - Each backend whose registered device can be opened. + Each backend whose device can be opened. """ for device_id in self.device_ids(): try: @@ -99,7 +99,7 @@ def _iter_backends(self, name: str | None = None) -> Iterator[QDMIBackend]: continue except (IndexError, RuntimeError, ValueError): warnings.warn( - f"Could not open registered QDMI device '{device_id}'.", + f"Could not open QDMI device '{device_id}'.", RuntimeWarning, stacklevel=3, ) diff --git a/python/mqt/core/qdmi/__init__.pyi b/python/mqt/core/qdmi/__init__.pyi index 04ad90bf02..2e5837d12c 100644 --- a/python/mqt/core/qdmi/__init__.pyi +++ b/python/mqt/core/qdmi/__init__.pyi @@ -6,15 +6,56 @@ # # Licensed under the MIT License -"""QDMI entities and access to MQT Core's QDMI driver.""" +"""QDMI Client entities.""" import enum +import os from collections.abc import Sequence from typing import overload -from mqt.core.qdmi import driver as driver from mqt.core.qdmi import slurm as slurm +class ClientSession: + """One initialized QDMI Client session.""" + + def __init__( + self, + *, + driver_path: str | os.PathLike | None = None, + token: str | None = None, + auth_file: str | os.PathLike | None = None, + auth_url: str | None = None, + username: str | None = None, + password: str | None = None, + project_id: str | None = None, + custom1: str | None = None, + custom2: str | None = None, + custom3: str | None = None, + custom4: str | None = None, + custom5: str | None = None, + ) -> None: ... + @property + def devices(self) -> list[Device]: + """The devices visible to this authenticated session.""" + +def open_device( + device_id: str, + *, + driver_path: str | os.PathLike | None = None, + token: str | None = None, + auth_file: str | os.PathLike | None = None, + auth_url: str | None = None, + username: str | None = None, + password: str | None = None, + project_id: str | None = None, + custom1: str | None = None, + custom2: str | None = None, + custom3: str | None = None, + custom4: str | None = None, + custom5: str | None = None, +) -> Device: + """Open a Client-visible device by stable ID in a fresh session.""" + class Job: """A job represents a submitted quantum program execution.""" @@ -220,6 +261,10 @@ class Device: def name(self) -> str: """Returns the name of the device.""" + @property + def id(self) -> str: + """The stable Client-visible device ID.""" + def version(self) -> str: """Returns the version of the device.""" diff --git a/python/mqt/core/qdmi/driver.pyi b/python/mqt/core/qdmi/driver.pyi deleted file mode 100644 index 8e23d0c900..0000000000 --- a/python/mqt/core/qdmi/driver.pyi +++ /dev/null @@ -1,151 +0,0 @@ -# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM -# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH -# All rights reserved. -# -# SPDX-License-Identifier: MIT -# -# Licensed under the MIT License - -"""Register, discover, and open QDMI devices through MQT Core.""" - -import os -import pathlib - -import mqt.core.qdmi - -class DeviceDefinition: - """A stable QDMI device registration that can be stored before loading.""" - - def __init__( - self, - device_id: str, - library_path: str | os.PathLike, - prefix: str, - *, - base_url: str | None = None, - token: str | None = None, - auth_file: str | os.PathLike | None = None, - auth_url: str | None = None, - username: str | None = None, - password: str | None = None, - device_config: str | None = None, - device_config_file: str | os.PathLike | None = None, - custom1: str | None = None, - custom2: str | None = None, - custom3: str | None = None, - custom4: str | None = None, - custom5: str | None = None, - ) -> None: - """Create a device definition without loading its native library. - - Args: - device_id: Stable identifier used by :func:`open_device`. - library_path: Path to the shared QDMI device library. - prefix: Function prefix used by the library (for example, ``MY_DEVICE``). - base_url: Optional base URL for the device API endpoint. - token: Optional authentication token. - auth_file: Optional path to an authentication file. - auth_url: Optional authentication server URL. - username: Optional authentication username. - password: Optional authentication password. - device_config: Optional inline JSON device description. - device_config_file: Optional device-description JSON file. - custom1: Optional custom configuration parameter 1. - custom2: Optional custom configuration parameter 2. - custom3: Optional custom configuration parameter 3. - custom4: Optional custom configuration parameter 4. - custom5: Optional custom configuration parameter 5. - """ - - @property - def device_id(self) -> str: - """Stable identifier used to open the device.""" - - @property - def library_path(self) -> pathlib.Path: - """Path to the native QDMI device library.""" - - @property - def prefix(self) -> str: - """Prefix used for the QDMI device interface functions.""" - -def register_device(definition: DeviceDefinition, *, replace: bool = False) -> None: - """Register a QDMI device definition without loading its library. - - Args: - definition: Definition to validate and store. - replace: Replace an existing definition if it has not been opened. - - Raises: - ValueError: If the definition is invalid or its ID is already registered. - RuntimeError: If replacing an already opened ID. - """ - -def register_device_if_absent(definition: DeviceDefinition) -> bool: - """Register a valid QDMI device definition if its ID is absent. - - Existing and explicitly disabled IDs are not inserted. Invalid definitions - still raise. - - Args: - definition: Definition to validate and store. - - Returns: - bool: Whether the definition was inserted. - - Raises: - ValueError: If the definition is invalid. - """ - -def registered_device_ids() -> list[str]: - """Return registered, enabled QDMI device IDs in registration order. - - This includes devices registered at runtime and does not load native device - libraries or expose their definitions. - """ - -def open_device( - device_id: str, - *, - base_url: str | None = None, - token: str | None = None, - auth_file: str | os.PathLike | None = None, - auth_url: str | None = None, - username: str | None = None, - password: str | None = None, - device_config: str | None = None, - device_config_file: str | os.PathLike | None = None, - custom1: str | None = None, - custom2: str | None = None, - custom3: str | None = None, - custom4: str | None = None, - custom5: str | None = None, -) -> mqt.core.qdmi.Device: - """Open a registered QDMI device by stable ID. - - Every call creates a fresh device session while keeping the stable registration - unchanged. Opening the device loads trusted native device code. - - Args: - device_id: Stable ID of a registered device. - base_url: Optional base URL override for the device API endpoint. - token: Optional authentication token override. - auth_file: Optional authentication-file override. - auth_url: Optional authentication server URL override. - username: Optional authentication username override. - password: Optional authentication password override. - device_config: Optional inline JSON device-description override. - device_config_file: Optional device-description JSON file override. - custom1: Optional custom configuration parameter 1 override. - custom2: Optional custom configuration parameter 2 override. - custom3: Optional custom configuration parameter 3 override. - custom4: Optional custom configuration parameter 4 override. - custom5: Optional custom configuration parameter 5 override. - - Returns: - mqt.core.qdmi.Device: The opened device, ready for direct backend construction. - - Raises: - IndexError: If the ID is not registered. - RuntimeError: If the device library cannot be loaded or initialized. - """ diff --git a/python/mqt/core/qdmi/slurm.pyi b/python/mqt/core/qdmi/slurm.pyi index 98ce6d6907..4c15545394 100644 --- a/python/mqt/core/qdmi/slurm.pyi +++ b/python/mqt/core/qdmi/slurm.pyi @@ -13,11 +13,10 @@ import mqt.core.qdmi def open_device_from_license() -> mqt.core.qdmi.Device: """Open the QDMI device named by the Slurm license environment. - ``SLURM_JOB_LICENSES`` must contain one local license whose name equals a - registered QDMI device ID. The optional count must be one. The function opens a - fresh device session from the persistent definition and accepts device status - ``IDLE`` or ``BUSY``. It does not apply job-specific QDMI configuration or - credentials. + ``SLURM_JOB_LICENSES`` must contain one local license whose name equals a stable + ID visible to the selected QDMI Driver. The optional count must be one. The + function opens a fresh Client session and accepts device status ``IDLE`` or + ``BUSY``. It does not apply job-specific QDMI configuration or credentials. Warning: ``SLURM_JOB_LICENSES`` is process-mutable. This function uses it only for diff --git a/python/mqt/core/typing.py b/python/mqt/core/typing.py index 26b7f3868e..3deeb5e2e2 100644 --- a/python/mqt/core/typing.py +++ b/python/mqt/core/typing.py @@ -21,14 +21,13 @@ def __dir__() -> list[str]: class QDMISessionParameters(TypedDict, total=False): """Keyword arguments accepted when opening a QDMI device session.""" - base_url: str | None + driver_path: str | os.PathLike[str] | None token: str | None auth_file: str | os.PathLike[str] | None auth_url: str | None username: str | None password: str | None - device_config: str | None - device_config_file: str | os.PathLike[str] | None + project_id: str | None custom1: str | None custom2: str | None custom3: str | None diff --git a/src/qdmi/CMakeLists.txt b/src/qdmi/CMakeLists.txt index aac16d4f52..835f4b9e94 100644 --- a/src/qdmi/CMakeLists.txt +++ b/src/qdmi/CMakeLists.txt @@ -13,7 +13,7 @@ add_subdirectory(driver) set(TARGET_NAME ${MQT_CORE_TARGET_NAME}-qdmi) if(NOT TARGET ${TARGET_NAME}) - add_mqt_core_library(${TARGET_NAME} ALIAS_NAME QDMI) + add_mqt_core_library(${TARGET_NAME} ALIAS_NAME QDMI FORCE_SHARED) target_sources(${TARGET_NAME} PRIVATE Client.cpp Slurm.cpp) @@ -27,7 +27,15 @@ if(NOT TARGET ${TARGET_NAME}) ${MQT_CORE_INCLUDE_BUILD_DIR}/qdmi/Client.hpp ${MQT_CORE_INCLUDE_BUILD_DIR}/qdmi/Slurm.hpp) - target_link_libraries(${TARGET_NAME} PUBLIC qdmi::qdmi MQT::CoreQDMICommon MQT::CoreQDMIDriver) + target_link_libraries( + ${TARGET_NAME} + PUBLIC qdmi::qdmi MQT::CoreQDMICommon + PRIVATE ${CMAKE_DL_LIBS}) + + target_compile_definitions( + ${TARGET_NAME} + PRIVATE "MQT_CORE_QDMI_DEFAULT_DRIVER_FILENAME=\"$\"") + add_dependencies(${TARGET_NAME} ${MQT_CORE_TARGET_NAME}-qdmi-driver) list(APPEND MQT_CORE_TARGETS ${TARGET_NAME}) endif() diff --git a/src/qdmi/Client.cpp b/src/qdmi/Client.cpp index f960114667..4e0f570b9b 100644 --- a/src/qdmi/Client.cpp +++ b/src/qdmi/Client.cpp @@ -12,7 +12,6 @@ #include "qdmi/common/Common.hpp" #include "qdmi/common/Diagnostics.hpp" -#include "qdmi/driver/Driver.hpp" #include @@ -24,18 +23,33 @@ #include #include #include +#include #include -#include #include #include #include #include #include +#include #include #include #include #include +#ifdef _WIN32 +#include +#else +#include +#ifdef __APPLE__ +#include +#endif +#endif + +#ifndef MQT_CORE_QDMI_DEFAULT_DRIVER_FILENAME +#error \ + "MQT_CORE_QDMI_DEFAULT_DRIVER_FILENAME must name the packaged Client driver" +#endif + namespace qdmi { namespace { /// Rejects the formats that `submitJob` cannot carry. @@ -54,8 +68,310 @@ void rejectUnsupportedProgramFormat(const QDMI_Program_Format format) { "trigger a calibration run"); } } + +#ifdef _WIN32 +using LibraryHandle = HMODULE; + +[[nodiscard]] auto thisModuleDirectory() -> std::filesystem::path { + HMODULE module = nullptr; + if (GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | + GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast(&thisModuleDirectory), + &module) == 0) { + throw std::runtime_error("Cannot locate the MQT Core QDMI module"); + } + std::wstring buffer(MAX_PATH, L'\0'); + while (true) { + const auto size = GetModuleFileNameW(module, buffer.data(), + static_cast(buffer.size())); + if (size == 0) { + throw std::runtime_error("Cannot locate the MQT Core QDMI module"); + } + if (size < buffer.size()) { + buffer.resize(size); + return std::filesystem::path(buffer).parent_path(); + } + buffer.resize(buffer.size() * 2U); + } +} + +[[nodiscard]] auto openLibrary(const std::filesystem::path& path) + -> LibraryHandle { + return LoadLibraryExW(path.wstring().c_str(), nullptr, + LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | + LOAD_LIBRARY_SEARCH_DEFAULT_DIRS); +} + +[[nodiscard]] auto findSymbol(LibraryHandle library, const char* name) + -> void* { + return reinterpret_cast(GetProcAddress(library, name)); +} + +void closeLibrary(LibraryHandle library) { FreeLibrary(library); } +#else +using LibraryHandle = void*; + +[[nodiscard]] auto thisModuleDirectory() -> std::filesystem::path { + Dl_info info{}; + if (dladdr(reinterpret_cast(&thisModuleDirectory), &info) == 0 || + info.dli_fname == nullptr) { + throw std::runtime_error("Cannot locate the MQT Core QDMI module"); + } + auto path = std::filesystem::path(info.dli_fname); + if (!path.is_absolute()) { +#ifdef __linux__ + std::error_code error; + path = std::filesystem::read_symlink("/proc/self/exe", error); + if (error) { + throw std::runtime_error("Cannot locate the MQT Core QDMI executable"); + } +#elif defined(__APPLE__) + uint32_t size = 0; + static_cast(_NSGetExecutablePath(nullptr, &size)); + std::vector buffer(size); + if (_NSGetExecutablePath(buffer.data(), &size) != 0) { + throw std::runtime_error("Cannot locate the MQT Core QDMI executable"); + } + path = buffer.data(); +#else + throw std::runtime_error("Cannot resolve the MQT Core QDMI module path"); +#endif + } + return std::filesystem::weakly_canonical(path).parent_path(); +} + +[[nodiscard]] auto openLibrary(const std::filesystem::path& path) + -> LibraryHandle { + return dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL); +} + +[[nodiscard]] auto findSymbol(LibraryHandle library, const char* name) + -> void* { + return dlsym(library, name); +} + +void closeLibrary(LibraryHandle library) { dlclose(library); } +#endif + +[[nodiscard]] auto normalizePath(const std::filesystem::path& path) + -> std::filesystem::path { + if (path.empty()) { + throw std::invalid_argument("QDMI Client driver path must not be empty"); + } + std::error_code error; + auto normalized = std::filesystem::weakly_canonical( + std::filesystem::absolute(path, error), error); + if (error) { + normalized = std::filesystem::absolute(path).lexically_normal(); + } + return normalized; +} + +[[nodiscard]] auto packagedDriverPath() -> std::filesystem::path { + const auto directory = thisModuleDirectory(); + auto filename = std::filesystem::path{MQT_CORE_QDMI_DEFAULT_DRIVER_FILENAME}; + for (const auto& candidate : { + directory / filename, + directory / "lib" / filename, + directory / "bin" / filename, + directory.parent_path() / "lib" / filename, + directory.parent_path() / "bin" / filename, + }) { + if (std::filesystem::exists(candidate)) { + return candidate; + } + } +#ifdef _WIN32 + return directory / filename; +#else + return filename; +#endif +} + +[[nodiscard]] auto requestedDriverPath(const SessionConfig& config) + -> std::filesystem::path { + if (config.driverPath) { + return normalizePath(*config.driverPath); + } + if (const auto environment = + detail::environmentUtf8("MQT_CORE_QDMI_DRIVER")) { + return normalizePath(detail::pathFromUtf8(*environment)); + } + const auto packaged = packagedDriverPath(); + return packaged.has_parent_path() ? normalizePath(packaged) : packaged; +} + +struct LoadedClient { + LibraryHandle library{}; + std::shared_ptr api; + + LoadedClient(LibraryHandle selectedLibrary, + std::shared_ptr selectedApi) + : library(selectedLibrary), api(std::move(selectedApi)) {} + + ~LoadedClient() { + if (library != nullptr) { + closeLibrary(library); + } + } + + LoadedClient(const LoadedClient&) = delete; + LoadedClient& operator=(const LoadedClient&) = delete; + LoadedClient(LoadedClient&& other) noexcept + : library(std::exchange(other.library, nullptr)), + api(std::move(other.api)) {} +}; + +template +[[nodiscard]] auto loadSymbol(LibraryHandle library, const char* name) + -> Function { + /// NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + const auto function = reinterpret_cast(findSymbol(library, name)); + if (function == nullptr) { + throw std::runtime_error("QDMI Client driver is missing symbol " + + std::string(name)); + } + return function; +} + +[[nodiscard]] auto loadClient(const std::filesystem::path& path) + -> LoadedClient { + auto* const library = openLibrary(path); + if (library == nullptr) { + throw std::runtime_error("Cannot load QDMI Client driver '" + + detail::pathToUtf8(path) + "'"); + } + + try { + auto api = std::make_shared(); + api->driverGetClientAbiVersion = + loadSymboldriverGetClientAbiVersion)>( + library, "QDMI_driver_get_client_abi_version"); + const auto actualAbi = api->driverGetClientAbiVersion(); + if (QDMI_VERSION_MAJOR(actualAbi) != + QDMI_VERSION_MAJOR(QDMI_CLIENT_ABI_VERSION) || + QDMI_VERSION_MINOR(actualAbi) != + QDMI_VERSION_MINOR(QDMI_CLIENT_ABI_VERSION)) { + throw std::runtime_error("QDMI Client driver has incompatible ABI " + + std::to_string(QDMI_VERSION_MAJOR(actualAbi)) + + "." + + std::to_string(QDMI_VERSION_MINOR(actualAbi))); + } + +#define LOAD_CLIENT_SYMBOL(field, symbol) \ + api->field = loadSymbolfield)>(library, #symbol) + LOAD_CLIENT_SYMBOL(sessionAlloc, QDMI_session_alloc); + LOAD_CLIENT_SYMBOL(sessionInit, QDMI_session_init); + LOAD_CLIENT_SYMBOL(sessionFree, QDMI_session_free); + LOAD_CLIENT_SYMBOL(sessionSetParameter, QDMI_session_set_parameter); + LOAD_CLIENT_SYMBOL(sessionQueryProperty, + QDMI_session_query_session_property); + LOAD_CLIENT_SYMBOL(deviceCreateJob, QDMI_device_create_job); + LOAD_CLIENT_SYMBOL(sessionRetrieveJobById, QDMI_session_retrieve_job_by_id); + LOAD_CLIENT_SYMBOL(jobFree, QDMI_job_free); + LOAD_CLIENT_SYMBOL(jobSetParameter, QDMI_job_set_parameter); + LOAD_CLIENT_SYMBOL(jobQueryProperty, QDMI_job_query_property); + LOAD_CLIENT_SYMBOL(jobSubmit, QDMI_job_submit); + LOAD_CLIENT_SYMBOL(jobCancel, QDMI_job_cancel); + LOAD_CLIENT_SYMBOL(jobCheck, QDMI_job_check); + LOAD_CLIENT_SYMBOL(jobWait, QDMI_job_wait); + LOAD_CLIENT_SYMBOL(jobGetResults, QDMI_job_get_results); + LOAD_CLIENT_SYMBOL(deviceQueryProperty, QDMI_device_query_device_property); + LOAD_CLIENT_SYMBOL(deviceQuerySiteProperty, + QDMI_device_query_site_property); + LOAD_CLIENT_SYMBOL(deviceQueryOperationProperty, + QDMI_device_query_operation_property); +#undef LOAD_CLIENT_SYMBOL + return {library, std::move(api)}; + } catch (...) { + closeLibrary(library); + throw; + } +} + +struct ClientSelection { + std::mutex mutex; + /// Keeps the selected driver loaded for the process. + LibraryHandle library{}; + std::shared_ptr api; + std::filesystem::path path; +}; + +[[nodiscard]] auto clientSelection() -> ClientSelection& { + /// The selected driver must remain loaded until process teardown. + /// NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + static auto* selection = new ClientSelection{}; + return *selection; +} + +void selectClient(ClientSelection& selection, LoadedClient& loaded, + std::filesystem::path& path) noexcept { + selection.library = std::exchange(loaded.library, nullptr); + selection.api = loaded.api; + selection.path.swap(path); +} + +using SessionGuard = + std::unique_ptr; + +void validateSessionAllocation(const int status, QDMI_Session session) { + if (session == nullptr && + (status == QDMI_SUCCESS || status == QDMI_WARN_GENERAL)) { + throw std::runtime_error("The QDMI Client driver returned a null session"); + } + qdmi::throwIfError(status, "Allocating QDMI session"); +} + +[[nodiscard]] auto allocateSession(const SessionConfig& config) + -> std::shared_ptr { + auto& selection = clientSelection(); + const std::scoped_lock lock(selection.mutex); + if (selection.api != nullptr) { + if (config.driverPath && + normalizePath(*config.driverPath) != selection.path) { + throw std::runtime_error( + "The QDMI Client driver is already selected for this process"); + } + const auto api = selection.api; + QDMI_Session session = nullptr; + const auto result = api->sessionAlloc(&session); + SessionGuard guard{session, api->sessionFree}; + validateSessionAllocation(result, session); + auto owner = std::make_shared(api, session); + /// The ClientSession now owns the raw QDMI session. + /// NOLINTNEXTLINE(bugprone-unused-return-value) + guard.release(); + return owner; + } + + auto path = requestedDriverPath(config); + auto loaded = loadClient(path); + const std::shared_ptr api = loaded.api; + QDMI_Session session = nullptr; + const auto result = api->sessionAlloc(&session); + SessionGuard guard{session, api->sessionFree}; + validateSessionAllocation(result, session); + auto owner = std::make_shared(api, session); + selectClient(selection, loaded, path); + /// The ClientSession now owns the raw QDMI session. + /// NOLINTNEXTLINE(bugprone-unused-return-value) + guard.release(); + return owner; +} } // namespace +detail::ClientSession::~ClientSession() { + if (handle != nullptr) { + api->sessionFree(handle); + } +} + +void detail::JobDeleter::operator()(QDMI_Job_impl_d* const job) const { + if (job != nullptr) { + session->api->jobFree(job); + } +} + size_t Site::getIndex() const { return queryProperty(QDMI_SITE_PROPERTY_INDEX); } @@ -156,9 +472,10 @@ std::optional> Operation::getSites() const { } std::vector returnedSites; returnedSites.reserve(qdmiSites->size()); - std::ranges::transform( - *qdmiSites, std::back_inserter(returnedSites), - [this](const QDMI_Site& site) -> Site { return {device_, site}; }); + std::ranges::transform(*qdmiSites, std::back_inserter(returnedSites), + [this](const QDMI_Site& site) -> Site { + return {device_, session_, site}; + }); return returnedSites; } std::optional>> @@ -193,6 +510,9 @@ Operation::getMeanShuttlingSpeed(const std::vector& sites, return queryProperty>( QDMI_OPERATION_PROPERTY_MEANSHUTTLINGSPEED, sites, params); } +std::string Device::getId() const { + return queryProperty(QDMI_DEVICE_PROPERTY_ID); +} std::string Device::getName() const { return queryProperty(QDMI_DEVICE_PROPERTY_NAME); } @@ -218,9 +538,10 @@ std::vector Device::getSites() const { queryProperty>(QDMI_DEVICE_PROPERTY_SITES); std::vector sites; sites.reserve(qdmiSites.size()); - std::ranges::transform( - qdmiSites, std::back_inserter(sites), - [this](const QDMI_Site& site) -> Site { return {device_, site}; }); + std::ranges::transform(qdmiSites, std::back_inserter(sites), + [this](const QDMI_Site& site) -> Site { + return {device_, session_, site}; + }); return sites; } @@ -252,8 +573,8 @@ Device::queryCustomOperations(const CustomProperty property) const { const auto qdmiProperty = detail::toDeviceProperty(property); const auto handles = detail::queryHandleArray( [this, qdmiProperty](const size_t size, void* value, size_t* sizeRet) { - return QDMI_device_query_device_property(device_.get(), qdmiProperty, - size, value, sizeRet); + return api().deviceQueryProperty(device_, qdmiProperty, size, value, + sizeRet); }, "custom operation list " + std::to_string(static_cast(property))); @@ -267,9 +588,10 @@ std::vector Device::wrapOperations(const std::span operations) const { std::vector wrappedOperations; wrappedOperations.reserve(operations.size()); - std::ranges::transform( - operations, std::back_inserter(wrappedOperations), - [this](const QDMI_Operation& op) -> Operation { return {device_, op}; }); + std::ranges::transform(operations, std::back_inserter(wrappedOperations), + [this](const QDMI_Operation& op) -> Operation { + return {device_, session_, op}; + }); return wrappedOperations; } @@ -288,8 +610,8 @@ Device::getCouplingMap() const { [this](const std::pair& pair) -> std::pair { return { - Site{device_, pair.first}, - Site{device_, pair.second}, + Site{device_, session_, pair.first}, + Site{device_, session_, pair.second}, }; }); return couplingMap; @@ -336,8 +658,8 @@ std::vector Device::getSupportedProgramFormats() const { std::vector Device::getChildDevices() const { size_t size = 0; - auto result = QDMI_device_query_device_property( - device_.get(), QDMI_DEVICE_PROPERTY_CHILDDEVICES, 0, nullptr, &size); + auto result = api().deviceQueryProperty( + device_, QDMI_DEVICE_PROPERTY_CHILDDEVICES, 0, nullptr, &size); if (result == QDMI_ERROR_NOTSUPPORTED) { return {}; } @@ -348,19 +670,18 @@ std::vector Device::getChildDevices() const { std::vector handles(size / sizeof(QDMI_Device)); if (size != 0) { - result = QDMI_device_query_device_property( - device_.get(), QDMI_DEVICE_PROPERTY_CHILDDEVICES, size, + result = api().deviceQueryProperty( + device_, QDMI_DEVICE_PROPERTY_CHILDDEVICES, size, static_cast(handles.data()), nullptr); qdmi::throwIfError(result, "Querying child devices"); } std::vector devices; devices.reserve(handles.size()); - std::ranges::transform( - handles, std::back_inserter(devices), - [this](QDMI_Device_impl_d* const handle) { - return Device(std::shared_ptr(device_, handle)); - }); + std::ranges::transform(handles, std::back_inserter(devices), + [this](QDMI_Device_impl_d* const handle) { + return Device(handle, session_); + }); return devices; } @@ -437,24 +758,23 @@ Job Device::submitJobImpl( const std::optional& custom4, const std::optional& custom5) const { QDMI_Job job = nullptr; - qdmi::throwIfError(QDMI_device_create_job(device_.get(), &job), - "Creating job"); - Job jobWrapper{job, device_}; + qdmi::throwIfError(api().deviceCreateJob(device_, &job), "Creating job"); + Job jobWrapper{job, session_}; - qdmi::throwIfError(QDMI_job_set_parameter(jobWrapper, - QDMI_JOB_PARAMETER_PROGRAMFORMAT, - sizeof(format), &format), + qdmi::throwIfError(api().jobSetParameter(jobWrapper, + QDMI_JOB_PARAMETER_PROGRAMFORMAT, + sizeof(format), &format), "Setting program format"); if (program.has_value()) { - qdmi::throwIfError(QDMI_job_set_parameter(jobWrapper, - QDMI_JOB_PARAMETER_PROGRAM, - program->size(), program->data()), + qdmi::throwIfError(api().jobSetParameter(jobWrapper, + QDMI_JOB_PARAMETER_PROGRAM, + program->size(), program->data()), "Setting program"); } if (numShots.has_value()) { - qdmi::throwIfError(QDMI_job_set_parameter(jobWrapper, - QDMI_JOB_PARAMETER_SHOTSNUM, - sizeof(*numShots), &*numShots), + qdmi::throwIfError(api().jobSetParameter(jobWrapper, + QDMI_JOB_PARAMETER_SHOTSNUM, + sizeof(*numShots), &*numShots), "Setting number of shots"); } @@ -474,7 +794,7 @@ Job Device::submitJobImpl( setCustomJobParam(jobWrapper, QDMI_JOB_PARAMETER_CUSTOM5, *custom5); } - qdmi::throwIfError(QDMI_job_submit(jobWrapper), "Submitting job"); + qdmi::throwIfError(api().jobSubmit(jobWrapper), "Submitting job"); return jobWrapper; } @@ -507,27 +827,26 @@ Job Device::submitCalibrationJob( Job Device::retrieveJobById(const std::string_view jobId) const { const std::string id{jobId}; QDMI_Job job = nullptr; - qdmi::throwIfError( - QDMI_session_retrieve_job_by_id(device_.get(), id.c_str(), &job), - "Retrieving job"); - return Job{job, device_}; + qdmi::throwIfError(api().sessionRetrieveJobById(device_, id.c_str(), &job), + "Retrieving job"); + return Job{job, session_}; } void Device::setCustomJobParam(QDMI_Job job, const QDMI_Job_Parameter param, - const CustomJobParameter& value) { + const CustomJobParameter& value) const { std::visit( [&](const CustomValue& customValue) { using T = std::decay_t; if constexpr (std::is_same_v) { - qdmi::throwIfError(QDMI_job_set_parameter(job, param, - customValue.size() + 1, - customValue.c_str()), + qdmi::throwIfError(api().jobSetParameter(job, param, + customValue.size() + 1, + customValue.c_str()), "Setting custom parameter"); } else { static_assert(std::is_trivially_copyable_v, "Custom job parameters must be trivially copyable"); qdmi::throwIfError( - QDMI_job_set_parameter(job, param, sizeof(T), &customValue), + api().jobSetParameter(job, param, sizeof(T), &customValue), "Setting custom parameter"); } }, @@ -536,13 +855,13 @@ void Device::setCustomJobParam(QDMI_Job job, const QDMI_Job_Parameter param, QDMI_Job_Status Job::check() const { QDMI_Job_Status status{}; - qdmi::throwIfError(QDMI_job_check(job_.get(), &status), + qdmi::throwIfError(api().jobCheck(job_.get(), &status), "Checking job status"); return status; } bool Job::wait(const size_t timeout) const { - const auto ret = QDMI_job_wait(job_.get(), timeout); + const auto ret = api().jobWait(job_.get(), timeout); if (ret == QDMI_SUCCESS) { return true; } @@ -554,52 +873,42 @@ bool Job::wait(const size_t timeout) const { } void Job::cancel() const { - qdmi::throwIfError(QDMI_job_cancel(job_.get()), "Cancelling job"); -} - -auto Job::operator=(Job&& other) noexcept -> Job& { - if (this != &other) { - // Release the current job while its owning device session is still alive. - job_.reset(); - device_ = std::move(other.device_); - job_ = std::move(other.job_); - } - return *this; + qdmi::throwIfError(api().jobCancel(job_.get()), "Cancelling job"); } std::string Job::getId() const { size_t size = 0; - qdmi::throwIfError(QDMI_job_query_property(job_.get(), QDMI_JOB_PROPERTY_ID, - 0, nullptr, &size), + qdmi::throwIfError(api().jobQueryProperty(job_.get(), QDMI_JOB_PROPERTY_ID, 0, + nullptr, &size), "Querying job ID size"); - std::string id(size - 1, '\0'); - qdmi::throwIfError(QDMI_job_query_property(job_.get(), QDMI_JOB_PROPERTY_ID, - size, id.data(), nullptr), + std::string id(size, '\0'); + qdmi::throwIfError(api().jobQueryProperty(job_.get(), QDMI_JOB_PROPERTY_ID, + size, id.data(), nullptr), "Querying job ID"); - return id; + return detail::decodeText(std::move(id), "Job ID"); } QDMI_Program_Format Job::getProgramFormat() const { QDMI_Program_Format format{}; - qdmi::throwIfError(QDMI_job_query_property(job_.get(), - QDMI_JOB_PROPERTY_PROGRAMFORMAT, - sizeof(format), &format, nullptr), + qdmi::throwIfError(api().jobQueryProperty(job_.get(), + QDMI_JOB_PROPERTY_PROGRAMFORMAT, + sizeof(format), &format, nullptr), "Querying program format"); return format; } std::vector Job::getProgramBytes() const { size_t size = 0; - qdmi::throwIfError(QDMI_job_query_property(job_.get(), - QDMI_JOB_PROPERTY_PROGRAM, 0, - nullptr, &size), + qdmi::throwIfError(api().jobQueryProperty(job_.get(), + QDMI_JOB_PROPERTY_PROGRAM, 0, + nullptr, &size), "Querying program size"); std::vector program(size); if (size != 0) { - qdmi::throwIfError(QDMI_job_query_property(job_.get(), - QDMI_JOB_PROPERTY_PROGRAM, size, - program.data(), nullptr), + qdmi::throwIfError(api().jobQueryProperty(job_.get(), + QDMI_JOB_PROPERTY_PROGRAM, size, + program.data(), nullptr), "Querying program"); } return program; @@ -624,8 +933,8 @@ std::string Job::getProgram() const { size_t Job::getNumShots() const { size_t numShots = 0; qdmi::throwIfError( - QDMI_job_query_property(job_.get(), QDMI_JOB_PROPERTY_SHOTSNUM, - sizeof(numShots), &numShots, nullptr), + api().jobQueryProperty(job_.get(), QDMI_JOB_PROPERTY_SHOTSNUM, + sizeof(numShots), &numShots, nullptr), "Querying number of shots"); return numShots; } @@ -633,15 +942,15 @@ size_t Job::getNumShots() const { std::optional Job::getQueuePosition() const { size_t queuePosition = 0; const auto result = - QDMI_job_query_property(job_.get(), QDMI_JOB_PROPERTY_QUEUEPOSITION, - sizeof(queuePosition), &queuePosition, nullptr); + api().jobQueryProperty(job_.get(), QDMI_JOB_PROPERTY_QUEUEPOSITION, + sizeof(queuePosition), &queuePosition, nullptr); return detail::queuePositionFromResult(result, queuePosition); } std::vector Job::getShots() const { size_t shotsSize = 0; - qdmi::throwIfError(QDMI_job_get_results(job_.get(), QDMI_JOB_RESULT_SHOTS, 0, - nullptr, &shotsSize), + qdmi::throwIfError(api().jobGetResults(job_.get(), QDMI_JOB_RESULT_SHOTS, 0, + nullptr, &shotsSize), "Querying shots size"); if (shotsSize == 0) { @@ -649,8 +958,8 @@ std::vector Job::getShots() const { } std::string shots(shotsSize, '\0'); - qdmi::throwIfError(QDMI_job_get_results(job_.get(), QDMI_JOB_RESULT_SHOTS, - shotsSize, shots.data(), nullptr), + qdmi::throwIfError(api().jobGetResults(job_.get(), QDMI_JOB_RESULT_SHOTS, + shotsSize, shots.data(), nullptr), "Querying shots"); shots.pop_back(); @@ -660,8 +969,8 @@ std::vector Job::getShots() const { std::map Job::getCounts() const { // Get the histogram keys size_t keysSize = 0; - qdmi::throwIfError(QDMI_job_get_results(job_.get(), QDMI_JOB_RESULT_HIST_KEYS, - 0, nullptr, &keysSize), + qdmi::throwIfError(api().jobGetResults(job_.get(), QDMI_JOB_RESULT_HIST_KEYS, + 0, nullptr, &keysSize), "Querying histogram keys size"); if (keysSize == 0) { @@ -669,16 +978,16 @@ std::map Job::getCounts() const { } std::string keys(keysSize, '\0'); - qdmi::throwIfError(QDMI_job_get_results(job_.get(), QDMI_JOB_RESULT_HIST_KEYS, - keysSize, keys.data(), nullptr), + qdmi::throwIfError(api().jobGetResults(job_.get(), QDMI_JOB_RESULT_HIST_KEYS, + keysSize, keys.data(), nullptr), "Querying histogram keys"); keys.pop_back(); // Get the histogram values size_t valuesSize = 0; - qdmi::throwIfError(QDMI_job_get_results(job_.get(), - QDMI_JOB_RESULT_HIST_VALUES, 0, - nullptr, &valuesSize), + qdmi::throwIfError(api().jobGetResults(job_.get(), + QDMI_JOB_RESULT_HIST_VALUES, 0, + nullptr, &valuesSize), "Querying histogram values size"); if (valuesSize % sizeof(size_t) != 0) { @@ -687,9 +996,9 @@ std::map Job::getCounts() const { } std::vector values(valuesSize / sizeof(size_t)); - qdmi::throwIfError(QDMI_job_get_results(job_.get(), - QDMI_JOB_RESULT_HIST_VALUES, - valuesSize, values.data(), nullptr), + qdmi::throwIfError(api().jobGetResults(job_.get(), + QDMI_JOB_RESULT_HIST_VALUES, + valuesSize, values.data(), nullptr), "Querying histogram values"); // Parse the keys (comma-separated) @@ -717,9 +1026,9 @@ std::map Job::getCounts() const { std::vector> Job::getDenseStateVector() const { size_t size = 0; - qdmi::throwIfError(QDMI_job_get_results(job_.get(), - QDMI_JOB_RESULT_STATEVECTOR_DENSE, 0, - nullptr, &size), + qdmi::throwIfError(api().jobGetResults(job_.get(), + QDMI_JOB_RESULT_STATEVECTOR_DENSE, 0, + nullptr, &size), "Querying dense state vector size"); if (size % sizeof(std::complex) != 0) { @@ -729,18 +1038,18 @@ std::vector> Job::getDenseStateVector() const { std::vector> stateVector(size / sizeof(std::complex)); - qdmi::throwIfError(QDMI_job_get_results(job_.get(), - QDMI_JOB_RESULT_STATEVECTOR_DENSE, - size, stateVector.data(), nullptr), + qdmi::throwIfError(api().jobGetResults(job_.get(), + QDMI_JOB_RESULT_STATEVECTOR_DENSE, + size, stateVector.data(), nullptr), "Querying dense state vector"); return stateVector; } std::vector Job::getDenseProbabilities() const { size_t size = 0; - qdmi::throwIfError(QDMI_job_get_results(job_.get(), - QDMI_JOB_RESULT_PROBABILITIES_DENSE, - 0, nullptr, &size), + qdmi::throwIfError(api().jobGetResults(job_.get(), + QDMI_JOB_RESULT_PROBABILITIES_DENSE, 0, + nullptr, &size), "Querying dense probabilities size"); if (size % sizeof(double) != 0) { @@ -749,9 +1058,9 @@ std::vector Job::getDenseProbabilities() const { } std::vector probabilities(size / sizeof(double)); - qdmi::throwIfError(QDMI_job_get_results(job_.get(), - QDMI_JOB_RESULT_PROBABILITIES_DENSE, - size, probabilities.data(), nullptr), + qdmi::throwIfError(api().jobGetResults(job_.get(), + QDMI_JOB_RESULT_PROBABILITIES_DENSE, + size, probabilities.data(), nullptr), "Querying dense probabilities"); return probabilities; } @@ -759,8 +1068,8 @@ std::vector Job::getDenseProbabilities() const { std::map> Job::getSparseStateVector() const { size_t keysSize = 0; qdmi::throwIfError( - QDMI_job_get_results(job_.get(), QDMI_JOB_RESULT_STATEVECTOR_SPARSE_KEYS, - 0, nullptr, &keysSize), + api().jobGetResults(job_.get(), QDMI_JOB_RESULT_STATEVECTOR_SPARSE_KEYS, + 0, nullptr, &keysSize), "Querying sparse state vector keys size"); if (keysSize == 0) { @@ -769,16 +1078,16 @@ std::map> Job::getSparseStateVector() const { std::string keys(keysSize, '\0'); qdmi::throwIfError( - QDMI_job_get_results(job_.get(), QDMI_JOB_RESULT_STATEVECTOR_SPARSE_KEYS, - keysSize, keys.data(), nullptr), + api().jobGetResults(job_.get(), QDMI_JOB_RESULT_STATEVECTOR_SPARSE_KEYS, + keysSize, keys.data(), nullptr), "Querying sparse state vector keys"); keys.pop_back(); size_t valuesSize = 0; - qdmi::throwIfError(QDMI_job_get_results( - job_.get(), QDMI_JOB_RESULT_STATEVECTOR_SPARSE_VALUES, - 0, nullptr, &valuesSize), - "Querying sparse state vector values size"); + qdmi::throwIfError( + api().jobGetResults(job_.get(), QDMI_JOB_RESULT_STATEVECTOR_SPARSE_VALUES, + 0, nullptr, &valuesSize), + "Querying sparse state vector values size"); if (valuesSize % sizeof(std::complex) != 0) { throw std::runtime_error( @@ -788,10 +1097,10 @@ std::map> Job::getSparseStateVector() const { std::vector> values(valuesSize / sizeof(std::complex)); - qdmi::throwIfError(QDMI_job_get_results( - job_.get(), QDMI_JOB_RESULT_STATEVECTOR_SPARSE_VALUES, - valuesSize, values.data(), nullptr), - "Querying sparse state vector values"); + qdmi::throwIfError( + api().jobGetResults(job_.get(), QDMI_JOB_RESULT_STATEVECTOR_SPARSE_VALUES, + valuesSize, values.data(), nullptr), + "Querying sparse state vector values"); // Parse the keys (comma-separated) std::map> stateVector; @@ -818,27 +1127,28 @@ std::map> Job::getSparseStateVector() const { std::map Job::getSparseProbabilities() const { size_t keysSize = 0; - qdmi::throwIfError(QDMI_job_get_results( - job_.get(), QDMI_JOB_RESULT_PROBABILITIES_SPARSE_KEYS, - 0, nullptr, &keysSize), - "Querying sparse probabilities keys size"); + qdmi::throwIfError( + api().jobGetResults(job_.get(), QDMI_JOB_RESULT_PROBABILITIES_SPARSE_KEYS, + 0, nullptr, &keysSize), + "Querying sparse probabilities keys size"); if (keysSize == 0) { return {}; // Empty probabilities } std::string keys(keysSize, '\0'); - qdmi::throwIfError(QDMI_job_get_results( - job_.get(), QDMI_JOB_RESULT_PROBABILITIES_SPARSE_KEYS, - keysSize, keys.data(), nullptr), - "Querying sparse probabilities keys"); - keys.pop_back(); + qdmi::throwIfError( + api().jobGetResults(job_.get(), QDMI_JOB_RESULT_PROBABILITIES_SPARSE_KEYS, + keysSize, keys.data(), nullptr), + "Querying sparse probabilities keys"); + keys = + detail::decodeText(std::move(keys), "Sparse probabilities keys result"); size_t valuesSize = 0; qdmi::throwIfError( - QDMI_job_get_results(job_.get(), - QDMI_JOB_RESULT_PROBABILITIES_SPARSE_VALUES, 0, - nullptr, &valuesSize), + api().jobGetResults(job_.get(), + QDMI_JOB_RESULT_PROBABILITIES_SPARSE_VALUES, 0, + nullptr, &valuesSize), "Querying sparse probabilities values size"); if (valuesSize % sizeof(double) != 0) { @@ -848,9 +1158,9 @@ std::map Job::getSparseProbabilities() const { std::vector values(valuesSize / sizeof(double)); qdmi::throwIfError( - QDMI_job_get_results(job_.get(), - QDMI_JOB_RESULT_PROBABILITIES_SPARSE_VALUES, - valuesSize, values.data(), nullptr), + api().jobGetResults(job_.get(), + QDMI_JOB_RESULT_PROBABILITIES_SPARSE_VALUES, + valuesSize, values.data(), nullptr), "Querying sparse probabilities values"); // Parse the keys (comma-separated) @@ -875,89 +1185,55 @@ std::map Job::getSparseProbabilities() const { return probabilities; } -Device Session::createSessionlessDevice(QDMI_Device device) { - return Device(device); -} - Device Session::openDevice(const std::string_view id, - const qdmi::DeviceSessionConfig& overrides) { - return Device(qdmi::Driver::get().openFresh(id, overrides)); + const SessionConfig& config) { + if (id.empty() || id.find('\0') != std::string_view::npos) { + throw std::invalid_argument( + "QDMI device ID must not be empty or contain null bytes"); + } + Session session(config); + const auto devices = session.getDevices(); + std::string available; + for (const auto& device : devices) { + const auto candidateId = device.getId(); + if (candidateId == id) { + return device; + } + if (!available.empty()) { + available += ", "; + } + available += candidateId; + } + throw std::out_of_range("QDMI Client session has no device with ID '" + + std::string(id) + "'; available IDs: " + available); } Session::Session(const SessionConfig& config) { - session_ = [] { - QDMI_Session session = nullptr; - const auto result = QDMI_session_alloc(&session); - qdmi::throwIfError(result, "Allocating QDMI session"); - return std::unique_ptr( - session, QDMI_session_free); - }(); + session_ = allocateSession(config); - // Helper to set session parameters const auto setParameter = [this](const std::optional& value, QDMI_Session_Parameter param) -> void { - if (value) { - const auto status = static_cast(QDMI_session_set_parameter( - session_.get(), param, value->size() + 1, value->c_str())); - if (status == QDMI_ERROR_NOTSUPPORTED) { - // Optional parameter not supported by session - skip it - qdmi::diagnostics::info("Session parameter {} not supported (skipped)", - qdmi::toString(param)); - return; - } - if (status == QDMI_SUCCESS) { - return; - } - std::ostringstream ss; - ss << "Setting session parameter " << qdmi::toString(param) << ": " - << qdmi::toString(status) << " (status = " << status << ")"; - qdmi::throwIfError(status, ss.str()); + if (!value) { + return; } - }; - - // Validate file existence for authFile - if (config.authFile) { - if (!std::filesystem::exists(*config.authFile)) { - throw std::runtime_error("Authentication file does not exist: " + - config.authFile->string()); + const auto status = static_cast(api().sessionSetParameter( + session_->handle, param, value->size() + 1U, value->c_str())); + if (status == QDMI_ERROR_NOTSUPPORTED) { + qdmi::diagnostics::info("Session parameter {} not supported (skipped)", + qdmi::toString(param)); + return; } - } - // Validate URL format for authUrl - if (config.authUrl) { - // Breakdown of the regex pattern: - // 1. ^https?:// -> Start with http:// or https:// - // 2. (?: -> Start Host Group - // \[[a-fA-F0-9:]+\] -> Branch A: IPv6 (Must be in brackets like - // [::1]) - // -> Note: No \b used here because ']' is a - // non-word char - // | -> OR - // (?: -> Branch B: Alphanumeric Hosts (Group for - // \b check) - // (?:\d{1,3}\.){3}\d{1,3} -> IPv4 (e.g., 127.0.0.1) - // | -> OR - // localhost -> Localhost - // | -> OR - // (?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6} -> - // Domain - // )\b -> End Branch B + Word Boundary (Prevents - // "localhostX") - // ) -> End Host Group - // 3. (?::\d+)? -> Optional Port (e.g., :8080) - // 4. (?:...)*$ -> Optional Path/Query params + End of - // string - static const std::regex URL_PATTERN( - R"(^https?://(?:\[[a-fA-F0-9:]+\]|(?:(?:\d{1,3}\.){3}\d{1,3}|localhost|(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6})\b)(?::\d+)?(?:[-a-zA-Z0-9()@:%_\+.~#?&/=]*)$)", - std::regex::optimize); - if (!std::regex_match(*config.authUrl, URL_PATTERN)) { - throw std::runtime_error("Invalid URL format: " + *config.authUrl); + if (status != QDMI_SUCCESS) { + std::ostringstream message; + message << "Setting session parameter " << qdmi::toString(param) << ": " + << qdmi::toString(status) << " (status = " << status << ")"; + qdmi::throwIfError(status, message.str()); } - } + }; - // Set session parameters setParameter(config.token, QDMI_SESSION_PARAMETER_TOKEN); if (config.authFile) { - const std::optional authFile = config.authFile->string(); + const std::optional authFile = detail::pathToUtf8(*config.authFile); setParameter(authFile, QDMI_SESSION_PARAMETER_AUTHFILE); } setParameter(config.authUrl, QDMI_SESSION_PARAMETER_AUTHURL); @@ -970,8 +1246,8 @@ Session::Session(const SessionConfig& config) { setParameter(config.custom4, QDMI_SESSION_PARAMETER_CUSTOM4); setParameter(config.custom5, QDMI_SESSION_PARAMETER_CUSTOM5); - // Initialize the session - qdmi::throwIfError(QDMI_session_init(session_.get()), "Initializing session"); + qdmi::throwIfError(api().sessionInit(session_->handle), + "Initializing session"); } std::vector Session::getDevices() { @@ -979,9 +1255,10 @@ std::vector Session::getDevices() { queryProperty>(QDMI_SESSION_PROPERTY_DEVICES); std::vector devices; devices.reserve(qdmiDevices.size()); - std::ranges::transform( - qdmiDevices, std::back_inserter(devices), - [](QDMI_Device_impl_d* const& dev) -> Device { return Device(dev); }); + std::ranges::transform(qdmiDevices, std::back_inserter(devices), + [this](QDMI_Device_impl_d* const& dev) -> Device { + return {dev, session_}; + }); return devices; } } // namespace qdmi diff --git a/src/qdmi/Slurm.cpp b/src/qdmi/Slurm.cpp index da7ed4e87e..b34a8eaf82 100644 --- a/src/qdmi/Slurm.cpp +++ b/src/qdmi/Slurm.cpp @@ -11,7 +11,6 @@ #include "qdmi/Slurm.hpp" #include "qdmi/Client.hpp" -#include "qdmi/driver/Driver.hpp" #include @@ -52,7 +51,7 @@ namespace { } [[nodiscard]] auto parseLicense(const std::string& licenseSpec, - const std::vector& registeredIds) + const std::vector& visibleIds) -> std::string { if (licenseSpec.empty()) { throw std::runtime_error( @@ -111,9 +110,9 @@ namespace { } } - if (std::ranges::find(registeredIds, deviceId) == registeredIds.end()) { + if (std::ranges::find(visibleIds, deviceId) == visibleIds.end()) { throw std::runtime_error("Slurm license '" + deviceId + - "' is not a registered QDMI device ID"); + "' is not a Client-visible QDMI device ID"); } return deviceId; } @@ -121,15 +120,22 @@ namespace { } // namespace Device openDeviceFromLicense() { - // The job can modify its environment. Use this value only to select a - // registered device; the provider or operating system must authorize access. + /// The job can modify its environment. Use this value only to select a + /// Client-visible device. The provider or operating system must authorize + /// access. const auto* const environmentValue = std::getenv("SLURM_JOB_LICENSES"); const std::string licenseSpec = environmentValue == nullptr ? std::string{} : environmentValue; - const auto deviceId = - parseLicense(licenseSpec, qdmi::Driver::get().registeredDeviceIds()); - - auto device = Session::openDevice(deviceId); + Session session; + auto devices = session.getDevices(); + std::vector deviceIds; + deviceIds.reserve(devices.size()); + std::ranges::transform(devices, std::back_inserter(deviceIds), + [](const Device& device) { return device.getId(); }); + const auto deviceId = parseLicense(licenseSpec, deviceIds); + const auto selected = + std::ranges::find(deviceIds, deviceId) - deviceIds.begin(); + auto device = devices[static_cast(selected)]; const auto status = device.getStatus(); if (status != QDMI_DEVICE_STATUS_IDLE && status != QDMI_DEVICE_STATUS_BUSY) { throw std::runtime_error("SLURM_JOB_LICENSES names QDMI device '" + diff --git a/src/qdmi/common/Common.cpp b/src/qdmi/common/Common.cpp index e44145e700..9524ec4a08 100644 --- a/src/qdmi/common/Common.cpp +++ b/src/qdmi/common/Common.cpp @@ -16,12 +16,52 @@ #include +#include #include +#include #include #include #include +#include + +#ifdef _WIN32 +#include + +#include +#endif namespace qdmi { +namespace detail { +auto environmentUtf8(const std::string_view name) + -> std::optional { +#ifdef _WIN32 + std::wstring wideName; + for (const char character : name) { + wideName.push_back( + static_cast(static_cast(character))); + } + const auto size = GetEnvironmentVariableW(wideName.c_str(), nullptr, 0); + if (size == 0) { + return std::nullopt; + } + std::wstring value(size, L'\0'); + const auto written = GetEnvironmentVariableW( + wideName.c_str(), value.data(), static_cast(value.size())); + if (written == 0 || written >= value.size()) { + return std::nullopt; + } + value.resize(written); + return pathToUtf8(std::filesystem::path(value)); +#else + const std::string ownedName{name}; + if (const auto* value = std::getenv(ownedName.c_str()); + value != nullptr && *value != '\0') { + return std::string(value); + } + return std::nullopt; +#endif +} +} // namespace detail auto throwIfError(const int result, const std::string& msg) -> void { switch (const auto res = static_cast(result)) { diff --git a/src/qdmi/driver/CMakeLists.txt b/src/qdmi/driver/CMakeLists.txt index d865043c8e..f3a7c3ce2e 100644 --- a/src/qdmi/driver/CMakeLists.txt +++ b/src/qdmi/driver/CMakeLists.txt @@ -10,7 +10,11 @@ set(TARGET_NAME ${MQT_CORE_TARGET_NAME}-qdmi-driver) if(NOT TARGET ${TARGET_NAME}) # Add driver library - add_mqt_core_library(${TARGET_NAME} ALIAS_NAME QDMIDriver) + add_mqt_core_library(${TARGET_NAME} ALIAS_NAME QDMIDriver FORCE_SHARED) + set_target_properties( + ${TARGET_NAME} PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/.." + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/..") + target_compile_definitions(${TARGET_NAME} PRIVATE QDMI_driver_EXPORTS) # Add sources to target target_sources(${TARGET_NAME} PRIVATE DeviceRegistry.cpp Driver.cpp) @@ -37,15 +41,7 @@ if(NOT TARGET ${TARGET_NAME}) # Ensure the driver can find the device libraries at runtime if(QDMI_DEVICE_TARGETS) - if(WIN32) - mqt_copy_qdmi_runtime(${TARGET_NAME} ${QDMI_DEVICE_TARGETS}) - else() - add_dependencies(${TARGET_NAME} ${QDMI_DEVICE_TARGETS}) - foreach(device IN LISTS QDMI_DEVICE_TARGETS) - target_link_options(${TARGET_NAME} INTERFACE - $>) - endforeach() - endif() + mqt_copy_qdmi_runtime(${TARGET_NAME} ${QDMI_DEVICE_TARGETS}) endif() # add to list of MQT core targets diff --git a/src/qdmi/driver/DeviceRegistry.cpp b/src/qdmi/driver/DeviceRegistry.cpp index dc168e23c3..a585acf6eb 100644 --- a/src/qdmi/driver/DeviceRegistry.cpp +++ b/src/qdmi/driver/DeviceRegistry.cpp @@ -36,6 +36,15 @@ #endif namespace qdmi::detail { +void validateDeviceId(const std::string_view id) { + if (id.empty()) { + throw std::invalid_argument("Device definition ID must not be empty"); + } + if (id.find('\0') != std::string_view::npos) { + throw std::invalid_argument("Device definition ID must not contain NUL"); + } +} + namespace { using Json = nlohmann::json; // NOLINT(misc-include-cleaner) @@ -201,6 +210,7 @@ parseDevicePatch(const Json& value, const std::filesystem::path& source, throw std::invalid_argument(sourceLabel(source, path + ".id") + " must be a non-empty string"); } + validateDeviceId(*id); DefinitionPatch patch; patch.id = *id; patch.source = source; diff --git a/src/qdmi/driver/DeviceRegistry.hpp b/src/qdmi/driver/DeviceRegistry.hpp index 83061f2f62..6a9441e4fb 100644 --- a/src/qdmi/driver/DeviceRegistry.hpp +++ b/src/qdmi/driver/DeviceRegistry.hpp @@ -13,10 +13,14 @@ #include "qdmi/driver/Driver.hpp" #include +#include #include namespace qdmi::detail { +/// Rejects IDs that the QDMI string-property ABI cannot represent. +void validateDeviceId(std::string_view id); + /// Discovers configured QDMI devices without loading their libraries. class DeviceRegistry { public: diff --git a/src/qdmi/driver/Driver.cpp b/src/qdmi/driver/Driver.cpp index dbcbfc8cc9..e2495954fe 100644 --- a/src/qdmi/driver/Driver.cpp +++ b/src/qdmi/driver/Driver.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -27,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -79,7 +81,7 @@ namespace { /// Loads the device library with the given name, searching in the driver /// directory if no path is specified. [[nodiscard]] auto loadDeviceLibrary(const std::string& libName) -> HMODULE { - const auto requested = std::filesystem::path(libName); + const auto requested = detail::pathFromUtf8(libName); // Bare filenames are resolved relative to the Driver. Configured paths are // already absolute or relative to their declaring file. const auto path = requested.has_parent_path() @@ -205,7 +207,7 @@ struct DynamicLibraryCache { if (error) { canonicalPath = std::filesystem::path(libName).lexically_normal(); } - const auto key = std::pair{canonicalPath.string(), prefix}; + const auto key = std::pair{detail::pathToUtf8(canonicalPath), prefix}; if (const auto library = cache.libraries[key].lock()) { return library; } @@ -249,9 +251,9 @@ void applyOverride(std::optional& value, QDMI_Device_impl_d::QDMI_Device_impl_d( std::shared_ptr lib, - const qdmi::DeviceSessionConfig& config, + const qdmi::DeviceSessionConfig& config, std::string id, QDMI_Child_Device_impl_d* const childDevice) - : library_(std::move(lib)) { + : id_(std::move(id)), library_(std::move(lib)) { if (library_->device_session_alloc(&deviceSession_) != QDMI_SUCCESS) { throw std::runtime_error("Failed to allocate device session"); } @@ -284,7 +286,7 @@ QDMI_Device_impl_d::QDMI_Device_impl_d( setParameter(config.baseUrl, QDMI_DEVICE_SESSION_PARAMETER_BASEURL); setParameter(config.token, QDMI_DEVICE_SESSION_PARAMETER_TOKEN); if (config.authFile) { - const std::optional authFile = config.authFile->string(); + const std::optional authFile = qdmi::detail::pathToUtf8(*config.authFile); setParameter(authFile, QDMI_DEVICE_SESSION_PARAMETER_AUTHFILE); } setParameter(config.authUrl, QDMI_DEVICE_SESSION_PARAMETER_AUTHURL); @@ -307,7 +309,7 @@ QDMI_Device_impl_d::QDMI_Device_impl_d( setParameter(std::optional{source.json}, QDMI_DEVICE_SESSION_PARAMETER_CUSTOM1); } else { - setParameter(std::optional{source.path.string()}, + setParameter(std::optional{qdmi::detail::pathToUtf8(source.path)}, QDMI_DEVICE_SESSION_PARAMETER_CUSTOM2); } }, @@ -380,9 +382,11 @@ QDMI_Device_impl_d::QDMI_Device_impl_d( try { childDevices_.reserve(children.size()); - for (auto* const child : children) { - childDevices_.emplace_back( - std::make_unique(library_, config, child)); + for (size_t index = 0; index < children.size(); ++index) { + auto childId = + id_.empty() ? std::string{} : id_ + "/child/" + std::to_string(index); + childDevices_.emplace_back(std::make_unique( + library_, config, std::move(childId), children[index])); } } catch (...) { childDevices_.clear(); @@ -450,6 +454,10 @@ auto QDMI_Device_impl_d::freeJob(QDMI_Job job) -> void { auto QDMI_Device_impl_d::queryDeviceProperty(QDMI_Device_Property prop, const size_t size, void* value, size_t* sizeRet) const -> int { + if (!id_.empty()) { + ADD_STRING_PROPERTY(QDMI_DEVICE_PROPERTY_ID, id_.c_str(), prop, size, value, + sizeRet) + } if (prop == QDMI_DEVICE_PROPERTY_CHILDDEVICES) { if (childDevices_.empty()) { return QDMI_ERROR_NOTSUPPORTED; @@ -611,7 +619,8 @@ auto QDMI_Session_impl_d::init() -> int { auto QDMI_Session_impl_d::setParameter(QDMI_Session_Parameter param, const size_t size, const void* value) const -> int { - if ((value != nullptr && size == 0) || param >= QDMI_SESSION_PARAMETER_MAX) { + if ((value != nullptr && size == 0) || + IS_INVALID_ARGUMENT(param, QDMI_SESSION_PARAMETER)) { return QDMI_ERROR_INVALIDARGUMENT; } if (status_ != qdmi::SessionStatus::ALLOCATED) { @@ -648,9 +657,7 @@ auto QDMI_Session_impl_d::querySessionProperty(QDMI_Session_Property prop, namespace qdmi { namespace { void validateDefinition(const DeviceDefinition& definition) { - if (definition.id.empty()) { - throw std::invalid_argument("Device definition ID must not be empty"); - } + detail::validateDeviceId(definition.id); if (definition.library.empty()) { throw std::invalid_argument("Device definition library must not be empty"); } @@ -763,8 +770,9 @@ auto Driver::open(const std::string_view id) -> QDMI_Device { std::unique_ptr candidate; try { candidate = std::make_unique( - getDynamicDeviceLibrary(definition.library.string(), definition.prefix), - definition.session); + getDynamicDeviceLibrary(detail::pathToUtf8(definition.library), + definition.prefix), + definition.session, definition.id); } catch (...) { { const std::scoped_lock lock(stateMutex_); @@ -821,8 +829,9 @@ auto Driver::openFresh(const std::string_view id, definition = *registered; } return std::make_shared( - getDynamicDeviceLibrary(definition.library.string(), definition.prefix), - mergeSessionConfig(definition.session, overrides)); + getDynamicDeviceLibrary(detail::pathToUtf8(definition.library), + definition.prefix), + mergeSessionConfig(definition.session, overrides), definition.id); } void Driver::materializeClientCatalog() { @@ -844,7 +853,7 @@ void Driver::materializeClientCatalog() { if (const auto definition = std::ranges::find(definitions_, id, &DeviceDefinition::id); definition != definitions_.end()) { - library = definition->library.string(); + library = detail::pathToUtf8(definition->library); } } catch (...) { library.clear(); @@ -865,6 +874,7 @@ auto Driver::sessionAlloc(QDMI_Session* session) -> int { if (session == nullptr) { return QDMI_ERROR_INVALIDARGUMENT; } + *session = nullptr; materializeClientCatalog(); const std::scoped_lock lock(stateMutex_); auto uniqueSession = std::make_unique(clientDevices_); @@ -886,8 +896,24 @@ auto Driver::sessionFree(QDMI_Session session) -> void { } } // namespace qdmi +uint32_t QDMI_driver_get_client_abi_version() { + return QDMI_CLIENT_ABI_VERSION; +} + int QDMI_session_alloc(QDMI_Session* session) { - return qdmi::Driver::get().sessionAlloc(session); + if (session == nullptr) { + return QDMI_ERROR_INVALIDARGUMENT; + } + *session = nullptr; + try { + return qdmi::Driver::get().sessionAlloc(session); + } catch (const std::bad_alloc&) { + return QDMI_ERROR_OUTOFMEM; + } catch (const std::invalid_argument&) { + return QDMI_ERROR_INVALIDARGUMENT; + } catch (...) { + return QDMI_ERROR_FATAL; + } } int QDMI_session_init(QDMI_Session session) { diff --git a/test/python/plugins/qiskit/test_backend.py b/test/python/plugins/qiskit/test_backend.py index 36c07ef9b6..7fcd122bd4 100644 --- a/test/python/plugins/qiskit/test_backend.py +++ b/test/python/plugins/qiskit/test_backend.py @@ -26,7 +26,7 @@ QDMIBackend, UnsupportedOperationError, ) -from mqt.core.qdmi.driver import open_device +from mqt.core.qdmi import open_device from mqt.core.typing import QDMISessionParameters if TYPE_CHECKING: @@ -70,16 +70,14 @@ def fake_open_device(device_id: str, **session_parameters: object) -> QDMIDevice monkeypatch.setattr("mqt.core.plugins.qiskit.backend.open_device", fake_open_device) auth_file = Path("auth.json") - config_file = Path("device.json") session_parameters: QDMISessionParameters = { - "base_url": "https://device.example", + "driver_path": Path("client-driver.so"), "token": "token", "auth_file": auth_file, "auth_url": "https://auth.example", "username": "user", "password": "password", - "device_config": "{}", - "device_config_file": config_file, + "project_id": "project", "custom1": "one", "custom2": "two", "custom3": "three", @@ -104,16 +102,6 @@ def test_qdmi_session_parameter_annotations_are_runtime_resolvable() -> None: assert annotations["auth_file"] == str | os.PathLike[str] | None -def test_backend_from_device_id_rejects_conflicting_device_configuration() -> None: - """Retain native validation for mutually exclusive device configuration sources.""" - with pytest.raises(ValueError, match="mutually exclusive"): - QDMIBackend.from_device_id( - "mqt.ddsim.default", - device_config="{}", - device_config_file=Path("device.json"), - ) - - def test_backend_instantiation(ddsim_backend: QDMIBackend) -> None: """Backend exposes target qubit count.""" assert ddsim_backend.target.num_qubits > 0 diff --git a/test/python/plugins/qiskit/test_mock_backend.py b/test/python/plugins/qiskit/test_mock_backend.py index 7124a16cf8..a098b6f2d7 100644 --- a/test/python/plugins/qiskit/test_mock_backend.py +++ b/test/python/plugins/qiskit/test_mock_backend.py @@ -327,11 +327,14 @@ def test_custom_device(mock_qdmi_device_factory): return MockQDMIDevice -def _patch_registered_devices(monkeypatch: pytest.MonkeyPatch, devices: list[MockQDMIDevice]) -> None: - """Make the driver functions expose the given mock devices.""" +def _patch_client_devices(monkeypatch: pytest.MonkeyPatch, devices: list[MockQDMIDevice]) -> None: + """Make the Client factory expose the given mock devices.""" device_ids = [f"test.device.{index}" for index in range(len(devices))] devices_by_id = dict(zip(device_ids, devices, strict=True)) - monkeypatch.setattr("mqt.core.plugins.qiskit.provider.registered_device_ids", lambda: device_ids) + monkeypatch.setattr( + "mqt.core.plugins.qiskit.provider.QDMIProvider.device_ids", + staticmethod(lambda: device_ids), + ) monkeypatch.setattr( "mqt.core.plugins.qiskit.backend.open_device", lambda device_id, **_kwargs: devices_by_id[device_id], @@ -349,8 +352,8 @@ def test_backend_warns_on_unmappable_operation( operations=["cz", "custom_unmappable_gate", "measure"], ) - # Use helper to patch registered driver devices - _patch_registered_devices(monkeypatch, [mock_device]) + # Use helper to patch Client-visible devices + _patch_client_devices(monkeypatch, [mock_device]) # Creating backend should trigger warning about unmappable operation with warnings.catch_warnings(record=True) as w: @@ -378,8 +381,8 @@ def test_backend_warns_on_missing_measurement_operation( operations=["cz"], # No measure operation ) - # Use helper to patch registered driver devices - _patch_registered_devices(monkeypatch, [mock_device]) + # Use helper to patch Client-visible devices + _patch_client_devices(monkeypatch, [mock_device]) # Creating backend should trigger warning about missing measurement operation with warnings.catch_warnings(record=True) as w: @@ -826,8 +829,8 @@ def test_backend_validation_uses_inverse_mapping( operations=["prx", "cz", "measure"], # Uses 'prx' instead of 'r' ) - # Use helper to patch registered driver devices - _patch_registered_devices(monkeypatch, [mock_device]) + # Use helper to patch Client-visible devices + _patch_client_devices(monkeypatch, [mock_device]) provider = QDMIProvider() backend = provider.get_backend("Test Device with PRX") diff --git a/test/python/plugins/qiskit/test_provider.py b/test/python/plugins/qiskit/test_provider.py index 1de03677a8..91bbf2d87f 100644 --- a/test/python/plugins/qiskit/test_provider.py +++ b/test/python/plugins/qiskit/test_provider.py @@ -11,12 +11,13 @@ from __future__ import annotations import warnings +from types import SimpleNamespace import pytest from mqt.core.plugins.qiskit import QDMIBackend, QDMIProvider from mqt.core.plugins.qiskit.exceptions import UnsupportedDeviceError -from mqt.core.qdmi.driver import open_device +from mqt.core.qdmi import open_device def test_provider_backends_filter_by_name() -> None: @@ -74,8 +75,9 @@ def test_provider_get_backend_stops_after_exact_match(monkeypatch: pytest.Monkey expected = QDMIBackend(open_device("mqt.ddsim.default"), device_id="matching.device") opened_ids: list[str] = [] monkeypatch.setattr( - "mqt.core.plugins.qiskit.provider.registered_device_ids", - lambda: ["unavailable.device", "matching.device", "later.device"], + QDMIProvider, + "device_ids", + staticmethod(lambda: ["unavailable.device", "matching.device", "later.device"]), ) def lookup(device_id: str, **_session_parameters: object) -> QDMIBackend: @@ -106,7 +108,7 @@ def test_provider_get_backend_nonexistent() -> None: def test_provider_get_backend_no_devices(monkeypatch: pytest.MonkeyPatch) -> None: """Provider raises ValueError when no devices available.""" - monkeypatch.setattr("mqt.core.plugins.qiskit.provider.registered_device_ids", list) + monkeypatch.setattr(QDMIProvider, "device_ids", staticmethod(list)) provider = QDMIProvider() with pytest.raises(ValueError, match="No backend found with name"): @@ -130,7 +132,7 @@ def test_backend_has_provider_reference() -> None: def test_provider_default_constructor() -> None: - """Provider discovers registered devices without generic session parameters.""" + """Provider discovers Client-visible devices without session parameters.""" provider = QDMIProvider() backends = provider.backends() assert len(backends) > 0 @@ -139,7 +141,7 @@ def test_provider_default_constructor() -> None: def test_provider_construction_opens_no_devices(monkeypatch: pytest.MonkeyPatch) -> None: - """Constructing a provider does not initialize any registered device.""" + """Constructing a provider does not initialize any device.""" monkeypatch.setattr( "mqt.core.plugins.qiskit.provider.QDMIBackend.from_device_id", lambda *_args, **_kwargs: pytest.fail("provider construction opened a device"), @@ -147,10 +149,13 @@ def test_provider_construction_opens_no_devices(monkeypatch: pytest.MonkeyPatch) QDMIProvider() -def test_provider_reads_registry_on_each_discovery_call(monkeypatch: pytest.MonkeyPatch) -> None: - """A provider reflects registrations made after its construction.""" +def test_provider_reads_client_session_on_each_discovery_call(monkeypatch: pytest.MonkeyPatch) -> None: + """A provider starts a fresh Client session for each discovery call.""" device_ids = ["first.device"] - monkeypatch.setattr("mqt.core.plugins.qiskit.provider.registered_device_ids", lambda: list(device_ids)) + monkeypatch.setattr( + "mqt.core.plugins.qiskit.provider.ClientSession", + lambda: SimpleNamespace(devices=[SimpleNamespace(id=device_id) for device_id in device_ids]), + ) provider = QDMIProvider() assert provider.device_ids() == ["first.device"] @@ -187,8 +192,9 @@ def test_provider_warns_with_only_id_and_skips_unavailable_device(monkeypatch: p """Enumeration reports an unavailable ID without leaking failure details.""" available = QDMIBackend(open_device("mqt.ddsim.default"), device_id="available.device") monkeypatch.setattr( - "mqt.core.plugins.qiskit.provider.registered_device_ids", - lambda: ["available.device", "unavailable.device"], + QDMIProvider, + "device_ids", + staticmethod(lambda: ["available.device", "unavailable.device"]), ) def lookup(device_id: str, **_session_parameters: object) -> QDMIBackend: @@ -214,8 +220,9 @@ def test_provider_silently_skips_incompatible_device(monkeypatch: pytest.MonkeyP """Enumeration silently omits devices that Qiskit cannot represent.""" available = QDMIBackend(open_device("mqt.ddsim.default"), device_id="available.device") monkeypatch.setattr( - "mqt.core.plugins.qiskit.provider.registered_device_ids", - lambda: ["incompatible.device", "available.device"], + QDMIProvider, + "device_ids", + staticmethod(lambda: ["incompatible.device", "available.device"]), ) def lookup(device_id: str, **_session_parameters: object) -> QDMIBackend: diff --git a/test/python/qdmi/test_qdmi.py b/test/python/qdmi/test_qdmi.py index 9fef03e6f5..6a5ed4e7ff 100644 --- a/test/python/qdmi/test_qdmi.py +++ b/test/python/qdmi/test_qdmi.py @@ -10,10 +10,8 @@ from __future__ import annotations -import json import os from collections import Counter -from pathlib import Path from typing import cast import pytest @@ -21,30 +19,25 @@ from mqt.core.mlir import CompilerTarget, OutputFormat, compile_program from mqt.core.qdmi import ( + ClientSession, CustomProperty, Device, Job, ProgramFormat, is_binary_program_format, -) -from mqt.core.qdmi.driver import ( - DeviceDefinition, open_device, - register_device, - register_device_if_absent, - registered_device_ids, ) CustomValueType = type[str] | type[bool] | type[int] | type[float] | type[bytes] def _get_devices() -> list[Device]: - """Open all registered QDMI devices. + """Open all devices visible to a fresh Client session. Returns: List of all available QDMI devices. """ - return [open_device(device_id) for device_id in registered_device_ids()] + return ClientSession().devices @pytest.fixture(params=_get_devices()) @@ -107,6 +100,11 @@ def test_device_name(device: Device) -> None: assert len(name) > 0 +def test_device_id(device: Device) -> None: + """Test that each Client-visible device has a stable ID.""" + assert device.id + + def test_device_version(device: Device) -> None: """Test that the device version is a non-empty string.""" version = device.version() @@ -893,43 +891,9 @@ def test_simulator_job_get_sparse_probabilities_returns_valid_probabilities(simu assert sparse_probabilities["11"] == pytest.approx(0.5) -def test_register_device_does_not_load_nonexistent_library() -> None: - """Registration stores metadata and opening performs native loading.""" - library_path = Path("/nonexistent/lib.so") - definition = DeviceDefinition("python.missing", library_path, "PREFIX") - assert definition.device_id == "python.missing" - assert definition.library_path == library_path - assert definition.prefix == "PREFIX" - register_device(definition) - with pytest.raises(RuntimeError): - open_device("python.missing") - - -def test_register_device_if_absent_only_ignores_existing_id() -> None: - """Idempotent registration still validates duplicate definitions.""" - definition = DeviceDefinition("python.if-absent", "/nonexistent/device.so", "PREFIX") - assert register_device_if_absent(definition) - assert not register_device_if_absent(definition) - with pytest.raises(ValueError, match="library must not be empty"): - register_device_if_absent(DeviceDefinition("python.if-absent", "", "PREFIX")) - - -def test_registered_device_ids_include_runtime_registrations_in_order() -> None: - """Stable-ID enumeration is ordered and does not load native libraries.""" - ids_before = registered_device_ids() - register_device(DeviceDefinition("python.enumeration.first", "/nonexistent/first.so", "FIRST")) - register_device(DeviceDefinition("python.enumeration.second", "/nonexistent/second.so", "SECOND")) - - assert registered_device_ids() == [ - *ids_before, - "python.enumeration.first", - "python.enumeration.second", - ] - - def test_open_device_rejects_unknown_id() -> None: - """Opening requires a stable registered ID.""" - with pytest.raises(IndexError, match="Unknown QDMI device ID"): + """Opening requires a stable Client-visible ID.""" + with pytest.raises(IndexError, match="has no device with ID"): open_device("python.unknown") @@ -940,57 +904,6 @@ def test_open_device_creates_a_fresh_session() -> None: assert first != second -def test_device_configuration_arguments_are_mutually_exclusive() -> None: - """Typed device configuration must select exactly one source.""" - DeviceDefinition( - "python.inline-config", - "/nonexistent/device.so", - "PREFIX", - device_config="{}", - ) - DeviceDefinition( - "python.file-config", - "/nonexistent/device.so", - "PREFIX", - device_config_file="device.json", - ) - with pytest.raises(ValueError, match="mutually exclusive"): - DeviceDefinition( - "python.config-conflict", - "/nonexistent/device.so", - "PREFIX", - device_config="{}", - device_config_file="device.json", - ) - with pytest.raises(ValueError, match="mutually exclusive"): - open_device( - "mqt.sc.default", - device_config="{}", - device_config_file="device.json", - ) - - -def test_sc_open_device_accepts_runtime_configuration(tmp_path: Path) -> None: - """The built-in SC provider should materialize a per-open file model.""" - configuration = json.loads(Path("json/sc/mqt-core-qdmi-sc-device.json").read_text(encoding="utf-8")) - configuration["name"] = "Python custom SC device" - configuration["numQubits"] = 5 - configuration["couplings"] = [[0, 1], [1, 2], [2, 3], [3, 4]] - configuration["qubitProperties"]["overrides"] = [] - for operation in configuration["operations"]: - operation.pop("sites", None) - operation["siteOverrides"] = [] - configuration_file = tmp_path / "sc-device.json" - configuration_file.write_text(json.dumps(configuration), encoding="utf-8") - - device = open_device( - "mqt.sc.default", - device_config_file=configuration_file, - ) - assert device.name() == "Python custom SC device" - assert device.qubits_num() == 5 - - def test_site_keeps_fresh_session_alive() -> None: """A site should remain usable after its device wrapper is destroyed.""" site = open_device("mqt.sc.default").sites()[0] diff --git a/test/python/test_mlir.py b/test/python/test_mlir.py index 10cba46ce7..63275a15af 100644 --- a/test/python/test_mlir.py +++ b/test/python/test_mlir.py @@ -33,7 +33,7 @@ QIRProgram, compile_program, ) -from mqt.core.qdmi.driver import open_device +from mqt.core.qdmi import open_device requires_qiskit_translation = pytest.mark.skipif( not ( @@ -637,16 +637,10 @@ def test_compiler_target_from_device_id_matches_opened_device() -> None: assert _compiler_target_metadata(by_id) == _compiler_target_metadata(direct) -def test_compiler_target_from_device_id_preserves_open_and_conversion_errors() -> None: - """Stable-ID construction retains registry and target compatibility errors.""" - with pytest.raises(IndexError, match="Unknown QDMI device ID"): +def test_compiler_target_from_device_id_preserves_open_errors() -> None: + """Stable-ID construction retains Client lookup errors.""" + with pytest.raises(IndexError, match="has no device with ID"): CompilerTarget.from_device_id("unknown.device") - with pytest.raises(ValueError, match="mutually exclusive"): - CompilerTarget.from_device_id( - "mqt.ddsim.default", - device_config="{}", - device_config_file=Path("device.json"), - ) def test_qco_program_runs_textual_pipeline() -> None: diff --git a/test/qdmi/CMakeLists.txt b/test/qdmi/CMakeLists.txt index 1bba045502..5edfec1600 100644 --- a/test/qdmi/CMakeLists.txt +++ b/test/qdmi/CMakeLists.txt @@ -13,10 +13,74 @@ add_subdirectory(registry) set(TARGET_NAME mqt-core-qdmi-test) if(TARGET MQT::CoreQDMI) - package_add_test(${TARGET_NAME} MQT::CoreQDMI test_client.cpp test_slurm.cpp) + add_library(mqt-core-qdmi-incomplete-client SHARED client_runtime_driver.cpp) + target_link_libraries(mqt-core-qdmi-incomplete-client PRIVATE qdmi::qdmi) + target_compile_definitions(mqt-core-qdmi-incomplete-client + PRIVATE QDMI_driver_EXPORTS TEST_CLIENT_ABI=QDMI_CLIENT_ABI_VERSION) + add_library(mqt-core-qdmi-incompatible-client SHARED client_runtime_driver.cpp) + target_link_libraries(mqt-core-qdmi-incompatible-client PRIVATE qdmi::qdmi) + target_compile_definitions(mqt-core-qdmi-incompatible-client + PRIVATE QDMI_driver_EXPORTS TEST_CLIENT_ABI=QDMI_MAKE_VERSION\(1,3,0\)) + add_library(mqt-core-qdmi-fake-client SHARED client_runtime_driver.cpp) + target_link_libraries(mqt-core-qdmi-fake-client PRIVATE qdmi::qdmi) + target_compile_definitions( + mqt-core-qdmi-fake-client PRIVATE QDMI_driver_EXPORTS TEST_CLIENT_ABI=QDMI_CLIENT_ABI_VERSION + TEST_FULL_CLIENT) + + if(WIN32) + # Discover tests after the Driver and device runtime copies finish. + set(CMAKE_GTEST_DISCOVER_TESTS_DISCOVERY_MODE PRE_TEST) + endif() + package_add_test(mqt-core-qdmi-client-runtime-test MQT::CoreQDMI test_client_runtime.cpp) + if(WIN32) + unset(CMAKE_GTEST_DISCOVER_TESTS_DISCOVERY_MODE) + endif() target_compile_definitions( - ${TARGET_NAME} - PRIVATE "MQT_CORE_QDMI_SLURM_TEST_DEVICE=\"$\"") + mqt-core-qdmi-client-runtime-test + PRIVATE "MQT_CORE_QDMI_TEST_DRIVER=\"$\"" + "MQT_CORE_QDMI_INCOMPLETE_DRIVER=\"$\"" + "MQT_CORE_QDMI_INCOMPATIBLE_DRIVER=\"$\"" + ) + add_dependencies(mqt-core-qdmi-client-runtime-test mqt-core-qdmi-incomplete-client + mqt-core-qdmi-incompatible-client mqt-core-qdmi-fake-client) + mqt_copy_qdmi_runtime(mqt-core-qdmi-client-runtime-test) + + add_executable(mqt-core-qdmi-packaged-runtime-test test_packaged_runtime.cpp) + target_link_libraries(mqt-core-qdmi-packaged-runtime-test PRIVATE MQT::CoreQDMI + MQT::ProjectWarnings) + target_compile_definitions( + mqt-core-qdmi-packaged-runtime-test + PRIVATE "MQT_CORE_QDMI_TEST_DRIVER_FILENAME=\"$\"") + mqt_copy_qdmi_runtime(mqt-core-qdmi-packaged-runtime-test) + add_test( + NAME mqt-core-qdmi-packaged-runtime + COMMAND + ${CMAKE_COMMAND} -E env --unset=MQT_CORE_QDMI_DRIVER -- ${CMAKE_COMMAND} -E chdir + "$" + "./$") + + if(WIN32) + # Discover tests after the Driver and device runtime copies finish. + set(CMAKE_GTEST_DISCOVER_TESTS_DISCOVERY_MODE PRE_TEST) + endif() + package_add_test(${TARGET_NAME} MQT::CoreQDMI test_client.cpp test_slurm.cpp) + if(WIN32) + unset(CMAKE_GTEST_DISCOVER_TESTS_DISCOVERY_MODE) + endif() + set(client_test_config "$/client-test-catalog.json") + file( + GENERATE + OUTPUT "${client_test_config}" + CONTENT + "{\n \"schema-version\": 1,\n \"qdmi\": {\n \"devices\": [\n {\"id\": \"test.unsupported-program-formats\", \"library\": \"$\", \"prefix\": \"TEST_SESSION\"},\n {\"id\": \"test.slurm.idle\", \"library\": \"$\", \"prefix\": \"TEST_SESSION\", \"session\": {\"custom4\": \"idle\"}},\n {\"id\": \"test.slurm.busy\", \"library\": \"$\", \"prefix\": \"TEST_SESSION\", \"session\": {\"custom4\": \"busy\"}},\n {\"id\": \"test.slurm.grammar\", \"library\": \"$\", \"prefix\": \"TEST_SESSION\", \"session\": {\"custom4\": \"idle\"}},\n {\"id\": \"test.slurm.single\", \"library\": \"$\", \"prefix\": \"TEST_SESSION\", \"session\": {\"custom4\": \"idle\"}},\n {\"id\": \"test.slurm.offline\", \"library\": \"$\", \"prefix\": \"TEST_SESSION\", \"session\": {\"custom4\": \"offline\"}},\n {\"id\": \"test.slurm.error\", \"library\": \"$\", \"prefix\": \"TEST_SESSION\", \"session\": {\"custom4\": \"error\"}},\n {\"id\": \"test.slurm.maintenance\", \"library\": \"$\", \"prefix\": \"TEST_SESSION\", \"session\": {\"custom4\": \"maintenance\"}},\n {\"id\": \"test.slurm.calibration\", \"library\": \"$\", \"prefix\": \"TEST_SESSION\", \"session\": {\"custom4\": \"calibration\"}},\n {\"id\": \"test.slurm.max\", \"library\": \"$\", \"prefix\": \"TEST_SESSION\", \"session\": {\"custom4\": \"max\"}}\n ]\n }\n}\n" + ) + add_custom_command( + TARGET ${TARGET_NAME} + POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different "$" + "$") + target_compile_definitions(${TARGET_NAME} + PRIVATE "MQT_CORE_QDMI_CLIENT_TEST_CONFIG=\"${client_test_config}\"") if(TARGET MQT::CoreQDMI_DDSIM_Device) target_compile_definitions(${TARGET_NAME} PRIVATE MQT_CORE_QDMI_HAS_DDSIM_DEVICE) endif() diff --git a/test/qdmi/client_runtime_driver.cpp b/test/qdmi/client_runtime_driver.cpp new file mode 100644 index 0000000000..0fa2f5fd20 --- /dev/null +++ b/test/qdmi/client_runtime_driver.cpp @@ -0,0 +1,310 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include + +#include + +#ifdef TEST_FULL_CLIENT +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef _WIN32 +/// NOLINTNEXTLINE(modernize-deprecated-headers) +#include +#endif + +struct QDMI_Site_impl_d { + size_t index = 0; +}; + +struct QDMI_Operation_impl_d {}; + +struct QDMI_Session_impl_d; + +struct QDMI_Device_impl_d { + QDMI_Session_impl_d* session; + QDMI_Site_impl_d site; + QDMI_Operation_impl_d operation; +}; + +struct QDMI_Session_impl_d { + std::string token; + bool initialized = false; + QDMI_Device_impl_d device{.session = this}; +}; + +namespace { +constexpr auto DEVICE_ID = "test.fake.client"; +constexpr auto FAIL_ALLOCATION = "MQT_CORE_QDMI_FAKE_FAIL_ALLOCATION"; + +[[nodiscard]] auto forcedAllocationStatus() -> std::optional { + const auto* value = std::getenv(FAIL_ALLOCATION); + if (value == nullptr || std::string_view{value} == "0") { + return std::nullopt; + } + const std::string requested{value}; +#ifdef _WIN32 + static_cast(_putenv_s(FAIL_ALLOCATION, "0")); +#else + static_cast(setenv(FAIL_ALLOCATION, "0", 1)); +#endif + if (requested == "1") { + return QDMI_ERROR_OUTOFMEM; + } + if (requested == "success-null") { + return QDMI_SUCCESS; + } + if (requested == "warning-null") { + return QDMI_WARN_GENERAL; + } + return std::nullopt; +} + +auto queryString(const std::string_view result, const size_t size, void* value, + size_t* sizeRet) -> int { + const auto required = result.size() + 1U; + if (sizeRet != nullptr) { + *sizeRet = required; + } + if (value == nullptr) { + return QDMI_SUCCESS; + } + if (size < required) { + return QDMI_ERROR_INVALIDARGUMENT; + } + std::memcpy(value, result.data(), result.size()); + const std::span output{static_cast(value), size}; + output[result.size()] = '\0'; + return QDMI_SUCCESS; +} + +template +auto queryValues(const std::span result, const size_t size, + void* value, size_t* sizeRet) -> int { + const auto required = result.size_bytes(); + if (sizeRet != nullptr) { + *sizeRet = required; + } + if (value == nullptr) { + return QDMI_SUCCESS; + } + if (size < required) { + return QDMI_ERROR_INVALIDARGUMENT; + } + if (!result.empty()) { + std::memcpy(value, static_cast(result.data()), required); + } + return QDMI_SUCCESS; +} + +template +auto queryValue(const T& result, const size_t size, void* value, + size_t* sizeRet) -> int { + return queryValues(std::span{std::addressof(result), 1U}, size, + value, sizeRet); +} +} // namespace +#endif + +/// NOLINTBEGIN(readability-identifier-naming, readability-named-parameter) +uint32_t QDMI_driver_get_client_abi_version() { return TEST_CLIENT_ABI; } + +#ifdef TEST_FULL_CLIENT +int QDMI_session_alloc(QDMI_Session* session) { + if (session == nullptr) { + return QDMI_ERROR_INVALIDARGUMENT; + } + *session = nullptr; + if (const auto status = forcedAllocationStatus()) { + return *status; + } + /// NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + *session = new (std::nothrow) QDMI_Session_impl_d; + return *session == nullptr ? QDMI_ERROR_OUTOFMEM : QDMI_SUCCESS; +} + +int QDMI_session_set_parameter(QDMI_Session session, + const QDMI_Session_Parameter param, + const size_t size, const void* value) { + if (session == nullptr) { + return QDMI_ERROR_INVALIDARGUMENT; + } + if (session->initialized) { + return QDMI_ERROR_BADSTATE; + } + if (param != QDMI_SESSION_PARAMETER_TOKEN) { + return QDMI_ERROR_NOTSUPPORTED; + } + if (value == nullptr) { + return QDMI_SUCCESS; + } + const std::string_view text{static_cast(value), size}; + if (text.empty() || text.back() != '\0' || + text.substr(0U, text.size() - 1U).find('\0') != std::string_view::npos) { + return QDMI_ERROR_INVALIDARGUMENT; + } + try { + session->token.assign(text.data(), text.size() - 1U); + } catch (const std::bad_alloc&) { + return QDMI_ERROR_OUTOFMEM; + } + return QDMI_SUCCESS; +} + +int QDMI_session_init(QDMI_Session session) { + if (session == nullptr) { + return QDMI_ERROR_INVALIDARGUMENT; + } + if (session->initialized) { + return QDMI_ERROR_BADSTATE; + } + session->initialized = true; + return QDMI_SUCCESS; +} + +int QDMI_session_query_session_property(QDMI_Session session, + const QDMI_Session_Property prop, + const size_t size, void* value, + size_t* sizeRet) { + if (session == nullptr || !session->initialized) { + return QDMI_ERROR_BADSTATE; + } + if (prop != QDMI_SESSION_PROPERTY_DEVICES) { + return QDMI_ERROR_NOTSUPPORTED; + } + if (session->token == "odd-size") { + if (sizeRet != nullptr) { + *sizeRet = sizeof(QDMI_Device) + 1U; + } + return value == nullptr ? QDMI_SUCCESS : QDMI_ERROR_INVALIDARGUMENT; + } + QDMI_Device device = &session->device; + return queryValue(device, size, value, sizeRet); +} + +void QDMI_session_free(QDMI_Session session) { + /// NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + delete session; +} + +int QDMI_device_query_device_property(QDMI_Device device, + const QDMI_Device_Property prop, + const size_t size, void* value, + size_t* sizeRet) { + if (device == nullptr || device->session == nullptr || + !device->session->initialized) { + return QDMI_ERROR_INVALIDARGUMENT; + } + if (prop == QDMI_DEVICE_PROPERTY_ID) { + return queryString(DEVICE_ID, size, value, sizeRet); + } + if (prop == QDMI_DEVICE_PROPERTY_NAME) { + return queryString(device->session->token, size, value, sizeRet); + } + if (prop == QDMI_DEVICE_PROPERTY_QUBITSNUM) { + constexpr size_t qubits = 1U; + return queryValue(qubits, size, value, sizeRet); + } + if (prop == QDMI_DEVICE_PROPERTY_SITES) { + if (device->session->token == "odd-device-size") { + if (sizeRet != nullptr) { + *sizeRet = sizeof(QDMI_Site) + 1U; + } + return value == nullptr ? QDMI_SUCCESS : QDMI_ERROR_INVALIDARGUMENT; + } + QDMI_Site site = &device->site; + return queryValue(site, size, value, sizeRet); + } + if (prop == QDMI_DEVICE_PROPERTY_OPERATIONS) { + QDMI_Operation operation = &device->operation; + return queryValue(operation, size, value, sizeRet); + } + return QDMI_ERROR_NOTSUPPORTED; +} + +int QDMI_device_query_site_property(QDMI_Device device, QDMI_Site site, + const QDMI_Site_Property prop, + const size_t size, void* value, + size_t* sizeRet) { + if (device == nullptr || site != &device->site) { + return QDMI_ERROR_INVALIDARGUMENT; + } + if (prop != QDMI_SITE_PROPERTY_INDEX) { + return QDMI_ERROR_NOTSUPPORTED; + } + return queryValue(site->index, size, value, sizeRet); +} + +int QDMI_device_query_operation_property( + QDMI_Device device, QDMI_Operation operation, size_t, const QDMI_Site*, + size_t, const double*, const QDMI_Operation_Property prop, const size_t, + // NOLINTNEXTLINE(misc-const-correctness): QDMI C ABI output. + void* value, size_t* sizeRet) { + if (device == nullptr || operation != &device->operation) { + return QDMI_ERROR_INVALIDARGUMENT; + } + if (prop == QDMI_OPERATION_PROPERTY_SITES && + device->session->token == "odd-operation-size") { + if (sizeRet != nullptr) { + *sizeRet = sizeof(QDMI_Site) + 1U; + } + return value == nullptr ? QDMI_SUCCESS : QDMI_ERROR_INVALIDARGUMENT; + } + return QDMI_ERROR_NOTSUPPORTED; +} + +int QDMI_device_create_job(QDMI_Device, QDMI_Job* job) { + if (job != nullptr) { + *job = nullptr; + } + return QDMI_ERROR_NOTSUPPORTED; +} + +int QDMI_session_retrieve_job_by_id(QDMI_Device, const char*, QDMI_Job* job) { + if (job != nullptr) { + *job = nullptr; + } + return QDMI_ERROR_NOTSUPPORTED; +} + +int QDMI_job_set_parameter(QDMI_Job, QDMI_Job_Parameter, size_t, const void*) { + return QDMI_ERROR_NOTSUPPORTED; +} + +int QDMI_job_query_property(QDMI_Job, QDMI_Job_Property, size_t, void*, + size_t*) { + return QDMI_ERROR_NOTSUPPORTED; +} + +int QDMI_job_submit(QDMI_Job) { return QDMI_ERROR_NOTSUPPORTED; } + +int QDMI_job_cancel(QDMI_Job) { return QDMI_ERROR_NOTSUPPORTED; } + +int QDMI_job_check(QDMI_Job, QDMI_Job_Status*) { + return QDMI_ERROR_NOTSUPPORTED; +} + +int QDMI_job_wait(QDMI_Job, size_t) { return QDMI_ERROR_NOTSUPPORTED; } + +int QDMI_job_get_results(QDMI_Job, QDMI_Job_Result, size_t, void*, size_t*) { + return QDMI_ERROR_NOTSUPPORTED; +} + +void QDMI_job_free(QDMI_Job) {} +#endif +/// NOLINTEND(readability-identifier-naming, readability-named-parameter) diff --git a/test/qdmi/driver/CMakeLists.txt b/test/qdmi/driver/CMakeLists.txt index 638daead94..325e9c3d04 100644 --- a/test/qdmi/driver/CMakeLists.txt +++ b/test/qdmi/driver/CMakeLists.txt @@ -38,7 +38,14 @@ if(TARGET MQT::CoreQDMIDriver) PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/device" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/device") + if(WIN32) + # Discover tests after the Driver and device runtime copies finish. + set(CMAKE_GTEST_DISCOVER_TESTS_DISCOVERY_MODE PRE_TEST) + endif() package_add_test(${TARGET_NAME} MQT::CoreQDMIDriver test_driver.cpp) + if(WIN32) + unset(CMAKE_GTEST_DISCOVER_TESTS_DISCOVERY_MODE) + endif() target_link_libraries(${TARGET_NAME} PRIVATE MQT::CoreQDMI) set(DIAGNOSTIC_TARGET_NAME mqt-core-qdmi-driver-diagnostic-test) @@ -70,19 +77,49 @@ if(TARGET MQT::CoreQDMIDriver) target_compile_definitions(${TARGET_NAME} PRIVATE MQT_CORE_QDMI_HAS_DDSIM_DEVICE) endif() - set(config_file "${CMAKE_CURRENT_BINARY_DIR}/$/configured-devices.json") - file( - GENERATE - OUTPUT "${config_file}" - CONTENT - "{\n \"schema-version\": 1,\n \"qdmi\": {\n \"devices\": [\n${configured_devices},\n {\"id\": \"test.disabled\", \"enabled\": false},\n {\"id\": \"broken.example\", \"library\": \"missing-device-library\", \"prefix\": \"BROKEN\"}\n ]\n }\n}\n" - ) file(READ "${PROJECT_SOURCE_DIR}/json/sc/mqt-core-qdmi-sc-device.json" custom_sc_json) string(REPLACE "MQT SC Default QDMI Device" "Custom SC Driver Device" custom_sc_json "${custom_sc_json}") string(REPLACE "\"duration\": 20" "\"duration\": 77" custom_sc_json "${custom_sc_json}") set(custom_sc_file "${CMAKE_CURRENT_BINARY_DIR}/custom-sc-device.json") file(WRITE "${custom_sc_file}" "${custom_sc_json}") + set(runtime_one_file "${CMAKE_CURRENT_BINARY_DIR}/runtime-one.json") + file( + GENERATE + OUTPUT "${runtime_one_file}" + CONTENT + [=[{ + "schema-version": 1, + "name": "SC runtime one", + "numQubits": 1, + "durationUnit": {"unit": "ns", "scaleFactor": 1}, + "qubitProperties": {"defaults": {"t1": 10, "t2": 20}, "overrides": []}, + "couplings": [], + "operations": [{"name": "r", "numParameters": 2, "numQubits": 1, "duration": 7, "fidelity": 0.8}] +} +]=]) + set(runtime_two_file "${CMAKE_CURRENT_BINARY_DIR}/runtime-two.json") + file( + GENERATE + OUTPUT "${runtime_two_file}" + CONTENT + [=[{ + "schema-version": 1, + "name": "SC runtime two", + "numQubits": 2, + "durationUnit": {"unit": "us", "scaleFactor": 0.5}, + "qubitProperties": {"defaults": {}, "overrides": []}, + "couplings": [[1, 0]], + "operations": [] +} +]=]) + set(config_file "${CMAKE_CURRENT_BINARY_DIR}/$/configured-devices.json") + file( + GENERATE + OUTPUT "${config_file}" + CONTENT + "{\n \"schema-version\": 1,\n \"qdmi\": {\n \"devices\": [\n${configured_devices},\n {\"id\": \"test.session-overrides\", \"library\": \"$\", \"prefix\": \"TEST_SESSION\", \"session\": {\"base-url\": \"registered-base\", \"token\": \"registered-token\", \"custom1\": \"registered-custom\"}},\n {\"id\": \"test.session-with-child\", \"library\": \"$\", \"prefix\": \"TEST_SESSION\", \"session\": {\"custom5\": \"with-child\"}},\n {\"id\": \"test.typed-configuration\", \"library\": \"$\", \"prefix\": \"TEST_SESSION\", \"session\": {\"device-config\": {\"inline\": {\"name\": \"inline\"}}}},\n {\"id\": \"test.sc.runtime-default\", \"library\": \"$\", \"prefix\": \"MQT_SC\"},\n {\"id\": \"test.sc.runtime-custom\", \"library\": \"$\", \"prefix\": \"MQT_SC\", \"session\": {\"device-config\": {\"file\": \"${custom_sc_file}\"}}},\n {\"id\": \"test.sc.runtime-invalid\", \"library\": \"$\", \"prefix\": \"MQT_SC\", \"session\": {\"device-config\": {\"inline\": {}}}},\n {\"id\": \"test.sc.runtime-one\", \"library\": \"$\", \"prefix\": \"MQT_SC\", \"session\": {\"device-config\": {\"file\": \"${runtime_one_file}\"}}},\n {\"id\": \"test.sc.runtime-two\", \"library\": \"$\", \"prefix\": \"MQT_SC\", \"session\": {\"device-config\": {\"file\": \"${runtime_two_file}\"}}},\n {\"id\": \"test.disabled\", \"enabled\": false},\n {\"id\": \"broken.example\", \"library\": \"missing-device-library\", \"prefix\": \"BROKEN\"}\n ]\n }\n}\n" + ) string(MAKE_C_IDENTIFIER "${TARGET_NAME}-mqt-core-qdmi-metadata-device" metadata_manifest_stem) set(metadata_manifest_file "$/${metadata_manifest_stem}.qdmi.json") diff --git a/test/qdmi/driver/test_driver.cpp b/test/qdmi/driver/test_driver.cpp index cb14aecea3..637c7ee56e 100644 --- a/test/qdmi/driver/test_driver.cpp +++ b/test/qdmi/driver/test_driver.cpp @@ -241,18 +241,24 @@ class ChildDeviceLibrary final : public qdmi::DeviceLibrary { } }; -[[nodiscard]] auto queryName(QDMI_Device_impl_d* const device) -> std::string { +[[nodiscard]] auto queryTextProperty(QDMI_Device_impl_d* const device, + const QDMI_Device_Property property) + -> std::string { size_t size = 0; - EXPECT_EQ(QDMI_device_query_device_property(device, QDMI_DEVICE_PROPERTY_NAME, - 0, nullptr, &size), - QDMI_SUCCESS); + EXPECT_EQ( + QDMI_device_query_device_property(device, property, 0, nullptr, &size), + QDMI_SUCCESS); std::string name(size - 1, '\0'); - EXPECT_EQ(QDMI_device_query_device_property(device, QDMI_DEVICE_PROPERTY_NAME, - size, name.data(), nullptr), + EXPECT_EQ(QDMI_device_query_device_property(device, property, size, + name.data(), nullptr), QDMI_SUCCESS); return name; } +[[nodiscard]] auto queryName(QDMI_Device_impl_d* const device) -> std::string { + return queryTextProperty(device, QDMI_DEVICE_PROPERTY_NAME); +} + [[nodiscard]] auto openTestDevice(const std::string& library, const std::string& prefix, const qdmi::DeviceSessionConfig& session = {}) @@ -357,12 +363,6 @@ TEST(ChildDeviceTest, WrapsOpaqueHandlesInStableClientDevices) { EXPECT_EQ(queryName(children[0]), "child-0"); EXPECT_EQ(queryName(children[1]), "child-1"); - const auto qdmiChildren = - qdmi::Session::createSessionlessDevice(&parent).getChildDevices(); - ASSERT_EQ(qdmiChildren.size(), 2); - EXPECT_EQ(qdmiChildren[0].getName(), "child-0"); - EXPECT_EQ(qdmiChildren[1].getName(), "child-1"); - std::array repeatedQuery{}; ASSERT_EQ(QDMI_device_query_device_property( &parent, QDMI_DEVICE_PROPERTY_CHILDDEVICES, size, @@ -434,6 +434,10 @@ TEST_P(DriverTest, SessionSetParameter) { QDMI_SESSION_PARAMETER_AUTHFILE, 13, authFile.c_str()), QDMI_ERROR_NOTSUPPORTED); + EXPECT_EQ(QDMI_session_set_parameter(uninitializedSession, + QDMI_SESSION_PARAMETER_CUSTOM1, 0, + nullptr), + QDMI_ERROR_NOTSUPPORTED); EXPECT_EQ(QDMI_session_set_parameter(uninitializedSession, QDMI_SESSION_PARAMETER_MAX, 0, nullptr), QDMI_ERROR_INVALIDARGUMENT); @@ -901,17 +905,21 @@ TEST(ConfiguredDriverTest, ExposesWorkingDefinitionsAndIsolatesFailures) { static_cast(devices.data()), nullptr), QDMI_SUCCESS); - std::vector names; - std::ranges::transform(devices, std::back_inserter(names), queryName); - std::vector expectedNames{ - "IQM Emerald", - "IQM Garnet", - "MQT SC Default QDMI Device", + std::vector ids; + std::ranges::transform(devices, std::back_inserter(ids), [](auto device) { + return queryTextProperty(device, QDMI_DEVICE_PROPERTY_ID); + }); + std::vector expectedIds{ + "mqt.sc.default", "mqt.sc.iqm.emerald", + "mqt.sc.iqm.garnet", "test.sc.runtime-default", + "test.sc.runtime-custom", "test.sc.runtime-one", + "test.sc.runtime-two", "test.session-overrides", + "test.session-with-child", "test.typed-configuration", }; #ifdef MQT_CORE_QDMI_HAS_DDSIM_DEVICE - expectedNames.emplace_back("MQT Core DDSIM QDMI Device"); + expectedIds.emplace_back("mqt.ddsim.default"); #endif - EXPECT_THAT(names, testing::UnorderedElementsAreArray(expectedNames)); + EXPECT_THAT(ids, testing::UnorderedElementsAreArray(expectedIds)); QDMI_session_free(session); } @@ -926,6 +934,10 @@ TEST(DeviceRegistrationTest, ValidatesDuplicatesAndReplacement) { .library = library, .prefix = prefix, }; + auto invalidId = original; + invalidId.id = std::string{"test.alias\0hidden", 17}; + EXPECT_THROW(driver.registerDevice(std::move(invalidId)), + std::invalid_argument); driver.registerDevice(original); EXPECT_THROW(driver.registerDevice(original), std::invalid_argument); @@ -1073,47 +1085,6 @@ TEST(DeviceRegistrationTest, SynthesizesManifestForMetadataOnlyTarget) { EXPECT_THAT(contents, testing::HasSubstr("mqt-core-qdmi-metadata-device")); } -TEST(DeviceRegistrationTest, - FreshOverridesMergeValuesOwnTheirSessionAndStayOutOfCatalog) { - registerSessionTestDevice(); - - const auto clientCatalogSize = [] { - QDMI_Session session = nullptr; - if (QDMI_session_alloc(&session) != QDMI_SUCCESS || - QDMI_session_init(session) != QDMI_SUCCESS) { - throw std::runtime_error("Failed to create QDMI test session"); - } - size_t size = 0; - const auto status = QDMI_session_query_session_property( - session, QDMI_SESSION_PROPERTY_DEVICES, 0, nullptr, &size); - QDMI_session_free(session); - if (status != QDMI_SUCCESS) { - throw std::runtime_error("Failed to query QDMI device catalog"); - } - return size; - }; - - const auto catalogSizeBefore = clientCatalogSize(); - { - qdmi::DeviceSessionConfig overrides; - overrides.token = "override-token"; - overrides.custom2 = "override-custom"; - auto const device = - qdmi::Session::openDevice("test.session-overrides", overrides); - EXPECT_EQ(device.getName(), - "base=registered-base;token=override-token;custom1=" - "registered-custom;custom2=override-custom;active=1"); - EXPECT_EQ(clientCatalogSize(), catalogSizeBefore); - } - - qdmi::DeviceSessionConfig probeOverrides; - probeOverrides.token = "probe-token"; - const auto probe = - qdmi::Session::openDevice("test.session-overrides", probeOverrides); - EXPECT_THAT(queryName(probe), testing::HasSubstr("active=1")); - EXPECT_EQ(clientCatalogSize(), catalogSizeBefore); -} - TEST(DeviceRegistrationTest, TypedConfigurationUsesExactlyOneAdapterSlot) { static_cast(qdmi::Driver::get().registerDeviceIfAbsent({ .id = "test.typed-configuration", @@ -1133,14 +1104,6 @@ TEST(DeviceRegistrationTest, TypedConfigurationUsesExactlyOneAdapterSlot) { EXPECT_THAT( inlineDevice.getName(), testing::HasSubstr(R"(custom1={"name":"inline"};custom2=)")); - - qdmi::DeviceSessionConfig fileOverrides; - fileOverrides.deviceConfiguration = - qdmi::FileDeviceConfiguration{.path = "device.json"}; - const auto fileDevice = - qdmi::Session::openDevice("test.typed-configuration", fileOverrides); - EXPECT_THAT(fileDevice.getName(), - testing::HasSubstr("custom1=;custom2=device.json")); } TEST(DeviceRegistrationTest, TypedConfigurationRejectsRawAdapterSlotConflict) { @@ -1157,15 +1120,6 @@ TEST(DeviceRegistrationTest, TypedConfigurationRejectsRawAdapterSlotConflict) { }, }; EXPECT_THROW(driver.registerDevice(definition), std::invalid_argument); - - registerSessionTestDevice(); - qdmi::DeviceSessionConfig overrides; - overrides.deviceConfiguration = - qdmi::FileDeviceConfiguration{.path = "device.json"}; - overrides.custom2 = "raw"; - EXPECT_THROW(static_cast(qdmi::Session::openDevice( - "test.session-overrides", overrides)), - std::invalid_argument); } TEST(DeviceRegistrationTest, @@ -1199,14 +1153,6 @@ TEST(DeviceRegistrationTest, EXPECT_EQ(defaultDevice.getOperations().front().getDuration(), 20); EXPECT_EQ(customDevice.getOperations().front().getDuration(), 77); - qdmi::DeviceSessionConfig overrides; - overrides.deviceConfiguration = - qdmi::FileDeviceConfiguration{.path = MQT_CORE_QDMI_DEFAULT_SC_FILE}; - const auto overridden = - qdmi::Session::openDevice("test.sc.runtime-custom", overrides); - EXPECT_EQ(overridden.getName(), "MQT SC Default QDMI Device"); - EXPECT_EQ(overridden.getOperations().front().getDuration(), 20); - static_cast(driver.registerDeviceIfAbsent({ .id = "test.sc.runtime-invalid", .library = MQT_CORE_QDMI_SC_LIBRARY, @@ -1214,12 +1160,14 @@ TEST(DeviceRegistrationTest, .session = { .deviceConfiguration = - qdmi::InlineDeviceConfiguration{.json = "{}"}, + qdmi::InlineDeviceConfiguration{ + .json = "{}", + }, }, })); EXPECT_THROW( static_cast(qdmi::Session::openDevice("test.sc.runtime-invalid")), - std::runtime_error); + std::out_of_range); EXPECT_EQ(qdmi::Session::openDevice("test.sc.runtime-default").getName(), "MQT SC Default QDMI Device"); } @@ -1367,30 +1315,15 @@ TEST(DeviceRegistrationTest, const auto secondCouplingMap = second.getCouplingMap(); ASSERT_TRUE(secondCouplingMap.has_value()); ASSERT_EQ(secondCouplingMap->size(), 1); - - qdmi::DeviceSessionConfig configurationOverride; - configurationOverride.deviceConfiguration = qdmi::InlineDeviceConfiguration{ - .json = R"({ - "schema-version":1, - "name":"SC per-open override", - "numQubits":3, - "durationUnit":{"unit":"ms","scaleFactor":2}, - "qubitProperties":{"defaults":{},"overrides":[]}, - "couplings":[[0,2]], - "operations":[] - })", - }; - const auto overridden = - qdmi::Session::openDevice("test.sc.runtime-one", configurationOverride); - EXPECT_EQ(overridden.getName(), "SC per-open override"); - EXPECT_EQ(overridden.getQubitsNum(), 3); } TEST(DeviceRegistrationTest, FreshJobRetainsItsDeviceSession) { registerSessionTestDevice(); std::optional job; + std::string baseline; { auto const device = qdmi::Session::openDevice("test.session-overrides"); + baseline = device.getName(); job.emplace( device.submitJob("OPENQASM 2.0;", QDMI_PROGRAM_FORMAT_QASM2, 1)); } @@ -1400,7 +1333,7 @@ TEST(DeviceRegistrationTest, FreshJobRetainsItsDeviceSession) { job.reset(); const auto probe = qdmi::Session::openDevice("test.session-overrides"); - EXPECT_THAT(queryName(probe), testing::HasSubstr("active=1")); + EXPECT_EQ(queryName(probe), baseline); } TEST(DeviceRegistrationTest, CustomBinaryJobDoesNotRequireShots) { @@ -1492,27 +1425,6 @@ TEST(DeviceRegistrationTest, RetrievesExistingJobs) { EXPECT_EQ(retrievedJob.getId(), "session-job"); } -TEST(DeviceRegistrationTest, FreshChildDeviceRetainsItsRootSession) { - registerSessionTestDevice(); - std::optional child; - { - qdmi::DeviceSessionConfig overrides; - overrides.custom5 = "with-child"; - auto const root = - qdmi::Session::openDevice("test.session-overrides", overrides); - auto children = root.getChildDevices(); - ASSERT_EQ(children.size(), 1); - child.emplace(std::move(children.front())); - } - - ASSERT_TRUE(child.has_value()); - EXPECT_EQ(child->getName(), "child;active=2"); - child.reset(); - - const auto probe = qdmi::Session::openDevice("test.session-overrides"); - EXPECT_THAT(queryName(probe), testing::HasSubstr("active=1")); -} - TEST(DeviceRegistrationTest, RuntimeRegistrationsStayOutOfClientCatalog) { auto& driver = qdmi::Driver::get(); QDMI_Session existingSession = nullptr; diff --git a/test/qdmi/registry/CMakeLists.txt b/test/qdmi/registry/CMakeLists.txt index 3bf9e19f70..0008a4004a 100644 --- a/test/qdmi/registry/CMakeLists.txt +++ b/test/qdmi/registry/CMakeLists.txt @@ -9,7 +9,14 @@ set(TARGET_NAME mqt-core-qdmi-registry-test) if(TARGET MQT::CoreQDMIDriver) + if(WIN32) + # Discover tests after the Driver and device runtime copies finish. + set(CMAKE_GTEST_DISCOVER_TESTS_DISCOVERY_MODE PRE_TEST) + endif() package_add_test(${TARGET_NAME} MQT::CoreQDMIDriver test_device_registry.cpp) + if(WIN32) + unset(CMAKE_GTEST_DISCOVER_TESTS_DISCOVERY_MODE) + endif() target_include_directories(${TARGET_NAME} PRIVATE ${PROJECT_SOURCE_DIR}/src/qdmi/driver ${PROJECT_SOURCE_DIR}/test) if(TARGET MQT::CoreQDMI_DDSIM_Device) diff --git a/test/qdmi/registry/test_device_registry.cpp b/test/qdmi/registry/test_device_registry.cpp index eb69898c1c..12297a08e5 100644 --- a/test/qdmi/registry/test_device_registry.cpp +++ b/test/qdmi/registry/test_device_registry.cpp @@ -138,6 +138,20 @@ TEST(DeviceRegistry, RejectsDuplicateIdsAndUnsupportedKeys) { } } +TEST(DeviceRegistry, RejectsIdsWithEmbeddedNul) { + const TemporaryDirectory directory; + const auto configFile = emptyConfig(directory); + const ScopedEnvironmentVariable configJson("MQT_CORE_QDMI_CONFIG_JSON", R"({ + "schema-version": 1, + "qdmi": {"devices": [{ + "id": "test.alias\u0000hidden", "library": "device", "prefix": "TEST" + }]} + })"); + + EXPECT_THROW(static_cast(qdmi::detail::DeviceRegistry()), + std::invalid_argument); +} + TEST(DeviceRegistry, MergesEnvironmentJsonOverExplicitFile) { const TemporaryDirectory directory; const auto path = directory.write("environment.json", R"({ diff --git a/test/qdmi/test_client.cpp b/test/qdmi/test_client.cpp index dc42231615..dbf8413c6b 100644 --- a/test/qdmi/test_client.cpp +++ b/test/qdmi/test_client.cpp @@ -14,21 +14,21 @@ #include #include #include +/// POSIX declares setenv in . +/// NOLINTNEXTLINE(modernize-deprecated-headers) +#include #include #include #include #include #include -#include -#include #include #include #include #include #include #include -#include #include #include @@ -36,6 +36,24 @@ namespace qdmi { namespace { +struct ConfiguredClientEnvironment { + ConfiguredClientEnvironment() noexcept { +#ifdef _WIN32 + if (_putenv_s("MQT_CORE_QDMI_CONFIG_FILE", + MQT_CORE_QDMI_CLIENT_TEST_CONFIG) != 0) { + std::abort(); + } +#else + if (setenv("MQT_CORE_QDMI_CONFIG_FILE", MQT_CORE_QDMI_CLIENT_TEST_CONFIG, + 1) != 0) { + std::abort(); + } +#endif + } +}; + +const ConfiguredClientEnvironment CONFIGURED_CLIENT_ENVIRONMENT; + auto queryBytes(const std::vector& bytes) { return [&bytes](const size_t size, void* value, size_t* sizeRet) { if (sizeRet != nullptr) { @@ -361,6 +379,7 @@ TEST(QDMITest, OperationPropertyToString) { } TEST(QDMITest, DevicePropertyToString) { + EXPECT_STREQ(qdmi::toString(QDMI_DEVICE_PROPERTY_ID), "ID"); EXPECT_STREQ(qdmi::toString(QDMI_DEVICE_PROPERTY_NAME), "NAME"); EXPECT_STREQ(qdmi::toString(QDMI_DEVICE_PROPERTY_VERSION), "VERSION"); EXPECT_STREQ(qdmi::toString(QDMI_DEVICE_PROPERTY_STATUS), "STATUS"); @@ -1202,91 +1221,15 @@ TEST(AuthenticationTest, ReportsSkippedUnsupportedParameter) { } TEST(AuthenticationTest, SessionConstructionWithAuthUrl) { - // Valid HTTPS URL - SessionConfig config1; - config1.authUrl = "https://example.com"; - EXPECT_NO_THROW({ const Session session(config1); }); - - // Valid HTTP URL with port and path - SessionConfig config2; - config2.authUrl = "http://auth.server.com:8080/api"; - EXPECT_NO_THROW({ const Session session(config2); }); - - // Valid HTTPS URL with query parameters - SessionConfig config3; - config3.authUrl = "https://auth.example.com/token?param=value"; - EXPECT_NO_THROW({ const Session session(config3); }); - - // Valid localhost URL - SessionConfig configLocalhost; - configLocalhost.authUrl = "http://localhost"; - EXPECT_NO_THROW({ const Session session(configLocalhost); }); - - // Valid localhost URL with port - SessionConfig configLocalhostPort; - configLocalhostPort.authUrl = "http://localhost:8080"; - EXPECT_NO_THROW({ const Session session(configLocalhostPort); }); - - // Valid localhost URL with port and path - SessionConfig configLocalhostPath; - configLocalhostPath.authUrl = "https://localhost:3000/auth/api"; - EXPECT_NO_THROW({ const Session session(configLocalhostPath); }); - - // Valid IPv4 address URL - SessionConfig configIPv4; - configIPv4.authUrl = "http://127.0.0.1:5000/auth"; - EXPECT_NO_THROW({ const Session session(configIPv4); }); - - // Valid IPv6 address URL - SessionConfig configIPv6; - configIPv6.authUrl = "https://[::1]:8080/auth"; - EXPECT_NO_THROW({ const Session session(configIPv6); }); - - // Invalid URL - not a URL at all (validation fails before setting parameter) - SessionConfig config4; - config4.authUrl = "not-a-url"; - EXPECT_THROW({ const Session session(config4); }, std::runtime_error); - - // Invalid URL - unsupported protocol - SessionConfig config5; - config5.authUrl = "ftp://invalid.com"; - EXPECT_THROW({ const Session session(config5); }, std::runtime_error); - - // Invalid URL - missing protocol - SessionConfig config6; - config6.authUrl = "example.com"; - EXPECT_THROW({ const Session session(config6); }, std::runtime_error); - - // Invalid URL - empty - SessionConfig config7; - config7.authUrl = ""; - EXPECT_THROW({ const Session session(config7); }, std::runtime_error); + SessionConfig config; + config.authUrl = "driver-specific:authentication-endpoint"; + EXPECT_NO_THROW({ const Session session(config); }); } TEST(AuthenticationTest, SessionConstructionWithAuthFile) { - // Non-existent file (validation fails before setting parameter) - SessionConfig config1; - config1.authFile = "/nonexistent/path/to/file.txt"; - EXPECT_THROW({ const Session session(config1); }, std::runtime_error); - - // Existing file (should succeed even if parameter is unsupported) - const auto tempDir = std::filesystem::temp_directory_path(); - auto const tmpPath = tempDir / ("qdmi_test_auth_" + - std::to_string(std::hash{}( - std::this_thread::get_id())) + - ".txt"); - { - std::ofstream tmpFile(tmpPath); - ASSERT_TRUE(tmpFile.is_open()) << "Failed to create temporary file"; - tmpFile << "test_token_content"; - } - - SessionConfig config2; - config2.authFile = tmpPath; - EXPECT_NO_THROW({ const Session session(config2); }); - - // Clean up - std::filesystem::remove(tmpPath); + SessionConfig config; + config.authFile = "/driver-owned/nonexistent/authentication-file"; + EXPECT_NO_THROW({ const Session session(config); }); } TEST(AuthenticationTest, SessionConstructionWithUsernamePassword) { @@ -1444,7 +1387,11 @@ namespace { // Helper function to get all devices for parameterized tests auto getDevices() -> std::vector { Session session; - return session.getDevices(); + auto devices = session.getDevices(); + std::erase_if(devices, [](const Device& device) { + return device.getId().starts_with("test."); + }); + return devices; } } // namespace diff --git a/test/qdmi/test_client_runtime.cpp b/test/qdmi/test_client_runtime.cpp new file mode 100644 index 0000000000..09b896d680 --- /dev/null +++ b/test/qdmi/test_client_runtime.cpp @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "TestUtils.hpp" +#include "qdmi/Client.hpp" + +#include +#include +/// POSIX declares setenv and unsetenv in this compatibility header. +/// NOLINTNEXTLINE(modernize-deprecated-headers) +#include + +#include +#include +#include +#include +#include + +namespace qdmi { +namespace { + +void setDriverEnvironment(const std::optional& value) { +#ifdef _WIN32 + ASSERT_EQ(_putenv_s("MQT_CORE_QDMI_DRIVER", value.value_or("").c_str()), 0); +#else + if (value) { + ASSERT_EQ(setenv("MQT_CORE_QDMI_DRIVER", value->c_str(), 1), 0); + } else { + ASSERT_EQ(unsetenv("MQT_CORE_QDMI_DRIVER"), 0); + } +#endif +} + +TEST(ClientRuntimeTest, ValidatesThenFreezesOneDriverAndRetainsSessions) { + const auto missing = + std::filesystem::path(MQT_CORE_QDMI_TEST_DRIVER).parent_path() / + "missing-client-driver"; + setDriverEnvironment(missing.string()); + EXPECT_THAT([] { return Session{}; }, + testing::ThrowsMessage( + testing::HasSubstr("Cannot load QDMI Client driver"))); + + EXPECT_THAT( + [] { + return Session{ + SessionConfig{.driverPath = MQT_CORE_QDMI_INCOMPLETE_DRIVER}}; + }, + testing::ThrowsMessage( + testing::HasSubstr("missing symbol QDMI_session_alloc"))); + EXPECT_THAT( + [] { + return Session{ + SessionConfig{.driverPath = MQT_CORE_QDMI_INCOMPATIBLE_DRIVER}}; + }, + testing::ThrowsMessage( + testing::HasSubstr("incompatible ABI"))); + + const SessionConfig firstConfig{ + .driverPath = MQT_CORE_QDMI_TEST_DRIVER, + .token = "first-token", + }; + setDriverEnvironment(MQT_CORE_QDMI_TEST_DRIVER); + { + const mqt::test::ScopedEnvironmentVariable nullAllocation{ + "MQT_CORE_QDMI_FAKE_FAIL_ALLOCATION", "warning-null"}; + testing::internal::CaptureStderr(); + EXPECT_THAT([&] { return Session{firstConfig}; }, + testing::ThrowsMessage( + testing::HasSubstr("returned a null session"))); + const auto errorOutput = testing::internal::GetCapturedStderr(); + EXPECT_THAT(errorOutput, testing::Not(testing::HasSubstr("Warning"))); + } + { + const mqt::test::ScopedEnvironmentVariable failAllocation{ + "MQT_CORE_QDMI_FAKE_FAIL_ALLOCATION", "1"}; + EXPECT_THROW(static_cast(Session{firstConfig}), std::bad_alloc); + } + EXPECT_THAT( + [] { + return Session{ + SessionConfig{.driverPath = MQT_CORE_QDMI_INCOMPLETE_DRIVER}}; + }, + testing::ThrowsMessage( + testing::HasSubstr("missing symbol QDMI_session_alloc"))); + + Session first(firstConfig); + { + const mqt::test::ScopedEnvironmentVariable nullAllocation{ + "MQT_CORE_QDMI_FAKE_FAIL_ALLOCATION", "success-null"}; + EXPECT_THAT([&] { return Session{firstConfig}; }, + testing::ThrowsMessage( + testing::HasSubstr("returned a null session"))); + } + Session second(SessionConfig{ + .driverPath = MQT_CORE_QDMI_TEST_DRIVER, + .token = "second-token", + }); + const auto firstDevices = first.getDevices(); + const auto secondDevices = second.getDevices(); + ASSERT_EQ(firstDevices.size(), 1U); + ASSERT_EQ(secondDevices.size(), 1U); + EXPECT_EQ(firstDevices.front().getId(), "test.fake.client"); + EXPECT_EQ(secondDevices.front().getId(), "test.fake.client"); + EXPECT_EQ(firstDevices.front().getName(), "first-token"); + EXPECT_EQ(secondDevices.front().getName(), "second-token"); + + Session oddSize(SessionConfig{ + .driverPath = MQT_CORE_QDMI_TEST_DRIVER, + .token = "odd-size", + }); + EXPECT_THROW(static_cast(oddSize.getDevices()), std::invalid_argument); + Session oddDeviceSize(SessionConfig{ + .driverPath = MQT_CORE_QDMI_TEST_DRIVER, + .token = "odd-device-size", + }); + EXPECT_THROW(static_cast(oddDeviceSize.getDevices().front().getSites()), + std::invalid_argument); + Session oddOperationSize(SessionConfig{ + .driverPath = MQT_CORE_QDMI_TEST_DRIVER, + .token = "odd-operation-size", + }); + EXPECT_THROW(static_cast(oddOperationSize.getDevices() + .front() + .getOperations() + .front() + .getSites()), + std::invalid_argument); + + EXPECT_THAT( + [] { + return Session{ + SessionConfig{.driverPath = MQT_CORE_QDMI_INCOMPLETE_DRIVER}}; + }, + testing::ThrowsMessage( + testing::HasSubstr("already selected"))); + + const auto site = [] { + const auto device = Session::openDevice( + "test.fake.client", SessionConfig{ + .driverPath = MQT_CORE_QDMI_TEST_DRIVER, + .token = "retained-token", + }); + auto sites = device.getSites(); + return sites.front(); + }(); + EXPECT_EQ(site.getIndex(), 0U); + + setDriverEnvironment(std::nullopt); +} + +} // namespace +} // namespace qdmi diff --git a/test/qdmi/test_packaged_runtime.cpp b/test/qdmi/test_packaged_runtime.cpp new file mode 100644 index 0000000000..c0312cd2af --- /dev/null +++ b/test/qdmi/test_packaged_runtime.cpp @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "qdmi/Client.hpp" + +#include +#include +#include +#include + +#ifndef MQT_CORE_QDMI_TEST_DRIVER_FILENAME +#error MQT_CORE_QDMI_TEST_DRIVER_FILENAME must name the packaged Client driver +#endif + +int main(const int argc, const char* const argv[]) { + try { + if (argc != 1) { + return EXIT_FAILURE; + } + const auto executable = std::filesystem::weakly_canonical(*argv); + const auto driver = + executable.parent_path() / MQT_CORE_QDMI_TEST_DRIVER_FILENAME; + if (!std::filesystem::is_regular_file(driver)) { + std::cerr << "Packaged QDMI Client driver is missing: " << driver << '\n'; + return EXIT_FAILURE; + } + std::filesystem::current_path(std::filesystem::temp_directory_path()); + qdmi::Session session; + return session.getDevices().empty() ? EXIT_FAILURE : EXIT_SUCCESS; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return EXIT_FAILURE; + } +} diff --git a/test/qdmi/test_slurm.cpp b/test/qdmi/test_slurm.cpp index 3a3e89f827..41cffe2ce8 100644 --- a/test/qdmi/test_slurm.cpp +++ b/test/qdmi/test_slurm.cpp @@ -9,7 +9,6 @@ */ #include "qdmi/Slurm.hpp" -#include "qdmi/driver/Driver.hpp" #include #include @@ -70,20 +69,9 @@ class ScopedSlurmLicenses { std::optional originalValue; }; -void registerStatusDevice(const std::string& id, - const std::string& configuredStatus) { - static_cast(qdmi::Driver::get().registerDeviceIfAbsent({ - .id = id, - .library = MQT_CORE_QDMI_SLURM_TEST_DEVICE, - .prefix = "TEST_SESSION", - .session = {.custom4 = configuredStatus}, - })); -} - } // namespace TEST(SlurmAdapterTest, AcceptsImplicitAndExplicitUnitCounts) { - registerStatusDevice("test.slurm.idle", "idle"); for (const auto* const value : {"test.slurm.idle", "test.slurm.idle:1"}) { const ScopedSlurmLicenses licenses(value); EXPECT_EQ(openDeviceFromLicense().getStatus(), QDMI_DEVICE_STATUS_IDLE); @@ -91,13 +79,11 @@ TEST(SlurmAdapterTest, AcceptsImplicitAndExplicitUnitCounts) { } TEST(SlurmAdapterTest, AcceptsBusyDevice) { - registerStatusDevice("test.slurm.busy", "busy"); const ScopedSlurmLicenses licenses("test.slurm.busy"); EXPECT_EQ(openDeviceFromLicense().getStatus(), QDMI_DEVICE_STATUS_BUSY); } TEST(SlurmAdapterTest, RejectsMissingAndMalformedValues) { - registerStatusDevice("test.slurm.grammar", "idle"); const std::array, 13> invalidValues{ std::nullopt, "", @@ -122,7 +108,6 @@ TEST(SlurmAdapterTest, RejectsMissingAndMalformedValues) { } TEST(SlurmAdapterTest, RejectsUnknownRemoteAndCompoundLicenses) { - registerStatusDevice("test.slurm.single", "idle"); constexpr std::array invalidValues{ "test.slurm.unknown", "test.slurm.single@license-server:1", "test.slurm.single,unrelated", "unrelated,test.slurm.single", @@ -147,7 +132,6 @@ TEST(SlurmAdapterTest, RejectsUnavailableDeviceWithIdAndStatus) { for (const auto& [configuredStatus, reportedStatus] : rejectedStates) { const auto id = std::string{"test.slurm."} + configuredStatus; - registerStatusDevice(id, configuredStatus); const ScopedSlurmLicenses licenses(id); EXPECT_THAT( [] { return openDeviceFromLicense(); }, diff --git a/test/slurm/run_integration.py b/test/slurm/run_integration.py index a8d173d670..94b53c7385 100644 --- a/test/slurm/run_integration.py +++ b/test/slurm/run_integration.py @@ -308,10 +308,10 @@ def main() -> None: registry_check = ( "from pathlib import Path; " "import mqt.core; " - "from mqt.core.qdmi import driver; " + "from mqt.core.qdmi import ClientSession; " "module_path = Path(mqt.core.__file__).resolve(); " "assert not any(module_path.is_relative_to(root) for root in ('/workspace', '/runtime')), module_path; " - "ids = driver.registered_device_ids(); " + "ids = {device.id for device in ClientSession().devices}; " "assert 'mqt.ddsim.default' in ids and 'mqt.sc.default' in ids, ids" ) controller("python3", "-c", registry_check)