Skip to content

Commit 5fab130

Browse files
authored
♻️ Auto-generate stub file (#773)
## Description After we have switched from `pybind11` to `nanobind` in #766, we can now auto-generate the stub file. This PR defines a corresponding `nox` session and copies over all existing docstrings to the bindings code. ## 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 37361a6 commit 5fab130

File tree

8 files changed

+353
-146
lines changed

8 files changed

+353
-146
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,8 +194,9 @@ jobs:
194194
if: fromJSON(needs.change-detection.outputs.run-python-tests)
195195
uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-python-linter.yml@d6314c45667c131055a0389afc110e8dedc6da3f # v1.17.11
196196
with:
197-
enable-ty: true
197+
check-stubs: true
198198
enable-mypy: false
199+
enable-ty: true
199200

200201
build-sdist:
201202
name: 🚀 CD

.license-tools-config.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
".*\\.profile",
3737
"uv\\.lock",
3838
"py\\.typed",
39-
".*build.*"
39+
".*build.*",
40+
"ddsim_patterns.txt"
4041
]
4142
}

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ This project adheres to [Semantic Versioning], with the exception that minor rel
1212
### Changed
1313

1414
- 🔧 Replace `mypy` with `ty` ([#770]) ([**@denialhaag**])
15-
- ♻️ Migrate Python bindings from `pybind11` to `nanobind` ([#766]) ([**@denialhaag**])
15+
- ♻️ Migrate Python bindings from `pybind11` to `nanobind` ([#766], [#773]) ([**@denialhaag**])
1616
- 📦️ Provide Stable ABI wheels for Python 3.12+ ([#766]) ([**@denialhaag**])
1717
- 👷 Stop testing on `ubuntu-22.04` and `ubuntu-22.04-arm` runners ([#740]) ([**@denialhaag**])
1818
- 👷 Stop testing with `clang-19` and start testing with `clang-21` ([#740]) ([**@denialhaag**])
@@ -89,6 +89,7 @@ _📚 Refer to the [GitHub Release Notes] for previous changelogs._
8989

9090
<!-- PR links -->
9191

92+
[#773]: https://github.com/munich-quantum-toolkit/ddsim/pull/773
9293
[#770]: https://github.com/munich-quantum-toolkit/ddsim/pull/770
9394
[#766]: https://github.com/munich-quantum-toolkit/ddsim/pull/766
9495
[#740]: https://github.com/munich-quantum-toolkit/ddsim/pull/740

bindings/bindings.cpp

Lines changed: 46 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
#include <nanobind/stl/list.h> // NOLINT(misc-include-cleaner)
2626
#include <nanobind/stl/map.h> // NOLINT(misc-include-cleaner)
2727
#include <nanobind/stl/optional.h> // NOLINT(misc-include-cleaner)
28+
#include <nanobind/stl/pair.h> // NOLINT(misc-include-cleaner)
2829
#include <nanobind/stl/string.h> // NOLINT(misc-include-cleaner)
2930
#include <nanobind/stl/vector.h> // NOLINT(misc-include-cleaner)
3031
#include <optional>
@@ -73,7 +74,6 @@ nb::class_<Sim> createSimulator(nb::module_ m, const std::string& name) {
7374
// NOLINTNEXTLINE(performance-unnecessary-value-param)
7475
NB_MODULE(MQT_DDSIM_MODULE_NAME, m) {
7576
nb::module_::import_("mqt.core.dd");
76-
m.doc() = "Python interface for the MQT DDSIM quantum circuit simulator";
7777

7878
// Circuit Simulator
7979
auto circuitSimulator =
@@ -100,7 +100,8 @@ NB_MODULE(MQT_DDSIM_MODULE_NAME, m) {
100100
"approximation_steps"_a = 1, "approximation_strategy"_a = "fidelity",
101101
"seed"_a = -1)
102102
.def("expectation_value", &CircuitSimulator::expectationValue,
103-
"observable"_a);
103+
"observable"_a,
104+
"Compute the expectation value for the given observable.");
104105

105106
// Stoch simulator
106107
auto stochasticNoiseSimulator =
@@ -166,7 +167,9 @@ NB_MODULE(MQT_DDSIM_MODULE_NAME, m) {
166167
"amp_damping_probability"_a = 0.02, "multi_qubit_gate_factor"_a = 2);
167168

168169
// Hybrid Schrödinger-Feynman Simulator
169-
nb::enum_<HybridSchrodingerFeynmanSimulator::Mode>(m, "HybridSimulatorMode")
170+
nb::enum_<HybridSchrodingerFeynmanSimulator::Mode>(
171+
m, "HybridSimulatorMode",
172+
R"pb(Enumeration of modes for the :class:`~HybridSimulator`.)pb")
170173
.value("DD", HybridSchrodingerFeynmanSimulator::Mode::DD)
171174
.value("amplitude", HybridSchrodingerFeynmanSimulator::Mode::Amplitude);
172175

@@ -199,12 +202,16 @@ NB_MODULE(MQT_DDSIM_MODULE_NAME, m) {
199202
"seed"_a = -1,
200203
"mode"_a = HybridSchrodingerFeynmanSimulator::Mode::Amplitude,
201204
"nthreads"_a = 2)
202-
.def("get_mode", &HybridSchrodingerFeynmanSimulator::getMode)
205+
.def("get_mode", &HybridSchrodingerFeynmanSimulator::getMode,
206+
"Get the mode of the hybrid simulator.")
203207
.def("get_final_amplitudes",
204-
&HybridSchrodingerFeynmanSimulator::getVectorFromHybridSimulation);
208+
&HybridSchrodingerFeynmanSimulator::getVectorFromHybridSimulation,
209+
"Get the final amplitudes from the hybrid simulation.");
205210

206211
// Path Simulator
207-
nb::enum_<PathSimulator::Configuration::Mode>(m, "PathSimulatorMode")
212+
nb::enum_<PathSimulator::Configuration::Mode>(
213+
m, "PathSimulatorMode",
214+
"Enumeration of modes for the :class:`~PathSimulator`.")
208215
.value("sequential", PathSimulator::Configuration::Mode::Sequential)
209216
.value("pairwise_recursive",
210217
PathSimulator::Configuration::Mode::PairwiseRecursiveGrouping)
@@ -214,21 +221,28 @@ NB_MODULE(MQT_DDSIM_MODULE_NAME, m) {
214221

215222
nb::class_<PathSimulator::Configuration>(
216223
m, "PathSimulatorConfiguration",
217-
"Configuration options for the :class:`~.PathSimulator`.")
224+
R"pb(Configuration options for the :class:`~.PathSimulator`.)pb")
218225
.def(nb::init())
219-
.def_rw(
220-
"mode", &PathSimulator::Configuration::mode,
221-
R"pbdoc(Setting the mode used for determining a simulation path)pbdoc")
226+
.def_rw("mode", &PathSimulator::Configuration::mode,
227+
R"pb(The mode used for determining a simulation path.)pb")
222228
.def_rw("bracket_size", &PathSimulator::Configuration::bracketSize,
223-
R"pbdoc(Size of the brackets one wants to combine)pbdoc")
229+
R"pb(Size of the brackets one wants to combine.)pb")
224230
.def_rw("starting_point", &PathSimulator::Configuration::startingPoint,
225-
R"pbdoc(Start of the alternating or gate_cost strategy)pbdoc")
231+
R"pb(Start of the alternating or gate_cost strategy.)pb")
226232
.def_rw(
227233
"gate_cost", &PathSimulator::Configuration::gateCost,
228-
R"pbdoc(A list that contains the number of gates which are considered in each step)pbdoc")
234+
R"pb(A list that contains the number of gates which are considered in each step.)pb")
229235
.def_rw("seed", &PathSimulator::Configuration::seed,
230-
R"pbdoc(Seed for the simulator)pbdoc")
231-
.def("json", &PathSimulator::Configuration::json)
236+
R"pb(Seed for the simulator.)pb")
237+
.def(
238+
"json",
239+
[](const PathSimulator::Configuration& config) {
240+
const auto json = nb::module_::import_("json");
241+
const auto loads = json.attr("loads");
242+
const auto dict = loads(config.json().dump());
243+
return nb::cast<nb::typed<nb::dict, nb::str, nb::any>>(dict);
244+
},
245+
"Get the configuration as a JSON-style dictionary.")
232246
.def("__repr__", &PathSimulator::Configuration::toString);
233247

234248
auto pathSimulator = createSimulator<PathSimulator>(m, "PathSimulator");
@@ -257,10 +271,17 @@ NB_MODULE(MQT_DDSIM_MODULE_NAME, m) {
257271
.def("set_simulation_path",
258272
nb::overload_cast<const PathSimulator::SimulationPath::Components&,
259273
bool>(&PathSimulator::setSimulationPath),
260-
"path"_a, "assume_correct_order"_a = false);
274+
"path"_a, "assume_correct_order"_a = false,
275+
R"pb(Set the simulation path.
276+
277+
Args:
278+
path: The components of the simulation path.
279+
assume_correct_order: Whether the provided path is assumed to be in the correct order. Defaults to False.)pb");
261280

262281
// Unitary Simulator
263-
nb::enum_<UnitarySimulator::Mode>(m, "UnitarySimulatorMode")
282+
nb::enum_<UnitarySimulator::Mode>(
283+
m, "UnitarySimulatorMode",
284+
R"pb(Enumeration of modes for the :class:`~UnitarySimulator`.)pb")
264285
.value("recursive", UnitarySimulator::Mode::Recursive)
265286
.value("sequential", UnitarySimulator::Mode::Sequential);
266287

@@ -288,8 +309,12 @@ NB_MODULE(MQT_DDSIM_MODULE_NAME, m) {
288309
"circ"_a, "approximation_step_fidelity"_a = 1.,
289310
"approximation_steps"_a = 1, "approximation_strategy"_a = "fidelity",
290311
"seed"_a = -1, "mode"_a = UnitarySimulator::Mode::Recursive)
291-
.def("get_mode", &UnitarySimulator::getMode)
292-
.def("get_construction_time", &UnitarySimulator::getConstructionTime)
293-
.def("get_final_node_count", &UnitarySimulator::getFinalNodeCount)
294-
.def("get_constructed_dd", &UnitarySimulator::getConstructedDD);
312+
.def("get_mode", &UnitarySimulator::getMode,
313+
"Get the mode of the unitary simulator.")
314+
.def("get_construction_time", &UnitarySimulator::getConstructionTime,
315+
"Get the time taken to construct the DD.")
316+
.def("get_final_node_count", &UnitarySimulator::getFinalNodeCount,
317+
"Get the final node count of the constructed DD.")
318+
.def("get_constructed_dd", &UnitarySimulator::getConstructedDD,
319+
"Get the constructed DD.");
295320
}

bindings/ddsim_patterns.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
_hashable_values_:
2+
3+
_unhashable_values_map_:

noxfile.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,5 +204,54 @@ def docs(session: nox.Session) -> None:
204204
)
205205

206206

207+
@nox.session(reuse_venv=True, venv_backend="uv")
208+
def stubs(session: nox.Session) -> None:
209+
"""Generate type stubs for Python bindings using nanobind."""
210+
env = {"UV_PROJECT_ENVIRONMENT": session.virtualenv.location}
211+
session.run(
212+
"uv",
213+
"sync",
214+
"--no-dev",
215+
"--group",
216+
"build",
217+
env=env,
218+
)
219+
220+
package_root = Path(__file__).parent / "python" / "mqt" / "ddsim"
221+
pattern_file = Path(__file__).parent / "bindings" / "ddsim_patterns.txt"
222+
223+
session.run(
224+
"python",
225+
"-m",
226+
"nanobind.stubgen",
227+
"--recursive",
228+
"--include-private",
229+
"--output-dir",
230+
package_root,
231+
"--pattern-file",
232+
pattern_file,
233+
"--module",
234+
"mqt.ddsim.pyddsim",
235+
)
236+
237+
pyi_files = list(package_root.glob("**/*.pyi"))
238+
239+
if not pyi_files:
240+
session.warn("No .pyi files found")
241+
return
242+
243+
if shutil.which("prek") is None:
244+
session.install("prek")
245+
246+
# Allow both 0 (no issues) and 1 as success codes for fixing up stubs
247+
success_codes = [0, 1]
248+
session.run("prek", "run", "license-tools", "--files", *pyi_files, external=True, success_codes=success_codes)
249+
session.run("prek", "run", "ruff-check", "--files", *pyi_files, external=True, success_codes=success_codes)
250+
session.run("prek", "run", "ruff-format", "--files", *pyi_files, external=True, success_codes=success_codes)
251+
252+
# Run ruff-check again to ensure everything is clean
253+
session.run("prek", "run", "ruff-check", "--files", *pyi_files, external=True)
254+
255+
207256
if __name__ == "__main__":
208257
nox.main()

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ known-first-party = ["mqt.ddsim"]
237237
"test/python/**" = ["T20", "ANN", "D10"]
238238
"docs/**" = ["T20"]
239239
"noxfile.py" = ["T20", "TID251"]
240-
"*.pyi" = ["D418", "PYI021"] # pydocstyle
240+
"*.pyi" = ["D418", "E501", "PYI021"]
241241
"*.ipynb" = [
242242
"D", # pydocstyle
243243
"E402", # Allow imports to appear anywhere in Jupyter notebooks

0 commit comments

Comments
 (0)