diff --git a/bindings/fomac/fomac.cpp b/bindings/fomac/fomac.cpp index cf64468e41..206d1593fa 100644 --- a/bindings/fomac/fomac.cpp +++ b/bindings/fomac/fomac.cpp @@ -32,6 +32,28 @@ namespace mqt { namespace nb = nanobind; using namespace nb::literals; +namespace { +[[nodiscard]] auto customJobParameterFromPython(const nb::object& value) + -> std::optional { + if (value.is_none()) { + return std::nullopt; + } + if (nb::isinstance(value)) { + return nb::cast(value); + } + if (nb::isinstance(value)) { + return nb::cast(value); + } + if (nb::isinstance(value)) { + return nb::cast(value); + } + if (nb::isinstance(value)) { + return nb::cast(value); + } + throw nb::type_error("custom parameter must be str, int, float, or bool"); +} +} // namespace + NB_MODULE(MQT_CORE_MODULE_NAME, m) { // Session class auto session = nb::class_( @@ -269,9 +291,31 @@ All authentication parameters are optional and can be provided as keyword argume &fomac::Device::getSupportedProgramFormats, "Returns the list of program formats supported by the device."); - device.def("submit_job", &fomac::Device::submitJob, "program"_a, - "program_format"_a, "num_shots"_a, - 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 programFormat, const size_t numShots, + const nb::object& custom1, const nb::object& custom2, + const nb::object& custom3, const nb::object& custom4, + const nb::object& custom5) { + return self.submitJob(program, programFormat, numShots, + customJobParameterFromPython(custom1), + customJobParameterFromPython(custom2), + customJobParameterFromPython(custom3), + customJobParameterFromPython(custom4), + customJobParameterFromPython(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, + nb::sig("def submit_job(self, program: str, program_format: " + "ProgramFormat, num_shots: int, *, custom1: str | int | float | " + "bool | None = None, custom2: str | int | float | bool | None " + "= None, custom3: str | int | float | bool | None = None, " + "custom4: str | int | float | bool | None = None, custom5: str " + "| int | float | bool | None = None) -> Job"), + "Submits a job to the device."); device.def("__repr__", [](const fomac::Device& dev) { return ""; diff --git a/include/mqt-core/fomac/FoMaC.hpp b/include/mqt-core/fomac/FoMaC.hpp index d34077ae43..4440d9edf6 100644 --- a/include/mqt-core/fomac/FoMaC.hpp +++ b/include/mqt-core/fomac/FoMaC.hpp @@ -14,11 +14,11 @@ #include "qdmi/types.h" #include +#include #include #include #include -#include #include #include #include @@ -28,9 +28,12 @@ #include #include #include +#include #include namespace fomac { +using CustomJobParameter = std::variant; + /** * @brief Concept for ranges that are contiguous in memory and can be * constructed with a size. @@ -318,9 +321,13 @@ class Device { getSupportedProgramFormats() const; /// @see QDMI_job_submit - [[nodiscard]] Job submitJob(const std::string& program, - QDMI_Program_Format format, - size_t numShots) const; + [[nodiscard]] Job submitJob( + const std::string& program, QDMI_Program_Format format, size_t numShots, + 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; auto operator<=>(const Device&) const noexcept = default; @@ -393,6 +400,30 @@ class Device { QDMI_Device device_; friend class Session; + + static void + setCustomJobParam(QDMI_Job job, QDMI_Job_Parameter param, + const std::optional& value) { + if (!value.has_value()) { + return; + } + + std::visit( + [&](const auto& customValue) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + qdmi::throwIfError(QDMI_job_set_parameter(job, param, + customValue.size() + 1, + customValue.c_str()), + "Setting custom parameter"); + } else { + qdmi::throwIfError( + QDMI_job_set_parameter(job, param, sizeof(T), &customValue), + "Setting custom parameter"); + } + }, + value.value()); + } }; /** @@ -498,6 +529,41 @@ static_assert(!std::is_copy_assignable()); static_assert(std::is_move_constructible()); static_assert(std::is_move_assignable()); +inline Job +Device::submitJob(const std::string& program, const QDMI_Program_Format format, + const size_t 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_, &job), "Creating job"); + Job jobWrapper{job}; + + qdmi::throwIfError(QDMI_job_set_parameter(jobWrapper, + QDMI_JOB_PARAMETER_PROGRAMFORMAT, + sizeof(format), &format), + "Setting program format"); + qdmi::throwIfError( + QDMI_job_set_parameter(jobWrapper, QDMI_JOB_PARAMETER_PROGRAM, + program.size() + 1, program.c_str()), + "Setting program"); + qdmi::throwIfError(QDMI_job_set_parameter(jobWrapper, + QDMI_JOB_PARAMETER_SHOTSNUM, + sizeof(numShots), &numShots), + "Setting number of shots"); + + setCustomJobParam(jobWrapper, QDMI_JOB_PARAMETER_CUSTOM1, custom1); + setCustomJobParam(jobWrapper, QDMI_JOB_PARAMETER_CUSTOM2, custom2); + setCustomJobParam(jobWrapper, QDMI_JOB_PARAMETER_CUSTOM3, custom3); + setCustomJobParam(jobWrapper, QDMI_JOB_PARAMETER_CUSTOM4, custom4); + setCustomJobParam(jobWrapper, QDMI_JOB_PARAMETER_CUSTOM5, custom5); + + qdmi::throwIfError(QDMI_job_submit(jobWrapper), "Submitting job"); + return jobWrapper; +} + /** * @brief Class representing a site (qubit) on the device. * @details diff --git a/python/mqt/core/fomac.pyi b/python/mqt/core/fomac.pyi index 6dfef7e4db..06c1110e8e 100644 --- a/python/mqt/core/fomac.pyi +++ b/python/mqt/core/fomac.pyi @@ -251,7 +251,18 @@ class Device: def supported_program_formats(self) -> list[ProgramFormat]: """Returns the list of program formats supported by the device.""" - def submit_job(self, program: str, program_format: ProgramFormat, num_shots: int) -> Job: + def submit_job( + self, + program: str, + program_format: ProgramFormat, + num_shots: int, + *, + custom1: str | int | float | bool | None = None, # noqa: PYI041 + custom2: str | int | float | bool | None = None, # noqa: PYI041 + custom3: str | int | float | bool | None = None, # noqa: PYI041 + custom4: str | int | float | bool | None = None, # noqa: PYI041 + custom5: str | int | float | bool | None = None, # noqa: PYI041 + ) -> Job: """Submits a job to the device.""" def __eq__(self, arg: object, /) -> bool: ... diff --git a/src/fomac/FoMaC.cpp b/src/fomac/FoMaC.cpp index 299fbbf9a8..b1822f765a 100644 --- a/src/fomac/FoMaC.cpp +++ b/src/fomac/FoMaC.cpp @@ -284,36 +284,6 @@ std::vector Device::getSupportedProgramFormats() const { QDMI_DEVICE_PROPERTY_SUPPORTEDPROGRAMFORMATS); } -Job Device::submitJob(const std::string& program, - const QDMI_Program_Format format, - const size_t numShots) const { - QDMI_Job job = nullptr; - qdmi::throwIfError(QDMI_device_create_job(device_, &job), "Creating job"); - Job jobWrapper{job}; // RAII wrapper to prevent leaks in case of exceptions - - // Set program format - qdmi::throwIfError(QDMI_job_set_parameter(jobWrapper, - QDMI_JOB_PARAMETER_PROGRAMFORMAT, - sizeof(format), &format), - "Setting program format"); - - // Set program - qdmi::throwIfError( - QDMI_job_set_parameter(jobWrapper, QDMI_JOB_PARAMETER_PROGRAM, - program.size() + 1, program.c_str()), - "Setting program"); - - // Set number of shots - qdmi::throwIfError(QDMI_job_set_parameter(jobWrapper, - QDMI_JOB_PARAMETER_SHOTSNUM, - sizeof(numShots), &numShots), - "Setting number of shots"); - - // Submit the job - qdmi::throwIfError(QDMI_job_submit(jobWrapper), "Submitting job"); - return jobWrapper; -} - QDMI_Job_Status Job::check() const { QDMI_Job_Status status{}; qdmi::throwIfError(QDMI_job_check(job_.get(), &status), diff --git a/src/qdmi/driver/Driver.cpp b/src/qdmi/driver/Driver.cpp index c2c1dba036..18b85434c7 100644 --- a/src/qdmi/driver/Driver.cpp +++ b/src/qdmi/driver/Driver.cpp @@ -275,6 +275,16 @@ namespace { return QDMI_DEVICE_JOB_PARAMETER_PROGRAMFORMAT; case QDMI_JOB_PARAMETER_SHOTSNUM: return QDMI_DEVICE_JOB_PARAMETER_SHOTSNUM; + case QDMI_JOB_PARAMETER_CUSTOM1: + return QDMI_DEVICE_JOB_PARAMETER_CUSTOM1; + case QDMI_JOB_PARAMETER_CUSTOM2: + return QDMI_DEVICE_JOB_PARAMETER_CUSTOM2; + case QDMI_JOB_PARAMETER_CUSTOM3: + return QDMI_DEVICE_JOB_PARAMETER_CUSTOM3; + case QDMI_JOB_PARAMETER_CUSTOM4: + return QDMI_DEVICE_JOB_PARAMETER_CUSTOM4; + case QDMI_JOB_PARAMETER_CUSTOM5: + return QDMI_DEVICE_JOB_PARAMETER_CUSTOM5; default: return QDMI_DEVICE_JOB_PARAMETER_MAX; } @@ -286,7 +296,8 @@ QDMI_Job_impl_d::~QDMI_Job_impl_d() { } auto QDMI_Job_impl_d::setParameter(QDMI_Job_Parameter param, const size_t size, const void* value) const -> int { - if ((value != nullptr && size == 0) || param >= QDMI_JOB_PARAMETER_MAX) { + if ((value != nullptr && size == 0) || + IS_INVALID_ARGUMENT(param, QDMI_JOB_PARAMETER)) { return QDMI_ERROR_INVALIDARGUMENT; } return device_->getLibrary().device_job_set_parameter( diff --git a/test/fomac/test_fomac.cpp b/test/fomac/test_fomac.cpp index 48ed1bffd3..209defa9b9 100644 --- a/test/fomac/test_fomac.cpp +++ b/test/fomac/test_fomac.cpp @@ -556,6 +556,25 @@ c = measure q;)"; EXPECT_EQ(job.check(), QDMI_JOB_STATUS_DONE); } +TEST_F(DDSimulatorDeviceTest, SubmitJobCustom1SupportedTypes) { + const std::string qasm3Program = R"(OPENQASM 3.0;)"; + + auto submitWithCustom1 = [&](auto custom1) { + try { + const auto job = device.submitJob(qasm3Program, QDMI_PROGRAM_FORMAT_QASM3, + 10, custom1); + EXPECT_NO_THROW(std::ignore = job.getNumShots()); + } catch (const std::runtime_error&) { + GTEST_SKIP() << "Custom job parameter not supported by backend"; + } + }; + + submitWithCustom1(std::string("custom")); + submitWithCustom1(42); + submitWithCustom1(3.14); + submitWithCustom1(true); +} + TEST_F(DDSimulatorDeviceTest, SubmitJobPreservesNumShots) { const std::string qasm3Program = R"( OPENQASM 3.0; diff --git a/test/python/fomac/test_fomac.py b/test/python/fomac/test_fomac.py index e53a04799b..37b826e416 100644 --- a/test/python/fomac/test_fomac.py +++ b/test/python/fomac/test_fomac.py @@ -421,6 +421,25 @@ def test_device_submit_job_returns_valid_job(ddsim_device: Device) -> None: assert job.num_shots == 100 +@pytest.mark.parametrize("custom_kwarg", ["custom1", "custom2", "custom3", "custom4", "custom5"]) +def test_device_submit_job_forwards_custom_parameters(ddsim_device: Device, custom_kwarg: str) -> None: + """Test that submit_job forwards custom job parameters to DDSIM.""" + with pytest.raises(RuntimeError, match=r"Setting custom parameter: Not supported\."): + ddsim_device.submit_job("OPENQASM 3.0;", ProgramFormat.QASM3, 1, **{custom_kwarg: "value"}) + + +@pytest.mark.parametrize("custom_value", ["value", 42, 1.5, True]) +def test_device_submit_job_accepts_custom_parameter_types(ddsim_device: Device, custom_value: object) -> None: + """Test that submit_job accepts supported custom parameter types.""" + with pytest.raises(RuntimeError, match=r"Setting custom parameter: Not supported\."): + ddsim_device.submit_job( + "OPENQASM 3.0;", + ProgramFormat.QASM3, + 1, + custom1=cast("str | int | float | bool", custom_value), + ) + + def test_device_submit_job_preserves_num_shots(ddsim_device: Device) -> None: """Test that different shot counts are correctly preserved.""" qasm3_program = """ diff --git a/test/qdmi/driver/test_driver.cpp b/test/qdmi/driver/test_driver.cpp index 42b876e5c1..563f7a2d5e 100644 --- a/test/qdmi/driver/test_driver.cpp +++ b/test/qdmi/driver/test_driver.cpp @@ -168,6 +168,16 @@ TEST_P(DriverJobTest, JobSetParameter) { EXPECT_THAT(QDMI_job_set_parameter(job, QDMI_JOB_PARAMETER_SHOTSNUM, sizeof(size_t), &numShots), testing::AnyOf(QDMI_SUCCESS, QDMI_ERROR_NOTSUPPORTED)); + constexpr std::array customParams{ + QDMI_JOB_PARAMETER_CUSTOM1, QDMI_JOB_PARAMETER_CUSTOM2, + QDMI_JOB_PARAMETER_CUSTOM3, QDMI_JOB_PARAMETER_CUSTOM4, + QDMI_JOB_PARAMETER_CUSTOM5}; + constexpr std::array customValue{std::byte{42}}; + for (const auto param : customParams) { + EXPECT_THAT(QDMI_job_set_parameter(job, param, customValue.size(), + customValue.data()), + testing::AnyOf(QDMI_SUCCESS, QDMI_ERROR_NOTSUPPORTED)); + } EXPECT_EQ(QDMI_job_set_parameter(job, QDMI_JOB_PARAMETER_MAX, 0, nullptr), QDMI_ERROR_INVALIDARGUMENT); }