/**
 * @page python_guide Python Guide
 *
 * @tableofcontents
 *
 * @section py_overview Overview
 *
 * Maestro ships high-performance Python bindings built with
 * [nanobind](https://github.com/wjakob/nanobind). The bindings expose
 * the full simulation pipeline — circuit construction, backend selection,
 * execution, and expectation-value estimation — in a Pythonic API.
 *
 * @subsection py_install Installation
 *
 * @code{.sh}
 * # From the repository root
 * pip install .
 * @endcode
 *
 * ---
 *
 * @section py_quick Quick Start — One-Line Execution
 *
 * The fastest way to run a circuit is `maestro.simple_execute`.
 * Pass an OpenQASM 2.0 string and get measurement counts back immediately.
 *
 * @code{.py}
 * import maestro
 *
 * qasm = """
 * OPENQASM 2.0;
 * include "qelib1.inc";
 * qreg q[2];
 * creg c[2];
 * h q[0];
 * cx q[0], q[1];
 * measure q -> c;
 * """
 *
 * result = maestro.simple_execute(qasm, shots=1024)
 *
 * print(result["counts"])      # e.g. {"00": 512, "11": 512}
 * print(result["simulator"])   # e.g. "QCSim"
 * print(result["method"])      # e.g. "Statevector"
 * print(f"{result['time_taken']:.4f}s")
 * @endcode
 *
 * **What's in the result dict?**
 *
 * | Key | Type | Description |
 * |-----|------|-------------|
 * | `counts` | `dict[str, int]` | Measurement outcome → count |
 * | `simulator` | `int` | Backend enum value (see `SimulatorType`) |
 * | `method` | `int` | Simulation method enum value (see `SimulationType`) |
 * | `time_taken` | `float` | Wall-clock time in seconds |
 *
 * ---
 *
 * @section py_backends Choosing a Simulation Backend
 *
 * You can explicitly select the simulator type and simulation method.
 * Maestro supports several backends; the most common CPU options are shown here.
 *
 * @subsection py_statevector Statevector Simulation
 *
 * @code{.py}
 * result = maestro.simple_execute(
 *     qasm,
 *     simulator_type=maestro.SimulatorType.QCSim,
 *     simulation_type=maestro.SimulationType.Statevector,
 *     shots=2000
 * )
 * @endcode
 *
 * @subsection py_mps Matrix Product State (MPS)
 *
 * MPS enables simulation of circuits with hundreds of qubits
 * when entanglement is bounded.
 *
 * @code{.py}
 * result = maestro.simple_execute(
 *     qasm,
 *     simulator_type=maestro.SimulatorType.QCSim,
 *     simulation_type=maestro.SimulationType.MatrixProductState,
 *     max_bond_dimension=64,
 *     singular_value_threshold=1e-10,
 *     shots=1000
 * )
 *
 * print(result["method"])  # "MatrixProductState"
 * @endcode
 *
 * @subsection py_stabilizer Stabilizer (Clifford-only)
 *
 * Ultra-fast simulation for circuits that only use Clifford gates
 * (H, S, CX, X, Y, Z).
 *
 * @code{.py}
 * clifford_qasm = """
 * OPENQASM 2.0;
 * include "qelib1.inc";
 * qreg q[100];
 * creg c[100];
 * h q[0];
 * cx q[0], q[1];
 * cx q[1], q[2];
 * measure q -> c;
 * """
 *
 * result = maestro.simple_execute(
 *     clifford_qasm,
 *     simulator_type=maestro.SimulatorType.QCSim,
 *     simulation_type=maestro.SimulationType.Stabilizer,
 *     shots=10000
 * )
 * @endcode
 *
 * @subsection py_backend_table Available Backends
 *
 * The table below shows which simulation types are supported by each backend.
 *
 * | SimulationType | QCSim | Qiskit Aer | GPU | QuEST |
 * |----------------|-------|------------|-----|-------|
 * | `Statevector` | ✅ | ✅ | ✅ | ✅ |
 * | `MatrixProductState` | ✅ | ✅ | ✅ | ❌ |
 * | `Stabilizer` | ✅ | ✅ | ❌ | ❌ |
 * | `TensorNetwork` | ✅ | ✅ | ✅ | ❌ |
 * | `PauliPropagator` | ✅ | ❌ | ✅ | ❌ |
 * | `ExtendedStabilizer` | ❌ | ✅ | ❌ | ❌ |
 *
 * **Distributed execution:** QCSim and Qiskit Aer support p-block composite
 * simulation via `CompositeQCSim` / `CompositeQiskitAer`. QuEST natively
 * supports MPI-distributed statevector simulation.
 *
 * @note Qiskit Aer support is optional and requires building with `AER_INCLUDE_DIR`.
 * GPU and QuEST are dynamically-loaded libraries — see the dedicated sections below.
 *
 * ---
 *
 * @section py_expectation Expectation Values
 *
 * Use `maestro.simple_estimate` to compute expectation values of
 * Pauli observables without measurements in the circuit.
 *
 * @code{.py}
 * import maestro
 *
 * # Prepare a GHZ state
 * ghz = """
 * OPENQASM 2.0;
 * include "qelib1.inc";
 * qreg q[3];
 * h q[0];
 * cx q[0], q[1];
 * cx q[1], q[2];
 * """
 *
 * # Estimate multiple Pauli observables at once (semicolon-separated)
 * result = maestro.simple_estimate(ghz, "ZZZ;XXX;IZI")
 *
 * obs = result["expectation_values"]
 * print(f"<ZZZ> = {obs[0]:.4f}")  # 1.0 (parity conserved)
 * print(f"<XXX> = {obs[1]:.4f}")  # 1.0
 * print(f"<IZI> = {obs[2]:.4f}")  # 0.0 (maximally mixed marginal)
 * @endcode
 *
 * You can also use a specific backend:
 *
 * @code{.py}
 * result = maestro.simple_estimate(
 *     ghz, "ZZ;XX",
 *     simulator_type=maestro.SimulatorType.QCSim,
 *     simulation_type=maestro.SimulationType.MatrixProductState,
 *     max_bond_dimension=8
 * )
 * @endcode
 *
 * ---
 *
 * @section py_circuit QuantumCircuit Builder
 *
 * For a more Pythonic workflow (similar to Qiskit), use the
 * `QuantumCircuit` class to build circuits programmatically
 * without writing QASM strings.
 *
 * @subsection py_circuit_basic Building and Running a Circuit
 *
 * @code{.py}
 * from maestro.circuits import QuantumCircuit
 *
 * qc = QuantumCircuit()
 *
 * # Build a Bell state
 * qc.h(0)
 * qc.cx(0, 1)
 * qc.measure([(0, 0), (1, 1)])  # (qubit, classical_bit) pairs
 *
 * # Execute with defaults
 * result = qc.execute(shots=1000)
 * print(result["counts"])  # {"00": ~500, "11": ~500}
 * @endcode
 *
 * @subsection py_circuit_gates Available Gates
 *
 * | Gate | Method | Parameters |
 * |------|--------|------------|
 * | Pauli-X | `qc.x(qubit)` | — |
 * | Pauli-Y | `qc.y(qubit)` | — |
 * | Pauli-Z | `qc.z(qubit)` | — |
 * | Hadamard | `qc.h(qubit)` | — |
 * | S | `qc.s(qubit)` | — |
 * | S† | `qc.sdg(qubit)` | — |
 * | T | `qc.t(qubit)` | — |
 * | T† | `qc.tdg(qubit)` | — |
 * | √X | `qc.sx(qubit)` | — |
 * | √X† | `qc.sxdg(qubit)` | — |
 * | K | `qc.k(qubit)` | — |
 * | Phase | `qc.p(qubit, λ)` | λ (radians) |
 * | Rx | `qc.rx(qubit, θ)` | θ (radians) |
 * | Ry | `qc.ry(qubit, θ)` | θ (radians) |
 * | Rz | `qc.rz(qubit, θ)` | θ (radians) |
 * | U | `qc.u(qubit, θ, φ, λ)` | 3 angles |
 * | CNOT | `qc.cx(ctrl, tgt)` | — |
 * | CY | `qc.cy(ctrl, tgt)` | — |
 * | CZ | `qc.cz(ctrl, tgt)` | — |
 * | CH | `qc.ch(ctrl, tgt)` | — |
 * | CSX | `qc.csx(ctrl, tgt)` | — |
 * | CSX† | `qc.csxdg(ctrl, tgt)` | — |
 * | SWAP | `qc.swap(q1, q2)` | — |
 * | CP | `qc.cp(ctrl, tgt, λ)` | λ |
 * | CRx | `qc.crx(ctrl, tgt, θ)` | θ |
 * | CRy | `qc.cry(ctrl, tgt, θ)` | θ |
 * | CRz | `qc.crz(ctrl, tgt, θ)` | θ |
 * | CU | `qc.cu(ctrl, tgt, θ, φ, λ, γ)` | 4 angles |
 * | Toffoli | `qc.ccx(c1, c2, tgt)` | — |
 * | Fredkin | `qc.cswap(ctrl, q1, q2)` | — |
 *
 * @subsection py_circuit_measure Measurements
 *
 * @code{.py}
 * # Measure specific qubits to specific classical bits
 * qc.measure([(0, 0), (1, 1), (2, 2)])
 *
 * # Or measure all qubits at once
 * qc.measure_all()
 * @endcode
 *
 * @subsection py_circuit_estimate Expectation Values with QuantumCircuit
 *
 * @code{.py}
 * qc = QuantumCircuit()
 * qc.h(0)
 * qc.cx(0, 1)
 *
 * # No measurements needed for estimation
 * result = qc.estimate(
 *     observables=["ZZ", "XX", "YY"],
 *     simulator_type=maestro.SimulatorType.QCSim,
 *     simulation_type=maestro.SimulationType.Statevector
 * )
 *
 * vals = result["expectation_values"]
 * print(f"<ZZ> = {vals[0]:.4f}")  #  1.0
 * print(f"<XX> = {vals[1]:.4f}")  #  1.0
 * print(f"<YY> = {vals[2]:.4f}")  # -1.0
 * @endcode
 *
 * ---
 *
 * @section py_advanced Advanced: Object-Oriented Simulator Control
 *
 * For full control over the simulation lifecycle, use the `Maestro`
 * class directly to create, configure, and destroy simulators.
 *
 * @code{.py}
 * import maestro
 *
 * m = maestro.Maestro()
 *
 * # Create a simulator handle
 * # Defaults to QCSim + MatrixProductState
 * handle = m.create_simulator(
 *     maestro.SimulatorType.QCSim,
 *     maestro.SimulationType.Statevector
 * )
 *
 * # Get the raw simulator object
 * sim = m.get_simulator(handle)
 *
 * # Clean up when done
 * m.destroy_simulator(handle)
 * @endcode
 *
 * @note The `Maestro` class is primarily used for handle-based lifecycle
 * management. For most Python workflows, the `QuantumCircuit` API or the
 * `simple_execute` / `simple_estimate` convenience functions are recommended.
 *
 * ---
 *
 * @section py_quest QuEST Backend
 *
 * Maestro supports the [QuEST](https://quest.qtechtheory.org/) simulator
 * as an alternative CPU backend. QuEST is loaded dynamically as a shared
 * library (`libmaestroquest`), so it is available only when the library
 * has been built and installed.
 *
 * QuEST natively supports **MPI-distributed statevector** simulation,
 * enabling execution across multiple nodes for larger qubit counts.
 *
 * @subsection py_quest_avail Checking Availability
 *
 * @code{.py}
 * import maestro
 *
 * # Check if the QuEST library is installed
 * if maestro.is_quest_available():
 *     print("QuEST is available")
 * else:
 *     print("QuEST not found — install libmaestroquest")
 *
 * # Explicitly initialise QuEST (optional — done automatically on first use)
 * maestro.init_quest()
 * @endcode
 *
 * @subsection py_quest_exec Running a Circuit on QuEST
 *
 * QuEST only supports **Statevector** simulation. Requesting any other
 * simulation type (MPS, Stabilizer, etc.) will raise an error.
 *
 * @code{.py}
 * qasm = """
 * OPENQASM 2.0;
 * include "qelib1.inc";
 * qreg q[2];
 * creg c[2];
 * h q[0];
 * cx q[0], q[1];
 * measure q -> c;
 * """
 *
 * result = maestro.simple_execute(
 *     qasm,
 *     simulator_type=maestro.SimulatorType.QuestSim,
 *     simulation_type=maestro.SimulationType.Statevector,
 *     shots=1000
 * )
 *
 * print(result["counts"])  # {"00": ~500, "11": ~500}
 * @endcode
 *
 * @subsection py_quest_estimate Expectation Values with QuEST
 *
 * @code{.py}
 * bell = """
 * OPENQASM 2.0;
 * include "qelib1.inc";
 * qreg q[2];
 * h q[0];
 * cx q[0], q[1];
 * """
 *
 * result = maestro.simple_estimate(
 *     bell, "ZZ;XX;YY",
 *     simulator_type=maestro.SimulatorType.QuestSim,
 *     simulation_type=maestro.SimulationType.Statevector
 * )
 *
 * vals = result["expectation_values"]
 * print(f"<ZZ> = {vals[0]:.4f}")  #  1.0
 * print(f"<XX> = {vals[1]:.4f}")  #  1.0
 * print(f"<YY> = {vals[2]:.4f}")  # -1.0
 * @endcode
 *
 * @subsection py_quest_circuit QuantumCircuit with QuEST
 *
 * The programmatic `QuantumCircuit` API works with QuEST too:
 *
 * @code{.py}
 * from maestro.circuits import QuantumCircuit
 *
 * qc = QuantumCircuit()
 * qc.h(0)
 * qc.cx(0, 1)
 * qc.measure_all()
 *
 * result = qc.execute(
 *     simulator_type=maestro.SimulatorType.QuestSim,
 *     simulation_type=maestro.SimulationType.Statevector,
 *     shots=100
 * )
 * print(result["counts"])
 * @endcode
 *
 * @note QuEST only supports `SimulationType.Statevector`.
 * Passing `MatrixProductState`, `Stabilizer`, `TensorNetwork`,
 * or `PauliPropagator` will raise an exception:
 * `"QuestSim only supports Statevector"`.
 *
 * ---
 *
 * @section py_gpu GPU Backend
 *
 * Maestro supports GPU-accelerated simulation via a dynamically-loaded
 * CUDA library (`libcomposer_gpu_simulators`). Like QuEST, the GPU backend
 * is optional and loaded at runtime only when requested.
 *
 * @note The GPU backend is **not included** in the open-source version of
 * Maestro. Contact [Qoro Quantum](https://qoroquantum.de) for access.
 *
 * Unlike QuEST, the GPU backend supports **multiple simulation types**
 * including Statevector, MPS, Tensor Network, and Pauli Propagation.
 *
 * @subsection py_gpu_avail Checking Availability
 *
 * @code{.py}
 * import maestro
 *
 * # Check if the GPU library is installed
 * if maestro.is_gpu_available():
 *     print("GPU backend is available")
 * else:
 *     print("GPU not found — install libcomposer_gpu_simulators")
 *
 * # Explicitly initialise GPU (optional — done automatically on first use)
 * maestro.init_gpu()
 * @endcode
 *
 * @subsection py_gpu_exec Running a Circuit on GPU
 *
 * @code{.py}
 * import maestro
 *
 * maestro.init_gpu()
 *
 * qasm = """
 * OPENQASM 2.0;
 * include "qelib1.inc";
 * qreg q[2];
 * creg c[2];
 * h q[0];
 * cx q[0], q[1];
 * measure q -> c;
 * """
 *
 * # GPU Statevector
 * result = maestro.simple_execute(
 *     qasm,
 *     simulator_type=maestro.SimulatorType.Gpu,
 *     simulation_type=maestro.SimulationType.Statevector,
 *     shots=1000
 * )
 * print(result["counts"])
 *
 * # GPU MPS
 * result_mps = maestro.simple_execute(
 *     qasm,
 *     simulator_type=maestro.SimulatorType.Gpu,
 *     simulation_type=maestro.SimulationType.MatrixProductState,
 *     max_bond_dimension=64,
 *     shots=1000
 * )
 * print(result_mps["counts"])
 * @endcode
 *
 * @subsection py_gpu_estimate Expectation Values on GPU
 *
 * @code{.py}
 * bell = """
 * OPENQASM 2.0;
 * include "qelib1.inc";
 * qreg q[2];
 * h q[0];
 * cx q[0], q[1];
 * """
 *
 * result = maestro.simple_estimate(
 *     bell, "ZZ;XX;YY",
 *     simulator_type=maestro.SimulatorType.Gpu,
 *     simulation_type=maestro.SimulationType.Statevector
 * )
 *
 * vals = result["expectation_values"]
 * print(f"<ZZ> = {vals[0]:.4f}")  #  1.0
 * print(f"<XX> = {vals[1]:.4f}")  #  1.0
 * print(f"<YY> = {vals[2]:.4f}")  # -1.0
 * @endcode
 *
 * @subsection py_gpu_circuit QuantumCircuit with GPU
 *
 * @code{.py}
 * from maestro.circuits import QuantumCircuit
 *
 * maestro.init_gpu()
 *
 * qc = QuantumCircuit()
 * qc.h(0)
 * qc.cx(0, 1)
 * qc.measure_all()
 *
 * result = qc.execute(
 *     simulator_type=maestro.SimulatorType.Gpu,
 *     simulation_type=maestro.SimulationType.Statevector,
 *     shots=2048
 * )
 * print(result["counts"])
 * @endcode
 *
 * @subsection py_gpu_simtypes Supported Simulation Types
 *
 * | SimulationType | GPU Support |
 * |----------------|-------------|
 * | `Statevector` | ✅ |
 * | `MatrixProductState` | ✅ |
 * | `TensorNetwork` | ✅ |
 * | `PauliPropagator` | ✅ |
 * | `Stabilizer` | ❌ |
 * | `ExtendedStabilizer` | ❌ |
 *
 * @subsection py_gpu_fallback Fallback Pattern
 *
 * For portable scripts that should work with or without GPU:
 *
 * @code{.py}
 * import maestro
 *
 * # Pick the best available backend
 * if maestro.init_gpu():
 *     sim_type = maestro.SimulatorType.Gpu
 *     print("Using GPU")
 * elif maestro.init_quest():
 *     sim_type = maestro.SimulatorType.QuestSim
 *     print("Using QuEST")
 * else:
 *     sim_type = maestro.SimulatorType.QCSim
 *     print("Using CPU (QCSim)")
 *
 * result = maestro.simple_execute(qasm, simulator_type=sim_type, shots=1000)
 * print(result["counts"])
 * @endcode
 *
 * ---
 *
 * @section py_enums Enumerations Reference
 *
 * @subsection py_enum_simtype SimulatorType
 *
 * | Value | Description | Dynamic? |
 * |-------|-------------|----------|
 * | `SimulatorType.QCSim` | QCSim CPU simulator | No (built-in) |
 * | `SimulatorType.CompositeQCSim` | Composite (distributed) CPU simulator | No (built-in) |
 * | `SimulatorType.QuestSim` | QuEST simulator | Yes (`libcomposer_quest`) |
 * | `SimulatorType.Gpu` | GPU-accelerated simulator (requires CUDA) | Yes (`libcomposer_gpu_simulators`) |
 *
 * @subsection py_enum_simmethod SimulationType
 *
 * | Value | Description | Supported By |
 * |-------|-------------|-------------|
 * | `SimulationType.Statevector` | Full statevector simulation | QCSim, Aer, GPU, QuEST |
 * | `SimulationType.MatrixProductState` | MPS / tensor-train simulation | QCSim, Aer, GPU |
 * | `SimulationType.Stabilizer` | Clifford stabiliser simulation | QCSim, Aer |
 * | `SimulationType.TensorNetwork` | Tensor network contraction | QCSim, Aer, GPU |
 * | `SimulationType.PauliPropagator` | Pauli frame propagation | QCSim, GPU |
 * | `SimulationType.ExtendedStabilizer` | Extended stabiliser decomposition | Aer only |
 *
 */
