diff --git a/.agent/plans/qdmi-multi-program-adoption.md b/.agent/plans/qdmi-multi-program-adoption.md new file mode 100644 index 0000000000..585cf1642d --- /dev/null +++ b/.agent/plans/qdmi-multi-program-adoption.md @@ -0,0 +1,36 @@ +# Independent native multi-program adoption + +Status: extracted from Core PR #2226; local validation complete. + +## Scope and decisions + +Core issue #2362 owns Client APIs, bindings, bundled-device adaptation, and +indexed results for QDMI PR #509. The program-format enum remains unchanged. +Metadata removal, replaceable drivers, and payload capabilities are independent. +This targets Core 4.1 with released QDMI 1.4 before publication. + +Retain existing single-program and calibration APIs, optional shot counts, +byte-exact binary payloads, session ownership and current concurrent execution. +DDSIM supports one program per native job for now; the model-only SC device does +not execute jobs. Neither device claims unsupported aggregate semantics. An +isolated test provider exercises multiple programs and indexed retrieval. + +## Validation + +Run the release build and CTest suite, generated stubs, Python QDMI/SDK tests, +repository lint, C++ lint, and the documented DDSIM example. Cover atomic +setters, indexed ordering and invalid indices, deep copies, binary bytes, +optional shots, retrieval, failure/cancellation and the single-program path. +Preserve current target inference and simulator concurrency regressions. + +Local results: 3,879 native tests passed with one existing skip; 413 Python +QDMI/SDK tests passed; minimum-dependency testing passed 412 tests with one +expected skip. Generated stubs, repository lint and C++ lint passed. Hosted CI +remains a separate publication gate. + +## Follow-ups + +Core issue #2359 coordinates independent SDK and provider consumers. Do not +implement their fallback policy here or equate concurrent single submissions +with a native aggregate job. No payload descriptors or execution-capability +properties are introduced by this extraction. diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e2cc9f384..4d9a0bac4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -92,6 +92,9 @@ releases may include breaking changes. - ✨ Expose ordered shots from DDSIM QDMI OpenQASM and QIR jobs, with matching histograms ([#2368]) ([**@burgholzer**]) +- ✨ Add native multi-program QDMI submissions and indexed results to the C++ + and Python clients, independently of concurrent single-program execution + ([#2373]) ([**@burgholzer**]) - 🐳 Add dev container configuration for a consistent local development environment ([#1786]) ([**@denialhaag**]) @@ -883,6 +886,7 @@ for previous changelogs._ [#2380]: https://github.com/munich-quantum-toolkit/core/pull/2380 +[#2373]: https://github.com/munich-quantum-toolkit/core/pull/2373 [#2368]: https://github.com/munich-quantum-toolkit/core/pull/2368 [#2358]: https://github.com/munich-quantum-toolkit/core/pull/2358 [#2349]: https://github.com/munich-quantum-toolkit/core/pull/2349 diff --git a/UPGRADING.md b/UPGRADING.md index 2d05eae44f..29415af9d0 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -6,6 +6,20 @@ of changes including minor and patch releases, please refer to the ## [Unreleased] +### QDMI multi-program job interface + +Native QDMI consumers must rebuild against QDMI 1.4. Replace the +`QDMI_JOB_PARAMETER_PROGRAM` setter with `QDMI_job_set_programs`, including for +one program. Device implementations provide `QDMI_device_job_set_programs` and +accept a program index in result retrieval. The program-format enum remains +unchanged. Calibration without a payload can still set only the format. + +Existing C++ and Python single-program submission and result calls remain valid. +Use `submitPrograms` or `submit_programs` for native program lists; omit the +shot count to preserve device defaults. A provider that cannot implement the +aggregate lifecycle must reject larger lists. Concurrent single-program +submission remains a separate fallback. + ### Removal of the classic circuit representation MQT Core 4 removes the complete classic circuit surface. This includes the C++ diff --git a/bindings/qdmi/qdmi.cpp b/bindings/qdmi/qdmi.cpp index 1339bd6b06..7a2bfcdef7 100644 --- a/bindings/qdmi/qdmi.cpp +++ b/bindings/qdmi/qdmi.cpp @@ -106,27 +106,42 @@ NB_MODULE(MQT_CORE_MODULE_NAME, qdmiModule) { job.def("cancel", &qdmi::Job::cancel, "Cancels the job."); - job.def("get_shots", &qdmi::Job::getShots, + job.def( + "get_results", + [](const qdmi::Job& self, const size_t programIndex, + const QDMI_Job_Result result) { + const auto value = self.getResults(programIndex, result); + return nb::bytes(reinterpret_cast(value.data()), + value.size()); + }, + "program_index"_a, "result"_a, nb::call_guard(), + "Returns one indexed result as exact bytes."); + + job.def("get_shots", &qdmi::Job::getShots, "program_index"_a = 0U, nb::call_guard(), "Returns the raw shot results from the job."); - job.def("get_counts", &qdmi::Job::getCounts, + job.def("get_counts", &qdmi::Job::getCounts, "program_index"_a = 0U, nb::call_guard(), "Returns the measurement counts from the job."); job.def("get_dense_statevector", &qdmi::Job::getDenseStateVector, + "program_index"_a = 0U, "Returns the dense statevector from the job (typically only " "available from simulator devices)."); job.def("get_dense_probabilities", &qdmi::Job::getDenseProbabilities, + "program_index"_a = 0U, "Returns the dense probabilities from the job (typically only " "available from simulator devices)."); job.def("get_sparse_statevector", &qdmi::Job::getSparseStateVector, + "program_index"_a = 0U, "Returns the sparse statevector from the job (typically only " "available from simulator devices)."); job.def("get_sparse_probabilities", &qdmi::Job::getSparseProbabilities, + "program_index"_a = 0U, "Returns the sparse probabilities from the job (typically only " "available from simulator devices)."); @@ -154,17 +169,19 @@ when the custom slot is unsupported.)pb"); job.def( "get_custom_result", [](const qdmi::Job& self, const qdmi::CustomProperty customProperty, - const nb::handle valueType) { + const nb::handle valueType, const size_t programIndex) { return queryCustomValue( - [&self, customProperty] { - return self.getCustomResult(customProperty); + [&self, customProperty, + programIndex] { + return self.getCustomResult(customProperty, programIndex); }, valueType); }, - "custom_property"_a, "value_type"_a, + "custom_property"_a, "value_type"_a, "program_index"_a = 0U, nb::sig("def get_custom_result(self, custom_property: CustomProperty, " "value_type: type[str] | type[bool] | type[int] | type[float] | " - "type[bytes]) -> str | bool | int | float | bytes | None"), + "type[bytes], program_index: int = 0) -> str | bool | int | " + "float | bytes | None"), R"pb(Return an implementation-defined custom job result. The caller must provide the type documented by the device implementation. @@ -186,6 +203,9 @@ when the custom slot is unsupported.)pb"); }, "The exact bytes of the submitted program."); + job.def_prop_ro("programs_num", &qdmi::Job::getProgramsNum, + "The number of programs in the job."); + job.def_prop_ro("num_shots", &qdmi::Job::getNumShots, "The number of shots."); job.def_prop_ro( @@ -209,6 +229,20 @@ when the custom slot is unsupported.)pb"); .value("FAILED", QDMI_JOB_STATUS_FAILED); // ProgramFormat enum + nb::enum_(job, "Result", "One raw job result format.") + .value("SHOTS", QDMI_JOB_RESULT_SHOTS) + .value("HIST_KEYS", QDMI_JOB_RESULT_HIST_KEYS) + .value("HIST_VALUES", QDMI_JOB_RESULT_HIST_VALUES) + .value("STATEVECTOR_DENSE", QDMI_JOB_RESULT_STATEVECTOR_DENSE) + .value("PROBABILITIES_DENSE", QDMI_JOB_RESULT_PROBABILITIES_DENSE) + .value("STATEVECTOR_SPARSE_KEYS", QDMI_JOB_RESULT_STATEVECTOR_SPARSE_KEYS) + .value("STATEVECTOR_SPARSE_VALUES", + QDMI_JOB_RESULT_STATEVECTOR_SPARSE_VALUES) + .value("PROBABILITIES_SPARSE_KEYS", + QDMI_JOB_RESULT_PROBABILITIES_SPARSE_KEYS) + .value("PROBABILITIES_SPARSE_VALUES", + QDMI_JOB_RESULT_PROBABILITIES_SPARSE_VALUES); + nb::enum_(qdmiModule, "ProgramFormat", "Enumeration of program formats.") .value("QASM2", QDMI_PROGRAM_FORMAT_QASM2) @@ -436,6 +470,49 @@ optional and may be a string or bytes. When it is given, the device defines what it means, which is usually a configuration for the run. A calibration run executes no circuit, so it takes no shot count.)pb"); + device.def( + "submit_programs", + [](const qdmi::Device& self, const std::vector& programs, + const QDMI_Program_Format format, const std::optional numShots, + const std::optional& custom1, + const std::optional& custom2, + const std::optional& custom3, + const std::optional& custom4, + const std::optional& custom5) { + return self.submitPrograms(programs, format, numShots, custom1, custom2, + custom3, custom4, custom5); + }, + "programs"_a, "program_format"_a, "num_shots"_a = nb::none(), + nb::kw_only(), "custom1"_a = nb::none(), "custom2"_a = nb::none(), + "custom3"_a = nb::none(), "custom4"_a = nb::none(), + "custom5"_a = nb::none(), nb::rv_policy::reference_internal, + "Submits an ordered list of text programs atomically."); + + device.def( + "submit_programs", + [](const qdmi::Device& self, const std::vector& programs, + const QDMI_Program_Format format, const std::optional numShots, + const std::optional& custom1, + const std::optional& custom2, + const std::optional& custom3, + const std::optional& custom4, + const std::optional& custom5) { + std::vector> bytes; + bytes.reserve(programs.size()); + for (const auto& program : programs) { + const std::span value{static_cast(program.data()), + program.size()}; + bytes.emplace_back(value.begin(), value.end()); + } + return self.submitPrograms(bytes, format, numShots, custom1, custom2, + custom3, custom4, custom5); + }, + "programs"_a, "program_format"_a, "num_shots"_a = nb::none(), + nb::kw_only(), "custom1"_a = nb::none(), "custom2"_a = nb::none(), + "custom3"_a = nb::none(), "custom4"_a = nb::none(), + "custom5"_a = nb::none(), nb::rv_policy::reference_internal, + "Submits an ordered list of exact byte programs atomically."); + device.def( "retrieve_job_by_id", [](const qdmi::Device& self, const std::string& jobId) { diff --git a/cmake/ExternalDependencies.cmake b/cmake/ExternalDependencies.cmake index a1d267880f..47d30544f1 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 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 "4949ecba4b61e9492160b1be96507a570bea3765" # enum-based multi-program jobs 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/glossary.md b/docs/glossary.md index c2ecdb889b..f311aa6051 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -167,6 +167,11 @@ legalization The act of replacing or rejecting IR until every remaining operation and type satisfies a declared conversion target or target capability. +native multi-program job + One submitted job containing an ordered list of programs with a shared + lifecycle. Results use the input program indices. This differs from concurrent + submission of independent single-program jobs. + compiler target An immutable MQT description of the operations, topology, and properties that a compiler pipeline may use for one destination. It is a snapshot used for diff --git a/docs/qdmi/driver.md b/docs/qdmi/driver.md index 7b15418d9b..0b57075ec9 100644 --- a/docs/qdmi/driver.md +++ b/docs/qdmi/driver.md @@ -67,3 +67,40 @@ for device_id in registered_device_ids(): device = open_device(device_id) print(device.name()) ``` + +## Native multi-program jobs + +`Device.submit_programs` submits an ordered list of programs with one format and +an optional shot count. A supporting provider returns one job ID and one +lifecycle for the complete list. Result index `i` refers to input program `i`, +regardless of execution order. Results are available only after every program +succeeds. Cancellation and failure apply to the aggregate job. + +The list may contain one program. DDSIM supports this case; it does not yet +support larger lists. The superconducting model device does not execute jobs. + +```{code-cell} ipython3 +from mqt.core.qdmi import ProgramFormat + +device = open_device("mqt.ddsim.default") +program = 'OPENQASM 3.0; include "stdgates.inc"; qubit q; bit c; x q; c = measure q;' +job = device.submit_programs([program], ProgramFormat.QASM3, 32) +assert job.wait() +assert job.programs_num == 1 +assert job.get_counts(program_index=0) == {"1": 32} +``` + +Pass strings for text formats and bytes for binary formats. Binary payloads +retain every byte, including embedded NULs. Omit `num_shots` to leave the device +default unchanged. Existing single-program calls and result access without an +index continue to work; the default index is zero. + +Native multi-program submission is not concurrent submission of independent +jobs. A provider may reject lists with more than one program. Applications and +SDK integrations must then retain their separate single-program workflow; they +must not represent unrelated remote jobs as one native aggregate job. + +At the C interface, `QDMI_job_set_programs` replaces the program setter and +copies the whole list atomically. A rejected update leaves the previous list +unchanged. Result retrieval takes a program index. The C++ counterparts are +`Device::submitPrograms`, `Job::getProgramsNum`, and the indexed result methods. diff --git a/include/mqt-core/qdmi/Client.hpp b/include/mqt-core/qdmi/Client.hpp index 71fa9681f3..336dd7ba15 100644 --- a/include/mqt-core/qdmi/Client.hpp +++ b/include/mqt-core/qdmi/Client.hpp @@ -704,6 +704,33 @@ class Device { const std::optional& custom4 = std::nullopt, const std::optional& custom5 = std::nullopt) const; + /** + * @brief Submits an ordered list of textual programs as one job. + * @details QDMI copies the complete list atomically. Each submitted payload + * includes exactly one trailing null byte. + */ + [[nodiscard]] Job submitPrograms( + std::span programs, QDMI_Program_Format format, + std::optional numShots = 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) const; + + /** + * @brief Submits an ordered list of binary programs as one job. + * @details QDMI copies every payload byte atomically. + */ + [[nodiscard]] Job submitPrograms( + std::span> programs, + QDMI_Program_Format format, std::optional numShots = 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) const; + /** * @brief Retrieves an existing job by its device-provided ID. * @details Opening a job does not submit, clone, or modify the remote job. @@ -803,6 +830,24 @@ class Device { const std::optional& custom4, const std::optional& custom5) const; + [[nodiscard]] Job + submitProgramsImpl(QDMI_Program_Format format, std::span sizes, + std::span programs, + std::optional numShots, + const std::optional& custom1, + const std::optional& custom2, + const std::optional& custom3, + const std::optional& custom4, + const std::optional& custom5) const; + + static void + setCommonJobParameters(QDMI_Job job, std::optional numShots, + const std::optional& custom1, + const std::optional& custom2, + const std::optional& custom3, + const std::optional& custom4, + const std::optional& custom5); + static void setCustomJobParam(QDMI_Job job, QDMI_Job_Parameter param, const CustomJobParameter& value); @@ -866,6 +911,13 @@ class Job { /// Get the number of shots [[nodiscard]] size_t getNumShots() const; + /// Return the number of programs in the job. + [[nodiscard]] size_t getProgramsNum() const; + + /// Return an indexed result without interpreting its bytes. + [[nodiscard]] std::vector getResults(size_t programIndex, + QDMI_Job_Result result) const; + /** * @brief Gets the current number of jobs ahead of this job in its queue. * @return The queue position, or `std::nullopt` if it is unavailable or not @@ -907,12 +959,14 @@ class Job { */ template [[nodiscard]] std::optional - getCustomResult(const CustomProperty property) const { + getCustomResult(const CustomProperty property, + const size_t programIndex = 0U) const { 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); + [this, programIndex, qdmiResult](const size_t size, void* value, + size_t* sizeRet) { + return QDMI_job_get_results(job_.get(), programIndex, qdmiResult, + size, value, sizeRet); }, "custom job result " + std::to_string(static_cast(property))); } @@ -921,26 +975,30 @@ class Job { * @brief Returns the measurement shots as a vector of bitstrings. * @see QDMI_JOB_RESULT_SHOTS */ - [[nodiscard]] std::vector getShots() const; + [[nodiscard]] std::vector + getShots(size_t programIndex = 0U) const; /** * @brief Returns a map of measurement outcomes to their respective counts. * @see QDMI_JOB_RESULT_HIST_KEYS * @see QDMI_JOB_RESULT_HIST_VALUES */ - [[nodiscard]] std::map getCounts() const; + [[nodiscard]] std::map + getCounts(size_t programIndex = 0U) const; /** * @brief Returns the dense state vector as a vector of complex numbers. * @see QDMI_JOB_RESULT_STATEVECTOR_DENSE */ - [[nodiscard]] std::vector> getDenseStateVector() const; + [[nodiscard]] std::vector> + getDenseStateVector(size_t programIndex = 0U) const; /** * @brief Returns the dense probabilities as a vector of doubles. * @see QDMI_JOB_RESULT_PROBABILITIES_DENSE */ - [[nodiscard]] std::vector getDenseProbabilities() const; + [[nodiscard]] std::vector + getDenseProbabilities(size_t programIndex = 0U) const; /** * @brief Returns the sparse state vector as a map of bitstrings to complex @@ -949,7 +1007,7 @@ class Job { * @see QDMI_JOB_RESULT_STATEVECTOR_SPARSE_VALUES */ [[nodiscard]] std::map> - getSparseStateVector() const; + getSparseStateVector(size_t programIndex = 0U) const; /** * @brief Returns the sparse probabilities as a map of bitstrings to @@ -957,7 +1015,8 @@ class Job { * @see QDMI_JOB_RESULT_PROBABILITIES_SPARSE_KEYS * @see QDMI_JOB_RESULT_PROBABILITIES_SPARSE_VALUES */ - [[nodiscard]] std::map getSparseProbabilities() const; + [[nodiscard]] std::map + getSparseProbabilities(size_t programIndex = 0U) const; auto operator<=>(const Job&) const noexcept = default; diff --git a/include/mqt-core/qdmi/devices/dd/Device.hpp b/include/mqt-core/qdmi/devices/dd/Device.hpp index 871e9241d1..2a5850034d 100644 --- a/include/mqt-core/qdmi/devices/dd/Device.hpp +++ b/include/mqt-core/qdmi/devices/dd/Device.hpp @@ -193,12 +193,18 @@ struct MQT_DDSIM_QDMI_Device_Job_impl_d { /// The program format QDMI_Program_Format format_ = QDMI_PROGRAM_FORMAT_QASM3; + /// Whether the program format has been set. + bool hasFormat_ = false; + /// The quantum program associated with the job. /// Text formats (QASM2/3, QIR Base/Adaptive String) are stored as /// @c std::string; binary formats (QIR Base/Adaptive Module) are stored as /// @c std::vector. std::variant> program_; + /// Whether the program payload has been set. + bool hasProgram_ = false; + /// The number of shots for the job size_t numShots_ = 1024U; @@ -297,22 +303,15 @@ struct MQT_DDSIM_QDMI_Device_Job_impl_d { */ auto free() -> void; - /** - * @brief Sets a parameter for the job. - * @note When setting @c QDMI_DEVICE_JOB_PARAMETER_PROGRAM, the device uses - * the current @c QDMI_DEVICE_JOB_PARAMETER_PROGRAMFORMAT to decide whether - * the payload's wire @p size: - * - includes a trailing @c '\0' (text formats: QASM2, QASM3, - * QIR Base/Adaptive String) or - * - is the exact byte count (binary formats: QIR Base/Adaptive Module). - * Callers should therefore set @c PROGRAMFORMAT before @c PROGRAM. - * The default of @c QDMI_PROGRAM_FORMAT_QASM3 is assumed if @c PROGRAMFORMAT - * is not set. - * @see MQT_DDSIM_QDMI_device_job_set_parameter - */ + /// @see MQT_DDSIM_QDMI_device_job_set_parameter auto setParameter(QDMI_Device_Job_Parameter param, size_t size, const void* value) -> QDMI_STATUS; + /// @see MQT_DDSIM_QDMI_device_job_set_programs + auto setPrograms(const QDMI_Program_Format* format, size_t count, + const size_t* sizes, const void* const* programs) + -> QDMI_STATUS; + /** * @brief Queries a property of the job. * @see MQT_DDSIM_QDMI_device_job_query_property @@ -348,6 +347,6 @@ struct MQT_DDSIM_QDMI_Device_Job_impl_d { * @brief Gets the results of the job. * @see MQT_DDSIM_QDMI_device_job_get_results */ - auto getResults(QDMI_Job_Result result, size_t size, void* data, - size_t* sizeRet) -> QDMI_STATUS; + auto getResults(size_t programIndex, QDMI_Job_Result result, size_t size, + void* data, size_t* sizeRet) -> QDMI_STATUS; }; diff --git a/include/mqt-core/qdmi/devices/sc/Device.hpp b/include/mqt-core/qdmi/devices/sc/Device.hpp index 18d2ebc79d..c6085bb553 100644 --- a/include/mqt-core/qdmi/devices/sc/Device.hpp +++ b/include/mqt-core/qdmi/devices/sc/Device.hpp @@ -68,14 +68,16 @@ struct MQT_SC_QDMI_Device_Job_impl_d { void free(); int setParameter(QDMI_Device_Job_Parameter parameter, size_t size, const void* value); + int setPrograms(const QDMI_Program_Format* format, size_t count, + const size_t* sizes, const void* const* programs); int queryProperty(QDMI_Device_Job_Property property, size_t size, void* value, size_t* sizeRet); int submit(); int cancel(); int check(QDMI_Job_Status* status); int wait(size_t timeout); - int getResults(QDMI_Job_Result result, size_t size, void* data, - size_t* sizeRet); + int getResults(size_t programIndex, QDMI_Job_Result result, size_t size, + void* data, size_t* sizeRet); }; struct MQT_SC_QDMI_Device_Session_impl_d { diff --git a/include/mqt-core/qdmi/driver/Driver.hpp b/include/mqt-core/qdmi/driver/Driver.hpp index 3e3d740bfb..14bbbef622 100644 --- a/include/mqt-core/qdmi/driver/Driver.hpp +++ b/include/mqt-core/qdmi/driver/Driver.hpp @@ -130,6 +130,8 @@ struct DeviceLibrary { decltype(QDMI_device_job_free)* device_job_free{}; /// Function pointer to @ref QDMI_device_job_set_parameter. decltype(QDMI_device_job_set_parameter)* device_job_set_parameter{}; + /// Function pointer to @ref QDMI_device_job_set_programs. + decltype(QDMI_device_job_set_programs)* device_job_set_programs{}; /// Function pointer to @ref QDMI_device_job_query_property. decltype(QDMI_device_job_query_property)* device_job_query_property{}; /// Function pointer to @ref QDMI_device_job_submit. @@ -346,6 +348,11 @@ struct QDMI_Job_impl_d { auto setParameter(QDMI_Job_Parameter param, size_t size, const void* value) const -> int; + /// @see QDMI_job_set_programs + auto setPrograms(const QDMI_Program_Format* format, size_t count, + const size_t* sizes, const void* const* programs) const + -> int; + /** * @brief Queries a property of the job. * @see QDMI_job_query_property @@ -382,8 +389,8 @@ struct QDMI_Job_impl_d { * @brief Gets the results of the job. * @see QDMI_job_get_results */ - auto getResults(QDMI_Job_Result result, size_t size, void* data, - size_t* sizeRet) const -> int; + auto getResults(size_t programIndex, QDMI_Job_Result result, size_t size, + void* data, size_t* sizeRet) const -> int; /** * @brief Frees the job. diff --git a/python/mqt/core/qdmi/__init__.pyi b/python/mqt/core/qdmi/__init__.pyi index 04ad90bf02..b99f906ead 100644 --- a/python/mqt/core/qdmi/__init__.pyi +++ b/python/mqt/core/qdmi/__init__.pyi @@ -34,22 +34,25 @@ class Job: def cancel(self) -> None: """Cancels the job.""" - def get_shots(self) -> list[str]: + def get_results(self, program_index: int, result: Job.Result) -> bytes: + """Returns one indexed result as exact bytes.""" + + def get_shots(self, program_index: int = 0) -> list[str]: """Returns the raw shot results from the job.""" - def get_counts(self) -> dict[str, int]: + def get_counts(self, program_index: int = 0) -> dict[str, int]: """Returns the measurement counts from the job.""" - def get_dense_statevector(self) -> list[complex]: + def get_dense_statevector(self, program_index: int = 0) -> list[complex]: """Returns the dense statevector from the job (typically only available from simulator devices).""" - def get_dense_probabilities(self) -> list[float]: + def get_dense_probabilities(self, program_index: int = 0) -> list[float]: """Returns the dense probabilities from the job (typically only available from simulator devices).""" - def get_sparse_statevector(self) -> dict[str, complex]: + def get_sparse_statevector(self, program_index: int = 0) -> dict[str, complex]: """Returns the sparse statevector from the job (typically only available from simulator devices).""" - def get_sparse_probabilities(self) -> dict[str, float]: + def get_sparse_probabilities(self, program_index: int = 0) -> dict[str, float]: """Returns the sparse probabilities from the job (typically only available from simulator devices).""" @overload @@ -110,6 +113,10 @@ class Job: def program_bytes(self) -> bytes: """The exact bytes of the submitted program.""" + @property + def programs_num(self) -> int: + """The number of programs in the job.""" + @property def num_shots(self) -> int: """The number of shots.""" @@ -138,6 +145,27 @@ class Job: FAILED = 6 + class Result(enum.Enum): + """One raw job result format.""" + + SHOTS = 0 + + HIST_KEYS = 1 + + HIST_VALUES = 2 + + STATEVECTOR_DENSE = 3 + + PROBABILITIES_DENSE = 4 + + STATEVECTOR_SPARSE_KEYS = 5 + + STATEVECTOR_SPARSE_VALUES = 6 + + PROBABILITIES_SPARSE_KEYS = 7 + + PROBABILITIES_SPARSE_VALUES = 8 + class ProgramFormat(enum.Enum): """Enumeration of program formats.""" @@ -350,6 +378,36 @@ class Device: executes no circuit, so it takes no shot count. """ + @overload + def submit_programs( + self, + programs: Sequence[str], + program_format: ProgramFormat, + num_shots: int | None = None, + *, + custom1: str | bool | float | None = None, + custom2: str | bool | float | None = None, + custom3: str | bool | float | None = None, + custom4: str | bool | float | None = None, + custom5: str | bool | float | None = None, + ) -> Job: + """Submits an ordered list of text programs atomically.""" + + @overload + def submit_programs( + self, + programs: Sequence[bytes], + program_format: ProgramFormat, + num_shots: int | None = None, + *, + custom1: str | bool | float | None = None, + custom2: str | bool | float | None = None, + custom3: str | bool | float | None = None, + custom4: str | bool | float | None = None, + custom5: str | bool | float | None = None, + ) -> Job: + """Submits an ordered list of exact byte programs atomically.""" + def retrieve_job_by_id(self, job_id: str) -> Job: """Retrieves an existing job by its device-provided ID.""" diff --git a/src/qdmi/Client.cpp b/src/qdmi/Client.cpp index f960114667..3f723e3c03 100644 --- a/src/qdmi/Client.cpp +++ b/src/qdmi/Client.cpp @@ -39,14 +39,15 @@ namespace qdmi { namespace { /// Rejects the formats that `submitJob` cannot carry. -/// A batch job's program is a list of job handles rather than a byte blob, so -/// this API cannot express it at all. A calibration run has its own entry -/// point, because its payload is optional and it takes no shot count. +/// The legacy BATCHJOB pseudo-format carries job handles, not program bytes. +/// Native program-list submission uses submitPrograms instead. Calibration has +/// its own entry point, because its payload is optional and it takes no shot +/// count. void rejectUnsupportedProgramFormat(const QDMI_Program_Format format) { if (format == QDMI_PROGRAM_FORMAT_BATCHJOB) { throw std::invalid_argument( - "MQT Core does not support batch jobs. A batch job's program is a list " - "of job handles, which this API cannot express"); + "The legacy BATCHJOB format carries job handles, not program bytes. " + "Use submitPrograms for native program-list submission"); } if (format == QDMI_PROGRAM_FORMAT_CALIBRATION) { throw std::invalid_argument( @@ -427,6 +428,52 @@ Job Device::submitJob(const std::span program, custom4, custom5); } +Job Device::submitPrograms( + const std::span programs, + const QDMI_Program_Format format, const std::optional numShots, + const std::optional& custom1, + const std::optional& custom2, + const std::optional& custom3, + const std::optional& custom4, + const std::optional& custom5) const { + rejectUnsupportedProgramFormat(format); + if (isBinaryProgramFormat(format)) { + throw std::invalid_argument( + "Binary program formats require exact-byte submission"); + } + std::vector sizes; + std::vector pointers; + sizes.reserve(programs.size()); + pointers.reserve(programs.size()); + for (const auto& program : programs) { + sizes.emplace_back(program.size() + 1U); + pointers.emplace_back(program.c_str()); + } + return submitProgramsImpl(format, sizes, pointers, numShots, custom1, custom2, + custom3, custom4, custom5); +} + +Job Device::submitPrograms( + const std::span> programs, + const QDMI_Program_Format format, const std::optional numShots, + const std::optional& custom1, + const std::optional& custom2, + const std::optional& custom3, + const std::optional& custom4, + const std::optional& custom5) const { + rejectUnsupportedProgramFormat(format); + std::vector sizes; + std::vector pointers; + sizes.reserve(programs.size()); + pointers.reserve(programs.size()); + for (const auto& program : programs) { + sizes.emplace_back(program.size()); + pointers.emplace_back(program.data()); + } + return submitProgramsImpl(format, sizes, pointers, numShots, custom1, custom2, + custom3, custom4, custom5); +} + Job Device::submitJobImpl( const QDMI_Program_Format format, const std::optional> program, @@ -441,41 +488,74 @@ Job Device::submitJobImpl( "Creating job"); Job jobWrapper{job, device_}; - qdmi::throwIfError(QDMI_job_set_parameter(jobWrapper, - QDMI_JOB_PARAMETER_PROGRAMFORMAT, - sizeof(format), &format), - "Setting program format"); if (program.has_value()) { + const size_t size = program->size(); + const void* data = program->data(); + qdmi::throwIfError( + QDMI_job_set_programs(jobWrapper, &format, 1U, &size, &data), + "Setting program"); + } else { qdmi::throwIfError(QDMI_job_set_parameter(jobWrapper, - QDMI_JOB_PARAMETER_PROGRAM, - program->size(), program->data()), - "Setting program"); + QDMI_JOB_PARAMETER_PROGRAMFORMAT, + sizeof(format), &format), + "Setting program format"); } + setCommonJobParameters(jobWrapper, numShots, custom1, custom2, custom3, + custom4, custom5); + + qdmi::throwIfError(QDMI_job_submit(jobWrapper), "Submitting job"); + return jobWrapper; +} + +Job Device::submitProgramsImpl( + const QDMI_Program_Format format, const std::span sizes, + const std::span programs, + const std::optional numShots, + const std::optional& custom1, + const std::optional& custom2, + const std::optional& custom3, + 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(QDMI_job_set_programs(jobWrapper, &format, programs.size(), + sizes.data(), programs.data()), + "Setting programs"); + setCommonJobParameters(jobWrapper, numShots, custom1, custom2, custom3, + custom4, custom5); + qdmi::throwIfError(QDMI_job_submit(jobWrapper), "Submitting job"); + return jobWrapper; +} + +void Device::setCommonJobParameters( + QDMI_Job job, const std::optional numShots, + const std::optional& custom1, + const std::optional& custom2, + const std::optional& custom3, + const std::optional& custom4, + const std::optional& custom5) { if (numShots.has_value()) { - qdmi::throwIfError(QDMI_job_set_parameter(jobWrapper, - QDMI_JOB_PARAMETER_SHOTSNUM, + qdmi::throwIfError(QDMI_job_set_parameter(job, QDMI_JOB_PARAMETER_SHOTSNUM, sizeof(*numShots), &*numShots), "Setting number of shots"); } - if (custom1.has_value()) { - setCustomJobParam(jobWrapper, QDMI_JOB_PARAMETER_CUSTOM1, *custom1); + setCustomJobParam(job, QDMI_JOB_PARAMETER_CUSTOM1, *custom1); } if (custom2.has_value()) { - setCustomJobParam(jobWrapper, QDMI_JOB_PARAMETER_CUSTOM2, *custom2); + setCustomJobParam(job, QDMI_JOB_PARAMETER_CUSTOM2, *custom2); } if (custom3.has_value()) { - setCustomJobParam(jobWrapper, QDMI_JOB_PARAMETER_CUSTOM3, *custom3); + setCustomJobParam(job, QDMI_JOB_PARAMETER_CUSTOM3, *custom3); } if (custom4.has_value()) { - setCustomJobParam(jobWrapper, QDMI_JOB_PARAMETER_CUSTOM4, *custom4); + setCustomJobParam(job, QDMI_JOB_PARAMETER_CUSTOM4, *custom4); } if (custom5.has_value()) { - setCustomJobParam(jobWrapper, QDMI_JOB_PARAMETER_CUSTOM5, *custom5); + setCustomJobParam(job, QDMI_JOB_PARAMETER_CUSTOM5, *custom5); } - - qdmi::throwIfError(QDMI_job_submit(jobWrapper), "Submitting job"); - return jobWrapper; } Job Device::submitCalibrationJob( @@ -630,6 +710,15 @@ size_t Job::getNumShots() const { return numShots; } +size_t Job::getProgramsNum() const { + size_t count = 0; + qdmi::throwIfError(QDMI_job_query_property(job_.get(), + QDMI_JOB_PROPERTY_PROGRAMSNUM, + sizeof(count), &count, nullptr), + "Querying program count"); + return count; +} + std::optional Job::getQueuePosition() const { size_t queuePosition = 0; const auto result = @@ -638,10 +727,26 @@ std::optional Job::getQueuePosition() const { return detail::queuePositionFromResult(result, queuePosition); } -std::vector Job::getShots() const { +std::vector Job::getResults(const size_t programIndex, + const QDMI_Job_Result result) const { + size_t size = 0U; + qdmi::throwIfError(QDMI_job_get_results(job_.get(), programIndex, result, 0U, + nullptr, &size), + "Querying result size"); + std::vector value(size); + if (size != 0U) { + qdmi::throwIfError(QDMI_job_get_results(job_.get(), programIndex, result, + size, value.data(), nullptr), + "Querying result"); + } + return value; +} + +std::vector Job::getShots(const size_t programIndex) const { size_t shotsSize = 0; - qdmi::throwIfError(QDMI_job_get_results(job_.get(), QDMI_JOB_RESULT_SHOTS, 0, - nullptr, &shotsSize), + qdmi::throwIfError(QDMI_job_get_results(job_.get(), programIndex, + QDMI_JOB_RESULT_SHOTS, 0, nullptr, + &shotsSize), "Querying shots size"); if (shotsSize == 0) { @@ -649,19 +754,21 @@ 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(QDMI_job_get_results(job_.get(), programIndex, + QDMI_JOB_RESULT_SHOTS, shotsSize, + shots.data(), nullptr), "Querying shots"); shots.pop_back(); return detail::parseShots(shots, getNumShots()); } -std::map Job::getCounts() const { +std::map Job::getCounts(const size_t programIndex) 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(QDMI_job_get_results(job_.get(), programIndex, + QDMI_JOB_RESULT_HIST_KEYS, 0, nullptr, + &keysSize), "Querying histogram keys size"); if (keysSize == 0) { @@ -669,14 +776,15 @@ 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(QDMI_job_get_results(job_.get(), programIndex, + 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::throwIfError(QDMI_job_get_results(job_.get(), programIndex, QDMI_JOB_RESULT_HIST_VALUES, 0, nullptr, &valuesSize), "Querying histogram values size"); @@ -687,7 +795,7 @@ std::map Job::getCounts() const { } std::vector values(valuesSize / sizeof(size_t)); - qdmi::throwIfError(QDMI_job_get_results(job_.get(), + qdmi::throwIfError(QDMI_job_get_results(job_.get(), programIndex, QDMI_JOB_RESULT_HIST_VALUES, valuesSize, values.data(), nullptr), "Querying histogram values"); @@ -715,9 +823,10 @@ std::map Job::getCounts() const { return counts; } -std::vector> Job::getDenseStateVector() const { +std::vector> +Job::getDenseStateVector(const size_t programIndex) const { size_t size = 0; - qdmi::throwIfError(QDMI_job_get_results(job_.get(), + qdmi::throwIfError(QDMI_job_get_results(job_.get(), programIndex, QDMI_JOB_RESULT_STATEVECTOR_DENSE, 0, nullptr, &size), "Querying dense state vector size"); @@ -729,16 +838,17 @@ std::vector> Job::getDenseStateVector() const { std::vector> stateVector(size / sizeof(std::complex)); - qdmi::throwIfError(QDMI_job_get_results(job_.get(), + qdmi::throwIfError(QDMI_job_get_results(job_.get(), programIndex, QDMI_JOB_RESULT_STATEVECTOR_DENSE, size, stateVector.data(), nullptr), "Querying dense state vector"); return stateVector; } -std::vector Job::getDenseProbabilities() const { +std::vector +Job::getDenseProbabilities(const size_t programIndex) const { size_t size = 0; - qdmi::throwIfError(QDMI_job_get_results(job_.get(), + qdmi::throwIfError(QDMI_job_get_results(job_.get(), programIndex, QDMI_JOB_RESULT_PROBABILITIES_DENSE, 0, nullptr, &size), "Querying dense probabilities size"); @@ -749,18 +859,20 @@ std::vector Job::getDenseProbabilities() const { } std::vector probabilities(size / sizeof(double)); - qdmi::throwIfError(QDMI_job_get_results(job_.get(), + qdmi::throwIfError(QDMI_job_get_results(job_.get(), programIndex, QDMI_JOB_RESULT_PROBABILITIES_DENSE, size, probabilities.data(), nullptr), "Querying dense probabilities"); return probabilities; } -std::map> Job::getSparseStateVector() const { +std::map> +Job::getSparseStateVector(const size_t programIndex) const { size_t keysSize = 0; qdmi::throwIfError( - QDMI_job_get_results(job_.get(), QDMI_JOB_RESULT_STATEVECTOR_SPARSE_KEYS, - 0, nullptr, &keysSize), + QDMI_job_get_results(job_.get(), programIndex, + QDMI_JOB_RESULT_STATEVECTOR_SPARSE_KEYS, 0, nullptr, + &keysSize), "Querying sparse state vector keys size"); if (keysSize == 0) { @@ -769,16 +881,18 @@ 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), + QDMI_job_get_results(job_.get(), programIndex, + 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( + QDMI_job_get_results(job_.get(), programIndex, + 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 +902,11 @@ 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( + QDMI_job_get_results(job_.get(), programIndex, + QDMI_JOB_RESULT_STATEVECTOR_SPARSE_VALUES, + valuesSize, values.data(), nullptr), + "Querying sparse state vector values"); // Parse the keys (comma-separated) std::map> stateVector; @@ -816,27 +931,30 @@ std::map> Job::getSparseStateVector() const { return stateVector; } -std::map Job::getSparseProbabilities() const { +std::map +Job::getSparseProbabilities(const size_t programIndex) 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( + QDMI_job_get_results(job_.get(), programIndex, + 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"); + qdmi::throwIfError( + QDMI_job_get_results(job_.get(), programIndex, + QDMI_JOB_RESULT_PROBABILITIES_SPARSE_KEYS, keysSize, + keys.data(), nullptr), + "Querying sparse probabilities keys"); keys.pop_back(); size_t valuesSize = 0; qdmi::throwIfError( - QDMI_job_get_results(job_.get(), + QDMI_job_get_results(job_.get(), programIndex, QDMI_JOB_RESULT_PROBABILITIES_SPARSE_VALUES, 0, nullptr, &valuesSize), "Querying sparse probabilities values size"); @@ -848,7 +966,7 @@ std::map Job::getSparseProbabilities() const { std::vector values(valuesSize / sizeof(double)); qdmi::throwIfError( - QDMI_job_get_results(job_.get(), + QDMI_job_get_results(job_.get(), programIndex, QDMI_JOB_RESULT_PROBABILITIES_SPARSE_VALUES, valuesSize, values.data(), nullptr), "Querying sparse probabilities values"); diff --git a/src/qdmi/devices/dd/Device.cpp b/src/qdmi/devices/dd/Device.cpp index 16d61150b5..866b29e537 100644 --- a/src/qdmi/devices/dd/Device.cpp +++ b/src/qdmi/devices/dd/Device.cpp @@ -56,6 +56,7 @@ namespace { constexpr uintptr_t OFFSET = 0x10000U; + template constexpr std::array iotaArray() { std::array result{}; std::iota(result.begin(), result.end(), OFFSET); @@ -248,7 +249,7 @@ auto Device::queryProperty(const QDMI_Device_Property prop, const size_t size, status_.load(), prop, size, value, sizeRet) ADD_SINGLE_VALUE_PROPERTY(QDMI_DEVICE_PROPERTY_QUBITSNUM, size_t, qubitsNum_, prop, size, value, sizeRet) - // This device never needs calibration + /// This device never needs calibration. ADD_SINGLE_VALUE_PROPERTY(QDMI_DEVICE_PROPERTY_NEEDSCALIBRATION, size_t, 0, prop, size, value, sizeRet) // This device does not support pulse-level control @@ -421,6 +422,9 @@ auto MQT_DDSIM_QDMI_Device_Job_impl_d::setParameter( switch (param) { case QDMI_DEVICE_JOB_PARAMETER_PROGRAMFORMAT: if (value != nullptr) { + if (size != sizeof(QDMI_Program_Format)) { + return QDMI_ERROR_INVALIDARGUMENT; + } const auto format = *static_cast(value); if (IS_INVALID_ARGUMENT(format, QDMI_PROGRAM_FORMAT)) { return QDMI_ERROR_INVALIDARGUMENT; @@ -429,33 +433,11 @@ auto MQT_DDSIM_QDMI_Device_Job_impl_d::setParameter( SUPPORTED_PROGRAM_FORMATS.end()) { return QDMI_ERROR_NOTSUPPORTED; } - format_ = format; - } - return QDMI_SUCCESS; - case QDMI_DEVICE_JOB_PARAMETER_PROGRAM: - if (value != nullptr) { - const bool isTextProgramFormat = - format_ == QDMI_PROGRAM_FORMAT_QASM2 || - format_ == QDMI_PROGRAM_FORMAT_QASM3 || - format_ == QDMI_PROGRAM_FORMAT_QIRBASESTRING || - format_ == QDMI_PROGRAM_FORMAT_QIRADAPTIVESTRING; - if (isTextProgramFormat) { - // Text payloads include the trailing '\0' in `size`. - // Strip it so it is not counted in the stored string's size. - const std::span text{static_cast(value), size}; - if (text.empty() || text.back() != '\0') { - return QDMI_ERROR_INVALIDARGUMENT; - } - const auto contents = text.first(text.size() - 1); - if (std::ranges::find(contents, '\0') != contents.end()) { - return QDMI_ERROR_INVALIDARGUMENT; - } - program_ = std::string(contents.begin(), contents.end()); - } else { - // Binary payloads are stored exactly as received. - const std::span bytes(static_cast(value), size); - program_ = std::vector(bytes.begin(), bytes.end()); + if (format_ != format) { + hasProgram_ = false; } + format_ = format; + hasFormat_ = true; } return QDMI_SUCCESS; case QDMI_DEVICE_JOB_PARAMETER_SHOTSNUM: @@ -476,6 +458,65 @@ auto MQT_DDSIM_QDMI_Device_Job_impl_d::setParameter( return QDMI_ERROR_NOTSUPPORTED; } } + +auto MQT_DDSIM_QDMI_Device_Job_impl_d::setPrograms( + const QDMI_Program_Format* const format, const size_t count, + const size_t* const sizes, const void* const* const programs) + -> QDMI_STATUS { + if (format == nullptr || count == 0U) { + return QDMI_ERROR_INVALIDARGUMENT; + } + if (status_.load() != QDMI_JOB_STATUS_CREATED) { + return QDMI_ERROR_BADSTATE; + } + if (IS_INVALID_ARGUMENT(*format, QDMI_PROGRAM_FORMAT)) { + return QDMI_ERROR_INVALIDARGUMENT; + } + if (count != 1U || std::ranges::none_of(SUPPORTED_PROGRAM_FORMATS, + [&](const auto& supported) { + return (*format == supported); + })) { + return QDMI_ERROR_NOTSUPPORTED; + } + if (programs == nullptr) { + return QDMI_SUCCESS; + } + if (sizes == nullptr) { + return QDMI_ERROR_INVALIDARGUMENT; + } + const std::span programSizes{sizes, count}; + const std::span programPointers{programs, count}; + if (programSizes.front() == 0U || programPointers.front() == nullptr) { + return QDMI_ERROR_INVALIDARGUMENT; + } + + std::variant> program; + if ((*format == QDMI_PROGRAM_FORMAT_QIRBASEMODULE || + *format == QDMI_PROGRAM_FORMAT_QIRADAPTIVEMODULE)) { + const std::span bytes{ + static_cast(programPointers.front()), + programSizes.front()}; + program = std::vector(bytes.begin(), bytes.end()); + } else { + const std::span text{static_cast(programPointers.front()), + programSizes.front()}; + if (text.back() != '\0') { + return QDMI_ERROR_INVALIDARGUMENT; + } + const auto contents = text.first(text.size() - 1U); + if (std::ranges::find(contents, '\0') != contents.end()) { + return QDMI_ERROR_INVALIDARGUMENT; + } + program = std::string(contents.begin(), contents.end()); + } + + format_ = *format; + program_ = std::move(program); + hasFormat_ = true; + hasProgram_ = true; + return QDMI_SUCCESS; +} + auto MQT_DDSIM_QDMI_Device_Job_impl_d::queryProperty( // NOLINTNEXTLINE(readability-non-const-parameter) const QDMI_Device_Job_Property prop, const size_t size, void* value, @@ -487,9 +528,17 @@ auto MQT_DDSIM_QDMI_Device_Job_impl_d::queryProperty( const auto str = std::to_string(id_); ADD_STRING_PROPERTY(QDMI_DEVICE_JOB_PROPERTY_ID, str.c_str(), prop, size, value, sizeRet) - ADD_SINGLE_VALUE_PROPERTY(QDMI_DEVICE_JOB_PROPERTY_PROGRAMFORMAT, - QDMI_Program_Format, format_, prop, size, value, - sizeRet) + if (prop == QDMI_DEVICE_JOB_PROPERTY_PROGRAMFORMAT) { + if (!hasFormat_) { + return QDMI_ERROR_BADSTATE; + } + ADD_SINGLE_VALUE_PROPERTY(QDMI_DEVICE_JOB_PROPERTY_PROGRAMFORMAT, + QDMI_Program_Format, format_, prop, size, value, + sizeRet) + } + if (prop == QDMI_DEVICE_JOB_PROPERTY_PROGRAM && !hasProgram_) { + return QDMI_ERROR_BADSTATE; + } if (std::holds_alternative(program_)) { const auto& text = std::get(program_); ADD_STRING_PROPERTY(QDMI_DEVICE_JOB_PROPERTY_PROGRAM, text.c_str(), prop, @@ -501,6 +550,14 @@ auto MQT_DDSIM_QDMI_Device_Job_impl_d::queryProperty( } ADD_SINGLE_VALUE_PROPERTY(QDMI_DEVICE_JOB_PROPERTY_SHOTSNUM, size_t, numShots_, prop, size, value, sizeRet) + if (prop == QDMI_DEVICE_JOB_PROPERTY_PROGRAMSNUM) { + if (!hasProgram_) { + return QDMI_ERROR_BADSTATE; + } + constexpr size_t programsNum = 1U; + ADD_SINGLE_VALUE_PROPERTY(QDMI_DEVICE_JOB_PROPERTY_PROGRAMSNUM, size_t, + programsNum, prop, size, value, sizeRet) + } return QDMI_ERROR_NOTSUPPORTED; } auto MQT_DDSIM_QDMI_Device_Job_impl_d::submitProgramAsync( @@ -611,8 +668,8 @@ auto MQT_DDSIM_QDMI_Device_Job_impl_d::submitQIRProgramStateExtraction() // State extraction stops the entry point at its first irreversible operation. // This preserves Base Profile semantics because measurements are terminal, // whereas Adaptive Profile measurements may feed later quantum control. - if (format_ != QDMI_PROGRAM_FORMAT_QIRBASEMODULE && - format_ != QDMI_PROGRAM_FORMAT_QIRBASESTRING) { + if (!(format_ == QDMI_PROGRAM_FORMAT_QIRBASEMODULE) && + !(format_ == QDMI_PROGRAM_FORMAT_QIRBASESTRING)) { return QDMI_ERROR_NOTSUPPORTED; } return submitProgramAsync([this] { @@ -641,9 +698,12 @@ auto MQT_DDSIM_QDMI_Device_Job_impl_d::submit() -> QDMI_STATUS { if (status_.load() != QDMI_JOB_STATUS_CREATED) { return QDMI_ERROR_BADSTATE; } + if (!hasFormat_ || !hasProgram_) { + return QDMI_ERROR_BADSTATE; + } status_.store(QDMI_JOB_STATUS_SUBMITTED); - if (format_ == QDMI_PROGRAM_FORMAT_QASM2 || - format_ == QDMI_PROGRAM_FORMAT_QASM3) { + if ((format_ == QDMI_PROGRAM_FORMAT_QASM2) || + (format_ == QDMI_PROGRAM_FORMAT_QASM3)) { return submitQASMProgram(); } return submitQIRProgram(); @@ -899,10 +959,14 @@ auto MQT_DDSIM_QDMI_Device_Job_impl_d::getProbabilities(const size_t size, } return QDMI_SUCCESS; } -auto MQT_DDSIM_QDMI_Device_Job_impl_d::getResults(const QDMI_Job_Result result, +auto MQT_DDSIM_QDMI_Device_Job_impl_d::getResults(const size_t programIndex, + const QDMI_Job_Result result, const size_t size, void* data, size_t* sizeRet) -> QDMI_STATUS { + if (programIndex != 0U) { + return QDMI_ERROR_OUTOFRANGE; + } if (IS_INVALID_ARGUMENT(result, QDMI_JOB_RESULT)) { return QDMI_ERROR_INVALIDARGUMENT; } @@ -1015,6 +1079,17 @@ int MQT_DDSIM_QDMI_device_job_query_property( return job->queryProperty(prop, size, value, size_ret); } +int MQT_DDSIM_QDMI_device_job_set_programs(MQT_DDSIM_QDMI_Device_Job job, + const QDMI_Program_Format* format, + const size_t count, + const size_t* sizes, + const void* const* programs) { + if (job == nullptr) { + return QDMI_ERROR_INVALIDARGUMENT; + } + return job->setPrograms(format, count, sizes, programs); +} + int MQT_DDSIM_QDMI_device_job_submit(MQT_DDSIM_QDMI_Device_Job job) { if (job == nullptr) { return QDMI_ERROR_INVALIDARGUMENT; @@ -1046,13 +1121,14 @@ int MQT_DDSIM_QDMI_device_job_wait(MQT_DDSIM_QDMI_Device_Job job, } int MQT_DDSIM_QDMI_device_job_get_results(MQT_DDSIM_QDMI_Device_Job job, + const size_t programIndex, QDMI_Job_Result result, const size_t size, void* data, size_t* size_ret) { if (job == nullptr) { return QDMI_ERROR_INVALIDARGUMENT; } - return job->getResults(result, size, data, size_ret); + return job->getResults(programIndex, result, size, data, size_ret); } int MQT_DDSIM_QDMI_device_session_query_device_property( diff --git a/src/qdmi/devices/sc/Device.cpp b/src/qdmi/devices/sc/Device.cpp index 3b1b01ea2b..a8c7432586 100644 --- a/src/qdmi/devices/sc/Device.cpp +++ b/src/qdmi/devices/sc/Device.cpp @@ -422,6 +422,16 @@ int MQT_SC_QDMI_Device_Job_impl_d::setParameter( return QDMI_ERROR_NOTSUPPORTED; } // NOLINTNEXTLINE(readability-convert-member-functions-to-static) +int MQT_SC_QDMI_Device_Job_impl_d::setPrograms( + const QDMI_Program_Format* const format, const size_t count, + [[maybe_unused]] const size_t* const sizes, + [[maybe_unused]] const void* const* const programs) { + if (format == nullptr || count == 0U) { + return QDMI_ERROR_INVALIDARGUMENT; + } + return QDMI_ERROR_NOTSUPPORTED; +} +// NOLINTNEXTLINE(readability-convert-member-functions-to-static) int MQT_SC_QDMI_Device_Job_impl_d::queryProperty( const QDMI_Device_Job_Property property, const size_t size, void* value, size_t* /*sizeRet*/) { @@ -445,7 +455,8 @@ int MQT_SC_QDMI_Device_Job_impl_d::wait(size_t /*timeout*/) { return QDMI_ERROR_NOTSUPPORTED; } // NOLINTNEXTLINE(readability-convert-member-functions-to-static) -int MQT_SC_QDMI_Device_Job_impl_d::getResults(const QDMI_Job_Result result, +int MQT_SC_QDMI_Device_Job_impl_d::getResults(const size_t /*programIndex*/, + const QDMI_Job_Result result, const size_t size, void* data, size_t* /*sizeRet*/) { if ((data != nullptr && size == 0) || @@ -501,6 +512,13 @@ int MQT_SC_QDMI_device_job_set_parameter( return job == nullptr ? QDMI_ERROR_INVALIDARGUMENT : job->setParameter(parameter, size, value); } +int MQT_SC_QDMI_device_job_set_programs(MQT_SC_QDMI_Device_Job job, + const QDMI_Program_Format* format, + const size_t count, const size_t* sizes, + const void* const* programs) { + return job == nullptr ? QDMI_ERROR_INVALIDARGUMENT + : job->setPrograms(format, count, sizes, programs); +} int MQT_SC_QDMI_device_job_query_property( MQT_SC_QDMI_Device_Job job, const QDMI_Device_Job_Property property, const size_t size, void* value, size_t* sizeRet) { @@ -522,11 +540,13 @@ int MQT_SC_QDMI_device_job_wait(MQT_SC_QDMI_Device_Job job, return job == nullptr ? QDMI_ERROR_INVALIDARGUMENT : job->wait(timeout); } int MQT_SC_QDMI_device_job_get_results(MQT_SC_QDMI_Device_Job job, + const size_t programIndex, const QDMI_Job_Result result, const size_t size, void* data, size_t* sizeRet) { - return job == nullptr ? QDMI_ERROR_INVALIDARGUMENT - : job->getResults(result, size, data, sizeRet); + return job == nullptr + ? QDMI_ERROR_INVALIDARGUMENT + : job->getResults(programIndex, result, size, data, sizeRet); } int MQT_SC_QDMI_device_session_query_device_property( MQT_SC_QDMI_Device_Session session, const QDMI_Device_Property property, diff --git a/src/qdmi/driver/Driver.cpp b/src/qdmi/driver/Driver.cpp index dbcbfc8cc9..48fdd1c5e0 100644 --- a/src/qdmi/driver/Driver.cpp +++ b/src/qdmi/driver/Driver.cpp @@ -146,6 +146,7 @@ DynamicDeviceLibrary::DynamicDeviceLibrary(const std::string& libName, LOAD_OPTIONAL_DYNAMIC_SYMBOL(device_session_retrieve_device_job_by_id) LOAD_DYNAMIC_SYMBOL(device_job_free) LOAD_DYNAMIC_SYMBOL(device_job_set_parameter) + LOAD_DYNAMIC_SYMBOL(device_job_set_programs) LOAD_DYNAMIC_SYMBOL(device_job_query_property) LOAD_DYNAMIC_SYMBOL(device_job_submit) LOAD_DYNAMIC_SYMBOL(device_job_cancel) @@ -494,8 +495,6 @@ namespace { [[nodiscard]] auto toDeviceJobParameter(const QDMI_Job_Parameter& param) -> QDMI_Device_Job_Parameter { switch (param) { - case QDMI_JOB_PARAMETER_PROGRAM: - return QDMI_DEVICE_JOB_PARAMETER_PROGRAM; case QDMI_JOB_PARAMETER_PROGRAMFORMAT: return QDMI_DEVICE_JOB_PARAMETER_PROGRAMFORMAT; case QDMI_JOB_PARAMETER_SHOTSNUM: @@ -540,6 +539,14 @@ auto QDMI_Job_impl_d::setParameter(QDMI_Job_Parameter param, const size_t size, deviceJob_, toDeviceJobParameter(param), size, value); } +auto QDMI_Job_impl_d::setPrograms(const QDMI_Program_Format* const format, + const size_t count, const size_t* const sizes, + const void* const* const programs) const + -> int { + return device_->getLibrary().device_job_set_programs(deviceJob_, format, + count, sizes, programs); +} + namespace { [[nodiscard]] auto toDeviceJobProperty(const QDMI_Job_Property& prop) -> QDMI_Device_Job_Property { @@ -554,6 +561,8 @@ namespace { return QDMI_DEVICE_JOB_PROPERTY_SHOTSNUM; case QDMI_JOB_PROPERTY_QUEUEPOSITION: return QDMI_DEVICE_JOB_PROPERTY_QUEUEPOSITION; + case QDMI_JOB_PROPERTY_PROGRAMSNUM: + return QDMI_DEVICE_JOB_PROPERTY_PROGRAMSNUM; case QDMI_JOB_PROPERTY_CUSTOM1: return QDMI_DEVICE_JOB_PROPERTY_CUSTOM1; case QDMI_JOB_PROPERTY_CUSTOM2: @@ -592,10 +601,11 @@ auto QDMI_Job_impl_d::wait(size_t timeout) const -> int { return device_->getLibrary().device_job_wait(deviceJob_, timeout); } -auto QDMI_Job_impl_d::getResults(QDMI_Job_Result result, const size_t size, +auto QDMI_Job_impl_d::getResults(const size_t programIndex, + QDMI_Job_Result result, const size_t size, void* data, size_t* sizeRet) const -> int { - return device_->getLibrary().device_job_get_results(deviceJob_, result, size, - data, sizeRet); + return device_->getLibrary().device_job_get_results( + deviceJob_, programIndex, result, size, data, sizeRet); } auto QDMI_Job_impl_d::free() -> void { device_->freeJob(this); } @@ -948,6 +958,15 @@ int QDMI_job_set_parameter(QDMI_Job job, QDMI_Job_Parameter param, return job->setParameter(param, size, value); } +int QDMI_job_set_programs(QDMI_Job job, const QDMI_Program_Format* format, + const size_t count, const size_t* sizes, + const void* const* programs) { + if (job == nullptr) { + return QDMI_ERROR_INVALIDARGUMENT; + } + return job->setPrograms(format, count, sizes, programs); +} + int QDMI_job_query_property(QDMI_Job job, QDMI_Job_Property prop, const size_t size, void* value, size_t* sizeRet) { if (job == nullptr) { @@ -984,12 +1003,13 @@ int QDMI_job_wait(QDMI_Job job, size_t timeout) { return job->wait(timeout); } -int QDMI_job_get_results(QDMI_Job job, QDMI_Job_Result result, - const size_t size, void* data, size_t* sizeRet) { +int QDMI_job_get_results(QDMI_Job job, const size_t programIndex, + QDMI_Job_Result result, const size_t size, void* data, + size_t* sizeRet) { if (job == nullptr) { return QDMI_ERROR_INVALIDARGUMENT; } - return job->getResults(result, size, data, sizeRet); + return job->getResults(programIndex, result, size, data, sizeRet); } int QDMI_device_query_device_property(QDMI_Device device, diff --git a/test/python/qdmi/test_multi_program.py b/test/python/qdmi/test_multi_program.py new file mode 100644 index 0000000000..e7e132d796 --- /dev/null +++ b/test/python/qdmi/test_multi_program.py @@ -0,0 +1,61 @@ +# 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 + +"""Native program-list submission remains separate from SDK concurrency.""" + +import pytest + +from mqt.core.qdmi import Job, ProgramFormat +from mqt.core.qdmi.driver import open_device + +PROGRAM = 'OPENQASM 3.0; include "stdgates.inc"; qubit q; bit c; x q; c = measure q;' + + +def test_single_program_list_and_indexed_results() -> None: + """The one-program list uses the aggregate API without changing results.""" + device = open_device("mqt.ddsim.default") + job = device.submit_programs([PROGRAM], ProgramFormat.QASM3, 32) + assert job.wait() + assert job.programs_num == 1 + assert job.get_counts(program_index=0) == {"1": 32} + with pytest.raises(IndexError): + job.get_counts(program_index=1) + + +def test_program_list_preserves_default_shots() -> None: + """Omitting shots retains DDSIM's default instead of requiring a value.""" + device = open_device("mqt.ddsim.default") + job = device.submit_programs([PROGRAM], ProgramFormat.QASM3) + assert job.wait() + assert job.get_counts() == {"1": job.num_shots} + + +def test_unsupported_aggregate_is_not_emulated() -> None: + """DDSIM declines larger lists instead of disguising separate jobs.""" + device = open_device("mqt.ddsim.default") + with pytest.raises(RuntimeError): + device.submit_programs([PROGRAM, PROGRAM], ProgramFormat.QASM3, 32) + + +def test_program_list_preserves_job_failure() -> None: + """An invalid program fails the real job, not a synthetic batch wrapper.""" + device = open_device("mqt.ddsim.default") + job = device.submit_programs(["not an OpenQASM program"], ProgramFormat.QASM3, 32) + assert job.wait() + assert job.check() == Job.Status.FAILED + with pytest.raises(RuntimeError): + job.get_counts() + + +def test_program_list_rejects_invalid_payload_kinds() -> None: + """Text cannot silently become binary and empty lists are invalid.""" + device = open_device("mqt.ddsim.default") + with pytest.raises(ValueError, match="Binary program formats require exact-byte submission"): + device.submit_programs([PROGRAM], ProgramFormat.QIR_BASE_MODULE, 32) + with pytest.raises(ValueError, match="Setting programs"): + device.submit_programs([], ProgramFormat.QASM3, 32) diff --git a/test/python/qdmi/test_qdmi.py b/test/python/qdmi/test_qdmi.py index 9fef03e6f5..1e6e8e36c3 100644 --- a/test/python/qdmi/test_qdmi.py +++ b/test/python/qdmi/test_qdmi.py @@ -521,8 +521,8 @@ def test_device_rejects_text_for_binary_format(ddsim_device: Device) -> None: def test_device_rejects_batch_jobs(ddsim_device: Device) -> None: - """State that MQT Core does not support batch jobs.""" - with pytest.raises(ValueError, match="does not support batch jobs"): + """Distinguish legacy job-handle batches from native program lists.""" + with pytest.raises(ValueError, match="legacy BATCHJOB"): ddsim_device.submit_job(b"", ProgramFormat.BATCH_JOB, num_shots=1) @@ -540,7 +540,7 @@ def test_calibration_job_reaches_the_device(ddsim_device: Device, program: str | matters is that the client no longer refuses before asking, so the failure is a device error rather than a `ValueError` about the argument. """ - with pytest.raises(RuntimeError, match="Setting program format"): + with pytest.raises(RuntimeError, match="Setting program"): ddsim_device.submit_calibration_job(program) diff --git a/test/qdmi/devices/dd/concurrency_test.cpp b/test/qdmi/devices/dd/concurrency_test.cpp index 0cd02d134a..746ad52d23 100644 --- a/test/qdmi/devices/dd/concurrency_test.cpp +++ b/test/qdmi/devices/dd/concurrency_test.cpp @@ -45,7 +45,7 @@ TEST(Concurrency, ConcurrentStatevectorReads) { auto const worker = [&] { std::vector buf(stateSize / sizeof(double)); EXPECT_EQ(MQT_DDSIM_QDMI_device_job_get_results( - j.job, QDMI_JOB_RESULT_STATEVECTOR_DENSE, stateSize, + j.job, 0U, QDMI_JOB_RESULT_STATEVECTOR_DENSE, stateSize, buf.data(), nullptr), QDMI_SUCCESS); }; @@ -76,17 +76,17 @@ TEST(Concurrency, ConcurrentHistogramReads) { auto const keysWorker = [&] { std::string buf(keysSize > 0 ? keysSize - 1 : 0, '\0'); - EXPECT_EQ( - MQT_DDSIM_QDMI_device_job_get_results(j.job, QDMI_JOB_RESULT_HIST_KEYS, - keysSize, buf.data(), nullptr), - QDMI_SUCCESS); + EXPECT_EQ(MQT_DDSIM_QDMI_device_job_get_results( + j.job, 0U, QDMI_JOB_RESULT_HIST_KEYS, keysSize, buf.data(), + nullptr), + QDMI_SUCCESS); }; auto const valsWorker = [&] { std::vector v(valsSize / sizeof(size_t)); - EXPECT_EQ( - MQT_DDSIM_QDMI_device_job_get_results( - j.job, QDMI_JOB_RESULT_HIST_VALUES, valsSize, v.data(), nullptr), - QDMI_SUCCESS); + EXPECT_EQ(MQT_DDSIM_QDMI_device_job_get_results( + j.job, 0U, QDMI_JOB_RESULT_HIST_VALUES, valsSize, v.data(), + nullptr), + QDMI_SUCCESS); }; std::thread t1(keysWorker); diff --git a/test/qdmi/devices/dd/error_handling_test.cpp b/test/qdmi/devices/dd/error_handling_test.cpp index b0270a6b41..be2938b7c2 100644 --- a/test/qdmi/devices/dd/error_handling_test.cpp +++ b/test/qdmi/devices/dd/error_handling_test.cpp @@ -26,8 +26,6 @@ class ErrorHandling : public ::testing::Test { static auto TearDownTestSuite() -> void { MQT_DDSIM_QDMI_device_finalize(); } }; -} // namespace - TEST_F(ErrorHandling, NullptrArguments) { EXPECT_EQ(MQT_DDSIM_QDMI_device_session_alloc(nullptr), QDMI_ERROR_INVALIDARGUMENT); @@ -58,8 +56,8 @@ TEST_F(ErrorHandling, NullptrArguments) { QDMI_ERROR_INVALIDARGUMENT); EXPECT_EQ(MQT_DDSIM_QDMI_device_job_wait(nullptr, 0), QDMI_ERROR_INVALIDARGUMENT); - EXPECT_EQ(MQT_DDSIM_QDMI_device_job_get_results(nullptr, QDMI_JOB_RESULT_MAX, - 0, nullptr, nullptr), + EXPECT_EQ(MQT_DDSIM_QDMI_device_job_get_results( + nullptr, 0U, QDMI_JOB_RESULT_MAX, 0, nullptr, nullptr), QDMI_ERROR_INVALIDARGUMENT); const qdmi_test::SessionGuard s{}; @@ -71,6 +69,8 @@ TEST_F(ErrorHandling, NullptrArguments) { QDMI_ERROR_INVALIDARGUMENT); } +} // namespace + TEST_F(ErrorHandling, GetResultsBeforeDone) { const qdmi_test::SessionGuard s{}; const qdmi_test::JobGuard j{s.session}; @@ -80,13 +80,13 @@ TEST_F(ErrorHandling, GetResultsBeforeDone) { ASSERT_EQ(qdmi_test::setShots(j.job, 16384), QDMI_SUCCESS); // Before submit → invalid EXPECT_EQ(MQT_DDSIM_QDMI_device_job_get_results( - j.job, QDMI_JOB_RESULT_HIST_KEYS, 0, nullptr, nullptr), + j.job, 0U, QDMI_JOB_RESULT_HIST_KEYS, 0, nullptr, nullptr), QDMI_ERROR_BADSTATE); ASSERT_EQ(MQT_DDSIM_QDMI_device_job_submit(j.job), QDMI_SUCCESS); // After submit but not necessarily done → still invalid or waits; contract // says invalid EXPECT_EQ(MQT_DDSIM_QDMI_device_job_get_results( - j.job, QDMI_JOB_RESULT_HIST_KEYS, 0, nullptr, nullptr), + j.job, 0U, QDMI_JOB_RESULT_HIST_KEYS, 0, nullptr, nullptr), QDMI_ERROR_BADSTATE); ASSERT_EQ(MQT_DDSIM_QDMI_device_job_wait(j.job, 0), QDMI_SUCCESS); } @@ -131,8 +131,8 @@ TEST_F(ErrorHandling, MaxEnums) { QDMI_SUCCESS); ASSERT_EQ(qdmi_test::setShots(j.job, 16), QDMI_SUCCESS); ASSERT_EQ(qdmi_test::submitAndWait(j.job, 0), QDMI_SUCCESS); - EXPECT_EQ(MQT_DDSIM_QDMI_device_job_get_results(j.job, QDMI_JOB_RESULT_MAX, 0, - nullptr, nullptr), + EXPECT_EQ(MQT_DDSIM_QDMI_device_job_get_results( + j.job, 0U, QDMI_JOB_RESULT_MAX, 0, nullptr, nullptr), QDMI_ERROR_INVALIDARGUMENT); } @@ -244,19 +244,19 @@ TEST_F(ErrorHandling, CustomEnums) { ASSERT_EQ(qdmi_test::submitAndWait(j.job, 0), QDMI_SUCCESS); EXPECT_EQ(MQT_DDSIM_QDMI_device_job_get_results( - j.job, QDMI_JOB_RESULT_CUSTOM1, 0, nullptr, nullptr), + j.job, 0U, QDMI_JOB_RESULT_CUSTOM1, 0, nullptr, nullptr), QDMI_ERROR_NOTSUPPORTED); EXPECT_EQ(MQT_DDSIM_QDMI_device_job_get_results( - j.job, QDMI_JOB_RESULT_CUSTOM2, 0, nullptr, nullptr), + j.job, 0U, QDMI_JOB_RESULT_CUSTOM2, 0, nullptr, nullptr), QDMI_ERROR_NOTSUPPORTED); EXPECT_EQ(MQT_DDSIM_QDMI_device_job_get_results( - j.job, QDMI_JOB_RESULT_CUSTOM3, 0, nullptr, nullptr), + j.job, 0U, QDMI_JOB_RESULT_CUSTOM3, 0, nullptr, nullptr), QDMI_ERROR_NOTSUPPORTED); EXPECT_EQ(MQT_DDSIM_QDMI_device_job_get_results( - j.job, QDMI_JOB_RESULT_CUSTOM4, 0, nullptr, nullptr), + j.job, 0U, QDMI_JOB_RESULT_CUSTOM4, 0, nullptr, nullptr), QDMI_ERROR_NOTSUPPORTED); EXPECT_EQ(MQT_DDSIM_QDMI_device_job_get_results( - j.job, QDMI_JOB_RESULT_CUSTOM5, 0, nullptr, nullptr), + j.job, 0U, QDMI_JOB_RESULT_CUSTOM5, 0, nullptr, nullptr), QDMI_ERROR_NOTSUPPORTED); } diff --git a/test/qdmi/devices/dd/helpers/test_utils.cpp b/test/qdmi/devices/dd/helpers/test_utils.cpp index c1431de37c..28b9d8ead3 100644 --- a/test/qdmi/devices/dd/helpers/test_utils.cpp +++ b/test/qdmi/devices/dd/helpers/test_utils.cpp @@ -94,12 +94,6 @@ int setProgram(MQT_DDSIM_QDMI_Device_Job job, const QDMI_Program_Format fmt, if (job == nullptr) { return QDMI_ERROR_INVALIDARGUMENT; } - int rc = MQT_DDSIM_QDMI_device_job_set_parameter( - job, QDMI_DEVICE_JOB_PARAMETER_PROGRAMFORMAT, sizeof(QDMI_Program_Format), - &fmt); - if (rc != QDMI_SUCCESS && rc != QDMI_ERROR_NOTSUPPORTED) { - return rc; - } // Text payloads include the trailing '\0' per the QDMI wire convention. // Binary payloads ship the exact byte count. // The `+1` is safe here because every existing call to `setProgram` with a @@ -111,9 +105,9 @@ int setProgram(MQT_DDSIM_QDMI_Device_Job job, const QDMI_Program_Format fmt, fmt == QDMI_PROGRAM_FORMAT_QIRADAPTIVESTRING; const auto bytesToSend = isTextProgramFormat ? program.size() + 1 : program.size(); - rc = MQT_DDSIM_QDMI_device_job_set_parameter( - job, QDMI_DEVICE_JOB_PARAMETER_PROGRAM, bytesToSend, program.data()); - return rc; + const void* const programData = program.data(); + return MQT_DDSIM_QDMI_device_job_set_programs(job, &fmt, 1U, &bytesToSend, + &programData); } int setShots(MQT_DDSIM_QDMI_Device_Job job, const size_t shots) { @@ -146,7 +140,7 @@ int submitAndWait(MQT_DDSIM_QDMI_Device_Job job, size_t timeoutSeconds) { size_t querySize(MQT_DDSIM_QDMI_Device_Job job, QDMI_Job_Result result) { size_t sz = 0; const auto rc = - MQT_DDSIM_QDMI_device_job_get_results(job, result, 0, nullptr, &sz); + MQT_DDSIM_QDMI_device_job_get_results(job, 0U, result, 0, nullptr, &sz); EXPECT_EQ(rc, QDMI_SUCCESS); return sz; } @@ -176,7 +170,7 @@ getHistogram(MQT_DDSIM_QDMI_Device_Job job) { std::string keys(ks > 0 ? ks - 1 : 0, '\0'); if (ks > 0) { const auto rc = MQT_DDSIM_QDMI_device_job_get_results( - job, QDMI_JOB_RESULT_HIST_KEYS, ks, keys.data(), nullptr); + job, 0U, QDMI_JOB_RESULT_HIST_KEYS, ks, keys.data(), nullptr); EXPECT_EQ(rc, QDMI_SUCCESS); } auto keyVec = splitCSV(keys); @@ -184,7 +178,7 @@ getHistogram(MQT_DDSIM_QDMI_Device_Job job) { std::vector vals(vs / sizeof(size_t)); if (vs > 0) { const auto rc = MQT_DDSIM_QDMI_device_job_get_results( - job, QDMI_JOB_RESULT_HIST_VALUES, vs, vals.data(), nullptr); + job, 0U, QDMI_JOB_RESULT_HIST_VALUES, vs, vals.data(), nullptr); EXPECT_EQ(rc, QDMI_SUCCESS); } return {std::move(keyVec), std::move(vals)}; @@ -195,7 +189,7 @@ std::vector> getDenseState(MQT_DDSIM_QDMI_Device_Job job) { std::vector buf(sz / sizeof(double)); if (sz > 0) { const auto rc = MQT_DDSIM_QDMI_device_job_get_results( - job, QDMI_JOB_RESULT_STATEVECTOR_DENSE, sz, buf.data(), nullptr); + job, 0U, QDMI_JOB_RESULT_STATEVECTOR_DENSE, sz, buf.data(), nullptr); EXPECT_EQ(rc, QDMI_SUCCESS); } std::vector> out; @@ -212,7 +206,8 @@ getSparseState(MQT_DDSIM_QDMI_Device_Job job) { std::string keys(ks > 0 ? ks - 1 : 0, '\0'); if (ks > 0) { const auto rc = MQT_DDSIM_QDMI_device_job_get_results( - job, QDMI_JOB_RESULT_STATEVECTOR_SPARSE_KEYS, ks, keys.data(), nullptr); + job, 0U, QDMI_JOB_RESULT_STATEVECTOR_SPARSE_KEYS, ks, keys.data(), + nullptr); EXPECT_EQ(rc, QDMI_SUCCESS); } auto keyVec = splitCSV(keys); @@ -220,7 +215,7 @@ getSparseState(MQT_DDSIM_QDMI_Device_Job job) { std::vector vals(vs / sizeof(double)); if (vs > 0) { const auto rc = MQT_DDSIM_QDMI_device_job_get_results( - job, QDMI_JOB_RESULT_STATEVECTOR_SPARSE_VALUES, vs, vals.data(), + job, 0U, QDMI_JOB_RESULT_STATEVECTOR_SPARSE_VALUES, vs, vals.data(), nullptr); EXPECT_EQ(rc, QDMI_SUCCESS); } @@ -237,7 +232,7 @@ std::vector getDenseProbabilities(MQT_DDSIM_QDMI_Device_Job job) { std::vector out(sz / sizeof(double)); if (sz > 0) { const auto rc = MQT_DDSIM_QDMI_device_job_get_results( - job, QDMI_JOB_RESULT_PROBABILITIES_DENSE, sz, out.data(), nullptr); + job, 0U, QDMI_JOB_RESULT_PROBABILITIES_DENSE, sz, out.data(), nullptr); EXPECT_EQ(rc, QDMI_SUCCESS); } return out; @@ -249,7 +244,7 @@ getSparseProbabilities(MQT_DDSIM_QDMI_Device_Job job) { std::string keys(ks > 0 ? ks - 1 : 0, '\0'); if (ks > 0) { const auto rc = MQT_DDSIM_QDMI_device_job_get_results( - job, QDMI_JOB_RESULT_PROBABILITIES_SPARSE_KEYS, ks, keys.data(), + job, 0U, QDMI_JOB_RESULT_PROBABILITIES_SPARSE_KEYS, ks, keys.data(), nullptr); EXPECT_EQ(rc, QDMI_SUCCESS); } @@ -258,7 +253,7 @@ getSparseProbabilities(MQT_DDSIM_QDMI_Device_Job job) { std::vector vals(vs / sizeof(double)); if (vs > 0) { const auto rc = MQT_DDSIM_QDMI_device_job_get_results( - job, QDMI_JOB_RESULT_PROBABILITIES_SPARSE_VALUES, vs, vals.data(), + job, 0U, QDMI_JOB_RESULT_PROBABILITIES_SPARSE_VALUES, vs, vals.data(), nullptr); EXPECT_EQ(rc, QDMI_SUCCESS); } diff --git a/test/qdmi/devices/dd/job_parameters_test.cpp b/test/qdmi/devices/dd/job_parameters_test.cpp index e127c70a7b..c79e3366ba 100644 --- a/test/qdmi/devices/dd/job_parameters_test.cpp +++ b/test/qdmi/devices/dd/job_parameters_test.cpp @@ -23,22 +23,18 @@ #include #include +namespace { +constexpr auto QASM3_FORMAT = QDMI_PROGRAM_FORMAT_QASM3; +constexpr auto QASM2_FORMAT = QDMI_PROGRAM_FORMAT_QASM2; +constexpr auto QIR_BINARY_FORMAT = QDMI_PROGRAM_FORMAT_QIRBASEMODULE; +} // namespace + TEST(JobParameters, SetAndQueryBasics) { const qdmi_test::SessionGuard s{}; const qdmi_test::JobGuard j{s.session}; - // Program format QASM3 - constexpr QDMI_Program_Format fmt = QDMI_PROGRAM_FORMAT_QASM3; - ASSERT_EQ(MQT_DDSIM_QDMI_device_job_set_parameter( - j.job, QDMI_DEVICE_JOB_PARAMETER_PROGRAMFORMAT, - sizeof(QDMI_Program_Format), &fmt), - QDMI_SUCCESS); - - // Program string - ASSERT_EQ(MQT_DDSIM_QDMI_device_job_set_parameter( - j.job, QDMI_DEVICE_JOB_PARAMETER_PROGRAM, - strlen(qdmi_test::QASM3_BELL_SAMPLING) + 1, - qdmi_test::QASM3_BELL_SAMPLING), + ASSERT_EQ(qdmi_test::setProgram(j.job, QDMI_PROGRAM_FORMAT_QASM3, + qdmi_test::QASM3_BELL_SAMPLING), QDMI_SUCCESS); // Shots @@ -89,22 +85,114 @@ TEST(JobParameters, SetAndQueryBasics) { nullptr), QDMI_SUCCESS); EXPECT_EQ(program, qdmi_test::QASM3_BELL_SAMPLING); + + size_t programsNum = 0U; + EXPECT_EQ(MQT_DDSIM_QDMI_device_job_query_property( + j.job, QDMI_DEVICE_JOB_PROPERTY_PROGRAMSNUM, sizeof(size_t), + &programsNum, nullptr), + QDMI_SUCCESS); + EXPECT_EQ(programsNum, 1U); +} + +TEST(JobParameters, RequiresACompleteProgramBeforeSubmission) { + const qdmi_test::SessionGuard session{}; + const qdmi_test::JobGuard job{session.session}; + + EXPECT_EQ( + MQT_DDSIM_QDMI_device_job_query_property( + job.job, QDMI_DEVICE_JOB_PROPERTY_PROGRAMFORMAT, 0, nullptr, nullptr), + QDMI_ERROR_BADSTATE); + EXPECT_EQ(MQT_DDSIM_QDMI_device_job_query_property( + job.job, QDMI_DEVICE_JOB_PROPERTY_PROGRAM, 0, nullptr, nullptr), + QDMI_ERROR_BADSTATE); + EXPECT_EQ( + MQT_DDSIM_QDMI_device_job_query_property( + job.job, QDMI_DEVICE_JOB_PROPERTY_PROGRAMSNUM, 0, nullptr, nullptr), + QDMI_ERROR_BADSTATE); + EXPECT_EQ(MQT_DDSIM_QDMI_device_job_submit(job.job), QDMI_ERROR_BADSTATE); + + ASSERT_EQ(qdmi_test::setProgram(job.job, QDMI_PROGRAM_FORMAT_QASM3, + qdmi_test::QASM3_BELL_SAMPLING), + QDMI_SUCCESS); + EXPECT_EQ(MQT_DDSIM_QDMI_device_job_set_parameter( + job.job, QDMI_DEVICE_JOB_PARAMETER_PROGRAMFORMAT, + sizeof(QDMI_Program_Format), &QASM2_FORMAT), + QDMI_SUCCESS); + EXPECT_EQ(MQT_DDSIM_QDMI_device_job_query_property( + job.job, QDMI_DEVICE_JOB_PROPERTY_PROGRAM, 0, nullptr, nullptr), + QDMI_ERROR_BADSTATE); + EXPECT_EQ(MQT_DDSIM_QDMI_device_job_submit(job.job), QDMI_ERROR_BADSTATE); +} + +TEST(JobParameters, BinaryProgramRoundTripsExactly) { + const qdmi_test::SessionGuard session{}; + const qdmi_test::JobGuard job{session.session}; + constexpr std::array expected{std::byte{0}, std::byte{0xff}, std::byte{0x7f}}; + auto program = expected; + + const size_t size = program.size(); + const void* const data = program.data(); + ASSERT_EQ(MQT_DDSIM_QDMI_device_job_set_programs(job.job, &QIR_BINARY_FORMAT, + 1U, &size, &data), + QDMI_SUCCESS); + + program.fill(std::byte{0}); + size_t resultSize = 0U; + ASSERT_EQ( + MQT_DDSIM_QDMI_device_job_query_property( + job.job, QDMI_DEVICE_JOB_PROPERTY_PROGRAM, 0, nullptr, &resultSize), + QDMI_SUCCESS); + ASSERT_EQ(resultSize, program.size()); + std::array result{}; + EXPECT_EQ(MQT_DDSIM_QDMI_device_job_query_property( + job.job, QDMI_DEVICE_JOB_PROPERTY_PROGRAM, result.size(), + result.data(), nullptr), + QDMI_SUCCESS); + EXPECT_EQ(result, expected); +} + +TEST(JobParameters, ProgramListsValidateAtomically) { + const qdmi_test::SessionGuard session{}; + const qdmi_test::JobGuard job{session.session}; + constexpr char program = '\0'; + constexpr std::array sizes{1U}; + const std::array programs{&program}; + + EXPECT_EQ(MQT_DDSIM_QDMI_device_job_set_programs( + nullptr, &QASM3_FORMAT, 1U, sizes.data(), programs.data()), + QDMI_ERROR_INVALIDARGUMENT); + EXPECT_EQ(MQT_DDSIM_QDMI_device_job_set_programs( + job.job, nullptr, 1U, sizes.data(), programs.data()), + QDMI_ERROR_INVALIDARGUMENT); + EXPECT_EQ(MQT_DDSIM_QDMI_device_job_set_programs( + job.job, &QASM3_FORMAT, 0U, sizes.data(), programs.data()), + QDMI_ERROR_INVALIDARGUMENT); + constexpr auto invalid = QDMI_PROGRAM_FORMAT_MAX; + EXPECT_EQ(MQT_DDSIM_QDMI_device_job_set_programs( + job.job, &invalid, 1U, sizes.data(), programs.data()), + QDMI_ERROR_INVALIDARGUMENT); + EXPECT_EQ(MQT_DDSIM_QDMI_device_job_set_programs(job.job, &QASM3_FORMAT, 1U, + sizes.data(), nullptr), + QDMI_SUCCESS); + EXPECT_EQ(MQT_DDSIM_QDMI_device_job_set_programs(job.job, &QASM3_FORMAT, 1U, + nullptr, programs.data()), + QDMI_ERROR_INVALIDARGUMENT); + constexpr std::array twoSizes{1U, 1U}; + const std::array twoPrograms{&program, &program}; + EXPECT_EQ(MQT_DDSIM_QDMI_device_job_set_programs(job.job, &QASM3_FORMAT, 2U, + twoSizes.data(), + twoPrograms.data()), + QDMI_ERROR_NOTSUPPORTED); } TEST(JobParameters, RejectsUnterminatedTextProgram) { const qdmi_test::SessionGuard s{}; const qdmi_test::JobGuard j{s.session}; - constexpr QDMI_Program_Format fmt = QDMI_PROGRAM_FORMAT_QASM3; - ASSERT_EQ(MQT_DDSIM_QDMI_device_job_set_parameter( - j.job, QDMI_DEVICE_JOB_PARAMETER_PROGRAMFORMAT, - sizeof(QDMI_Program_Format), &fmt), - QDMI_SUCCESS); - - EXPECT_EQ(MQT_DDSIM_QDMI_device_job_set_parameter( - j.job, QDMI_DEVICE_JOB_PARAMETER_PROGRAM, - strlen(qdmi_test::QASM3_BELL_SAMPLING), - qdmi_test::QASM3_BELL_SAMPLING), + const size_t size = strlen(qdmi_test::QASM3_BELL_SAMPLING); + const void* const program = qdmi_test::QASM3_BELL_SAMPLING; + EXPECT_EQ(MQT_DDSIM_QDMI_device_job_set_programs(j.job, &QASM3_FORMAT, 1U, + &size, &program), QDMI_ERROR_INVALIDARGUMENT); } @@ -112,16 +200,11 @@ TEST(JobParameters, RejectsInteriorNullInTextProgram) { const qdmi_test::SessionGuard s{}; const qdmi_test::JobGuard j{s.session}; - constexpr QDMI_Program_Format fmt = QDMI_PROGRAM_FORMAT_QASM3; - ASSERT_EQ(MQT_DDSIM_QDMI_device_job_set_parameter( - j.job, QDMI_DEVICE_JOB_PARAMETER_PROGRAMFORMAT, - sizeof(QDMI_Program_Format), &fmt), - QDMI_SUCCESS); - constexpr auto program = std::to_array("OPENQASM 3.0;\0garbage"); - EXPECT_EQ(MQT_DDSIM_QDMI_device_job_set_parameter( - j.job, QDMI_DEVICE_JOB_PARAMETER_PROGRAM, program.size(), - program.data()), + const size_t size = program.size(); + const void* const data = program.data(); + EXPECT_EQ(MQT_DDSIM_QDMI_device_job_set_programs(j.job, &QASM3_FORMAT, 1U, + &size, &data), QDMI_ERROR_INVALIDARGUMENT); } @@ -129,7 +212,7 @@ TEST(JobParameters, ProgramFormatSupport) { const qdmi_test::SessionGuard s{}; const qdmi_test::JobGuard j{s.session}; - // Supported + /// Supported program formats. for (QDMI_Program_Format fmt : { QDMI_PROGRAM_FORMAT_QASM2, QDMI_PROGRAM_FORMAT_QASM3, @@ -144,22 +227,18 @@ TEST(JobParameters, ProgramFormatSupport) { QDMI_SUCCESS); } - // Unsupported → NOTSUPPORTED - for (QDMI_Program_Format fmt : { - QDMI_PROGRAM_FORMAT_CALIBRATION, - QDMI_PROGRAM_FORMAT_QPY, - QDMI_PROGRAM_FORMAT_IQMJSON, - QDMI_PROGRAM_FORMAT_CUSTOM1, - QDMI_PROGRAM_FORMAT_CUSTOM2, - QDMI_PROGRAM_FORMAT_CUSTOM3, - QDMI_PROGRAM_FORMAT_CUSTOM4, - QDMI_PROGRAM_FORMAT_CUSTOM5, - }) { - EXPECT_EQ(MQT_DDSIM_QDMI_device_job_set_parameter( - j.job, QDMI_DEVICE_JOB_PARAMETER_PROGRAMFORMAT, - sizeof(QDMI_Program_Format), &fmt), - QDMI_ERROR_NOTSUPPORTED); - } + /// A valid but unsupported format is rejected. + constexpr QDMI_Program_Format unsupported = QDMI_PROGRAM_FORMAT_QPY; + EXPECT_EQ(MQT_DDSIM_QDMI_device_job_set_parameter( + j.job, QDMI_DEVICE_JOB_PARAMETER_PROGRAMFORMAT, + sizeof(QDMI_Program_Format), &unsupported), + QDMI_ERROR_NOTSUPPORTED); + + constexpr auto invalid = QDMI_PROGRAM_FORMAT_MAX; + EXPECT_EQ(MQT_DDSIM_QDMI_device_job_set_parameter( + j.job, QDMI_DEVICE_JOB_PARAMETER_PROGRAMFORMAT, + sizeof(QDMI_Program_Format), &invalid), + QDMI_ERROR_INVALIDARGUMENT); } TEST(JobParameters, SamplingSeed) { diff --git a/test/qdmi/devices/dd/results_probabilities_test.cpp b/test/qdmi/devices/dd/results_probabilities_test.cpp index 820fbb967f..c05a618460 100644 --- a/test/qdmi/devices/dd/results_probabilities_test.cpp +++ b/test/qdmi/devices/dd/results_probabilities_test.cpp @@ -43,8 +43,8 @@ TEST(ResultsProbabilities, DenseSumToOneAndBufferTooSmall) { if (sz > 0) { std::vector tooSmall(sz - 1); EXPECT_EQ(MQT_DDSIM_QDMI_device_job_get_results( - j.job, QDMI_JOB_RESULT_PROBABILITIES_DENSE, tooSmall.size(), - tooSmall.data(), nullptr), + j.job, 0U, QDMI_JOB_RESULT_PROBABILITIES_DENSE, + tooSmall.size(), tooSmall.data(), nullptr), QDMI_ERROR_INVALIDARGUMENT); } } @@ -71,7 +71,7 @@ TEST(ResultsProbabilities, SparseSumToOneAndBufferTooSmall) { if (ksz > 0) { std::vector tooSmall(ksz - 1); EXPECT_EQ(MQT_DDSIM_QDMI_device_job_get_results( - j.job, QDMI_JOB_RESULT_PROBABILITIES_SPARSE_KEYS, + j.job, 0U, QDMI_JOB_RESULT_PROBABILITIES_SPARSE_KEYS, tooSmall.size(), tooSmall.data(), nullptr), QDMI_ERROR_INVALIDARGUMENT); } @@ -80,7 +80,7 @@ TEST(ResultsProbabilities, SparseSumToOneAndBufferTooSmall) { if (vsz > 0) { std::vector tooSmall(vsz - 1); EXPECT_EQ(MQT_DDSIM_QDMI_device_job_get_results( - j.job, QDMI_JOB_RESULT_PROBABILITIES_SPARSE_VALUES, + j.job, 0U, QDMI_JOB_RESULT_PROBABILITIES_SPARSE_VALUES, tooSmall.size(), tooSmall.data(), nullptr), QDMI_ERROR_INVALIDARGUMENT); } diff --git a/test/qdmi/devices/dd/results_sampling_test.cpp b/test/qdmi/devices/dd/results_sampling_test.cpp index 26942b14d8..9a9f1d8280 100644 --- a/test/qdmi/devices/dd/results_sampling_test.cpp +++ b/test/qdmi/devices/dd/results_sampling_test.cpp @@ -41,8 +41,8 @@ namespace { std::vector getShots(MQT_DDSIM_QDMI_Device_Job job) { const size_t size = qdmi_test::querySize(job, QDMI_JOB_RESULT_SHOTS); std::string result(size, '\0'); - EXPECT_EQ(MQT_DDSIM_QDMI_device_job_get_results(job, QDMI_JOB_RESULT_SHOTS, - size, result.data(), nullptr), + EXPECT_EQ(MQT_DDSIM_QDMI_device_job_get_results( + job, 0U, QDMI_JOB_RESULT_SHOTS, size, result.data(), nullptr), QDMI_SUCCESS); EXPECT_FALSE(result.empty()); if (!result.empty()) { @@ -291,12 +291,12 @@ TEST(ResultsSampling, EmptyQASM3YieldsEmptyHistogram) { char dummy{}; for (const auto result : results) { size_t size = 1; - EXPECT_EQ( - MQT_DDSIM_QDMI_device_job_get_results(j.job, result, 0, nullptr, &size), - QDMI_SUCCESS); + EXPECT_EQ(MQT_DDSIM_QDMI_device_job_get_results(j.job, 0U, result, 0, + nullptr, &size), + QDMI_SUCCESS); EXPECT_EQ(size, 0U); - EXPECT_EQ(MQT_DDSIM_QDMI_device_job_get_results(j.job, result, 0, &dummy, - nullptr), + EXPECT_EQ(MQT_DDSIM_QDMI_device_job_get_results(j.job, 0U, result, 0, + &dummy, nullptr), QDMI_SUCCESS); } } @@ -314,7 +314,7 @@ TEST(ResultsSampling, BufferTooSmallErrors) { ASSERT_EQ(shotsSize, 512U * 3U); std::vector shotsTooSmall(shotsSize - 1); EXPECT_EQ(MQT_DDSIM_QDMI_device_job_get_results( - j.job, QDMI_JOB_RESULT_SHOTS, shotsTooSmall.size(), + j.job, 0U, QDMI_JOB_RESULT_SHOTS, shotsTooSmall.size(), shotsTooSmall.data(), nullptr), QDMI_ERROR_INVALIDARGUMENT); @@ -322,7 +322,7 @@ TEST(ResultsSampling, BufferTooSmallErrors) { ks > 0) { std::vector tooSmall(ks - 1); EXPECT_EQ(MQT_DDSIM_QDMI_device_job_get_results( - j.job, QDMI_JOB_RESULT_HIST_KEYS, tooSmall.size(), + j.job, 0U, QDMI_JOB_RESULT_HIST_KEYS, tooSmall.size(), tooSmall.data(), nullptr), QDMI_ERROR_INVALIDARGUMENT); } @@ -332,7 +332,7 @@ TEST(ResultsSampling, BufferTooSmallErrors) { vs > 0) { std::vector tooSmall(vs - 1); EXPECT_EQ(MQT_DDSIM_QDMI_device_job_get_results( - j.job, QDMI_JOB_RESULT_HIST_VALUES, tooSmall.size(), + j.job, 0U, QDMI_JOB_RESULT_HIST_VALUES, tooSmall.size(), tooSmall.data(), nullptr), QDMI_ERROR_INVALIDARGUMENT); } @@ -347,27 +347,28 @@ TEST(ResultsSampling, StateAndProbRequestsAreInvalidWhenShotsPositive) { ASSERT_EQ(qdmi_test::setShots(j.job, 32), QDMI_SUCCESS); ASSERT_EQ(qdmi_test::submitAndWait(j.job, 0), QDMI_SUCCESS); - EXPECT_EQ(MQT_DDSIM_QDMI_device_job_get_results( - j.job, QDMI_JOB_RESULT_STATEVECTOR_DENSE, 0, nullptr, nullptr), - QDMI_ERROR_INVALIDARGUMENT); EXPECT_EQ( MQT_DDSIM_QDMI_device_job_get_results( - j.job, QDMI_JOB_RESULT_STATEVECTOR_SPARSE_KEYS, 0, nullptr, nullptr), + j.job, 0U, QDMI_JOB_RESULT_STATEVECTOR_DENSE, 0, nullptr, nullptr), QDMI_ERROR_INVALIDARGUMENT); EXPECT_EQ(MQT_DDSIM_QDMI_device_job_get_results( - j.job, QDMI_JOB_RESULT_STATEVECTOR_SPARSE_VALUES, 0, nullptr, + j.job, 0U, QDMI_JOB_RESULT_STATEVECTOR_SPARSE_KEYS, 0, nullptr, nullptr), QDMI_ERROR_INVALIDARGUMENT); + EXPECT_EQ(MQT_DDSIM_QDMI_device_job_get_results( + j.job, 0U, QDMI_JOB_RESULT_STATEVECTOR_SPARSE_VALUES, 0, + nullptr, nullptr), + QDMI_ERROR_INVALIDARGUMENT); EXPECT_EQ( MQT_DDSIM_QDMI_device_job_get_results( - j.job, QDMI_JOB_RESULT_PROBABILITIES_DENSE, 0, nullptr, nullptr), + j.job, 0U, QDMI_JOB_RESULT_PROBABILITIES_DENSE, 0, nullptr, nullptr), QDMI_ERROR_INVALIDARGUMENT); EXPECT_EQ(MQT_DDSIM_QDMI_device_job_get_results( - j.job, QDMI_JOB_RESULT_PROBABILITIES_SPARSE_KEYS, 0, nullptr, - nullptr), + j.job, 0U, QDMI_JOB_RESULT_PROBABILITIES_SPARSE_KEYS, 0, + nullptr, nullptr), QDMI_ERROR_INVALIDARGUMENT); EXPECT_EQ(MQT_DDSIM_QDMI_device_job_get_results( - j.job, QDMI_JOB_RESULT_PROBABILITIES_SPARSE_VALUES, 0, nullptr, - nullptr), + j.job, 0U, QDMI_JOB_RESULT_PROBABILITIES_SPARSE_VALUES, 0, + nullptr, nullptr), QDMI_ERROR_INVALIDARGUMENT); } diff --git a/test/qdmi/devices/dd/results_statevector_test.cpp b/test/qdmi/devices/dd/results_statevector_test.cpp index d697b6af1b..46c4133513 100644 --- a/test/qdmi/devices/dd/results_statevector_test.cpp +++ b/test/qdmi/devices/dd/results_statevector_test.cpp @@ -83,12 +83,12 @@ TEST(ResultsStatevector, EmptyQASM3YieldsEmptyResults) { char dummy{}; for (const auto result : results) { size_t size = 1; - EXPECT_EQ( - MQT_DDSIM_QDMI_device_job_get_results(j.job, result, 0, nullptr, &size), - QDMI_SUCCESS); + EXPECT_EQ(MQT_DDSIM_QDMI_device_job_get_results(j.job, 0U, result, 0, + nullptr, &size), + QDMI_SUCCESS); EXPECT_EQ(size, 0U); - EXPECT_EQ(MQT_DDSIM_QDMI_device_job_get_results(j.job, result, 0, &dummy, - nullptr), + EXPECT_EQ(MQT_DDSIM_QDMI_device_job_get_results(j.job, 0U, result, 0, + &dummy, nullptr), QDMI_SUCCESS); } } @@ -115,7 +115,7 @@ TEST(ResultsStatevector, DenseNormalizedAndBufferTooSmall) { if (sz > 0) { std::vector tooSmall(sz - 1); EXPECT_EQ(MQT_DDSIM_QDMI_device_job_get_results( - j.job, QDMI_JOB_RESULT_STATEVECTOR_DENSE, tooSmall.size(), + j.job, 0U, QDMI_JOB_RESULT_STATEVECTOR_DENSE, tooSmall.size(), tooSmall.data(), nullptr), QDMI_ERROR_INVALIDARGUMENT); } @@ -143,7 +143,7 @@ TEST(ResultsStatevector, SparseNormalizedAndBufferTooSmall) { if (ksz > 0) { std::vector tooSmall(ksz - 1); EXPECT_EQ(MQT_DDSIM_QDMI_device_job_get_results( - j.job, QDMI_JOB_RESULT_STATEVECTOR_SPARSE_KEYS, + j.job, 0U, QDMI_JOB_RESULT_STATEVECTOR_SPARSE_KEYS, tooSmall.size(), tooSmall.data(), nullptr), QDMI_ERROR_INVALIDARGUMENT); } @@ -152,7 +152,7 @@ TEST(ResultsStatevector, SparseNormalizedAndBufferTooSmall) { if (vsz > 0) { std::vector tooSmall(vsz - 1); EXPECT_EQ(MQT_DDSIM_QDMI_device_job_get_results( - j.job, QDMI_JOB_RESULT_STATEVECTOR_SPARSE_VALUES, + j.job, 0U, QDMI_JOB_RESULT_STATEVECTOR_SPARSE_VALUES, tooSmall.size(), tooSmall.data(), nullptr), QDMI_ERROR_INVALIDARGUMENT); } @@ -167,14 +167,14 @@ TEST(ResultsStatevector, SamplingRequestsInvalidWithShotsZero) { ASSERT_EQ(qdmi_test::setShots(j.job, 0), QDMI_SUCCESS); ASSERT_EQ(qdmi_test::submitAndWait(j.job, 0), QDMI_SUCCESS); - EXPECT_EQ(MQT_DDSIM_QDMI_device_job_get_results(j.job, QDMI_JOB_RESULT_SHOTS, - 0, nullptr, nullptr), + EXPECT_EQ(MQT_DDSIM_QDMI_device_job_get_results( + j.job, 0U, QDMI_JOB_RESULT_SHOTS, 0, nullptr, nullptr), QDMI_ERROR_INVALIDARGUMENT); EXPECT_EQ(MQT_DDSIM_QDMI_device_job_get_results( - j.job, QDMI_JOB_RESULT_HIST_KEYS, 0, nullptr, nullptr), + j.job, 0U, QDMI_JOB_RESULT_HIST_KEYS, 0, nullptr, nullptr), QDMI_ERROR_INVALIDARGUMENT); EXPECT_EQ(MQT_DDSIM_QDMI_device_job_get_results( - j.job, QDMI_JOB_RESULT_HIST_VALUES, 0, nullptr, nullptr), + j.job, 0U, QDMI_JOB_RESULT_HIST_VALUES, 0, nullptr, nullptr), QDMI_ERROR_INVALIDARGUMENT); } diff --git a/test/qdmi/devices/sc/test_device.cpp b/test/qdmi/devices/sc/test_device.cpp index 37e5e7bbb9..9bb65db87d 100644 --- a/test/qdmi/devices/sc/test_device.cpp +++ b/test/qdmi/devices/sc/test_device.cpp @@ -503,7 +503,7 @@ TEST_F(ScQDMISpecificationTest, JobSetParameter) { } TEST_F(ScQDMIJobSpecificationTest, JobSetParameter) { - QDMI_Program_Format value = QDMI_PROGRAM_FORMAT_QASM2; + constexpr QDMI_Program_Format value = QDMI_PROGRAM_FORMAT_QASM2; EXPECT_THAT(MQT_SC_QDMI_device_job_set_parameter( job, QDMI_DEVICE_JOB_PARAMETER_PROGRAMFORMAT, sizeof(QDMI_Program_Format), &value), @@ -513,6 +513,32 @@ TEST_F(ScQDMIJobSpecificationTest, JobSetParameter) { QDMI_ERROR_INVALIDARGUMENT); } +TEST_F(ScQDMISpecificationTest, JobSetPrograms) { + EXPECT_EQ(MQT_SC_QDMI_device_job_set_programs(nullptr, nullptr, 0U, nullptr, + nullptr), + QDMI_ERROR_INVALIDARGUMENT); +} + +TEST_F(ScQDMIJobSpecificationTest, JobSetPrograms) { + constexpr QDMI_Program_Format format = QDMI_PROGRAM_FORMAT_QASM2; + constexpr char program = '\0'; + constexpr std::array sizes{1U}; + const std::array programs{&program}; + + EXPECT_EQ(MQT_SC_QDMI_device_job_set_programs(job, nullptr, 1U, sizes.data(), + programs.data()), + QDMI_ERROR_INVALIDARGUMENT); + EXPECT_EQ(MQT_SC_QDMI_device_job_set_programs(job, &format, 0U, sizes.data(), + programs.data()), + QDMI_ERROR_INVALIDARGUMENT); + EXPECT_EQ(MQT_SC_QDMI_device_job_set_programs(job, &format, 1U, sizes.data(), + nullptr), + QDMI_ERROR_NOTSUPPORTED); + EXPECT_EQ(MQT_SC_QDMI_device_job_set_programs(job, &format, 1U, nullptr, + programs.data()), + QDMI_ERROR_NOTSUPPORTED); +} + TEST_F(ScQDMISpecificationTest, JobQueryProperty) { EXPECT_EQ(MQT_SC_QDMI_device_job_query_property( nullptr, QDMI_DEVICE_JOB_PROPERTY_MAX, 0, nullptr, nullptr), @@ -585,16 +611,16 @@ TEST_F(ScQDMIJobSpecificationTest, JobWait) { } TEST_F(ScQDMISpecificationTest, JobGetResults) { - EXPECT_EQ(MQT_SC_QDMI_device_job_get_results(nullptr, QDMI_JOB_RESULT_MAX, 0, - nullptr, nullptr), + EXPECT_EQ(MQT_SC_QDMI_device_job_get_results(nullptr, 0U, QDMI_JOB_RESULT_MAX, + 0, nullptr, nullptr), QDMI_ERROR_INVALIDARGUMENT); } TEST_F(ScQDMIJobSpecificationTest, JobGetResults) { - EXPECT_THAT(MQT_SC_QDMI_device_job_get_results(job, QDMI_JOB_RESULT_SHOTS, 0, - nullptr, nullptr), + EXPECT_THAT(MQT_SC_QDMI_device_job_get_results(job, 0U, QDMI_JOB_RESULT_SHOTS, + 0, nullptr, nullptr), testing::AnyOf(QDMI_SUCCESS, QDMI_ERROR_NOTSUPPORTED)); - EXPECT_EQ(MQT_SC_QDMI_device_job_get_results(job, QDMI_JOB_RESULT_MAX, 0, + EXPECT_EQ(MQT_SC_QDMI_device_job_get_results(job, 0U, QDMI_JOB_RESULT_MAX, 0, nullptr, nullptr), QDMI_ERROR_INVALIDARGUMENT); } diff --git a/test/qdmi/driver/session_device.cpp b/test/qdmi/driver/session_device.cpp index e59af21d81..6d290100c1 100644 --- a/test/qdmi/driver/session_device.cpp +++ b/test/qdmi/driver/session_device.cpp @@ -15,8 +15,11 @@ #include #include #include +#include #include #include +#include +#include struct QDMI_Child_Device_impl_d {}; @@ -35,7 +38,11 @@ struct QDMI_Device_Session_impl_d { struct QDMI_Device_Job_impl_d { QDMI_Device_Session session = nullptr; bool retrieved = false; + bool submitted = false; + std::string id = "session-job"; QDMI_Program_Format format = QDMI_PROGRAM_FORMAT_MAX; + size_t shots = 0U; + std::vector> programs; }; namespace { @@ -147,6 +154,23 @@ auto queryValue(const T& result, const size_t size, void* value, std::memcpy(value, &result, sizeof(T)); return QDMI_SUCCESS; } + +auto queryBytes(const std::span result, const size_t size, + void* value, size_t* sizeRet) -> int { + if (sizeRet != nullptr) { + *sizeRet = result.size(); + } + if (value == nullptr) { + return QDMI_SUCCESS; + } + if (size < result.size()) { + return QDMI_ERROR_INVALIDARGUMENT; + } + if (!result.empty()) { + std::memcpy(value, result.data(), result.size()); + } + return QDMI_SUCCESS; +} } // namespace // QDMI requires these exported C symbols to use the configured device prefix. @@ -353,71 +377,183 @@ extern "C" int TEST_SESSION_QDMI_device_session_retrieve_device_job_by_id( } // The QDMI C API transfers this allocation through an opaque raw handle. // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - *job = new (std::nothrow) + auto* retrieved = new (std::nothrow) QDMI_Device_Job_impl_d{.session = session, .retrieved = true}; - return *job == nullptr ? QDMI_ERROR_OUTOFMEM : QDMI_SUCCESS; + if (retrieved == nullptr) { + return QDMI_ERROR_OUTOFMEM; + } + retrieved->id = jobId; + retrieved->format = QDMI_PROGRAM_FORMAT_QIRBASEMODULE; + retrieved->shots = 2U; + retrieved->programs = { + {std::byte{'x'}, std::byte{0}, std::byte{'y'}, std::byte{0}}, + { + std::byte{'z'}, + std::byte{0}, + }, + }; + *job = retrieved; + return QDMI_SUCCESS; } extern "C" int TEST_SESSION_QDMI_device_job_set_parameter( QDMI_Device_Job job, const QDMI_Device_Job_Parameter parameter, const size_t size, const void* value) { - if (job == nullptr) { + if (job == nullptr || (value != nullptr && size == 0U)) { return QDMI_ERROR_INVALIDARGUMENT; } - if (job->retrieved) { + if (job->retrieved || job->submitted) { return QDMI_ERROR_BADSTATE; } if (parameter == QDMI_DEVICE_JOB_PARAMETER_PROGRAMFORMAT) { - if (value == nullptr || size != sizeof(job->format)) { + if (value == nullptr || size != sizeof(QDMI_Program_Format)) { return QDMI_ERROR_INVALIDARGUMENT; } - std::memcpy(&job->format, value, size); + job->format = *static_cast(value); + return QDMI_SUCCESS; } if (parameter == QDMI_DEVICE_JOB_PARAMETER_SHOTSNUM && job->format == QDMI_PROGRAM_FORMAT_CUSTOM1) { return QDMI_ERROR_NOTSUPPORTED; } + if (parameter == QDMI_DEVICE_JOB_PARAMETER_SHOTSNUM) { + if (value == nullptr || size != sizeof(size_t)) { + return QDMI_ERROR_INVALIDARGUMENT; + } + job->shots = *static_cast(value); + return QDMI_SUCCESS; + } + return QDMI_ERROR_NOTSUPPORTED; +} + +extern "C" int TEST_SESSION_QDMI_device_job_set_programs( + QDMI_Device_Job job, const QDMI_Program_Format* format, const size_t count, + const size_t* sizes, const void* const* programs) { + if (job == nullptr || format == nullptr || count == 0U) { + return QDMI_ERROR_INVALIDARGUMENT; + } + if (job->retrieved || job->submitted) { + return QDMI_ERROR_BADSTATE; + } + if (programs == nullptr) { + return QDMI_SUCCESS; + } + if (sizes == nullptr) { + return QDMI_ERROR_INVALIDARGUMENT; + } + const std::span programSizes{sizes, count}; + const std::span programPointers{programs, count}; + std::vector> copies; + copies.reserve(count); + for (size_t index = 0U; index < count; ++index) { + if (programSizes[index] == 0U || programPointers[index] == nullptr) { + return QDMI_ERROR_INVALIDARGUMENT; + } + const std::span bytes{static_cast(programPointers[index]), + programSizes[index]}; + copies.emplace_back(bytes.begin(), bytes.end()); + } + job->format = *format; + job->programs = std::move(copies); return QDMI_SUCCESS; } extern "C" int TEST_SESSION_QDMI_device_job_query_property( QDMI_Device_Job job, const QDMI_Device_Job_Property prop, const size_t size, void* value, size_t* sizeRet) { - if (job == nullptr || job->session == nullptr || - (prop != QDMI_DEVICE_JOB_PROPERTY_ID && - prop != QDMI_DEVICE_JOB_PROPERTY_QUEUEPOSITION)) { + if (job == nullptr || job->session == nullptr) { return QDMI_ERROR_INVALIDARGUMENT; } if (prop == QDMI_DEVICE_JOB_PROPERTY_QUEUEPOSITION) { return QDMI_ERROR_NOTSUPPORTED; } - return queryString("session-job", size, value, sizeRet); + if (prop == QDMI_DEVICE_JOB_PROPERTY_ID) { + return queryString(job->id, size, value, sizeRet); + } + if (prop == QDMI_DEVICE_JOB_PROPERTY_PROGRAMSNUM) { + if (job->programs.empty()) { + return QDMI_ERROR_BADSTATE; + } + return queryValue(job->programs.size(), size, value, sizeRet); + } + if (prop == QDMI_DEVICE_JOB_PROPERTY_SHOTSNUM) { + return queryValue(job->shots, size, value, sizeRet); + } + if (prop == QDMI_DEVICE_JOB_PROPERTY_PROGRAMFORMAT) { + return queryValue(job->format, size, value, sizeRet); + } + if (prop == QDMI_DEVICE_JOB_PROPERTY_PROGRAM) { + if (job->programs.size() != 1U) { + return QDMI_ERROR_NOTSUPPORTED; + } + return queryBytes(job->programs.front(), size, value, sizeRet); + } + return QDMI_ERROR_NOTSUPPORTED; } extern "C" int TEST_SESSION_QDMI_device_job_submit(QDMI_Device_Job job) { if (job == nullptr || job->session == nullptr) { return QDMI_ERROR_INVALIDARGUMENT; } - return job->retrieved ? QDMI_ERROR_BADSTATE : QDMI_SUCCESS; + if (job->retrieved || job->submitted) { + return QDMI_ERROR_BADSTATE; + } + if (job->programs.empty()) { + return QDMI_ERROR_BADSTATE; + } + job->submitted = true; + return QDMI_SUCCESS; } extern "C" int TEST_SESSION_QDMI_device_job_cancel(QDMI_Device_Job /*job*/) { return QDMI_ERROR_NOTSUPPORTED; } -extern "C" int TEST_SESSION_QDMI_device_job_check(QDMI_Device_Job /*job*/, - QDMI_Job_Status* /*status*/) { - return QDMI_ERROR_NOTSUPPORTED; +extern "C" int TEST_SESSION_QDMI_device_job_check(QDMI_Device_Job job, + QDMI_Job_Status* status) { + if (job == nullptr || status == nullptr) { + return QDMI_ERROR_INVALIDARGUMENT; + } + *status = job->submitted || job->retrieved ? QDMI_JOB_STATUS_DONE + : QDMI_JOB_STATUS_CREATED; + return QDMI_SUCCESS; } -extern "C" int TEST_SESSION_QDMI_device_job_wait(QDMI_Device_Job /*job*/, +extern "C" int TEST_SESSION_QDMI_device_job_wait(QDMI_Device_Job job, size_t /*timeout*/) { - return QDMI_ERROR_NOTSUPPORTED; + return job != nullptr && (job->submitted || job->retrieved) + ? QDMI_SUCCESS + : QDMI_ERROR_BADSTATE; } extern "C" int TEST_SESSION_QDMI_device_job_get_results( - QDMI_Device_Job /*job*/, QDMI_Job_Result /*result*/, size_t /*size*/, - void* /*value*/, size_t* /*sizeRet*/) { + QDMI_Device_Job job, const size_t programIndex, + const QDMI_Job_Result result, const size_t size, void* value, + size_t* sizeRet) { + if (job == nullptr) { + return QDMI_ERROR_INVALIDARGUMENT; + } + if (!job->retrieved && !job->submitted) { + return QDMI_ERROR_BADSTATE; + } + if (programIndex >= job->programs.size()) { + return QDMI_ERROR_OUTOFRANGE; + } + if (result == QDMI_JOB_RESULT_SHOTS) { + return queryString(programIndex == 0U ? "10,01" : "11,00", size, value, + sizeRet); + } + if (result == QDMI_JOB_RESULT_HIST_KEYS) { + return queryString(programIndex == 0U ? "10,01" : "11,00", size, value, + sizeRet); + } + if (result == QDMI_JOB_RESULT_HIST_VALUES) { + constexpr std::array VALUES{1U, 1U}; + return queryBytes(std::as_bytes(std::span{VALUES}), size, value, sizeRet); + } + if (result == QDMI_JOB_RESULT_CUSTOM5) { + return queryBytes(job->programs[programIndex], size, value, sizeRet); + } return QDMI_ERROR_NOTSUPPORTED; } diff --git a/test/qdmi/driver/test_driver.cpp b/test/qdmi/driver/test_driver.cpp index cb14aecea3..c3ae3c59f0 100644 --- a/test/qdmi/driver/test_driver.cpp +++ b/test/qdmi/driver/test_driver.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -458,10 +459,13 @@ TEST_P(DriverTest, JobSetParameter) { QDMI_ERROR_INVALIDARGUMENT); } +TEST_P(DriverTest, JobSetPrograms) { + constexpr auto format = QDMI_PROGRAM_FORMAT_QASM2; + EXPECT_EQ(QDMI_job_set_programs(nullptr, &format, 0U, nullptr, nullptr), + QDMI_ERROR_INVALIDARGUMENT); +} + TEST_P(DriverJobTest, JobSetParameter) { - EXPECT_THAT(QDMI_job_set_parameter(job, QDMI_JOB_PARAMETER_PROGRAM, - sizeof(QDMI_Program_Format), nullptr), - testing::AnyOf(QDMI_SUCCESS, QDMI_ERROR_NOTSUPPORTED)); const QDMI_Program_Format value = QDMI_PROGRAM_FORMAT_QASM2; EXPECT_THAT(QDMI_job_set_parameter(job, QDMI_JOB_PARAMETER_PROGRAMFORMAT, sizeof(QDMI_Program_Format), &value), @@ -496,19 +500,19 @@ TEST_P(DriverJobTest, JobQueryProperty) { EXPECT_THAT(QDMI_job_query_property(job, QDMI_JOB_PROPERTY_PROGRAM, 0, nullptr, nullptr), - testing::AnyOf(QDMI_SUCCESS, QDMI_ERROR_NOTSUPPORTED)); + testing::AnyOf(QDMI_ERROR_BADSTATE, QDMI_ERROR_NOTSUPPORTED)); QDMI_Program_Format value = QDMI_PROGRAM_FORMAT_QASM2; auto result = QDMI_job_set_parameter(job, QDMI_JOB_PARAMETER_PROGRAMFORMAT, sizeof(QDMI_Program_Format), &value); EXPECT_THAT(result, testing::AnyOf(QDMI_SUCCESS, QDMI_ERROR_NOTSUPPORTED)); if (result == QDMI_SUCCESS) { - value = QDMI_PROGRAM_FORMAT_MAX; + value = {}; EXPECT_EQ(QDMI_job_query_property(job, QDMI_JOB_PROPERTY_PROGRAMFORMAT, sizeof(QDMI_Program_Format), &value, nullptr), QDMI_SUCCESS); - EXPECT_EQ(value, QDMI_PROGRAM_FORMAT_QASM2); + EXPECT_TRUE(value == QDMI_PROGRAM_FORMAT_QASM2); } size_t numShots = 1; result = QDMI_job_set_parameter(job, QDMI_JOB_PARAMETER_SHOTSNUM, @@ -543,7 +547,7 @@ TEST_P(DriverTest, JobSubmit) { TEST_P(DriverJobTest, JobSubmit) { EXPECT_THAT(QDMI_job_submit(job), - testing::AnyOf(QDMI_SUCCESS, QDMI_ERROR_NOTSUPPORTED)); + testing::AnyOf(QDMI_ERROR_BADSTATE, QDMI_ERROR_NOTSUPPORTED)); } TEST_P(DriverTest, JobCancel) { @@ -577,14 +581,14 @@ TEST_P(DriverJobTest, JobWait) { } TEST_P(DriverTest, JobGetResults) { - EXPECT_EQ( - QDMI_job_get_results(nullptr, QDMI_JOB_RESULT_MAX, 0, nullptr, nullptr), - QDMI_ERROR_INVALIDARGUMENT); + EXPECT_EQ(QDMI_job_get_results(nullptr, 0U, QDMI_JOB_RESULT_MAX, 0, nullptr, + nullptr), + QDMI_ERROR_INVALIDARGUMENT); } TEST_P(DriverJobTest, JobGetResults) { EXPECT_THAT( - QDMI_job_get_results(job, QDMI_JOB_RESULT_SHOTS, 0, nullptr, nullptr), + QDMI_job_get_results(job, 0U, QDMI_JOB_RESULT_SHOTS, 0, nullptr, nullptr), testing::AnyOf(QDMI_SUCCESS, QDMI_ERROR_NOTSUPPORTED, QDMI_ERROR_BADSTATE)); } @@ -1490,6 +1494,99 @@ TEST(DeviceRegistrationTest, RetrievesExistingJobs) { const auto retrievedJob = device.retrieveJobById("session-job"); EXPECT_EQ(retrievedJob.getId(), "session-job"); + EXPECT_EQ(retrievedJob.getProgramsNum(), 2U); + EXPECT_EQ(retrievedJob.getNumShots(), 2U); + EXPECT_EQ(retrievedJob.getShots(), (std::vector{"10", "01"})); + EXPECT_EQ(retrievedJob.getResults(0U, QDMI_JOB_RESULT_CUSTOM5), + (std::vector{std::byte{'x'}, std::byte{0}, + std::byte{'y'}, std::byte{0}})); + EXPECT_EQ(retrievedJob.getShots(1U), (std::vector{"11", "00"})); + EXPECT_EQ(retrievedJob.getResults(1U, QDMI_JOB_RESULT_CUSTOM5), + (std::vector{std::byte{'z'}, std::byte{0}})); + EXPECT_EQ(retrievedJob.getResults(1U, QDMI_JOB_RESULT_CUSTOM5), + retrievedJob.getResults(1U, QDMI_JOB_RESULT_CUSTOM5)); + EXPECT_THROW(std::ignore = + retrievedJob.getResults(2U, QDMI_JOB_RESULT_CUSTOM5), + std::out_of_range); +} + +TEST(DeviceRegistrationTest, SubmitsOrderedBinaryProgramsAtomically) { + registerSessionTestDevice(); + const auto device = qdmi::Session::openDevice("test.session-overrides"); + const std::array programs{ + std::vector{ + std::byte{'a'}, + std::byte{0}, + std::byte{'b'}, + std::byte{0}, + }, + std::vector{ + std::byte{0xff}, + std::byte{0}, + }, + }; + + const auto job = + device.submitPrograms(programs, QDMI_PROGRAM_FORMAT_QIRBASEMODULE, 3U); + EXPECT_EQ(job.getProgramsNum(), programs.size()); + EXPECT_EQ(job.getResults(0U, QDMI_JOB_RESULT_CUSTOM5), programs[0]); + EXPECT_EQ(job.getResults(1U, QDMI_JOB_RESULT_CUSTOM5), programs[1]); +} + +TEST(DeviceRegistrationTest, SubmitsOrderedTextProgramsAtomically) { + registerSessionTestDevice(); + const auto device = qdmi::Session::openDevice("test.session-overrides"); + const std::array programs{"OPENQASM 3.0;", "OPENQASM 3.0;"}; + + const auto job = + device.submitPrograms(programs, QDMI_PROGRAM_FORMAT_QASM3, 3U); + ASSERT_EQ(job.getProgramsNum(), programs.size()); + for (size_t index = 0U; index < programs.size(); ++index) { + const auto output = job.getResults(index, QDMI_JOB_RESULT_CUSTOM5); + ASSERT_EQ(output.size(), programs[index].size() + 1U); + EXPECT_EQ( + std::memcmp(output.data(), programs[index].c_str(), output.size()), 0); + } +} + +TEST(DeviceRegistrationTest, RejectedProgramListPreservesPreviousPayload) { + registerSessionTestDevice(); + const auto device = qdmi::Session::openDevice("test.session-overrides"); + QDMI_Job handle = nullptr; + ASSERT_EQ(QDMI_device_create_job(device, &handle), QDMI_SUCCESS); + const std::unique_ptr job{ + handle, &QDMI_job_free}; + constexpr auto format = QDMI_PROGRAM_FORMAT_QIRBASEMODULE; + constexpr std::array payload{std::byte{'x'}, std::byte{0}}; + const std::array sizes{payload.size(), payload.size()}; + const std::array pointers{payload.data(), nullptr}; + ASSERT_EQ(QDMI_job_set_programs(job.get(), &format, 1U, sizes.data(), + pointers.data()), + QDMI_SUCCESS); + EXPECT_EQ(QDMI_job_set_programs(job.get(), &format, 2U, sizes.data(), + pointers.data()), + QDMI_ERROR_INVALIDARGUMENT); + size_t count = 0U; + EXPECT_EQ(QDMI_job_query_property(job.get(), QDMI_JOB_PROPERTY_PROGRAMSNUM, + sizeof(count), &count, nullptr), + QDMI_SUCCESS); + EXPECT_EQ(count, 1U); + std::array actual{}; + EXPECT_EQ(QDMI_job_query_property(job.get(), QDMI_JOB_PROPERTY_PROGRAM, + actual.size(), actual.data(), nullptr), + QDMI_SUCCESS); + EXPECT_EQ(actual, payload); +} + +TEST(DeviceRegistrationTest, RetrievesIndexedHistograms) { + registerSessionTestDevice(); + const auto device = qdmi::Session::openDevice("test.session-overrides"); + const auto job = device.retrieveJobById("session-job"); + + EXPECT_EQ(job.getCounts(0U), + (std::map{{"10", 1U}, {"01", 1U}})); + EXPECT_EQ(job.getCounts(1U), + (std::map{{"11", 1U}, {"00", 1U}})); } TEST(DeviceRegistrationTest, FreshChildDeviceRetainsItsRootSession) {