Skip to content

Commit e9ad020

Browse files
authored
♻️ Replace pybind11 with nanobind (#817)
## Description This PR fully replaces `pybind11` with `nanobind`. This change will allow us to ship stable ABI wheels, saving us PyPI space. ## Checklist: - [x] The pull request only contains commits that are focused and relevant to this change. - [x] ~I have added appropriate tests that cover the new/changed functionality.~ - [x] I have updated the documentation to reflect these changes. - [x] I have added entries to the changelog for any noteworthy additions, changes, fixes, or removals. - [x] I have added migration instructions to the upgrade guide (if needed). - [x] The changes follow the project's style guidelines and introduce no new warnings. - [x] The changes are fully tested and pass the CI checks. - [x] I have reviewed my own code changes.
1 parent 6d2dcfd commit e9ad020

File tree

15 files changed

+262
-281
lines changed

15 files changed

+262
-281
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ jobs:
148148
clang-version: 20
149149
cmake-args: -DBUILD_MQT_QCEC_BINDINGS=ON
150150
files-changed-only: true
151-
install-pkgs: "pybind11==3.0.1"
151+
install-pkgs: "nanobind==2.10.2"
152152
setup-python: true
153153
cpp-linter-extra-args: "-std=c++20"
154154

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ This project adheres to [Semantic Versioning], with the exception that minor rel
1111

1212
### Changed
1313

14+
- ♻️ Migrate Python bindings from `pybind11` to `nanobind` ([#817]) ([**@denialhaag**])
15+
- 📦️ Provide Stable ABI wheels for Python 3.12+ ([#817]) ([**@denialhaag**])
16+
- ⬆️ Bump minimum required `mqt-core` version to `3.4.0` ([#817]) ([**@denialhaag**])
1417
- 👷 Stop testing on `ubuntu-22.04` and `ubuntu-22.04-arm` runners ([#796]) ([**@denialhaag**])
1518
- 👷 Stop testing with `clang-19` and start testing with `clang-21` ([#796]) ([**@denialhaag**])
1619
- 👷 Fix macOS tests with Homebrew Clang via new `munich-quantum-toolkit/workflows` version ([#796]) ([**@denialhaag**])
@@ -112,6 +115,7 @@ _📚 Refer to the [GitHub Release Notes] for previous changelogs._
112115

113116
<!-- PR links -->
114117

118+
[#817]: https://github.com/munich-quantum-toolkit/qcec/pull/817
115119
[#796]: https://github.com/munich-quantum-toolkit/qcec/pull/796
116120
[#735]: https://github.com/munich-quantum-toolkit/qcec/pull/735
117121
[#730]: https://github.com/munich-quantum-toolkit/qcec/pull/730

CMakeLists.txt

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
# Licensed under the MIT License
88

99
# set required cmake version
10-
cmake_minimum_required(VERSION 3.24...4.0)
10+
cmake_minimum_required(VERSION 3.24...4.2)
1111

1212
project(
1313
mqt-qcec
@@ -37,10 +37,8 @@ if(BUILD_MQT_QCEC_BINDINGS)
3737
endif()
3838

3939
# top-level call to find Python
40-
find_package(
41-
Python 3.10 REQUIRED
42-
COMPONENTS Interpreter Development.Module
43-
OPTIONAL_COMPONENTS Development.SABIModule)
40+
find_package(Python 3.10 REQUIRED COMPONENTS Interpreter Development.Module
41+
${SKBUILD_SABI_COMPONENT})
4442
endif()
4543

4644
# check if this is the master project or used via add_subdirectory

UPGRADING.md

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,24 @@ This document describes breaking changes and how to upgrade. For a complete list
44

55
## [Unreleased]
66

7-
### Removal of Python 3.13t wheels
7+
### Python wheels
88

9+
This release contains two changes to the distributed wheels.
10+
11+
First, we have removed all wheels for Python 3.13t.
912
Free-threading Python was introduced as an experimental feature in Python 3.13.
1013
It became stable in Python 3.14.
11-
To conserve space on PyPI and to reduce the CD build times, we have removed all wheels for Python 3.13t from our CI.
12-
We continue to provide wheels for the regular Python versions 3.10 to 3.14, as well as 3.14t.
14+
15+
Second, for Python 3.12+, we are now providing Stable ABI wheels instead of separate version-specific wheels.
16+
This was enabled by migrating our Python bindings from `pybind11` to `nanobind`.
17+
18+
Both of these changes were made in the interest of conserving PyPI space and reducing CI/CD build times.
19+
The full list of wheels now reads:
20+
21+
- 3.10
22+
- 3.11
23+
- 3.12+ Stable ABI
24+
- 3.14t
1325

1426
## [3.3.0]
1527

bindings/CMakeLists.txt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ list(
2727

2828
file(GLOB_RECURSE QCEC_SOURCES **.cpp)
2929

30-
add_mqt_python_binding(
30+
add_mqt_python_binding_nanobind(
3131
QCEC
3232
${MQT_QCEC_TARGET_NAME}-bindings
3333
${QCEC_SOURCES}
@@ -36,8 +36,7 @@ add_mqt_python_binding(
3636
INSTALL_DIR
3737
.
3838
LINK_LIBS
39-
MQT::QCEC
40-
pybind11_json)
39+
MQT::QCEC)
4140

4241
# install the Python stub files in editable mode for better IDE support
4342
if(SKBUILD_STATE STREQUAL "editable")

bindings/application_scheme.cpp

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,16 @@
1010

1111
#include "checker/dd/applicationscheme/ApplicationScheme.hpp"
1212

13-
#include <pybind11/native_enum.h>
14-
#include <pybind11/pybind11.h>
15-
#include <pybind11/stl.h> // NOLINT(misc-include-cleaner)
13+
#include <nanobind/nanobind.h>
1614

1715
namespace ec {
1816

19-
namespace py = pybind11;
20-
using namespace pybind11::literals;
17+
namespace nb = nanobind;
18+
using namespace nb::literals;
2119

2220
// NOLINTNEXTLINE(misc-use-internal-linkage)
23-
void registerApplicationSchema(const py::module& mod) {
24-
py::native_enum<ApplicationSchemeType>(
25-
mod, "ApplicationScheme", "enum.Enum",
26-
"Enumeration describing the application order of operations.")
21+
void registerApplicationSchema(const nb::module_& m) {
22+
nb::enum_<ApplicationSchemeType>(m, "ApplicationScheme", nb::is_arithmetic())
2723
.value("sequential", ApplicationSchemeType::Sequential)
2824
.value("reference", ApplicationSchemeType::Sequential)
2925
.value("one_to_one", ApplicationSchemeType::OneToOne)
@@ -37,8 +33,7 @@ void registerApplicationSchema(const py::module& mod) {
3733
"second circuit. Referred to as *compilation_flow* in "
3834
":cite:p:`burgholzer2020verifyingResultsIBM`.")
3935
.value("compilation_flow", ApplicationSchemeType::GateCost)
40-
.value("proportional", ApplicationSchemeType::Proportional)
41-
.finalize();
36+
.value("proportional", ApplicationSchemeType::Proportional);
4237
}
4338

4439
} // namespace ec

bindings/bindings.cpp

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,27 @@
88
* Licensed under the MIT License
99
*/
1010

11-
#include <pybind11/pybind11.h>
12-
#include <pybind11/stl.h> // NOLINT(misc-include-cleaner)
11+
#include <nanobind/nanobind.h>
1312

1413
namespace ec {
1514

16-
namespace py = pybind11;
17-
using namespace pybind11::literals;
15+
namespace nb = nanobind;
16+
using namespace nb::literals;
1817

1918
// forward declarations
20-
void registerApplicationSchema(const py::module& mod);
21-
void registerConfiguration(const py::module& mod);
22-
void registerEquivalenceCheckingManager(const py::module& mod);
23-
void registerEquivalenceCriterion(const py::module& mod);
24-
void registerStateType(const py::module& mod);
19+
void registerApplicationSchema(const nb::module_& m);
20+
void registerConfiguration(const nb::module_& m);
21+
void registerEquivalenceCheckingManager(const nb::module_& m);
22+
void registerEquivalenceCriterion(const nb::module_& m);
23+
void registerStateType(const nb::module_& m);
2524

26-
// NOLINTNEXTLINE(misc-include-cleaner)
27-
PYBIND11_MODULE(MQT_QCEC_MODULE_NAME, mod, py::mod_gil_not_used()) {
28-
registerApplicationSchema(mod);
29-
registerConfiguration(mod);
30-
registerEquivalenceCheckingManager(mod);
31-
registerEquivalenceCriterion(mod);
32-
registerStateType(mod);
25+
// NOLINTNEXTLINE(performance-unnecessary-value-param)
26+
NB_MODULE(MQT_QCEC_MODULE_NAME, m) {
27+
registerApplicationSchema(m);
28+
registerConfiguration(m);
29+
registerEquivalenceCheckingManager(m);
30+
registerEquivalenceCriterion(m);
31+
registerStateType(m);
3332
}
3433

3534
} // namespace ec

bindings/configuration.cpp

Lines changed: 78 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -10,109 +10,111 @@
1010

1111
#include "Configuration.hpp"
1212

13-
#include <pybind11/pybind11.h>
14-
#include <pybind11/stl.h> // NOLINT(misc-include-cleaner)
13+
#include <nanobind/nanobind.h>
14+
#include <nanobind/stl/string.h> // NOLINT(misc-include-cleaner)
1515

1616
namespace ec {
1717

18-
namespace py = pybind11;
19-
using namespace pybind11::literals;
18+
namespace nb = nanobind;
19+
using namespace nb::literals;
2020

2121
// NOLINTNEXTLINE(misc-use-internal-linkage)
22-
void registerConfiguration(const py::module& mod) {
22+
void registerConfiguration(const nb::module_& m) {
2323
// Class definitions
24-
auto configuration = py::class_<Configuration>(mod, "Configuration");
24+
auto configuration = nb::class_<Configuration>(m, "Configuration");
2525
auto execution =
26-
py::class_<Configuration::Execution>(configuration, "Execution");
26+
nb::class_<Configuration::Execution>(configuration, "Execution");
2727
auto optimizations =
28-
py::class_<Configuration::Optimizations>(configuration, "Optimizations");
28+
nb::class_<Configuration::Optimizations>(configuration, "Optimizations");
2929
auto application =
30-
py::class_<Configuration::Application>(configuration, "Application");
30+
nb::class_<Configuration::Application>(configuration, "Application");
3131
auto functionality =
32-
py::class_<Configuration::Functionality>(configuration, "Functionality");
32+
nb::class_<Configuration::Functionality>(configuration, "Functionality");
3333
auto simulation =
34-
py::class_<Configuration::Simulation>(configuration, "Simulation");
34+
nb::class_<Configuration::Simulation>(configuration, "Simulation");
3535
auto parameterized =
36-
py::class_<Configuration::Parameterized>(configuration, "Parameterized");
36+
nb::class_<Configuration::Parameterized>(configuration, "Parameterized");
3737

3838
// Configuration
39-
configuration.def(py::init<>())
40-
.def_readwrite("execution", &Configuration::execution)
41-
.def_readwrite("optimizations", &Configuration::optimizations)
42-
.def_readwrite("application", &Configuration::application)
43-
.def_readwrite("functionality", &Configuration::functionality)
44-
.def_readwrite("simulation", &Configuration::simulation)
45-
.def_readwrite("parameterized", &Configuration::parameterized)
46-
.def("json", &Configuration::json)
39+
configuration.def(nb::init<>())
40+
.def_rw("execution", &Configuration::execution)
41+
.def_rw("optimizations", &Configuration::optimizations)
42+
.def_rw("application", &Configuration::application)
43+
.def_rw("functionality", &Configuration::functionality)
44+
.def_rw("simulation", &Configuration::simulation)
45+
.def_rw("parameterized", &Configuration::parameterized)
46+
.def("json",
47+
[](const Configuration& config) {
48+
const nb::module_ json = nb::module_::import_("json");
49+
const nb::object loads = json.attr("loads");
50+
return loads(config.json().dump());
51+
})
4752
.def("__repr__", &Configuration::toString);
4853

4954
// execution options
50-
execution.def(py::init<>())
51-
.def_readwrite("parallel", &Configuration::Execution::parallel)
52-
.def_readwrite("nthreads", &Configuration::Execution::nthreads)
53-
.def_readwrite("timeout", &Configuration::Execution::timeout)
54-
.def_readwrite("run_construction_checker",
55-
&Configuration::Execution::runConstructionChecker)
56-
.def_readwrite("run_simulation_checker",
57-
&Configuration::Execution::runSimulationChecker)
58-
.def_readwrite("run_alternating_checker",
59-
&Configuration::Execution::runAlternatingChecker)
60-
.def_readwrite("run_zx_checker", &Configuration::Execution::runZXChecker)
61-
.def_readwrite("numerical_tolerance",
62-
&Configuration::Execution::numericalTolerance)
63-
.def_readwrite("set_all_ancillae_garbage",
64-
&Configuration::Execution::setAllAncillaeGarbage);
55+
execution.def(nb::init<>())
56+
.def_rw("parallel", &Configuration::Execution::parallel)
57+
.def_rw("nthreads", &Configuration::Execution::nthreads)
58+
.def_rw("timeout", &Configuration::Execution::timeout)
59+
.def_rw("run_construction_checker",
60+
&Configuration::Execution::runConstructionChecker)
61+
.def_rw("run_simulation_checker",
62+
&Configuration::Execution::runSimulationChecker)
63+
.def_rw("run_alternating_checker",
64+
&Configuration::Execution::runAlternatingChecker)
65+
.def_rw("run_zx_checker", &Configuration::Execution::runZXChecker)
66+
.def_rw("numerical_tolerance",
67+
&Configuration::Execution::numericalTolerance)
68+
.def_rw("set_all_ancillae_garbage",
69+
&Configuration::Execution::setAllAncillaeGarbage);
6570

6671
// optimization options
67-
optimizations.def(py::init<>())
68-
.def_readwrite("fuse_single_qubit_gates",
69-
&Configuration::Optimizations::fuseSingleQubitGates)
70-
.def_readwrite("reconstruct_swaps",
71-
&Configuration::Optimizations::reconstructSWAPs)
72-
.def_readwrite(
73-
"remove_diagonal_gates_before_measure",
74-
&Configuration::Optimizations::removeDiagonalGatesBeforeMeasure)
75-
.def_readwrite("transform_dynamic_circuit",
76-
&Configuration::Optimizations::transformDynamicCircuit)
77-
.def_readwrite("reorder_operations",
78-
&Configuration::Optimizations::reorderOperations)
79-
.def_readwrite(
80-
"backpropagate_output_permutation",
81-
&Configuration::Optimizations::backpropagateOutputPermutation)
82-
.def_readwrite("elide_permutations",
83-
&Configuration::Optimizations::elidePermutations);
72+
optimizations.def(nb::init<>())
73+
.def_rw("fuse_single_qubit_gates",
74+
&Configuration::Optimizations::fuseSingleQubitGates)
75+
.def_rw("reconstruct_swaps",
76+
&Configuration::Optimizations::reconstructSWAPs)
77+
.def_rw("remove_diagonal_gates_before_measure",
78+
&Configuration::Optimizations::removeDiagonalGatesBeforeMeasure)
79+
.def_rw("transform_dynamic_circuit",
80+
&Configuration::Optimizations::transformDynamicCircuit)
81+
.def_rw("reorder_operations",
82+
&Configuration::Optimizations::reorderOperations)
83+
.def_rw("backpropagate_output_permutation",
84+
&Configuration::Optimizations::backpropagateOutputPermutation)
85+
.def_rw("elide_permutations",
86+
&Configuration::Optimizations::elidePermutations);
8487

8588
// application options
86-
application.def(py::init<>())
87-
.def_readwrite("construction_scheme",
88-
&Configuration::Application::constructionScheme)
89-
.def_readwrite("simulation_scheme",
90-
&Configuration::Application::simulationScheme)
91-
.def_readwrite("alternating_scheme",
92-
&Configuration::Application::alternatingScheme)
93-
.def_readwrite("profile", &Configuration::Application::profile);
89+
application.def(nb::init<>())
90+
.def_rw("construction_scheme",
91+
&Configuration::Application::constructionScheme)
92+
.def_rw("simulation_scheme",
93+
&Configuration::Application::simulationScheme)
94+
.def_rw("alternating_scheme",
95+
&Configuration::Application::alternatingScheme)
96+
.def_rw("profile", &Configuration::Application::profile);
9497

9598
// functionality options
96-
functionality.def(py::init<>())
97-
.def_readwrite("trace_threshold",
98-
&Configuration::Functionality::traceThreshold)
99-
.def_readwrite("check_partial_equivalence",
100-
&Configuration::Functionality::checkPartialEquivalence);
99+
functionality.def(nb::init<>())
100+
.def_rw("trace_threshold", &Configuration::Functionality::traceThreshold)
101+
.def_rw("check_partial_equivalence",
102+
&Configuration::Functionality::checkPartialEquivalence);
101103

102104
// simulation options
103-
simulation.def(py::init<>())
104-
.def_readwrite("fidelity_threshold",
105-
&Configuration::Simulation::fidelityThreshold)
106-
.def_readwrite("max_sims", &Configuration::Simulation::maxSims)
107-
.def_readwrite("state_type", &Configuration::Simulation::stateType)
108-
.def_readwrite("seed", &Configuration::Simulation::seed);
105+
simulation.def(nb::init<>())
106+
.def_rw("fidelity_threshold",
107+
&Configuration::Simulation::fidelityThreshold)
108+
.def_rw("max_sims", &Configuration::Simulation::maxSims)
109+
.def_rw("state_type", &Configuration::Simulation::stateType)
110+
.def_rw("seed", &Configuration::Simulation::seed);
109111

110112
// parameterized options
111-
parameterized.def(py::init<>())
112-
.def_readwrite("parameterized_tolerance",
113-
&Configuration::Parameterized::parameterizedTol)
114-
.def_readwrite("additional_instantiations",
115-
&Configuration::Parameterized::nAdditionalInstantiations);
113+
parameterized.def(nb::init<>())
114+
.def_rw("parameterized_tolerance",
115+
&Configuration::Parameterized::parameterizedTol)
116+
.def_rw("additional_instantiations",
117+
&Configuration::Parameterized::nAdditionalInstantiations);
116118
}
117119

118120
} // namespace ec

0 commit comments

Comments
 (0)