Skip to content

Commit c4bf4e8

Browse files
authored
✨ Make Python enums PEP-435 compatible (#607)
## Description This PR exposes enums to Python via `pybind11`'s new `enum.Enum`-compatible `py::native_enum`. Related to munich-quantum-toolkit/core#1075 ## 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 4977872 commit c4bf4e8

File tree

8 files changed

+48
-80
lines changed

8 files changed

+48
-80
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ _If you are upgrading: please see [`UPGRADING.md`](UPGRADING.md#Unreleased)._
1717

1818
### Changed
1919

20+
- ✨ Expose enums to Python via `pybind11`'s new (`enum.Enum`-compatible) `py::native_enum` ([#607]) ([**@denialhaag**])
2021
- **Breaking**: ⬆️ Bump minimum required `mqt-core` version to `3.1.0` ([#591]) ([**@denialhaag**])
2122
- **Breaking**: ⬆️ Bump minimum required `pybind11` version to `3.0.0` ([#591]) ([**@denialhaag**])
2223
- ♻️ Move the C++ code for the Python bindings to the top-level `bindings` directory ([#567]) ([**@denialhaag**])
@@ -49,6 +50,7 @@ _📚 Refer to the [GitHub Release Notes] for previous changelogs._
4950

5051
<!-- PR links -->
5152

53+
[#607]: https://github.com/munich-quantum-toolkit/ddsim/pull/607
5254
[#606]: https://github.com/munich-quantum-toolkit/ddsim/pull/606
5355
[#591]: https://github.com/munich-quantum-toolkit/ddsim/pull/591
5456
[#571]: https://github.com/munich-quantum-toolkit/ddsim/pull/571

UPGRADING.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ As a result, the return values of the `Estimator` and `Sampler` have been change
3838
To developers of MQT DDSIM, it is worth mentioning that all Python code (except tests) has been moved to the top-level `python` directory.
3939
Furthermore, the C++ code for the Python bindings has been moved to the top-level `bindings` directory.
4040

41+
The `ConstructionMode`, `HybridMode`, and `PathSimulatorMode` enums are now exposed via `pybind11`'s new `py::native_enum`, which makes them compatible with Python's `enum.Enum` class (PEP 435).
42+
As a result, the enums can no longer be initialized using a string.
43+
Instead of `PathSimulatorMode("sequential")` or `"sequential"`, use `PathSimulatorMode.sequential`.
44+
4145
<!-- Version links -->
4246

4347
[unreleased]: https://github.com/munich-quantum-toolkit/qmap/compare/v1.24.0...HEAD

bindings/bindings.cpp

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
#include <memory>
2323
#include <optional>
2424
#include <pybind11/cast.h>
25+
#include <pybind11/native_enum.h>
2526
#include <pybind11/numpy.h> // NOLINT(misc-include-cleaner)
2627
#include <pybind11/pybind11.h>
2728
#include <pybind11/stl.h> // NOLINT(misc-include-cleaner)
@@ -136,10 +137,12 @@ PYBIND11_MODULE(MQT_DDSIM_MODULE_NAME, m, py::mod_gil_not_used()) {
136137
"amp_damping_probability"_a = 0.02, "multi_qubit_gate_factor"_a = 2);
137138

138139
// Hybrid Schrödinger-Feynman Simulator
139-
py::enum_<HybridSchrodingerFeynmanSimulator::Mode>(m, "HybridMode")
140+
py::native_enum<HybridSchrodingerFeynmanSimulator::Mode>(
141+
m, "HybridMode", "enum.Enum",
142+
"Enumeration of modes for the HybridSchrodingerFeynmanSimulator.")
140143
.value("DD", HybridSchrodingerFeynmanSimulator::Mode::DD)
141144
.value("amplitude", HybridSchrodingerFeynmanSimulator::Mode::Amplitude)
142-
.export_values();
145+
.finalize();
143146

144147
auto hsfSimulator = createSimulator<HybridSchrodingerFeynmanSimulator>(
145148
m, "HybridCircuitSimulator");
@@ -158,18 +161,16 @@ PYBIND11_MODULE(MQT_DDSIM_MODULE_NAME, m, py::mod_gil_not_used()) {
158161
&HybridSchrodingerFeynmanSimulator::getVectorFromHybridSimulation);
159162

160163
// Path Simulator
161-
py::enum_<PathSimulator::Configuration::Mode>(m, "PathSimulatorMode")
164+
py::native_enum<PathSimulator::Configuration::Mode>(
165+
m, "PathSimulatorMode", "enum.Enum",
166+
"Enumeration of modes for the PathSimulator.")
162167
.value("sequential", PathSimulator::Configuration::Mode::Sequential)
163168
.value("pairwise_recursive",
164169
PathSimulator::Configuration::Mode::PairwiseRecursiveGrouping)
165170
.value("bracket", PathSimulator::Configuration::Mode::BracketGrouping)
166171
.value("alternating", PathSimulator::Configuration::Mode::Alternating)
167172
.value("gate_cost", PathSimulator::Configuration::Mode::GateCost)
168-
.export_values()
169-
.def(py::init(
170-
[](const std::string& str) -> PathSimulator::Configuration::Mode {
171-
return PathSimulator::Configuration::modeFromString(str);
172-
}));
173+
.finalize();
173174

174175
py::class_<PathSimulator::Configuration>(
175176
m, "PathSimulatorConfiguration",
@@ -211,10 +212,12 @@ PYBIND11_MODULE(MQT_DDSIM_MODULE_NAME, m, py::mod_gil_not_used()) {
211212
"path"_a, "assume_correct_order"_a = false);
212213

213214
// Unitary Simulator
214-
py::enum_<UnitarySimulator::Mode>(m, "ConstructionMode")
215+
py::native_enum<UnitarySimulator::Mode>(
216+
m, "ConstructionMode", "enum.Enum",
217+
"Enumeration of modes for the UnitarySimulator.")
215218
.value("recursive", UnitarySimulator::Mode::Recursive)
216219
.value("sequential", UnitarySimulator::Mode::Sequential)
217-
.export_values();
220+
.finalize();
218221

219222
auto unitarySimulator =
220223
createSimulator<UnitarySimulator>(m, "UnitarySimulator");

python/mqt/ddsim/hybridqasmsimulator.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ def _run_experiment(self, qc: QuantumCircuit, **options: Any) -> ExperimentResul
7676
seed = options.get("seed", -1)
7777
mode = options.get("mode", "amplitude")
7878
nthreads = int(options.get("nthreads", local_hardware_info()["cpus"]))
79+
7980
if mode == "amplitude":
8081
hybrid_mode = HybridMode.amplitude
8182
max_qubits = 30 # hard-coded 16GiB memory limit

python/mqt/ddsim/pathqasmsimulator.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from qiskit.transpiler import Target
2525

2626
from .header import DDSIMHeader
27-
from .pyddsim import PathCircuitSimulator, PathSimulatorConfiguration, PathSimulatorMode
27+
from .pyddsim import PathCircuitSimulator, PathSimulatorConfiguration
2828
from .qasmsimulator import QasmSimulatorBackend
2929
from .target import DDSIMTargetBuilder
3030

@@ -83,7 +83,7 @@ def _run_experiment(self, qc: QuantumCircuit, **options: Any) -> ExperimentResul
8383

8484
mode = options.get("mode")
8585
if mode is not None:
86-
pathsim_configuration.mode = PathSimulatorMode(mode)
86+
pathsim_configuration.mode = mode
8787

8888
bracket_size = options.get("bracket_size")
8989
if bracket_size is not None:

python/mqt/ddsim/pyddsim.pyi

Lines changed: 20 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
#
77
# Licensed under the MIT License
88

9-
from typing import Any, ClassVar, overload
9+
from enum import Enum
10+
from typing import Any, overload
1011

1112
from mqt.core.dd import MatrixDD, VectorDD
1213
from mqt.core.ir import QuantumComputation
@@ -67,25 +68,11 @@ class DeterministicNoiseSimulator:
6768
def simulate(self, shots: int) -> dict[str, int]: ...
6869
def statistics(self) -> dict[str, str]: ...
6970

70-
class HybridMode:
71-
DD: ClassVar[HybridMode] # value = <HybridMode.DD: 0>
72-
__members__: ClassVar[
73-
dict[str, HybridMode]
74-
] # value = {'DD': <HybridMode.DD: 0>, 'amplitude': <HybridMode.amplitude: 1>}
75-
amplitude: ClassVar[HybridMode] # value = <HybridMode.amplitude: 1>
76-
77-
def __eq__(self, other: object) -> bool: ...
78-
def __getstate__(self) -> int: ...
79-
def __hash__(self) -> int: ...
80-
def __index__(self) -> int: ...
81-
def __init__(self, value: int) -> None: ...
82-
def __int__(self) -> int: ...
83-
def __ne__(self, other: object) -> bool: ...
84-
def __setstate__(self, state: int) -> None: ...
85-
@property
86-
def name(self) -> str: ...
87-
@property
88-
def value(self) -> int: ...
71+
class HybridMode(Enum):
72+
"""Enumeration of modes for the :class:`~HybridCircuitSimulator`."""
73+
74+
DD = ...
75+
amplitude = ...
8976

9077
class HybridCircuitSimulator:
9178
def __init__(
@@ -110,31 +97,14 @@ class HybridCircuitSimulator:
11097
def simulate(self, shots: int) -> dict[str, int]: ...
11198
def statistics(self) -> dict[str, str]: ...
11299

113-
class PathSimulatorMode:
114-
__members__: ClassVar[
115-
dict[str, PathSimulatorMode]
116-
] # value = {'sequential': <PathSimulatorMode.sequential: 0>, 'pairwise_recursive': <PathSimulatorMode.pairwise_recursive: 1>, 'bracket': <PathSimulatorMode.bracket: 2>, 'alternating': <PathSimulatorMode.alternating: 3>, 'gate_cost': <PathSimulatorMode.gate_cost: 4>}
117-
alternating: ClassVar[PathSimulatorMode] # value = <PathSimulatorMode.alternating: 3>
118-
bracket: ClassVar[PathSimulatorMode] # value = <PathSimulatorMode.bracket: 2>
119-
gate_cost: ClassVar[PathSimulatorMode] # value = <PathSimulatorMode.gate_cost: 4>
120-
pairwise_recursive: ClassVar[PathSimulatorMode] # value = <PathSimulatorMode.pairwise_recursive: 1>
121-
sequential: ClassVar[PathSimulatorMode] # value = <PathSimulatorMode.sequential: 0>
122-
123-
def __eq__(self, other: object) -> bool: ...
124-
def __getstate__(self) -> int: ...
125-
def __hash__(self) -> int: ...
126-
def __index__(self) -> int: ...
127-
@overload
128-
def __init__(self, value: int) -> None: ...
129-
@overload
130-
def __init__(self, arg0: str) -> None: ...
131-
def __int__(self) -> int: ...
132-
def __ne__(self, other: object) -> bool: ...
133-
def __setstate__(self, state: int) -> None: ...
134-
@property
135-
def name(self) -> str: ...
136-
@property
137-
def value(self) -> int: ...
100+
class PathSimulatorMode(Enum):
101+
"""Enumeration of modes for the :class:`~PathCircuitSimulator`."""
102+
103+
alternating = ...
104+
bracket = ...
105+
gate_cost = ...
106+
pairwise_recursive = ...
107+
sequential = ...
138108

139109
class PathSimulatorConfiguration:
140110
def __init__(self) -> None: ...
@@ -207,25 +177,11 @@ class StochasticNoiseSimulator:
207177
def simulate(self, shots: int) -> dict[str, int]: ...
208178
def statistics(self) -> dict[str, str]: ...
209179

210-
class ConstructionMode:
211-
__members__: ClassVar[
212-
dict[str, ConstructionMode]
213-
] # value = {'recursive': <ConstructionMode.recursive: 1>, 'sequential': <ConstructionMode.sequential: 0>}
214-
recursive: ClassVar[ConstructionMode] # value = <ConstructionMode.recursive: 1>
215-
sequential: ClassVar[ConstructionMode] # value = <ConstructionMode.sequential: 0>
216-
217-
def __eq__(self, other: object) -> bool: ...
218-
def __getstate__(self) -> int: ...
219-
def __hash__(self) -> int: ...
220-
def __index__(self) -> int: ...
221-
def __init__(self, value: int) -> None: ...
222-
def __int__(self) -> int: ...
223-
def __ne__(self, other: object) -> bool: ...
224-
def __setstate__(self, state: int) -> None: ...
225-
@property
226-
def name(self) -> str: ...
227-
@property
228-
def value(self) -> int: ...
180+
class ConstructionMode(Enum):
181+
"""Enumeration of modes for the :class:`~UnitarySimulator`."""
182+
183+
recursive = ...
184+
sequential = ...
229185

230186
class UnitarySimulator:
231187
def __init__(

test/python/taskbasedsimulator/test_path_sim_qasm_simulator.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
from qiskit import QuantumCircuit
1414

15+
from mqt.ddsim import PathSimulatorMode
1516
from mqt.ddsim.pathqasmsimulator import PathQasmSimulatorBackend
1617

1718

@@ -79,7 +80,7 @@ def test_qasm_simulator_access(self) -> None:
7980
def test_qasm_simulator_pairwise(self) -> None:
8081
"""Test data counts output for single circuit run against reference."""
8182
shots = 8192
82-
result = self.backend.run(self.circuit, shots=shots, mode="pairwise_recursive").result()
83+
result = self.backend.run(self.circuit, shots=shots, mode=PathSimulatorMode.pairwise_recursive).result()
8384
threshold = 0.04 * shots
8485
counts = result.get_counts()
8586
target = {
@@ -101,7 +102,7 @@ def test_qasm_simulator_pairwise(self) -> None:
101102
def test_qasm_simulator_bracket(self) -> None:
102103
"""Test data counts output for single circuit run against reference."""
103104
shots = 8192
104-
result = self.backend.run(self.circuit, shots=shots, mode="bracket").result()
105+
result = self.backend.run(self.circuit, shots=shots, mode=PathSimulatorMode.bracket).result()
105106

106107
print(result)
107108
threshold = 0.04 * shots
@@ -125,7 +126,7 @@ def test_qasm_simulator_bracket(self) -> None:
125126
def test_qasm_simulator_alternating(self) -> None:
126127
"""Test data counts output for single circuit run against reference."""
127128
shots = 8192
128-
result = self.backend.run(self.circuit, shots=shots, mode="alternating").result()
129+
result = self.backend.run(self.circuit, shots=shots, mode=PathSimulatorMode.alternating).result()
129130

130131
print(result)
131132
threshold = 0.04 * shots

test/python/taskbasedsimulator/test_path_sim_statevector_simulator.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
from qiskit import QuantumCircuit, QuantumRegister
1515

16+
from mqt.ddsim import PathSimulatorMode
1617
from mqt.ddsim.pathstatevectorsimulator import PathStatevectorSimulatorBackend
1718

1819

@@ -38,7 +39,7 @@ def test_statevector_output(self) -> None:
3839

3940
def test_statevector_output_pairwise(self) -> None:
4041
"""Test final state vector for single circuit run."""
41-
mode = "pairwise_recursive"
42+
mode = PathSimulatorMode.pairwise_recursive
4243
result = self.backend.run(self.q_circuit, mode=mode).result()
4344
assert result.success
4445
actual = result.get_statevector()

0 commit comments

Comments
 (0)