Skip to content
Merged
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ releases may include breaking changes.

### Added

- ✨ Add binary-safe QDMI program submission and retrieval to FoMaC ([#1957])
([**@burgholzer**])
- ✨ Add versioned, relocatable configuration and stable-ID registration for
QDMI device libraries, including disabled-ID reservations, fresh device
sessions, idempotent registration, and external-device target metadata
Expand Down Expand Up @@ -667,6 +669,7 @@ changelogs._

<!-- PR links -->

[#1957]: https://github.com/munich-quantum-toolkit/core/pull/1957
[#1953]: https://github.com/munich-quantum-toolkit/core/pull/1953
[#1952]: https://github.com/munich-quantum-toolkit/core/pull/1952
[#1950]: https://github.com/munich-quantum-toolkit/core/pull/1950
Expand Down
53 changes: 47 additions & 6 deletions bindings/fomac/fomac.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
#include <cstddef>
#include <filesystem>
#include <optional>
#include <span>
#include <string>
#include <utility>
#include <vector>
Expand Down Expand Up @@ -268,6 +269,14 @@ when the custom slot is unsupported.)pb");

job.def_prop_ro("program", &fomac::Job::getProgram, "The submitted program.");

job.def_prop_ro(
"program_bytes",
[](const fomac::Job& self) {
const auto program = self.getProgramBytes();
return nb::bytes(program.data(), program.size());
},
"The exact bytes of the submitted program.");

job.def_prop_ro("num_shots", &fomac::Job::getNumShots,
"The number of shots.");

Expand Down Expand Up @@ -298,6 +307,7 @@ when the custom slot is unsupported.)pb");
.value("CALIBRATION", QDMI_PROGRAM_FORMAT_CALIBRATION)
.value("QPY", QDMI_PROGRAM_FORMAT_QPY)
.value("IQM_JSON", QDMI_PROGRAM_FORMAT_IQMJSON)
.value("BATCH_JOB", QDMI_PROGRAM_FORMAT_BATCHJOB)
.value("CUSTOM1", QDMI_PROGRAM_FORMAT_CUSTOM1)
.value("CUSTOM2", QDMI_PROGRAM_FORMAT_CUSTOM2)
.value("CUSTOM3", QDMI_PROGRAM_FORMAT_CUSTOM3)
Expand Down Expand Up @@ -407,12 +417,43 @@ The caller must provide the type documented by the device implementation.
Use ``bytes`` to retrieve the value without interpretation. Returns ``None``
when the custom slot is unsupported.)pb");

device.def("submit_job", &fomac::Device::submitJob, "program"_a,
"program_format"_a, "num_shots"_a, 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 a job to the device.");
device.def(
"submit_job",
[](const fomac::Device& self, const std::string& program,
const QDMI_Program_Format format, const size_t numShots,
const std::optional<fomac::CustomJobParameter>& custom1,
const std::optional<fomac::CustomJobParameter>& custom2,
const std::optional<fomac::CustomJobParameter>& custom3,
const std::optional<fomac::CustomJobParameter>& custom4,
const std::optional<fomac::CustomJobParameter>& custom5) {
return self.submitJob(program, format, numShots, custom1, custom2,
custom3, custom4, custom5);
},
"program"_a, "program_format"_a, "num_shots"_a, 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 a text job to the device.");

device.def(
"submit_job",
[](const fomac::Device& self, const nb::bytes& program,
const QDMI_Program_Format format, const size_t numShots,
const std::optional<fomac::CustomJobParameter>& custom1,
const std::optional<fomac::CustomJobParameter>& custom2,
const std::optional<fomac::CustomJobParameter>& custom3,
const std::optional<fomac::CustomJobParameter>& custom4,
const std::optional<fomac::CustomJobParameter>& custom5) {
const auto bytes = std::span{
static_cast<const std::byte*>(program.data()), program.size()};
return self.submitJob(bytes, format, numShots, custom1, custom2,
custom3, custom4, custom5);
},
"program"_a, "program_format"_a, "num_shots"_a, 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 exact byte payload to the device.");

device.def("__repr__", [](const fomac::Device& dev) {
return "<Device name=\"" + dev.getName() + "\">";
Expand Down
45 changes: 25 additions & 20 deletions cmake/ExternalDependencies.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -99,24 +99,28 @@ FetchContent_Declare(
FIND_PACKAGE_ARGS ${QDMI_MINIMUM_VERSION})
list(APPEND FETCH_PACKAGES qdmi)

set(SPDLOG_VERSION
1.17.0
CACHE STRING "spdlog version")
set(SPDLOG_URL https://github.com/gabime/spdlog/archive/refs/tags/v${SPDLOG_VERSION}.tar.gz)
# Add position independent code for spdlog, this is required for python bindings on linux
set(SPDLOG_BUILD_PIC ON)
set(SPDLOG_SYSTEM_INCLUDES
ON
CACHE INTERNAL "Treat the library headers like system headers")
cmake_dependent_option(MQT_CORE_SPDLOG_INSTALL "Install spdlog library" ON "MQT_CORE_INSTALL" OFF)
# Disable upstream spdlog install rules and install with explicit MQT components below.
set(SPDLOG_INSTALL
OFF
CACHE BOOL "Disable upstream spdlog install rules; handled by mqt-core" FORCE)
cmake_dependent_option(SPDLOG_BUILD_SHARED "Build spdlog as shared library" ON
"BUILD_MQT_CORE_SHARED_LIBS" OFF)
FetchContent_Declare(spdlog URL ${SPDLOG_URL} FIND_PACKAGE_ARGS ${SPDLOG_VERSION})
list(APPEND FETCH_PACKAGES spdlog)
set(MQT_CORE_MANAGES_SPDLOG OFF)
if(NOT TARGET spdlog::spdlog)
set(SPDLOG_VERSION
1.17.0
CACHE STRING "spdlog version")
set(SPDLOG_URL https://github.com/gabime/spdlog/archive/refs/tags/v${SPDLOG_VERSION}.tar.gz)
# Add position independent code for spdlog, this is required for Python bindings on Linux.
set(SPDLOG_BUILD_PIC ON)
set(SPDLOG_SYSTEM_INCLUDES
ON
CACHE INTERNAL "Treat the library headers like system headers")
cmake_dependent_option(MQT_CORE_SPDLOG_INSTALL "Install spdlog library" ON "MQT_CORE_INSTALL" OFF)
# Disable upstream spdlog install rules and install with explicit MQT components below.
set(SPDLOG_INSTALL
OFF
CACHE BOOL "Disable upstream spdlog install rules; handled by mqt-core" FORCE)
cmake_dependent_option(SPDLOG_BUILD_SHARED "Build spdlog as shared library" ON
"BUILD_MQT_CORE_SHARED_LIBS" OFF)
FetchContent_Declare(spdlog URL ${SPDLOG_URL} FIND_PACKAGE_ARGS ${SPDLOG_VERSION})
list(APPEND FETCH_PACKAGES spdlog)
set(MQT_CORE_MANAGES_SPDLOG ON)
endif()

# Make all declared dependencies available.
FetchContent_MakeAvailable(${FETCH_PACKAGES})
Expand Down Expand Up @@ -180,7 +184,7 @@ if(MQT_CORE_JSON_INSTALL AND TARGET nlohmann_json)
endif()

# Ensure external shared libraries end up in a common lib layout used by our RUNPATH
if(TARGET spdlog)
if(MQT_CORE_MANAGES_SPDLOG AND TARGET spdlog)
set_target_properties(
spdlog
PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}"
Expand All @@ -189,7 +193,8 @@ if(TARGET spdlog)
endif()

# Install spdlog with explicit MQT components.
if(MQT_CORE_SPDLOG_INSTALL
if(MQT_CORE_MANAGES_SPDLOG
AND MQT_CORE_SPDLOG_INSTALL
AND TARGET spdlog
AND TARGET spdlog_header_only)
include(CMakePackageConfigHelpers)
Expand Down
22 changes: 22 additions & 0 deletions docs/qir/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,25 @@ Ordered; anything else, or a missing attribute, selects Labeled.
The QDMI Device accepts jobs in the following program formats: QASM2, QASM3, QIR
Base/Adaptive Profile Module (LLVM bitcode), and QIR Base/Adaptive Profile
String (LLVM assembly).

FoMaC C++ applications submit textual programs through the
`Device::submitJob(const std::string&, ...)` overload, which includes the
terminating null byte required by QDMI. Binary module payloads use the
`Device::submitJob(std::span<const std::byte>, ...)` overload instead. It
preserves embedded null bytes and submits exactly the span's size without
appending a terminator. `Job::getProgramBytes()` retrieves such a payload
without interpreting its format or removing terminal null bytes; the existing
`Job::getProgram()` remains the textual, null-terminated accessor. It rejects
known binary and non-text formats based on their QDMI format identifier, even if
their payload happens to end in a null byte.

The Python API follows the same distinction: pass `str` to `Device.submit_job`
for a textual program and `bytes` for an exact binary payload.
`Job.program_bytes` always returns the unmodified payload, while `Job.program`
expects a null-terminated UTF-8 text payload and rejects known binary or
non-text formats.

The generic submission APIs intentionally reject QDMI calibration and batch-job
formats. Calibration jobs do not carry a program, while batch jobs contain job
handles rather than serialized program bytes. Their format identifiers remain
available for capability discovery; they require dedicated typed APIs.
38 changes: 36 additions & 2 deletions include/mqt-core/fomac/FoMaC.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
#include <memory>
#include <optional>
#include <ranges>
#include <span>
#include <stdexcept>
#include <string>
#include <string_view>
Expand Down Expand Up @@ -517,7 +518,14 @@ class Device {
std::to_string(static_cast<unsigned>(property)));
}

/// @see QDMI_job_submit
/**
* @brief Submits a textual program.
* @details The terminating null byte required by QDMI text formats is
* included in the submitted payload.
* @throws std::invalid_argument If the format requires binary submission or
* does not carry a generic program payload.
* @see QDMI_job_submit
*/
[[nodiscard]] Job submitJob(
const std::string& program, QDMI_Program_Format format, size_t numShots,
const std::optional<CustomJobParameter>& custom1 = std::nullopt,
Expand All @@ -526,6 +534,23 @@ class Device {
const std::optional<CustomJobParameter>& custom4 = std::nullopt,
const std::optional<CustomJobParameter>& custom5 = std::nullopt) const;

/**
* @brief Submits a binary program.
* @details The bytes are submitted exactly as provided without appending a
* null byte.
* @throws std::invalid_argument If the format does not carry a generic
* program payload.
* @see QDMI_job_submit
*/
[[nodiscard]] Job submitJob(
std::span<const std::byte> program, QDMI_Program_Format format,
size_t numShots,
const std::optional<CustomJobParameter>& custom1 = std::nullopt,
const std::optional<CustomJobParameter>& custom2 = std::nullopt,
const std::optional<CustomJobParameter>& custom3 = std::nullopt,
const std::optional<CustomJobParameter>& custom4 = std::nullopt,
const std::optional<CustomJobParameter>& custom5 = std::nullopt) const;

auto operator<=>(const Device&) const noexcept = default;

private:
Expand Down Expand Up @@ -649,9 +674,18 @@ class Job {
/// Get the program format
[[nodiscard]] QDMI_Program_Format getProgramFormat() const;

/// Get the program to be executed
/**
* @brief Gets a textual program without its terminating null byte.
* @throws std::invalid_argument If the format is not textual or the device
* does not return a null-terminated payload.
*/
[[nodiscard]] std::string getProgram() const;

/**
* @brief Gets the submitted program bytes exactly as returned by the device.
*/
[[nodiscard]] std::vector<std::byte> getProgramBytes() const;

/// Get the number of shots
[[nodiscard]] size_t getNumShots() const;

Expand Down
24 changes: 23 additions & 1 deletion python/mqt/core/fomac.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,10 @@ class Job:
def program(self) -> str:
"""The submitted program."""

@property
def program_bytes(self) -> bytes:
"""The exact bytes of the submitted program."""

@property
def num_shots(self) -> int:
"""The number of shots."""
Expand Down Expand Up @@ -217,6 +221,8 @@ class ProgramFormat(enum.Enum):

IQM_JSON = 8

BATCH_JOB = 9

CUSTOM1 = 999999995

CUSTOM2 = 999999996
Expand Down Expand Up @@ -333,6 +339,7 @@ class Device:
when the custom slot is unsupported.
"""

@overload
def submit_job(
self,
program: str,
Expand All @@ -345,7 +352,22 @@ class Device:
custom4: str | bool | float | None = None,
custom5: str | bool | float | None = None,
) -> Job:
"""Submits a job to the device."""
"""Submits a text job to the device."""

@overload
def submit_job(
self,
program: bytes,
program_format: ProgramFormat,
num_shots: int,
*,
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 exact byte payload to the device."""

def __eq__(self, arg: object, /) -> bool: ...
def __ne__(self, arg: object, /) -> bool: ...
Expand Down
Loading
Loading