diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 21c2406..ca3bb84 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -88,7 +88,7 @@ jobs: clang-version: 20 cmake-args: -DBUILD_MQT_DEBUGGER_BINDINGS=ON files-changed-only: true - install-pkgs: "pybind11==3.0.1" + install-pkgs: "nanobind==2.10.2" setup-python: true cpp-linter-extra-args: "-std=c++20" @@ -125,6 +125,8 @@ jobs: needs: change-detection if: fromJSON(needs.change-detection.outputs.run-python-tests) uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-python-linter.yml@d6314c45667c131055a0389afc110e8dedc6da3f # v1.17.11 + with: + check-stubs: true build-sdist: name: 🚀 CD diff --git a/CMakeLists.txt b/CMakeLists.txt index 62c727d..3bb5999 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -42,10 +42,8 @@ if(BUILD_MQT_DEBUGGER_BINDINGS) CACHE BOOL "Prevent multiple searches for Python and instead cache the results.") # top-level call to find Python - find_package( - Python 3.10 REQUIRED - COMPONENTS Interpreter Development.Module - OPTIONAL_COMPONENTS Development.SABIModule) + find_package(Python 3.10 REQUIRED COMPONENTS Interpreter Development.Module + ${SKBUILD_SABI_COMPONENT}) endif() include(cmake/ExternalDependencies.cmake) diff --git a/bindings/CMakeLists.txt b/bindings/CMakeLists.txt index 2462db4..099dd20 100644 --- a/bindings/CMakeLists.txt +++ b/bindings/CMakeLists.txt @@ -5,7 +5,7 @@ # # Licensed under the MIT License -add_mqt_python_binding( +add_mqt_python_binding_nanobind( DEBUGGER ${MQT_DEBUGGER_TARGET_NAME}-bindings bindings.cpp @@ -16,5 +16,4 @@ add_mqt_python_binding( INSTALL_DIR . LINK_LIBS - MQT::Debugger - pybind11_json) + MQT::Debugger) diff --git a/bindings/InterfaceBindings.cpp b/bindings/InterfaceBindings.cpp index 9b2ea20..1d636a4 100644 --- a/bindings/InterfaceBindings.cpp +++ b/bindings/InterfaceBindings.cpp @@ -14,8 +14,6 @@ * diagnostics interfaces. */ -#include "python/InterfaceBindings.hpp" - #include "backend/debug.h" #include "backend/diagnostics.h" #include "common.h" @@ -23,19 +21,17 @@ #include #include #include -#include -#include -#include -#include -#include -#include // NOLINT(misc-include-cleaner) +#include +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) #include #include #include #include -namespace py = pybind11; -using namespace pybind11::literals; +namespace nb = nanobind; +using namespace nb::literals; namespace { @@ -64,53 +60,48 @@ struct StatevectorCPP { std::vector amplitudes; }; -void bindFramework(py::module& m) { +// NOLINTNEXTLINE(misc-use-internal-linkage) +void bindFramework(nb::module_& m) { // Bind the VariableType enum - py::native_enum(m, "VariableType", "enum.Enum", - "The type of a classical variable.") + nb::enum_(m, "VariableType", + "The type of a classical variable.") .value("VarBool", VarBool, "A boolean variable.") .value("VarInt", VarInt, "An integer variable.") - .value("VarFloat", VarFloat, "A floating-point variable.") - .export_values() - .finalize(); + .value("VarFloat", VarFloat, "A floating-point variable."); // Bind the VariableValue union - py::class_(m, "VariableValue") - .def(py::init<>()) - .def_readwrite("bool_value", &VariableValue::boolValue, - "The value of a boolean variable.") - .def_readwrite("int_value", &VariableValue::intValue, - "The value of an integer variable.") - .def_readwrite("float_value", &VariableValue::floatValue, - "The value of a floating-point variable.") + nb::class_(m, "VariableValue") + .def(nb::init<>()) + .def_rw("bool_value", &VariableValue::boolValue, + "The value of a boolean variable.") + .def_rw("int_value", &VariableValue::intValue, + "The value of an integer variable.") + .def_rw("float_value", &VariableValue::floatValue, + "The value of a floating-point variable.") .doc() = R"(Represents the value of a classical variable. Only one of these fields has a valid value at a time, based on the variable's `VariableType`.)"; // Bind the Variable struct - py::class_(m, "Variable") - .def(py::init<>(), "Creates a new `Variable` instance.") - .def_readwrite("name", &Variable::name, "The name of the variable.") - .def_readwrite("type", &Variable::type, "The type of the variable.") - .def_readwrite("value", &Variable::value, "The value of the variable.") + nb::class_(m, "Variable") + .def(nb::init<>(), "Creates a new `Variable` instance.") + .def_rw("name", &Variable::name, "The name of the variable.") + .def_rw("type_", &Variable::type, "The type of the variable.") + .def_rw("value", &Variable::value, "The value of the variable.") .doc() = "Represents a classical variable."; // Bind the Complex struct - py::class_(m, "Complex") - .def(py::init<>(), R"(Initializes a new complex number. - -Args: - real (float, optional): The real part of the complex number. Defaults to 0.0. - imaginary (float, optional): The imaginary part of the complex number. Defaults to 0.0.)") - .def(py::init(), R"(Initializes a new complex number. + nb::class_(m, "Complex") + .def(nb::init<>(), R"(Initializes a new complex number.)") + .def(nb::init(), "real"_a = 0.0, "imaginary"_a = 0.0, + R"(Initializes a new complex number. Args: - real (float, optional): The real part of the complex number. Defaults to 0.0. - imaginary (float, optional): The imaginary part of the complex number. Defaults to 0.0.)") - .def_readwrite("real", &Complex::real, - "The real part of the complex number.") - .def_readwrite("imaginary", &Complex::imaginary, - "The imaginary part of the complex number.") + real: The real part of the complex number. Defaults to 0.0. + imaginary: The imaginary part of the complex number. Defaults to 0.0.)") + .def_rw("real", &Complex::real, "The real part of the complex number.") + .def_rw("imaginary", &Complex::imaginary, + "The imaginary part of the complex number.") .def( "__str__", [](const Complex& self) { @@ -118,8 +109,9 @@ Only one of these fields has a valid value at a time, based on the variable's `V std::to_string(self.imaginary) + "i"; }, R"(Returns a string representation of the complex number. + Returns: - str: The string representation of the complex number. + The string representation of the complex number. )") .def( "__repr__", @@ -130,44 +122,42 @@ Only one of these fields has a valid value at a time, based on the variable's `V R"(Returns a string representation of the complex number. Returns: - str: The string representation of the complex number.)") + The string representation of the complex number.)") .doc() = "Represents a complex number."; // Bind the Statevector struct - py::class_(m, "Statevector") - .def(py::init<>(), "Creates a new `Statevector` instance.") - .def_readwrite("num_qubits", &StatevectorCPP::numQubits, - "The number of qubits in the state vector.") - .def_readwrite("num_states", &StatevectorCPP::numStates, - R"(The number of states in the state vector. + nb::class_(m, "Statevector") + .def(nb::init<>(), "Creates a new `Statevector` instance.") + .def_rw("num_qubits", &StatevectorCPP::numQubits, + "The number of qubits in the state vector.") + .def_rw("num_states", &StatevectorCPP::numStates, + R"(The number of states in the state vector. This is always equal to 2^`num_qubits`.)") - .def_readwrite("amplitudes", &StatevectorCPP::amplitudes, - R"(The amplitudes of the state vector. + .def_rw("amplitudes", &StatevectorCPP::amplitudes, + R"(The amplitudes of the state vector. Contains one element for each of the `num_states` states in the state vector.)") .doc() = "Represents a state vector."; - py::class_(m, "CompilationSettings") - .def(py::init(), py::arg("opt"), - py::arg("slice_index") = 0, + nb::class_(m, "CompilationSettings") + .def(nb::init(), "opt"_a, "slice_index"_a = 0, R"(Initializes a new set of compilation settings. Args: - opt (int): The optimization level that should be used. - slice_index (int, optional): The index of the slice that should be compiled (defaults to 0).)") - .def_readwrite( + opt: The optimization level that should be used. + slice_index: The index of the slice that should be compiled (defaults to 0).)") + .def_rw( "opt", &CompilationSettings ::opt, "The optimization level that should be used. Exact meaning depends " - "on " - "the implementation, but typically 0 means no optimization.") - .def_readwrite("slice_index", &CompilationSettings::sliceIndex, - "The index of the slice that should be compiled.") + "on the implementation, but typically 0 means no optimization.") + .def_rw("slice_index", &CompilationSettings::sliceIndex, + "The index of the slice that should be compiled.") .doc() = "The settings that should be used to compile an assertion program."; - py::class_(m, "SimulationState") - .def(py::init<>(), "Creates a new `SimulationState` instance.") + nb::class_(m, "SimulationState") + .def(nb::init<>(), "Creates a new `SimulationState` instance.") .def( "init", [](SimulationState* self) { checkOrThrow(self->init(self)); }, "Initializes the simulation state.") @@ -176,10 +166,11 @@ Contains one element for each of the `num_states` states in the state vector.)") [](SimulationState* self, const char* code) { checkOrThrow(self->loadCode(self, code)); }, + "code"_a, R"(Loads the given code into the simulation state. Args: - code (str): The code to load.)") + code: The code to load.)") .def( "step_forward", [](SimulationState* self) { checkOrThrow(self->stepForward(self)); }, @@ -226,7 +217,7 @@ Contains one element for each of the `num_states` states in the state vector.)") R"(Runs the simulation until it finishes, even if assertions fail. Returns: -int: The number of assertions that failed during execution.)") + The number of assertions that failed during execution.)") .def( "run_simulation", [](SimulationState* self) { @@ -274,7 +265,7 @@ The simulation is unable to step forward if it has finished or if the simulation has not been set up yet. Returns: -bool: True, if the simulation can step forward.)") + True, if the simulation can step forward.)") .def( "can_step_backward", [](SimulationState* self) { return self->canStepBackward(self); }, @@ -284,31 +275,32 @@ The simulation is unable to step backward if it is at the beginning or if the simulation has not been set up yet. Returns: -bool: True, if the simulation can step backward.)") + True, if the simulation can step backward.)") .def( "change_classical_variable_value", [](SimulationState* self, const std::string& variableName, - const py::object& newValue) { + const nb::object& newValue) { VariableValue value{}; - if (py::isinstance(newValue)) { - value.boolValue = newValue.cast(); - } else if (py::isinstance(newValue)) { - value.intValue = newValue.cast(); - } else if (py::isinstance(newValue)) { - value.floatValue = newValue.cast(); + if (nb::isinstance(newValue)) { + value.boolValue = nb::cast(newValue); + } else if (nb::isinstance(newValue)) { + value.intValue = nb::cast(newValue); + } else if (nb::isinstance(newValue)) { + value.floatValue = nb::cast(newValue); } else { - throw py::type_error( + throw nb::type_error( "change_classical_variable_value requires a bool, " "int, or float value"); } checkOrThrow(self->changeClassicalVariableValue( self, variableName.c_str(), &value)); }, + "variable_name"_a, "new_value"_a, R"(Updates the value of a classical variable. Args: - variableName (str): The name of the classical variable to update. - newValue (bool | float): The desired value.)") + variable_name: The name of the classical variable to update. + new_value: The desired value.)") .def( "change_amplitude_value", [](SimulationState* self, const std::string& basisState, @@ -316,6 +308,7 @@ bool: True, if the simulation can step backward.)") checkOrThrow( self->changeAmplitudeValue(self, basisState.c_str(), &value)); }, + "basis_state"_a, "value"_a, R"(Updates the amplitude of a given computational basis state. The basis state is provided as a bitstring whose length matches the @@ -324,8 +317,8 @@ remaining amplitudes so that the state vector stays normalized and to reject invalid bitstrings or amplitudes that violate normalization. Args: - basisState (str): The bitstring identifying the basis state to update. - value (Complex): The desired complex amplitude.)") + basis_state: The bitstring identifying the basis state to update. + value: The desired complex amplitude.)") .def( "is_finished", [](SimulationState* self) { return self->isFinished(self); }, @@ -334,7 +327,7 @@ reject invalid bitstrings or amplitudes that violate normalization. The execution is considered finished if it has reached the end of the code. Returns: -bool: True, if the execution has finished.)") + True, if the execution has finished.)") .def( "did_assertion_fail", [](SimulationState* self) { return self->didAssertionFail(self); }, @@ -344,7 +337,7 @@ If execution is continued after a failed assertion, then this flag will be set to false again. Returns: -bool: True, if an assertion failed during the last step.)") + True, if an assertion failed during the last step.)") .def( "was_breakpoint_hit", [](SimulationState* self) { return self->wasBreakpointHit(self); }, @@ -354,7 +347,7 @@ If execution is continued after a breakpoint, then this flag will be set to false again. Returns: -bool: True, if a breakpoint was hit during the last step.)") + True, if a breakpoint was hit during the last step.)") .def( "get_current_instruction", [](SimulationState* self) { @@ -363,14 +356,14 @@ bool: True, if a breakpoint was hit during the last step.)") R"(Gets the current instruction index. Returns: -int: The index of the current instruction.)") + The index of the current instruction.)") .def( "get_instruction_count", [](SimulationState* self) { return self->getInstructionCount(self); }, R"(Gets the number of instructions in the code. Returns: - int: The number of instructions in the code.)") + The number of instructions in the code.)") .def( "get_instruction_position", [](SimulationState* self, size_t instruction) { @@ -380,22 +373,23 @@ int: The index of the current instruction.)") self->getInstructionPosition(self, instruction, &start, &end)); return std::make_pair(start, end); }, + "instruction"_a, R"(Gets the position of the given instruction in the code. Start and end positions are inclusive and white-spaces are ignored. Args: - instruction (int): The instruction to find. + instruction: The instruction to find. Returns: - tuple[int, int]: The start and end positions of the instruction.)") + The start and end positions of the instruction.)") .def( "get_num_qubits", [](SimulationState* self) { return self->getNumQubits(self); }, R"(Gets the number of qubits used by the program. Returns: - int: The number of qubits used by the program.)") + The number of qubits used by the program.)") .def( "get_amplitude_index", [](SimulationState* self, size_t qubit) { @@ -403,16 +397,17 @@ Start and end positions are inclusive and white-spaces are ignored. checkOrThrow(self->getAmplitudeIndex(self, qubit, &output)); return output; }, + "index"_a, R"(Gets the complex amplitude of a state in the full state vector. The amplitude is selected by an integer index that corresponds to the binary representation of the state. Args: - index (int): The index of the state in the full state vector. + index: The index of the state in the full state vector. Returns: - Complex: The complex amplitude of the state.)") + The complex amplitude of the state.)") .def( "get_amplitude_bitstring", [](SimulationState* self, const char* bitstring) { @@ -420,15 +415,16 @@ binary representation of the state. checkOrThrow(self->getAmplitudeBitstring(self, bitstring, &output)); return output; }, + "bitstring"_a, R"(Gets the complex amplitude of a state in the full state vector. The amplitude is selected by a bitstring representing the state. Args: - bitstring (str): The index of the state as a bitstring. + bitstring: The index of the state as a bitstring. Returns: - Complex: The complex amplitude of the state.)") + The complex amplitude of the state.)") .def( "get_classical_variable", [](SimulationState* self, const char* name) { @@ -436,16 +432,17 @@ The amplitude is selected by a bitstring representing the state. checkOrThrow(self->getClassicalVariable(self, name, &output)); return output; }, + "name"_a, R"(Gets a classical variable by name. For registers, the name should be the register name followed by the index in square brackets. Args: - name (str): The name of the variable. + name: The name of the variable. Returns: - Variable: The fetched variable.)") + The fetched variable.)") .def( "get_num_classical_variables", [](SimulationState* self) { @@ -456,7 +453,7 @@ in square brackets. For registers, each index is counted as a separate variable. Returns: - int: The number of classical variables in the simulation.)") + The number of classical variables in the simulation.)") .def( "get_classical_variable_name", [](SimulationState* self, size_t variableIndex) { @@ -469,6 +466,7 @@ For registers, each index is counted as a separate variable. } return output; }, + "index"_a, R"(Gets the name of a classical variable by its index. For registers, each index is counted as a separate variable and can be @@ -476,10 +474,10 @@ accessed separately. This method will return the name of the specific index of the register. Args: - index (int): The index of the variable. + index: The index of the variable. Returns: - str: The name of the variable.)") + The name of the variable.)") .def( "get_quantum_variable_name", [](SimulationState* self, size_t variableIndex) { @@ -492,6 +490,7 @@ index of the register. } return output; }, + "index"_a, R"(Gets the name of a quantum variable by its index. For registers, each index is counted as a separate variable and can be @@ -499,10 +498,10 @@ accessed separately. This method will return the name of the specific index of the register. Args: - index (int): The index of the variable. + index: The index of the variable. Returns: - str: The name of the variable.)") + The name of the variable.)") .def( "get_state_vector_full", [](SimulationState* self) { @@ -519,11 +518,8 @@ index of the register. }, R"(Gets the full state vector of the simulation at the current time. -The state vector is expected to be initialized with the correct number of -qubits and allocated space for the amplitudes before calling this method. - Returns: - Statevector: The full state vector of the current simulation state.)") + The full state vector of the current simulation state.)") .def( "get_state_vector_sub", [](SimulationState* self, std::vector qubits) { @@ -539,19 +535,17 @@ qubits and allocated space for the amplitudes before calling this method. &output)); return result; }, + "qubits"_a, R"(Gets a sub-state of the state vector of the simulation at the current time. -The state vector is expected to be initialized with the correct number of -qubits and allocated space for the amplitudes before calling this method. - This method also supports the re-ordering of qubits, but does not allow qubits to be repeated. Args: - qubits (list[int]): The qubits to include in the sub-state. + qubits: The qubits to include in the sub-state. Returns: - Statevector: The sub-state vector of the current simulation state.)") + The sub-state vector of the current simulation state.)") .def( "set_breakpoint", [](SimulationState* self, size_t desiredPosition) { @@ -560,16 +554,17 @@ qubits to be repeated. self->setBreakpoint(self, desiredPosition, &actualPosition)); return actualPosition; }, + "desired_position"_a, R"(Sets a breakpoint at the desired position in the code. The position is given as a 0-indexed character position in the full code string. Args: - desired_position (int): The position in the code to set the breakpoint. + desired_position: The position in the code to set the breakpoint. Returns: - int: The index of the instruction where the breakpoint was set.)") + The index of the instruction where the breakpoint was set.)") .def( "clear_breakpoints", [](SimulationState* self) { @@ -588,7 +583,7 @@ string. Each custom gate call corresponds to one stack entry. Returns: - int: The current stack depth of the simulation.)") + The current stack depth of the simulation.)") .def( "get_stack_trace", [](SimulationState* self, size_t maxDepth) { @@ -600,6 +595,7 @@ Each custom gate call corresponds to one stack entry. self->getStackTrace(self, maxDepth, stackTrace.data())); return stackTrace; }, + "max_depth"_a, R"(Gets the current stack trace of the simulation. The stack trace is represented as a list of instruction indices. Each @@ -607,18 +603,18 @@ instruction index represents a single return address for the corresponding stack entry. Args: - max_depth (int): The maximum depth of the stack trace. + max_depth: The maximum depth of the stack trace. Returns: - list[int]: The stack trace of the simulation.)") + The stack trace of the simulation.)") .def( "get_diagnostics", [](SimulationState* self) { return self->getDiagnostics(self); }, - py::return_value_policy::reference_internal, + nb::rv_policy::reference_internal, R"(Gets the diagnostics instance employed by this debugger. Returns: - Diagnostics: The diagnostics instance employed by this debugger.)") + The diagnostics instance employed by this debugger.)") .def( "compile", [](SimulationState* self, CompilationSettings& settings) { @@ -631,44 +627,44 @@ stack entry. std::string result(buffer.data(), size - 1); return result; }, + "settings"_a, R"(Compiles the given code into a quantum circuit without assertions. Args: - settings (CompilationSettings): The settings to use for the compilation. + settings: The settings to use for the compilation. Returns: - str: The compiled code.)") + The compiled code.)") .doc() = R"(Represents the state of a quantum simulation for debugging. -"This is the main class of the `mqt-debugger` library, allowing developers to step through the code and inspect the state of the simulation.)"; +This is the main class of the `mqt-debugger` library, allowing developers to step through the code and inspect the state of the simulation.)"; } -void bindDiagnostics(py::module& m) { +// NOLINTNEXTLINE(misc-use-internal-linkage) +void bindDiagnostics(nb::module_& m) { // Bind the ErrorCauseType enum - py::native_enum(m, "ErrorCauseType", "enum.Enum", - "The type of a potential error cause.") + nb::enum_(m, "ErrorCauseType", + "The type of a potential error cause.") .value("Unknown", Unknown, "An unknown error cause.") .value("MissingInteraction", MissingInteraction, "Indicates that an entanglement error may be caused by a missing " "interaction.") .value("ControlAlwaysZero", ControlAlwaysZero, "Indicates that an error may be related to a controlled gate with " - "a control that is always zero.") - .export_values() - .finalize(); + "a control that is always zero."); // Bind the ErrorCause struct - py::class_(m, "ErrorCause") - .def(py::init<>()) - .def_readwrite("instruction", &ErrorCause ::instruction, - "The instruction where the error may originate from or " - "where the error can be detected.") - .def_readwrite("type", &ErrorCause ::type, - "The type of the potential error cause.") + nb::class_(m, "ErrorCause") + .def(nb::init<>()) + .def_rw("instruction", &ErrorCause ::instruction, + "The instruction where the error may originate from or " + "where the error can be detected.") + .def_rw("type_", &ErrorCause ::type, + "The type of the potential error cause.") .doc() = "Represents a potential cause of an assertion error."; - py::class_(m, "Diagnostics") - .def(py::init<>(), "Creates a new `Diagnostics` instance.") + nb::class_(m, "Diagnostics") + .def(nb::init<>(), "Creates a new `Diagnostics` instance.") .def( "init", [](Diagnostics* self) { checkOrThrow(self->init(self)); }, "Initializes the diagnostics instance.") @@ -678,14 +674,14 @@ void bindDiagnostics(py::module& m) { R"(Get the number of qubits in the system. Returns: - int: The number of qubits in the system.)") + The number of qubits in the system.)") .def( "get_instruction_count", [](Diagnostics* self) { return self->getInstructionCount(self); }, R"(Get the number of instructions in the code. Returns: - int: The number of instructions in the code.)") + The number of instructions in the code.)") .def( "get_data_dependencies", [](Diagnostics* self, size_t instruction, bool includeCallers) { @@ -703,7 +699,7 @@ void bindDiagnostics(py::module& m) { } return result; }, - py::arg("instruction"), py::arg("include_callers") = false, + "instruction"_a, "include_callers"_a = false, R"(Extract all data dependencies for a given instruction. If the instruction is inside a custom gate definition, the data @@ -719,11 +715,11 @@ This method can be performed without running the program, as it is a static analysis method. Args: - instruction (int): The instruction to extract the data dependencies for. - include_callers (bool, optional): True, if the data dependencies should include all possible callers of the containing custom gate. Defaults to False. + instruction: The instruction to extract the data dependencies for. + include_callers: True, if the data dependencies should include all possible callers of the containing custom gate. Defaults to False. Returns: - list[int]: A list of instruction indices that are data dependencies of the given instruction.)") + A list of instruction indices that are data dependencies of the given instruction.)") .def( "get_interactions", [](Diagnostics* self, size_t beforeInstruction, size_t qubit) { @@ -741,6 +737,7 @@ analysis method. } return result; }, + "before_instruction"_a, "qubit"_a, R"(Extract all qubits that interact with a given qubit up to a specific instruction. If the target instruction is inside a custom gate definition, the @@ -753,11 +750,11 @@ This method can be performed without running the program, as it is a static analysis method. Args: - before_instruction (int): The instruction to extract the interactions up to (excluding). - qubit (int): The qubit to extract the interactions for. + before_instruction: The instruction to extract the interactions up to (excluding). + qubit: The qubit to extract the interactions for. Returns: - list[int]: A list of qubit indices that interact with the given qubit up to the target instruction.)") + A list of qubit indices that interact with the given qubit up to the target instruction.)") .def( "get_zero_control_instructions", [](Diagnostics* self) { @@ -785,7 +782,7 @@ This method can only be performed at runtime, as it is a dynamic analysis method. Returns: - list[int]: The indices of instructions that are controlled gates with only zero controls.)") + The indices of instructions that are controlled gates with only zero controls.)") .def( "potential_error_causes", [](Diagnostics* self) { @@ -807,7 +804,7 @@ This method should be run after the program has been executed and reached an assertion error. Returns: - list[ErrorCause]: A list of potential error causes encountered during execution.)") + A list of potential error causes encountered during execution.)") .def( "suggest_assertion_movements", [](Diagnostics* self) { @@ -831,7 +828,7 @@ Each entry of the resulting list consists of the original position of the assert suggested position. Returns: - list[tuple[int, int]]: A list of moved assertions. + A list of moved assertions. )") .def( "suggest_new_assertions", @@ -877,7 +874,7 @@ Each entry of the resulting list consists of the suggested position for the new string representation. Returns: - list[tuple[int, str]]: A list of new assertions. + A list of new assertions. )") .doc() = "Provides diagnostics capabilities such as different analysis " "methods for the debugger."; diff --git a/bindings/bindings.cpp b/bindings/bindings.cpp index af2cb34..68bfb03 100644 --- a/bindings/bindings.cpp +++ b/bindings/bindings.cpp @@ -15,16 +15,17 @@ * Central file for defining the Python bindings for the framework. */ -#include "python/InterfaceBindings.hpp" -#include "python/dd/DDSimDebugBindings.hpp" +#include -#include -#include +namespace nb = nanobind; +using namespace nb::literals; -namespace py = pybind11; -using namespace pybind11::literals; +// forward declarations +void bindFramework(nb::module_& m); +void bindDiagnostics(nb::module_& m); +void bindBackend(nb::module_& m); -PYBIND11_MODULE(MQT_DEBUGGER_MODULE_NAME, m, py::mod_gil_not_used()) { +NB_MODULE(MQT_DEBUGGER_MODULE_NAME, m) { bindDiagnostics(m); bindFramework(m); bindBackend(m); diff --git a/bindings/dd/DDSimDebugBindings.cpp b/bindings/dd/DDSimDebugBindings.cpp index 9fffb32..7f1cb93 100644 --- a/bindings/dd/DDSimDebugBindings.cpp +++ b/bindings/dd/DDSimDebugBindings.cpp @@ -16,16 +16,16 @@ * Diagnostics states. */ -#include "python/dd/DDSimDebugBindings.hpp" - #include "backend/dd/DDSimDebug.hpp" #include "backend/debug.h" -#include "pybind11/pybind11.h" +#include "nanobind/nanobind.h" +namespace nb = nanobind; +using namespace nb::literals; using namespace mqt::debugger; -void bindBackend(pybind11::module& m) { - +// NOLINTNEXTLINE(misc-use-internal-linkage) +void bindBackend(nb::module_& m) { m.def( "create_ddsim_simulation_state", []() { @@ -37,17 +37,17 @@ void bindBackend(pybind11::module& m) { R"(Creates a new `SimulationState` instance using the DD backend for simulation and the OpenQASM language as input format. Returns: - SimulationState: The created simulation state.)"); + The created simulation state.)"); m.def( "destroy_ddsim_simulation_state", [](SimulationState* state) { - // NOLINTBEGIN(cppcoreguidelines-pro-type-reinterpret-cast) + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) destroyDDSimulationState(reinterpret_cast(state)); - // NOLINTEND(cppcoreguidelines-pro-type-reinterpret-cast) }, + "state"_a, R"(Delete a given DD-based `SimulationState` instance and free up resources. Args: - state (SimulationState): The simulation state to delete.)"); + state: The simulation state to delete.)"); } diff --git a/bindings/debugger_patterns.txt b/bindings/debugger_patterns.txt new file mode 100644 index 0000000..55bcfaf --- /dev/null +++ b/bindings/debugger_patterns.txt @@ -0,0 +1,3 @@ +_hashable_values_: + +_unhashable_values_map_: diff --git a/cmake/ExternalDependencies.cmake b/cmake/ExternalDependencies.cmake index 7d6c911..a45b82d 100644 --- a/cmake/ExternalDependencies.cmake +++ b/cmake/ExternalDependencies.cmake @@ -10,92 +10,50 @@ include(FetchContent) set(FETCH_PACKAGES "") if(BUILD_MQT_DEBUGGER_BINDINGS) - if(NOT SKBUILD) - # Manually detect the installed pybind11 package. - execute_process( - COMMAND "${Python_EXECUTABLE}" -m pybind11 --cmakedir - OUTPUT_STRIP_TRAILING_WHITESPACE - OUTPUT_VARIABLE pybind11_DIR) - - # Add the detected directory to the CMake prefix path. - list(APPEND CMAKE_PREFIX_PATH "${pybind11_DIR}") - endif() - - # add pybind11 library - find_package(pybind11 3.0.1 CONFIG REQUIRED) + execute_process( + COMMAND "${Python_EXECUTABLE}" -m nanobind --cmake_dir + OUTPUT_STRIP_TRAILING_WHITESPACE + OUTPUT_VARIABLE nanobind_ROOT) + find_package(nanobind CONFIG REQUIRED) endif() # ---------------------------------------------------------------------------------Fetch MQT Core # cmake-format: off -set(MQT_CORE_MINIMUM_VERSION 3.3.1 +set(MQT_CORE_MINIMUM_VERSION 3.4.0 CACHE STRING "MQT Core minimum version") -set(MQT_CORE_VERSION 3.3.3 +set(MQT_CORE_VERSION 3.4.0 CACHE STRING "MQT Core version") -set(MQT_CORE_REV "8c9f6ab24968401e450812fc0ff7d05b5ae07a63" +set(MQT_CORE_REV "6bcc01e7d135058c6439c64fdd5f14b65ab88816" CACHE STRING "MQT Core identifier (tag, branch or commit hash)") set(MQT_CORE_REPO_OWNER "munich-quantum-toolkit" CACHE STRING "MQT Core repository owner (change when using a fork)") # cmake-format: on -if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.24) - # Fetch MQT Core - FetchContent_Declare( - mqt-core - GIT_REPOSITORY https://github.com/${MQT_CORE_REPO_OWNER}/core.git - GIT_TAG ${MQT_CORE_REV}) - list(APPEND FETCH_PACKAGES mqt-core) -else() - find_package(mqt-core ${MQT_CORE_MINIMUM_VERSION} QUIET) - if(NOT mqt-core_FOUND) - FetchContent_Declare( - mqt-core - GIT_REPOSITORY https://github.com/${MQT_CORE_REPO_OWNER}/core.git - GIT_TAG ${MQT_CORE_REV}) - list(APPEND FETCH_PACKAGES mqt-core) - endif() -endif() +FetchContent_Declare( + mqt-core + GIT_REPOSITORY https://github.com/${MQT_CORE_REPO_OWNER}/core.git + GIT_TAG ${MQT_CORE_REV}) +list(APPEND FETCH_PACKAGES mqt-core) # ---------------------------------------------------------------------------------Fetch Eigen3 # cmake-format: off set(EIGEN_VERSION 3.4.0 CACHE STRING "Eigen3 version") # cmake-format: on -if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.24) - # Fetch Eigen3 - FetchContent_Declare( - Eigen3 - GIT_REPOSITORY https://gitlab.com/libeigen/eigen.git - GIT_TAG ${EIGEN_VERSION} - GIT_SHALLOW TRUE) - list(APPEND FETCH_PACKAGES Eigen3) - set(EIGEN_BUILD_TESTING - OFF - CACHE BOOL "Disable testing for Eigen") - set(BUILD_TESTING - OFF - CACHE BOOL "Disable general testing") - set(EIGEN_BUILD_DOC - OFF - CACHE BOOL "Disable documentation build for Eigen") -else() - find_package(Eigen3 ${EIGEN3_VERSION} REQUIRED NO_MODULE) - if(NOT Eigen3_FOUND) - FetchContent_Declare( - Eigen3 - GIT_REPOSITORY https://gitlab.com/libeigen/eigen.git - GIT_TAG ${EIGEN3_VERSION} - GIT_SHALLOW TRUE) - list(APPEND FETCH_PACKAGES Eigen3) - set(EIGEN_BUILD_TESTING - OFF - CACHE BOOL "Disable testing for Eigen") - set(BUILD_TESTING - OFF - CACHE BOOL "Disable general testing") - set(EIGEN_BUILD_DOC - OFF - CACHE BOOL "Disable documentation build for Eigen") - endif() -endif() +FetchContent_Declare( + Eigen3 + GIT_REPOSITORY https://gitlab.com/libeigen/eigen.git + GIT_TAG ${EIGEN_VERSION} + GIT_SHALLOW TRUE) +list(APPEND FETCH_PACKAGES Eigen3) +set(EIGEN_BUILD_TESTING + OFF + CACHE BOOL "Disable testing for Eigen") +set(BUILD_TESTING + OFF + CACHE BOOL "Disable general testing") +set(EIGEN_BUILD_DOC + OFF + CACHE BOOL "Disable documentation build for Eigen") if(BUILD_MQT_DEBUGGER_TESTS) set(gtest_force_shared_crt @@ -105,33 +63,8 @@ if(BUILD_MQT_DEBUGGER_TESTS) 1.17.0 CACHE STRING "Google Test version") set(GTEST_URL https://github.com/google/googletest/archive/refs/tags/v${GTEST_VERSION}.tar.gz) - if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.24) - FetchContent_Declare(googletest URL ${GTEST_URL} FIND_PACKAGE_ARGS ${GTEST_VERSION} NAMES GTest) - list(APPEND FETCH_PACKAGES googletest) - else() - find_package(googletest ${GTEST_VERSION} QUIET NAMES GTest) - if(NOT googletest_FOUND) - FetchContent_Declare(googletest URL ${GTEST_URL}) - list(APPEND FETCH_PACKAGES googletest) - endif() - endif() -endif() - -if(BUILD_MQT_DEBUGGER_BINDINGS) - # add pybind11_json library - if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.24) - FetchContent_Declare( - pybind11_json - GIT_REPOSITORY https://github.com/pybind/pybind11_json - FIND_PACKAGE_ARGS) - list(APPEND FETCH_PACKAGES pybind11_json) - else() - find_package(pybind11_json QUIET) - if(NOT pybind11_json_FOUND) - FetchContent_Declare(pybind11_json GIT_REPOSITORY https://github.com/pybind/pybind11_json) - list(APPEND FETCH_PACKAGES pybind11_json) - endif() - endif() + FetchContent_Declare(googletest URL ${GTEST_URL} FIND_PACKAGE_ARGS ${GTEST_VERSION} NAMES GTest) + list(APPEND FETCH_PACKAGES googletest) endif() # Make all declared dependencies available. diff --git a/include/python/InterfaceBindings.hpp b/include/python/InterfaceBindings.hpp deleted file mode 100644 index a8cf9b3..0000000 --- a/include/python/InterfaceBindings.hpp +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (c) 2024 - 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 - */ - -/** - * @file InterfaceBindings.hpp - * @brief This file defines methods to be used for defining Python bindings for - * the debugging and diagnostics backends. - */ - -#pragma once - -#include "pybind11/pybind11.h" - -/** - * @brief Binds the main debugging framework to Python. - * @param m The `pybind11` module. - */ -void bindFramework(pybind11::module& m); - -/** - * @brief Binds the diagnostics backend to Python. - * @param m The `pybind11` module. - */ -void bindDiagnostics(pybind11::module& m); diff --git a/include/python/dd/DDSimDebugBindings.hpp b/include/python/dd/DDSimDebugBindings.hpp deleted file mode 100644 index 0f7deb4..0000000 --- a/include/python/dd/DDSimDebugBindings.hpp +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (c) 2024 - 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 - */ - -/** - * @file DDSimDebugBindings.hpp - * @brief This file defines methods to be used for defining Python bindings for - * the DD Debugger. - */ - -#pragma once - -#include "pybind11/pybind11.h" - -/** - * @brief Binds the dd debugging backend to Python. - * @param m The `pybind11` module. - */ -void bindBackend(pybind11::module& m); diff --git a/noxfile.py b/noxfile.py index 57f3a49..8e183ec 100755 --- a/noxfile.py +++ b/noxfile.py @@ -209,5 +209,54 @@ def docs(session: nox.Session) -> None: ) +@nox.session(reuse_venv=True, venv_backend="uv") +def stubs(session: nox.Session) -> None: + """Generate type stubs for Python bindings using nanobind.""" + env = {"UV_PROJECT_ENVIRONMENT": session.virtualenv.location} + session.run( + "uv", + "sync", + "--no-dev", + "--group", + "build", + env=env, + ) + + package_root = Path(__file__).parent / "python" / "mqt" / "debugger" + pattern_file = Path(__file__).parent / "bindings" / "debugger_patterns.txt" + + session.run( + "python", + "-m", + "nanobind.stubgen", + "--recursive", + "--include-private", + "--output-dir", + str(package_root), + "--pattern-file", + str(pattern_file), + "--module", + "mqt.debugger.pydebugger", + ) + + pyi_files = list(package_root.glob("**/*.pyi")) + + if not pyi_files: + session.warn("No .pyi files found") + return + + if shutil.which("prek") is None: + session.install("prek") + + # Allow both 0 (no issues) and 1 as success codes for fixing up stubs + success_codes = [0, 1] + session.run("prek", "run", "license-tools", "--files", *pyi_files, external=True, success_codes=success_codes) + session.run("prek", "run", "ruff-check", "--files", *pyi_files, external=True, success_codes=success_codes) + session.run("prek", "run", "ruff-format", "--files", *pyi_files, external=True, success_codes=success_codes) + + # Run ruff-check again to ensure everything is clean + session.run("prek", "run", "ruff-check", "--files", *pyi_files, external=True) + + if __name__ == "__main__": nox.main() diff --git a/pyproject.toml b/pyproject.toml index 8500f77..5066f1b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ [build-system] requires = [ - "pybind11>=3.0.1", + "nanobind>=2.10.2", "setuptools-scm>=9.2.2", "scikit-build-core>=0.11.6", ] @@ -79,6 +79,10 @@ wheel.install-dir = "mqt/debugger" # Explicitly set the package directory wheel.packages = ["python/mqt"] +# Enable Stable ABI builds for CPython 3.12+ +wheel.py-api = "cp312" + +# Set required Ninja version ninja.version = ">=1.10" # Setuptools-style build caching in a local directory @@ -233,7 +237,7 @@ known-first-party = ["mqt.debugger"] "test/python/**" = ["T20", "ANN"] "docs/**" = ["T20"] "noxfile.py" = ["T20", "TID251"] -"*.pyi" = ["D418", "PYI021"] # pydocstyle +"*.pyi" = ["D418", "E501", "PYI021"] "*.ipynb" = [ "D", # pydocstyle "E402", # Allow imports to appear anywhere in Jupyter notebooks @@ -291,7 +295,6 @@ test-skip = [ "cp*-win_arm64", # no numpy, qiskit, ... wheels ] - [tool.cibuildwheel.linux] environment = { DEPLOY = "ON" } @@ -302,10 +305,15 @@ environment = { MACOSX_DEPLOYMENT_TARGET = "11.0" } before-build = "uv pip install delvewheel>=1.11.2" repair-wheel-command = "delvewheel repair -w {dest_dir} {wheel} --namespace-pkg mqt --ignore-existing" +[[tool.cibuildwheel.overrides]] +select = "cp312-*" +inherit.repair-wheel-command = "append" +repair-wheel-command = "uvx abi3audit --strict --report {wheel}" + [dependency-groups] build = [ - "pybind11>=3.0.1", + "nanobind>=2.10.2", "setuptools-scm>=9.2.2", "scikit-build-core>=0.11.6", ] diff --git a/python/mqt/debugger/dap/dap_server.py b/python/mqt/debugger/dap/dap_server.py index 27f99ea..b508fd2 100644 --- a/python/mqt/debugger/dap/dap_server.py +++ b/python/mqt/debugger/dap/dap_server.py @@ -385,9 +385,9 @@ def format_error_cause(self, cause: mqt.debugger.ErrorCause) -> str: start_line, _ = self.code_pos_to_coordinates(start_pos) return ( "The qubits never interact with each other. Are you missing a CX gate?" - if cause.type == mqt.debugger.ErrorCauseType.MissingInteraction + if cause.type_ == mqt.debugger.ErrorCauseType.MissingInteraction else f"Control qubit is always zero in line {start_line}." - if cause.type == mqt.debugger.ErrorCauseType.ControlAlwaysZero + if cause.type_ == mqt.debugger.ErrorCauseType.ControlAlwaysZero else "" ) diff --git a/python/mqt/debugger/dap/messages/change_bit_dap_message.py b/python/mqt/debugger/dap/messages/change_bit_dap_message.py index 30a69fc..fdd6315 100644 --- a/python/mqt/debugger/dap/messages/change_bit_dap_message.py +++ b/python/mqt/debugger/dap/messages/change_bit_dap_message.py @@ -133,7 +133,7 @@ def _apply_change(self, server: DAPServer, name: str) -> bool: msg = f"The variable '{name}' is not a classical bit." raise ValueError(msg) from exc - if variable.type != mqt.debugger.VariableType.VarBool: + if variable.type_ != mqt.debugger.VariableType.VarBool: msg = "Only boolean classical variables can be changed." raise ValueError(msg) diff --git a/python/mqt/debugger/pydebugger.pyi b/python/mqt/debugger/pydebugger.pyi index c4476b0..cfc4f0e 100644 --- a/python/mqt/debugger/pydebugger.pyi +++ b/python/mqt/debugger/pydebugger.pyi @@ -6,114 +6,300 @@ # # Licensed under the MIT License -"""Type stubs for python bindings of the debug module.""" - import enum +from collections.abc import Sequence +from typing import overload + +class ErrorCauseType(enum.Enum): + """The type of a potential error cause.""" + + Unknown = 0 + """An unknown error cause.""" + + MissingInteraction = 1 + """ + Indicates that an entanglement error may be caused by a missing interaction. + """ + + ControlAlwaysZero = 2 + """ + Indicates that an error may be related to a controlled gate with a control that is always zero. + """ + +class ErrorCause: + """Represents a potential cause of an assertion error.""" + + def __init__(self) -> None: ... + @property + def instruction(self) -> int: + """The instruction where the error may originate from or where the error can be detected.""" + + @instruction.setter + def instruction(self, arg: int, /) -> None: ... + @property + def type_(self) -> ErrorCauseType: + """The type of the potential error cause.""" + + @type_.setter + def type_(self, arg: ErrorCauseType, /) -> None: ... + +class Diagnostics: + """Provides diagnostics capabilities such as different analysis methods for the debugger.""" + + def __init__(self) -> None: + """Creates a new `Diagnostics` instance.""" + + def init(self) -> None: + """Initializes the diagnostics instance.""" + + def get_num_qubits(self) -> int: + """Get the number of qubits in the system. + + Returns: + The number of qubits in the system. + """ + + def get_instruction_count(self) -> int: + """Get the number of instructions in the code. + + Returns: + The number of instructions in the code. + """ + + def get_data_dependencies(self, instruction: int, include_callers: bool = False) -> list[int]: + """Extract all data dependencies for a given instruction. + + If the instruction is inside a custom gate definition, the data + dependencies will by default not go outside of the custom gate, unless a + new call instruction is found. By setting `include_callers` to `True`, all + possible callers of the custom gate will also be included and further + dependencies outside the custom gate will be taken from there. + + The line itself will also be counted as a dependency. Gate and register + declarations will not. + + This method can be performed without running the program, as it is a static + analysis method. + + Args: + instruction: The instruction to extract the data dependencies for. + include_callers: True, if the data dependencies should include all possible callers of the containing custom gate. Defaults to False. + + Returns: + A list of instruction indices that are data dependencies of the given instruction. + """ + + def get_interactions(self, before_instruction: int, qubit: int) -> list[int]: + """Extract all qubits that interact with a given qubit up to a specific instruction. + + If the target instruction is inside a custom gate definition, the + interactions will only be searched inside the custom gate, unless a new + call instruction is found. + + The qubit itself will also be counted as an interaction. + + This method can be performed without running the program, as it is a static + analysis method. + + Args: + before_instruction: The instruction to extract the interactions up to (excluding). + qubit: The qubit to extract the interactions for. + + Returns: + A list of qubit indices that interact with the given qubit up to the target instruction. + """ + + def get_zero_control_instructions(self) -> list[int]: + """Extract all controlled gates that have been marked as only having controls with value zero. + + This method expects a continuous memory block of booleans with size equal + to the number of instructions. Each element represents an instruction and + will be set to true if the instruction is a controlled gate with only zero + controls. + + This method can only be performed at runtime, as it is a dynamic analysis + method. + + Returns: + The indices of instructions that are controlled gates with only zero controls. + """ -# Enums + def potential_error_causes(self) -> list[ErrorCause]: + """Extract a list of potential error causes encountered during execution. + + This method should be run after the program has been executed and reached + an assertion error. + + Returns: + A list of potential error causes encountered during execution. + """ + + def suggest_assertion_movements(self) -> list[tuple[int, int]]: + """Suggest movements of assertions to better positions. + + Each entry of the resulting list consists of the original position of the assertion, followed by its new + suggested position. + + Returns: + A list of moved assertions. + """ + + def suggest_new_assertions(self) -> list[tuple[int, str]]: + """Suggest new assertions to be added to the program. + + Each entry of the resulting list consists of the suggested position for the new assertion, followed by its + string representation. + + Returns: + A list of new assertions. + """ class VariableType(enum.Enum): - """Represents possible types of classical variables.""" + """The type of a classical variable.""" VarBool = 0 """A boolean variable.""" + VarInt = 1 - """A 32-bit integer variable.""" + """An integer variable.""" + VarFloat = 2 """A floating-point variable.""" -class ErrorCauseType(enum.Enum): - """Represents the type of a potential error cause.""" - - Unknown = 0 - """An unknown error cause.""" - MissingInteraction = 1 - """Indicates that an entanglement error may be caused by a missing interaction.""" - ControlAlwaysZero = 2 - """Indicates that an error may be related to a controlled gate with a control that is always zero.""" - -# Classes - class VariableValue: """Represents the value of a classical variable. Only one of these fields has a valid value at a time, based on the variable's `VariableType`. """ - bool_value: bool - """The value of a boolean variable.""" - int_value: int - """The value of a 32-bit integer variable.""" - float_value: float - """The value of a floating-point variable.""" + def __init__(self) -> None: ... + @property + def bool_value(self) -> bool: + """The value of a boolean variable.""" - def __init__(self) -> None: - """Creates a new `VariableValue` instance.""" + @bool_value.setter + def bool_value(self, arg: bool, /) -> None: ... + @property + def int_value(self) -> int: + """The value of an integer variable.""" + + @int_value.setter + def int_value(self, arg: int, /) -> None: ... + @property + def float_value(self) -> float: + """The value of a floating-point variable.""" + + @float_value.setter + def float_value(self, arg: float, /) -> None: ... class Variable: """Represents a classical variable.""" - name: str - """The name of the variable.""" - type: VariableType - """The type of the variable.""" - value: VariableValue - """The value of the variable.""" - def __init__(self) -> None: """Creates a new `Variable` instance.""" + @property + def name(self) -> str: + """The name of the variable.""" + + @name.setter + def name(self, arg: str, /) -> None: ... + @property + def type_(self) -> VariableType: + """The type of the variable.""" + + @type_.setter + def type_(self, arg: VariableType, /) -> None: ... + @property + def value(self) -> VariableValue: + """The value of the variable.""" + + @value.setter + def value(self, arg: VariableValue, /) -> None: ... + class Complex: """Represents a complex number.""" - real: float - """The real part of the complex number.""" - imaginary: float - """The imaginary part of the complex number.""" + @overload + def __init__(self) -> None: + """Initializes a new complex number.""" + @overload def __init__(self, real: float = 0.0, imaginary: float = 0.0) -> None: """Initializes a new complex number. Args: - real (float, optional): The real part of the complex number. Defaults to 0.0. - imaginary (float, optional): The imaginary part of the complex number. Defaults to 0.0. + real: The real part of the complex number. Defaults to 0.0. + imaginary: The imaginary part of the complex number. Defaults to 0.0. """ + @property + def real(self) -> float: + """The real part of the complex number.""" + + @real.setter + def real(self, arg: float, /) -> None: ... + @property + def imaginary(self) -> float: + """The imaginary part of the complex number.""" + + @imaginary.setter + def imaginary(self, arg: float, /) -> None: ... + +class Statevector: + """Represents a state vector.""" + + def __init__(self) -> None: + """Creates a new `Statevector` instance.""" + + @property + def num_qubits(self) -> int: + """The number of qubits in the state vector.""" + + @num_qubits.setter + def num_qubits(self, arg: int, /) -> None: ... + @property + def num_states(self) -> int: + """The number of states in the state vector. + + This is always equal to 2^`num_qubits`. + """ + + @num_states.setter + def num_states(self, arg: int, /) -> None: ... + @property + def amplitudes(self) -> list[Complex]: + """The amplitudes of the state vector. + + Contains one element for each of the `num_states` states in the state vector. + """ + + @amplitudes.setter + def amplitudes(self, arg: Sequence[Complex], /) -> None: ... + class CompilationSettings: """The settings that should be used to compile an assertion program.""" - opt: int - """The optimization level that should be used. Exact meaning depends on the implementation, but typically 0 means no optimization.""" - slice_index: int - """The index of the slice that should be compiled.""" - def __init__(self, opt: int, slice_index: int = 0) -> None: """Initializes a new set of compilation settings. Args: - opt (int): The optimization level that should be used. - slice_index (int, optional): The index of the slice that should be compiled (defaults to 0). + opt: The optimization level that should be used. + slice_index: The index of the slice that should be compiled (defaults to 0). """ -class Statevector: - """Represents a state vector.""" - - num_qubits: int - """The number of qubits in the state vector.""" - num_states: int - """The number of states in the state vector. - - This is always equal to 2^`num_qubits`. - """ - - amplitudes: list[Complex] - """The amplitudes of the state vector. + @property + def opt(self) -> int: + """The optimization level that should be used. Exact meaning depends on the implementation, but typically 0 means no optimization.""" - Contains one element for each of the `num_states` states in the state vector. - """ + @opt.setter + def opt(self, arg: int, /) -> None: ... + @property + def slice_index(self) -> int: + """The index of the slice that should be compiled.""" - def __init__(self) -> None: - """Creates a new `Statevector` instance.""" + @slice_index.setter + def slice_index(self, arg: int, /) -> None: ... class SimulationState: """Represents the state of a quantum simulation for debugging. @@ -131,7 +317,7 @@ class SimulationState: """Loads the given code into the simulation state. Args: - code (str): The code to load. + code: The code to load. """ def step_forward(self) -> None: @@ -156,28 +342,7 @@ class SimulationState: """Runs the simulation until it finishes, even if assertions fail. Returns: - int: The number of assertions that failed during execution. - """ - - def change_classical_variable_value(self, variable_name: str, value: bool | float) -> None: - """Updates the value of a classical variable. - - Args: - variable_name (str): The name of the classical variable to update. - value (bool | float): The desired value. - """ - - def change_amplitude_value(self, basis_state: str, value: Complex) -> None: - """Updates the amplitude of a given computational basis state. - - The basis state is provided as a bitstring whose length matches the - current number of qubits. Implementations are expected to renormalize the - remaining amplitudes so that the state vector stays normalized and to - reject invalid bitstrings or amplitudes that violate normalization. - - Args: - basis_state (str): The bitstring identifying the basis state to update. - value (Complex): The desired complex amplitude. + The number of assertions that failed during execution. """ def run_simulation(self) -> None: @@ -215,7 +380,7 @@ class SimulationState: simulation has not been set up yet. Returns: - bool: True, if the simulation can step forward. + True, if the simulation can step forward. """ def can_step_backward(self) -> bool: @@ -225,7 +390,28 @@ class SimulationState: the simulation has not been set up yet. Returns: - bool: True, if the simulation can step backward. + True, if the simulation can step backward. + """ + + def change_classical_variable_value(self, variable_name: str, new_value: object) -> None: + """Updates the value of a classical variable. + + Args: + variable_name: The name of the classical variable to update. + new_value: The desired value. + """ + + def change_amplitude_value(self, basis_state: str, value: Complex) -> None: + """Updates the amplitude of a given computational basis state. + + The basis state is provided as a bitstring whose length matches the + current number of qubits. Implementations are expected to renormalize the + remaining amplitudes so that the state vector stays normalized and to + reject invalid bitstrings or amplitudes that violate normalization. + + Args: + basis_state: The bitstring identifying the basis state to update. + value: The desired complex amplitude. """ def is_finished(self) -> bool: @@ -234,7 +420,7 @@ class SimulationState: The execution is considered finished if it has reached the end of the code. Returns: - bool: True, if the execution has finished. + True, if the execution has finished. """ def did_assertion_fail(self) -> bool: @@ -244,7 +430,7 @@ class SimulationState: be set to false again. Returns: - bool: True, if an assertion failed during the last step. + True, if an assertion failed during the last step. """ def was_breakpoint_hit(self) -> bool: @@ -254,21 +440,21 @@ class SimulationState: be set to false again. Returns: - bool: True, if a breakpoint was hit during the last step. + True, if a breakpoint was hit during the last step. """ def get_current_instruction(self) -> int: """Gets the current instruction index. Returns: - int: The index of the current instruction. + The index of the current instruction. """ def get_instruction_count(self) -> int: """Gets the number of instructions in the code. Returns: - int: The number of instructions in the code. + The number of instructions in the code. """ def get_instruction_position(self, instruction: int) -> tuple[int, int]: @@ -277,17 +463,17 @@ class SimulationState: Start and end positions are inclusive and white-spaces are ignored. Args: - instruction (int): The instruction to find. + instruction: The instruction to find. Returns: - tuple[int, int]: The start and end positions of the instruction. + The start and end positions of the instruction. """ def get_num_qubits(self) -> int: """Gets the number of qubits used by the program. Returns: - int: The number of qubits used by the program. + The number of qubits used by the program. """ def get_amplitude_index(self, index: int) -> Complex: @@ -297,10 +483,10 @@ class SimulationState: binary representation of the state. Args: - index (int): The index of the state in the full state vector. + index: The index of the state in the full state vector. Returns: - Complex: The complex amplitude of the state. + The complex amplitude of the state. """ def get_amplitude_bitstring(self, bitstring: str) -> Complex: @@ -309,10 +495,10 @@ class SimulationState: The amplitude is selected by a bitstring representing the state. Args: - bitstring (str): The index of the state as a bitstring. + bitstring: The index of the state as a bitstring. Returns: - Complex: The complex amplitude of the state. + The complex amplitude of the state. """ def get_classical_variable(self, name: str) -> Variable: @@ -322,10 +508,10 @@ class SimulationState: in square brackets. Args: - name (str): The name of the variable. + name: The name of the variable. Returns: - Variable: The fetched variable. + The fetched variable. """ def get_num_classical_variables(self) -> int: @@ -334,7 +520,7 @@ class SimulationState: For registers, each index is counted as a separate variable. Returns: - int: The number of classical variables in the simulation. + The number of classical variables in the simulation. """ def get_classical_variable_name(self, index: int) -> str: @@ -345,36 +531,44 @@ class SimulationState: index of the register. Args: - index (int): The index of the variable. + index: The index of the variable. Returns: - str: The name of the variable. + The name of the variable. + """ + + def get_quantum_variable_name(self, index: int) -> str: + """Gets the name of a quantum variable by its index. + + For registers, each index is counted as a separate variable and can be + accessed separately. This method will return the name of the specific + index of the register. + + Args: + index: The index of the variable. + + Returns: + The name of the variable. """ def get_state_vector_full(self) -> Statevector: """Gets the full state vector of the simulation at the current time. - The state vector is expected to be initialized with the correct number of - qubits and allocated space for the amplitudes before calling this method. - Returns: - Statevector: The full state vector of the current simulation state. + The full state vector of the current simulation state. """ - def get_state_vector_sub(self, qubits: list[int]) -> Statevector: + def get_state_vector_sub(self, qubits: Sequence[int]) -> Statevector: """Gets a sub-state of the state vector of the simulation at the current time. - The state vector is expected to be initialized with the correct number of - qubits and allocated space for the amplitudes before calling this method. - This method also supports the re-ordering of qubits, but does not allow qubits to be repeated. Args: - qubits (list[int]): The qubits to include in the sub-state. + qubits: The qubits to include in the sub-state. Returns: - Statevector: The sub-state vector of the current simulation state. + The sub-state vector of the current simulation state. """ def set_breakpoint(self, desired_position: int) -> int: @@ -384,10 +578,10 @@ class SimulationState: string. Args: - desired_position (int): The position in the code to set the breakpoint. + desired_position: The position in the code to set the breakpoint. Returns: - int: The index of the instruction where the breakpoint was set. + The index of the instruction where the breakpoint was set. """ def clear_breakpoints(self) -> None: @@ -399,7 +593,7 @@ class SimulationState: Each custom gate call corresponds to one stack entry. Returns: - int: The current stack depth of the simulation. + The current stack depth of the simulation. """ def get_stack_trace(self, max_depth: int) -> list[int]: @@ -410,160 +604,39 @@ class SimulationState: stack entry. Args: - max_depth (int): The maximum depth of the stack trace. + max_depth: The maximum depth of the stack trace. Returns: - list[int]: The stack trace of the simulation. + The stack trace of the simulation. """ def get_diagnostics(self) -> Diagnostics: """Gets the diagnostics instance employed by this debugger. Returns: - Diagnostics: The diagnostics instance employed by this debugger. + The diagnostics instance employed by this debugger. """ def compile(self, settings: CompilationSettings) -> str: - """Compiles the program in the current state. - - Args: - settings (CompilationSettings): The settings to use for the compilation. - - Returns: - str: The compiled code. - """ - -class ErrorCause: - """Represents a potential cause of an assertion error.""" - - instruction: int - """The instruction where the error may originate from or where the error can be detected.""" - type: ErrorCauseType - """The type of the potential error cause.""" - - def __init__(self) -> None: - """Creates a new `ErrorCause` instance.""" - -class Diagnostics: - """Provides diagnostics capabilities such as different analysis methods for the debugger.""" - def __init__(self) -> None: - """Creates a new `Diagnostics` instance.""" - - def init(self) -> None: - """Initializes the diagnostics instance.""" - - def get_num_qubits(self) -> int: - """Get the number of qubits in the system. - - Returns: - int: The number of qubits in the system. - """ - - def get_instruction_count(self) -> int: - """Get the number of instructions in the code. - - Returns: - int: The number of instructions in the code. - """ - - def get_data_dependencies(self, instruction: int, include_callers: bool = False) -> list[int]: - """Extract all data dependencies for a given instruction. - - If the instruction is inside a custom gate definition, the data - dependencies will by default not go outside of the custom gate, unless a - new call instruction is found. By setting `include_callers` to `True`, all - possible callers of the custom gate will also be included and further - dependencies outside the custom gate will be taken from there. - - The line itself will also be counted as a dependency. Gate and register - declarations will not. - - This method can be performed without running the program, as it is a static - analysis method. + """Compiles the given code into a quantum circuit without assertions. Args: - instruction (int): The instruction to extract the data dependencies for. - include_callers (bool, optional): True, if the data dependencies should include all possible callers of the containing custom gate. Defaults to False. - - Returns: - list[int]: A list of instruction indices that are data dependencies of the given instruction. - """ - - def get_interactions(self, before_instruction: int, qubit: int) -> list[int]: - """Extract all qubits that interact with a given qubit up to a specific instruction. - - If the target instruction is inside a custom gate definition, the - interactions will only be searched inside the custom gate, unless a new - call instruction is found. - - The qubit itself will also be counted as an interaction. - - This method can be performed without running the program, as it is a static - analysis method. - - Args: - before_instruction (int): The instruction to extract the interactions up to (excluding). - qubit (int): The qubit to extract the interactions for. - - Returns: - list[int]: A list of qubit indices that interact with the given qubit up to the target instruction. - """ - - def get_zero_control_instructions(self) -> list[int]: - """Extract all controlled gates that have been marked as only having controls with value zero. - - This method expects a continuous memory block of booleans with size equal - to the number of instructions. Each element represents an instruction and - will be set to true if the instruction is a controlled gate with only zero - controls. - - This method can only be performed at runtime, as it is a dynamic analysis - method. - - Returns: - list[int]: The indices of instructions that are controlled gates with only zero controls. - """ - - def potential_error_causes(self) -> list[ErrorCause]: - """Extract a list of potential error causes encountered during execution. - - This method should be run after the program has been executed and reached - an assertion error. - - Returns: - list[ErrorCause]: A list of potential error causes encountered during execution. - """ - - def suggest_assertion_movements(self) -> tuple[int, int]: - """Suggest movements of assertions to better positions. - - Each entry of the resulting list consists of the original position of the assertion, followed by its new - suggested position. - - Returns: - list[tuple[int, int]]: A list of moved assertions. - """ - - def suggest_new_assertions(self) -> tuple[int, str]: - """Suggest new assertions to be added to the program. - - Each entry of the resulting list consists of the suggested position for the new assertion, followed by its - string representation. + settings: The settings to use for the compilation. Returns: - list[tupke[int, str]]: A list of new assertions. + The compiled code. """ def create_ddsim_simulation_state() -> SimulationState: """Creates a new `SimulationState` instance using the DD backend for simulation and the OpenQASM language as input format. Returns: - SimulationState: The created simulation state. + The created simulation state. """ def destroy_ddsim_simulation_state(state: SimulationState) -> None: """Delete a given DD-based `SimulationState` instance and free up resources. Args: - state (SimulationState): The simulation state to delete. + state: The simulation state to delete. """ diff --git a/test/python/test_diagnosis.py b/test/python/test_diagnosis.py index 122d411..2341b19 100644 --- a/test/python/test_diagnosis.py +++ b/test/python/test_diagnosis.py @@ -67,10 +67,10 @@ def test_control_always_zero() -> None: assert len(causes) == 2 - assert causes[0].type == ErrorCauseType.ControlAlwaysZero + assert causes[0].type_ == ErrorCauseType.ControlAlwaysZero assert causes[0].instruction == 3 - assert causes[1].type == ErrorCauseType.ControlAlwaysZero + assert causes[1].type_ == ErrorCauseType.ControlAlwaysZero assert causes[1].instruction == 12 @@ -78,10 +78,10 @@ def test_missing_interaction() -> None: """Test the missing-interaction error diagnosis.""" s = load_instance("missing-interaction") s.run_simulation() - causes = [x for x in s.get_diagnostics().potential_error_causes() if x.type == ErrorCauseType.MissingInteraction] + causes = [x for x in s.get_diagnostics().potential_error_causes() if x.type_ == ErrorCauseType.MissingInteraction] assert len(causes) == 0 s.run_simulation() - causes = [x for x in s.get_diagnostics().potential_error_causes() if x.type == ErrorCauseType.MissingInteraction] + causes = [x for x in s.get_diagnostics().potential_error_causes() if x.type_ == ErrorCauseType.MissingInteraction] assert len(causes) == 1 assert causes[0].instruction == 4 diff --git a/test/python/test_python_bindings.py b/test/python/test_python_bindings.py index 14dc7b5..d7f5a3f 100644 --- a/test/python/test_python_bindings.py +++ b/test/python/test_python_bindings.py @@ -322,9 +322,9 @@ def test_classical_get(simulation_instance_classical: SimulationInstance) -> Non assert simulation_state.get_classical_variable("c[1]").name == "c[1]" assert simulation_state.get_classical_variable("c[2]").name == "c[2]" - assert simulation_state.get_classical_variable("c[0]").type == mqt.debugger.VariableType.VarBool - assert simulation_state.get_classical_variable("c[1]").type == mqt.debugger.VariableType.VarBool - assert simulation_state.get_classical_variable("c[2]").type == mqt.debugger.VariableType.VarBool + assert simulation_state.get_classical_variable("c[0]").type_ == mqt.debugger.VariableType.VarBool + assert simulation_state.get_classical_variable("c[1]").type_ == mqt.debugger.VariableType.VarBool + assert simulation_state.get_classical_variable("c[2]").type_ == mqt.debugger.VariableType.VarBool first = simulation_state.get_classical_variable("c[0]").value.bool_value assert simulation_state.get_classical_variable("c[1]").value.bool_value == first diff --git a/uv.lock b/uv.lock index e97940a..dff7682 100644 --- a/uv.lock +++ b/uv.lock @@ -1449,13 +1449,13 @@ check = [ [package.dev-dependencies] build = [ - { name = "pybind11" }, + { name = "nanobind" }, { name = "scikit-build-core" }, { name = "setuptools-scm" }, ] dev = [ + { name = "nanobind" }, { name = "nox" }, - { name = "pybind11" }, { name = "pytest" }, { name = "pytest-console-scripts" }, { name = "pytest-cov" }, @@ -1503,13 +1503,13 @@ provides-extras = ["check"] [package.metadata.requires-dev] build = [ - { name = "pybind11", specifier = ">=3.0.1" }, + { name = "nanobind", specifier = ">=2.10.2" }, { name = "scikit-build-core", specifier = ">=0.11.6" }, { name = "setuptools-scm", specifier = ">=9.2.2" }, ] dev = [ + { name = "nanobind", specifier = ">=2.10.2" }, { name = "nox", specifier = ">=2025.11.12" }, - { name = "pybind11", specifier = ">=3.0.1" }, { name = "pytest", specifier = ">=9.0.1" }, { name = "pytest-console-scripts", specifier = ">=1.4.1" }, { name = "pytest-cov", specifier = ">=7.0.0" }, @@ -1585,6 +1585,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5f/df/76d0321c3797b54b60fef9ec3bd6f4cfd124b9e422182156a1dd418722cf/myst_parser-4.0.1-py3-none-any.whl", hash = "sha256:9134e88959ec3b5780aedf8a99680ea242869d012e8821db3126d427edc9c95d", size = 84579, upload-time = "2025-02-12T10:53:02.078Z" }, ] +[[package]] +name = "nanobind" +version = "2.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/7b/818fe4f6d1fdd516a14386ba86f2cbbac1b7304930da0f029724e9001658/nanobind-2.10.2.tar.gz", hash = "sha256:08509910ce6d1fadeed69cb0880d4d4fcb77739c6af9bd8fb4419391a3ca4c6b", size = 993651, upload-time = "2025-12-10T10:55:32.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/06/cb08965f985a5e1b9cb55ed96337c1f6daaa6b9cbdaeabe6bb3f7a1a11df/nanobind-2.10.2-py3-none-any.whl", hash = "sha256:6976c1b04b90481d2612b346485a3063818c6faa5077fe9d8bbc9b5fbe29c380", size = 246514, upload-time = "2025-12-10T10:55:30.741Z" }, +] + [[package]] name = "nbclient" version = "0.10.4" @@ -2100,15 +2109,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, ] -[[package]] -name = "pybind11" -version = "3.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2f/7b/a6d8dcb83c457e24a9df1e4d8fd5fb8034d4bbc62f3c324681e8a9ba57c2/pybind11-3.0.1.tar.gz", hash = "sha256:9c0f40056a016da59bab516efb523089139fcc6f2ba7e4930854c61efb932051", size = 546914, upload-time = "2025-08-22T20:09:27.265Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cd/8a/37362fc2b949d5f733a8b0f2ff51ba423914cabefe69f1d1b6aab710f5fe/pybind11-3.0.1-py3-none-any.whl", hash = "sha256:aa8f0aa6e0a94d3b64adfc38f560f33f15e589be2175e103c0a33c6bce55ee89", size = 293611, upload-time = "2025-08-22T20:09:25.235Z" }, -] - [[package]] name = "pybtex" version = "0.25.1"