diff --git a/CHANGELOG.md b/CHANGELOG.md index a7d4b909ac..362d304d4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,9 +12,9 @@ releases may include breaking changes. ### Added -- ✨ Support retrieving existing jobs by ID through the QDMI client API and - expose optional device queue length and job queue position through the C++ and - Python FoMaC APIs ([#2010]) ([**@burgholzer**]) +- ✨ Support retrieving existing jobs by ID through the QDMI client API and C++ + and Python FoMaC APIs, and expose optional device queue length and job queue + position ([#2008], [#2010]) ([**@burgholzer**]) - 🐍 Start building CPython 3.15 wheels ([#2011]) ([**@denialhaag**]) - ✨ Add PennyLane support for gate-based QDMI devices ([#2005]) ([**@burgholzer**]) @@ -728,6 +728,7 @@ for previous changelogs._ [#2026]: https://github.com/munich-quantum-toolkit/core/pull/2026 [#2011]: https://github.com/munich-quantum-toolkit/core/pull/2011 [#2010]: https://github.com/munich-quantum-toolkit/core/pull/2010 +[#2008]: https://github.com/munich-quantum-toolkit/core/pull/2008 [#2007]: https://github.com/munich-quantum-toolkit/core/pull/2007 [#2006]: https://github.com/munich-quantum-toolkit/core/pull/2006 [#2005]: https://github.com/munich-quantum-toolkit/core/pull/2005 diff --git a/bindings/fomac/fomac.cpp b/bindings/fomac/fomac.cpp index 4ef4b096c9..8075caca34 100644 --- a/bindings/fomac/fomac.cpp +++ b/bindings/fomac/fomac.cpp @@ -478,6 +478,14 @@ when the custom slot is unsupported.)pb"); "custom5"_a = nb::none(), nb::rv_policy::reference_internal, "Submits an exact byte payload to the device."); + device.def( + "retrieve_job_by_id", + [](const fomac::Device& self, const std::string& jobId) { + return self.retrieveJobById(jobId); + }, + "job_id"_a, nb::rv_policy::reference_internal, + "Retrieves an existing job by its device-provided ID."); + device.def("__repr__", [](const fomac::Device& dev) { return ""; }); diff --git a/include/mqt-core/fomac/FoMaC.hpp b/include/mqt-core/fomac/FoMaC.hpp index b4e4b2b089..2d7a20a64f 100644 --- a/include/mqt-core/fomac/FoMaC.hpp +++ b/include/mqt-core/fomac/FoMaC.hpp @@ -563,6 +563,16 @@ class Device { const std::optional& custom4 = std::nullopt, const std::optional& custom5 = std::nullopt) const; + /** + * @brief Retrieves an existing job by its device-provided ID. + * @details Opening a job does not submit, clone, or modify the remote job. + * The returned handle can be used to query its state and retrieve results. + * @param jobId The nonempty opaque ID returned by @ref Job::getId. + * @throws std::runtime_error If the driver or device cannot retrieve the job. + * @see QDMI_session_retrieve_job_by_id + */ + [[nodiscard]] Job retrieveJobById(std::string_view jobId) const; + auto operator<=>(const Device&) const noexcept = default; private: diff --git a/python/mqt/core/fomac.pyi b/python/mqt/core/fomac.pyi index 44ef66aefd..3fe0cc4491 100644 --- a/python/mqt/core/fomac.pyi +++ b/python/mqt/core/fomac.pyi @@ -376,6 +376,9 @@ class Device: ) -> Job: """Submits an exact byte payload to the device.""" + def retrieve_job_by_id(self, job_id: str) -> Job: + """Retrieves an existing job by its device-provided ID.""" + def __eq__(self, arg: object, /) -> bool: ... def __ne__(self, arg: object, /) -> bool: ... diff --git a/src/fomac/FoMaC.cpp b/src/fomac/FoMaC.cpp index 16c0ab75ed..6ce81b931d 100644 --- a/src/fomac/FoMaC.cpp +++ b/src/fomac/FoMaC.cpp @@ -411,6 +411,15 @@ Job Device::submitJob(const std::span program, return jobWrapper; } +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_}; +} + void Device::setCustomJobParam(QDMI_Job job, const QDMI_Job_Parameter param, const CustomJobParameter& value) { std::visit( diff --git a/src/qdmi/devices/dd/Device.cpp b/src/qdmi/devices/dd/Device.cpp index 7a85a8038c..2d387650a0 100644 --- a/src/qdmi/devices/dd/Device.cpp +++ b/src/qdmi/devices/dd/Device.cpp @@ -844,6 +844,13 @@ int MQT_DDSIM_QDMI_device_session_create_device_job( return session->createDeviceJob(job); } +int MQT_DDSIM_QDMI_device_session_retrieve_device_job_by_id( + [[maybe_unused]] MQT_DDSIM_QDMI_Device_Session session, + [[maybe_unused]] const char* jobId, + [[maybe_unused]] MQT_DDSIM_QDMI_Device_Job* job) { + return QDMI_ERROR_NOTSUPPORTED; +} + void MQT_DDSIM_QDMI_device_job_free(MQT_DDSIM_QDMI_Device_Job job) { job->free(); } diff --git a/src/qdmi/devices/na/Device.cpp b/src/qdmi/devices/na/Device.cpp index a6a68d2204..1618a064dd 100644 --- a/src/qdmi/devices/na/Device.cpp +++ b/src/qdmi/devices/na/Device.cpp @@ -684,6 +684,13 @@ int MQT_NA_QDMI_device_session_create_device_job( return session->createDeviceJob(job); } +int MQT_NA_QDMI_device_session_retrieve_device_job_by_id( + [[maybe_unused]] MQT_NA_QDMI_Device_Session session, + [[maybe_unused]] const char* jobId, + [[maybe_unused]] MQT_NA_QDMI_Device_Job* job) { + return QDMI_ERROR_NOTSUPPORTED; +} + void MQT_NA_QDMI_device_job_free(MQT_NA_QDMI_Device_Job job) { if (job != nullptr) { job->free(); diff --git a/src/qdmi/devices/sc/Device.cpp b/src/qdmi/devices/sc/Device.cpp index a62a7955d9..97d5a64cbb 100644 --- a/src/qdmi/devices/sc/Device.cpp +++ b/src/qdmi/devices/sc/Device.cpp @@ -470,6 +470,12 @@ int MQT_SC_QDMI_device_session_create_device_job( return session == nullptr ? QDMI_ERROR_INVALIDARGUMENT : session->createDeviceJob(job); } +int MQT_SC_QDMI_device_session_retrieve_device_job_by_id( + [[maybe_unused]] MQT_SC_QDMI_Device_Session session, + [[maybe_unused]] const char* jobId, + [[maybe_unused]] MQT_SC_QDMI_Device_Job* job) { + return QDMI_ERROR_NOTSUPPORTED; +} void MQT_SC_QDMI_device_job_free(MQT_SC_QDMI_Device_Job job) { if (job != nullptr) { job->free(); diff --git a/test/python/fomac/test_fomac.py b/test/python/fomac/test_fomac.py index a78d9dd2b0..32764b6e45 100644 --- a/test/python/fomac/test_fomac.py +++ b/test/python/fomac/test_fomac.py @@ -594,6 +594,14 @@ def test_device_submit_job_preserves_num_shots(ddsim_device: Device) -> None: assert job3.num_shots == 1000 +def test_device_retrieve_job_by_id_reports_unsupported_provider( + ddsim_device: Device, +) -> None: + """Expose job retrieval through Python without requiring DDSIM support.""" + with pytest.raises(RuntimeError, match=r"Retrieving job: Not supported\."): + ddsim_device.retrieve_job_by_id("unknown") + + @pytest.fixture def submitted_job(ddsim_device: Device) -> Job: """Fixture that provides a submitted job for testing. diff --git a/test/qdmi/driver/test_driver.cpp b/test/qdmi/driver/test_driver.cpp index bf375348c0..87fb8aaa27 100644 --- a/test/qdmi/driver/test_driver.cpp +++ b/test/qdmi/driver/test_driver.cpp @@ -1244,6 +1244,9 @@ TEST(DeviceRegistrationTest, RetrievesExistingJobs) { EXPECT_EQ(QDMI_session_retrieve_job_by_id(device, "missing", &job), QDMI_ERROR_NOTFOUND); EXPECT_EQ(job, nullptr); + + const auto retrievedJob = device.retrieveJobById("session-job"); + EXPECT_EQ(retrievedJob.getId(), "session-job"); } TEST(DeviceRegistrationTest, FreshChildDeviceRetainsItsRootSession) {