From 497f47ca310f0245255d25d7292caafc814c1b7d Mon Sep 17 00:00:00 2001 From: Marcel Walter Date: Tue, 9 Dec 2025 14:57:08 +0100 Subject: [PATCH 1/4] :sparkles: Instantiate `Device` from dynamic QDMI device library --- bindings/fomac/fomac.cpp | 8 +++++--- include/mqt-core/fomac/FoMaC.hpp | 10 ++++++++++ include/mqt-core/qdmi/Driver.hpp | 5 ++++- python/mqt/core/fomac.pyi | 13 ++++++++----- src/qdmi/Driver.cpp | 3 ++- test/qdmi/test_driver.cpp | 25 +++++++++++++++++++++++++ 6 files changed, 54 insertions(+), 10 deletions(-) diff --git a/bindings/fomac/fomac.cpp b/bindings/fomac/fomac.cpp index 03de2ca837..59423cf016 100644 --- a/bindings/fomac/fomac.cpp +++ b/bindings/fomac/fomac.cpp @@ -240,7 +240,8 @@ PYBIND11_MODULE(MQT_CORE_MODULE_NAME, m, py::mod_gil_not_used()) { const std::optional& custom2 = std::nullopt, const std::optional& custom3 = std::nullopt, const std::optional& custom4 = std::nullopt, - const std::optional& custom5 = std::nullopt) -> void { + const std::optional& custom5 = + std::nullopt) -> fomac::Session::Device { const qdmi::DeviceSessionConfig config{.baseUrl = baseUrl, .token = token, .authFile = authFile, @@ -252,8 +253,9 @@ PYBIND11_MODULE(MQT_CORE_MODULE_NAME, m, py::mod_gil_not_used()) { .custom3 = custom3, .custom4 = custom4, .custom5 = custom5}; - qdmi::Driver::get().addDynamicDeviceLibrary(libraryPath, prefix, - config); + const auto qdmiDevice = qdmi::Driver::get().addDynamicDeviceLibrary( + libraryPath, prefix, config); + return fomac::Session::Device::fromQDMIDevice(qdmiDevice); }, "library_path"_a, "prefix"_a, "base_url"_a = std::nullopt, "token"_a = std::nullopt, "auth_file"_a = std::nullopt, diff --git a/include/mqt-core/fomac/FoMaC.hpp b/include/mqt-core/fomac/FoMaC.hpp index 2a99cf0f11..68fcb56660 100644 --- a/include/mqt-core/fomac/FoMaC.hpp +++ b/include/mqt-core/fomac/FoMaC.hpp @@ -612,6 +612,16 @@ class Session { * @param device The QDMI_Device handle to wrap. */ Device(Session::Token /* unused */, QDMI_Device device) : device_(device) {} + /** + * @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 Token + * construction is not accessible. + */ + [[nodiscard]] static auto fromQDMIDevice(QDMI_Device device) -> Device { + return Device(Session::Token{}, device); + } /// @returns the underlying QDMI_Device object. [[nodiscard]] auto getQDMIDevice() const -> QDMI_Device { return device_; } // NOLINTNEXTLINE(google-explicit-constructor, *-explicit-conversions) diff --git a/include/mqt-core/qdmi/Driver.hpp b/include/mqt-core/qdmi/Driver.hpp index 34fd17f124..7c477f0642 100644 --- a/include/mqt-core/qdmi/Driver.hpp +++ b/include/mqt-core/qdmi/Driver.hpp @@ -437,6 +437,8 @@ class Driver final { * library. * @param config Configuration for device session parameters. * + * @return A pointer to the newly created device. + * * @note This function is only available on non-Windows platforms. * * @throws std::runtime_error If the device cannot be initialized. @@ -444,7 +446,8 @@ class Driver final { */ auto addDynamicDeviceLibrary(const std::string& libName, const std::string& prefix, - const DeviceSessionConfig& config = {}) -> void; + const DeviceSessionConfig& config = {}) + -> QDMI_Device; #endif // _WIN32 /** * @brief Allocates a new session. diff --git a/python/mqt/core/fomac.pyi b/python/mqt/core/fomac.pyi index e2d6ad8d93..4b23b8909a 100644 --- a/python/mqt/core/fomac.pyi +++ b/python/mqt/core/fomac.pyi @@ -297,7 +297,7 @@ if sys.platform != "win32": custom3: str | None = None, custom4: str | None = None, custom5: str | None = None, - ) -> None: + ) -> Device: """Load a dynamic device library into the QDMI driver. This function loads a shared library (.so, .dll, or .dylib) that implements @@ -320,6 +320,9 @@ if sys.platform != "win32": custom4: Optional custom configuration parameter 4. custom5: Optional custom configuration parameter 5. + Returns: + Device: The newly loaded device that can be used to create backends. + Raises: RuntimeError: If library loading fails or configuration is invalid. @@ -327,12 +330,12 @@ if sys.platform != "win32": Load a device library with configuration: >>> import mqt.core.fomac as fomac - >>> fomac.add_dynamic_device_library( + >>> device = fomac.add_dynamic_device_library( ... "/path/to/libmy_device.so", "MY_DEVICE", base_url="http://localhost:8080", custom1="API_V2" ... ) - Now the device is available in sessions: + Now the device can be used directly: - >>> session = fomac.Session() - >>> devices = session.get_devices() + >>> from mqt.core.plugins.qiskit import QDMIBackend + >>> backend = QDMIBackend(device=device) """ diff --git a/src/qdmi/Driver.cpp b/src/qdmi/Driver.cpp index c0eadd10c0..f6d674337d 100644 --- a/src/qdmi/Driver.cpp +++ b/src/qdmi/Driver.cpp @@ -388,9 +388,10 @@ Driver::~Driver() { auto Driver::addDynamicDeviceLibrary(const std::string& libName, const std::string& prefix, const DeviceSessionConfig& config) - -> void { + -> QDMI_Device { devices_.emplace_back(std::make_unique( std::make_unique(libName, prefix), config)); + return devices_.back().get(); } #endif diff --git a/test/qdmi/test_driver.cpp b/test/qdmi/test_driver.cpp index 1728f3b465..287979b311 100644 --- a/test/qdmi/test_driver.cpp +++ b/test/qdmi/test_driver.cpp @@ -705,6 +705,31 @@ TEST(DeviceSessionConfigTest, IdempotentLoadingWithDifferentConfigs) { } } } + +TEST(DynamicDeviceLibraryTest, addDynamicDeviceLibraryReturnsDevice) { + // Test that addDynamicDeviceLibrary returns a valid device pointer + if constexpr (DYN_DEV_LIBS.empty()) { + GTEST_SKIP() << "No dynamic device libraries configured for testing."; + } + auto& driver = qdmi::Driver::get(); + for (const auto& [lib, prefix] : DYN_DEV_LIBS) { + const qdmi::DeviceSessionConfig config; + QDMI_Device device = nullptr; + ASSERT_NO_THROW( + { device = driver.addDynamicDeviceLibrary(lib, prefix, config); }); + EXPECT_NE(device, nullptr) + << "addDynamicDeviceLibrary should return a non-null device pointer"; + + // Verify the device is valid by querying its name + if (device != nullptr) { + size_t size = 0; + EXPECT_EQ(QDMI_device_query_device_property( + device, QDMI_DEVICE_PROPERTY_NAME, 0, nullptr, &size), + QDMI_SUCCESS); + EXPECT_GT(size, 0) << "Device should have a non-empty name"; + } + } +} #endif // _WIN32 INSTANTIATE_TEST_SUITE_P( From fd7619ee334c974378e92bd2ae2fe4b30218f1ff Mon Sep 17 00:00:00 2001 From: Marcel Walter Date: Tue, 9 Dec 2025 15:06:47 +0100 Subject: [PATCH 2/4] :rotating_light: Fix `clang-tidy` warning --- bindings/fomac/fomac.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindings/fomac/fomac.cpp b/bindings/fomac/fomac.cpp index 59423cf016..df98373c17 100644 --- a/bindings/fomac/fomac.cpp +++ b/bindings/fomac/fomac.cpp @@ -253,7 +253,7 @@ PYBIND11_MODULE(MQT_CORE_MODULE_NAME, m, py::mod_gil_not_used()) { .custom3 = custom3, .custom4 = custom4, .custom5 = custom5}; - const auto qdmiDevice = qdmi::Driver::get().addDynamicDeviceLibrary( + auto* const qdmiDevice = qdmi::Driver::get().addDynamicDeviceLibrary( libraryPath, prefix, config); return fomac::Session::Device::fromQDMIDevice(qdmiDevice); }, From f195c69a93b47a8f87ec27d77cdd52667a533fe5 Mon Sep 17 00:00:00 2001 From: Marcel Walter Date: Tue, 9 Dec 2025 16:23:08 +0100 Subject: [PATCH 3/4] :art: Apply CodeRabbit's suggestion --- test/qdmi/test_driver.cpp | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/test/qdmi/test_driver.cpp b/test/qdmi/test_driver.cpp index 287979b311..61c64b4f9c 100644 --- a/test/qdmi/test_driver.cpp +++ b/test/qdmi/test_driver.cpp @@ -717,17 +717,15 @@ TEST(DynamicDeviceLibraryTest, addDynamicDeviceLibraryReturnsDevice) { QDMI_Device device = nullptr; ASSERT_NO_THROW( { device = driver.addDynamicDeviceLibrary(lib, prefix, config); }); - EXPECT_NE(device, nullptr) + ASSERT_NE(device, nullptr) << "addDynamicDeviceLibrary should return a non-null device pointer"; // Verify the device is valid by querying its name - if (device != nullptr) { - size_t size = 0; - EXPECT_EQ(QDMI_device_query_device_property( - device, QDMI_DEVICE_PROPERTY_NAME, 0, nullptr, &size), - QDMI_SUCCESS); - EXPECT_GT(size, 0) << "Device should have a non-empty name"; - } + size_t size = 0; + EXPECT_EQ(QDMI_device_query_device_property( + device, QDMI_DEVICE_PROPERTY_NAME, 0, nullptr, &size), + QDMI_SUCCESS); + EXPECT_GT(size, 0) << "Device should have a non-empty name"; } } #endif // _WIN32 From f430b654299efb10aabdf0ffe52f5b8fee0563cd Mon Sep 17 00:00:00 2001 From: Marcel Walter Date: Tue, 9 Dec 2025 16:27:14 +0100 Subject: [PATCH 4/4] :memo: Add CHANGELOG entry --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 032c542e87..d1ecce5c68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ This project adheres to [Semantic Versioning], with the exception that minor rel ### Added +- ✨ Return device handle from `add_dynamic_device_library` for direct backend creation ([#1381]) ([**@marcelwa**]) - ✨ Add IQM JSON support for job submission in Qiskit-QDMI Backend ([#1375]) ([**@marcelwa**]) - ✨ Add authentication support for QDMI sessions with token, username/password, auth file, auth URL, and project ID parameters ([#1355]) ([**@marcelwa**]) - ✨ Add a new QDMI device that represents a superconducting architecture featuring a coupling map ([#1328]) ([**@ystade**]) @@ -279,6 +280,7 @@ _📚 Refer to the [GitHub Release Notes](https://github.com/munich-quantum-tool +[#1381]: https://github.com/munich-quantum-toolkit/core/pull/1381 [#1375]: https://github.com/munich-quantum-toolkit/core/pull/1375 [#1371]: https://github.com/munich-quantum-toolkit/core/pull/1371 [#1359]: https://github.com/munich-quantum-toolkit/core/pull/1359