# eigenkit eigenkit is a provider-neutral quantum computing library by EigenOrbit: a circuit is defined once and runs on any supported simulator or quantum processor by changing the `provider=` argument. Install with `pip install eigenkit`. # Docs # eigenkit **Build quantum algorithms once. Run them anywhere.** `eigenkit` is a provider-neutral quantum computing library that allows users to define quantum algorithms once and execute them across different simulators and quantum hardware through a common interface. Switching between providers requires changing only the `provider` argument, without modifying the algorithm itself. The library also provides a consistent interface for computing gradients across backends. `eigenkit` supports a wide range of applications, including quantum machine learning, optimization, and quantum chemistry, with ready-to-use algorithms and models. It integrates naturally with the classical scientific computing ecosystem, including NumPy, PyTorch, and scikit-learn, making it easy to incorporate quantum components into existing workflows. The library includes a built-in simulator and supports multiple external execution providers, including PennyLane, Qiskit, Amazon Braket, IQM, NVIDIA CUDA-Q, TensorCircuit, Qulacs, and qsim, plus Quandela's Perceval for photonic circuits. External dependencies are optional, allowing users to install only the backends they need. For cloud-based quantum hardware, `eigenkit` also provides cost warnings and execution previews to help users estimate resource requirements before submitting jobs. ______________________________________________________________________ ## Quick look A circuit is written in one of two ways: from ready-made blocks, or gate by gate with `CircuitBuilder`. Both produce the same provider-neutral `CircuitSpec`, so everything after construction (backends, gradients, the torch interface) works the same either way. By default a circuit runs on the numpy interface and the built-in numpy simulator, with no extra dependencies. Blocks cover the common shapes: an encoding of the input features followed by trainable layers. The numpy circuit owns no weights; you pass them per call: ```python import numpy as np from eigenkit import Circuit, Encoding, Variational, all_z circuit = Circuit( blocks=[ Encoding(in_features=4, rotation="RX"), Variational(depth=2, rotation="RY"), ], observables=all_z(4), ) weights = np.zeros(circuit.spec.n_weights) x = np.random.uniform(0, np.pi, size=(8, 4)) # batch of 8 samples y = circuit(weights, x) # shape (8, 4) - one expectation per qubit grad = circuit.jacobian(weights, x[0]) # d/d(weights), no torch ``` `CircuitBuilder` appends one gate per call, in the style of Qiskit, Braket and CUDA-Q, and returns itself so calls can be chained: ```python from eigenkit import Circuit, CircuitBuilder, X, Z qc = CircuitBuilder(n_qubits=2) qc.h(0).cnot(0, 1) # a Bell pair bell = Circuit(qc.build(), observables=[Z(0) * Z(1), X(0) * X(1)]) print(bell([])) # [1. 1.] - no trainable weights, so an empty weight vector print(bell.probabilities([])) # [0.5 0. 0. 0.5] ``` Trainable angles and input slots come from `qc.add_weights(n)` and `qc.add_inputs(n)`, and gate angles accept arithmetic on them, such as `weights[0] * inputs[1]`. See [Circuit Builder](https://docs.kit.eigenorbit.com/user-guide/circuit-builder/index.md). For a differentiable `torch.nn.Module` that owns its weights, pass `interface="torch"` (the `torch` extra): ```python import torch from eigenkit import Circuit, Encoding, Variational, all_z layer = Circuit( blocks=[Encoding(in_features=4), Variational(depth=2)], observables=all_z(4), interface="torch", ) y = layer(torch.randn(8, 4)) # shape (8, 4); backprops into a torch model ``` The `provider` argument is polymorphic: pass a registered name (`"numpy"`, `"pennylane"`, `"qiskit"`, `"braket"`, … — see [Providers](https://docs.kit.eigenorbit.com/user-guide/providers/index.md) for the full list), a config (`PennyLaneConfig(...)`, `QiskitConfig(...)`, `BraketConfig(...)`), or a Backend instance. ______________________________________________________________________ ## What's in the box - **Circuit construction.** Define quantum circuits using reusable blocks, build them gate by gate with `CircuitBuilder`, or combine both approaches using `to_block()`. Add observables and execute circuits on any supported backend. - **High-level quantum layers.** Use ready-made layers such as `Basic`, `Reupload`, `Ensemble`, and `MinMaxNorm`, which can be integrated directly into PyTorch's `nn.Sequential`. - **Scikit-learn estimators.** Ready-to-use classifiers and regressors with a familiar scikit-learn interface, including `HQNC`, `HQNR`, `RecurrentHQNC`, `RecurrentHQNR`, `QSVC`, `QSVR`, `CustomQuantumClassifier`, `CustomQuantumRegressor`, `GenericClassifier`, `GenericRegressor`, `QuantumTimeSeriesClassifier`, `QuantumTimeSeriesRegressor`, `QuantumReservoirClassifier`, and `QuantumReservoirRegressor`. - **Quantum algorithms.** Implementations of `HHL` for linear systems, `IQAE` for amplitude estimation, and `PortfolioRiskEstimator` for Value at Risk (VaR) and Conditional Value at Risk (CVaR). Available in `eigenkit.algorithms`, these solvers follow a simple configure-and-run workflow and return immutable results rather than using a `fit`/`predict` interface. - **Optimization.** Tools for formulating `QUBO` and `Ising` problems, solving Max-Cut problems with `max_cut`, and running `QAOA`. Available in `eigenkit.optimization`, with NumPy and SciPy as the only numerical dependencies. - **Quantum chemistry.** Variational ground-state calculations with `VQE`, fermionic operators, Jordan-Wigner and Bravyi-Kitaev mappings, and UCCSD ansätze. Available in `eigenkit.chemistry` without requiring PyTorch. Molecular Hamiltonians (`molecular_hamiltonian`, `hydrogen_molecule`) require the optional `chemistry` dependencies. - **Model persistence.** Save and load trained estimators as `.eigenkit` bundles, and export benchmark results for individual random seeds. - **Datasets and benchmarking.** Built-in datasets, exploratory data analysis (EDA) reports, and multi-seed benchmarking tools for comparing quantum models against classical baselines. - **Backend support.** Execute circuits using the built-in NumPy simulator or external providers, including PennyLane, Qiskit, Qiskit Aer (dense and MPS), Amazon Braket (local simulators, cloud simulators, and QPUs), Qiskit Runtime, IQM, NVIDIA CUDA-Q, TensorCircuit, Qulacs, and qsim. All backends use the same `provider=` interface. CUDA-Q and Qiskit Runtime require explicit configuration through a backend instance or a registered provider name. - **Photonic computing.** Build and execute photonic circuits using Quandela's Perceval, either locally or through Quandela Cloud. Available in `eigenkit.photonics` with the optional `perceval` dependencies. ______________________________________________________________________ ## Navigation - [Getting Started](https://docs.kit.eigenorbit.com/getting-started/index.md) — install and run your first circuit. - User Guide — blocks and the gate-by-gate circuit builder, the numpy & torch interfaces, layers, observables, providers, quantum kernels, algorithms, QPU/cloud workflows. - [Examples](https://docs.kit.eigenorbit.com/examples/basic-circuit/index.md) — end-to-end worked examples. - API Reference — auto-generated from source docstrings. # Getting Started This guide walks you through the basics of `eigenkit`, from installation to running your first quantum circuit. You will learn how to build circuits using different approaches, integrate them with PyTorch for automatic differentiation, run variational algorithms without PyTorch, and use ready-made quantum layers. Finally, you will see how to execute the same circuits on different backends. For more advanced topics, explore the [User Guide](https://docs.kit.eigenorbit.com/user-guide/encoding/index.md), which covers each feature in more detail. ## Installation `eigenkit` supports Python 3.12, 3.13, and 3.14. You can install it using pip or [uv](https://docs.astral.sh/uv/): ```bash pip install eigenkit ``` Or, if you are using a uv-managed project: ```bash uv add eigenkit ``` The core installation is lightweight, requiring only NumPy, SciPy, and Pydantic. It includes everything needed to build and execute quantum circuits on the built-in NumPy simulator, as well as the framework-independent optimization and quantum chemistry modules. Additional integrations, including PyTorch, scikit-learn, and external quantum computing backends, are available as optional dependencies. This allows you to install only the components you need. ### Optional extras Extras go in square brackets, and several can be combined (`uv add` takes the same form): ```bash pip install "eigenkit[qiskit,braket]" pip install "eigenkit[qml]" # torch-free kernel QML (QSVC/QSVR) pip install "eigenkit[torch]" # differentiable hybrid models (HQNC/HQNR) pip install "eigenkit[all]" ``` | Extra | Adds | Unlocks | | --------------- | ----------------------------------------- | ----------------------------------------------------------------------------------- | | `torch` | PyTorch + scikit-learn | `interface="torch"`, the layers, and the hybrid estimators (HQNC/…) | | `pennylane` | PennyLane + Lightning | `provider="pennylane"` | | `qiskit` | Qiskit + Aer + IBM Runtime | `provider="qiskit"`, `"qiskit-aer"`, `"qiskit-aer-mps"`; IBM Runtime / QPU | | `braket` | Amazon Braket SDK | `provider="braket"` (local, cloud, AWS QPUs) | | `iqm` | iqm-client + its Qiskit adapter | `provider="iqm"` (IQM Resonance and IQM's fake backends) | | `cudaquantum` | NVIDIA CUDA-Q | `CudaQuantumBackend` — opt-in, see below (Linux CPU + GPU; macOS Apple Silicon CPU) | | `tensorcircuit` | tensorcircuit-ng + JAX | `provider="tensorcircuit"` | | `qulacs` | Qulacs | `provider="qulacs"` | | `qsim` | qsimcirq + Cirq | `provider="qsim"` (single-precision) | | `perceval` | Perceval (Quandela's photonic SDK) | `eigenkit.photonics` (local simulation, Quandela Cloud) | | `qml` | scikit-learn (no PyTorch) | `QuantumKernel`, `QSVC`/`QSVR` | | `datasets` | scikit-learn | `eigenkit.datasets` | | `benchmark` | matplotlib + pandas + scikit-learn | the comparison harness | | `chemistry` | PySCF + OpenFermion (no PySCF on Windows) | real molecules (`molecular_hamiltonian`, `hydrogen_molecule`) | | `viz` | matplotlib | `Circuit.draw_mpl()` (the text drawer needs nothing) | | `all` | every extra above except `perceval` | — | Backend classes and their configurations are always available for import, even when their optional dependencies are not installed. However, attempting to use a backend without its required dependencies, such as `Circuit(provider="qiskit")`, will raise an error with instructions on how to install them. The same applies to PyTorch and scikit-learn features. Two backends are **not registered automatically**: NVIDIA CUDA-Q, which selects a process-wide execution target, and Qiskit Runtime, which can submit billable jobs to IBM quantum hardware. You can pass either one's configuration or a backend instance directly to `provider=`, for example `provider=QiskitRuntimeConfig(...)`, with no registration. To use them by name like any other backend, register an instance once: ```python from eigenkit import CudaQuantumBackend, QiskitRuntimeBackend, register register("cudaq", CudaQuantumBackend()) register("qiskit-runtime", QiskitRuntimeBackend()) ``` All other backends are registered automatically when their dependencies are available. ### Platform compatibility Some backends have additional compatibility restrictions depending on the Python version and operating system: - **Python 3.14:** The `qulacs`, `qsim`, and `iqm` extras can be installed, but their corresponding SDKs are not included due to compatibility limitations. Qulacs and qsim do not yet provide Python 3.14 wheels, while IQM requires a Pydantic version that conflicts with Braket. To use these providers directly, we recommend Python 3.12 or 3.13. IQM hardware remains accessible through Braket on Python 3.14. - **Windows:** The `chemistry` extra installs OpenFermion without PySCF, because PySCF does not support Windows. Real molecules (`molecular_hamiltonian`, `hydrogen_molecule`) need PySCF, so run them under WSL. The rest of `eigenkit.chemistry` works on Windows. - **Linux ARM:** The `qsim` extra does not include `qsimcirq`, as compatible wheels are unavailable. - **Linux ARM:** The `perceval` extra needs the system library `libatomic.so.1`, which minimal images such as Ubuntu Server leave out. Install it with `sudo apt install libatomic1`. - **Linux ARM with PyTorch and Qiskit:** PyTorch, scikit-learn, CUDA-Q and PennyLane Lightning each bring their own copy of the OpenMP runtime. Loaded in one process together with Qiskit, for example with the `torch` and `qiskit` extras or with `all`, the last library to load fails with `cannot allocate memory in static TLS block`, whatever the import order. Set this variable before you start Python: ```bash export GLIBC_TUNABLES=glibc.rtld.optional_static_tls=262144 ``` - **Linux ARM with Python 3.14:** The `qiskit` extra does not include Qiskit Aer because compatible wheels are unavailable. These limitations affect specific optional backends and do not prevent you from using the built-in simulator or other compatible providers. ### Choosing a PyTorch build The `torch` extra asks for any PyTorch release in `torch>=2.5,<3`, and installs whichever build your package index serves by default. To get a specific build, CPU-only on Linux for example, install PyTorch from its own index first; eigenkit then uses the PyTorch that is already there: ```bash pip install torch --index-url https://download.pytorch.org/whl/cpu pip install "eigenkit[torch]" ``` In a uv project, point `torch` at PyTorch's index in your own `pyproject.toml`, as [uv's PyTorch guide](https://docs.astral.sh/uv/guides/integration/pytorch/) describes. Working on eigenkit itself is different: the repository routes PyTorch to a CUDA index on Linux and Windows, and [CONTRIBUTING.md](https://github.com/eigenorbit/eigenkit/blob/main/CONTRIBUTING.md) explains how to swap in a CPU build. ## Your first circuit A `Circuit` is built from an ordered list of *blocks* (or gate by gate, as shown below) and an *observables* spec. By default it runs on the **numpy** interface and the built-in **numpy** simulator — no extra required. The numpy circuit doesn't own weights; you pass them at call time: ```python import numpy as np from eigenkit import Circuit, Encoding, Variational, all_z # 1. Define blocks. Encoding maps classical features to rotations; # Variational adds depth * n_qubits trainable parameters. # 2. Choose what to measure — all_z(4) measures Z on every qubit. circuit = Circuit( blocks=[Encoding(in_features=4, rotation="RX"), Variational(depth=2, rotation="RY")], observables=all_z(4), ) # 3. Run: pass weights and inputs explicitly. weights = np.zeros(circuit.spec.n_weights) x = np.array([0.1, 0.2, 0.3, 0.4]) y = circuit(weights, x) print(y.shape) # (4,) ``` The qubit count is inferred from `Encoding.in_features` (or from the `in_features` of a `Reuploading` block). The output dimension equals the number of observables. Both blocks above pass `rotation` explicitly; left out, it is `"RY"` on each. Where the names come from Everything in the top-level `__all__` is imported from `eigenkit` itself: `Circuit`, the blocks, the observable helpers, the backend configs, the estimators, `CircuitBuilder`, `QuantumKernel`, and the solvers `HHL`, `IQAE` and `PortfolioRiskEstimator`. The domain packages keep their own names, so those are imported from the package they belong to — `eigenkit.optimization` for `QUBO`, `Ising`, `max_cut` and `QAOA`, `eigenkit.chemistry` for `VQE`, `eigenkit.benchmark` for `compare`, `eigenkit.datasets` for the datasets and their EDA reports, and `eigenkit.core.ir` for the parameter-expression nodes `Scale`, `Negate`, `Sum` and `Multiply`. Every page follows that. ### The same circuit, gate by gate Instead of using predefined blocks, you can construct circuits gate by gate with `CircuitBuilder`, following a similar approach to Qiskit, Braket, and CUDA-Q. The `add_weights()` and `add_inputs()` methods define symbolic parameters for trainable weights and input data, respectively. You can then use these parameters when adding gates to the circuit. Each gate method returns the builder, allowing you to chain operations together. Once the circuit is complete, `build()` converts it into a `CircuitSpec`, a provider-neutral representation that can be executed by `Circuit` on any supported backend: ```python from eigenkit import CircuitBuilder qc = CircuitBuilder(n_qubits=4) params = qc.add_weights(8) features = qc.add_inputs(4) # Encoding(in_features=4, rotation="RX"): one RX per input. for q in range(4): qc.rx(features[q], qubit=q) # Variational(depth=2, rotation="RY"): per layer, an RY on every qubit, # then a chain of CNOTs. for layer in range(2): for q in range(4): qc.ry(params[layer * 4 + q], qubit=q) for q in range(3): qc.cnot(q, q + 1) by_hand = Circuit(qc.build(), observables=all_z(4)) print(by_hand.spec == circuit.spec) # True - the very same circuit print(by_hand.draw()) ``` ```text q_0: ──RX(x_0)──RY(w_0)──●──RY(w_4)──────────────●─────────── │ │ q_1: ──RX(x_1)──RY(w_1)──X─────●─────RY(w_5)─────X─────●───── │ │ q_2: ──RX(x_2)──RY(w_2)────────X────────●─────RY(w_6)──X──●── │ │ q_3: ──RX(x_3)──RY(w_3)─────────────────X─────RY(w_7)─────X── ``` Both approaches produce equivalent circuits and can be used interchangeably in the examples that follow. Predefined blocks offer a simpler way to build common circuit architectures, particularly those combining data encoding and variational layers. `CircuitBuilder`, on the other hand, provides more flexibility by allowing you to define arbitrary gate sequences. The builder also supports arithmetic expressions involving symbolic parameters, such as `2 * math.pi * features[0]` or `params[0] * features[1]`, making it easy to define parameterised gates. You can also combine both approaches using `qc.to_block()`, which converts a circuit built with `CircuitBuilder` into a reusable block. For a complete list of supported gates and additional examples, see the [Circuit Builder](https://docs.kit.eigenorbit.com/user-guide/circuit-builder/index.md) guide. ### A differentiable PyTorch circuit To integrate a quantum circuit into a PyTorch model, set `interface="torch"`. This creates a `torch.nn.Module` with trainable parameters managed as `nn.Parameter` objects. The resulting module supports batched inputs and automatic differentiation through backpropagation, allowing quantum circuits to be trained alongside classical neural networks in hybrid models. This functionality requires the optional `torch` dependencies, and it runs on the built-in NumPy simulator, which is differentiable: ```python import torch from eigenkit import Circuit, Encoding, Variational, all_z circuit = Circuit( blocks=[Encoding(in_features=4, rotation="RX"), Variational(depth=2)], observables=all_z(4), interface="torch", ) y = circuit(torch.randn(3, 4)) # batch=3, features=4 -> torch.Size([3, 4]) ``` ### A variational ground-state loop (no PyTorch) The numpy circuit plugs straight into the scipy-based variational spine: ```python import numpy as np from eigenkit import Circuit, Variational, Z, minimize_expectation # Variational alone declares a single wire, and minimize_expectation takes # exactly one observable — the Hamiltonian. circuit = Circuit([Variational(depth=2, rotation="RY")], observables=Z(0)) # Start from a small random point — an all-zeros init sits on a symmetric # saddle where many gradients vanish, which stalls gradient-based optimizers. rng = np.random.default_rng(0) x0 = rng.uniform(0, 2 * np.pi, size=circuit.spec.n_weights) result = minimize_expectation(circuit, initial_parameters=x0) print(result.optimal_value) ``` The `minimize_expectation` function also accepts several optional keyword arguments to customise the optimization process: - `optimizer`: The optimization method to use. Defaults to `"L-BFGS-B"`, but supports any method available in `scipy.optimize.minimize`, `"spsa"`, or a custom callable. - `max_iterations` and `tol`: Control the maximum number of iterations and convergence tolerance. Both default to `None`, allowing the optimizer to use its own settings. - `random_state`: Controls the random initialisation of parameters when `initial_parameters` is not provided. Defaults to `42` for reproducibility. - `callback`: An optional function called during optimization. Defaults to `None`. ### Computing gradients without PyTorch Quantum circuits can also be differentiated directly through the NumPy interface, without requiring PyTorch. The `circuit.jacobian(weights, inputs)` method computes the derivatives of expectation values with respect to the trainable parameters, using analytic adjoint differentiation or parameter-shift methods, depending on the backend. The NumPy interface also provides methods for inspecting and executing circuits, including `probabilities()`, `statevector()`, `zero_state_probability()`, `zero_state_probabilities()`, `draw()`, `draw_mpl()`, `submit()`, and `dry_run_info()`. For more information about these methods, see the [Interfaces](https://docs.kit.eigenorbit.com/user-guide/interfaces/index.md) guide. ## Your first quantum layer `eigenkit` provides ready-to-use quantum layers that simplify the integration of quantum circuits into PyTorch models. These layers wrap differentiable quantum circuits and can be used alongside classical layers to build hybrid neural networks. They require the optional `torch` dependencies, and they run on the built-in NumPy simulator unless you pass `provider=`: ```python import torch from eigenkit import Basic layer = Basic(n_qubits=4, depth=2) out = layer(torch.randn(8, 4)) print(out.shape) # torch.Size([8, 4]) ``` The available layers are: - `Basic` — one Encoding + one Variational + measurement. - `Reupload` — standard data reuploading (optionally with trainable scale). - `Ensemble` — N independent circuits over input chunks. - `MinMaxNorm` — feature-wise min/max normalisation. See the [User Guide](https://docs.kit.eigenorbit.com/user-guide/encoding/index.md) for block-level control or the [Examples](https://docs.kit.eigenorbit.com/examples/basic-circuit/index.md) section for end-to-end models. ## Switching backends The `provider` argument accepts three shapes: ```python from eigenkit import Circuit, Encoding, Variational, PennyLaneConfig blocks = [Encoding(in_features=2), Variational(depth=1)] # 1. A registered name string ("numpy" is the default). Circuit(blocks=blocks, provider="numpy") # 2. A config dataclass — needs the matching extra installed. Circuit(blocks=blocks, provider=PennyLaneConfig(device_name="lightning.qubit")) # 3. A Backend instance (advanced) — used as it is. from eigenkit import NumpySimulator, register Circuit(blocks=blocks, provider=NumpySimulator()) # Registering an instance gives it a name you can reuse anywhere. register("numpy-copy", NumpySimulator()) Circuit(blocks=blocks, provider="numpy-copy") ``` The built-in NumPy simulator is the default backend and is always available without additional dependencies. Other backends become available automatically when their corresponding extras are installed. The only exceptions are CUDA-Q and Qiskit Runtime, which require explicit configuration. You can use either backend by passing an instance directly to `provider=`, or by registering it with `register()` under a custom name for reuse. # Architecture: How eigenkit is organised This guide provides an overview of `eigenkit`'s architecture, from its main components to the internal representation and execution of quantum circuits. It covers four key aspects: 1. **Library structure.** How the core library connects to optional backends, application modules, and framework integrations. 1. **Circuit execution.** How a circuit is processed, from its definition to execution on a selected backend. 1. **Circuit representation.** How `CircuitSpec` provides a common, provider-neutral representation of quantum circuits. 1. **Backends and interfaces.** How different execution providers and user-facing interfaces fit into the architecture. At the center of `eigenkit` is a lightweight core built on NumPy and SciPy, which represents quantum circuits independently of any execution provider through `CircuitSpec`. Additional functionality, including external backends, application-specific modules, and integrations with classical frameworks, is organised into optional layers that depend on the core, while keeping the core independent of them. ______________________________________________________________________ ## 1. The library structure: Core and optional modules `eigenkit` is built around a lightweight, framework-independent core that uses only NumPy, SciPy, and Pydantic. This core contains the essential components for defining, compiling, and executing quantum circuits, including a built-in simulator. Additional functionality is organised into optional modules that extend the core without introducing new dependencies into it. Quantum SDK backends connect through the provider registry, while integrations with classical frameworks use the interface registry. Application-specific modules, such as optimization and quantum chemistry, build on the core's variational optimization tools. Importing `eigenkit` loads only the core components. Additional modules are loaded when needed. ``` flowchart TB subgraph CORE["core/  — compute layer  (numpy · scipy · pydantic — framework-free)"] direction TB DATA["Data (IR)
CircuitSpec · Gate · Parameter tree
PauliTerm · Observable · adjoint · resolve"] CONS["Construct
CircuitBuilder · Block · assemble
observable algebra (X/Y/Z · PauliExpr)"] PROV["Providers
Provider protocol · Capabilities · registry
CompiledCircuit (compile / run split)
parameter_shift_jacobian (gradient floor)"] NPB["numpy_backend.py
built-in reference simulator
analytic-adjoint gradients · zero extra deps"] RUN["Run
numpy Circuit (weights per call) · jacobian
minimize_expectation (scipy variational spine)"] IFACE["Interfaces
core/interfaces.py registry
numpy built-in"] end BACK["backends/ ring  (SDK extras)
pennylane · qiskit · qiskit-aer · braket
qiskit-runtime · iqm · cudaq · tensorcircuit
qulacs · qsim"] OPT["optimization/  (numpy)
QAOA · QUBO/Ising · max_cut"] CHEM["chemistry/  (numpy; PySCF = extra)
VQE · UCCSD · Jordan-Wigner / Bravyi-Kitaev"] ALGR["algorithms/  (numpy)
HHL · IQAE · PortfolioRiskEstimator"] RESR["reservoir/  (numpy)
run_reservoir · state policies
feature maps · LinearReadout"] QMLR["qml/  (sklearn extra)
QuantumKernel · QSVC / QSVR
sklearn estimator base"] TORCHR["torch/  (torch extra)
TorchCircuit nn.Module · layers
hybrid estimators"] PHOT["photonics/  (perceval extra)
PhotonicCircuitSpec · PercevalBackend
photonic parameter-shift · Quandela Cloud"] DATA --> CONS --> RUN PROV --> RUN NPB -. implements .-> PROV IFACE --> RUN BACK -. "register()" .-> PROV TORCHR -. "register interface" .-> IFACE OPT --> RUN CHEM --> RUN ALGR --> RUN RESR --> RUN QMLR --> RUN TORCHR --> RUN PHOT -. "Parameter tree" .-> DATA classDef core stroke:#5c6bc0,stroke-width:2px; classDef ring stroke:#e65100,stroke-width:2px; classDef sim stroke:#00897b,stroke-width:2px; class DATA,CONS,PROV,RUN,IFACE core; class NPB sim; class BACK,OPT,CHEM,ALGR,RESR,QMLR,TORCHR,PHOT ring; ``` Two additional packages provide utilities for working with quantum models without being part of the circuit execution architecture: - **Datasets (`datasets/`).** Built-in datasets and exploratory data analysis (EDA) tools. - **Benchmarking (`benchmark/`).** Multi-seed experiments and comparisons against classical machine-learning baselines. These tools interact with the library through the scikit-learn estimator interface rather than directly through the core execution layer. The architecture follows a simple dependency rule: all optional modules depend on the core, but the core never depends on them. This separation is enforced in continuous integration (CI) using import-linter. The built-in NumPy simulator is the only execution backend included directly in the core. It serves as the reference implementation against which other backends are validated, helping ensure consistent behavior across providers. ______________________________________________________________________ ## 2. Data flow: From circuit definition to execution The execution of a quantum circuit in `eigenkit` follows three main stages: 1. **Construction.** Define a circuit using predefined blocks, `CircuitBuilder`, or a combination of both. The result is a `CircuitSpec`, a provider-neutral representation of the circuit. 1. **Compilation.** Convert the `CircuitSpec` into the native representation required by the selected backend. This step prepares the circuit for execution and is performed only once, allowing the compiled objects to be reused. 1. **Execution and differentiation.** Run the compiled circuit or compute its gradients using `run()` and `jacobian()`. These operations can be called repeatedly with different parameter values without recompiling the circuit. ``` flowchart LR subgraph BUILD["① Construct (once)"] direction TB B["blocks=[Encoding, Variational, ...]"] BLD["CircuitBuilder
qc.rx(...) · qc.cnot(...)"] ASM["assemble()
relocate local WeightRef/InputRef
into the global frame"] SPEC[("CircuitSpec
+ Observable(s)")] B --> ASM --> SPEC BLD -- "build()" --> SPEC BLD -. "to_block()" .-> B end subgraph COMPILE["② provider.compile() (once)"] direction TB VAL["validate spec vs Capabilities
(gates, max_qubits)"] CC[["CompiledCircuit
cached native objects:
matrices · qnodes · parametric QuantumCircuit · Pauli ops"]] VAL --> CC end subgraph CALL["③ Per call"] direction TB FWD["circuit(weights, inputs)
resolve Parameter exprs
→ expectation array"] JAC["circuit.jacobian(weights, inputs)
d⟨O⟩ / d(weights)"] end SPEC --> VAL CC --> FWD CC --> JAC JAC --> GRADOFF{"gradient_method
= none?"} GRADOFF -- "yes (hardware, unless
enable_gradients=True)" --> REFUSE["raises: train on a simulator,
or opt in"] GRADOFF -- "no" --> GRADPATH{"backend has
its own jacobian?"} GRADPATH -- "yes (adjoint: numpy, Qulacs,
TensorCircuit; PennyLane autodiff)" --> NATIVE["use it directly"] GRADPATH -- "no (Qiskit, Aer, Braket, CUDA-Q, qsim;
hardware with enable_gradients=True)" --> PS["core parameter-shift
f(θ+π/2) − f(θ−π/2) / 2"] class REFUSE stage; class GRADOFF branch; PS --> UNSAFE{"angles wrap weights
in Scale/Multiply/Sum?"} UNSAFE -- "no" --> FASTPS["naive shift (fast path)"] UNSAFE -- "yes" --> CHAIN["chain-rule jacobian
Σ (∂θ/∂wᵢ)·(∂⟨O⟩/∂θ)"] classDef stage stroke:#5c6bc0,stroke-width:2px; classDef data stroke:#00897b,stroke-width:2px; classDef branch stroke:#e65100,stroke-width:2px; class B,BLD,ASM,VAL,FWD,JAC,NATIVE,PS,FASTPS,CHAIN stage; class SPEC,CC data; class GRADPATH,UNSAFE branch; ``` ### Why separate compilation from execution? Creating backend-specific objects, such as a Qiskit `QuantumCircuit` or a PennyLane device, can be significantly more expensive than evaluating an already compiled circuit. By separating compilation from execution, `eigenkit` avoids rebuilding these objects for every forward or backward pass, reducing overhead during repeated evaluations and variational optimization. For hardware backends, compilation may also include device-specific transpilation. In Qiskit Runtime and IQM (with the default `transpile_mode="parametric"`), this transformation is performed once, allowing subsequent executions with different parameter values to reuse the transpiled circuit. ______________________________________________________________________ ## 3. The data model: `CircuitSpec` and symbolic parameters All backends in `eigenkit` use the same provider-neutral representation: a `CircuitSpec` together with a list of `Observable` objects. One important feature of this representation is that gate angles can be symbolic expressions rather than fixed numerical values. Instead of specifying an angle directly, users can define expressions such as `2π·x₀` or `w₀·x₁`, where `x` represents input data and `w` represents trainable parameters. This allows circuits to support operations such as feature scaling without requiring users to preprocess their data, while also enabling transformations such as `adjoint` to operate on parameterised gates. ``` classDiagram class CircuitSpec { +int n_qubits +int n_weights +int n_inputs +tuple~Gate~ gates } class Gate { +GateName name +tuple~int~ qubits +tuple~Parameter~ params +tuple data +bool adjoint } class Parameter { <> } class Constant { +float value } class WeightRef { +int index } class InputRef { +int index } class Scale { +Parameter expression +float factor } class Negate { +Parameter expression } class Sum { +tuple~Parameter~ expressions } class Multiply { +Parameter a +Parameter b } class Observable { +tuple~PauliTerm~ terms +from_string(s) +z(q) } class PauliTerm { +float coeff +tuple paulis } CircuitSpec "1" *-- "many" Gate Gate "1" *-- "many" Parameter Parameter <|-- Constant Parameter <|-- WeightRef Parameter <|-- InputRef Parameter <|-- Scale Parameter <|-- Negate Parameter <|-- Sum Parameter <|-- Multiply Observable "1" *-- "many" PauliTerm ``` ### Symbolic parameter expressions The parameter tree has three types of leaf nodes: - `Constant`: A fixed numerical value. - `WeightRef`: A reference to a trainable parameter. - `InputRef`: A reference to an input feature. These nodes can be combined using four supported operators. In particular, `Multiply` allows two symbolic expressions to be multiplied together, enabling operations such as trainable feature scaling in `Reuploading(trainable_scale=True)`. Because both operands can depend on parameters, these expressions require the chain rule when computing gradients, as illustrated in the data flow diagram in Section 2. | Node | Meaning | Use case | | --------------------------- | --------------------- | ------------------------- | | `Constant(0.5)` | literal | `RX(0.5)` | | `WeightRef(3)` | `weights[3]` | trainable rotations | | `InputRef(0)` | `inputs[0]` | data encoding | | `Scale(expression, factor)` | `factor · expression` | `RY(2π·x)` | | `Negate(expression)` | `−expression` | adjoint of a trainable | | `Sum(expressions)` | `e1 + e2 + …` | data reuploading | | `Multiply(a, b)` | `a · b` (bilinear) | trainable feature scaling | ### Gate representation Each gate contains additional information beyond its parameter expressions: - `Gate.data` stores data that cannot be represented as a single angle, such as the state vector used by `AmplitudeEncoding` or the matrix defining a `QubitUnitary`. - `Gate.adjoint` indicates whether a gate has been inverted by the `adjoint` transformation. - `Gate.name` identifies the gate using one of the predefined names in `GateName`, ensuring that only supported gate types can be represented. ### Observables and Pauli terms Measurements are represented using `Observable` objects, which are composed of `PauliTerm` elements. Each `PauliTerm` has two main components: - `coeff`: A real-valued coefficient, ensuring that the observable represents a Hermitian operator. Providing a complex coefficient raises a `TypeError` that identifies the invalid value. - `paulis`: A tuple of `(Pauli operator, qubit index)` pairs, such as `(("Z", 0), ("X", 1))`, rather than a dictionary or mapping. Together, these structures provide a consistent representation of circuits and observables that can be interpreted by different execution backends. ______________________________________________________________________ ## 4. The backend landscape: One protocol, many providers All backends implement the same `Provider` protocol over `CircuitSpec + Observable`. They differ in how they get gradients, how they represent symbolic parameters, and whether they support async submission to cloud/QPU hardware. ``` flowchart TB PROTO["Provider protocol
compile(spec, observables) → CompiledCircuit
Capabilities · run() · optional submit()"] subgraph LOCAL["Local simulators"] NP["numpy — default
grads: adjoint (exact)
auto-registered · no deps"] PL["pennylane
grads: native autodiff
run_torch: autograd through the device"] QK["qiskit
grads: parameter-shift
statevector sim"] BL["braket (local)
grads: parameter-shift
LocalSimulator"] CQ["cudaq (opt-in)
grads: parameter-shift
restricted param exprs"] AER["qiskit-aer / -mps
grads: parameter-shift
dense core · matrix-product-state"] TC["tensorcircuit
grads: jax autodiff
tensor-network · jax.vmap"] QL["qulacs
grads: adjoint (backprop)
fast C++ statevector"] QS["qsim
grads: parameter-shift
single-precision (fp32)"] end subgraph CLOUD["Cloud / QPU  (async submit() · CostWarning · dry_run_info)"] BC["braket (cloud/QPU)
config switch via device_arn
QPU: shots>0 · grads=none
unless enable_gradients=True"] QR["qiskit-runtime (opt-in)
IBM Eagle/Heron + FakeBackends
grads=none unless enable_gradients=True
transpile + EstimatorV2/SamplerV2"] IQ["iqm
IQM Resonance + local noise-modelled devices
grads=none unless enable_gradients=True
counts-only, shots>0"] end PROTO --> NP & PL & QK & BL & CQ PROTO --> AER & TC & QL & QS PROTO --> BC & QR & IQ classDef proto stroke:#5c6bc0,stroke-width:2px; classDef sim stroke:#00897b,stroke-width:2px; classDef cloud stroke:#ec407a,stroke-width:2px; class PROTO proto; class NP,PL,QK,BL,CQ,AER,TC,QL,QS sim; class BC,QR,IQ cloud; ``` The `provider=` argument is **polymorphic** — the same slot accepts three shapes, each obvious at the call site: ``` flowchart LR A["provider='pennylane'"] -->|registry lookup| R["resolve_provider()"] B["provider=PennyLaneConfig(...)"] -->|config → make_backend| R C["provider=PennyLaneBackend(...)"] -->|explicit instance| R R --> P["a Provider"] classDef n stroke:#e65100,stroke-width:2px; class A,B,C,R,P n; ``` ______________________________________________________________________ ## 5. Interface dispatch: NumPy and PyTorch The **interface** decides what the top-level `Circuit(...)` returns. The default is numpy (framework-free, weights passed per call). `interface="torch"` returns a `TorchCircuit` nn.Module that *owns* its weights — but it's the same `CircuitSpec` and the same provider underneath. The torch machinery lives in the torch ring and registers into the core interface registry lazily, so a numpy-only environment never imports torch. ``` flowchart TB TOP["Circuit(blocks, observables, interface=, provider=)
top-level dispatcher"] TOP --> DISP{"interface?"} DISP -- "numpy (default)" --> NPC["core.circuit.Circuit
returns numpy array
weights passed per call
.jacobian() — no torch"] DISP -- "torch" --> TC["TorchCircuit (nn.Module, torch ring)
owns weights as nn.Parameter"] TC --> GM{"backend
gradient_method?"} GM -- "native (PennyLane)" --> RT["run_torch — autograd
flows through the device"] GM -- "adjoint or parameter_shift" --> AF["wrap numpy run() in a
torch.autograd.Function
backward → same jacobian ladder"] GM -- "none (hardware, unless
enable_gradients=True)" --> ERR[".backward() raises
a clear error"] NPC -.-> SHARED RT -.-> SHARED AF -.-> SHARED SHARED[("same CircuitSpec
+ provider underneath")] classDef top stroke:#5c6bc0,stroke-width:2px; classDef np stroke:#00897b,stroke-width:2px; classDef tc stroke:#e65100,stroke-width:2px; classDef branch stroke:#ab47bc,stroke-width:2px; class TOP top; class NPC np; class TC,RT,AF,ERR tc; class DISP,GM branch; class SHARED np; ``` ______________________________________________________________________ ## 6. The user-facing stack: Two ways to work with eigenkit `eigenkit` provides two complementary ways to develop quantum applications: building circuits directly or using high-level components. Both rely on the same underlying architecture, allowing users to choose the level of abstraction that best suits their needs. ### Direct circuit construction The circuit-building API provides full control over circuit design and execution. Users can define circuits in different ways: - **Predefined blocks.** Compose reusable `Block` objects to construct circuits declaratively, such as `Circuit(blocks=[Encoding, Variational, ...])`. - **Gate-by-gate construction.** Use `CircuitBuilder` to define custom gate sequences with methods such as `qc.rx(...)` and `qc.cnot(...)`. The `build()` method creates a complete circuit, while `to_block()` converts a sequence into a reusable block. - **Observable algebra.** Define Hamiltonians and measurement operators using expressions such as `Z(0)*X(1) + 0.5*Y(2)`. This API is a central part of `eigenkit`, not just an internal implementation detail. It provides the flexibility to develop custom quantum algorithms and serves as the foundation for the library's higher-level functionality. ### High-level components For common applications, `eigenkit` provides ready-to-use components that simplify development without requiring users to construct every circuit manually: - **Machine-learning estimators.** Scikit-learn-compatible models with familiar `fit()` and `predict()` methods. - **Quantum layers.** PyTorch-compatible layers that can be integrated directly into `nn.Sequential` and hybrid neural networks. - **Domain-specific algorithms.** Implementations of QAOA for optimization, VQE for quantum chemistry, HHL for linear systems, IQAE for amplitude estimation, and portfolio risk estimation. These components are built on the same circuit representation, reusable blocks, and variational optimization framework available through the direct API. This makes it possible to start with ready-made models and move to custom circuit implementations when greater flexibility is needed. ``` flowchart TB subgraph HIGH["High-level conveniences  — wrappers on top"] direction LR EST["sklearn estimators
HQNC · HQNR · QSVC · QSVR
RecurrentHQNC · RecurrentHQNR · CustomQuantum*
Generic* · QuantumTimeSeries* · QuantumReservoir*"] LAY["torch layers
Basic · Reupload
Ensemble · MinMaxNorm"] ALG["domain algorithms
QAOA (optimization)
VQE / UCCSD (chemistry)
HHL · IQAE · portfolio risk (algorithms)"] KER["QuantumKernel
QSVC / QSVR"] end subgraph MID["Rings"] TORCHM["torch/ — TorchCircuit nn.Module"] QMLM["qml/ — kernels + sklearn base"] DOMM["optimization/ + chemistry/ + algorithms/ (numpy)"] end subgraph DIRECT["Direct circuit building  — first-class core API"] direction LR BLK["block composition
Circuit(blocks=[Encoding, Variational, ...])"] BLD["imperative CircuitBuilder
qc.rx(...) · qc.cnot(...)
build() · to_block()"] OBS["observable algebra
Z(0)*X(1) + 0.5*Y(2)"] end CIRCUIT["Circuit (core)
CircuitSpec + Observable + Provider"] SPINE["minimize_expectation
(scipy variational spine)"] EST --> TORCHM EST --> QMLM LAY --> TORCHM KER --> QMLM ALG --> DOMM TORCHM --> CIRCUIT QMLM --> CIRCUIT DOMM --> CIRCUIT BLK --> CIRCUIT BLD -- "build()" --> CIRCUIT BLD -. "to_block()" .-> BLK OBS --> CIRCUIT DOMM --> SPINE CIRCUIT --> SPINE classDef user stroke:#e65100,stroke-width:2px; classDef mid stroke:#ec407a,stroke-width:2px; classDef direct stroke:#00897b,stroke-width:2px; classDef core stroke:#5c6bc0,stroke-width:2px; class EST,LAY,ALG,KER user; class TORCHM,QMLM,DOMM mid; class BLK,BLD,OBS direct; class CIRCUIT,SPINE core; ``` **The iceberg analogy.** High-level components such as estimators, layers, and algorithms are the visible part of `eigenkit`, providing convenient interfaces for common applications. Beneath them lies the more flexible circuit-building API, including reusable blocks, `CircuitBuilder`, and the observable algebra. These are all publicly accessible, allowing users to move from ready-made components to fully customised quantum circuits whenever they need more control. # Use with AI These docs are published in forms that AI assistants read directly, so a coding assistant can answer questions about eigenkit from the current documentation instead of from what it remembers. ## Copy a page The menu at the top right of every page copies the page as Markdown, opens its Markdown version, or starts a conversation about it in Claude or ChatGPT. The Markdown version of a page lives next to it: the page `https://docs.kit.eigenorbit.com/user-guide/encoding/` is also served as `https://docs.kit.eigenorbit.com/user-guide/encoding/index.md`. ## llms.txt - [`llms.txt`](https://docs.kit.eigenorbit.com/llms.txt): an index of every page with a link to its Markdown version, following [llmstxt.org](https://llmstxt.org/). - [`llms-full.txt`](https://docs.kit.eigenorbit.com/llms-full.txt): every page, the API reference included, in one file. ## MCP server The docs are also served by a [Model Context Protocol](https://modelcontextprotocol.io) server at `https://kit.eigenorbit.com/mcp`. It needs no account or key, and it gives the assistant two tools: - `search_docs`: find the pages that match a query. - `read_page`: read one page in full, as Markdown. ```bash claude mcp add --transport http eigenkit-docs https://kit.eigenorbit.com/mcp ``` Open the **Connectors** settings, click **+ Add**, then **Add custom connector**. Enter the name `eigenkit-docs` and the URL `https://kit.eigenorbit.com/mcp`, and choose **No sign in**. Add the server to `~/.cursor/mcp.json`, or to `.cursor/mcp.json` in a project: ```json { "mcpServers": { "eigenkit-docs": { "url": "https://kit.eigenorbit.com/mcp" } } } ``` Add the server to `.vscode/mcp.json`: ```json { "servers": { "eigenkit-docs": { "type": "http", "url": "https://kit.eigenorbit.com/mcp" } } } ``` # Changelog ## v1.0.0 (2026-10-13) ### Chores - **deps**: Refresh uv.lock ([#12](https://github.com/eigenorbit/eigenkit/pull/12), [`8a6874a`](https://github.com/eigenorbit/eigenkit/commit/8a6874a4746b13b3496b0c7a9b56f48daf2737d6)) ### Features - Eigenkit 1.0.0 ([#1](https://github.com/eigenorbit/eigenkit/pull/1), [`babce1f`](https://github.com/eigenorbit/eigenkit/commit/babce1fa8c8fffe9c3894dbd4d24b3d13870ba25)) # User Guide # Algorithms `eigenkit.algorithms` contains three standalone quantum algorithms. `HHL` solves linear systems with the algorithm of Harrow, Hassidim and Lloyd (HHL). `IQAE` runs Iterative Quantum Amplitude Estimation (IQAE). `PortfolioRiskEstimator` estimates portfolio risk and is built on IQAE. The first two are single-call solvers. You configure a solver in its constructor, call one method, and get back a frozen result dataclass. There is no `fit`/`predict` cycle and no fitted state to keep. `PortfolioRiskEstimator` works differently, because it is first fitted to a price history (see [below](#portfolioriskestimator-var-and-cvar)). All three default to `provider="numpy"`, the built-in simulator that every installation includes. Two of the modes of `IQAE`, a callable oracle and `shot_mode="provider"`, need `provider="pennylane"` and the `pennylane` extra. The [API reference](#api-reference) at the end of this page lists the full signatures and defaults, with notes on every argument. | Solver | Answers | Result carries | | ---------------------------------------------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | | [`HHL`](#hhl-linear-systems) | the solution `x` of `Ax = b` | `solution`, `euclidean_norm`, `residual`, `success_probability`, `readout_probability`, `raw_solution` | | [`IQAE`](#iqae-amplitude-estimation) | an unknown amplitude, with an interval | `estimation`, `confidence_interval`, `n_rounds`, `n_oracle_calls` | | [`PortfolioRiskEstimator`](#portfolioriskestimator-var-and-cvar) | the Value at Risk (VaR) and the Conditional Value at Risk (CVaR) | `var`, `cvar`, `tail_probability`, `confidence_interval`, `iqae_result` | Each result is a frozen dataclass, so its fields can be read but not changed. The theory behind these two algorithms is explained in the [HHL](https://docs.kit.eigenorbit.com/algorithms/hhl/index.md) and [IQAE](https://docs.kit.eigenorbit.com/algorithms/iqae/index.md) chapters of the Algorithms Explained section. ### What counts as a solver A solver inherits from BaseQuantumSolver. It has a `solve()` method that takes a problem and returns a result, with no training phase and no fitted state. Four classes are solvers: `HHL` and `IQAE` on this page, the Quantum Approximate Optimization Algorithm ([`QAOA`](https://docs.kit.eigenorbit.com/user-guide/optimization/index.md)) in `optimization`, and the Variational Quantum Eigensolver ([`VQE`](https://docs.kit.eigenorbit.com/user-guide/chemistry/index.md)) in `chemistry`. The base class is defined in `core` and not in `algorithms`, because `QAOA` and `VQE` depend only on `core`. ```python from eigenkit.core import BaseQuantumSolver from eigenkit import HHL isinstance(HHL(), BaseQuantumSolver) # True ``` `solve()` is an abstract method, so a solver class that does not implement it cannot be instantiated. The base class also stores the `provider` attribute for all four solvers. Each subclass sets its own default in its signature and passes it to the base class, and the default is `"numpy"` for all four. `PortfolioRiskEstimator` does not inherit from `BaseQuantumSolver`, by design. It follows the scikit-learn estimator pattern, with `fit()`, `estimate()` and `var_at()` and no `solve()` method, so `isinstance` returns `False` for it. This is the expected result when you check whether an object can be given a problem directly. ## HHL: Linear systems `HHL` solves `Ax = b`. If `A` is Hermitian, it is used directly, even when it is not positive semidefinite. Any other invertible square matrix is converted to the normal equations `A†Ax = A†b`, which have the same solution and a Hermitian system matrix. ```python import numpy as np from eigenkit import HHL A = np.array([[1.0, 0.5], [0.5, 1.5]]) b = np.array([1.0, 0.0]) result = HHL(n_eigenvalue_qubits=3, provider="numpy").solve(A, b) print(result.solution.real) # [ 1.1888 -0.3789] print(np.linalg.solve(A, b)) # [ 1.2 -0.4 ] the classical answer print(result.residual) # 0.026 print(result.success_probability) # 0.7282 ``` The solution is returned as a complex array because it is read from a statevector. Use `.real` when the system is real. The circuit that `solve()` builds has the same stages in both modes. `|b⟩` is prepared on the input register. Phase estimation writes the eigenvalues of `A` into the clock register, whose size is set by `n_eigenvalue_qubits`. The `1/x` rotation turns each eigenvalue into its reciprocal on the ancilla, and `QPE+`, the adjoint of quantum phase estimation (QPE), resets the clock register. The `mode` argument changes how the phase-estimation step is built, but not these stages. The answer is read from the branch where the ancilla is in the useful state and the clock register has returned to zero. This is why the backend must provide a statevector, and `readout_probability` measures the weight of that branch. `success_probability` counts only the ancilla, over every clock state. Two arguments control the accuracy. `n_eigenvalue_qubits` sets the size of the clock register, which determines how finely the spectrum is resolved. A finer resolution needs a larger circuit. The example above uses `3` qubits and gets within about `1 %` of the classical answer. `epsilon` is an eigenvalue cutoff. Eigenvalues below it are treated as noise and dropped, which prevents a nearly singular system from producing very large values. The evolution time `t` is chosen automatically from the spectrum, and we recommend keeping the automatic value. If `t` is too large, phase estimation aliases. The solver detects this and reports it, instead of returning a wrong answer. A third argument, `mode`, chooses which circuit is built. The default, `mode="reference"`, computes the eigenvalues classically and applies `e^{iAt}` as one dense unitary. It is exact and fast, but its cost does not reflect how HHL scales. `mode="honest"` builds the same algorithm from elementary gates instead. The evolution is Trotterised from the Pauli decomposition of `A`, and the evolution time comes from a Gershgorin bound, so no circuit parameter depends on an eigendecomposition. This mode needs at least two clock qubits, and `trotter_steps` (`"auto"` by default), `trotter_order` and `trotter_tolerance` control the Trotter grid. The [HHL page](https://docs.kit.eigenorbit.com/algorithms/hhl/index.md) compares the two modes, and the [worked example](https://docs.kit.eigenorbit.com/examples/hhl-honest/index.md) measures the cost of each one. The result has six fields. `residual` is the best measure of the quality of a run. It is the relative error `‖Ax − b‖ / ‖b‖`, computed against the original system, so it measures the answer itself and not the algorithm's own estimate of it. `success_probability` matters for anything beyond a simulator. It is the probability that the ancilla lands in the useful branch, so its reciprocal is roughly the number of times a real device would have to repeat the state preparation. `readout_probability` is the narrower physical rate: the probability that both the ancilla and the clock register land where the solution is read, which is what an estimate from shots needs. `raw_solution` holds the post-selected amplitudes produced by the circuit, before the classical rescaling. It is the only part of the output that comes directly from the quantum computation. The circuit reads the full statevector and post-selects, so the backend must provide a statevector. The numpy simulator, PennyLane `default.qubit` and `lightning.qubit`, and Qiskit Aer all do. Shot-based backends and quantum processing units (QPUs) raise `NotImplementedError`. ## IQAE: Amplitude estimation `IQAE` estimates `a = |⟨ψ₁|A|0⟩|²`, the probability that a state-preparation circuit ends in the marked state. The estimate comes with a confidence interval, so you do not have to infer one from repeated runs. The oracle is the state preparation `A` itself. It can be given as a `CircuitSpec` built with `CircuitBuilder`, or as a PennyLane callable `oracle(wires)`, which needs `provider="pennylane"`. **The last qubit is the ancilla.** The oracle must have no trainable weights and no inputs, and its number of qubits must equal `n_qubits`. ```python import numpy as np from eigenkit import CircuitBuilder from eigenkit import IQAE # RY(pi/3) on the ancilla prepares an amplitude of sin^2(pi/6) = 0.25 exactly. qc = CircuitBuilder(n_qubits=2) qc.ry(np.pi / 3, qubit=1) oracle = qc.build() result = IQAE(epsilon=0.02, alpha=0.05, n_qubits=2, random_state=0).solve(oracle) print(result.estimation) # 0.246 print(result.confidence_interval) # (0.2407, 0.2513) contains the true 0.25 print(result.n_rounds) # 4 print(result.n_oracle_calls) # 2600 ``` This is the operator that IQAE repeats. One repetition applies the state preparation `A` (the oracle you pass, drawn as `CU` in the figure), then a phase flip on the marked state, then `A†`, then the diffusion operator. Each repetition rotates more amplitude onto the marked state. The adaptive rule of Grinko et al. decides how the number of repetitions grows from one round to the next, and `n_oracle_calls` counts the applications of `A` and `A†` over the whole run. The Hadamard layer at the start comes from Grover search, where `A` prepares the uniform superposition. In IQAE, the initial state is whatever your oracle prepares. `epsilon`, `alpha` and `n_qubits` are required and have no default values. `epsilon` is the half-width of the interval that you are willing to accept, `alpha` is the probability that the interval is allowed to miss the true value, and `n_qubits` is the number of qubits of the oracle, including the ancilla. Set `random_state` whenever a run must be repeatable, because the shot noise is seeded from it. The result contains the estimate, its interval, and two cost counters. Use `n_oracle_calls` when comparing against classical sampling, because the advantage of the algorithm is its convergence rate: as `epsilon` gets smaller, the error decreases roughly as `1/n_oracle_calls`, compared with `1/√n` when drawing `n` classical samples. ## PortfolioRiskEstimator: VaR and CVaR `PortfolioRiskEstimator` takes a price history, fits a loss distribution to it, and reports the Value at Risk (VaR) and the Conditional Value at Risk (CVaR). The tail probability behind these numbers is estimated with `IQAE`, which is the quantum part of the method. By default, VaR and CVaR are computed classically from the fitted bins. A quantum bisection, described below, is optional. Not financial advice The estimates come from a model fitted to the price history you pass in, and they carry no guarantee. They are not investment advice, and they are not meant to be the only basis of a decision about risk. `fit` takes `X` of shape `(n_timesteps, n_assets)`, containing closing prices and not returns. Portfolio weights are optional, and all assets are weighted equally by default. ```python import numpy as np from eigenkit import PortfolioRiskEstimator rng = np.random.default_rng(0) returns = rng.normal(0.0005, 0.02, size=(250, 3)) prices = 100 * np.exp(np.cumsum(returns, axis=0)) estimator = PortfolioRiskEstimator(n_qubits=5, provider="numpy", random_state=0) estimator.fit(prices, weights=np.array([0.5, 0.3, 0.2])) risk = estimator.estimate(confidence_level=0.95) print(risk.var) # 0.0237 loses at most 2.37% on 95% of days print(risk.cvar) # 0.0262 average loss on the worst 5% print(risk.tail_probability) # 0.0659 IQAE estimate of the tail mass ``` `n_qubits` sets the resolution. One qubit is the ancilla and the others discretise the loss distribution, so `5` qubits give `16` bins. With coarse bins, the VaR changes in visible steps. If the result looks like a round number, increase `n_qubits` first. Two arguments decide how much of the computation is quantum. With the default `n_bisection_steps=0`, VaR and CVaR are the classical values from the bins, and only the tail probability is estimated on the circuit. A positive value runs a binary search that calls IQAE at every step to find the VaR threshold. `measure_tail_probability` controls the final IQAE call. Turn it off when you only need the classical values and do not want to run the circuit at all. The result reports `var` and `cvar` as fractions of the portfolio value, with the estimated `tail_probability` and a confidence interval for it. The interval applies to the tail probability, not to the VaR. The nested `iqae_result` contains the underlying IQAE estimate, and is `None` when the final IQAE call was skipped. `var_at()` is the array version of `estimate()`. It takes a list of confidence levels and returns the VaR at each one. For the fit above, `var_at([0.90, 0.95, 0.99])` returns `[0.0144, 0.0237, 0.0330]`, and the middle value matches `estimate(0.95).var`. The method is not called `predict()`, on purpose. In scikit-learn, `predict` takes samples from the same feature space that `fit` received, and this method takes confidence levels instead. Calling `predict()` raises an error that points to `var_at()`. A confidence level below `0.5` is valid (the VaR at `0.5` is the median loss), but such values also look like a series of returns or normalised features, so `var_at()` issues a warning in that case. Passing such an array is the only mistake that this method does not reject with an error. A price series is rejected because prices are not in `(0, 1)`, and a price matrix is rejected because it is not 1-D. ## API reference ### `eigenkit.algorithms` General-purpose quantum algorithms. This package contains three algorithms: - HHL solves linear systems with the algorithm of Harrow, Hassidim and Lloyd (HHL). - IQAE runs Iterative Quantum Amplitude Estimation (IQAE). - PortfolioRiskEstimator estimates the Value at Risk (VaR) and the Conditional Value at Risk (CVaR) of a portfolio with IQAE. `HHL` and `IQAE` inherit from BaseQuantumSolver, the base class for solvers that work in a single call and keep no fitted state. The Quantum Approximate Optimization Algorithm (`QAOA`) and the Variational Quantum Eigensolver (`VQE`) also inherit from it, in their own packages. The base class is defined in `core` because `QAOA` and `VQE` depend only on `core`. `PortfolioRiskEstimator` does not inherit from it, by design. It is a scikit-learn-style estimator, registered in the estimator registry of `eigenkit.qml` so that it can be saved and loaded. It therefore needs scikit-learn and is imported on first use; `HHL` and `IQAE` need only the core install. #### `HHL` Bases: `BaseQuantumSolver` Quantum linear solver based on the algorithm of Harrow, Hassidim and Lloyd (HHL). Solves `Ax = b` for any invertible square matrix `A`. If `A` is Hermitian, it is used directly, even when it is not positive semidefinite. Any other matrix is converted to the normal equations `A†Ax = A†b`, which have the same solution and a Hermitian, positive semidefinite system matrix. The cost of this conversion is that the condition number `κ` is squared. The circuit itself inverts the eigenvalues. It applies controlled `RY` rotations to the ancilla, with one fixed angle for each state of the clock register, so the reciprocal `1/λ` of each eigenvalue is encoded in the amplitudes without any classical division by the eigenvalues. The readout is different: it uses the simulator's full statevector and keeps only the part where the ancilla is in `|1⟩` and the clock register is in `|0⟩`. HHL produces the quantum state `|x⟩`, so reading a classical vector from it is only possible on a simulator. See HHLResult. Warning The backend must provide `statevector()` on its compiled circuit. The built-in numpy simulator, PennyLane `default.qubit` and `lightning.qubit`, and Qiskit Aer all do. A custom backend registered with register() must implement it, otherwise `solve()` raises `NotImplementedError`. **Scope and scaling.** HHL is efficient only when three conditions hold: `A` is a sum of a few short Pauli terms, `A` is well conditioned, and the state `|b⟩` can be prepared efficiently. - *Hamiltonian simulation.* `e^{iAt}` is Trotterised from the Pauli decomposition of `A`, so its cost follows the number of Pauli terms. This means sparsity in the Pauli basis, which is different from sparsity of the matrix. The module `algorithms/_hamiltonian_sim.py` explains what this means in practice. In short, a sparse matrix is not necessarily cheap to simulate here. - *State preparation.* `|b⟩` is prepared with AmplitudeEncoding, which is treated as an oracle of unit cost. Loading general amplitudes costs `O(N)` gates, so a real speedup assumes that `b` comes from a formula, from an earlier quantum routine, or from a quantum random-access memory (qRAM). - *Conditioning.* The condition number `κ` is never an input. It only bounds the achievable success probability and the size of the clock register. - *Inversion angles.* The gates are elementary (see `_uniformly_controlled_ry()`), so the circuit built in `"honest"` mode contains no unitary-matrix gate. However, the `2^n_l` angles, where `n_l` is the number of clock qubits, are listed one by one instead of being computed by the circuit. The classical description of the circuit therefore has size `Θ(2^n_l)`. The circuit can be built from elementary gates, but its description is not compact. Solvers for dense matrices, for example the one of Wossnig, Zhao and Prakash, reach a quadratic speedup when the spectral norm is bounded. They need a specialised memory architecture that this package does not provide. **Real and complex inputs.** For real `A` and `b`, the readout discards the imaginary part. The leading error of the Trotter eigenvectors enters the raw vector as a purely imaginary component, so discarding the imaginary part removes it. This matters at `trotter_order=1`, where it improves the convergence by one order: the measured error falls as `steps⁻²` for real inputs, and as `steps⁻¹` for a complex Hermitian `A` or a complex `b`, which keep that error. At `trotter_order=2`, the default, the first-order commutator already cancels by construction, so there is nothing left to remove, and real and complex inputs both converge as `steps⁻²`. This advantage was not designed. It depends on the test for real inputs in solve(), so a change to that test changes the convergence rate at order 1. Parameters: | Name | Type | Description | Default | | --------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `n_eigenvalue_qubits` | `int` | The number of qubits in the clock register. Eigenvalues are resolved into 2\*\*n_eigenvalue_qubits buckets. The value must be at least 1, and at least 2 in "honest" mode. More qubits give a finer resolution of the spectrum and a larger circuit. The default is 4. | `4` | | `t` | \`float | None\` | The evolution time for U = e^{iAt}. If given, it must be positive and finite. If None, it is chosen automatically: from the spectrum in "reference" mode, and from a Gershgorin bound in "honest" mode. The default is None. | | `epsilon` | `float` | The relative eigenvalue cutoff, in \[0, 1). For a Hermitian A, buckets whose estimated eigenvalue is below epsilon * λ_max are treated as zero. For the normal equations, the cutoff applies to the singular values instead, and it is implemented as epsilon² on the squared spectrum. If no eigenvalue is above the cutoff, solve() raises an error. The default is 0.01. | `0.01` | | `provider` | `ProviderSpec` | The backend to use: a registered name ("numpy", "pennylane", "qiskit" and so on), a config instance, or a backend object. It must provide statevector(). The default is "numpy". | `'numpy'` | | `mode` | `str` | How the circuit is built. This argument is keyword-only. "reference" (the default): the eigenvalues are computed classically with eigvalsh, e^{iAt} is a dense QubitUnitary computed with expm, and t is tuned to the known spectrum. This mode is fast and exact, but the O(N³) precomputation and the dense unitary mean that its cost does not follow the scaling of HHL. "honest": the circuit is built as it would be on hardware. No eigendecomposition sets any circuit parameter: t comes from a Gershgorin bound, the scaling constant C of the inversion comes from the resolution of the clock register (see \_honest_qpe_params()), and e^{iAt} is Trotterised into elementary gates. This mode requires n_eigenvalue_qubits >= 2. Input validation still runs an O(N³) rank check. This check makes the solver safer to use and is not part of the algorithm; see solve(). | `'reference'` | | `trotter_steps` | \`int | Literal['auto']\` | The number of Trotter steps for the base evolution time in "honest" mode. It is ignored in "reference" mode. With "auto", the default, the solver picks the smallest number of steps for which a computable bound on the Trotter drift ‖A_eff − A‖ stays below trotter_tolerance × C, which is below the eigenvalue resolution of the clock register. This matters because, with a fixed step size, the circuit is exact HHL for an effective matrix A_eff whose eigenvectors are rotated even when its eigenvalues are exact. That changes the direction of the answer while every check based on the spectrum reports success. An integer turns off the automatic choice, and the solver warns if the drift bound then exceeds C/2. Above that value, an extreme eigenvalue can be captured by the Nyquist bucket or wrap around the branch cut. The automatic choice is limited by a gate budget, and the solver says so when the limit is reached. The default is "auto". | | `trotter_order` | `int` | The Trotter order, 1 or 2. The default is 2. The symmetric product of order 2 is a palindrome, so the first-order commutator cancels, the error falls from O(τ) to O(τ²), and A_eff stays real for a real A. At order 1, A_eff picks up Y-type terms, and a real system returns a complex state. Order 1 is kept for cost comparisons. | `2` | | `trotter_tolerance` | `float` | The target for trotter_steps="auto". This argument is keyword-only. The drift bound must not exceed trotter_tolerance × C. The default, 0.25, keeps the drift at a quarter of a bucket, below the C/2 threshold at which the Nyquist bucket can capture an eigenvalue. At order 2, the cost grows as steps ∝ 1/√tolerance. | `_AUTO_TROTTER_TOLERANCE` | Raises: | Type | Description | | ------------ | ---------------------------------------------------------------------------------------------------------- | | `ValueError` | If n_eigenvalue_qubits is below 1, if t is not a positive finite number, or if epsilon is outside \[0, 1). | Example This example solves a `2 × 2` Hermitian system: ```python import numpy as np from eigenkit import HHL A = np.array([[2, 1], [1, 2]], dtype=float) b = np.array([1, 0], dtype=float) result = HHL(n_eigenvalue_qubits=6).solve(A, b) print(result.solution) print(result.residual) ``` This example solves the same system on a faster simulator: ```python from eigenkit import HHL, PennyLaneConfig solver = HHL( n_eigenvalue_qubits=6, provider=PennyLaneConfig(device_name="lightning.qubit"), ) result = solver.solve(A, b) ``` ##### `solve(A, b)` Solve the linear system `Ax = b`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ----------------------------------------------------------------------------------------------------------------------- | ---------- | | `A` | `ndarray` | A real or complex square matrix, which must be invertible. A non-Hermitian matrix is converted to the normal equations. | *required* | | `b` | `ndarray` | The right-hand side, of length A.shape[0]. | *required* | Returns: | Type | Description | | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | `HHLResult` | An HHLResult. Its solution is read from the simulator's statevector and completed with a classical least-squares rescaling. See the notes on HHLResult. | Raises: | Type | Description | | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | `ValueError` | If A is not square or is singular, if b has the wrong length, is zero or is not finite, or if t causes aliasing in quantum phase estimation (QPE). | | `NotImplementedError` | If the backend does not implement statevector(), or if it returns something else, such as a density matrix. | Note Validation runs one `O(N³)` singular value decomposition (SVD) to reject a singular `A` and to compute a condition number for the warnings. No quantity derived from it reaches `t`, `C`, the rotation angles or the number of Trotter steps, and changing it would leave the circuit exactly the same. `mode="honest"` therefore keeps its defining property, but `solve()` itself still does `O(N³)` work. #### `HHLResult` Bases: `ValueEquality` Output of HHL.solve(). The result is read-only. Assigning to a field raises `FrozenInstanceError`, and writing into an array field raises `ValueError`. This prevents a mistyped `=` (instead of `==`) or an in-place edit from leaving a result whose scalar fields describe a different run from the arrays it holds. To get a writable copy of an array, use `np.array(result.field)`. Results compare by value, including their arrays. They are not hashable, because a result is a collection of numbers and not a key, and a correct hash of a float array is expensive to compute. Attributes: | Name | Type | Description | | --------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | `solution` | `ndarray` | The best-fit x with Ax ≈ b. It is float64 for real inputs, and complex128 when A or b has a non-zero imaginary part. | | `euclidean_norm` | `float` | The norm ‖solution‖ of the returned solution. It is computed after the classical least-squares rescaling, so it is not a norm that HHL itself measures. | | `residual` | `float` | The relative error ‖Ax − b‖ / ‖b‖, computed with the original A and b. | | `success_probability` | `float` | The probability that the ancilla is in | | `readout_probability` | `float` | The probability of the joint event from which the solution is read: the ancilla in | | `raw_solution` | \`ndarray | None\` | Note HHL produces the quantum state `|x⟩`, not a vector of numbers. The full array in this result is read from the simulator's statevector, which only a simulator can provide. On hardware, only expectation values can be measured, and extracting all `N` components of `|x⟩` costs `O(N)` measurements, which cancels the speedup. In practice, `|x⟩` is passed to another quantum routine, or used to estimate one observable. Recovering the sign of `1/λ` needs a separate protocol. All of this applies to both values of `mode`. Warning `solution` is completed by two classical steps that hardware cannot perform. For real inputs, the imaginary part is discarded. The magnitude and the sign then come from a least-squares projection, `x = raw · ⟨A·raw, b⟩ / ‖A·raw‖²`, which uses the full classical `A` and `b`. Only the direction of `raw` comes from the quantum computation. The rescaling can change `residual` by an order of magnitude, and for an indefinite `A` its sign correction is not reliable, because `⟨Ax, b⟩ ≥ 0` does not imply `⟨x, A⁻¹b⟩ ≥ 0`. ##### `__post_init__()` Make the array fields read-only. Freezing the dataclass prevents a field from being reassigned, but it does not prevent writing into the array that the field holds, and a result that has been written into still reports the scalar values of the original run. The result therefore keeps its own copy, so the array it was built from can still be changed by the caller. To get a writable copy, use `np.array(result.solution)`. Both array fields are frozen. `raw_solution` is optional, and `freeze_arrays` would turn a `None` into a zero-dimensional object array, so it is only frozen when the run recorded one. #### `IQAE` Bases: `BaseQuantumSolver` Iterative Quantum Amplitude Estimation (IQAE). Estimates `a = |⟨ψ₁|A|0⟩|²` to an additive precision `epsilon`, with a failure probability of at most `alpha`. It uses the Grover operator `Q = A S₀ A† S_ψ₁`, built from the oracle `A` that you provide. The oracle can be given in two forms: - A **PennyLane callable** `oracle(wires)`, which applies the circuit `A` in place. The last wire (`wires[-1]`) must be the ancilla qubit: ```python def my_oracle(wires): qml.RY(theta, wires=wires[0]) qml.CNOT(wires=[wires[0], wires[-1]]) ``` The matrix of the circuit is extracted internally with `qml.matrix`, so this form requires `provider="pennylane"` and the `pennylane` extra. - A CircuitSpec, which works on any backend that supports the `QubitUnitary`, `X`, `MCZ` and `Z` gates. With a provider other than PennyLane, pass `shot_mode="synthetic"` or `shot_mode="auto"`, because `shot_mode="provider"` works only with PennyLane. Parameters: | Name | Type | Description | Default | | -------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | `epsilon` | `float` | The additive precision to reach, that is, the half-width of the final confidence interval for a. It must be in (0, π/8). | *required* | | `alpha` | `float` | The total failure probability. The algorithm applies a Bonferroni correction, so the confidence interval of each round is valid at level alpha / T, where T = ceil(log2(π / (8 * epsilon))) is the maximum number of rounds. It must be in (0, 1). | *required* | | `n_qubits` | `int` | The total number of qubits, including the ancilla. | *required* | | `provider` | `ProviderSpec` | The backend to use: a registered name ("numpy", "pennylane", "qiskit" and so on), a config instance, or a backend object. The default is "numpy", the built-in simulator. | `'numpy'` | | `n_shots` | `int` | The number of measurement shots per round. With shot_mode="synthetic", which is used on analytic backends, it sets the size of a seeded binomial draw on top of the exact probability of the ancilla. With shot_mode="provider" or "auto", it is the number of samples per round on the hardware or simulator, unless the provider config sets its own shots, which takes precedence. For example, a device configured for 50 shots samples 50 times, whatever the value of n_shots. To control sampling on those backends, set shots on the provider config. The default is 100. | `100` | | `shot_mode` | `ShotMode` | How the ancilla measurements are obtained. With "auto", the default, the provider's shot count is used when the backend config sets a finite shots, and synthetic binomial noise is used otherwise. "provider" always samples from the backend. "synthetic" always uses exact expectation values plus seeded binomial noise. | `'auto'` | | `min_ratio` | `float` | The minimum ratio K\_{i+1} / K_i between the Grover powers of consecutive rounds. It must be greater than 1. | `2.0` | | `random_state` | \`int | None\` | The seed of the random number generator used for the shot noise. An integer gives reproducible results, and None uses a non-deterministic seed. | Example ```python import numpy as np from eigenkit import CircuitBuilder from eigenkit.algorithms import IQAE # RY(pi/3) on the ancilla prepares an amplitude of sin^2(pi/6) = 0.25. qc = CircuitBuilder(n_qubits=2) qc.ry(np.pi / 3, qubit=1) oracle = qc.build() iqae = IQAE(epsilon=0.02, alpha=0.05, n_qubits=2, random_state=0) result = iqae.solve(oracle) print(result.estimation) # 0.246 print(result.confidence_interval) # (0.2407, 0.2513), which contains 0.25 print(result.n_oracle_calls) # 2600 ``` ##### `solve(oracle)` Estimate the amplitude `a = |⟨ψ₁|A|0⟩|²`. Parameters: | Name | Type | Description | Default | | -------- | ---------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `oracle` | \`Callable | CircuitSpec\` | Either a PennyLane callable oracle(wires) that builds the state-preparation circuit A, or a CircuitSpec produced by CircuitBuilder. In both cases, the last qubit is treated as the ancilla. | Returns: | Type | Description | | ------------ | --------------------------------------------------------------------------------------- | | `IQAEResult` | An IQAEResult with the point estimate, the confidence interval and the resource counts. | Raises: | Type | Description | | ------------ | ------------------------------------------------------------------------------------------------- | | `ValueError` | If the selected provider does not support the gates that IQAE needs (QubitUnitary, X, MCZ and Z). | #### `IQAEResult` Bases: `ValueEquality` Outcome of a completed Iterative Quantum Amplitude Estimation (IQAE) run. The result is read-only. Assigning to a field raises `FrozenInstanceError`, so a mistyped `=` (instead of `==`) cannot change a result. This type holds no arrays, so nothing else needs protecting. Results compare by value. They are not hashable, so that all result types behave in the same way: the types that hold arrays cannot be hashed both correctly and efficiently. Attributes: | Name | Type | Description | | --------------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `estimation` | `float` | The point estimate of the amplitude a. | | `confidence_interval` | `tuple[float, float]` | The (lower, upper) Clopper-Pearson interval at the requested confidence level 1 - alpha. | | `n_rounds` | `int` | The number of rounds that the run executed. It can also be read as n_iterations, the name that the other result types use. | | `n_oracle_calls` | `int` | The total number of oracle calls over all rounds. | | `success` | `bool` | Whether the interval reached the requested precision. This is the same test that ends the loop over rounds, and the one reported by the warning that the run did not converge. False means that the run stopped at max_rounds with an interval still wider than 2 * epsilon. | ##### `n_iterations` The number of rounds, under the name that the other results use. `n_rounds` is the name that IQAE uses, and it stays available. This property lets one loop read the number of iterations from any result. Returns: | Type | Description | | ----- | ------------------------------------------- | | `int` | The number of rounds that the run executed. | #### `PortfolioRiskEstimator` Bases: `BaseQuantumEstimator` Quantum estimator of portfolio risk. It estimates the Value at Risk (VaR) and the Conditional Value at Risk (CVaR) of a portfolio. The estimator can use IQAE to estimate the tail probability on a quantum circuit. The oracle for IQAE is built from the fitted return distribution with amplitude encoding. Note This is not a standard scikit-learn regressor. The risk at a confidence level comes from `estimate()` and `var_at()`, which take confidence levels and not feature vectors, and the estimator has no meaningful scikit-learn `score()`. It inherits only from BaseQuantumEstimator. Not financial advice The estimates come from a model fitted to the price history you pass in, and they carry no guarantee. They are not investment advice. Parameters: | Name | Type | Description | Default | | -------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- | | `n_qubits` | `int` | The total number of qubits, including the ancilla. The loss distribution is discretised into 2^(n_qubits - 1) bins. The minimum is 2: one data qubit and the ancilla. | `6` | | `epsilon` | `float` | The additive precision of the IQAE estimate of the tail probability. | `0.01` | | `alpha` | `float` | The failure probability of IQAE. The confidence level is 1 − alpha. | `0.05` | | `horizon_days` | `int` | The investment horizon, in trading days, used to scale daily log-returns to the target horizon. The default is 1, which gives daily risk measures. | `1` | | `provider` | `ProviderSpec` | The backend that IQAE uses: a registered name ("pennylane", "qiskit", "braket" and so on), a config instance, or a backend object. The default is "numpy". Note The Grover operator built internally uses the adjoint of an AmplitudeEncoding block, so the provider must support the adjoint of a state preparation and provide a statevector. The built-in "numpy" simulator and PennyLane both do, and their results agree to the last digit. A backend that does not raises an error when the operator is built. Calling IQAE directly with a CircuitSpec based on QubitUnitary avoids this requirement. | `'numpy'` | | `n_shots` | `int` | The number of shots per IQAE round. The default is 100. | `100` | | `random_state` | \`int | None\` | The seed that makes the results reproducible. | | `n_bisection_steps` | `int` | The number of binary-search steps used to locate the VaR with quantum estimates of the tail probability. With 0, the default, the VaR and CVaR are computed classically from the fitted bins. A positive value calls \_quantum_var_bisection(), which refines the threshold by running IQAE at each step of the bisection. | `0` | | `measure_tail_probability` | `bool` | When True, the default, estimate() runs IQAE once after the VaR and CVaR are computed, and reports the quantum estimate of the tail in tail_probability and confidence_interval. Set it to False to skip that final IQAE call when only the classical VaR and CVaR are needed and n_bisection_steps=0. | `True` | | `distribution` | `str` | The model of the loss distribution that fit() uses. "gaussian" (the default) fits the mean μ and the standard deviation σ of the scaled log-returns, and integrates the Gaussian cumulative distribution function (CDF) over each bin. The distribution is truncated at ±6σ. A VaR beyond that point is capped at the limit, and a RuntimeWarning is issued. "empirical" places each historical return in the nearest bin of equal width, without assuming a parametric form. "t" fits a Student-t distribution by maximum-likelihood estimation, and integrates its CDF over each bin. It has heavier tails than the Gaussian. | `'gaussian'` | Example ```python import numpy as np from eigenkit.algorithms import PortfolioRiskEstimator rng = np.random.default_rng(0) returns = rng.normal(0.0005, 0.02, size=(250, 3)) prices = 100 * np.exp(np.cumsum(returns, axis=0)) estimator = PortfolioRiskEstimator( n_qubits=5, provider="numpy", random_state=0 ) estimator.fit(prices, weights=np.array([0.5, 0.3, 0.2])) risk = estimator.estimate(confidence_level=0.95) print(risk.var) # 0.0237: at most a 2.37% loss on 95% of days print(risk.cvar) # 0.0262: the average loss on the worst 5% print(risk.tail_probability) # 0.0659: the IQAE estimate of the tail mass ``` ##### `distribution_params` Summary statistics of the fitted return distribution. The value is `None` if `fit()` has not been called. Otherwise it always contains `"mu"` (the mean) and `"sigma"` (the standard deviation) of the scaled log-returns, whatever the `distribution` model. For `"t"`, the degrees of freedom are not stored here, because they are an intermediate result of the maximum-likelihood fit and not part of the state of the estimator. ##### `is_fitted` Whether `fit()` has completed successfully. ##### `estimate(confidence_level=0.95, *, _seed_seq=None)` Estimate the VaR and CVaR at `confidence_level`. The VaR and CVaR come from the discretised loss distribution. When `n_bisection_steps > 0`, the VaR is refined by a bisection that uses IQAE. Otherwise, the classical quantiles of the bins are used. Unless `measure_tail_probability=False`, a final IQAE run estimates `P(loss ≥ VaR)`, for checking and diagnostics. Parameters: | Name | Type | Description | Default | | ------------------ | -------------- | ---------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `confidence_level` | `float` | The probability level of the risk measures, for example 0.95 for the 95 % VaR. It must be in (0, 1). | `0.95` | | `_seed_seq` | \`SeedSequence | None\` | A private override of the random seed, used by var_at() so that the shot noise of the IQAE runs for different confidence levels is independent. The default is None, which derives the seed from random_state. | Returns: | Type | Description | | --------------------- | ---------------------- | | `PortfolioRiskResult` | A PortfolioRiskResult. | Raises: | Type | Description | | -------------- | -------------------------------------------------------------------------------- | | `RuntimeError` | If fit() has not been called. | | `ValueError` | If the selected provider cannot compile the oracle for the tail that IQAE needs. | ##### `fit(X, y=None, *, weights=None)` Fit the return distribution to historical price data. Parameters: | Name | Type | Description | Default | | --------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------- | | `X` | `ndarray` | A 2-D array of shape (T, n_assets) with daily closing prices, where T must be at least 2. A 1-D array of length T is accepted for a portfolio with a single asset. | *required* | | `y` | \`ndarray | None\` | Ignored, because this estimator is unsupervised. It is present for compatibility with the scikit-learn interface. | | `weights` | \`ndarray | None\` | A 1-D array of portfolio weights that sum to 1. If it is not given, all assets have equal weights. | Raises: | Type | Description | | ------------ | -------------------------------------------------------------------------------------------------------------------------------------- | | `ValueError` | If distribution is not one of the supported values. This can happen when distribution is changed with set_params() after construction. | ##### `predict(X)` Not supported. Use `var_at()` instead. The estimator base class requires a `predict` method. In scikit-learn, `predict` takes samples from the same feature space that `fit` received, but the risk at a confidence level is a different question, which `var_at()` answers. Parameters: | Name | Type | Description | Default | | ---- | --------- | ----------- | ---------- | | `X` | `ndarray` | Ignored. | *required* | Raises: | Type | Description | | --------------------- | --------------------------------------- | | `NotImplementedError` | Always. The message points to var_at(). | ##### `var_at(confidence_levels)` Return the VaR at each confidence level. This is the array version of `estimate()`. It runs IQAE once for each level and returns only the `var` of each result. Parameters: | Name | Type | Description | Default | | ------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ---------- | | `confidence_levels` | `ndarray` | A 1-D array of confidence levels in (0, 1), for example [0.95, 0.99]. A scalar or an (n, 1) column is also accepted, and it is read as n levels. | *required* | Returns: | Type | Description | | --------- | ------------------------------------------------------------------------------------------- | | `ndarray` | An array of VaR values, as fractions of the portfolio value lost, with one value per level. | Raises: | Type | Description | | ---------------- | ------------------------------------------------------------- | | `ValueError` | If the argument is not 1-D after a single column is squeezed. | | `NotFittedError` | If fit() has not been called. | Warns: | Type | Description | | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `UserWarning` | If any level is below 0.5. Such a level is valid, but values below 0.5 are also what a price series or a feature matrix looks like, and this warning catches the mistake of passing one of those. | Warning Each level runs IQAE once, so `n` levels need `n` separate quantum circuits. For a single risk measure, call `estimate()` instead. Example ```python estimator.var_at([0.90, 0.95, 0.99]) ``` #### `PortfolioRiskResult` Bases: `ValueEquality` Outcome of a PortfolioRiskEstimator run. The result is read-only. Assigning to a field raises `FrozenInstanceError`, so a mistyped `=` (instead of `==`) cannot change a result. This type holds no arrays, so nothing else needs protecting. Results compare by value. They are not hashable, so that all result types behave in the same way: the types that hold arrays cannot be hashed both correctly and efficiently. Attributes: | Name | Type | Description | | --------------------- | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | `var` | `float` | The Value at Risk (VaR) at the requested confidence_level, as a fraction of the portfolio value lost. For example, 0.12 means a loss of 12 %. | | `cvar` | `float` | The Conditional Value at Risk (CVaR), also called Expected Shortfall, at the same confidence level. | | `tail_probability` | `float` | The IQAE point estimate of P(loss ≥ VaR). | | `confidence_interval` | `tuple[float, float]` | The (lower, upper) Clopper-Pearson interval for the tail probability, at level 1 - alpha. | | `iqae_result` | \`IQAEResult | None\` | # Benchmark `eigenkit.benchmark` compares quantum and classical models. A single call to `compare()` trains several estimators on one dataset with several random seeds, and returns a `BenchmarkResult`. The result can show the scores as a formatted table, plot them as grouped bar charts, and export them as a pandas DataFrame. The benchmark needs the `benchmark` extra, which installs matplotlib, pandas and scikit-learn: `pip install eigenkit[benchmark]`. The quantum models that you compare need their own extras: `qml` for `QSVC` and `QSVR`, and `torch` for `HQNC`, `HQNR` and the other differentiable estimators. By default, named datasets are split into `70 %` for training, `15 %` for validation and `15 %` for testing. The metrics are computed on the test split. The validation split is passed only to estimators whose `fit()` method accepts `X_val` and `y_val`. `compare()` repeats the split into training, validation and test sets for each seed. It does not use k-fold cross-validation. For k-fold cross-validation, call scikit-learn directly, for example with `cross_val_score` or `GridSearchCV(cv=...)`. ```python from eigenkit import QSVC, HQNC from eigenkit.benchmark import compare from sklearn.svm import SVC from sklearn.ensemble import RandomForestClassifier result = compare( models=[ QSVC(feature_map="Angle", reps=2), HQNC( in_features=4, depth=1, epochs=3, show_progress=False, ), SVC(C=0.1), RandomForestClassifier(n_estimators=20), ], dataset="iris", metrics=["accuracy", "f1"], n_seeds=3, validation_size=0.15, test_size=0.15, ) print(result.table()) result.plot(save_path="comparison.png") result.plot_history(save_path="losses.png") ``` The run used for this guide produced the following table: ```text ┌────────────────────────┬───────────────┬───────────────┐ │ Model │ Accuracy │ F1 │ ├────────────────────────┼───────────────┼───────────────┤ │ QSVC │ 0.989 ± 0.019 │ 0.989 ± 0.019 │ │ HQNC │ 0.389 ± 0.096 │ 0.242 ± 0.131 │ │ SVC │ 0.911 ± 0.051 │ 0.909 ± 0.053 │ │ RandomForestClassifier │ 0.967 ± 0.033 │ 0.966 ± 0.034 │ └────────────────────────┴───────────────┴───────────────┘ ``` Each cell shows the mean and the standard deviation across seeds, as `mean ± std`. ## Supported models Each entry in `models` must be one of the following: 1. An **eigenkit estimator**, that is, a subclass of `BaseQuantumEstimator`, `BaseQuantumClassifier` or `BaseQuantumRegressor`, such as `HQNC`, `QSVC` or `CustomQuantumClassifier`. 1. A **built-in scikit-learn estimator**, that is, a class from the `sklearn` package, such as `SVC`, `RandomForestClassifier` or `LinearRegression`. Models from other libraries are not accepted yet. This includes XGBoost, LightGBM and custom `BaseEstimator` subclasses defined outside `sklearn`. Every model must implement `fit(X, y)` and `predict(X)`. Internally, `compare()` uses `sklearn.base.clone` to make a fresh copy of each model for every seed. ## `compare()` `models` is the only required argument. Every other argument has a default value. | Argument | Default | Description | | ------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | `models` | required | A list of estimator instances, or a dict that maps display names to estimators. | | `dataset` | `"breast_cancer_wisconsin"` | A dataset name from the registry, or a dict with `X_train`, `y_train`, `X_test` and `y_test`. | | `metrics` | `None` | The names of the metrics to compute, or `custom_metric(...)` specifications. `None` selects the default metrics for the task, listed below. | | `n_seeds` | `5` | The number of seeds to average over. | | `test_size` | `0.15` | The fraction of the data kept for the final test metrics. | | `show_progress` | `False` | Whether to print progress: the current seed, the current model and the estimated time remaining. | | `save_dir` | `None` | If set, the directory where each fitted model is saved after every seed (see below). | | `store_predictions` | `False` | If `True`, the result keeps `y_true`, `y_pred` and, when available, `y_prob` for the last seed. | | `validation_size` | `0.15` | The fraction of the data kept for validation. | If you call `compare(models)` with no other argument, the benchmark uses the `breast_cancer_wisconsin` dataset. The default `test_size` here is `0.15`, which is different from the `0.2` that [`load_dataset`](https://docs.kit.eigenorbit.com/user-guide/datasets/index.md) uses. Set `show_progress=True` to see the progress of the run: which model is running and how many runs are left. By default nothing is printed, which keeps notebooks and continuous integration (CI) logs quiet. Pass a dict to choose the names that appear in tables, plots, progress messages and saved file names. This is useful when you compare several instances of the same estimator class: ```python from sklearn.svm import SVC from eigenkit import QSVC from eigenkit.benchmark import compare result = compare( models={ "Quantum SVC": QSVC(feature_map="Angle", reps=1, probability=True), "Regularized SVC": SVC(C=0.3), "Flexible SVC": SVC(C=3.0), }, dataset="iris", metrics=["accuracy"], n_seeds=1, ) print(list(result.fitted_models)) ``` ```text ['Quantum SVC', 'Regularized SVC', 'Flexible SVC'] ``` Dict keys must not have whitespace at the start or the end, and must not contain control characters. When `save_dir` is set, the keys are also used as prefixes of file names. In that case, they must not end with a full stop and must not contain any of these characters: `<>:"/\|?*`. Use a different key for each model. If the same key appears twice in a Python dict literal, Python keeps only the last model before `compare()` receives the dict. If you pass a dataset dict that is already split and you have a validation set, include `X_val` and `y_val`. If they are left out, `compare()` uses only the training and test sets that you provided. ### Saving fitted models When `save_dir` is set, each model is written to a file after `fit()`: | Model type | Output file | | --------------------- | -------------------------------------------------------- | | eigenkit | `{ModelName}_seed{N}.eigenkit`, written with `save()` | | built-in scikit-learn | `{ModelName}_seed{N}.joblib`, written with `joblib.dump` | ```python compare( models=[HQNC(in_features=4, depth=2), SVC()], dataset="iris", n_seeds=2, save_dir="runs/iris_benchmark/", ) # runs/iris_benchmark/HQNC_seed0.eigenkit, HQNC_seed1.eigenkit, SVC_seed0.joblib, ... ``` `ModelName` is the name of the model in the benchmark. When the same class appears more than once in a list, the names are numbered (`SVC_0`, `SVC_1`). When `models` is a dict, `ModelName` is the dict key. To load an eigenkit checkpoint, use `eigenkit.benchmark.load_checkpoint()` or the estimator's `load()` method, for example `HQNC.load(...)`. To load a scikit-learn checkpoint, use `eigenkit.benchmark.load_checkpoint()` or `joblib.load(...)`. `.joblib` files use pickle internally, so only load them from sources that you trust. See [Model persistence](https://docs.kit.eigenorbit.com/user-guide/model-persistence/index.md) for more details. ### Classification metrics The available metrics are `accuracy`, `f1` (the F1 score), `precision`, `recall`, `roc_auc` (the area under the receiver operating characteristic curve), `fit_time` and `predict_time`. When `metrics` is `None`, the benchmark computes `accuracy`, `f1`, `fit_time` and `predict_time`. `roc_auc` needs `predict_proba`. `QSVC` with `probability=True` and `HQNC` both provide it. For a model without `predict_proba`, requesting `roc_auc` issues a `UserWarning`, and the metric is skipped. ### Regression metrics The available metrics are `r2` (the coefficient of determination), `mae` (the mean absolute error), `rmse` (the root mean squared error), `mse` (the mean squared error), `fit_time` and `predict_time`. The type of task is taken from the first model: a model that inherits from `RegressorMixin` makes it a regression task. When `metrics` is `None`, the benchmark computes `r2`, `mae`, `mse`, `fit_time` and `predict_time`. `rmse` is not computed by default, so request it by name if you need it. ### Custom metrics A custom metric must be wrapped with `custom_metric(fn, *, direction, name=None)`. The function receives `(y_true, y_pred)`. `direction` states whether higher or lower values are better. `name` sets the column label, which otherwise comes from the name of the function: ```python from eigenkit.benchmark import custom_metric def error_rate(y_true, y_pred): return (y_true != y_pred).mean() def match_rate(y_true, y_pred): return (y_true == y_pred).mean() metrics = [ custom_metric(error_rate, direction="lower"), custom_metric(match_rate, direction="higher"), ] ``` ## `BenchmarkResult` `compare()` returns a `BenchmarkResult`. It holds the raw values for every seed, the fitted copies of the models, and methods to display, analyse and use the results. ### Attributes | Attribute | Type | Description | | ------------------- | ----------------- | --------------------------------------------------------------------------------------------------- | | `raw` | `list[dict]` | One row per value, with the keys `"model"`, `"seed"`, `"metric"` and `"value"`. | | `fitted_models` | `dict[str, list]` | The fitted estimators for each model name, one per seed. | | `predictions` | `dict[str, dict]` | The `y_true`, `y_pred` and `y_prob` of the last seed for each model, when `store_predictions=True`. | | `model_artifacts` | `dict[str, list]` | The paths of the checkpoint files written to `save_dir`, grouped by model name. | | `loss_curves` | `dict[str, list]` | The training and validation loss curves for each seed, when the fitted estimators record them. | | `metric_directions` | `dict[str, str]` | The direction, `"higher"` or `"lower"`, of every metric in the run. | ### Methods | Method | Description | | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `table(show_significance=False)` | A text table with `mean ± std` for each model and metric. It needs no optional dependency. With `show_significance=True`, the best model is marked with `*` or `**` when a test against another model gives `p < 0.05` or `p < 0.01`. | | `plot(save_path=None, theme="light")` | A grouped bar chart with error bars. `theme` is `"light"` or `"dark"`. It needs `matplotlib`. | | `plot_history(save_path=None, theme="light", seed="last")` | The training and validation loss curves of the models that record them. `seed` chooses the run to draw: `"last"` or an integer index. It needs `matplotlib`. | | `to_dataframe()` | A DataFrame with one row per seed and metric. It needs `pandas`. | | `best_model(metric=None, seed="last")` | The fitted estimator with the best mean on `metric`, taking the direction of the metric into account. | | `predict(X, metric=None)` | A shortcut for `best_model(metric=metric).predict(X)`. | | `inference(X, models=None, seed="last")` | The predictions and probabilities of all fitted models, or of the selected ones. | | `significance_test(metric, method="wilcoxon")` | A matrix of p-values for each pair of models, computed from the scores of each seed. `method="ttest"` is also supported. It needs `pandas` and `scipy`. | | `confusion_matrix(model_name=None, **kwargs)` | A scikit-learn confusion matrix computed from the stored predictions. `**kwargs` are passed to `sklearn.metrics.confusion_matrix`. It needs `store_predictions=True`. | | `save(path)` | Writes the numeric results to a JSON file. The fitted estimators are not saved. | | `BenchmarkResult.load(path)` | Restores a result from a JSON file written by `save()`. | The significance markers come from a Wilcoxon signed-rank test, which needs at least `6` seeds shared by the two models. With the default `n_seeds=5`, `table(show_significance=True)` cannot mark any model, and it issues a warning instead. To use plotting, DataFrame export and significance tests, install the optional extra: ```bash pip install eigenkit[benchmark] ``` `plot_history()` draws the models that record a loss history, and warns about the ones that do not. For example, `HQNC` produces training and validation curves, while `SVC` appears only in the metric table and the bar chart. ### Accessing fitted models Every fitted copy produced by `compare()` is stored in `result.fitted_models`, a `dict[str, list[estimator]]` keyed by the name of the model in the benchmark. With a list of models, the names come from the class (`SVC`, or `SVC_0` and `SVC_1` when the same class appears more than once). With a dict, the keys are exactly the names that you passed. Each value is a list of fitted copies, one per seed. ```python print(list(result.fitted_models)) # ['Quantum SVC', 'Regularized SVC', 'Flexible SVC'] quantum_fits = result.fitted_models["Quantum SVC"] model_seed0 = quantum_fits[0] ``` #### `best_model(metric, seed)` Returns the fitted estimator with the best mean on `metric`. When `metric` is not given, the primary metric is chosen automatically. It is the first metric in the results that is not a timing metric, usually `"accuracy"` for classification and `"r2"` for regression with the default metrics. ```python winner = result.best_model() # auto-selects metric winner = result.best_model(metric="f1", seed=0) # explicit metric, seed 0 winner = result.best_model(metric="r2") # explicit for regression ``` | Parameter | Default | Description | | --------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | `metric` | `None` | The metric used for ranking. `None` selects the primary metric of the task. Built-in metrics and custom metrics use their registered direction. | | `seed` | `"last"` | The seed whose copy is returned: `"last"`, or a 0-based `int`. | #### `predict(X, metric)` This is a shortcut for `best_model(metric=metric).predict(X)`. It returns only the predictions of the selected model. Use `inference()` when you also need probabilities, or the outputs of several models. ```python preds = result.predict(X_new) # auto-selects metric preds = result.predict(X_new, metric="r2") # explicit for regression ``` `X` is passed directly to the selected estimator. The return value is what that estimator's `predict(X)` returns, usually an array of shape `(n_samples,)`. ### Inference on new data `result.inference(X)` runs all fitted models on new data and returns their predictions together with the class probabilities, in a single call. ```python out = result.inference(X_new) # { # "HQNC": {"predictions": array([...]), "probabilities": array([[...]])}, # "SVC": {"predictions": array([...]), "probabilities": None}, # no predict_proba # } preds = out["HQNC"]["predictions"] # shape (n_samples,) probs = out["HQNC"]["probabilities"] # shape (n_samples, n_classes), or None ``` | Output key | Type | Description | | --------------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | `predictions` | `ndarray` | The output of `estimator.predict(X)`. | | `probabilities` | `ndarray` or `None` | The output of `predict_proba(X)` for classifiers that support it, and `None` for regressors and for classifiers without `predict_proba`. | #### Selecting models and seed By default, `inference()` runs all models, each with the copy fitted on the last seed. You can restrict both: ```python # Only a subset of models out = result.inference(X_new, models=["SVC"]) # Specific seed (0-based index into the per-seed list) out = result.inference(X_new, seed=0) out = result.inference(X_new, models=["HQNC", "SVC"], seed=1) ``` Passing an unknown model name raises a `ValueError` that lists the available names. #### Probabilities `probabilities` is an array of shape `(n_samples, n_classes)` when the estimator provides `predict_proba`, and `None` otherwise: - It is filled for `HQNC`, `QSVC(probability=True)`, `CustomQuantumClassifier` and `SVC(probability=True)`. - It is `None` for all regressors, for classifiers without `predict_proba` (for example, a plain `SVC()` without `probability=True`), and for a `QSVC` without `probability=True`, which also issues a warning. ## `quick_baselines()` ```python from eigenkit import QSVC from eigenkit.benchmark import compare, quick_baselines results = compare( models=[QSVC(feature_map="Angle"), *quick_baselines()], dataset="iris", n_seeds=2, ) ``` `quick_baselines()` returns built-in scikit-learn classifiers with sensible defaults: a `SVC` with a radial basis function (RBF) kernel and probabilities, a `RandomForestClassifier`, a `LogisticRegression` and a `KNeighborsClassifier`. scikit-learn 1.9 deprecates `SVC(probability=True)`, so from that version the support vector classifier is wrapped as `CalibratedClassifierCV(SVC(), ensemble=False)`. ## Timing a block of code `timer()` measures the wall-clock time of the code inside a `with` block. For example, it can measure the time needed to compute one kernel matrix on two providers: ```python import numpy as np from eigenkit import QuantumKernel from eigenkit.benchmark import timer X = np.random.default_rng(0).uniform(0, np.pi, size=(150, 4)) for provider in ("numpy", "pennylane"): # "pennylane" needs the pennylane extra kernel = QuantumKernel(n_features=4, feature_map="AngleCNOT", provider=provider) with timer() as t: kernel.compute_kernel_matrix(X) print(provider, f"{t['elapsed']:.2f} s") ``` ## API reference ### `eigenkit.benchmark` Comparison of quantum and classical estimators. The compare() function trains models that follow the scikit-learn interface on a dataset with several random seeds. It returns a BenchmarkResult with the aggregated metrics. Example ```python from sklearn.svm import SVC from eigenkit.qml import QSVC from eigenkit.benchmark import compare results = compare( models={ "Quantum SVC": QSVC( feature_map="Angle", reps=1, probability=True ), "Regularized SVC": SVC(C=0.3), "Flexible SVC": SVC(C=3.0), }, dataset="iris", metrics=["accuracy"], n_seeds=1, ) print(list(results.fitted_models)) # ['Quantum SVC', 'Regularized SVC', 'Flexible SVC'] ``` #### `BenchmarkResult` Results of a benchmark comparison. It stores the raw metric values of every seed, and aggregates them when you display them with `table()`, plot them with `plot()`, or analyse them with `to_dataframe()`. Each entry in `raw` is a dict with the keys `"model"`, `"seed"`, `"metric"` and `"value"`. `fitted_models` holds every fitted copy produced by compare(), keyed by the name of the model in the benchmark. The names usually match the class names, and repeated names are numbered (for example, `SVC_0` and `SVC_1`). If `compare(models={...})` receives a dict, its keys are used instead. Example ```python from sklearn.svm import SVC from eigenkit.qml import QSVC from eigenkit.benchmark import compare results = compare( models={ "Quantum SVC": QSVC( feature_map="Angle", reps=1, probability=True ), "Regularized SVC": SVC(C=0.3), "Flexible SVC": SVC(C=3.0), }, dataset="iris", metrics=["accuracy"], n_seeds=1, ) print(list(results.fitted_models)) # ['Quantum SVC', 'Regularized SVC', 'Flexible SVC'] results.plot(save_path="svc_comparison.png") df = results.to_dataframe() X_new = [[5.1, 3.5, 1.4, 0.2]] # Access the fitted estimators winner = results.best_model(metric="accuracy") preds = results.predict(X_new) quantum_fits = results.fitted_models["Quantum SVC"] # Run inference on new data, with all models or a subset out = results.inference(X_new) out = results.inference(X_new, models=["Quantum SVC"], seed=0) ``` ##### `best_model(metric=None, seed='last')` Return the fitted estimator with the best mean score on `metric`. When `metric` is `None`, the default, the first metric recorded in `raw` that is not a timing metric is used. With the default metrics, this is usually `"accuracy"` for classification and `"r2"` for regression. If custom metrics were passed in a different order, the first of them is used. Parameters: | Name | Type | Description | Default | | -------- | ----- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `metric` | \`str | None\` | The metric used for ranking, for example "accuracy" or "r2". None selects the primary metric of the task. Built-in metrics use their registered direction, and custom metrics use the direction given to custom_metric(...). | | `seed` | \`str | int\` | The seed whose estimator is returned for the winning model. "last", the default, returns the estimator of the last seed. An int is a 0-based index into the list of seeds, in the order they ran. | Returns: | Type | Description | | ----- | --------------------- | | `Any` | The fitted estimator. | Raises: | Type | Description | | ------------ | ------------------------------------------------------------------- | | `ValueError` | If no fitted models are stored, or if metric is not in the results. | | `IndexError` | If seed is out of range. | ##### `confusion_matrix(model_name=None, **kwargs)` Return a scikit-learn confusion matrix computed from the stored predictions. Parameters: | Name | Type | Description | Default | | ------------ | ----- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------ | | `model_name` | \`str | None\` | The model whose predictions are used. None, the default, uses the first model with stored predictions. | | `**kwargs` | `Any` | Passed on to sklearn.metrics.confusion_matrix. | `{}` | Returns: | Type | Description | | --------- | --------------------------------------- | | `ndarray` | The confusion matrix, as a NumPy array. | Raises: | Type | Description | | ------------- | ------------------------------------------------------------------------------------------------------------- | | `ValueError` | If no predictions are stored (run compare(..., store_predictions=True)), or if there are none for model_name. | | `ImportError` | If scikit-learn is not installed. | ##### `inference(X, models=None, seed='last')` Run every fitted model, or a subset of them, on new data. Parameters: | Name | Type | Description | Default | | -------- | ----------- | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `X` | `Any` | The input features to predict on. | *required* | | `models` | \`list[str] | None\` | The names of the models to run. None, the default, runs all fitted models. Use the exact keys of fitted_models: the names based on the class for a list input (for example, "SVC" or "SVC_0"), or your own keys for a dict input. | | `seed` | \`str | int\` | The seed whose estimator is used for each model. "last", the default, uses the estimator of the last seed. An int is a 0-based index into the list of seeds, in the order they ran. | Returns: | Type | Description | | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `dict[str, dict[str, Any]]` | A dict keyed by model name. Each value contains two entries: "predictions": the array returned by estimator.predict(X). "probabilities": the array returned by estimator.predict_proba(X) for classifiers that support it, and None for regressors and for classifiers without predict_proba. | Raises: | Type | Description | | ------------ | ------------------------------------------------------------------------- | | `ValueError` | If no fitted models are stored, or if an unknown model name is requested. | | `IndexError` | If seed is out of range for any model. | Example ```python out = results.inference(X_new) out = results.inference(X_new, models=["Flexible SVC"], seed=0) preds = out["Flexible SVC"]["predictions"] probs = out["Flexible SVC"]["probabilities"] # None for regressors ``` ##### `load(path)` Load numeric benchmark results saved by `BenchmarkResult.save()`. Parameters: | Name | Type | Description | Default | | ------ | ----- | ----------- | ---------------------- | | `path` | \`str | Path\` | The JSON file to read. | Returns: | Type | Description | | ------------------- | ------------------------------------------------------------------------------------------------------ | | `'BenchmarkResult'` | A BenchmarkResult with the stored values. It has no fitted models, because save() does not store them. | ##### `plot(save_path=None, theme='light')` Plot a grouped bar chart with error bars. Parameters: | Name | Type | Description | Default | | ----------- | ----- | ------------------------------------------------ | ------------------------------------------------------------------ | | `save_path` | \`str | None\` | If given, the figure is saved to this path instead of being shown. | | `theme` | `str` | The plot theme, "light" (the default) or "dark". | `'light'` | Raises: | Type | Description | | ------------- | ------------------------------- | | `ImportError` | If matplotlib is not installed. | ##### `plot_history(save_path=None, theme='light', seed='last')` Plot the stored training and validation loss curves. Models without a loss history are left out, and a warning names them. Parameters: | Name | Type | Description | Default | | ----------- | ----- | ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `save_path` | \`str | None\` | If given, the figure is saved to this path instead of being shown. | | `theme` | `str` | The plot theme, "light" (the default) or "dark". | `'light'` | | `seed` | \`str | int\` | The run to draw: "last" (the default), or an integer index into the list of seeds. With "last", a model whose last seed failed shows its most recent recorded curve. | Raises: | Type | Description | | ------------- | ---------------------------------------------------- | | `TypeError` | If seed is neither "last" nor an int. | | `ValueError` | If no model has a training or validation loss curve. | | `ImportError` | If matplotlib is not installed. | ##### `predict(X, metric=None)` Predict with the best model ranked by `metric`. This is a shortcut for `best_model()`. It calls `predict()` on the estimator returned by `best_model(metric=metric)`. Parameters: | Name | Type | Description | Default | | -------- | ----- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | `X` | `Any` | The input features. | *required* | | `metric` | \`str | None\` | The metric used to select the best model. None, the default, selects the primary metric of the task: "accuracy" for classification and "r2" for regression. | Returns: | Type | Description | | ----- | --------------------------------------------------------- | | `Any` | The predictions of the best estimator's predict() method. | ##### `save(path)` Save the numeric results of the benchmark to a JSON file. The fitted estimators are not saved, by design. Parameters: | Name | Type | Description | Default | | ------ | ----- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | `path` | \`str | Path\` | The file to write. If it is an existing directory, a file named bench\_.json is created inside it. If it has no extension, .json is added. | Returns: | Type | Description | | ------ | -------------------------------------- | | `Path` | The path of the file that was written. | ##### `significance_test(metric, method='wilcoxon')` Return a matrix of p-values for each pair of models. The p-values compare the values of `metric` on the seeds that both models share. Parameters: | Name | Type | Description | Default | | -------- | ----- | ----------------------------------------------------------------------------------------- | ------------ | | `metric` | `str` | The metric to compare. | *required* | | `method` | `str` | "wilcoxon" (the default) for a Wilcoxon signed-rank test, or "ttest" for a paired t-test. | `'wilcoxon'` | Returns: | Type | Description | | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | `'pd.DataFrame'` | A pandas DataFrame with one row and one column per model. The diagonal is 1, and a pair with fewer than two shared seeds has the value NaN. | Raises: | Type | Description | | ------------- | ------------------------------------------------------------------------------- | | `ImportError` | If pandas or scipy is not installed. | | `ValueError` | If method is not supported, or if fewer than two models have values for metric. | ##### `table(show_significance=False)` Return the results as a text table. The table is drawn with Unicode box-drawing characters and needs no optional dependency. Parameters: | Name | Type | Description | Default | | ------------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | | `show_significance` | `bool` | If True, mark the best model with * or \*\* when a Wilcoxon signed-rank test against another model gives p < 0.05 or p < 0.01. The test needs at least 6 seeds shared by both models, so with fewer seeds no model is marked, and a warning explains why. The test needs pandas, from the benchmark extra. | `False` | Returns: | Type | Description | | ----- | --------------------------------------------------------------------------------------------- | | `str` | A string with one row per model and one column per metric. Each value is shown as mean ± std. | Raises: | Type | Description | | ------------- | --------------------------------------------------------- | | `ImportError` | If show_significance is True and pandas is not installed. | ##### `to_dataframe()` Return the raw results of every seed as a pandas DataFrame. Returns: | Type | Description | | ---------------- | ---------------------------------------------------------------------------------------------- | | `'pd.DataFrame'` | A DataFrame with the columns model, seed, metric and value, and one row for each entry of raw. | Raises: | Type | Description | | ------------- | --------------------------- | | `ImportError` | If pandas is not installed. | #### `compare(models, dataset='breast_cancer_wisconsin', metrics=None, n_seeds=5, test_size=0.15, show_progress=False, save_dir=None, store_predictions=False, validation_size=0.15)` Compare eigenkit models and built-in scikit-learn models on a dataset. Each model must be a built-in scikit-learn estimator (from the `sklearn` package), or a subclass of BaseQuantumEstimator, BaseQuantumClassifier or BaseQuantumRegressor. For every seed, each model is copied with `sklearn.base.clone()`. Parameters: | Name | Type | Description | Default | | ------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `models` | \`list | dict[str, Any]\` | The estimator instances to compare. If a list is passed, the display names come from type(model).__name__, and repeated names are numbered (for example, SVC_0 and SVC_1). If a dict is passed, its keys are used as display names. Dict keys must be non-empty strings, with no whitespace at the start or the end and no control characters. Use a different key for each model. If the same key appears twice in a Python dict literal, Python keeps only the last model before compare() receives the dict. When save_dir is set, the keys must also be valid prefixes for file names. | | `dataset` | \`str | dict\` | A dataset name, or a dict that is already split, with X_train, y_train, X_test and y_test. Such a dict can also include X_val and y_val. | | `metrics` | \`list | None\` | The metrics to compute. For classification, the options are accuracy, f1, precision, recall, roc_auc, fit_time and predict_time. For regression, they are r2, mae, rmse, mse, fit_time and predict_time. A custom function must be wrapped with custom_metric(...), so that its direction (higher or lower is better) is explicit. The default depends on the type of task of the first model. | | `n_seeds` | `int` | The number of seeds to average over. The default is 5. | `5` | | `test_size` | `float` | The fraction of the data kept for the final test set. | `0.15` | | `show_progress` | `bool` | Whether to print the progress of the benchmark, with the current model and an estimate of the remaining time. The default is False, which runs silently. | `False` | | `save_dir` | \`str | Path | None\` | | `store_predictions` | `bool` | If True, the result keeps y_true, y_pred and y_prob for the last seed of each model. | `False` | | `validation_size` | `float` | The fraction of the data kept for validation. | `0.15` | Returns: | Type | Description | | ----------------- | ----------------------------------------------------------------------------------- | | `BenchmarkResult` | An object with the results, and methods such as table(), plot() and to_dataframe(). | Raises: | Type | Description | | ------------ | ------------------------------------------------------------------------------------ | | `ValueError` | If models is empty, if a model name is not valid, or if dataset is not well formed. | | `TypeError` | If a model name is not a string, or if a model is not a supported type of estimator. | #### `custom_metric(fn, *, direction, name=None)` Define a custom metric and the direction in which it improves. Parameters: | Name | Type | Description | Default | | ----------- | ------------------------------------- | ---------------------------------------------------------------------------- | -------------------------------------------------------------- | | `fn` | `Callable[[ndarray, ndarray], float]` | The metric function. It receives (y_true, y_pred) and returns a number. | *required* | | `direction` | `MetricDirection` | "higher" if higher values are better, or "lower" if lower values are better. | *required* | | `name` | \`str | None\` | The column label of the metric. The default is the name of fn. | Returns: | Type | Description | | -------------- | --------------------------------------------------------- | | `CustomMetric` | A metric specification to pass to compare(metrics=[...]). | #### `load_checkpoint(path, *, model=None)` Load a model checkpoint written by `compare(..., save_dir=...)`. Parameters: | Name | Type | Description | Default | | ------- | ----- | ----------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | | `path` | \`str | Path\` | The path to the checkpoint file (\*.eigenkit or \*.joblib). | | `model` | `Any` | The nn.Module architecture for GenericRegressor or GenericClassifier checkpoints. It is ignored for other types of estimator. | `None` | Returns: | Type | Description | | ----- | --------------------- | | `Any` | The loaded estimator. | Raises: | Type | Description | | ------------ | ------------------------------------------------------------ | | `TypeError` | If a .joblib file does not contain a scikit-learn estimator. | | `ValueError` | If the file extension is not supported. | #### `quick_baselines()` Return a list of common scikit-learn classifiers with sensible defaults. The list contains a `SVC` with a radial basis function (RBF) kernel and probabilities, so that `roc_auc` can be computed, a `RandomForestClassifier`, a `LogisticRegression` and a `KNeighborsClassifier`. `XGBClassifier` and `LGBMClassifier` are added when their packages can be imported. scikit-learn 1.9 deprecates `SVC(probability=True)`. From that version, the support vector classifier is wrapped as `CalibratedClassifierCV(SVC(), ensemble=False)`, the replacement that scikit-learn documents, so the results table calls it `CalibratedClassifierCV`. Returns: | Type | Description | | ----------- | ------------------------------------------------------------------------------------- | | `list[Any]` | A list of classifiers that follow the scikit-learn interface, created but not fitted. | Example ```python from eigenkit.benchmark import compare, quick_baselines results = compare(models=quick_baselines(), dataset="iris", n_seeds=2) ``` #### `timer()` Measure the wall-clock time of a block of code. Yields: | Type | Description | | ------ | -------------------------------------------------------------------------------------- | | `dict` | A dict. When the with block ends, its "elapsed" key holds the elapsed time in seconds. | Example ```python with timer() as t: do_work() print(t["elapsed"]) ``` # Quantum Chemistry `eigenkit.chemistry` computes ground-state energies. The process always has the same four steps: build a fermionic Hamiltonian, map it onto qubits, choose an ansatz, and minimise the energy with the Variational Quantum Eigensolver (VQE). Each step is a separate function, so you can stop after any of them and inspect the result. Like `optimization` and `reservoir`, this package belongs to the core of the library. It uses only NumPy and SciPy, and needs neither PyTorch nor a backend package, so most of it runs on a bare installation. The exceptions are the functions that call external chemistry software: `molecular_hamiltonian`, `hydrogen_molecule` and `bravyi_kitaev`. They need the `chemistry` extra (`pip install eigenkit[chemistry]`), which installs PySCF, OpenFermion and openfermionpyscf. ## The whole process in one example The two-site Fermi-Hubbard model, also called the Fermi-Hubbard dimer, is the smallest model worth solving, and it needs no external software. This example solves it from start to finish and compares the result with the exact answer: ```python from eigenkit.chemistry import ( VQE, exact_ground_state_energy, fermi_hubbard_dimer, jordan_wigner, ) hamiltonian = fermi_hubbard_dimer(t=1.0, u=2.0) # 6 fermionic terms observable, offset = jordan_wigner(hamiltonian) # 10 Pauli terms, offset 1.0 result = VQE(layers=3, restarts=5).solve(observable, 4, offset=offset) exact = exact_ground_state_energy(observable, 4, offset=offset) print(result.energy) # -1.236068 print(exact) # -1.236068 print(result.success) # True ``` The model uses `4` qubits, two spin-orbitals per site. `VQE` uses `provider="numpy"` by default, so this example runs with no optional dependencies. The rest of this page goes through the four steps one at a time. ## Building a Hamiltonian A `FermionOperator` is a dictionary of fermionic terms and their coefficients. Each term is a tuple of `(mode, kind)` pairs, where `kind` is `1` for a creation operator and `0` for an annihilation operator. For example, `((0, 1), (2, 0))` is `c†₀c₂`: ```python from eigenkit.chemistry import FermionOperator, fermi_hubbard_dimer hamiltonian = fermi_hubbard_dimer(t=1.0, u=2.0) for term, coeff in hamiltonian.terms.items(): print(term, coeff) # ((0, 1), (2, 0)) (-1+0j) hopping between the two sites # ((2, 1), (0, 0)) (-1+0j) # ((1, 1), (3, 0)) (-1+0j) # ((3, 1), (1, 0)) (-1+0j) # ((0, 1), (0, 0), (1, 1), (1, 0)) (2+0j) on-site repulsion, u # ((2, 1), (2, 0), (3, 1), (3, 0)) (2+0j) # The same notation builds one by hand: c†₀c₁ + c†₁c₀ hopping = FermionOperator({((0, 1), (1, 0)): 1.0, ((1, 1), (0, 0)): 1.0}) print(len(hopping.terms)) # 2 ``` `fermi_hubbard_dimer(t, u)` uses only NumPy. `t` is the hopping amplitude between the two sites, and `u` is the on-site repulsion. Because it needs nothing else, it is the right model to develop against. For real molecules, `molecular_hamiltonian(geometry, basis=...)` runs PySCF and OpenFermion, and `hydrogen_molecule(bond_length)` is a shortcut for `H₂`. Both return a `Molecule`. A `Molecule` holds the fermionic operator, the number of electrons and the nuclear repulsion energy, which are the values needed to turn a converged VQE energy into a physical one: ```python # Requires the `chemistry` extra: PySCF and OpenFermion. from eigenkit.chemistry import hydrogen_molecule molecule = hydrogen_molecule(bond_length=0.7414) print(molecule.n_qubits, molecule.n_electrons) ``` ## Mapping onto qubits A fermionic operator cannot be measured directly, so it has to be rewritten with Pauli operators. Both mappings return the same pair: an `Observable` and a scalar `offset`: ```python from eigenkit.chemistry import exact_ground_state_energy, jordan_wigner observable, offset = jordan_wigner(hamiltonian) print(len(observable.terms)) # 10 print(offset) # 1.0 print(observable.terms[0]) # PauliTerm(coeff=-0.5, paulis=(('Y', 0), ('Z', 1), ('Y', 2))) ``` **The offset must always be passed.** It is the identity part of the Hamiltonian. It contains no Pauli operator, so it is left out of the observable. Pass it back in every time, otherwise every energy is wrong by a constant: ```python print(exact_ground_state_energy(observable, 4, offset=offset)) # -1.236068 print(exact_ground_state_energy(observable, 4)) # -2.236068 ``` The two results differ by exactly `1.0`, and nothing in the wrong one looks suspicious. The two mappings have different dependencies, which is useful to know before you choose one. `jordan_wigner` is implemented in eigenkit and needs nothing else. `bravyi_kitaev` uses OpenFermion, and it raises `ModuleNotFoundError` without the `chemistry` extra. Jordan-Wigner is the natural default. Bravyi-Kitaev gives shorter Pauli strings for larger systems, at the cost of that dependency: ```python # Requires the `chemistry` extra: OpenFermion. from eigenkit.chemistry import bravyi_kitaev observable_bk, offset_bk = bravyi_kitaev(hamiltonian, n_qubits=4) print(len(observable_bk.terms), offset_bk) ``` If you already work with OpenFermion, three functions convert between the two libraries. `fermion_operator_from_openfermion` and `to_openfermion_fermion_operator` convert fermionic operators in either direction, and `observable_from_openfermion` turns an `openfermion.QubitOperator` into the same `(Observable, offset)` pair that the mappings return. All three need OpenFermion. ## Choosing an ansatz The table compares the two ansätze that `VQE` supports: the hardware-efficient ansatz, and the unitary coupled-cluster ansatz with single and double excitations (UCCSD). | | Hardware-efficient | UCCSD | | ---------------------------------------- | ------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | | When it is used | By default | When you pass it with `ansatz=` | | How it is built | A layer of `RY` rotations, then `layers` repetitions of a chain of `CNOT` gates and a pair of rotations | Single and double excitations, with a Trotter approximation, on a Hartree-Fock reference | | Why you would choose it | A device can run it | The chemistry of the problem suggests it | | Weights on `4` qubits with `2` electrons | `20` with `layers=2` | `10` with `reps=2` | | Depth | Shallow | Much deeper for the same number of parameters | | Reference state | None: it starts from \` | 0…0⟩\` | By default, `VQE` builds a hardware-efficient ansatz: a layer of `RY` rotations, then `layers` repetitions of a chain of `CNOT` gates and a pair of rotations. `hardware_efficient_ansatz` builds the same circuit on its own, and `hartree_fock_state` builds the Hartree-Fock reference state that UCCSD starts from. Both are useful to count the parameters before you start a run: ```python from eigenkit.chemistry import hardware_efficient_ansatz, hartree_fock_state print(hardware_efficient_ansatz(n_qubits=4, layers=2).n_weights) # 20 print(hartree_fock_state(n_qubits=4, n_electrons=2).n_weights) # 0, it is a fixed state ``` The ansatz designed for chemistry is `uccsd_ansatz`: a unitary coupled-cluster ansatz, with a Trotter approximation, built on the Hartree-Fock reference. Pass it explicitly: ```python from eigenkit.chemistry import uccsd_ansatz ansatz = uccsd_ansatz(n_qubits=4, n_electrons=2, reps=2) result = VQE(restarts=3).solve(observable, 4, offset=offset, ansatz=ansatz) print(ansatz.n_weights) # 10 print(result.energy) # -1.236068 ``` This figure draws the UCCSD column of the table above. The Hartree-Fock reference state is on the left. Then come the single and double excitations that `uccsd_ansatz` produces, with a Trotter approximation, and then the Pauli measurements whose weighted sum is the energy that VQE minimises. `reps` is the number of times the block of excitations is repeated. With one Trotter step, the ansatz converges to the first excited level, and with two it reaches the ground state. The value of `reps` has a large effect. With `reps=1`, the ansatz has `5` parameters and converges to `-1.0`. This is not a near miss: it is the *first excited level* of this Hamiltonian, `0.236` above the ground state, and the result reports `success=True`. The optimiser did converge, but one Trotter step cannot reach the ground state. `reps=2` doubles the number of parameters and reaches the exact energy. For finer control, `apply_hartree_fock(circuit, n_electrons)` prepares the reference state on a `CircuitBuilder` that you are already building, and `uccsd_excitation_generators(n_qubits, n_electrons)` returns the generators `T - T†` themselves (`5` of them for this system) if you want to build the ansatz in a different way. ## Checking the answer VQE gives an upper bound on the ground-state energy. For a system this small, you can compute the true value and see how close the bound is. `dense_hamiltonian` builds the matrix of the Hamiltonian, so the whole spectrum is available, and not only the lowest level: ```python import numpy as np from eigenkit.chemistry import ( dense_hamiltonian, fermi_hubbard_dimer_ground_energy, ) matrix = dense_hamiltonian(observable, 4, offset=offset) print(matrix.shape) # (16, 16) print(np.linalg.eigvalsh(matrix)[:3]) # [-1.236068 -1. -1.] print(fermi_hubbard_dimer_ground_energy(t=1.0, u=2.0)) # -1.236068 ``` The second eigenvalue is where the UCCSD run with `reps=1` above stopped. `exact_ground_state_energy` is the shortcut when only the lowest level is needed. For this model, the analytic formula at half filling agrees with the diagonalisation to the last digit, so three different methods give the same number. Add this check to any workflow, because **`success=True` only means that the optimiser converged; it does not mean that the energy is correct**. Dense diagonalisation becomes impractical at around `14` qubits, where the matrix already needs about `4` gigabytes of memory. By then, you will have calibrated the ansatz on smaller cases where the check was possible. ## API reference ### `eigenkit.chemistry` Quantum chemistry with the core of the library. Build a fermionic Hamiltonian (`FermionOperator`), map it to a qubit `Observable` (`jordan_wigner`), and find its ground-state energy with the Variational Quantum Eigensolver (`VQE`), which uses the variational optimisation tools of the core. The package uses only NumPy and SciPy, needs no PyTorch, and works with any backend. For small systems, `exact_ground_state_energy` gives the classical reference value. Example ```python from eigenkit.chemistry import ( fermi_hubbard_dimer, jordan_wigner, VQE, exact_ground_state_energy, ) hamiltonian, offset = jordan_wigner(fermi_hubbard_dimer(t=1.0, u=2.0)) result = VQE(layers=3, restarts=5).solve(hamiltonian, n_qubits=4, offset=offset) exact = exact_ground_state_energy(hamiltonian, n_qubits=4, offset=offset) print(result.energy, exact) ``` #### `FermionOperator` A complex linear combination of fermionic terms, stored as `{term: coeff}`. ##### `__init__(terms=None)` Build an operator from a mapping of terms to coefficients. Parameters: | Name | Type | Description | Default | | ------- | --------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `terms` | \`dict[Term, complex] | None\` | Maps each term to its coefficient. A term is a tuple of (mode, action) pairs, where action is 1 for creation and 0 for annihilation, applied from left to right. The empty tuple is the identity. The coefficients of repeated terms are added together. Leave it out for the zero operator. The default is None. | ##### `annihilation(mode)` The annihilation operator `a_mode`. Parameters: | Name | Type | Description | Default | | ------ | ----- | ---------------------------------------------------------------- | ---------- | | `mode` | `int` | The index of the spin-orbital from which an electron is removed. | *required* | Returns: | Type | Description | | ------------------- | ----------------------------------- | | `'FermionOperator'` | The operator, as a FermionOperator. | ##### `creation(mode)` The creation operator `a†_mode`. Parameters: | Name | Type | Description | Default | | ------ | ----- | -------------------------------------------------------------- | ---------- | | `mode` | `int` | The index of the spin-orbital in which an electron is created. | *required* | Returns: | Type | Description | | ------------------- | ----------------------------------- | | `'FermionOperator'` | The operator, as a FermionOperator. | ##### `hermitian_conjugate()` The adjoint of the operator. The order of each term is reversed, creation and annihilation are swapped, and each coefficient is replaced by its complex conjugate. Returns: | Type | Description | | ------------------- | ---------------------------------- | | `'FermionOperator'` | The adjoint, as a FermionOperator. | ##### `identity(coefficient=1.0)` The identity operator, optionally scaled. Parameters: | Name | Type | Description | Default | | ------------- | -------- | ------------------------------------------------------------ | ------- | | `coefficient` | `Scalar` | The factor that multiplies the identity. The default is 1.0. | `1.0` | Returns: | Type | Description | | ------------------- | ------------------------------------------ | | `'FermionOperator'` | The scaled identity, as a FermionOperator. | ##### `number(mode)` The number operator `a†_mode a_mode`. It counts the electrons in one spin-orbital. Parameters: | Name | Type | Description | Default | | ------ | ----- | ------------------------------ | ---------- | | `mode` | `int` | The index of the spin-orbital. | *required* | Returns: | Type | Description | | ------------------- | ----------------------------------- | | `'FermionOperator'` | The operator, as a FermionOperator. | ##### `zero()` The zero operator, which has no terms. Returns: | Type | Description | | ------------------- | ------------------------- | | `'FermionOperator'` | The zero FermionOperator. | #### `Molecule` A molecular Hamiltonian in second quantisation. `fermion_operator` already includes the constant term (the nuclear repulsion), so `jordan_wigner(fermion_operator)` returns it as the offset. `hf_energy` and `fci_energy`, when computed, are the reference energies from PySCF, which are useful targets for the Variational Quantum Eigensolver (VQE). Attributes: | Name | Type | Description | | ------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `fermion_operator` | `FermionOperator` | The Hamiltonian in second quantisation, including the constant term. | | `n_qubits` | `int` | The number of qubits needed after the mapping, one per spin-orbital with Jordan-Wigner. | | `n_electrons` | `int` | The number of electrons, which sets the Hartree-Fock reference state and the excitations that a unitary coupled-cluster singles and doubles (UCCSD) ansatz can use. | | `nuclear_repulsion` | `float` | The constant nuclear term, in Hartree. It is already included in fermion_operator, and it is kept separately so that it can be reported. | | `hf_energy` | `float` | The Hartree-Fock energy from PySCF, in Hartree. A VQE run should reach a lower energy. | | `fci_energy` | \`float | None\` | #### `VQE` Bases: `BaseQuantumSolver` Variational Quantum Eigensolver (VQE). Parameters: | Name | Type | Description | Default | | -------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------- | | `layers` | `int` | The depth of the default hardware-efficient ansatz. The default is 2. | `2` | | `provider` | `ProviderSpec` | The backend to use. The default is "numpy", the built-in reference simulator. | `'numpy'` | | `optimizer` | `Optimizer` | The optimiser: the name of a scipy.optimize.minimize method, "spsa", or a custom callable (see minimize_expectation). The default is "L-BFGS-B". | `'L-BFGS-B'` | | `restarts` | `int` | The number of runs from random starting points. The lowest energy is kept, because variational landscapes have local minima. The default is 1. | `1` | | `random_state` | \`int | None\` | The base seed. Restart r uses random_state + r, and None leaves every restart unseeded. The default is 42. | ##### `solve(hamiltonian, n_qubits, *, offset=0.0, ansatz=None)` Minimise the expectation value of the Hamiltonian over the ansatz. It runs `restarts` independent optimisations from different random starting points and keeps the lowest energy, because a variational landscape has local minima. Parameters: | Name | Type | Description | Default | | ------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | | `hamiltonian` | `Observable` | The Hamiltonian to minimise, as a qubit Observable, usually the one returned by jordan_wigner. | *required* | | `n_qubits` | `int` | The number of qubits of the circuit. It must match the Hamiltonian. | *required* | | `offset` | `float` | The constant added to the minimised value. This is where the identity term returned by the mapping goes, which for a molecule includes the nuclear repulsion, in Hartree. If it is left out, the energy is wrong by exactly that constant. The default is 0.0. | `0.0` | | `ansatz` | \`CircuitSpec | None\` | The circuit to optimise. The default, None, uses the built-in hardware-efficient ansatz with layers layers. | Returns: | Type | Description | | ----------- | ------------------------------------------------------------------------------------- | | `VQEResult` | A VQEResult with the best energy, its parameters, and whether that restart converged. | #### `VQEResult` Bases: `ValueEquality` Outcome of a run of the Variational Quantum Eigensolver (VQE). The result is read-only. Assigning to a field raises `FrozenInstanceError`, and writing into an array field raises `ValueError`. This prevents a mistyped `=` (instead of `==`) or an in-place edit from leaving a result whose scalar fields describe a different run from the array it holds. To get a writable copy of the array, use `np.array(result.field)`. Results compare by value, including their arrays. They are not hashable, because a result is a collection of numbers and not a key, and a correct hash of a float array is expensive to compute. Attributes: | Name | Type | Description | | -------------------- | --------- | --------------------------------------------------------------------------------------- | | `energy` | `float` | The lowest energy found, with the offset added back. For a molecule, it is in Hartree. | | `optimal_parameters` | `ndarray` | The ansatz weights that produced this energy. | | `n_iterations` | `int` | The number of optimiser iterations in the best restart. It is not summed over restarts. | | `success` | `bool` | Whether the optimiser of that restart reported convergence. | ##### `__post_init__()` Make the array field read-only. Freezing the dataclass prevents a field from being reassigned, but it does not prevent writing into the array that the field holds, and a result that has been written into still reports the scalar values of the original run. The result therefore keeps its own copy, so the array it was built from can still be changed by the caller. To get a writable copy, use `np.array(result.optimal_parameters)`. #### `apply_hartree_fock(circuit, n_electrons)` Prepare the Hartree-Fock reference state on an existing builder. It applies an `X` gate to each of the lowest `n_electrons` spin-orbitals, which marks them as occupied (`|1⟩`). The convention is the one of the Jordan-Wigner mapping: mode `j` is qubit `j`, and the spin-orbitals are ordered so that the lowest `n_electrons` of them are occupied in the reference state, for example `|1…1 0…0⟩`. Parameters: | Name | Type | Description | Default | | ------------- | ---------------- | --------------------------------------------------------------------- | ---------- | | `circuit` | `CircuitBuilder` | The builder to add the gates to. | *required* | | `n_electrons` | `int` | The number of electrons, which sets the Hartree-Fock reference state. | *required* | Returns: | Type | Description | | ---------------- | ----------------------------------------------- | | `CircuitBuilder` | The same builder, so that calls can be chained. | #### `bravyi_kitaev(operator, n_qubits=None)` Map a `FermionOperator` to qubits with the Bravyi-Kitaev transformation. The transformation itself is done by OpenFermion (the `chemistry` extra), and the result is converted to an eigenkit `Observable`. The Jordan-Wigner mapping, in contrast, is implemented in eigenkit. `n_qubits` defaults to the largest mode index plus one. Parameters: | Name | Type | Description | Default | | ---------- | ----------------- | ------------------------------ | ---------- | | `operator` | `FermionOperator` | The fermionic operator to map. | *required* | | `n_qubits` | `int` | The number of qubits. | `None` | Returns: | Name | Type | Description | | ------------ | ------------ | ---------------------------------------------------------------------------- | | `observable` | `Observable` | The operator on qubits, without its identity part. | | `offset` | `float` | The coefficient of the identity part, which the Observable does not contain. | #### `dense_hamiltonian(observable, n_qubits, offset=0.0)` The `2**n_qubits × 2**n_qubits` matrix of `observable + offset * I`. Qubit 0 is the most significant factor of the tensor product, which matches the ordering of basis states in the simulators. Parameters: | Name | Type | Description | Default | | ------------ | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `observable` | `Observable` | The qubit operator. | *required* | | `n_qubits` | `int` | The number of qubits. | *required* | | `offset` | `float` | The constant (identity) term that the mapping returned separately, added back to the energy. For a molecule, it includes the nuclear repulsion, in Hartree. | `0.0` | Returns: | Type | Description | | --------- | ------------------------------------- | | `ndarray` | The matrix, as a complex NumPy array. | #### `exact_ground_state_energy(observable, n_qubits, offset=0.0)` The lowest eigenvalue of `observable + offset * I`, by dense diagonalisation. Parameters: | Name | Type | Description | Default | | ------------ | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `observable` | `Observable` | The qubit operator. | *required* | | `n_qubits` | `int` | The number of qubits. | *required* | | `offset` | `float` | The constant (identity) term that the mapping returned separately, added back to the energy. For a molecule, it includes the nuclear repulsion, in Hartree. | `0.0` | Returns: | Type | Description | | ------- | ------------------------ | | `float` | The ground-state energy. | #### `fermi_hubbard_dimer(t, u)` The two-site Fermi-Hubbard model with spin `1/2`. It has four spin-orbital modes, indexed by `mode = 2 * site + spin`, with spin `0` for up and `1` for down. Site `0` has modes `0` and `1`, and site `1` has modes `2` and `3`. The Hamiltonian is: ```text H = -t Σ_σ (a†_{0σ} a_{1σ} + a†_{1σ} a_{0σ}) + U (n_{0↑} n_{0↓} + n_{1↑} n_{1↓}) ``` The ground-state energy at half filling has the closed form fermi_hubbard_dimer_ground_energy(). This makes the model an exact check of the fermion-to-qubit mapping and of VQE, with no external dependency. Parameters: | Name | Type | Description | Default | | ---- | ------- | -------------------------------------------- | ---------- | | `t` | `float` | The hopping amplitude between the two sites. | *required* | | `u` | `float` | The on-site Coulomb repulsion. | *required* | Returns: | Type | Description | | ----------------- | -------------------------------------- | | `FermionOperator` | The Hamiltonian, as a FermionOperator. | #### `fermi_hubbard_dimer_ground_energy(t, u)` The ground-state energy of the Fermi-Hubbard dimer at half filling. The energy of fermi_hubbard_dimer() with two electrons in a singlet state is `(U - sqrt(U² + 16t²)) / 2`. This value is the ground state over all particle numbers only when `U < 3t`. For a larger `U`, double occupancy costs so much energy that the state with a single electron, of energy `-t`, is lower, and the ground state over all particle numbers is `min(-t, (U - sqrt(U² + 16t²)) / 2)`. An ansatz that conserves the number of particles, such as UCCSD, targets the half-filling sector directly. The hardware-efficient ansatz explores all sectors, so use it only where the half-filling state is the overall ground state. Parameters: | Name | Type | Description | Default | | ---- | ------- | --------------------------------------- | ---------- | | `t` | `float` | The hopping amplitude, as in the model. | *required* | | `u` | `float` | The on-site repulsion, as in the model. | *required* | Returns: | Type | Description | | ------- | -------------------------------------------------------------- | | `float` | The ground-state energy at half filling, from the closed form. | #### `fermion_operator_from_openfermion(operator)` Convert an `openfermion.FermionOperator` to an eigenkit `FermionOperator`. Parameters: | Name | Type | Description | Default | | ---------- | ----------------- | ---------------------------------- | ---------- | | `operator` | `FermionOperator` | The fermionic operator to convert. | *required* | Returns: | Type | Description | | ----------------- | -------------------------------------------------- | | `FermionOperator` | The same operator, as an eigenkit FermionOperator. | #### `hardware_efficient_ansatz(n_qubits, layers)` A standard hardware-efficient ansatz. It starts with a layer of `RY` rotations, then repeats `layers` times a linear chain of `CNOT` gates followed by an `RY` and an `RZ` rotation on each qubit. It has `n_qubits + 2 * layers * n_qubits` trainable weights. Parameters: | Name | Type | Description | Default | | ---------- | ----- | -------------------------- | ---------- | | `n_qubits` | `int` | The number of qubits. | *required* | | `layers` | `int` | The number of repetitions. | *required* | Returns: | Type | Description | | ------------- | ----------------------------- | | `CircuitSpec` | The ansatz, as a CircuitSpec. | #### `hartree_fock_state(n_qubits, n_electrons)` A circuit that prepares the Hartree-Fock reference state. The state is `|1…1 0…0⟩` on `n_qubits` qubits, with `n_electrons` occupied spin-orbitals. Parameters: | Name | Type | Description | Default | | ------------- | ----- | --------------------------------------------------------------------- | ---------- | | `n_qubits` | `int` | The number of qubits. | *required* | | `n_electrons` | `int` | The number of electrons, which sets the Hartree-Fock reference state. | *required* | Returns: | Type | Description | | ------------- | -------------------------------------------------------- | | `CircuitSpec` | The circuit, as a CircuitSpec with no trainable weights. | #### `hydrogen_molecule(bond_length=0.7414, *, basis='sto-3g', run_fci=False)` The hydrogen molecule `H₂` at the given bond length, in angstroms. In the minimal STO-3G basis set, this is the standard 4-qubit benchmark for VQE. Its FCI ground-state energy is about `-1.137` Hartree at equilibrium. Parameters: | Name | Type | Description | Default | | ------------- | ------- | --------------------------------------------------------------------------------------------- | ---------- | | `bond_length` | `float` | The distance between the nuclei, in angstroms. The default, 0.7414, is close to equilibrium. | `0.7414` | | `basis` | `str` | The name of the PySCF basis set. | `'sto-3g'` | | `run_fci` | `bool` | Whether to also compute the exact FCI energy, the target against which a VQE run is measured. | `False` | Returns: | Type | Description | | ---------- | ---------------------------------------------------------- | | `Molecule` | The molecule, with its Hamiltonian and reference energies. | #### `jordan_wigner(operator, *, atol=1e-10)` Map a `FermionOperator` to qubits with the Jordan-Wigner transformation. The operator must be Hermitian, as a Hamiltonian is. If a resulting coefficient is not real, a `ValueError` is raised. Parameters: | Name | Type | Description | Default | | ---------- | ----------------- | ---------------------------------------------------------------------------- | ---------- | | `operator` | `FermionOperator` | The fermionic operator to map. | *required* | | `atol` | `float` | Terms whose coefficient is smaller than this value in magnitude are dropped. | `1e-10` | Returns: | Name | Type | Description | | ------------ | ------------ | ---------------------------------------------------------------------------- | | `observable` | `Observable` | The operator on qubits, without its identity part. | | `offset` | `float` | The coefficient of the identity part, which the Observable does not contain. | #### `molecular_hamiltonian(geometry, *, basis='sto-3g', charge=0, multiplicity=1, run_fci=False)` Build a `Molecule` from a geometry, with PySCF and OpenFermion. Parameters: | Name | Type | Description | Default | | -------------- | ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `geometry` | `list[tuple[str, tuple[float, float, float]]]` | A list of (element, (x, y, z)) entries, with the coordinates in angstroms. | *required* | | `basis` | `str` | The name of the PySCF basis set. The default is the minimal basis set "sto-3g". | `'sto-3g'` | | `charge` | `int` | The total charge of the molecule. | `0` | | `multiplicity` | `int` | The spin multiplicity, 2S + 1. | `1` | | `run_fci` | `bool` | Whether to also compute the FCI energy, which is exact in this basis set and a useful target for VQE. It needs an exact diagonalisation, so it is only possible for small systems. | `False` | Returns: | Type | Description | | ---------- | ---------------------------------------------------------- | | `Molecule` | The Molecule, with its Hamiltonian and reference energies. | #### `observable_from_openfermion(qubit_operator, *, atol=1e-10)` Convert an `openfermion.QubitOperator` to an `(Observable, offset)` pair. The identity term becomes the real `offset`. The coefficients must be real, because the operator must be Hermitian, and a `ValueError` is raised otherwise. Parameters: | Name | Type | Description | Default | | ---------------- | --------------- | ---------------------------------------------------------------------------- | ---------- | | `qubit_operator` | `QubitOperator` | The operator to convert. | *required* | | `atol` | `float` | Terms whose coefficient is smaller than this value in magnitude are dropped. | `1e-10` | Returns: | Name | Type | Description | | ------------ | ------------ | ---------------------------------------- | | `observable` | `Observable` | The operator, without its identity term. | | `offset` | `float` | The coefficient of the identity term. | #### `to_openfermion_fermion_operator(operator)` Convert an eigenkit `FermionOperator` to an `openfermion.FermionOperator`. Parameters: | Name | Type | Description | Default | | ---------- | ----------------- | ---------------------------------- | ---------- | | `operator` | `FermionOperator` | The fermionic operator to convert. | *required* | Returns: | Type | Description | | ----------------- | ---------------------------------- | | `FermionOperator` | The same operator, in OpenFermion. | #### `uccsd_ansatz(n_qubits, n_electrons, reps=1)` The UCCSD ansatz on the Hartree-Fock reference, with a Trotter approximation. UCCSD is the unitary coupled-cluster ansatz with single and double excitations. The circuit prepares `(∏_k exp(θ_k (T_k − T_k†)))^reps |HF⟩`, where `|HF⟩` is the Hartree-Fock state. Each generator `G = T − T†` is anti-Hermitian. The Jordan-Wigner mapping turns `H = −iG` into a Hermitian qubit operator, and `exp(θG) = exp(iθH)` is then a product of `pauli_exponential` gates. The Pauli terms of one generator commute, so that product is exact. The product over different generators is a first-order Trotter approximation. `reps` repeats the whole sequence with independent weights, which reduces the Trotter error: more repetitions bring the circuit closer to the exact UCCSD unitary. The ansatz has `reps * n_excitations` weights in total. Parameters: | Name | Type | Description | Default | | ------------- | ----- | -------------------------------------------------------------------------------------------- | ---------- | | `n_qubits` | `int` | The number of qubits, one per spin-orbital. | *required* | | `n_electrons` | `int` | The number of electrons, which sets the Hartree-Fock reference and the possible excitations. | *required* | | `reps` | `int` | The number of Trotter repetitions of the product of excitations. The default is 1. | `1` | Returns: | Type | Description | | ------------- | --------------------------------------------------------------------------------------- | | `CircuitSpec` | The ansatz as a CircuitSpec, with one trainable angle per generator and per repetition. | #### `uccsd_excitation_generators(n_qubits, n_electrons)` The excitation generators `T - T†` of the UCCSD ansatz. UCCSD is the unitary coupled-cluster ansatz with single and double excitations. The excitations go from the occupied spin-orbitals (the lowest `n_electrons`) to the unoccupied ones. Single and double excitations are taken over all choices of occupied and unoccupied indices, with no restriction from spin symmetry, which keeps the construction simple and correct. A variant restricted by spin symmetry, which would have fewer parameters, is not implemented. Parameters: | Name | Type | Description | Default | | ------------- | ----- | --------------------------------------------------------------------- | ---------- | | `n_qubits` | `int` | The number of qubits. | *required* | | `n_electrons` | `int` | The number of electrons, which sets the Hartree-Fock reference state. | *required* | Returns: | Type | Description | | ----------------------- | ----------------------------------------------------- | | `list[FermionOperator]` | The generators, as a list of FermionOperator objects. | # Circuit Builder `CircuitBuilder` builds a circuit one gate at a time. Its style follows Qiskit, Braket and CUDA Quantum: `qc.rx(angle, qubit=0)` adds a gate and returns the builder, so that calls can be chained. Use it when no ready-made block fits, or when you want to write a part of a circuit and then use it in a larger composition. ```python from eigenkit import CircuitBuilder qc = CircuitBuilder(n_qubits=3) weights = qc.add_weights(6) inputs = qc.add_inputs(3) # Encoding. for q in range(3): qc.ry(inputs[q], qubit=q) # Two variational layers + CNOT ladder. for depth in range(2): for q in range(3): qc.ry(weights[depth * 3 + q], qubit=q) for q in range(2): qc.cnot(q, q + 1) spec = qc.build() ``` `spec` is a `CircuitSpec`, the same data type, independent of any provider, that `assemble()` returns. Pass it directly to `Circuit`: ```python from eigenkit import Circuit, all_z circuit = Circuit(blocks=spec, observables=all_z(3)) # numpy interface + sim by default ``` `Circuit` also accepts the builder itself and calls `build()` for you, so `Circuit(blocks=qc, observables=all_z(3))` is the same circuit. In both cases, the circuit gets the gates added so far: gates added to `qc` afterwards are not part of it. ## Declaring weights and inputs `add_weights(n)` and `add_inputs(n)` return lists of `WeightRef` and `InputRef`. These are symbolic handles to the trainable parameters and to the input vector. The gate methods also accept constants, which are wrapped as `Constant` automatically: ```python qc = CircuitBuilder(n_qubits=2) weights = qc.add_weights(2) # Trainable rotation. qc.rz(weights[0], qubit=0) # Constant rotation: 0.5 is wrapped as Constant(0.5). qc.rx(0.5, qubit=1) ``` ## Available gates | Method | Gate | Parameters | | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | ---------------- | | `rx(angle, qubit)` | `RX(angle)` | One `Parameter`. | | `ry(angle, qubit)` | `RY(angle)` | One `Parameter`. | | `rz(angle, qubit)` | `RZ(angle)` | One `Parameter`. | | `h(qubit)` | The Hadamard gate. | None. | | `cnot(control, target)` | The controlled-NOT (CNOT) gate. | None. | | `x(qubit)` | The Pauli `X` gate. | None. | | `z(qubit)` | The Pauli `Z` gate. | None. | | `mcx(*qubits)` | A multi-controlled `X` gate. Every qubit except the last is a control. | None. | | `mcz(*qubits)` | A multi-controlled `Z` gate. Every qubit except the last is a control. | None. | | `phase_shift(angle, qubit)` | A phase shift, which applies `e^(iφ)` to `\|1⟩` and leaves `\|0⟩` unchanged. | One `Parameter`. | | `swap(qubit0, qubit1)` | The SWAP gate. | None. | | `cry(angle, control, target)` | A controlled `RY` gate. | One `Parameter`. | | `s(qubit)` | The `S` gate, a quarter turn about `Z`. | None. | | `sdg(qubit)` | The `S†` gate, the inverse of `s()`. | None. | | `cphase(angle, control, target)` | A controlled phase shift, which is symmetric in its two qubits. | One `Parameter`. | | `pauli_exponential(paulis, angle, coefficient=1.0)` | `exp(-i · coefficient · angle · P)` for a Pauli product `P`, given as `(operator, qubit)` pairs on different qubits. | One `Parameter`. | `pauli_exponential` is the Trotter building block that the cost and mixer layers of the Quantum Approximate Optimization Algorithm (QAOA) and the excitation operators of unitary coupled cluster with singles and doubles (UCCSD) are made of. It is decomposed into the core set of gates. `to_block()` is not a gate. It wraps what has been built so far as a `CustomBlock`, so that a part of a circuit built gate by gate can be placed in a list of blocks. `add_weights(count)`, `add_inputs(count)` and `build()` are the other three methods that are not gates. ## Parameter expressions Gate angles are not raw values. They are expression trees that the backends evaluate when the circuit runs. The most common nodes are these: | Node | Meaning | | --------------------- | --------------------------------------------------------------------------------------- | | `Constant(0.5)` | A literal value. An `int` or a `float` is wrapped automatically. | | `WeightRef(i)` | The trainable weight number `i`. `add_weights()` returns these. | | `InputRef(i)` | The input number `i`. `add_inputs()` returns these. | | `Scale(expr, factor)` | The product `factor * expr`. | | `Negate(expr)` | The negation `-expr`. | | `Sum((a, b, ...))` | The sum of the expressions. | | `Multiply(a, b)` | The product of two expressions, for example `lambda * x` for trainable feature scaling. | You rarely need to name these nodes yourself. The handles from `add_weights()` and `add_inputs()` support ordinary Python arithmetic, and each operator builds exactly the node that its constructor would build. So the two ways of writing give the same intermediate representation, and you can mix them freely: ```python import math qc = CircuitBuilder(n_qubits=2) weights = qc.add_weights(2) inputs = qc.add_inputs(2) # 2*pi * x_0. qc.rx(2 * math.pi * inputs[0], qubit=0) # -w_0 (useful for adjoints). qc.rz(-weights[0], qubit=1) # w_0 + x_1. qc.ry(weights[0] + inputs[1], qubit=0) # Trainable feature scaling, w_1 * x_0. qc.rx(weights[1] * inputs[0], qubit=1) # A ZZ-style feature-map angle, 2 * (pi - x_0) * (pi - x_1). qc.cnot(0, 1) qc.phase_shift(2 * ((math.pi - inputs[0]) * (math.pi - inputs[1])), qubit=1) qc.cnot(0, 1) ``` `+`, `-`, `*`, `/` and the unary `-` work on every node, with plain numbers allowed on either side. Two details are worth knowing: - **Multiplication chooses the node for you.** A constant factor becomes a `Scale`, which a backend folds into the angle of the gate. Only a symbol times a symbol becomes a `Multiply`. It is easy to get this wrong when you build the nodes by hand, and nothing warns you when you do. - **Addition is flattened.** `a + b + c` is one `Sum` with three terms, not a `Sum` inside another `Sum`. You can divide only by a number (`x / 2` is `Scale(x, 0.5)`). The intermediate representation has no division node, so `x / y` raises a `TypeError`. The explicit constructors are still available from `eigenkit.core.ir`, for code that builds expression trees programmatically: ```python from eigenkit.core.ir import Multiply, Negate, Scale, Sum qc.rx(Scale(inputs[0], factor=2 * math.pi), qubit=0) # same as 2*pi * inputs[0] qc.rz(Negate(weights[0]), qubit=1) # same as -weights[0] qc.ry(Sum((weights[0], inputs[1])), qubit=0) # same as weights[0] + inputs[1] qc.rx(Multiply(weights[1], inputs[0]), qubit=1) # same as weights[1] * inputs[0] ``` On backends without their own automatic differentiation, a `Multiply` makes the gradient use the parameter-shift rule together with the chain rule. The gradients match the automatic differentiation of PennyLane to machine precision on PennyLane, Qiskit and Braket. CUDA Quantum does not support products of two symbols at the moment. ## Building an inversion circuit A common pattern is to build `U(x_1)` followed by the adjoint of `U(x_2)`, to read out the fidelity `|<0...0| U^dag(x_2) U(x_1) |0...0>|^2`. This is what `QuantumKernel` uses, and it serves any fidelity measure in the style of a swap test. ```python from eigenkit import Circuit, CircuitBuilder from eigenkit.core.ir import Negate n_features = 3 qc = CircuitBuilder(n_qubits=n_features) x1 = qc.add_inputs(n_features) x2 = qc.add_inputs(n_features) # Forward U(x_1). for q in range(n_features): qc.rx(x1[q], qubit=q) # Adjoint U^dag(x_2): reversed order, negated angles. for q in reversed(range(n_features)): qc.rx(Negate(x2[q]), qubit=q) circuit = Circuit(blocks=qc.build()) # No trainable weights here, so pass an empty weight vector; the data is inputs. print(circuit.zero_state_probability([], [0.4, -0.2, 0.6, 0.4, -0.2, 0.6])) # ~1.0 ``` See [Quantum Kernels](https://docs.kit.eigenorbit.com/user-guide/kernels/index.md) for the full implementation of the kernel. ## Inspecting the gates `circuit.draw()` returns a drawing, as text over several lines, of the circuit that you have just built. It is the easiest way to check a circuit written by hand before training: ```python print(circuit.draw()) ``` ```text q_0: ──RX(x_0)──RX(-x_3)── q_1: ──RX(x_1)──RX(-x_4)── q_2: ──RX(x_2)──RX(-x_5)── ``` The drawing works in the same way for circuits assembled from blocks. See [Basic Circuit](https://docs.kit.eigenorbit.com/examples/basic-circuit/index.md) for an example with `Encoding` and `Variational`. ## Composing with blocks `qc.to_block()` wraps the current state of the builder as a `CustomBlock`, which you can use anywhere a `Block` is accepted. The assembler moves the local `WeightRef` and `InputRef` indices of the block into the numbering of the whole circuit. In a list of blocks, you can also leave out `to_block()` and put `qc` in the list directly. The assembler then makes the same call for you. ```python from eigenkit import Circuit, CircuitBuilder, Variational, all_z from eigenkit.core.ir import Scale # Hand-written encoding: scale every input by 2*pi. qc = CircuitBuilder(n_qubits=4) inputs = qc.add_inputs(4) for q in range(4): qc.ry(Scale(inputs[q], factor=2 * 3.14159), qubit=q) my_encoding = qc.to_block() # Compose with a standard variational block. circuit = Circuit( blocks=[my_encoding, Variational(depth=2)], observables=all_z(4), ) ``` ## When to use the builder - You need a sequence of gates that no ready-made block expresses. - You want full control over the parameter expressions, with your own patterns of `Multiply`, `Sum` and `Scale`. - You want to write a part of a circuit gate by gate, and then use it in a list of blocks through `.to_block()`. - You are following a paper that uses a specific decomposition into gates. For the usual shape of an encoding followed by a variational block, prefer the block interface ([Encoding](https://docs.kit.eigenorbit.com/user-guide/encoding/index.md), [Variational](https://docs.kit.eigenorbit.com/user-guide/variational/index.md), [Reuploading](https://docs.kit.eigenorbit.com/user-guide/reuploading/index.md)). It is shorter, and it stays readable as the circuit grows. ## API reference ### `eigenkit.core.builder.CircuitBuilder` Build a `CircuitSpec` one gate at a time. Example ```python qc = CircuitBuilder(n_qubits=3) weights = qc.add_weights(6) inputs = qc.add_inputs(3) for qubit in range(3): qc.ry(inputs[qubit], qubit=qubit) for depth in range(2): for qubit in range(3): qc.ry(weights[depth * 3 + qubit], qubit=qubit) for qubit in range(2): qc.cnot(qubit, qubit + 1) spec = qc.build() ``` Parameters: | Name | Type | Description | Default | | ---------- | ----- | ----------------------------------------------------------------------------------------------------------------------- | ---------- | | `n_qubits` | `int` | The number of qubits of the circuit being built. Every qubit index that a gate uses must be between 0 and n_qubits - 1. | *required* | Raises: | Type | Description | | ------------ | ---------------------------- | | `ValueError` | If n_qubits is not positive. | #### `n_inputs` The number of input features that `add_inputs()` has reserved so far. The built CircuitSpec has the same number. #### `n_qubits` The number of qubits of the circuit, as given to the constructor. Every qubit index that a gate uses must be smaller than this number, and the number never changes. #### `n_weights` The number of trainable weights that `add_weights()` has reserved so far. The built CircuitSpec has the same number. #### `add_inputs(count)` Reserve `count` input features and return references to them. This is the counterpart of `add_weights()` for data instead of trainable parameters. Parameters: | Name | Type | Description | Default | | ------- | ----- | ---------------------------------------- | ---------- | | `count` | `int` | The number of input features to reserve. | *required* | Returns: | Type | Description | | ---------------- | ------------------------------------------------- | | `list[InputRef]` | One InputRef for each reserved feature, in order. | #### `add_weights(count)` Reserve `count` trainable weights and return references to them. Pass a returned `WeightRef` wherever a gate takes an angle. The indices are local to this builder and start at `0`. When the block is combined with others, `assemble` shifts them to their positions in the whole circuit. Parameters: | Name | Type | Description | Default | | ------- | ----- | --------------------------------- | ---------- | | `count` | `int` | The number of weights to reserve. | *required* | Returns: | Type | Description | | ----------------- | ------------------------------------------------- | | `list[WeightRef]` | One WeightRef for each reserved weight, in order. | #### `build()` Return the current state of the builder as an immutable `CircuitSpec`. #### `cnot(control, target)` Add a controlled-NOT (CNOT) gate. Parameters: | Name | Type | Description | Default | | --------- | ----- | --------------------------------- | ---------- | | `control` | `int` | The qubit that controls the flip. | *required* | | `target` | `int` | The qubit that is flipped. | *required* | Returns: | Type | Description | | ------------------ | ------------------------------------------- | | `'CircuitBuilder'` | This builder, so that calls can be chained. | Raises: | Type | Description | | ------------ | ----------------------------------------- | | `ValueError` | If control and target are the same qubit. | #### `cphase(angle, control, target)` Add a controlled phase shift gate. The gate is symmetric: the control and the target play the same role. Parameters: | Name | Type | Description | Default | | --------- | --------------- | ------------------------------------------------------------------------------- | ---------- | | `angle` | `ParameterLike` | The phase. It can be a number or a symbolic reference. | *required* | | `control` | `int` | One of the two qubits. | *required* | | `target` | `int` | The other qubit. The gate acts in the same way if the two qubits are exchanged. | *required* | Returns: | Type | Description | | ------------------ | ------------------------------------------- | | `'CircuitBuilder'` | This builder, so that calls can be chained. | #### `cry(angle, control, target)` Add a controlled `RY` gate. Parameters: | Name | Type | Description | Default | | --------- | --------------- | --------------------------------------------------------------- | ---------- | | `angle` | `ParameterLike` | The rotation angle. It can be a number or a symbolic reference. | *required* | | `control` | `int` | The qubit that controls the rotation. | *required* | | `target` | `int` | The qubit that is rotated. | *required* | Returns: | Type | Description | | ------------------ | ------------------------------------------- | | `'CircuitBuilder'` | This builder, so that calls can be chained. | #### `h(qubit)` Add a Hadamard gate. Parameters: | Name | Type | Description | Default | | ------- | ----- | ----------------- | ---------- | | `qubit` | `int` | The target qubit. | *required* | Returns: | Type | Description | | ------------------ | ------------------------------------------- | | `'CircuitBuilder'` | This builder, so that calls can be chained. | #### `mcx(*qubits)` Add a multi-controlled `X` gate. All the qubits except the last one are controls. Parameters: | Name | Type | Description | Default | | --------- | ----- | ------------------------------------------------------------------------------------- | ------- | | `*qubits` | `int` | Two or more distinct qubits. The controls come first, and the target is the last one. | `()` | Returns: | Type | Description | | ------------------ | ------------------------------------------- | | `'CircuitBuilder'` | This builder, so that calls can be chained. | Raises: | Type | Description | | ------------ | -------------------------------------------------------------- | | `ValueError` | If fewer than two qubits are given, or if a qubit is repeated. | #### `mcz(*qubits)` Add a multi-controlled `Z` gate. All the qubits except the last one are controls. Parameters: | Name | Type | Description | Default | | --------- | ----- | ------------------------------------------------------------------------------------- | ------- | | `*qubits` | `int` | Two or more distinct qubits. The controls come first, and the target is the last one. | `()` | Returns: | Type | Description | | ------------------ | ------------------------------------------- | | `'CircuitBuilder'` | This builder, so that calls can be chained. | Raises: | Type | Description | | ------------ | -------------------------------------------------------------- | | `ValueError` | If fewer than two qubits are given, or if a qubit is repeated. | #### `pauli_exponential(paulis, angle, coefficient=1.0)` Add the exponential of a product of Pauli operators. The gate is `exp(-i * coefficient * angle * P)`, where `P` is the product of `paulis`. Each element of `paulis` is a pair `(op, qubit)`, where `op` is `"X"`, `"Y"` or `"Z"`, and the qubits are distinct. This is the Trotter building block that the quantum approximate optimisation algorithm (QAOA) needs for its cost and mixer layers, and that the unitary coupled-cluster singles and doubles (UCCSD) ansatz needs for its excitation operators. It is built from the core gates only. First, `H` gates (for `X`) and `RX(π/2)` gates (for `Y`) change each factor to the `Z` basis. Then a ladder of controlled-NOT (CNOT) gates accumulates the parity on the last qubit, and a central `RZ(2 * coefficient * angle)` is applied (because `RZ(φ) = exp(-i φ Z / 2)`). Finally, the inverse of the first steps is applied. Parameters: | Name | Type | Description | Default | | ------------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `paulis` | `Sequence[tuple[str, int]]` | The Pauli product, as (operator, qubit) pairs, where the operator is "X", "Y" or "Z" and the qubits are distinct. An empty sequence is the identity, and nothing is added. | *required* | | `angle` | `ParameterLike` | The rotation angle. It can be a number or a symbolic reference. | *required* | | `coefficient` | `float` | A number that multiplies the angle. This is where the weight of a Hamiltonian term goes. Default: 1.0. | `1.0` | Returns: | Type | Description | | ------------------ | ------------------------------------------- | | `'CircuitBuilder'` | This builder, so that calls can be chained. | #### `phase_shift(angle, qubit)` Add a phase shift gate `R(φ)`. The gate multiplies `|1⟩` by `e^(iφ)` and leaves `|0⟩` unchanged. Parameters: | Name | Type | Description | Default | | ------- | --------------- | -------------------------------------------------------- | ---------- | | `angle` | `ParameterLike` | The phase φ. It can be a number or a symbolic reference. | *required* | | `qubit` | `int` | The target qubit. | *required* | Returns: | Type | Description | | ------------------ | ------------------------------------------- | | `'CircuitBuilder'` | This builder, so that calls can be chained. | #### `rx(angle, qubit)` Add a rotation about the `X` axis. Parameters: | Name | Type | Description | Default | | ------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ---------- | | `angle` | `ParameterLike` | The rotation angle. It can be a number, or a WeightRef, an InputRef or an expression, which is evaluated when the circuit is called. | *required* | | `qubit` | `int` | The target qubit. | *required* | Returns: | Type | Description | | ------------------ | ------------------------------------------- | | `'CircuitBuilder'` | This builder, so that calls can be chained. | #### `ry(angle, qubit)` Add a rotation about the `Y` axis. Parameters: | Name | Type | Description | Default | | ------- | --------------- | ------------------------------------------------------------------------------------- | ---------- | | `angle` | `ParameterLike` | The rotation angle. It can be a number, or a WeightRef, an InputRef or an expression. | *required* | | `qubit` | `int` | The target qubit. | *required* | Returns: | Type | Description | | ------------------ | ------------------------------------------- | | `'CircuitBuilder'` | This builder, so that calls can be chained. | #### `rz(angle, qubit)` Add a rotation about the `Z` axis. Parameters: | Name | Type | Description | Default | | ------- | --------------- | ------------------------------------------------------------------------------------- | ---------- | | `angle` | `ParameterLike` | The rotation angle. It can be a number, or a WeightRef, an InputRef or an expression. | *required* | | `qubit` | `int` | The target qubit. | *required* | Returns: | Type | Description | | ------------------ | ------------------------------------------- | | `'CircuitBuilder'` | This builder, so that calls can be chained. | #### `s(qubit)` Add an `S` gate, which is a quarter turn about the `Z` axis. Its inverse is `sdg()`. Parameters: | Name | Type | Description | Default | | ------- | ----- | ----------------- | ---------- | | `qubit` | `int` | The target qubit. | *required* | Returns: | Type | Description | | ------------------ | ------------------------------------------- | | `'CircuitBuilder'` | This builder, so that calls can be chained. | #### `sdg(qubit)` Add an S-dagger gate (`Sdg`), which is the inverse of `s()`. Parameters: | Name | Type | Description | Default | | ------- | ----- | ----------------- | ---------- | | `qubit` | `int` | The target qubit. | *required* | Returns: | Type | Description | | ------------------ | ------------------------------------------- | | `'CircuitBuilder'` | This builder, so that calls can be chained. | #### `swap(qubit0, qubit1)` Add a `SWAP` gate, which exchanges the states of two qubits. Parameters: | Name | Type | Description | Default | | -------- | ----- | ----------------- | ---------- | | `qubit0` | `int` | The first qubit. | *required* | | `qubit1` | `int` | The second qubit. | *required* | Returns: | Type | Description | | ------------------ | ------------------------------------------- | | `'CircuitBuilder'` | This builder, so that calls can be chained. | #### `to_block()` Wrap the current state of the builder in a `CustomBlock`. This is useful to build a part of a circuit one gate at a time, and then to combine it with other blocks. When the blocks are assembled, the indices of the `WeightRef` and `InputRef` objects of this block are shifted to their positions in the whole circuit. #### `x(qubit)` Add a Pauli `X` gate, which is a bit flip. Parameters: | Name | Type | Description | Default | | ------- | ----- | ----------------- | ---------- | | `qubit` | `int` | The target qubit. | *required* | Returns: | Type | Description | | ------------------ | ------------------------------------------- | | `'CircuitBuilder'` | This builder, so that calls can be chained. | #### `z(qubit)` Add a Pauli `Z` gate, which is a phase flip. Parameters: | Name | Type | Description | Default | | ------- | ----- | ----------------- | ---------- | | `qubit` | `int` | The target qubit. | *required* | Returns: | Type | Description | | ------------------ | ------------------------------------------- | | `'CircuitBuilder'` | This builder, so that calls can be chained. | # Datasets eigenkit includes a registry of standard datasets for classification, regression and time series, which you can use directly in quantum machine learning pipelines. The datasets need the `datasets` extra, which installs scikit-learn: `pip install eigenkit[datasets]`. This is enough to load a dataset and to call `DatasetReport.summarize()`. The plots of the exploratory data analysis (EDA), `DatasetReport.plot*`, also need matplotlib, which the `viz` extra installs. The `benchmark` extra installs it too. ## Load a dataset ```python from eigenkit.datasets import load_dataset data = load_dataset("iris") X_train, y_train = data["X_train"], data["y_train"] X_test, y_test = data["X_test"], data["y_test"] ``` To also get a validation split, pass `validation_size`: ```python data = load_dataset("iris", test_size=0.15, validation_size=0.15) X_val, y_val = data["X_val"], data["y_val"] ``` To get the whole dataset without a train/test split, pass `split=False`: ```python data = load_dataset("moons", split=False) X, y = data["X"], data["y"] ``` ### `load_dataset` parameters | Parameter | Type | Default | Description | | ----------------- | ------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `name` | `str` | Required | The name of the dataset in the registry. It is one of the names in the table below. | | `split` | `bool` | `True` | With `True`, the data is split into `X_train` and `X_test`. With `False`, the whole dataset is returned as `X` and `y`. | | `test_size` | `float` | `0.2` | The fraction of the samples held out for the test split. | | `validation_size` | `float` | `0.0` | The fraction of the samples held out for validation. Any value above `0` adds `X_val` and `y_val`. | | `random_state` | `int \| None` | `42` | The seed of the split, and of the generator for the synthetic datasets. With `None`, the split is not seeded. It does not affect the split of the `timeseries` datasets, which always follows the time order without shuffling. | | `cache_dir` | `str \| None` | `None` | The directory where the downloaded datasets are stored. With `None`, it is the `EIGENKIT_DATA_DIR` environment variable when it is set, and `~/.eigenkit/datasets` otherwise. | ## Available datasets The registry holds twelve datasets. The Reference column names the loader or the generator that produces the data. | Dataset | Task | Description | Reference | | ------------------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `iris` | classification | Fisher's iris dataset. The task is to classify iris flowers into three species from `4` measurements of the flowers. | `sklearn.datasets.load_iris` | | `wine` | classification | The wine recognition dataset. The task is to classify wines into three cultivars from `13` features of a chemical analysis. | `sklearn.datasets.load_wine` | | `breast_cancer_wisconsin` | classification | The Wisconsin diagnostic breast cancer dataset. The task is to classify tumours as malignant or benign from `30` real-valued features. | `sklearn.datasets.load_breast_cancer` | | `moons` | classification | Two half-circles that interleave. It is a binary benchmark whose classes cannot be separated by a straight line. | `generate_moons` | | `circles` | classification | Two concentric circles, a large one and a small one. It is a binary benchmark. | `generate_circles` | | `checkerboard` | classification | A checkerboard pattern. It is a binary benchmark with a very non-linear decision boundary. | `generate_checkerboard` | | `diabetes` | regression | The task is to predict a score of disease progression from `10` features. | `sklearn.datasets.load_diabetes` | | `california_housing` | regression | The task is to predict the median house value from `8` features. | `sklearn.datasets.fetch_california_housing` | | `concrete` | regression | The compressive strength of concrete, in megapascals. The task is to predict it from `8` features: the amounts of cement, slag, fly ash, water, superplasticiser, coarse aggregate and fine aggregate, and the age of the concrete. | [UC Irvine Machine Learning Repository, dataset 165](https://archive.ics.uci.edu/dataset/165/concrete+compressive+strength), by I-Cheng Yeh, under the [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) licence, downloaded from a [hosted CSV file](https://kit.eigenorbit.com/datasets/concrete.csv) | | `sine_wave` | timeseries | A sine wave with noise. The task is to predict the next value from a window of past values. | `generate_timeseries(kind="sine")` | | `ar_process` | timeseries | A synthetic autoregressive process of order `1`, written AR(1). Its dependence over time is more realistic than that of a sine wave. | `generate_timeseries(kind="ar")` | | `beijing_pm25` | timeseries | The hourly air quality in Beijing. The task is to predict the concentration of fine particulate matter (PM2.5) from the weather and the wind direction. | [UC Irvine Machine Learning Repository, dataset 381](https://archive.ics.uci.edu/dataset/381/beijing+pm2+5+data), by Song Xi Chen, under the [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) licence, downloaded from a [hosted CSV file](https://kit.eigenorbit.com/datasets/beijing_pm25.csv) | `list_datasets()` returns the same list when your code runs, with all the metadata of each dataset: ```python from eigenkit.datasets import list_datasets for ds in list_datasets(): print(ds.name, ds.task, ds.n_features, ds.n_classes) ``` ## Synthetic generators To choose the size and the seed of a synthetic dataset, call its generator function directly: ```python from eigenkit.datasets import ( generate_ad_hoc, generate_moons, generate_circles, generate_checkerboard, generate_regression, generate_timeseries, ) X, y = generate_moons(random_state=0) X, y = generate_circles(random_state=42) X, y = generate_regression(random_state=7) X, y = generate_ad_hoc(n_samples=200, n_qubits=2, gap=0.3, random_state=0) ``` `generate_ad_hoc` builds the "ad hoc" dataset of [Havlíček et al.](https://arxiv.org/abs/1804.11326), which is designed so that a quantum kernel separates it. Each point in `[0, 2π)^n` is encoded with the ZZ feature map, and its label is the sign of a `Z` parity measured after a fixed random unitary. The points closer than `gap` to the decision boundary are dropped, and the two classes have the same size. A kernel built from the same ZZ feature map classifies the data almost perfectly, and classical kernels usually do not. It is a test case for quantum kernels. It says nothing about an advantage on real data. ## Exploratory data analysis report and plots Use `DatasetReport` to run an exploratory data analysis in one call, on a built-in dataset or on your own data. ```python from eigenkit.datasets import DatasetReport, load_dataset data = load_dataset("iris", split=False) report = DatasetReport.from_data(data, theme="light") figures = report.plot_overview() summary = report.summarize() print(summary["text"]) ``` For your own data: ```python import numpy as np from eigenkit.datasets import DatasetReport X = np.random.randn(300, 6) y = (X[:, 0] + 0.3 * X[:, 1] > 0).astype(int) report = DatasetReport.from_data( X=X, y=y, feature_names=[f"f{i}" for i in range(X.shape[1])], task="classification", theme="light", ) figures = report.plot_overview(max_features=4) ``` For a real hourly time series, try the [Beijing PM2.5 dataset](https://archive.ics.uci.edu/dataset/381/beijing+pm2+5+data) (dataset 381 of the UC Irvine Machine Learning Repository). It is split in time order automatically, with the first `80%` of the samples for training and the last `20%` for testing: ```python data = load_dataset("beijing_pm25") X_train, y_train = data["X_train"], data["y_train"] X_test, y_test = data["X_test"], data["y_test"] ``` For a time-series target, pass a time index to get the seasonal analysis: ```python data = load_dataset("sine_wave", split=False) report = DatasetReport.from_data( data, task="timeseries", time_index=data["X"], # datetime-like index also supported theme="light", ) ts_fig = report.plot(kind="timeseries_target") ts_summary = report.summarize()["timeseries"] print(ts_summary["recommended_lags"]) ``` With `theme="dark"`, the backgrounds of the plots and the legends are transparent, and the text is white. ## The same style for your own figures `DatasetReport` draws inside a matplotlib context that gives every figure of the library the same palette, font sizes and spines. This context is public, so your own plots can use the same style. Examples are a training curve next to an EDA report, a figure for a paper, or the plots in the [worked examples](https://docs.kit.eigenorbit.com/examples/circles-hqnc/index.md): ```python import matplotlib import matplotlib.pyplot as plt from eigenkit.datasets import STYLE_COLORS, rc_params with matplotlib.rc_context(rc_params(theme="light")): fig, ax = plt.subplots(figsize=(7, 4)) ax.plot(model.train_losses_, color=STYLE_COLORS[0], label="train loss") ax.plot(model.validation_losses_, color=STYLE_COLORS[1], label="validation loss") ax.set_xlabel("epoch") ax.set_ylabel("loss") ax.legend() ``` | Name | What it is | | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `rc_params(theme="light")` | The settings for one figure, as a dictionary for `matplotlib.rc_context()`. The theme is `"light"` or `"dark"`, and any other value raises a `ValueError`. | | `STYLE_COLORS` | The palette, as a tuple of eight hexadecimal colour strings in the order of the colour cycle. `rc_params()` sets it as `axes.prop_cycle`, so index it only when a series needs a fixed colour. | | `FONTSIZE_AXES` | `20`, the font size of the axis labels and the titles. | | `FONTSIZE_LEGEND` | `18`, the font size of the legend entries. | | `FONTSIZE_TICKS` | `16`, the font size of the tick labels, which is also the base `font.size`. | `rc_params()` returns a new dictionary and never changes the global `matplotlib.rcParams`, so the settings apply only inside the `rc_context` block. Two calls with the same theme return equal dictionaries. This is why a figure can be drawn again with the other theme and differ only in its background and in the colour of its text and lines. ## API reference ### `eigenkit.datasets` The datasets of eigenkit: the registry of their metadata, and the loaders. #### `FONTSIZE_AXES = 20` The font size, in points, that `rc_params()` gives the axis labels and titles. #### `FONTSIZE_LEGEND = 18` The font size, in points, that `rc_params()` gives the legend entries. #### `FONTSIZE_TICKS = 16` The font size, in points, of the tick labels, which is also the base `font.size`. #### `STYLE_COLORS = ('#CE2173', '#AA5EFF', '#7D4ECF', '#43CDFF', '#3333FB', '#11E57A', '#E6C541', '#94A3B8')` The plot palette of the library, in the order of the colour cycle. `rc_params()` sets it as `axes.prop_cycle`, so a plot drawn inside that context uses these colours without naming them. Index it when a series needs a fixed colour, and not the next one in the cycle. For example, use `STYLE_COLORS[0]` for a training curve and `STYLE_COLORS[1]` for validation, so that the two curves keep their colours across figures. It is a tuple, so that an `append` by mistake cannot change how every later plot in the process looks. #### `DatasetInfo` Bases: `BaseModel` The metadata of a dataset that eigenkit provides. Parameters: | Name | Type | Description | Default | | --------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `name` | `str` | The unique name of the dataset, which is also its key in REGISTRY. | *required* | | `source` | `str` | Where the dataset comes from. It is one of these values: 'sklearn': the dataset is loaded with a function of sklearn.datasets. 'url': the dataset is downloaded from a web address. 'synthetic': the dataset is generated by a function, for example make_moons. | *required* | | `url` | `str` | The web address of the file to download. It is used only when source='url'. | `None` | | `sha256` | `str` | The SHA-256 hash, in hexadecimal, that the downloaded file must match. A downloaded file that does not match is deleted, and a ValueError is raised. A cached file that does not match is downloaded again. It is used only when source='url'. | `None` | | `sklearn_loader` | `str` | The name of the sklearn.datasets function, for example 'load_iris'. It is used only when source='sklearn'. | `None` | | `synthetic_generator` | `str` | The name of the generator function, for example 'make_moons'. It is used only when source='synthetic'. | `None` | | `task` | `str` | The type of task: 'classification', 'regression', 'timeseries', 'optimization' or 'chemistry'. | *required* | | `n_features` | `int` | The number of input features. It must be at least 1. | *required* | | `n_samples` | `int` | The total number of samples. It is None when the number is not recorded, for example for the synthetic datasets, whose size is chosen when they are generated. | `None` | | `n_classes` | `int` | The number of classes. It is None for regression and time-series tasks. | `None` | | `feature_names` | `list[str]` | Readable names of the input features. | `None` | | `target_names` | `list[str]` | Readable names of the classes, or of the output. | `None` | | `window_size` | `int` | The recommended size of the sliding window, for time-series datasets. | `None` | | `description` | `str` | A short description of the dataset. | *required* | Example ```python info = DatasetInfo( name="iris", source="sklearn", sklearn_loader="load_iris", task="classification", n_features=4, n_samples=150, n_classes=3, description="Classic multi-class classification dataset.", ) ``` #### `DatasetReport` An exploratory data analysis (EDA) report for a built-in or custom dataset. Parameters: | Name | Type | Description | Default | | --------------- | ----------- | ---------------------------------------------------------------------------------------------- | ---------- | | `X` | `ndarray` | The feature matrix, of shape (n_samples, n_features). | *required* | | `y` | `ndarray` | The target vector, of shape (n_samples,). | *required* | | `feature_names` | `list[str]` | Readable names of the features, in the order of the columns of X. | *required* | | `target_name` | `str` | The name of the target in the plots and the summary. | *required* | | `task` | `str` | The type of task, usually "classification", "regression" or "timeseries". | *required* | | `metadata` | `Any` | The metadata of the dataset, for example a DatasetInfo. | `None` | | `time_index` | `Any` | The time axis for the time-series plots. It can be numeric (array-like) or a pd.DatetimeIndex. | `None` | | `theme` | `str` | The theme of the plots, "light" or "dark". | `'light'` | Notes The recommended way to build a report is `from_data()`, which accepts the dictionaries returned by `load_dataset(...)` as well as your own `X` and `y`. Example ```python from eigenkit.datasets import DatasetReport, load_dataset data = load_dataset("iris", split=False) report = DatasetReport.from_data(data) figs = report.plot_overview() summary = report.summarize() ``` ##### `from_data(data=None, *, X=None, y=None, feature_names=None, target_name='target', task=None, time_index=None, theme='light')` Build a report from a dataset dictionary, or from your own arrays or tensors. Parameters: | Name | Type | Description | Default | | --------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `data` | `dict[str, Any]` | A dataset dictionary in the format of eigenkit, either {"X", "y"} or {"X_train", "y_train", "X_test", "y_test"}. The training and test sets are combined. | `None` | | `X` | `Any` | Your own feature matrix, as a NumPy array, a pandas object, a PyTorch tensor or another array-like. | `None` | | `y` | `Any` | Your own target vector, in the same formats as X. | `None` | | `feature_names` | `list[str]` | The names of the features. If they are not given, the names come from the metadata, or they are feature_0, feature_1 and so on. | `None` | | `target_name` | `str` | The name of the target in the plots and the summary. | `'target'` | | `task` | `str` | The type of task. If it is not given, it is taken from the metadata, or guessed from y. | `None` | | `time_index` | `Any` | The time axis for the time-series analysis. | `None` | | `theme` | `str` | The theme of the plots, "light" or "dark". | `'light'` | Returns: | Type | Description | | ----------------- | --------------------------------------------------- | | `'DatasetReport'` | The report, ready for plotting and for the summary. | Raises: | Type | Description | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------ | | `ValueError` | If the inputs are missing or do not agree, for example if the shapes do not match, or if feature_names does not have one name per feature. | ##### `plot(kind, **kwargs)` Build one plot, chosen by its kind. Parameters: | Name | Type | Description | Default | | ---------- | ----- | ------------------------------------------------------------------------------------------------------------------- | ---------- | | `kind` | `str` | The kind of plot: "data_health", "target", "distributions", "correlation", "feature_target" or "timeseries_target". | *required* | | `**kwargs` | `Any` | Options of the plot, such as bins, max_features and max_points. | `{}` | Returns: | Type | Description | | ----- | ---------------------- | | `Any` | The matplotlib figure. | Raises: | Type | Description | | ------------ | -------------------------------------- | | `ValueError` | If kind is not one of the kinds above. | ##### `plot_overview(*, max_features=None, bins=30, max_points=2000, path=None)` Build the default set of plots of the exploratory data analysis. Parameters: | Name | Type | Description | Default | | -------------- | ----- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | | `max_features` | `int` | The largest number of features shown in the panels about features. None shows every feature. | `None` | | `bins` | `int` | The number of histogram bins in the distribution plots. | `30` | | `max_points` | `int` | The largest number of points in the scatter plots. Larger datasets are subsampled. | `2000` | | `path` | \`str | Path\` | A directory where every overview plot is saved as a PNG file. With None, the plots are not saved. | Returns: | Type | Description | | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `dict[str, Any]` | A dictionary from each kind of plot to its matplotlib figure. The "timeseries_target" plot is included only for a time-series task. | ##### `summarize()` Build a reproducible summary of the dataset, with recommendations. Returns: | Type | Description | | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `dict[str, Any]` | A dictionary with statistics about the shape and the quality of the data, signals specific to the task when there are any, and a readable summary under the "text" key. | #### `generate_ad_hoc(n_samples=200, n_qubits=2, gap=0.3, random_state=42)` Generate the "ad hoc" dataset of Havlíček et al., Nature 567, 209 (2019). The dataset is designed so that a quantum kernel separates it. Each point `x` in `[0, 2π)^n` is encoded with the ZZ feature map, with two repetitions as in Qiskit's `ZZFeatureMap`. Its label is the sign of `⟨ψ(x)| V† Z^{⊗n} V |ψ(x)⟩` for a fixed random unitary `V`. The points closer than `gap` to the decision boundary are dropped, and the two classes are balanced. A kernel built from the same ZZ feature map classifies the data almost perfectly, and classical kernels usually do not. Parameters: | Name | Type | Description | Default | | -------------- | ------- | -------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | | `n_samples` | `int` | The number of points, split evenly between the two classes. | `200` | | `n_qubits` | `int` | The number of features, which is also the number of qubits of the circuit that generates the data. | `2` | | `gap` | `float` | The margin around the decision boundary, in \[0, 1). | `0.3` | | `random_state` | \`int | None\` | The seed of the unitary V and of the sampled points. | Returns: | Type | Description | | ------------------------- | ----------------------------------------------------------------------------------------------------------- | | `tuple[ndarray, ndarray]` | The pair (X, y), with X of shape (n_samples, n_qubits) and y of shape (n_samples,). The labels are 0 and 1. | Raises: | Type | Description | | ------------ | --------------------------------------------- | | `ValueError` | If gap is not in \[0, 1), or if n_qubits < 1. | #### `generate_checkerboard(n_samples=200, grid_size=2, random_state=42)` Generate a dataset with a checkerboard pattern. The points are drawn uniformly in `[0, grid_size]²`. The label is `(floor(x) + floor(y)) % 2`, which gives regions of alternating classes. Parameters: | Name | Type | Description | Default | | -------------- | ----- | ------------------------------------ | ------------------------------------------ | | `n_samples` | `int` | The number of points to generate. | `200` | | `grid_size` | `int` | The number of cells along each axis. | `2` | | `random_state` | \`int | None\` | The random seed, for reproducible results. | Returns: | Type | Description | | ------------------------- | ---------------------------------------------------------------------------- | | `tuple[ndarray, ndarray]` | The pair (X, y), with X of shape (n_samples, 2) and y of shape (n_samples,). | #### `generate_circles(n_samples=200, noise=0.05, factor=0.5, random_state=42)` Generate two concentric circles. Parameters: | Name | Type | Description | Default | | -------------- | ------- | ------------------------------------------------------------------- | ------------------------------------------ | | `n_samples` | `int` | The number of points to generate. | `200` | | `noise` | `float` | The standard deviation of the Gaussian noise added to the data. | `0.05` | | `factor` | `float` | The ratio of the inner circle to the outer circle, between 0 and 1. | `0.5` | | `random_state` | \`int | None\` | The random seed, for reproducible results. | Returns: | Type | Description | | ------------------------- | ---------------------------------------------------------------------------- | | `tuple[ndarray, ndarray]` | The pair (X, y), with X of shape (n_samples, 2) and y of shape (n_samples,). | #### `generate_moons(n_samples=200, noise=0.1, random_state=42)` Generate two half-circles that interleave, called moons. Parameters: | Name | Type | Description | Default | | -------------- | ------- | --------------------------------------------------------------- | ------------------------------------------ | | `n_samples` | `int` | The number of points to generate. | `200` | | `noise` | `float` | The standard deviation of the Gaussian noise added to the data. | `0.1` | | `random_state` | \`int | None\` | The random seed, for reproducible results. | Returns: | Type | Description | | ------------------------- | ---------------------------------------------------------------------------- | | `tuple[ndarray, ndarray]` | The pair (X, y), with X of shape (n_samples, 2) and y of shape (n_samples,). | #### `generate_regression(n_samples=200, n_features=4, noise=0.1, random_state=42)` Generate a synthetic regression dataset with scikit-learn's `make_regression`. Parameters: | Name | Type | Description | Default | | -------------- | ------- | ----------------------------------------------------------- | ------------------------------------------ | | `n_samples` | `int` | The number of samples to generate. | `200` | | `n_features` | `int` | The number of input features. | `4` | | `noise` | `float` | The standard deviation of the Gaussian noise on the output. | `0.1` | | `random_state` | \`int | None\` | The random seed, for reproducible results. | Returns: | Type | Description | | ------------------------- | ----------------------------------------------------------------------------------------------------------- | | `tuple[ndarray, ndarray]` | The pair (X, y), with X of shape (n_samples, n_features) and y of shape (n_samples,), both of type float32. | #### `generate_timeseries(n_samples=300, kind='sine', noise=0.05, random_state=42)` Generate a 1-D time series, as the pair `(t, y)`. Parameters: | Name | Type | Description | Default | | -------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | | `n_samples` | `int` | The number of time steps. | `300` | | `kind` | `str` | The shape of the series: "sine" for a sine wave with noise, or "ar" for an autoregressive process of order 1, written AR(1). | `'sine'` | | `noise` | `float` | The standard deviation of the Gaussian noise. | `0.05` | | `random_state` | \`int | None\` | The random seed, for reproducible results. | Returns: | Type | Description | | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `tuple[ndarray, ndarray]` | The pair (t, y), two arrays of shape (n_samples,) and type float32. t holds time values spaced evenly in [0, 1], and y is the signal. The arrays are not cut into windows, because the algorithms do that. | #### `get_dataset_info(name)` Return the `DatasetInfo` of a dataset. Parameters: | Name | Type | Description | Default | | ------ | ----- | ----------------------------------- | ---------- | | `name` | `str` | The key of the dataset in REGISTRY. | *required* | Returns: | Type | Description | | ------------- | ---------------------------- | | `DatasetInfo` | The metadata of the dataset. | Raises: | Type | Description | | ------------ | ----------------------------------------------------------------------- | | `ValueError` | If name is not in the registry. The message lists every available name. | Example ```python info = get_dataset_info("iris") ``` #### `list_datasets()` Return the metadata of every available dataset. Returns: | Type | Description | | ------------------- | ------------------------------------------------- | | `list[DatasetInfo]` | One DatasetInfo for each dataset in the registry. | Example ```python datasets = list_datasets() print(datasets[0].name) # 'breast_cancer_wisconsin' ``` #### `load_dataset(name, *, split=True, test_size=0.2, validation_size=0.0, random_state=42, cache_dir=None)` Load a dataset by name. Parameters: | Name | Type | Description | Default | | ----------------- | ------- | --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | | `name` | `str` | The name of the dataset in REGISTRY, for example "iris". | *required* | | `split` | `bool` | If True, the default, the data is split into a training set and a test set. | `True` | | `test_size` | `float` | The fraction of the samples used for the test set. | `0.2` | | `validation_size` | `float` | The fraction of the samples used for the validation set. With 0, the default, there is no validation set. | `0.0` | | `random_state` | \`int | None\` | The random seed of the split, and of the generator for the synthetic datasets. | | `cache_dir` | \`str | None\` | A cache directory for the downloaded datasets, in place of the default one. | Returns: | Type | Description | | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `dict` | A dictionary with these keys: With split=True: "X_train", "y_train", "X_test", "y_test" and "metadata", and also "X_val" and "y_val" when validation_size is above 0. With split=False: "X", "y" and "metadata". The synthetic time-series datasets also have a "sequence" key, which holds the whole series. "metadata" is the DatasetInfo of the dataset. | Raises: | Type | Description | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------- | | `ValueError` | If name is not in the registry, if validation_size is not in \[0, 1), or if test_size + validation_size is 1 or more with split=True. | Example ```python data = load_dataset("iris") X_train, y_train = data["X_train"], data["y_train"] ``` #### `rc_params(theme='light')` Return the matplotlib settings of the library for one plot, as rcParams. Every figure that the library draws uses these settings: `DatasetReport.plot_overview()`, the benchmark curves and the plots in the worked examples. This is what makes them look like one set. Pass the result to `matplotlib.rc_context()` and draw as usual. The settings apply only inside that block, because the returned dictionary is a new copy and the global `matplotlib.rcParams` is never changed. The two themes differ only in what has to change for a dark page. `"dark"` makes the backgrounds of the figure, the axes and the legend transparent, and makes the text, the ticks and the spines white. The sizes, the spines, the grid and STYLE_COLORS are the same in both themes, so a figure saved with each theme differs only in its background and in the colour of its text and lines. Parameters: | Name | Type | Description | Default | | ------- | ----- | ------------------------------------------------------------------- | --------- | | `theme` | `str` | "light" or "dark". The case and any surrounding spaces are ignored. | `'light'` | Returns: | Type | Description | | ------ | ------------------------------------------------------------- | | `dict` | The rcParams to draw with, ready for matplotlib.rc_context(). | Raises: | Type | Description | | ------------ | --------------------------------------- | | `ValueError` | If theme is neither "light" nor "dark". | Example ```python import matplotlib import matplotlib.pyplot as plt from eigenkit.datasets import STYLE_COLORS, rc_params with matplotlib.rc_context(rc_params(theme="light")): fig, ax = plt.subplots(figsize=(7, 4)) ax.plot(history, color=STYLE_COLORS[0], label="train loss") ax.legend() fig.savefig("loss.png", bbox_inches="tight") ``` # Encoding The `Encoding` block maps classical features to quantum states, with one single-qubit rotation per feature. ```python from eigenkit import Encoding encoding = Encoding(in_features=4, rotation="RX") ``` ## Parameters | Parameter | Type | Default | Description | | ------------- | ------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | `in_features` | `int` | Required | The number of features to encode, one per qubit. It must be positive. | | `rotation` | `"RX"` | `"RY"` | `"RZ"` | | `scale` | `float` | `1.0` | A factor that multiplies every input angle. It is useful when the data is normalised to `[0, 1]` and you want angles in `[0, 2π]`. It must be finite. | The arguments are checked when the block is created A non-positive `in_features` is rejected immediately: `Encoding(in_features=0)` raises `ValueError: in_features must be positive, got 0`. A `rotation` other than `RX`, `RY` or `RZ`, and a `scale` that is not finite, are rejected in the same way. Every block in the library checks its arguments like this. `Encoding` is the name exported from the top level and from `eigenkit.configs`. The class itself is AngleEncoding, which is the name that appears in tracebacks and in the heading of the API reference below. ## Methods | Method | What it does | | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | `required_qubits()` | Returns the number of qubits that the block needs, which is `in_features`. | | `build(n_qubits)` | Builds the gates of the block for a circuit with `n_qubits` qubits, as an `IRFragment`. `assemble()` calls it, so you rarely call it yourself. | | `to_dict()` | Returns a description of the block that can be saved as JSON, including a `type` key. | | `from_dict(d)` | A class method that rebuilds the block from the output of `to_dict()`. | The `.eigenkit` bundle format relies on `to_dict()` and `from_dict()` to save and load blocks. See [Model persistence](https://docs.kit.eigenorbit.com/user-guide/model-persistence/index.md). ## How the number of qubits is found The number of qubits comes from `Encoding.in_features`. Every block reports the number of qubits that it needs through `required_qubits()`. `assemble()` combines a list of blocks into one `CircuitSpec`: it takes the largest of these numbers, and it concatenates the weights and the inputs of the blocks in the order of the blocks. `Circuit` calls `assemble()` for you when you pass `blocks=[...]`. See [Circuit Builder](https://docs.kit.eigenorbit.com/user-guide/circuit-builder/index.md) to work with a `CircuitSpec` directly. ```python encoding = Encoding(in_features=6, rotation="RZ") # This circuit uses 6 qubits. ``` ## Rotation gates - `"RX"` applies `RX(feature_value)` to each qubit. - `"RY"` applies `RY(feature_value)` to each qubit. - `"RZ"` applies `RZ(feature_value)` to each qubit. For data in a bounded range that does not cover `[0, 2π]`, use the `scale` argument, or normalise the data first with `MinMaxNorm`: ```python from eigenkit import MinMaxNorm import torch norm = MinMaxNorm(feature_dim=4, min_val=0.0, max_val=torch.pi) ``` ## API reference ### `eigenkit.configs.Encoding` Encode `in_features` data values as single-qubit rotations. Feature `i` sets the angle of one rotation on qubit `i`, so the block uses the qubits `0` to `in_features - 1`. This matters only when the block is combined with others. A circuit has as many qubits as its widest block, and when another block in the list needs more qubits, this block leaves the extra qubits unchanged. The block takes `in_features` inputs and no weights. Parameters: | Name | Type | Description | Default | | ------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `in_features` | `int` | The number of features to encode, one per qubit. It must be positive, and a ValueError is raised otherwise. | *required* | | `rotation` | `GateName` | The rotation gate: "RX", "RY" or "RZ". Default: "RY". | `'RY'` | | `scale` | `float` | A constant factor applied to every input angle. It is useful when the data is normalised to [0, 1] and the circuit needs angles in [0, 2π]. It must be finite. Default: 1.0. | `1.0` | #### `build(n_qubits)` Build the gates of this block for a circuit with `n_qubits` qubits. The qubits with an index of `in_features` or more are left unchanged. Parameters: | Name | Type | Description | Default | | ---------- | ----- | ------------------------------------------------------------------------------------------------------------------- | ---------- | | `n_qubits` | `int` | The number of qubits of the circuit being assembled, which is the number of qubits of the widest block in the list. | *required* | Returns: | Type | Description | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `IRFragment` | The gates of the block, as an IRFragment. Its weight and input indices start at 0, and they are shifted when the blocks are combined into a circuit. | #### `from_dict(d)` Rebuild a block from the output of `to_dict()`. Parameters: | Name | Type | Description | Default | | ---- | ------ | ----------------------------------- | ---------- | | `d` | `dict` | A dictionary returned by to_dict(). | *required* | Returns: | Type | Description | | ----------------- | -------------------------- | | `'AngleEncoding'` | The rebuilt AngleEncoding. | #### `required_qubits()` Return `in_features`, because the block uses one qubit per feature. #### `to_dict()` Return a description of this block that can be saved as JSON. When a saved model is loaded, the `type` key selects the block class whose `from_dict()` rebuilds the block. Returns: | Type | Description | | ------ | -------------------------------------- | | `dict` | The fields of the block, and its type. | # Estimators eigenkit includes quantum estimators that are compatible with sklearn and built on PyTorch. They are hybrid classifiers and regressors, their recurrent versions, wrappers around your own networks, and adapters for time series. Every estimator follows the standard sklearn interface, `fit(X, y)`, `predict(X)` and `score(X, y)`, and works with `GridSearchCV`, `Pipeline`, `clone` and the rest of sklearn. Import them from the top level: `from eigenkit import HQNC`. **Install:** these estimators need the `torch` extra (`pip install eigenkit[torch]`). They run on the built-in NumPy simulator. Another backend needs its own extra, for example `eigenkit[pennylane]` for `provider="pennylane"`. You can save a fitted model to a **`.eigenkit`** bundle, which is a zip file, and load it again with `save()` and `load()`, or with `load_model()`. See [Model persistence](https://docs.kit.eigenorbit.com/user-guide/model-persistence/index.md) for the format of the bundle, the supported classes, and how the benchmark uses `save_dir`. How to get the trained network: `model_` `fit()` trains a deep copy of the network, so it never changes the module that you gave to the estimator. After training, the object that you passed to `GenericClassifier(model=...)` therefore still has its initial weights, whatever the network did. This comes from the copy, and says nothing about the model. `model_` returns the network that was actually trained. It exists on every estimator that trains a PyTorch network (all the estimators of this page except the time-series wrappers), and it raises `NotFittedError` before `fit()`. ```python from torch import nn from eigenkit import Basic, GenericClassifier from eigenkit.datasets import load_dataset data = load_dataset("iris") X_train, y_train = data["X_train"], data["y_train"] layer = Basic(n_qubits=4, depth=2) clf = GenericClassifier(model=nn.Sequential(layer, nn.Linear(4, 3)), in_features=4) clf.fit(X_train, y_train) layer.state_dict() # unchanged, this object never trained clf.model_[0].state_dict() # the trained weights ``` ## Classifiers `HQNC` uses the PyTorch loss `CrossEntropyLoss`, and `train_losses_` stores the cross-entropy of each epoch. When you pass validation data with `fit(..., X_val=..., y_val=...)`, `validation_losses_` stores the matching validation curve. `n_classes` is optional on every classifier of this section. If you leave it out, the set of labels is inferred from `y` when you call `fit()`. The examples of this section use the built-in `iris` dataset. It has `4` features and `3` classes, which is what the constructors below are sized for: ```python from eigenkit.datasets import load_dataset data = load_dataset("iris") X_train, y_train = data["X_train"], data["y_train"] X_test, y_test = data["X_test"], data["y_test"] ``` ### HQNC `HQNC` is the hybrid quantum neural network classifier. It places a quantum layer in a small `nn.Sequential` pipeline, and trains the whole pipeline together with the cross-entropy loss. ```python from eigenkit import HQNC model = HQNC( in_features=4, depth=2, n_classes=3, epochs=30, ) model.fit(X_train, y_train) predictions = model.predict(X_test) print(model.score(X_test, y_test)) # accuracy print(model.train_losses_) # per-epoch loss ``` You choose the quantum layer with `layer_type`: | `layer_type` | Quantum layer | Notes | | ------------------- | --------------------------------------------- | ------------------------------------------- | | `"Basic"` (default) | One encoding block and one variational block. | It needs `n_qubits == in_quantum`. | | `"Reupload"` | Standard data reuploading. | It needs `n_qubits == in_quantum`. | | `"Ensemble"` | `N` independent circuits. | It needs `n_qubits` to divide `in_quantum`. | Note: if `n_qubits` and `in_quantum` differ for `Basic` or `Reupload`, a linear adapter `Linear(in_features, n_qubits)` is added in front automatically, and a `UserWarning` is raised. `enc_rotation`, `var_rotation`, `variational_type` and `basis` are passed to that layer unchanged. Their default is `None`, which means "the value that the layer you named uses". So the architecture that an estimator builds is the one that the [layer page](https://docs.kit.eigenorbit.com/user-guide/layers/index.md) describes, and if you build it yourself from that page, you get the same model. Pass any of these four arguments to replace the default of the layer. ```python # Ensemble of 4-qubit circuits over 16 features. model = HQNC(in_features=16, n_qubits=4, layer_type="Ensemble", depth=2) ``` ### RecurrentHQNC `RecurrentHQNC` is a hybrid classifier for sequences. An encoder that is a recurrent neural network (RNN), of type `"rnn"`, `"gru"` or `"lstm"`, reads the sequence and passes the encoded vector to a quantum layer like the one of `HQNC`. The whole model is trained together, with one optimiser and one loss, so the RNN and the quantum block are updated at the same time. `X` must be 3-D, with shape `(n_samples, seq_len, seq_feature_dim)`. By default, only the last time step of the RNN goes to the quantum block. To flatten the whole sequence instead, set `rnn_return_last_timestep=False` and also give `sequence_length`. ```python import numpy as np from eigenkit import RecurrentHQNC X = np.random.randn(32, 10, 4).astype(np.float32) y = np.where(X[:, -1, 0] > 0, "up", "down") model = RecurrentHQNC( seq_feature_dim=4, rnn_type="gru", rnn_hidden_dim=16, depth=2, epochs=30, ) model.fit(X, y) predictions = model.predict(X) probabilities = model.predict_proba(X) ``` The quantum layer takes the same arguments as in `HQNC`: `layer_type`, `n_qubits`, `enc_rotation`, `var_rotation`, `variational_type` and `basis`. See the [table of `layer_type` values of `HQNC`](#hqnc) above. The other options of the RNN are `rnn_num_layers`, `rnn_dropout` and `rnn_bidirectional`. ### CustomQuantumClassifier `CustomQuantumClassifier` builds its architecture from any list of blocks. Use it when none of the `layer_type` presets fits: ```python from eigenkit import ( Encoding, Variational, StrongVariational, all_z, ) from eigenkit import CustomQuantumClassifier blocks = [ Encoding(in_features=4, rotation="RY"), StrongVariational(depth=2), Variational(depth=1, rotation="RX"), ] model = CustomQuantumClassifier( blocks=blocks, observables=all_z(4), n_classes=2, epochs=30, ) # This estimator is configured for two classes, so keep iris labels 0 and 1. binary = y_train < 2 model.fit(X_train[binary], y_train[binary]) predictions = model.predict(X_test) ``` `observables=` accepts the same forms as `Circuit(observables=...)` (see [Observables](https://docs.kit.eigenorbit.com/user-guide/observables/index.md)). When the number of features of the data differs from the input size of the circuit, an `nn.Linear` adapter is added in front automatically. The input size of the circuit is inferred from the first `Encoding` or `Reuploading` block. ## Regressors The examples of this section use the built-in `concrete` dataset. It has `8` features and a continuous target. The examples use only part of the data, so that they run quickly: ```python from eigenkit.datasets import load_dataset data = load_dataset("concrete") X_train, y_train = data["X_train"][:200], data["y_train"][:200] X_test, y_test = data["X_test"][:50], data["y_test"][:50] ``` ### HQNR `HQNR` is the regression version of `HQNC`. Its output is a single neuron, trained with the regression loss that you choose. ```python from eigenkit import HQNR model = HQNR(in_features=8, depth=2, epochs=50) model.fit(X_train, y_train) predictions = model.predict(X_test) print(model.score(X_test, y_test)) # R^2 ``` It takes the same `layer_type` and `n_qubits` arguments as `HQNC`. You choose the loss with `loss_func`: `"mse"`, `"mae"` or `"huber"`. ### RecurrentHQNR `RecurrentHQNR` is a hybrid regressor for sequences. An RNN encoder, of type `"rnn"`, `"gru"` or `"lstm"`, reads the sequence and passes the encoded vector to a quantum layer like the one of `HQNR`. The whole model is trained together, with one optimiser and one loss, so the RNN and the quantum block are updated at the same time. `X` must be 3-D, with shape `(n_samples, seq_len, seq_feature_dim)`. By default, only the last time step of the RNN goes to the quantum block. To flatten the whole sequence instead, set `rnn_return_last_timestep=False` and also give `sequence_length`. ```python import numpy as np from eigenkit import RecurrentHQNR X = np.random.randn(32, 10, 4).astype(np.float32) last = X[:, -1, :] y = ( 0.5 * last[:, 0] + 0.3 * np.sin(last[:, 1]) + 0.2 * np.cos(2 * np.pi * last[:, 2]) + 0.1 * last[:, 3] * last[:, 0] ).astype(np.float32) model = RecurrentHQNR( seq_feature_dim=4, rnn_type="gru", rnn_hidden_dim=16, depth=2, epochs=30, ) model.fit(X, y) predictions = model.predict(X) ``` ### CustomQuantumRegressor `CustomQuantumRegressor` is the regression version of `CustomQuantumClassifier`. It builds the circuit from a list of blocks, and adds a `Linear` head with a single neuron, trained with the regression loss that you choose. ## Generic estimators `GenericClassifier` and `GenericRegressor` are wrappers, compatible with sklearn, that train **any** `torch.nn.Module` through the standard `fit` and `predict` interface. `HQNC` and `HQNR` build the architecture of the model for you. The generic estimators instead expect a complete network, output head included, so you have full control over the architecture. Pre-trained weights and the behaviour of `fit()` `fit()` does not start from pre-trained weights. It copies the `nn.Module`, and then calls `reset_parameters()` on every submodule that has this method (such as `nn.Linear` and the quantum layers of eigenkit), seeded with `random_state`. So the weights of these layers are drawn again before training, even if the module that you pass already holds trained weights. Each later call to `fit()` (for example inside cross-validation, or after `clone()`) also starts again from new weights, not from the weights of the previous `fit()`. To keep the trained weights, use the fitted estimator, or save it with `save()` and load it again with `load()`, and do not call `fit()` on it again. `random_state` does not reseed your process Every estimator applies `random_state` inside `fit()` and `load()` on a private copy of the state of the PyTorch random number generators, that is the generator of the central processing unit (CPU) and the generator of every CUDA device. Two runs with the same `random_state` give the same model, on the CPU and on the graphics processing unit (GPU), and neither run changes a seed that you set yourself. Only `model` is required. If you leave out `in_features` (default `None`), the number of features is taken from the data. `sequence_input` (default `False`) tells the estimator that `X` has shape `(batch, timesteps, features)` instead of `(batch, features)`. The training arguments are `lr` (`0.01`), `optimizer` (`"adam"`), `optimizer_kwargs` (`None`), `epochs` (`50`), `batch_size` (`32`), `show_progress` (`True`) and `random_state` (`42`). `GenericClassifier` adds `n_classes`, and `GenericRegressor` adds `loss_func`, `huber_delta` and `huber_reduction`. None of these arguments is about the circuit, because the network is yours. The examples below use `moons` for the classifier and `diabetes` for the regressor, which match the input sizes of their networks: ```python from eigenkit.datasets import load_dataset moons = load_dataset("moons") X_train, y_train = moons["X_train"], moons["y_train"] X_test, y_test = moons["X_test"], moons["y_test"] diabetes = load_dataset("diabetes") X_reg, y_reg = diabetes["X_train"][:200], diabetes["y_train"][:200] X_reg_test, y_reg_test = diabetes["X_test"][:50], diabetes["y_test"][:50] ``` ### GenericClassifier ```python from torch import nn from eigenkit import GenericClassifier from eigenkit import Basic N_QUBITS, N_CLASSES = 4, 2 clf_net = nn.Sequential( nn.Linear(2, N_QUBITS), nn.Tanh(), Basic(n_qubits=N_QUBITS, depth=2), nn.Linear(N_QUBITS, N_CLASSES), ) clf = GenericClassifier( model=clf_net, in_features=2, epochs=30, lr=0.01, batch_size=16, random_state=42, ) clf.fit(X_train, y_train) print(clf.score(X_test, y_test)) # accuracy labels = clf.predict(X_test) probs = clf.predict_proba(X_test) # shape (n_samples, n_classes) ``` The `forward()` method of the network must return logits of shape `(batch, n_classes)`. The wrapper encodes the labels automatically, so `y` can hold integers, strings or any other hashable labels. `in_features` is optional but recommended. When it is set, `fit` raises an error at once if the shape of the data does not match. ### GenericRegressor ```python from torch import nn from eigenkit import GenericRegressor from eigenkit import Basic N_QUBITS_REG, IN_FEATURES = 6, 10 reg_net = nn.Sequential( nn.Linear(IN_FEATURES, N_QUBITS_REG), nn.Tanh(), Basic(n_qubits=N_QUBITS_REG, depth=2), nn.Linear(N_QUBITS_REG, 1), ) reg = GenericRegressor( model=reg_net, in_features=IN_FEATURES, loss_func="mse", epochs=50, lr=0.01, batch_size=32, random_state=42, ) reg.fit(X_reg, y_reg) print(reg.score(X_reg_test, y_reg_test)) # R² preds = reg.predict(X_reg_test) # float32 array, shape (n_samples,) ``` The `forward()` method of the network must return shape `(batch,)` or `(batch, 1)`. The losses available by name are `"mse"`, `"mae"` and `"huber"`. Any callable `(output, target) -> scalar_tensor` is also accepted, but a model trained with a callable loss cannot be saved. ### Save and load Generic estimators use the same `.eigenkit` bundle format as the other estimators, with one difference: the bundle does not store the architecture. You must pass a module with the **same `nn.Module` structure** to `.load()`: ```python clf.save("clf_hybrid.eigenkit") clf_net_fresh = nn.Sequential( nn.Linear(2, N_QUBITS), nn.Tanh(), Basic(n_qubits=N_QUBITS, depth=2), nn.Linear(N_QUBITS, N_CLASSES), ) clf_loaded = GenericClassifier.load("clf_hybrid.eigenkit", model=clf_net_fresh) ``` `load_model()` also works for generic estimators, but the `model=` argument is required, because the bundle does not store the architecture and it cannot be inferred. If you call either method without `model=`, it raises `SerializationError`. ### sklearn compatibility `GenericClassifier` inherits from `ClassifierMixin` and `GenericRegressor` from `RegressorMixin`, so both work with `Pipeline`, `cross_val_score`, `GridSearchCV` and `clone`: ```python from sklearn.base import clone from sklearn.model_selection import cross_val_score scores = cross_val_score(clf, X_train, y_train, cv=3, scoring="accuracy") cloned = clone(clf) # unfitted copy with the same hyperparameters ``` See the [Generic Estimators example](https://docs.kit.eigenorbit.com/examples/generic-estimators/index.md) for a complete example. ## Time series `QuantumTimeSeriesRegressor` and `QuantumTimeSeriesClassifier` wrap an inner estimator and apply a sliding window to data in time order. Each row of `X` is one time step. With a window of length `in_size`, the inner model receives a 3-D batch of shape `(n_windows, in_size, n_features)`, and the window that starts at row `i` predicts `y[i + in_size - 1 + shift]`. With `autoregressive=True`, an extra channel is added to each time step, and it holds the target value at that step. For tabular estimators (`HQNR`, `CustomQuantumRegressor`, and so on), the wrapper flattens each window before it calls `fit` or `predict`. For `RecurrentHQNR` and `RecurrentHQNC`, and for the generic estimators with `sequence_input=True`, the windows stay 3-D, so that the RNN sees the time axis. ```python import numpy as np from eigenkit import ( QuantumTimeSeriesRegressor, RecurrentHQNR, ) n_feat = 5 window = 10 X = np.random.randn(200, n_feat).astype(np.float32) y = X[:, 0] + 0.1 * np.random.randn(200).astype(np.float32) inner = RecurrentHQNR( seq_feature_dim=n_feat, rnn_type="gru", rnn_hidden_dim=16, depth=2, ) model = QuantumTimeSeriesRegressor(estimator=inner, in_size=window, shift=1) model.fit(X, y) predictions = model.predict(X) # length n_timesteps - in_size - shift + 1 ``` Classification uses the same alignment of the windows: ```python import numpy as np from eigenkit import ( QuantumTimeSeriesClassifier, RecurrentHQNC, ) n_feat = 5 window = 10 X = np.random.randn(200, n_feat).astype(np.float32) y = np.where(X[:, 0] > 0, "up", "down") inner = RecurrentHQNC( seq_feature_dim=n_feat, rnn_type="gru", rnn_hidden_dim=16, depth=2, ) model = QuantumTimeSeriesClassifier(estimator=inner, in_size=window, shift=1) model.fit(X, y) predictions = model.predict(X) # length n_timesteps - in_size - shift + 1 probabilities = model.predict_proba(X) ``` **Autoregressive mode.** Set `autoregressive=True` to add the past values of the target to each window, as an extra channel of features. This extra channel counts as a feature: the inner estimator must be built with `seq_feature_dim = n_feat + 1`, and if you reuse the one above, you get `ValueError: Last dimension of X must equal seq_feature_dim=5, got 6`. When you predict, you must pass the known history as `y_history`: ```python # The appended target channel makes it n_feat + 1 inputs, so this needs its # own inner estimator. inner_ar = RecurrentHQNC( seq_feature_dim=n_feat + 1, rnn_type="gru", rnn_hidden_dim=16, depth=2, ) # Fit on the head of the series, predict on the tail. X_hist, y_hist = X[:150], y[:150] X_future, y_future = X[150:], y[150:] model_ar = QuantumTimeSeriesClassifier( estimator=inner_ar, in_size=window, shift=1, autoregressive=True, ) model_ar.fit(X_hist, y_hist) predictions = model_ar.predict(X_future, y_history=y_future) probabilities = model_ar.predict_proba(X_future, y_history=y_future) ``` ## sklearn compatibility Every algorithm class is a subclass of `sklearn.base.BaseEstimator`, and stores each argument of its constructor as an attribute of the same name on `self`, as sklearn expects. This is what makes `clone`, `Pipeline`, `cross_val_score` and `GridSearchCV` work without extra code. ```python from sklearn.base import clone from sklearn.model_selection import cross_val_score cloned = clone(model) scores = cross_val_score(model, X, y, cv=5) ``` ```python from sklearn.model_selection import GridSearchCV from eigenkit import HQNC from eigenkit.datasets import load_dataset iris = load_dataset("iris") binary = iris["y_train"] < 2 # the estimator below declares two classes X_iris, y_iris = iris["X_train"][binary], iris["y_train"][binary] grid = GridSearchCV( HQNC(in_features=4, n_classes=2), param_grid={"depth": [1, 2], "epochs": [20, 40]}, cv=3, ) grid.fit(X_iris, y_iris) best = grid.best_estimator_ ``` `BaseQuantumClassifier` inherits from `ClassifierMixin`, so its `score()` returns the accuracy. `BaseQuantumRegressor` inherits from `RegressorMixin`, so its `score()` returns `R²`. Import the base classes when you build your own estimators: ```python from eigenkit import ( BaseQuantumClassifier, BaseQuantumEstimator, BaseQuantumRegressor, ) ``` ## Choosing a backend Every estimator accepts the same `provider=` argument as `Circuit`: the name of a registered backend as a string, a configuration dataclass, or a `Backend` instance. ```python from eigenkit import PennyLaneConfig # String. Requires the matching extra, here `qiskit`. HQNC(in_features=4, depth=2, provider="qiskit") # Config: forwards device-level options. Requires the `pennylane` extra. HQNC( in_features=4, depth=2, provider=PennyLaneConfig(device_name="lightning.qubit", shots=None), ) ``` ## API reference ### `eigenkit.qml.BaseQuantumEstimator` Bases: `BaseEstimator`, `ABC` Base class, compatible with scikit-learn, for all quantum algorithms. Parameters: | Name | Type | Description | Default | | -------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `provider` | `ProviderSpec` | The backend. It can be a registered name ("pennylane", "qiskit", "braket", or any name passed to register()), a configuration instance (for example PennyLaneConfig(device_name=...)), or a backend instance. | `'numpy'` | | `random_state` | \`int | None\` | The seed for reproducibility, applied inside fit() and load(). What it seeds depends on the estimator. The PyTorch estimators isolate the random number generator of PyTorch, so seeding one of them does not reseed the random stream of the caller. The kernel estimators are deterministic, and they take it only so that all the estimators have the same interface. | #### `fit(X, y)` Fit the estimator on `X` and `y`. Parameters: | Name | Type | Description | Default | | ---- | --------- | -------------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | | `y` | `ndarray` | One target for each sample, of shape (n_samples,). | *required* | Returns: | Type | Description | | ------------------------ | --------------------------------------------- | | `'BaseQuantumEstimator'` | This estimator, so that calls can be chained. | #### `load(path, *, provider=None)` Load a fitted model from a `.eigenkit` bundle. Load only bundles that you trust. A bundle can contain joblib pickles, and unpickling runs code, as with `torch.load` or `joblib.load`. Parameters: | Name | Type | Description | Default | | ---------- | -------------- | ----------- | --------------------------------------- | | `path` | \`str | Path\` | The path of the bundle. | | `provider` | \`ProviderSpec | None\` | A provider that replaces the saved one. | Returns: | Type | Description | | ------------------------ | --------------------- | | `'BaseQuantumEstimator'` | The loaded estimator. | Raises: | Type | Description | | ----------- | ---------------------------------------------------------------------------------------------------------------------------- | | `TypeError` | If the method is called on a subclass and the bundle holds a different class. BaseQuantumEstimator.load() accepts any class. | #### `predict(X)` Predict a value for every sample in `X`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | Returns: | Type | Description | | --------- | ------------------------------- | | `ndarray` | One prediction for each sample. | #### `save(path)` Save a fitted model to a `.eigenkit` bundle. The bundles of models that hold scikit-learn objects (`QSVC`, `QSVR`, the reservoir estimators and the time-series estimators) contain joblib pickles, so loading one can run code. Share them only with people who trust you. Parameters: | Name | Type | Description | Default | | ------ | ----- | ----------- | ------------------------------------------------------------------------- | | `path` | \`str | Path\` | The path of the bundle. The suffix .eigenkit is added when it is missing. | ### `eigenkit.qml.BaseQuantumClassifier` Bases: `ClassifierMixin`, `BaseQuantumEstimator` Base class for quantum classifiers. Its `score()` returns the accuracy. #### `predict_proba(X)` Predict the class probabilities. Subclasses should return the probabilities in the order of `classes_`, with shape `(n_samples, n_classes)`. The default implementation raises a `NotImplementedError`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | Returns: | Type | Description | | --------- | ----------------------------------------------------------------------------------------------- | | `ndarray` | The probabilities, of shape (n_samples, n_classes), with the columns in the order of classes\_. | ### `eigenkit.qml.BaseQuantumRegressor` Bases: `RegressorMixin`, `BaseQuantumEstimator` Base class for quantum regressors. Its `score()` returns `R²`. ### `eigenkit.torch.estimators` The PyTorch estimators: differentiable hybrid quantum-classical models. The estimator base classes compatible with scikit-learn, the quantum kernels, the support vector machines and the saving and loading of models are in a separate package, `eigenkit.qml`, which does not need PyTorch. The base classes and `load_model` are also exported here for convenience, because the hybrid estimators build on them. #### `BaseQuantumClassifier` Bases: `ClassifierMixin`, `BaseQuantumEstimator` Base class for quantum classifiers. Its `score()` returns the accuracy. ##### `predict_proba(X)` Predict the class probabilities. Subclasses should return the probabilities in the order of `classes_`, with shape `(n_samples, n_classes)`. The default implementation raises a `NotImplementedError`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | Returns: | Type | Description | | --------- | ----------------------------------------------------------------------------------------------- | | `ndarray` | The probabilities, of shape (n_samples, n_classes), with the columns in the order of classes\_. | #### `BaseQuantumEstimator` Bases: `BaseEstimator`, `ABC` Base class, compatible with scikit-learn, for all quantum algorithms. Parameters: | Name | Type | Description | Default | | -------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `provider` | `ProviderSpec` | The backend. It can be a registered name ("pennylane", "qiskit", "braket", or any name passed to register()), a configuration instance (for example PennyLaneConfig(device_name=...)), or a backend instance. | `'numpy'` | | `random_state` | \`int | None\` | The seed for reproducibility, applied inside fit() and load(). What it seeds depends on the estimator. The PyTorch estimators isolate the random number generator of PyTorch, so seeding one of them does not reseed the random stream of the caller. The kernel estimators are deterministic, and they take it only so that all the estimators have the same interface. | ##### `fit(X, y)` Fit the estimator on `X` and `y`. Parameters: | Name | Type | Description | Default | | ---- | --------- | -------------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | | `y` | `ndarray` | One target for each sample, of shape (n_samples,). | *required* | Returns: | Type | Description | | ------------------------ | --------------------------------------------- | | `'BaseQuantumEstimator'` | This estimator, so that calls can be chained. | ##### `load(path, *, provider=None)` Load a fitted model from a `.eigenkit` bundle. Load only bundles that you trust. A bundle can contain joblib pickles, and unpickling runs code, as with `torch.load` or `joblib.load`. Parameters: | Name | Type | Description | Default | | ---------- | -------------- | ----------- | --------------------------------------- | | `path` | \`str | Path\` | The path of the bundle. | | `provider` | \`ProviderSpec | None\` | A provider that replaces the saved one. | Returns: | Type | Description | | ------------------------ | --------------------- | | `'BaseQuantumEstimator'` | The loaded estimator. | Raises: | Type | Description | | ----------- | ---------------------------------------------------------------------------------------------------------------------------- | | `TypeError` | If the method is called on a subclass and the bundle holds a different class. BaseQuantumEstimator.load() accepts any class. | ##### `predict(X)` Predict a value for every sample in `X`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | Returns: | Type | Description | | --------- | ------------------------------- | | `ndarray` | One prediction for each sample. | ##### `save(path)` Save a fitted model to a `.eigenkit` bundle. The bundles of models that hold scikit-learn objects (`QSVC`, `QSVR`, the reservoir estimators and the time-series estimators) contain joblib pickles, so loading one can run code. Share them only with people who trust you. Parameters: | Name | Type | Description | Default | | ------ | ----- | ----------- | ------------------------------------------------------------------------- | | `path` | \`str | Path\` | The path of the bundle. The suffix .eigenkit is added when it is missing. | #### `BaseQuantumRegressor` Bases: `RegressorMixin`, `BaseQuantumEstimator` Base class for quantum regressors. Its `score()` returns `R²`. #### `CustomQuantumClassifier` Bases: `ModelAccessMixin`, `BaseQuantumClassifier` Classifier compatible with scikit-learn, built from your own list of blocks. Give any sequence of blocks (`Encoding`, `Variational`, `StrongVariational`, `Reuploading`, or a custom block from `CircuitBuilder.to_block()`) and an `observables` specification, as for `Circuit`. The estimator connects them into a quantum layer followed by a classical `nn.Linear` head, and trains the whole model with the cross-entropy loss. When the number of features of the data differs from the number of inputs that the circuit expects, an `nn.Linear` adapter is added in front automatically. Parameters: | Name | Type | Description | Default | | ------------------ | ---------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | | `blocks` | `list[Block]` | The list of blocks, in order. | *required* | | `observables` | `ObservableLike` | The observables. It takes the same options as Circuit(observables=...). Default: None (Z on every qubit). | `None` | | `provider` | `ProviderSpec` | The backend. Default: "numpy". | `'numpy'` | | `in_features` | \`int | None\` | The number of features of the data. If None, it is inferred from the first Encoding or Reuploading block. | | `n_classes` | \`int | None\` | The number of output classes. If None, it is inferred from the training labels. | | `lr` | `float` | The learning rate of the optimiser. | `0.01` | | `optimizer` | `str` | The name of the optimiser. The supported names are "adam", "adamw", "rmsprop" and "sgd". | `'adam'` | | `optimizer_kwargs` | \`dict | None\` | Extra keyword arguments passed to the optimiser. | | `epochs` | `int` | The number of training epochs. | `50` | | `batch_size` | `int` | The size of each mini-batch. | `32` | | `show_progress` | `bool` | If True, a tqdm progress bar is shown. | `True` | | `random_state` | \`int | None\` | The seed. | Attributes: | Name | Type | Description | | -------------------- | ---- | -------------------------------------------------------------------------------------------- | | `train_losses_` | | The cross-entropy loss of each epoch, set by fit(). | | `validation_losses_` | | The validation cross-entropy loss of each epoch, set by fit() when validation data is given. | Example ```python import numpy as np from eigenkit import ( CustomQuantumClassifier, Encoding, Variational, all_z, ) rng = np.random.default_rng(0) X_train, X_test = rng.uniform(0, np.pi, (20, 4)), rng.uniform(0, np.pi, (5, 4)) y_train = np.array([0, 1] * 10) blocks = [ Encoding(in_features=4, rotation="RY"), Variational(depth=2, rotation="RY"), ] model = CustomQuantumClassifier( blocks=blocks, observables=all_z(4), epochs=10, ) model.fit(X_train, y_train) predictions = model.predict(X_test) ``` ##### `fit(X, y, X_val=None, y_val=None)` Fit the classifier on training data. Parameters: | Name | Type | Description | Default | | ------- | --------- | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | | `X` | `ndarray` | The samples, of shape (n_samples, in_features). | *required* | | `y` | `ndarray` | The class labels, of shape (n_samples,). They can be of any type that scikit-learn's LabelEncoder accepts. | *required* | | `X_val` | \`ndarray | None\` | Optional validation samples, used to compute the validation loss of each epoch. | | `y_val` | \`ndarray | None\` | Optional validation labels, used to compute the validation loss of each epoch. | Returns: | Type | Description | | --------------------------- | --------------------------------------------- | | `'CustomQuantumClassifier'` | This estimator, so that calls can be chained. | Raises: | Type | Description | | ------------ | -------------------------------------------------------------- | | `ValueError` | If in_features is set and X does not have in_features columns. | ##### `predict(X)` Predict the class labels of `X`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | Returns: | Type | Description | | --------- | ------------------------------- | | `ndarray` | One prediction for each sample. | ##### `predict_proba(X)` Predict the probability of each class, with a softmax over the logits. It returns a float32 array of shape `(n_samples, n_classes)`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | Returns: | Type | Description | | --------- | ----------------------------------------------------------------------------------------------- | | `ndarray` | The probabilities, of shape (n_samples, n_classes), with the columns in the order of classes\_. | #### `CustomQuantumRegressor` Bases: `ModelAccessMixin`, `BaseQuantumRegressor` Regressor compatible with scikit-learn, built from your own list of blocks. It is the regression counterpart of CustomQuantumClassifier. It wraps your quantum `Circuit` with an `nn.Linear` head of one neuron, trained with the chosen regression loss. An `nn.Linear` input adapter is added when the number of features of the data differs from the number of inputs of the circuit. Parameters: | Name | Type | Description | Default | | ------------------ | ---------------- | ----------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | | `blocks` | `list[Block]` | The list of blocks, in order. | *required* | | `observables` | `ObservableLike` | The observables. It takes the same options as Circuit(observables=...). | `None` | | `provider` | `ProviderSpec` | The backend. Default: "numpy". | `'numpy'` | | `in_features` | \`int | None\` | The number of features of the data. If None, it is inferred from the first Encoding or Reuploading block. | | `lr` | `float` | The learning rate of the optimiser. | `0.01` | | `optimizer` | `str` | The name of the optimiser: "adam", "adamw", "rmsprop" or "sgd". | `'adam'` | | `optimizer_kwargs` | \`dict | None\` | Extra keyword arguments passed to the optimiser. | | `epochs` | `int` | The number of training epochs. | `50` | | `batch_size` | `int` | The size of each mini-batch. | `32` | | `loss_func` | `str` | The regression loss: "mse", "mae" or "huber". | `'mse'` | | `huber_delta` | `float` | The threshold of the Huber loss. | `1.0` | | `huber_reduction` | `str` | The reduction of the Huber loss: "mean" or "sum". | `'mean'` | | `show_progress` | `bool` | If True, a tqdm progress bar is shown. | `True` | | `random_state` | \`int | None\` | The seed. | Attributes: | Name | Type | Description | | -------------------- | ---- | ------------------------------------------------------------------------------ | | `train_losses_` | | The training loss of each epoch, set by fit(). | | `validation_losses_` | | The validation loss of each epoch, set by fit() when validation data is given. | ##### `fit(X, y, X_val=None, y_val=None)` Fit the regressor on training data. Parameters: | Name | Type | Description | Default | | ------- | --------- | ----------------------------------------------- | ------------------------------------------------------------------------------- | | `X` | `ndarray` | The samples, of shape (n_samples, in_features). | *required* | | `y` | `ndarray` | The real-valued targets, of shape (n_samples,). | *required* | | `X_val` | \`ndarray | None\` | Optional validation samples, used to compute the validation loss of each epoch. | | `y_val` | \`ndarray | None\` | Optional validation targets, used to compute the validation loss of each epoch. | Returns: | Type | Description | | -------------------------- | --------------------------------------------- | | `'CustomQuantumRegressor'` | This estimator, so that calls can be chained. | ##### `predict(X)` Predict the continuous targets of `X`. It returns a float32 array. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | Returns: | Type | Description | | --------- | ------------------------------- | | `ndarray` | One prediction for each sample. | #### `GenericClassifier` Bases: `ModelAccessMixin`, `BaseQuantumClassifier` Classifier compatible with scikit-learn that wraps any `torch.nn.Module`. You supply a complete model whose `forward(X)` accepts a float32 tensor of shape `(batch, in_features)` and returns logits of shape `(batch, n_classes)`. For sequence models (for example a long short-term memory network), set `sequence_input=True`. The wrapper then passes a 3-D tensor of shape `(batch, seq_len, in_features)` as it is, instead of flattening it. The wrapper trains with `CrossEntropyLoss` and encodes the labels automatically. Parameters: | Name | Type | Description | Default | | ------------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `model` | `Module` | Any nn.Module. It must include its own classification head, which gives n_classes logits. | *required* | | `in_features` | \`int | None\` | The expected number of features at each time step (the last dimension of the input). When it is given, fit raises an error if X.shape[-1] does not match. Pass None to skip the check. | | `sequence_input` | `bool` | If True, the estimator tells QuantumTimeSeriesClassifier that it expects a 3-D tensor of shape (batch, seq_len, in_features), so the windowed data must not be flattened. Default: False. | `False` | | `n_classes` | \`int | None\` | The number of output classes. If None, it is inferred from the training labels. If it is given, it must equal the number of distinct labels in y. | | `lr` | `float` | The learning rate of the optimiser. | `0.01` | | `optimizer` | `str` | The name of the optimiser: "adam", "adamw", "rmsprop" or "sgd". | `'adam'` | | `optimizer_kwargs` | \`dict | None\` | Extra keyword arguments passed to the constructor of the optimiser. | | `epochs` | `int` | The number of full passes over the training data. | `50` | | `batch_size` | `int` | The size of each mini-batch. | `32` | | `show_progress` | `bool` | If True, a tqdm progress bar is shown during training. | `True` | | `random_state` | \`int | None\` | The seed, applied before the first training step. | Attributes: | Name | Type | Description | | -------------------- | ---- | -------------------------------------------------------------------------------------------- | | `train_losses_` | | The average cross-entropy loss of each epoch, set by fit(). | | `validation_losses_` | | The validation cross-entropy loss of each epoch, set by fit() when validation data is given. | Example ```python import numpy as np from torch import nn from eigenkit.torch.estimators import GenericClassifier from eigenkit.torch.layers import Basic rng = np.random.default_rng(0) X_train, X_test = rng.normal(size=(30, 4)), rng.normal(size=(5, 4)) y_train = np.arange(30) % 3 # A plain PyTorch network, with a head for 3 classes. net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 3)) clf = GenericClassifier(model=net, in_features=4, epochs=20) clf.fit(X_train, y_train) labels = clf.predict(X_test) probs = clf.predict_proba(X_test) # A quantum layer with a classification head. q = Basic(n_qubits=4, depth=2) hybrid = nn.Sequential(q, nn.Linear(4, 3)) clf2 = GenericClassifier(model=hybrid, in_features=4) ``` ##### `accepts_sequence_input` Whether the model expects a 3-D input `(batch, seq_len, features)`. ##### `fit(X, y, X_val=None, y_val=None)` Fit the model on training data. Parameters: | Name | Type | Description | Default | | ------- | --------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------- | | `X` | `ndarray` | The samples, of shape (n_samples, in_features) for flat models, or (n_samples, seq_len, in_features) when sequence_input=True. | *required* | | `y` | `ndarray` | The class labels, of shape (n_samples,). | *required* | | `X_val` | \`ndarray | None\` | Optional validation samples, used to compute the validation loss of each epoch. | | `y_val` | \`ndarray | None\` | Optional validation labels, used to compute the validation loss of each epoch. | Returns: | Type | Description | | --------------------- | --------------------------------------------- | | `'GenericClassifier'` | This estimator, so that calls can be chained. | Raises: | Type | Description | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ValueError` | If X.shape[-1] or X_val.shape[-1] differs from in_features, if n_classes differs from the number of distinct labels, or if the model does not give one logit for each class. | ##### `load(path, *, model=None, provider=None)` Load a fitted `GenericClassifier` from a `.eigenkit` bundle. The architecture of the model cannot be rebuilt automatically, so you must give it. The saved weights and the label encoder are restored automatically. Parameters: | Name | Type | Description | Default | | ---------- | -------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `path` | \`str | Path\` | The path of the .eigenkit bundle. | | `model` | \`Module | None\` | An nn.Module with the same architecture as the one used in training. Its weights are not changed: the saved weights go into a deep copy, which is model\_ of the loaded estimator. | | `provider` | \`ProviderSpec | None\` | It is ignored. It is kept so that the signature matches the other estimators. | Returns: | Type | Description | | --------------------- | --------------------- | | `'GenericClassifier'` | The loaded estimator. | Raises: | Type | Description | | -------------------- | -------------------------------------- | | `SerializationError` | If model is not given. | | `TypeError` | If the bundle holds a different class. | Example ```python from torch import nn from eigenkit.torch.estimators import GenericClassifier net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 3)) clf = GenericClassifier.load("checkpoint.eigenkit", model=net) ``` ##### `predict(X)` Predict the class labels of `X`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | Returns: | Type | Description | | --------- | ------------------------------- | | `ndarray` | One prediction for each sample. | ##### `predict_proba(X)` Predict the probability of each class, with a softmax over the logits. It returns a float32 array of shape `(n_samples, n_classes)`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | Returns: | Type | Description | | --------- | ----------------------------------------------------------------------------------------------- | | `ndarray` | The probabilities, of shape (n_samples, n_classes), with the columns in the order of classes\_. | #### `GenericRegressor` Bases: `ModelAccessMixin`, `BaseQuantumRegressor` Regressor compatible with scikit-learn that wraps any `torch.nn.Module`. You supply a complete model whose `forward(X)` accepts a float32 tensor of shape `(batch, in_features)` and returns a tensor of shape `(batch, 1)` or `(batch,)`. For sequence models (for example a long short-term memory network), set `sequence_input=True`. The wrapper then passes a 3-D tensor of shape `(batch, seq_len, in_features)` as it is, instead of flattening it. The wrapper trains the model with the standard `train_loop()` of the library, and returns the predictions as a float32 NumPy array. Parameters: | Name | Type | Description | Default | | ------------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `model` | `Module` | Any nn.Module. It must include its own output head. | *required* | | `in_features` | \`int | None\` | The expected number of features at each time step (the last dimension of the input). When it is given, fit raises an error if X.shape[-1] does not match. Pass None to skip the check. | | `sequence_input` | `bool` | If True, the estimator tells QuantumTimeSeriesRegressor that it expects a 3-D tensor of shape (batch, seq_len, in_features), so the windowed data must not be flattened. Default: False. | `False` | | `loss_func` | \`str | Callable\` | The loss: "mse", "mae", "huber", or any callable (output, target) -> scalar_tensor. | | `huber_delta` | `float` | The threshold of the Huber loss. It is ignored for the other losses. | `1.0` | | `huber_reduction` | `str` | The reduction of the Huber loss: "mean" or "sum". | `'mean'` | | `lr` | `float` | The learning rate of the optimiser. | `0.01` | | `optimizer` | `str` | The name of the optimiser: "adam", "adamw", "rmsprop" or "sgd". | `'adam'` | | `optimizer_kwargs` | \`dict | None\` | Extra keyword arguments passed to the constructor of the optimiser. | | `epochs` | `int` | The number of full passes over the training data. | `50` | | `batch_size` | `int` | The size of each mini-batch. | `32` | | `show_progress` | `bool` | If True, a tqdm progress bar is shown during training. | `True` | | `random_state` | \`int | None\` | The seed, applied before the first training step. | Attributes: | Name | Type | Description | | -------------------- | ---- | ------------------------------------------------------------------------------------------ | | `train_losses_` | | The list of the average loss of each epoch, set by fit(). | | `validation_losses_` | | The list of the validation loss of each epoch, set by fit() when validation data is given. | Example ```python import numpy as np from torch import nn from eigenkit.torch.estimators import GenericRegressor from eigenkit.torch.layers import Basic rng = np.random.default_rng(0) X_train, X_test = rng.normal(size=(30, 8)), rng.normal(size=(5, 8)) y_train = X_train.sum(axis=1) # A plain PyTorch network. net = nn.Sequential(nn.Linear(8, 16), nn.ReLU(), nn.Linear(16, 1)) reg = GenericRegressor(model=net, in_features=8, epochs=30) reg.fit(X_train, y_train) preds = reg.predict(X_test) # A quantum layer with a classical head. q = Basic(n_qubits=4, depth=2) hybrid = nn.Sequential(q, nn.Linear(4, 1)) reg2 = GenericRegressor(model=hybrid, in_features=4) ``` ##### `accepts_sequence_input` Whether the model expects a 3-D input `(batch, seq_len, features)`. ##### `fit(X, y, X_val=None, y_val=None)` Fit the model on training data. Parameters: | Name | Type | Description | Default | | ------- | --------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------- | | `X` | `ndarray` | The samples, of shape (n_samples, in_features) for flat models, or (n_samples, seq_len, in_features) when sequence_input=True. | *required* | | `y` | `ndarray` | The real-valued targets, of shape (n_samples,). | *required* | | `X_val` | \`ndarray | None\` | Optional validation samples, used to compute the validation loss of each epoch. | | `y_val` | \`ndarray | None\` | Optional validation targets, used to compute the validation loss of each epoch. | Returns: | Type | Description | | -------------------- | --------------------------------------------- | | `'GenericRegressor'` | This estimator, so that calls can be chained. | Raises: | Type | Description | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------- | | `ValueError` | If X.shape[-1] or X_val.shape[-1] differs from in_features, or if the output of the model does not have shape (batch,) or (batch, 1). | ##### `load(path, *, model=None, provider=None)` Load a fitted `GenericRegressor` from a `.eigenkit` bundle. The architecture of the model cannot be rebuilt automatically, so you must give it. The saved weights are loaded into a copy of it. Parameters: | Name | Type | Description | Default | | ---------- | -------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `path` | \`str | Path\` | The path of the .eigenkit bundle. | | `model` | \`Module | None\` | An nn.Module with the same architecture as the one used in training. Its weights are not changed: the saved weights go into a deep copy, which is model\_ of the loaded estimator. | | `provider` | \`ProviderSpec | None\` | It is ignored. It is kept so that the signature matches the other estimators. | Returns: | Type | Description | | -------------------- | --------------------- | | `'GenericRegressor'` | The loaded estimator. | Raises: | Type | Description | | -------------------- | -------------------------------------- | | `SerializationError` | If model is not given. | | `TypeError` | If the bundle holds a different class. | Example ```python from torch import nn from eigenkit.torch.estimators import GenericRegressor net = nn.Sequential(nn.Linear(8, 16), nn.ReLU(), nn.Linear(16, 1)) reg = GenericRegressor.load("checkpoint.eigenkit", model=net) ``` ##### `predict(X)` Predict the continuous targets of `X`. It returns a float32 array. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | Returns: | Type | Description | | --------- | ------------------------------- | | `ndarray` | One prediction for each sample. | #### `HQNC` Bases: `ModelAccessMixin`, `BaseQuantumClassifier` Hybrid quantum neural network classifier (HQNC). The model is `MinMaxNorm -> quantum_layer -> nn.Linear(q_out, n_classes)`. If the number of features of the data differs from the resolved input size of the quantum layer (derived from `in_quantum`, `n_qubits` and `layer_type`), an `nn.Linear` adapter is added in front automatically. Parameters: | Name | Type | Description | Default | | ------------------ | ---------------------------- | ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `in_features` | `int` | The number of input features. | *required* | | `in_quantum` | \`int | None\` | The number of inputs that the quantum layer expects. It defaults to in_features. | | `n_qubits` | \`int | None\` | The number of qubits in each circuit. It defaults to in_quantum. | | `depth` | `int` | The number of repetitions of the variational layer. | `2` | | `n_classes` | \`int | None\` | The number of output classes. If None, it is inferred from the training labels. | | `layer_type` | `str` | The quantum layer. It is one of "Basic", "Reupload" and "Ensemble". | `'Basic'` | | `enc_rotation` | \`Literal['RX', 'RY', 'RZ'] | None\` | The encoding rotation: "RX", "RY", "RZ" or None. | | `var_rotation` | \`Literal['RX', 'RY', 'RZ'] | None\` | The variational rotation of the "basic" ansatz: "RX", "RY", "RZ" or None. | | `variational_type` | \`Literal['basic', 'strong'] | None\` | The type of variational block: "basic", "strong" or None. | | `basis` | \`Literal['X', 'Y', 'Z'] | None\` | The measurement basis: "X", "Y", "Z" or None. For these four arguments, None means "use the default of the layer named by layer_type", which is where each of these values is decided. | | `provider` | `ProviderSpec` | The backend. It takes the same options as Circuit(provider=...). | `'numpy'` | | `lr` | `float` | The learning rate of the optimiser. | `0.01` | | `optimizer` | `str` | The name of the optimiser. The supported names are "adam", "adamw", "rmsprop" and "sgd". | `'adam'` | | `optimizer_kwargs` | \`dict | None\` | Extra keyword arguments passed to the optimiser. | | `epochs` | `int` | The number of training epochs. | `50` | | `batch_size` | `int` | The size of each mini-batch. | `32` | | `show_progress` | `bool` | If True, a tqdm progress bar is shown. | `True` | | `random_state` | \`int | None\` | The seed for the initialisation of the weights. | Attributes: | Name | Type | Description | | -------------------- | ---- | -------------------------------------------------------------------------------------------- | | `train_losses_` | | The average cross-entropy loss of each epoch, set by fit(). | | `validation_losses_` | | The validation cross-entropy loss of each epoch, set by fit() when validation data is given. | Example ```python import numpy as np from eigenkit.torch.estimators import HQNC rng = np.random.default_rng(0) X_train, X_test = rng.normal(size=(30, 4)), rng.normal(size=(5, 4)) y_train = np.arange(30) % 3 model = HQNC(in_features=4, depth=2, n_classes=3) model.fit(X_train, y_train) predictions = model.predict(X_test) ``` ##### `__init__(in_features, in_quantum=None, n_qubits=None, depth=2, n_classes=None, layer_type='Basic', enc_rotation=None, var_rotation=None, variational_type=None, basis=None, provider='numpy', lr=0.01, optimizer='adam', optimizer_kwargs=None, epochs=50, batch_size=32, show_progress=True, random_state=42)` Initialise a hybrid quantum neural network classifier (HQNC). Parameters: | Name | Type | Description | Default | | ------------------ | ---------------------------- | ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `in_features` | `int` | The number of input features that fit() expects. | *required* | | `in_quantum` | \`int | None\` | The number of inputs that the quantum layer expects. It defaults to in_features. | | `n_qubits` | \`int | None\` | The number of qubits in each circuit. It defaults to in_quantum. | | `depth` | `int` | The number of repetitions of the variational layer. | `2` | | `n_classes` | \`int | None\` | The number of output classes. If None, it is inferred from the labels passed to fit(). | | `layer_type` | `str` | The family of the quantum layer. It is one of "Basic", "Reupload" and "Ensemble". | `'Basic'` | | `enc_rotation` | \`Literal['RX', 'RY', 'RZ'] | None\` | The encoding rotation: "RX", "RY", "RZ" or None. | | `var_rotation` | \`Literal['RX', 'RY', 'RZ'] | None\` | The variational rotation of the basic ansatz: "RX", "RY", "RZ" or None. | | `variational_type` | \`Literal['basic', 'strong'] | None\` | The type of variational block: "basic", "strong" or None. | | `basis` | \`Literal['X', 'Y', 'Z'] | None\` | The measurement basis: "X", "Y", "Z" or None. For these four arguments, None means "use the default of the layer named by layer_type", which is where each of these values is decided. | | `provider` | `ProviderSpec` | The backend. It takes the same options as Circuit(provider=...). | `'numpy'` | | `lr` | `float` | The learning rate of the optimiser. | `0.01` | | `optimizer` | `str` | The name of the optimiser. The supported names are "adam", "adamw", "rmsprop" and "sgd". | `'adam'` | | `optimizer_kwargs` | \`dict | None\` | Extra keyword arguments passed to the optimiser. | | `epochs` | `int` | The number of training epochs. | `50` | | `batch_size` | `int` | The size of each mini-batch. | `32` | | `show_progress` | `bool` | If True, a tqdm progress bar is shown during training. | `True` | | `random_state` | \`int | None\` | The seed for the initialisation of the weights. | ##### `fit(X, y, X_val=None, y_val=None)` Fit the classifier on training data. Parameters: | Name | Type | Description | Default | | ------- | --------- | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | | `X` | `ndarray` | The samples, of shape (n_samples, in_features). | *required* | | `y` | `ndarray` | The class labels, of shape (n_samples,). They can be of any type that scikit-learn's LabelEncoder accepts. | *required* | | `X_val` | \`ndarray | None\` | Optional validation samples, used to compute the validation loss of each epoch. | | `y_val` | \`ndarray | None\` | Optional validation labels, used to compute the validation loss of each epoch. | Returns: | Type | Description | | ------ | --------------------------------------------- | | `HQNC` | This estimator, so that calls can be chained. | Raises: | Type | Description | | ------------ | --------------------------------------- | | `ValueError` | If X does not have in_features columns. | ##### `predict(X)` Predict the class labels of `X`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | Returns: | Type | Description | | --------- | ------------------------------- | | `ndarray` | One prediction for each sample. | ##### `predict_proba(X)` Predict the probability of each class, with a softmax over the logits. It returns a float32 array of shape `(n_samples, n_classes)`. Each row sums to `1`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | Returns: | Type | Description | | --------- | ----------------------------------------------------------------------------------------------- | | `ndarray` | The probabilities, of shape (n_samples, n_classes), with the columns in the order of classes\_. | #### `HQNR` Bases: `ModelAccessMixin`, `BaseQuantumRegressor` Hybrid quantum neural network regressor (HQNR). It is the regression counterpart of `HQNC`. The model is `MinMaxNorm -> quantum_layer -> nn.Linear(q_out, 1)`. If the number of features of the data differs from the resolved input size of the quantum layer, an `nn.Linear` adapter is added in front automatically. Parameters: | Name | Type | Description | Default | | ------------------ | ---------------------------- | ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `in_features` | `int` | The number of input features. | *required* | | `in_quantum` | \`int | None\` | The number of inputs that the quantum layer expects. It defaults to in_features. | | `n_qubits` | \`int | None\` | The number of qubits in each circuit. It defaults to in_quantum. | | `depth` | `int` | The number of repetitions of the variational layer. | `2` | | `layer_type` | `str` | The quantum layer. It is one of "Basic", "Reupload" and "Ensemble". | `'Basic'` | | `enc_rotation` | \`Literal['RX', 'RY', 'RZ'] | None\` | The encoding rotation: "RX", "RY", "RZ" or None. | | `var_rotation` | \`Literal['RX', 'RY', 'RZ'] | None\` | The variational rotation of the "basic" ansatz: "RX", "RY", "RZ" or None. | | `variational_type` | \`Literal['basic', 'strong'] | None\` | The type of variational block: "basic", "strong" or None. | | `basis` | \`Literal['X', 'Y', 'Z'] | None\` | The measurement basis: "X", "Y", "Z" or None. For these four arguments, None means "use the default of the layer named by layer_type", which is where each of these values is decided. | | `provider` | `ProviderSpec` | The backend. It takes the same options as Circuit(provider=...). | `'numpy'` | | `lr` | `float` | The learning rate of the optimiser. | `0.01` | | `optimizer` | `str` | The name of the optimiser. The supported names are "adam", "adamw", "rmsprop" and "sgd". | `'adam'` | | `optimizer_kwargs` | \`dict | None\` | Extra keyword arguments passed to the optimiser. | | `epochs` | `int` | The number of training epochs. | `50` | | `batch_size` | `int` | The size of each mini-batch. | `32` | | `show_progress` | `bool` | If True, a tqdm progress bar is shown. | `True` | | `loss_func` | `str` | The regression loss: "mse", "mae" or "huber". | `'mse'` | | `huber_delta` | `float` | The threshold of the Huber loss. | `1.0` | | `huber_reduction` | `str` | The reduction of the Huber loss: "mean" or "sum". | `'mean'` | | `random_state` | \`int | None\` | The seed. | Attributes: | Name | Type | Description | | -------------------- | ---- | ------------------------------------------------------------------------------ | | `train_losses_` | | The training loss of each epoch, set by fit(). | | `validation_losses_` | | The validation loss of each epoch, set by fit() when validation data is given. | Example ```python import numpy as np from eigenkit.torch.estimators import HQNR rng = np.random.default_rng(0) X_train, X_test = rng.normal(size=(30, 8)), rng.normal(size=(5, 8)) y_train = X_train.sum(axis=1) model = HQNR(in_features=8, depth=2) model.fit(X_train, y_train) predictions = model.predict(X_test) ``` ##### `fit(X, y, X_val=None, y_val=None)` Fit the regressor on training data. Parameters: | Name | Type | Description | Default | | ------- | --------- | ----------------------------------------------- | ------------------------------------------------------------------------------- | | `X` | `ndarray` | The samples, of shape (n_samples, in_features). | *required* | | `y` | `ndarray` | The real-valued targets, of shape (n_samples,). | *required* | | `X_val` | \`ndarray | None\` | Optional validation samples, used to compute the validation loss of each epoch. | | `y_val` | \`ndarray | None\` | Optional validation targets, used to compute the validation loss of each epoch. | Returns: | Type | Description | | ------ | --------------------------------------------- | | `HQNR` | This estimator, so that calls can be chained. | Raises: | Type | Description | | ------------ | --------------------------------------- | | `ValueError` | If X does not have in_features columns. | ##### `predict(X)` Predict the continuous targets of `X`. It returns a float32 array. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | Returns: | Type | Description | | --------- | ------------------------------- | | `ndarray` | One prediction for each sample. | #### `QuantumTimeSeriesClassifier` Bases: `BaseQuantumClassifier` Supervised wrapper for time-series classification. It builds sliding windows over the rows of `X` (one row for each time step). Each window has shape `(in_size, n_features)`. Stacked along the batch axis, they form an array of shape `(n_windows, in_size, n_features)`, ready for sequence models such as `RecurrentHQNC`. When `autoregressive=True`, an extra last channel holds the target value of each timestep, so each step has `n_features + 1` values. For a window that starts at row `i`, the classification target is `y[i + in_size - 1 + shift]`. Parameters: | Name | Type | Description | Default | | ---------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | | `estimator` | `Any` | The inner estimator. It must have fit(X, y) and predict(X). | *required* | | `in_size` | \`int | None\` | The length of the window, in time steps. If None, it is treated as 1. | | `shift` | `int` | How many steps after the last timestep of the window the target is. It must be zero or more. shift=0 predicts the target at the last timestep. | `1` | | `autoregressive` | `bool` | If True, a channel with y[i + k] for k = 0, ..., in_size - 1 is added to each window. With shift=0, this leaks the target into the inputs, and a UserWarning is raised. When autoregressive=True, predict() and predict_proba() require a y_history argument. | `False` | Attributes: | Name | Type | Description | | --------------- | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `classes_` | | The distinct class labels seen by fit(). | | `train_losses_` | | The training loss of each epoch, copied from the inner estimator if it has train_losses\_ (HQNC, RecurrentHQNC), and an empty list otherwise. It is set by fit(). | Example ```python import numpy as np from eigenkit.torch.estimators import ( QuantumTimeSeriesClassifier, RecurrentHQNC, ) rng = np.random.default_rng(0) X_train, X_test = rng.normal(size=(40, 4)), rng.normal(size=(15, 4)) y_train = np.arange(40) % 2 inner = RecurrentHQNC(seq_feature_dim=4, rnn_hidden_dim=4, depth=2) model = QuantumTimeSeriesClassifier(estimator=inner, in_size=10, shift=1) model.fit(X_train, y_train) predictions = model.predict(X_test) ``` ##### `output` The number of classes. After `fit()`, it is the number of classes seen. Before, it is the `n_classes` of the inner estimator, or None if it has none. ##### `fit(X, y)` Fit the inner classifier on the windows and their labels. Parameters: | Name | Type | Description | Default | | ---- | --------- | --------------------------------------------------------------- | ---------- | | `X` | `ndarray` | The features in time order, of shape (n_timesteps, n_features). | *required* | | `y` | `ndarray` | The class labels, of shape (n_timesteps,) or (n_timesteps, 1). | *required* | Returns: | Type | Description | | ------------------------------- | --------------------------------------------- | | `'QuantumTimeSeriesClassifier'` | This estimator, so that calls can be chained. | ##### `load(path, *, provider=None, model=None)` Load a fitted time-series classifier wrapper from a `.eigenkit` bundle. Parameters: | Name | Type | Description | Default | | ---------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | | `path` | \`str | Path\` | The path of the .eigenkit bundle. | | `provider` | \`ProviderSpec | None\` | An optional provider that replaces the saved one. It is passed to the inner estimator. | | `model` | `Any` | The nn.Module architecture, required when the inner estimator is GenericClassifier. Pass the same architecture as the one used in training. | `None` | Returns: | Type | Description | | ------------------------------- | ------------------- | | `'QuantumTimeSeriesClassifier'` | The loaded wrapper. | Raises: | Type | Description | | -------------------- | ------------------------------------------------------- | | `TypeError` | If the bundle holds a different class. | | `SerializationError` | If the inner estimator needs model and it is not given. | ##### `predict(X, y_history=None)` Predict the class labels of the windows of `X`. Parameters: | Name | Type | Description | Default | | ----------- | --------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `X` | `ndarray` | The features in time order, of shape (n_timesteps, n_features). | *required* | | `y_history` | \`ndarray | None\` | The past class labels, aligned with the rows of X. They fill the autoregressive channel of each window. It is required when autoregressive=True, and it must have at least n_timesteps - shift elements (that is, n_timesteps - in_size - shift + 1 + in_size - 1). | Returns: | Type | Description | | --------- | -------------------------------------------------------------------- | | `ndarray` | The predicted labels, of shape (n_timesteps - in_size - shift + 1,). | ##### `predict_proba(X, y_history=None)` Return the estimated class probabilities of the windows of `X`. Parameters: | Name | Type | Description | Default | | ----------- | --------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------ | | `X` | `ndarray` | The features in time order, of shape (n_timesteps, n_features). | *required* | | `y_history` | \`ndarray | None\` | The past class labels. It is required when autoregressive=True. See predict(). | Returns: | Type | Description | | --------- | ------------------------------------------------ | | `ndarray` | A float32 array of shape (n_windows, n_classes). | Raises: | Type | Description | | --------------------- | -------------------------------------------- | | `NotImplementedError` | If the inner estimator has no predict_proba. | ##### `score(X, y, *, return_dict=False)` ```python score( X: np.ndarray, y: np.ndarray, *, return_dict: Literal[False] = False, ) -> float ``` ```python score( X: np.ndarray, y: np.ndarray, *, return_dict: Literal[True], ) -> dict[str, float] ``` Score the estimator on the window targets, aligned as in `predict`. By default, it returns the accuracy, for compatibility with scikit-learn (`cross_val_score`, `GridSearchCV`). Pass `return_dict=True` to get a dict with `accuracy`, `f1` (weighted), `precision` (weighted) and `recall` (weighted). Note When `autoregressive=True`, the true labels `y` are used as the autoregressive history channel (teacher forcing). This is the correct behaviour for held-out evaluation, but it means that `score` cannot be called without the true labels of all the timesteps in `X`. Parameters: | Name | Type | Description | Default | | ------------- | --------- | ---------------------------------------------------------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). The windows are built inside. | *required* | | `y` | `ndarray` | One target for each sample, of shape (n_samples,). | *required* | | `return_dict` | `bool` | If True, every metric is returned in a dict, instead of the single main score. Default: False. | `False` | Returns: | Type | Description | | ------- | ------------------ | | \`float | dict[str, float]\` | #### `QuantumTimeSeriesRegressor` Bases: `BaseQuantumRegressor` Supervised wrapper with a single output, for time-series regression. It builds sliding windows over the rows of `X` (one row for each time step). Each window has shape `(in_size, n_features)`. Stacked along the batch axis, they form an array of shape `(n_windows, in_size, n_features)`, ready for sequence models such as `torch.nn.LSTM`. When `autoregressive=True`, an extra last channel holds the target value of each timestep, so each step has `n_features + 1` values. For a window that starts at row `i`, the regression target is `y[i + in_size - 1 + shift]`. Parameters: | Name | Type | Description | Default | | ---------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | | `estimator` | `Any` | The inner estimator. It must have fit(X, y) and predict(X). | *required* | | `in_size` | \`int | None\` | The length of the window, in time steps. If None, it is treated as 1. | | `shift` | `int` | How many steps after the last timestep of the window the target is. It must be zero or more. shift=0 predicts the target at the last timestep. | `1` | | `autoregressive` | `bool` | If True, a channel with y[i + k] for k = 0, ..., in_size - 1 is added to each window. With shift=0, this leaks the target into the inputs, and a UserWarning is raised. | `False` | Attributes: | Name | Type | Description | | --------------- | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `train_losses_` | | The training loss of each epoch, copied from the inner estimator if it has train_losses\_ (HQNR, RecurrentHQNR), and an empty list otherwise. It is set by fit(). | Example ```python import numpy as np from eigenkit.torch.estimators import QuantumTimeSeriesRegressor, RecurrentHQNR rng = np.random.default_rng(0) X_train, X_test = rng.normal(size=(40, 3)), rng.normal(size=(15, 3)) y_train = np.sin(np.arange(40) / 4) # The inner estimator should accept X of shape (batch, in_size, n_features). inner = RecurrentHQNR(seq_feature_dim=3, rnn_hidden_dim=4, epochs=5) model = QuantumTimeSeriesRegressor(estimator=inner, in_size=10, shift=1) model.fit(X_train, y_train) predictions = model.predict(X_test) ``` ##### `output` The number of outputs, which is always `1`. ##### `fit(X, y)` Fit the inner estimator on the windows and their targets. Parameters: | Name | Type | Description | Default | | ---- | --------- | --------------------------------------------------------------- | ---------- | | `X` | `ndarray` | The features in time order, of shape (n_timesteps, n_features). | *required* | | `y` | `ndarray` | The targets, of shape (n_timesteps,) or (n_timesteps, 1). | *required* | Returns: | Type | Description | | ------------------------------ | --------------------------------------------- | | `'QuantumTimeSeriesRegressor'` | This estimator, so that calls can be chained. | ##### `load(path, *, provider=None, model=None)` Load a fitted time-series wrapper from a `.eigenkit` bundle. Parameters: | Name | Type | Description | Default | | ---------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------- | | `path` | \`str | Path\` | The path of the .eigenkit bundle. | | `provider` | \`ProviderSpec | None\` | An optional provider that replaces the saved one. It is passed to the inner estimator. | | `model` | `Any` | The nn.Module architecture, required when the inner estimator is GenericRegressor. Pass the same architecture as the one used in training. | `None` | Returns: | Type | Description | | ------------------------------ | ------------------- | | `'QuantumTimeSeriesRegressor'` | The loaded wrapper. | Raises: | Type | Description | | -------------------- | ------------------------------------------------------- | | `TypeError` | If the bundle holds a different class. | | `SerializationError` | If the inner estimator needs model and it is not given. | ##### `predict(X, y_history=None)` Predict the regression targets of the windows of `X`. Parameters: | Name | Type | Description | Default | | ----------- | --------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | `X` | `ndarray` | The features in time order, of shape (n_timesteps, n_features). | *required* | | `y_history` | \`ndarray | None\` | The past targets, aligned with the rows of X. They fill the autoregressive channel of each window. It is required when autoregressive=True. | Returns: | Type | Description | | --------- | --------------------------------------------------------------- | | `ndarray` | The predictions, of shape (n_timesteps - in_size - shift + 1,). | ##### `score(X, y, *, return_dict=False)` ```python score( X: np.ndarray, y: np.ndarray, *, return_dict: Literal[False] = False, ) -> float ``` ```python score( X: np.ndarray, y: np.ndarray, *, return_dict: Literal[True], ) -> dict[str, float] ``` Score the estimator on the window targets, aligned as in `predict`. By default, it returns `R²`, for compatibility with scikit-learn (`cross_val_score`, `GridSearchCV`). Pass `return_dict=True` to get a dict with `r2`, `mse`, `mae`, `rmse` and `mape`. Parameters: | Name | Type | Description | Default | | ------------- | --------- | ---------------------------------------------------------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). The windows are built inside. | *required* | | `y` | `ndarray` | One target for each sample, of shape (n_samples,). | *required* | | `return_dict` | `bool` | If True, every metric is returned in a dict, instead of the single main score. Default: False. | `False` | Returns: | Type | Description | | ------- | ------------------ | | \`float | dict[str, float]\` | #### `RecurrentHQNC` Bases: `ModelAccessMixin`, `BaseQuantumClassifier` Hybrid classifier: a recurrent encoder, a quantum layer and a linear class head. The recurrent encoder is a recurrent neural network (RNN). The estimator expects 3-D inputs of shape `(n_samples, seq_len, seq_feature_dim)`. By default, only the last timestep of the RNN output goes to the quantum layer. Set `rnn_return_last_timestep=False`, together with `sequence_length`, to use the full RNN output (flattened) instead. The quantum part is the same as in HQNC: `MinMaxNorm`, the quantum layer and an `nn.Linear` head. Parameters: | Name | Type | Description | Default | | -------------------------- | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `seq_feature_dim` | `int` | The number of features at each time step (the last axis of X). | *required* | | `rnn_type` | `Literal['rnn', 'gru', 'lstm']` | The type of recurrent layer: "rnn", "gru" (gated recurrent unit) or "lstm" (long short-term memory). | `'gru'` | | `rnn_hidden_dim` | `int` | The hidden size in each direction. | `32` | | `rnn_num_layers` | `int` | The number of stacked RNN layers. | `1` | | `rnn_dropout` | `float` | The dropout between the RNN layers. It applies only if rnn_num_layers > 1. | `0.0` | | `rnn_bidirectional` | `bool` | If True, the RNN is bidirectional, and its output size doubles. | `False` | | `rnn_return_last_timestep` | `bool` | If True, only the last output step is kept. Otherwise, the full sequence is flattened, which requires sequence_length. | `True` | | `sequence_length` | \`int | None\` | The number of time steps in each sample. It is required when rnn_return_last_timestep=False, and it must equal X.shape[1] in fit. | | `in_quantum` | \`int | None\` | The number of inputs of the quantum layer. It defaults to the size of the RNN output that reaches the quantum layer: rnn_hidden_dim, doubled when the RNN is bidirectional, and multiplied by sequence_length when rnn_return_last_timestep=False. | | `n_qubits` | \`int | None\` | The number of qubits in each circuit. It defaults to in_quantum. | | `depth` | `int` | The number of repetitions of the variational layer. | `2` | | `n_classes` | \`int | None\` | The number of output classes. If None, it is inferred from the labels passed to fit(). If it is given, it must equal the number of distinct labels in y. | | `layer_type` | `str` | The quantum layer. It is one of "Basic", "Reupload" and "Ensemble". | `'Basic'` | | `enc_rotation` | \`Literal['RX', 'RY', 'RZ'] | None\` | The encoding rotation: "RX", "RY", "RZ" or None. | | `var_rotation` | \`Literal['RX', 'RY', 'RZ'] | None\` | The variational rotation of the basic ansatz: "RX", "RY", "RZ" or None. | | `variational_type` | \`Literal['basic', 'strong'] | None\` | The type of variational block: "basic", "strong" or None. | | `basis` | \`Literal['X', 'Y', 'Z'] | None\` | The measurement basis: "X", "Y", "Z" or None. For these four arguments, None means "use the default of the layer named by layer_type", which is where each of these values is decided. | | `provider` | `ProviderSpec` | The backend. It takes the same options as Circuit(provider=...). | `'numpy'` | | `lr` | `float` | The learning rate of the optimiser. | `0.01` | | `optimizer` | `str` | The name of the optimiser. The supported names are "adam", "adamw", "rmsprop" and "sgd". | `'adam'` | | `optimizer_kwargs` | \`dict | None\` | Extra keyword arguments passed to the optimiser. | | `epochs` | `int` | The number of training epochs. | `50` | | `batch_size` | `int` | The size of each mini-batch. | `32` | | `show_progress` | `bool` | If True, a tqdm progress bar is shown during training. | `True` | | `random_state` | \`int | None\` | The seed for the initialisation of the weights. | Attributes: | Name | Type | Description | | -------------------- | ---- | -------------------------------------------------------------------------------------------- | | `train_losses_` | | The cross-entropy loss of each epoch, set by fit(). | | `validation_losses_` | | The validation cross-entropy loss of each epoch, set by fit() when validation data is given. | ##### `fit(X, y, X_val=None, y_val=None)` Fit the estimator on `X` and `y`. Parameters: | Name | Type | Description | Default | | ------- | --------- | -------------------------------------------------------------- | --------------------------------------------------------------------- | | `X` | `ndarray` | The sequences, of shape (n_samples, seq_len, seq_feature_dim). | *required* | | `y` | `ndarray` | One label for each sequence, of shape (n_samples,). | *required* | | `X_val` | \`ndarray | None\` | Optional validation sequences, of the same shape as X. Default: None. | | `y_val` | \`ndarray | None\` | The labels of X_val. Default: None. | Returns: | Type | Description | | --------------- | --------------------------------------------- | | `RecurrentHQNC` | This estimator, so that calls can be chained. | ##### `predict(X)` Predict a label for every sample in `X`. Parameters: | Name | Type | Description | Default | | ---- | --------- | -------------------------------------------------------------- | ---------- | | `X` | `ndarray` | The sequences, of shape (n_samples, seq_len, seq_feature_dim). | *required* | Returns: | Type | Description | | --------- | -------------------------------------------------------- | | `ndarray` | One prediction for each sequence, of shape (n_samples,). | ##### `predict_proba(X)` Predict the class probabilities of every sample in `X`. Parameters: | Name | Type | Description | Default | | ---- | --------- | -------------------------------------------------------------- | ---------- | | `X` | `ndarray` | The sequences, of shape (n_samples, seq_len, seq_feature_dim). | *required* | Returns: | Type | Description | | --------- | ----------------------------------------------------------------------------------------------- | | `ndarray` | The probabilities, of shape (n_samples, n_classes), with the columns in the order of classes\_. | #### `RecurrentHQNR` Bases: `ModelAccessMixin`, `BaseQuantumRegressor` Hybrid regressor: a recurrent encoder, a quantum layer and a linear head. The recurrent encoder is a recurrent neural network (RNN). The estimator expects 3-D inputs of shape `(n_samples, seq_len, seq_feature_dim)`. By default, only the last timestep of the RNN output goes to the quantum layer. Set `rnn_return_last_timestep=False`, together with `sequence_length`, to use the full RNN output (flattened) instead. The quantum part is the same as in HQNR: `MinMaxNorm`, the quantum layer and an `nn.Linear` head. Parameters: | Name | Type | Description | Default | | -------------------------- | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `seq_feature_dim` | `int` | The number of features at each time step (the last axis of X). | *required* | | `rnn_type` | `Literal['rnn', 'gru', 'lstm']` | The type of recurrent layer: "rnn", "gru" (gated recurrent unit) or "lstm" (long short-term memory). | `'gru'` | | `rnn_hidden_dim` | `int` | The hidden size in each direction. | `32` | | `rnn_num_layers` | `int` | The number of stacked RNN layers. | `1` | | `rnn_dropout` | `float` | The dropout between the RNN layers. It applies only if rnn_num_layers > 1. | `0.0` | | `rnn_bidirectional` | `bool` | If True, the RNN is bidirectional, and its output size doubles. | `False` | | `rnn_return_last_timestep` | `bool` | If True, only the last output step is kept. Otherwise, the full sequence is flattened, which requires sequence_length. | `True` | | `sequence_length` | \`int | None\` | The number of time steps in each sample. It is required when rnn_return_last_timestep=False, and it must equal X.shape[1] in fit. | | `in_quantum` | \`int | None\` | The number of inputs of the quantum layer. It defaults to the size of the RNN output that reaches the quantum layer: rnn_hidden_dim, doubled when the RNN is bidirectional, and multiplied by sequence_length when rnn_return_last_timestep=False. | | `n_qubits` | \`int | None\` | The number of qubits in each circuit. It defaults to in_quantum. | | `depth` | `int` | The number of repetitions of the variational layer. | `2` | | `layer_type` | `str` | The quantum layer. It is one of "Basic", "Reupload" and "Ensemble". | `'Basic'` | | `enc_rotation` | \`Literal['RX', 'RY', 'RZ'] | None\` | The encoding rotation: "RX", "RY", "RZ" or None. | | `var_rotation` | \`Literal['RX', 'RY', 'RZ'] | None\` | The variational rotation of the "basic" ansatz: "RX", "RY", "RZ" or None. | | `variational_type` | \`Literal['basic', 'strong'] | None\` | The type of variational block: "basic", "strong" or None. | | `basis` | \`Literal['X', 'Y', 'Z'] | None\` | The measurement basis: "X", "Y", "Z" or None. For these four arguments, None means "use the default of the layer named by layer_type", which is where each of these values is decided. | | `provider` | `ProviderSpec` | The backend. It takes the same options as Circuit(provider=...). | `'numpy'` | | `lr` | `float` | The learning rate of the optimiser. | `0.01` | | `optimizer` | `str` | The name of the optimiser. | `'adam'` | | `optimizer_kwargs` | \`dict | None\` | Extra keyword arguments passed to the optimiser. | | `epochs` | `int` | The number of training epochs. | `50` | | `batch_size` | `int` | The size of each mini-batch. | `32` | | `show_progress` | `bool` | If True, a tqdm progress bar is shown. | `True` | | `loss_func` | `str` | The regression loss: "mse", "mae" or "huber". | `'mse'` | | `huber_delta` | `float` | The threshold of the Huber loss. | `1.0` | | `huber_reduction` | `str` | The reduction of the Huber loss: "mean" or "sum". | `'mean'` | | `random_state` | \`int | None\` | The seed. | Attributes: | Name | Type | Description | | -------------------- | ---- | ------------------------------------------------------------------------------ | | `train_losses_` | | The training loss of each epoch, set by fit(). | | `validation_losses_` | | The validation loss of each epoch, set by fit() when validation data is given. | ##### `fit(X, y, X_val=None, y_val=None)` Fit the estimator on `X` and `y`. Parameters: | Name | Type | Description | Default | | ------- | --------- | ---------------------------------------------------------------------- | --------------------------------------------------------------------- | | `X` | `ndarray` | The sequences, of shape (n_samples, seq_len, seq_feature_dim). | *required* | | `y` | `ndarray` | One target for each sequence, of shape (n_samples,) or (n_samples, 1). | *required* | | `X_val` | \`ndarray | None\` | Optional validation sequences, of the same shape as X. Default: None. | | `y_val` | \`ndarray | None\` | The targets of X_val. Default: None. | Returns: | Type | Description | | --------------- | --------------------------------------------- | | `RecurrentHQNR` | This estimator, so that calls can be chained. | ##### `predict(X)` Predict a value for every sample in `X`. Parameters: | Name | Type | Description | Default | | ---- | --------- | -------------------------------------------------------------- | ---------- | | `X` | `ndarray` | The sequences, of shape (n_samples, seq_len, seq_feature_dim). | *required* | Returns: | Type | Description | | --------- | --------------------------------------------------------------------------- | | `ndarray` | One prediction for each sequence, as a float32 array of shape (n_samples,). | #### `load_model(path, *, provider=None, model=None)` Load any saved model. The class of the model is read from `metadata.json`. Load only bundles that you trust. A bundle can contain joblib pickles, and unpickling runs code, as with `torch.load` or `joblib.load`. Parameters: | Name | Type | Description | Default | | ---------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------- | | `path` | \`str | Path\` | The path to the .eigenkit bundle. | | `provider` | \`ProviderSpec | None\` | A provider that replaces the saved one. It is passed to the load() method of the estimator. | | `model` | `Any` | The nn.Module architecture, for checkpoints of GenericRegressor and GenericClassifier. It is ignored for the other estimators. | `None` | Returns: | Type | Description | | ---------------------- | ------------------------------------------------------------------- | | `BaseQuantumEstimator` | The loaded estimator, an instance of the class named in the bundle. | # Interfaces `Circuit(...)` returns a different type of circuit depending on its `interface=` argument, which you set explicitly, as in PennyLane. Both interfaces run the **same** `CircuitSpec` on the **same** providers. They differ only in *how you pass the weights* and *how you get gradients*: - **`"numpy"`**, the default, is the core circuit, which does not depend on any framework. You pass the `weights` at every call, and the gradients come from `circuit.jacobian(...)`. It does not use PyTorch, and `pip install eigenkit` with no extra is enough. - **`"torch"`** is a `torch.nn.Module` that stores its weights as an `nn.Parameter` and computes gradients through `torch.autograd`. It is part of the optional PyTorch package (the `torch` extra). | | `numpy` (default) | `torch` | | ---------- | --------------------------------------------------------------------------------------- | -------------------------------------------------------------- | | Returns | A `core.circuit.Circuit`. | A `TorchCircuit`, which is an `nn.Module`. | | Weights | They are passed at each call: `circuit(weights, x)`. | The circuit stores them as an `nn.Parameter`: `circuit(x)`. | | Gradients | `circuit.jacobian(weights, x)`. | `loss.backward()`, through autograd. | | Dependency | The core (NumPy and SciPy). | The `torch` extra. | | Best for | Chemistry and optimisation, variational loops, and custom training without a framework. | Hybrid quantum-classical models trained end to end in PyTorch. | Both accept the same `provider=` argument, in any of its forms. See [Providers](https://docs.kit.eigenorbit.com/user-guide/providers/index.md). `Circuit` is a function, not a class The top-level `Circuit(...)` is a **factory function**. It returns one of two concrete types, depending on `interface=`: the NumPy circuit `eigenkit.core.circuit.Circuit`, or the PyTorch circuit `TorchCircuit`, which is an `nn.Module`. So it is not a class: `isinstance(obj, Circuit)`, `issubclass(...)`, subclassing `Circuit`, and using `Circuit` as a type annotation do not work. Check the type against the concrete class, or subclass it, instead (`from eigenkit.core.circuit import Circuit as NumpyCircuit`, or `from eigenkit.torch import TorchCircuit`). ______________________________________________________________________ ## The NumPy interface (default) `Circuit(blocks, observables)` returns the core NumPy circuit. It does **not** store parameters: you pass a 1-D `weights` vector, of length `circuit.spec.n_weights`, and an `inputs` vector at each call. ```python import numpy as np from eigenkit import Circuit, Encoding, Variational, all_z circuit = Circuit( blocks=[Encoding(in_features=4, rotation="RX"), Variational(depth=2)], observables=all_z(4), ) # provider="numpy", interface="numpy" weights = np.zeros(circuit.spec.n_weights) x = np.array([0.1, 0.2, 0.3, 0.4]) y = circuit(weights, x) # shape (4,): one expectation per observable ``` ### Forward calls and batching `inputs` can be 1-D, for a single sample, or 2-D, with shape `(batch, n_inputs)`. `weights` stays 1-D, because the weights are shared across the batch. The output has shape `(n_observables,)` or `(batch, n_observables)`. It becomes a scalar, or shape `(batch,)`, when a single observable is measured. ```python x_batch = np.random.uniform(0, np.pi, size=(32, 4)) circuit(weights, x_batch) # shape (32, 4) ``` ### Gradients with `circuit.jacobian` The NumPy interface is fully differentiable without PyTorch: ```python g = circuit.jacobian(weights, x) # d/d(weights) gw, gx = circuit.jacobian(weights, x, input_grad=True) # also d/d(inputs) ``` `jacobian` returns shape `(n_weights,)` for a single observable, and `(n_observables, n_weights)` for several, in the same way as `__call__`. It uses the differentiation of the active backend when there is one, such as the analytic **adjoint** method of the NumPy simulator, or the automatic differentiation of PennyLane. Otherwise, it uses the **parameter-shift** rule, which works on any backend. Pass one 1-D sample. For a batch, loop over the rows. On a hardware backend, it raises an error, like `.backward()`, unless the configuration of the device sets `enable_gradients=True`. The variational optimisation tools use this gradient, so a loop that finds a ground state takes a few lines and does not need PyTorch. These tools work on a **purely** variational circuit, which has no `inputs`, so the example takes an ansatz in place of the encoding circuit above: ```python from eigenkit import Variational, Z, minimize_expectation ansatz = Circuit([Variational(depth=2)], observables=Z(0)) # n_inputs == 0 rng = np.random.default_rng(0) x0 = rng.uniform(0, 2 * np.pi, size=ansatz.spec.n_weights) result = minimize_expectation(ansatz, initial_parameters=x0) print(result.optimal_value) ``` ### Other primitives `zero_state_probability(weights, inputs)`, which gives the fidelity used to invert kernels, and `probabilities(weights, inputs)`, which gives the whole distribution of measurement outcomes, take the same `(weights, inputs)` arguments: ```python circuit.zero_state_probability(weights, x) circuit.probabilities(weights, x) # shape (2**n_qubits,) ``` A circuit with no trainable weights, such as the inversion circuit of a kernel that uses only data, takes an empty weight vector: `inversion.zero_state_probability([], x)`. ### When to use it The NumPy interface is the right default for the domains that do not use a framework (`chemistry`, `optimization`), for variational loops driven by SciPy, for any custom training where you control the optimiser, and for light installs that should not install PyTorch. ______________________________________________________________________ ## The PyTorch interface Pass `interface="torch"` to get a `torch.nn.Module`. It stores its weights as an `nn.Parameter`, so you call it with the inputs only, and you train it with any PyTorch optimiser. It needs the `torch` extra, and it runs on the built-in NumPy simulator, which is differentiable: ```python import torch from eigenkit import Circuit, Encoding, Variational, all_z layer = Circuit( blocks=[Encoding(in_features=4), Variational(depth=2)], observables=all_z(4), interface="torch", init=None, # optional initial weights, shape (n_weights,) ) layer.weights # nn.Parameter of shape (n_weights,) list(layer.parameters()) # plugs into torch.optim like any module ``` `init` sets the initial weight tensor. When it is not given, the weights are drawn at random with `torch.randn`. Both the top-level `Circuit(...)` factory and a `TorchCircuit(...)` built directly default to `provider="numpy"`. ### What the layer exposes | Member | What it does | | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `layer(x)` | The forward pass. `x` can be 1-D (one sample) or 2-D, with shape `(batch, n_inputs)`. | | `weights` | The trainable `nn.Parameter`, of shape `(n_weights,)`. | | `spec`, `n_qubits`, `out_features` | The assembled `CircuitSpec`, and the two sizes derived from it. | | `draw(decimals=3)` | A drawing of the circuit as text, over several lines. It takes no input, because the layer stores its weights. | | `draw_mpl(**kwargs)` | The same drawing as a matplotlib `(fig, ax)`. It needs the `viz` extra. | | `zero_state_probability(x)` | The probability of the all-zeros outcome, which is the fidelity that the kernels use. | | `submit(x)` | Submits the circuit without waiting, and returns a `JobHandle`. It works on Braket, Qiskit Runtime and IQM only. | | `dry_run_info()` | Gives, without submitting anything, the gate counts, the shots and the number of circuits that a call runs. It works on Braket, Qiskit Runtime and IQM, and the other backends raise `NotImplementedError`. | | `with_provider(provider, ...)` | Builds the same circuit again on another backend. See below. | `probabilities()` and `jacobian()` are **not** available on the PyTorch circuit. The whole distribution of measurement outcomes is a primitive of the NumPy interface, and the gradients come from `loss.backward()` instead of a call to a jacobian. `submit()` and `dry_run_info()` are explained in [QPU and Cloud](https://docs.kit.eigenorbit.com/user-guide/qpu/index.md). ### Forward calls and batching ```python y = layer(torch.randn(8, 4)) # (8, 4): batch of 8, one value per observable ``` A 1-D input is a single sample, and a 2-D input is a batch. The weights are shared across the batch, exactly as in the NumPy interface. ### Gradients with `loss.backward()` The gradients flow through `torch.autograd`: ```python x = torch.randn(8, 4) target = torch.zeros(8, 4) opt = torch.optim.Adam(layer.parameters(), lr=0.05) loss = (layer(x) - target).pow(2).mean() loss.backward() # weights.grad is populated opt.step() ``` Backends with their own automatic differentiation, such as PennyLane, differentiate through their own engine. For the other backends, the forward pass is wrapped in a `torch.autograd.Function`, whose backward pass uses the same parameter-shift or adjoint jacobian as the NumPy interface. Backends without gradients (`gradient_method="none"`, for example quantum hardware and Qiskit Runtime) raise a clear error on `.backward()`. See [QPU and Cloud](https://docs.kit.eigenorbit.com/user-guide/qpu/index.md) for the pattern of training on a simulator and then running on the device, and for `enable_gradients=True`, which lets a hardware configuration use the parameter-shift rule on the device. Training also works on the default **NumPy** provider, without PennyLane. The analytic adjoint method of the NumPy backend differentiates a whole batch in one sweep, so `Circuit(..., interface="torch")`, with the NumPy provider by default, trains hybrid models efficiently. Switch to `provider="pennylane"` if you prefer its simulator. Both process batches natively: the forward and backward passes of a `(B, …)` batch run in one call, not in a Python loop over the samples. ### Moving a circuit to another provider with `with_provider` `with_provider` builds the **same** `CircuitSpec` and observables again on a different backend, without listing the blocks again: ```python fast = layer.with_provider("qiskit") # clones weights fresh = layer.with_provider("qiskit", copy_weights=False) # re-initialises exact = layer.with_provider( "qiskit", init=torch.zeros(layer.spec.n_weights, dtype=torch.float64), ) ``` The weights are cloned by default. `copy_weights=False` draws new random weights, and `init=` sets them explicitly (it takes precedence over `copy_weights`). The new layer takes its data type from `init`, so pass `dtype=torch.float64` to keep the double precision that a layer built from blocks starts with. A plain `torch.zeros(n)` is `float32`, and it would halve the precision without any warning. ### Composing and saving Because it is a plain `nn.Module`, a PyTorch circuit can be used inside `nn.Sequential` and next to the ready-made layers (see [Layers](https://docs.kit.eigenorbit.com/user-guide/layers/index.md)). The scikit-learn compatible estimators (`HQNC`, `HQNR` and others) are built on it. Its state can be saved and restored with the usual PyTorch methods (`state_dict()` and `load_state_dict()`), and fitted estimators are saved as `.eigenkit` bundles. See [Model persistence](https://docs.kit.eigenorbit.com/user-guide/model-persistence/index.md). ### When to use it Use the PyTorch interface when the quantum circuit is one layer in a larger PyTorch model, or whenever you want to train end to end with automatic differentiation and the optimisers and tools of PyTorch. ______________________________________________________________________ ## Same circuit, both interfaces The interface is a choice made *when the code runs*, over one definition of the circuit. The blocks, the observables and the numerical results of the provider are the same: ```python import numpy as np, torch from eigenkit import Circuit, Encoding, Variational, all_z blocks = [Encoding(in_features=2), Variational(depth=1)] np_circuit = Circuit(blocks, all_z(2)) # numpy tc = Circuit(blocks, all_z(2), interface="torch") # torch w = tc.weights.detach().numpy() x = np.array([0.3, -0.2]) np.allclose(np_circuit(w, x), tc(torch.tensor(x)).detach().numpy()) # True ``` ## API reference The factory function that chooses the type from `interface=`, and the two concrete types that it returns. ### `eigenkit.circuit.Circuit(blocks, observables=None, provider='numpy', interface='numpy', init=None)` Build a circuit and compile it for a backend. This is the entry point for the rest of the library: you give it blocks, and it returns a circuit that you can call. The type of the circuit depends on `interface`. The NumPy interface returns a core `Circuit`, and the PyTorch interface returns a `TorchCircuit`. Parameters: | Name | Type | Description | Default | | ------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `blocks` | `_BlocksOrSpec` | The circuit. It can be a list of blocks to assemble, a CircuitSpec that is already assembled, or a CircuitBuilder. A builder is built when Circuit is called, so gates added to it afterwards are not part of this circuit. A builder can also take the place of a block in the list. | *required* | | `observables` | `ObservableLike` | What to measure. None measures Z on every qubit and returns one value per qubit. A single Observable or PauliExpr returns a single value, and a list of them returns one value for each. Default: None. | `None` | | `provider` | `ProviderSpec` | The backend that runs the circuit. It can be a registered name ("numpy", "pennylane", "qiskit" and others), a configuration instance such as PennyLaneConfig(...), or a backend object. Default: "numpy". | `'numpy'` | | `interface` | `_Interface` | "numpy" returns a circuit that takes the weights at each call and does not depend on any framework. "torch" returns a TorchCircuit, which is a PyTorch nn.Module that stores the weights. Default: "numpy". | `'numpy'` | | `init` | \`Tensor | None\` | The initial weights of the PyTorch circuit, of shape (n_weights,). They are random when init is not given. The NumPy interface stores no weights, so passing init with it raises a ValueError. Default: None. | Returns: | Name | Type | Description | | --------- | --------- | -------------- | | `circuit` | \`Circuit | TorchCircuit\` | Raises: | Type | Description | | ------------ | ------------------------------------------------------------------------------------------- | | `ValueError` | If interface is neither "numpy" nor "torch", or if init is passed with the NumPy interface. | Example ```python import numpy as np from eigenkit import Circuit, Encoding, Variational, all_z circuit = Circuit( blocks=[Encoding(in_features=4), Variational(depth=2)], observables=all_z(4), ) weights = np.zeros(circuit.spec.n_weights) values = circuit(weights, np.random.uniform(0, np.pi, size=4)) ``` ### `eigenkit.core.circuit.Circuit` A compiled circuit, ready to evaluate. It holds a `CircuitSpec`, the observables to measure and a compiled backend handle. The circuit is compiled once, when the instance is created, and every call reuses the compiled handle. The instance does not store weights: you pass the `weights` at each call. This is the NumPy interface. The top-level `Circuit(...)` function of the package returns this class for `interface="numpy"` and a `TorchCircuit` for `interface="torch"`. Parameters: | Name | Type | Description | Default | | ------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------- | | `spec` | `CircuitSpec` | The circuit to run, usually built with assemble(blocks). | *required* | | `observables` | `ObservableLike` | What to measure. None measures Z on every qubit and returns one value per qubit. A single Observable or PauliExpr returns a single value, and a list of them returns one value for each. Default: None. | `None` | | `provider` | `ProviderSpec` | The backend that runs the circuit. It can be a registered name ("numpy", "pennylane" and others), a configuration instance, or a provider object. Default: "numpy". | `'numpy'` | | `interface` | `Interface` | How a call is evaluated. With "numpy", a call runs the NumPy path and returns NumPy values. With "torch", each call is passed to the registered PyTorch interface, and TorchCircuit builds its core circuit in this way. The top-level Circuit function always passes "numpy". Default: "numpy". | `'numpy'` | Example ```python import numpy as np from eigenkit import Circuit, Encoding, Variational, all_z circuit = Circuit( blocks=[Encoding(in_features=2), Variational(depth=1)], observables=all_z(2), ) # The circuit stores no weights, so you pass them at every call. weights = np.zeros(circuit.spec.n_weights) values = circuit(weights, np.array([0.1, 0.2])) # Gradients, without PyTorch. grad = circuit.jacobian(weights, np.array([0.1, 0.2])) ``` #### `draw(*, decimals=3)` Return a text drawing of the circuit, over several lines. The drawing does not depend on the provider: it is built from the `CircuitSpec`, not from the compiled backend object. See `eigenkit.core.draw` for the details of the layout. Parameters: | Name | Type | Description | Default | | ---------- | ----- | ---------------------------------------------------------------------------- | ------- | | `decimals` | `int` | The number of significant digits shown for a numeric gate angle. Default: 3. | `3` | Returns: | Type | Description | | ----- | ------------------------------------------------------- | | `str` | The drawing, as a single string that contains newlines. | #### `draw_mpl(**kwargs)` Draw the circuit as a matplotlib figure, and return `(fig, ax)`. Gates are drawn as labelled boxes and controls as filled dots, and each measured wire (from `self.observables`) ends with a measurement symbol. As with `draw()`, the picture is built from the `CircuitSpec` and the observables, not from the compiled backend, so it does not depend on the provider. Pass `style="eigen"` for the brand colours (each gate is coloured by its kind: rotations, `H` and block gates, and measurements), or `style="roles"` to colour the outline of each gate by its role (encoding, trainable or fixed). The default is black on white. Each style has a `"-dark"` variant (for example `"eigen-dark"`) for dark backgrounds, and `"eigen-universal"` is a single image that works on both. matplotlib is an optional dependency. Install it with `eigenkit[viz]`. Parameters: | Name | Type | Description | Default | | ---------- | ----- | --------------------------------------------------------------------------------------------------------- | ------- | | `**kwargs` | `Any` | Arguments passed to draw_mpl(): decimals, show_params, show_measurements, fontsize, style, legend and ax. | `{}` | Returns: | Name | Type | Description | | -------- | ------- | ------------------------------ | | `fig_ax` | `tuple` | The matplotlib (fig, ax) pair. | #### `dry_run_info()` Describe the prepared circuit in a dictionary, without submitting it. It is available only on backends whose `CompiledCircuit` provides a `dry_run_info` method. Currently, these are Braket (on its local simulator as well as on cloud devices), Qiskit Runtime and IQM. It is useful to check the size of the circuit, the gate counts after transpilation, the number of shots and the number of circuits that each call runs, before you submit to a cloud simulator or a quantum processing unit (QPU). It does not estimate a price. #### `jacobian(weights, inputs=None, *, input_grad=False)` Return the gradient of the expectation values with respect to the weights. It returns `d/d(weights)`, of shape `(n_weights,)` for a single observable or `(n_observables, n_weights)` for a list of observables, like the output of `__call__()`. Pass a single 1-D `inputs` sample. For a batch, call it once for each row. The NumPy interface can compute every gradient without PyTorch. This method uses the same sequence of gradient methods as the variational optimisation tools and the PyTorch interface. It uses the backend's own differentiation when the backend has one (the analytic adjoint method of the NumPy simulator, the differentiation of PennyLane, and so on), and otherwise the parameter-shift rule, which works on any backend. With `input_grad=True`, it returns `(weight_jacobian, input_jacobian)`, where `input_jacobian` is `d/d(inputs)`, with the same leading shape. Parameters: | Name | Type | Description | Default | | ------------ | --------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `weights` | `ndarray` | The weight vector, of shape (n_weights,). | *required* | | `inputs` | \`ndarray | None\` | One 1-D sample, of shape (n_inputs,). This method takes a single sample, so for a batch, call it once for each row. None works only for a circuit that takes no inputs, and it raises a ValueError for any other circuit. Default: None. | | `input_grad` | `bool` | If True, also return the jacobian with respect to the inputs. Default: False. | `False` | Returns: | Name | Type | Description | | ---------- | --------- | ------------------------- | | `jacobian` | \`ndarray | tuple[ndarray, ndarray]\` | Raises: | Type | Description | | -------------- | ----------------------------------------------------------------------------------------------- | | `RuntimeError` | On a hardware backend whose configuration leaves enable_gradients off (gradient_method="none"). | #### `probabilities(weights, inputs=None)` Return the measurement distribution over all `2**n_qubits` basis states. The basis states are ordered with qubit `0` as the most significant bit. The distribution is used to decode samples. For example, the quantum approximate optimisation algorithm (QAOA) reads the most probable bitstrings from the optimised state. Parameters: | Name | Type | Description | Default | | --------- | --------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `weights` | `ndarray` | The weight vector, of shape (n_weights,). | *required* | | `inputs` | \`ndarray | None\` | One 1-D sample. None works only for a circuit that takes no inputs, and it raises a ValueError for any other circuit. Default: None. | Returns: | Type | Description | | --------- | ----------------------------------------------------------------- | | `ndarray` | The probabilities of all basis states, of shape (2\*\*n_qubits,). | Raises: | Type | Description | | --------------------- | ------------------------------------------------------------------------------------------------------------ | | `NotImplementedError` | On a backend that has neither a probabilities() method nor a state vector to compute the probabilities from. | #### `resources(**kwargs)` Report the resources that the circuit uses once transpiled for its target. It reports the native gate counts, the number of two-qubit gates and the two-qubit depth, the physical qubits chosen by the layout, the number of two-qubit gates added by routing (compared with the same circuit on the native gates with all-to-all connectivity), and the success probability predicted by the error rates of the target. Nothing is submitted. Parameters: | Name | Type | Description | Default | | ---------- | ----- | ------------------------------------------------------------------------------------------------------------- | ------- | | `**kwargs` | `Any` | Arguments passed to the backend, for example measured=False on Qiskit Runtime for the bare estimator circuit. | `{}` | Returns: | Type | Description | | ------ | ----------------------------------------------------------------- | | `dict` | A dictionary. See QiskitRuntimeCompiled.resources() for its keys. | Raises: | Type | Description | | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `NotImplementedError` | On backends that do not transpile for a device, which includes every simulator. To inspect a device locally, use Qiskit Runtime or IQM with a fake backend_instance. | #### `sample(weights, inputs=None)` Return the raw measurement histogram, which maps each bitstring to a count. It is what a sampling backend measured, before any averaging. These are the counts behind `probabilities()`, and it is the only form that stays usable when the circuit has too many qubits for a dense distribution. In each bitstring, qubit `0` is the most significant bit, so `"100"` means that qubit `0` was measured in state `1`. Only sampling backends can return a histogram, because it needs a finite number of shots. Exact statevector simulators raise an error instead of making up a histogram. Use `probabilities()` with those. Parameters: | Name | Type | Description | Default | | --------- | --------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `weights` | `ndarray` | The weight vector, of shape (n_weights,). | *required* | | `inputs` | \`ndarray | None\` | One 1-D sample. None works only for a circuit that takes no inputs, and it raises a ValueError for any other circuit. Default: None. | Returns: | Name | Type | Description | | -------- | ---------------- | -------------------------------------------------------------------------------------------------------------- | | `counts` | `dict[str, int]` | The number of shots for each measured bitstring. The counts add up to the number of shots set for the backend. | Raises: | Type | Description | | --------------------- | -------------------------------------------------------------------------------------------------- | | `NotImplementedError` | On backends that do not sample. This includes every exact simulator, which has no shots to report. | #### `statevector(weights, inputs=None, initial_state=None)` Return the full complex statevector after the circuit runs. It is a flat array of length `2**n_qubits`, ordered with qubit `0` as the most significant bit. A 2-D `inputs` batch returns an array of shape `(B, 2**n_qubits)`. With `initial_state`, a normalised flat statevector in the same order, the evolution starts from that state instead of `|0…0⟩`. Reading a state out and passing it back in this way is the basic operation behind the state carried over in quantum reservoirs and behind amplitude encoding. Parameters: | Name | Type | Description | Default | | --------------- | --------- | ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | `weights` | `ndarray` | The weight vector, of shape (n_weights,). | *required* | | `inputs` | \`ndarray | None\` | A 1-D sample, or a 2-D batch. None works only for a circuit that takes no inputs, and it raises a ValueError for any other circuit. Default: None. | | `initial_state` | \`ndarray | None\` | The state to start the evolution from, instead of | Returns: | Type | Description | | --------- | ------------------------------------------------------------------------------ | | `ndarray` | The statevector, of shape (2\*\*n_qubits,), or (B, 2\*\*n_qubits) for a batch. | Raises: | Type | Description | | --------------------- | --------------------------------------------------------------------------------------------------------------------------- | | `NotImplementedError` | On backends without access to the statevector, which is the case for most quantum processing unit (QPU) and cloud backends. | #### `submit(weights, inputs=None)` Submit a job without waiting for it, and return a `JobHandle`. `handle.result()` returns the expectation value of each observable. Parameters: | Name | Type | Description | Default | | --------- | --------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `weights` | `ndarray` | The weight vector, of shape (n_weights,). | *required* | | `inputs` | \`ndarray | None\` | One 1-D sample. None works only for a circuit that takes no inputs, and it raises a ValueError for any other circuit. Default: None. | Returns: | Name | Type | Description | | -------- | ----------- | ------------------------------- | | `handle` | `JobHandle` | A handle for the submitted job. | Raises: | Type | Description | | --------------------- | -------------------------------------------------------------------------------------------------------------------- | | `NotImplementedError` | If the CompiledCircuit of the backend does not provide submit. Currently, Braket, Qiskit Runtime and IQM provide it. | #### `transpiled_circuit(**kwargs)` Return the `qiskit.QuantumCircuit` that a hardware backend submits. The circuit is transpiled for the target of the backend. Use it to draw the circuit (`.draw("mpl")`) or to inspect it. Raises: | Type | Description | | --------------------- | ----------------------------------------------- | | `NotImplementedError` | On backends that do not transpile for a device. | #### `zero_state_probabilities(weights, inputs)` Compute `zero_state_probability()` for every row of a batch of inputs. `inputs` has shape `(B, n_inputs)`, and the result is an array of shape `(B,)` of all-zeros probabilities. Backends that provide a batched `zero_state_probabilities` (such as the NumPy simulator) compute the whole batch at once. The other backends call the single-sample method once for each row. This is what lets QuantumKernel build a fidelity matrix without a Python loop over pairs of samples. Parameters: | Name | Type | Description | Default | | --------- | --------- | ----------------------------------------- | ---------- | | `weights` | `ndarray` | The weight vector, of shape (n_weights,). | *required* | | `inputs` | `ndarray` | A batch, of shape (B, n_inputs). | *required* | Returns: | Type | Description | | --------- | ------------------------------------------------------------------ | | `ndarray` | One all-zeros probability for each row, as an array of shape (B,). | #### `zero_state_probability(weights, inputs=None)` Return the probability of measuring the all-zeros state after the circuit. It is useful for quantities based on fidelity: quantum kernels built from inversion circuits, swap tests and the Bures distance. Each backend computes it in its own way. PennyLane returns `qml.probs(...)[0]`, the Qiskit and NumPy simulators return the squared amplitude of the all-zeros basis state, Braket uses its own probability result, and IQM and Qiskit Runtime estimate it from shots. This method does not go through the `Observable` path, because the value is not the expectation value of a Pauli operator. Parameters: | Name | Type | Description | Default | | --------- | --------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `weights` | `ndarray` | The weight vector, of shape (n_weights,). | *required* | | `inputs` | \`ndarray | None\` | One 1-D sample. None works only for a circuit that takes no inputs, and it raises a ValueError for any other circuit. Default: None. | Returns: | Type | Description | | ------- | ----------------------------------- | | `float` | The probability, a float in [0, 1]. | Raises: | Type | Description | | --------------------- | -------------------------------------------------------------- | | `NotImplementedError` | If the active backend does not provide zero_state_probability. | ### `eigenkit.torch.circuit.TorchCircuit` Bases: `Module` A quantum layer that stores trainable weights and runs on any registered backend. It runs through the PyTorch interface, so that PyTorch can compute the gradients of its output. Parameters: | Name | Type | Description | Default | | ------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | | `blocks` | `_BlocksOrSpec` | The circuit. It can be a list of blocks to assemble, a CircuitSpec that is already assembled, or a CircuitBuilder. A builder is built when the layer is created, so gates added to it afterwards are not part of this layer. | *required* | | `observables` | `ObservableLike` | What to measure. None measures Z on every qubit and returns one value per qubit. A single Observable or PauliExpr returns a single value, and a list of them returns one value for each. Default: None. | `None` | | `provider` | `ProviderSpec` | The backend that runs the circuit. It can be a registered name, a configuration instance, or a provider object. Any backend works. A backend that computes gradients itself avoids the fallback to the parameter-shift rule. Default: "numpy". | `'numpy'` | | `init` | \`Tensor | None\` | The initial weights, of shape (n_weights,). They are random when init is not given. Default: None. | #### `n_qubits` The number of qubits of the circuit in `spec`. #### `out_features` The number of observables, or `1` for a single observable. #### `provider` The backend that this circuit was compiled for. It is the same as Circuit.provider, which turns the `provider=` argument (a registered name, a configuration or a backend object) into one `Provider`. The registered name is `provider.name`. To run the same circuit on another backend, use `with_provider()`, which returns a new instance and leaves this one unchanged. #### `spec` The circuit that this layer runs. It is the CircuitSpec assembled from `blocks`, or the one that was passed in already assembled. #### `draw(*, decimals=3)` Return a text drawing of the circuit, over several lines. The drawing does not depend on the provider: it is built from the `CircuitSpec`, so it is the same whatever the active backend is. Print the returned string directly. Example ```python from eigenkit import Circuit, Encoding, Variational blocks = [Encoding(in_features=2), Variational(depth=1)] circuit = Circuit(blocks=blocks, interface="torch") print(circuit.draw()) # q_0: ──RY(x_0)──RY(w_0)──●── # │ # q_1: ──RY(x_1)──RY(w_1)──X── ``` Parameters: | Name | Type | Description | Default | | ---------- | ----- | ---------------------------------------------------------------------------- | ------- | | `decimals` | `int` | The number of significant digits shown for a numeric gate angle. Default: 3. | `3` | Returns: | Type | Description | | ----- | ------------------------------------------------------- | | `str` | The drawing, as a single string that contains newlines. | #### `draw_mpl(**kwargs)` Draw the circuit as a matplotlib figure, and return `(fig, ax)`. The picture does not depend on the provider, because it is built from the `CircuitSpec` and the observables. matplotlib is an optional dependency. Install it with `eigenkit[viz]`. The keyword arguments are passed to draw_mpl(). #### `dry_run_info()` Describe the prepared circuit in a dictionary, without submitting it. It is useful to check the size of the circuit, the gate counts after transpilation, the number of shots and the number of circuits that each call runs, before you submit to a cloud simulator or a quantum processing unit (QPU). It does not estimate a price. It is available only on the backends that provide it. Currently, these are Braket (on its local simulator as well as on cloud devices), Qiskit Runtime and IQM. Other backends raise a `NotImplementedError`. #### `forward(x=None)` Run the layer. Parameters: | Name | Type | Description | Default | | ---- | -------- | ----------------------------------------------- | ------- | | `x` | `Tensor` | The input batch, of shape (batch, in_features). | `None` | Returns: | Type | Description | | -------- | --------------------------------------- | | `Tensor` | The output of the layer for that batch. | #### `reset_parameters()` Draw new circuit weights from the current PyTorch random number generator. Every `nn.Module` that PyTorch provides defines this method. The estimators call `model.apply(reset_parameters)` after setting the seed, so that `random_state` controls the whole initialisation. Without this method, the estimators would skip the layer: two fits with different values of `random_state` would give different classical weights but exactly the same quantum weights, and a user who restarts with new seeds to escape a barren plateau would change only the part of the model that is not the circuit. #### `sample(x=None)` Return the raw measurement histogram for the current weights and input. The histogram maps each bitstring to its number of shots. In each bitstring, qubit `0` is the most significant bit. It is the second step of the workflow in which you train on a simulator and then run on hardware: you train the layer, and then you read what the device measured. It takes one sample, and its result is not differentiable, because the counts are integers that PyTorch does not record for automatic differentiation. Parameters: | Name | Type | Description | Default | | ---- | -------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `x` | \`Tensor | None\` | The input features of one sample, as a 1-D tensor. None works only for a circuit that takes no inputs, and it raises a ValueError for any other circuit. Default: None. | Returns: | Name | Type | Description | | -------- | ---------------- | ------------------------------------------------ | | `counts` | `dict[str, int]` | The number of shots for each measured bitstring. | Raises: | Type | Description | | --------------------- | ------------------------------- | | `NotImplementedError` | On backends that do not sample. | | `ValueError` | If x is 2-D. | #### `submit(x=None)` Submit a job without waiting for it, and return a `JobHandle`. Only the backends that provide submission support it (Braket, Qiskit Runtime and IQM). Parameters: | Name | Type | Description | Default | | ---- | -------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `x` | \`Tensor | None\` | One 1-D input sample. None works only for a circuit that takes no inputs, and it raises a ValueError for any other circuit. Default: None. | Returns: | Name | Type | Description | | -------- | ----------- | ------------------------------- | | `handle` | `JobHandle` | A handle for the submitted job. | #### `with_provider(provider, *, copy_weights=True, init=None)` Return a new `TorchCircuit` for another `provider`. The new circuit has the same `spec` and the same `observables` argument as this instance, and it is compiled for the given `provider`. `provider` accepts the same forms as in `__init__()`: a registered name (a `str`), a configuration object with a `make_backend()` method (for example `PennyLaneConfig`), or a backend instance. The gradients follow the `gradient_method` of the new backend. The forward values should match across simulators up to numerical tolerance, but not exactly. Parameters: | Name | Type | Description | Default | | -------------- | -------------- | ------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | `provider` | `ProviderSpec` | The backend for the new circuit. | *required* | | `copy_weights` | `bool` | If True, the new circuit starts from a copy of self.weights. It is ignored when init is given. Default: True. | `True` | | `init` | \`Tensor | None\` | An optional weight tensor of shape (spec.n_weights,). When it is given, it takes precedence over copy_weights. Default: None. | Returns: | Type | Description | | ---------------- | --------------------- | | `'TorchCircuit'` | The new TorchCircuit. | #### `zero_state_probability(x=None)` Return the probability of measuring the all-zeros state. It uses the current weights and the given input. It is useful for fidelity kernels and swap tests. It takes one sample, and its result is not differentiable. The core reads the amplitude through the backend's access to the state and returns a plain `float`, so PyTorch does not record it for automatic differentiation. A batch raises an error, because a single number cannot hold a batch axis. For a batch, call this method once for each row, or use `Circuit.zero_state_probabilities`. Parameters: | Name | Type | Description | Default | | ---- | -------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `x` | \`Tensor | None\` | The input features of one sample, as a 1-D tensor. None works only for a circuit that takes no inputs, and it raises a ValueError for any other circuit. Default: None. | Returns: | Type | Description | | ------- | --------------------------------------------- | | `float` | The all-zeros probability, a float in [0, 1]. | Raises: | Type | Description | | ------------ | ------------ | | `ValueError` | If x is 2-D. | # Quantum Kernels `QuantumKernel` computes a quantum kernel matrix based on fidelity, for use with kernel methods such as support vector machines (SVMs), Gaussian processes and kernel ridge regression. It works with any provider. The feature map is any circuit that you write, or one of two built-in maps. The inverse half of the circuit is derived with `adjoint()`. The Gram matrix comes from a contraction of statevectors on simulators, or from the `zero_state_probability` primitive on shot-based and hardware backends. It is defined in the **qml package** (`eigenkit.qml`), which does not use PyTorch, and it is re-exported at the top level, which is where the examples below import it from. It runs the inversion circuit on the NumPy interface, so it needs scikit-learn, which the `qml` extra installs, and not PyTorch. It needs no provider extra either: `provider` defaults to the built-in NumPy simulator, which every install has. ```python import numpy as np from eigenkit import QuantumKernel qk = QuantumKernel(n_features=4, feature_map="AngleCNOT", reps=2) X_train = np.random.randn(50, 4) X_test = np.random.randn(20, 4) y_train = (X_train[:, 0] > 0).astype(int) # toy labels for the sklearn hand-off below K_train = qk.compute_kernel_matrix(X_train) # (50, 50) K_test = qk.compute_kernel_matrix(X_test, X_train) # (20, 50) ``` | Parameter | Type | Default | Description | | ---------------- | ------------------------------------------------------------------------ | ------------- | ------------------------------------------------------------------------------------------------------------- | | `n_features` | `int` | Required | The number of features. For the built-in maps, it is also the number of qubits. | | `feature_map` | `str`, `CircuitSpec`, `CircuitBuilder`, a block or blocks, or a callable | `"AngleCNOT"` | `"Angle"`, `"AngleCNOT"`, or a custom forward map `U(x; w)`. See [Custom feature maps](#custom-feature-maps). | | `reps` | `int` | `2` | The number of repetitions of a built-in map. It is ignored for a custom map. | | `provider` | `ProviderSpec` | `"numpy"` | The backend, with the same options as `Circuit(provider=...)`. | | `weights` | array | `None` | The trainable weights of the feature map. `None` means zeros. | | `scale` | `float` | `1.0` | The bandwidth: the inputs are multiplied by it before they are encoded. | | `n_weights` | `int` | `None` | The number of weights that a callable map reserves. | | `n_qubits` | `int` | `None` | The number of qubits of a callable map. It defaults to `n_features`. | | `psd_projection` | `bool` | `False` | Whether to clip the negative eigenvalues of a Gram matrix with shot noise. | ## Feature maps | `feature_map` | Description | | ------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `"Angle"` | Angle encoding only, without entanglement, which gives a product state. It uses `RX` on the first repetition and `RZ` on the next ones. | | `"AngleCNOT"` | The same as `"Angle"`, plus a ladder of controlled-NOT (CNOT) gates before each repetition after the first. | `reps` sets the depth of the feature map. `reps=1` uses only the `RX` embedding, and `reps>=2` adds repetitions of the `RZ` embedding, with CNOT gates in between when `feature_map="AngleCNOT"`. `"Angle"` prepares a product state, so its kernel factorises into `prod_q cos^2((x_q - x'_q) / 2)`, which is a classical kernel. Use an entangling map when you want a kernel that a classical computer cannot write down in one line. ### Custom feature maps Pass the forward map `U(x; w)` itself. The inversion circuit is derived with `adjoint()`, and the two halves share the weights: ```python from eigenkit import QuantumKernel def zz_map(qc, x, w): """H, a data phase per qubit, and a ZZ interaction on each pair.""" for q in range(len(x)): qc.h(q) qc.phase_shift(x[q], qubit=q) for a in range(len(x)): for b in range(a + 1, len(x)): qc.pauli_exponential([("Z", a), ("Z", b)], x[a]) qk = QuantumKernel(n_features=3, feature_map=zz_map) qk.draw() # the forward map; draw(inversion=True) for U(x1) U(x2)^dagger qk.draw_mpl(style="eigen") ``` `feature_map` accepts a callable `emit(builder, x, w)` as above, a `CircuitSpec`, a `CircuitBuilder`, a block such as `Encoding(3, rotation="RX")`, or a list of blocks. The map must read exactly `n_features` inputs. The circuits are public. `qk.feature_map_circuit` is the forward map `U(x; w)`, `qk.circuit` is the inversion circuit that shot-based backends run, and `qk.kernel_spec` is the `CircuitSpec` of the inversion circuit. `qk.draw_mpl(inversion=True)` draws the circuit that a quantum processing unit (QPU) runs for one entry of the kernel. ### Trainable kernels and kernel alignment Give the map weights, and train them with `align_kernel`. It minimises the dual objective of the SVM (`loss="svc"`, which is quantum kernel alignment), or minus the kernel-target alignment (`loss="kta"`). By default, it uses simultaneous perturbation stochastic approximation (SPSA): ```python from eigenkit import QSVC, align_kernel def trainable_map(qc, x, w): for q in range(len(x)): qc.ry(w[0], qubit=q) zz_map(qc, x, w) kernel = QuantumKernel(n_features=4, feature_map=trainable_map, n_weights=1) result = align_kernel(kernel, X_train, y_train, loss="svc", maxiter=20) model = QSVC(kernel=result.kernel).fit(X_train, y_train) result.loss_history # plot it ``` `align_kernel` returns a `KernelAlignmentResult`. It holds the trained `weights`, a `kernel` that carries them, the final `loss` and the `initial_loss`, the `loss_history`, the number of kernel matrices computed (`n_evaluations`), and `success`, which says whether the loss improved. Every evaluation of the loss computes one Gram matrix. SPSA computes three per iteration, whatever the number of weights: two for the estimate of the gradient, and one to record the loss. It computes one more at the start. When `learning_rate=None`, the default, it also calibrates its first step, which costs `10` more evaluations. It returns the best point that it visited. `optimizer` also accepts the name of a `scipy.optimize.minimize` method, such as `"COBYLA"`, or a callable `optimizer(fun, x0) -> x`, and `loss` accepts a callable `loss(K, y) -> float`. The `"svc"` loss needs binary labels. Both built-in losses also work on their own, to score a kernel without training it: ```python from eigenkit.qml import kernel_target_alignment, svc_dual_loss K_train = kernel.compute_kernel_matrix(X_train) kernel_target_alignment(K_train, y_train) # in [-1, 1]; higher is better svc_dual_loss(K_train, y_train, C=1.0) # the "svc" loss; lower is better ``` To evaluate a kernel with other weights, pass them at each call, or take a copy. `kernel.compute_kernel_matrix(X, weights=w)` leaves the kernel unchanged, and `kernel.with_weights(w)` returns a new kernel that carries `w`. ### Bandwidth `scale` multiplies every input before it is encoded. Small values keep the states close together, and they avoid the exponential concentration of kernel values that wide and expressive maps suffer from. `QSVC(scale=...)` exposes the same setting to `GridSearchCV`. Set it in one place only: when both `QSVC(scale=...)` and the `scale` of the kernel are set, the two are multiplied. ## How the kernel value is computed For two inputs `x_i` and `x_j`, the kernel value is the fidelity of the two state preparations: ```text K[i, j] = |<0...0| U^dag(x_j) U(x_i) |0...0>|^2 ``` How this value is obtained depends on the backend, and the two ways differ in cost: - **Backends that expose a statevector** (NumPy, PennyLane in analytic mode, Qiskit, Qiskit Aer, Qulacs, qsim). The whole Gram matrix comes from one batched state evolution and a single matrix product. This takes `O(N)` state evolutions, and no inversion circuit is involved. The kernel tries this way on first use, and keeps it when it works. - **Shot-based and QPU backends** (Qiskit Runtime, IQM, Braket, PennyLane with shots). The kernel uses the inversion circuit: it applies `U(x_i)`, then the adjoint of `U(x_j)`, and reads the probability of the all-zeros state. On Qiskit Runtime and IQM, all the pairs go into one batched call, which is a single job. The other backends evaluate the pairs in smaller calls. A Gram matrix measures only the `N(N-1)/2` pairs above its diagonal. A Gram matrix with shot noise can have small negative eigenvalues. `QuantumKernel(psd_projection=True)` clips them, and `project_psd(K)` from `eigenkit.qml` does the same to any matrix. The circuits are compiled the first time they are used, so building or cloning a kernel never transpiles or submits anything. ## On hardware ```python from eigenkit import QiskitRuntimeConfig X_small = X_train[:8, :2] # a few points, two features for the 2-qubit map qk = QuantumKernel( n_features=2, feature_map="AngleCNOT", provider=QiskitRuntimeConfig( backend_name="ibm_fez", primitive="sampler", shots=1000, seed_transpiler=0 ), ) qk.estimate_cost(X_small) # circuits, jobs, shots, transpiled 2q gates qk.circuit.resources() # depth, 2q count, routing, est. success job = qk.submit_kernel_matrix(X_small) job.save("kernel_job.json") # job id + pair layout K = job.result() # or, later: qk.retrieve_kernel_matrix("kernel_job.json").result() ``` `submit_kernel_matrix()` returns a `KernelJob` without waiting for the device. The job has `job_id()`, `status()`, `result()`, which waits and then assembles the matrix, and `save(path)`, which writes the job identifier and the layout of the pairs. `qk.retrieve_kernel_matrix(path)` attaches to the job again from another session, and returns a `KernelJob` again. With a bare job identifier in place of the saved file, also pass the size of the matrix: `n_x` for a Gram matrix, and also `n_y` for a test matrix. Attaching again needs a device looked up through the service, because fake backends and Aer keep no history of jobs. The same code runs on IQM with `IQMConfig(...)`. It also runs locally on fake devices (`backend_instance=FakeFez()`, `IQMFakeGarnet()`), or on a noiseless `AerSimulator()` to see the shot noise alone. ## Choosing a backend The same `provider=` argument as in `Circuit` works here, and it defaults to `"numpy"`, the built-in reference simulator, which every install has. A kernel entry is a fidelity, so every backend returns the same number. What changes is how fast it arrives, and what has to be installed. Name any other registered provider to switch: ```python # The default: no extra dependencies. qk = QuantumKernel(n_features=4, feature_map="Angle", reps=1) # Any other registered provider works the same way, once its extra is # installed: qk = QuantumKernel(n_features=4, feature_map="Angle", reps=1, provider="pennylane") ``` The results agree to machine precision across the NumPy simulator, PennyLane, Qiskit and Braket. CUDA Quantum also works, as long as the feature map does not need parameter expressions that it does not support. ## Use with scikit-learn estimators `QuantumKernel` is the kernel behind `QSVC` and `QSVR`, which pass it to scikit-learn's `SVC(kernel="precomputed")` and `SVR(kernel="precomputed")`. Pass any kernel, whether custom, trained or on hardware, with `QSVC(kernel=qk)`. It survives `clone`, `GridSearchCV`, `Pipeline`, and saving and loading as `.eigenkit`. You can also give `K_train` and `K_test` to any scikit-learn estimator that supports precomputed kernels: ```python from sklearn.svm import SVC clf = SVC(kernel="precomputed", C=1.0) clf.fit(K_train, y_train) predictions = clf.predict(K_test) ``` ## Direct use of `zero_state_probability` For kernels, pass a hand-built map to `QuantumKernel(feature_map=...)`, as shown above. This section shows the primitive underneath, for quantities based on fidelity other than kernels. One such map: `RY(x_q)` embeds one feature per qubit, then a `CNOT-RZ-CNOT` block acts on each pair of neighbouring qubits, which is the usual way to write a two-qubit interaction into a feature map. It has six qubits, six features and one repetition. Another map, with two repetitions: a layer of Hadamard gates, `RZ` and `RX` rotations that carry the data, and `CZ` gates across every pair of qubits instead of along a ladder, then the rotations and the entangling gates once more. Neither of these two maps is `"Angle"` or `"AngleCNOT"`. Both are the kind of map that you build with `CircuitBuilder` and pass to `QuantumKernel(feature_map=...)`. For custom observables based on fidelity (swap tests, the Bures distance and so on), call `circuit.zero_state_probability(weights, inputs)` directly. It returns the probability of the all-zeros state without going through the Pauli machinery: ```python from eigenkit import Circuit, CircuitBuilder from eigenkit.core.ir import Negate qc = CircuitBuilder(n_qubits=2) x = qc.add_inputs(2) for q in range(2): qc.rx(x[q], qubit=q) for q in reversed(range(2)): qc.rx(Negate(x[q]), qubit=q) spec = qc.build() circuit = Circuit(blocks=spec) # The inversion circuit has no trainable weights, so pass an empty weight vector. print(circuit.zero_state_probability([], [0.4, -0.2])) # ~1.0 (U U^dag) ``` ## API reference ### `eigenkit.qml._kernels.QuantumKernel` Quantum kernel matrix `K[i, j] = fidelity(U(x_i), U(x_j))`. Parameters: | Name | Type | Description | Default | | ---------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- | | `n_features` | `int` | The number of features. For the built-in maps, it is also the number of qubits. | *required* | | `feature_map` | `FeatureMapLike` | The feature map. It is "Angle", "AngleCNOT", or a custom forward map U(x; w). A custom map is a CircuitSpec, a CircuitBuilder, a block, a list of blocks, or a callable emit(builder, x, w) that writes the map onto a CircuitBuilder, given the input references x and the weight references w. A custom map must read exactly n_features inputs. Default: "AngleCNOT". | `'AngleCNOT'` | | `reps` | `int` | The number of repetitions of a built-in feature map. reps=1 uses only the RX encoding, and reps>=2 adds repetitions of the RZ encoding (with ladders of controlled-NOT (CNOT) gates for "AngleCNOT"). It is ignored for a custom map. Default: 2. | `2` | | `provider` | `ProviderSpec` | The backend. It takes the same options as Circuit(provider=...). Default: "numpy", which needs no extra. | `'numpy'` | | `weights` | \`Sequence[float] | ndarray | None\` | | `scale` | `float` | The bandwidth. Every input is multiplied by it before it reaches the circuit, so scale sets how far apart two samples are in Hilbert space. Default: 1.0. | `1.0` | | `n_weights` | \`int | None\` | The number of weights that a callable feature_map reserves. For any other map, it is read from the circuit and, if given, checked. | | `n_qubits` | \`int | None\` | The number of qubits of a callable feature_map. Default: n_features. | | `psd_projection` | `bool` | If True, a sampled Gram matrix is projected onto the nearest positive semidefinite matrix. It only affects backends that use shots. Default: False. | `False` | Example ```python import numpy as np from eigenkit import QuantumKernel rng = np.random.default_rng(0) X_train, X_test = rng.uniform(0, np.pi, (8, 4)), rng.uniform(0, np.pi, (3, 4)) qk = QuantumKernel(n_features=4, feature_map="AngleCNOT", reps=2) K_train = qk.compute_kernel_matrix(X_train) K_test = qk.compute_kernel_matrix(X_test, X_train) # A custom feature map with a trainable weight. def zz_map(qc, x, w): for q in range(len(x)): qc.ry(w[0], q) qc.h(q) qc.rz(x[q], q) qc.pauli_exponential([("Z", 0), ("Z", 1)], x[0]) trainable = QuantumKernel(n_features=2, feature_map=zz_map, n_weights=1) K = trainable.compute_kernel_matrix(X_train[:, :2], weights=[0.3]) ``` #### `circuit` The compiled inversion circuit. A backend with shots runs it for each kernel entry. It is compiled when it is first accessed. #### `feature_map_circuit` The compiled forward feature map `U(x; w)`. It is compiled when it is first accessed. #### `feature_map_spec` The forward feature map `U(x; w)`, as a `CircuitSpec`. #### `kernel_spec` The inversion circuit `U(x_1; w) U(x_2; w)^dagger`, as a `CircuitSpec`. It has `2 * n_features` inputs. #### `n_qubits` The number of qubits of the feature-map circuit. #### `n_weights` The number of trainable weights in the feature map. #### `__sklearn_clone__()` Return a new copy of the kernel, not compiled yet, for `sklearn.base.clone`. `GridSearchCV`, `cross_val_score` and `Pipeline` use it. The copy has the same feature map, provider, weights, scale and `psd_projection`. #### `compute_kernel_matrix(X, Y=None, *, weights=None)` Return the kernel matrix between the rows of `X` and the rows of `Y`. Parameters: | Name | Type | Description | Default | | --------- | --------- | ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | | `X` | `ndarray` | The samples, of shape (n_X, n_features). | *required* | | `Y` | \`ndarray | None\` | The second set of samples, of shape (n_Y, n_features). If None, the method returns K(X, X). | | `weights` | \`ndarray | None\` | The feature-map weights for this evaluation. By default, they are the kernel's own weights, or zeros if it has none. | Returns: | Type | Description | | --------- | ----------------------------- | | `ndarray` | An array of shape (n_X, n_Y). | #### `draw(*, inversion=False, decimals=3)` Return a text drawing of the feature map, or of the inversion circuit. Parameters: | Name | Type | Description | Default | | ----------- | ------ | ---------------------------------------------------- | ------- | | `inversion` | `bool` | If True, draw U(x_1) U(x_2)^dagger instead of U(x). | `False` | | `decimals` | `int` | The number of significant digits of constant angles. | `3` | Returns: | Type | Description | | ----- | ------------------------------------------ | | `str` | The drawing, as a string of several lines. | #### `draw_mpl(*, inversion=False, **kwargs)` Draw the feature map, or the inversion circuit, with Matplotlib. The inversion circuit is drawn with a measurement on every qubit, and the forward map without one. This method needs the `viz` extra. Parameters: | Name | Type | Description | Default | | ----------- | ------ | ---------------------------------------------------------- | ------- | | `inversion` | `bool` | If True, draw U(x_1) U(x_2)^dagger instead of U(x). | `False` | | `**kwargs` | `Any` | Arguments passed to draw_mpl(), for example style="eigen". | `{}` | Returns: | Type | Description | | ----------------------- | ------------------------------------------------------ | | `'tuple[Figure, Axes]'` | The Matplotlib figure and axes, as the pair (fig, ax). | #### `estimate_cost(X, Y=None)` Return what computing `K(X, Y)` would need on the backend of this kernel. Nothing is submitted. On a sampling backend, the inversion circuit is compiled (and transpiled, for hardware) to report its size. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------- | ------------------------------------------------------------------------------------------- | | `X` | `ndarray` | The samples, of shape (n_X, n_features). | *required* | | `Y` | \`ndarray | None\` | The second set of samples, of shape (n_Y, n_features), or None for the Gram matrix K(X, X). | Returns: | Type | Description | | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `dict` | A dictionary with the keys method ("statevector" or "inversion"), n_circuits, n_jobs, shots and total_shots. With the "statevector" method, it also has n_state_evolutions. When the backend reports them, it also has the transpiled two-qubit gate count n_2q_gates, the depth and the backend_name. | #### `from_json_dict(data, provider=None)` Rebuild a kernel written by `to_json_dict()`. Parameters: | Name | Type | Description | Default | | ---------- | -------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | `data` | `dict` | The stored description. | *required* | | `provider` | \`ProviderSpec | None\` | A provider that replaces the stored one. It is needed when the stored provider held a secret, or to move a saved model onto another backend. | Returns: | Type | Description | | ----------------- | ------------------- | | `'QuantumKernel'` | The rebuilt kernel. | #### `kernel_value(x1, x2, *, weights=None)` Return the kernel value of one pair `(x1, x2)`, from the inversion circuit. #### `retrieve_kernel_matrix(job, n_x=None, n_y=None)` Reconnect to a kernel job submitted earlier, possibly in another session. Parameters: | Name | Type | Description | Default | | ----- | ----- | ----------- | ------------------------------------------------------------------------------------ | | `job` | \`str | Path\` | The JSON file written by KernelJob.save(), or a job id on its own. | | `n_x` | \`int | None\` | The number of rows of the matrix. It is required with a job id on its own. | | `n_y` | \`int | None\` | The number of columns, for a rectangular matrix. Leave it as None for a Gram matrix. | Returns: | Type | Description | | ----------- | ---------------------------------------------------- | | `KernelJob` | A KernelJob. Call .result() on it to get the matrix. | #### `submit_kernel_matrix(X, Y=None, *, weights=None)` Submit `K(X, Y)` as one asynchronous job, and return its handle. This is for the queues of a quantum processing unit (QPU), which can take minutes or hours. Print the job id of the handle or call its `save()`, and collect the matrix later with `job.result()` or, from a new session, with `retrieve_kernel_matrix()`. Parameters: | Name | Type | Description | Default | | --------- | --------- | ---------------------------------------- | ----------------------------------------------------------------------------------- | | `X` | `ndarray` | The samples, of shape (n_X, n_features). | *required* | | `Y` | \`ndarray | None\` | The second set of samples, of shape (n_Y, n_features), or None for the Gram matrix. | | `weights` | \`ndarray | None\` | The feature-map weights. By default, they are the kernel's own. | Returns: | Type | Description | | ----------- | ------------ | | `KernelJob` | A KernelJob. | Raises: | Type | Description | | --------------------- | ----------------------------------------------------------------------------------------------------------------------- | | `NotImplementedError` | On backends without asynchronous submission, which includes every local simulator. Use compute_kernel_matrix() on them. | #### `to_json_dict()` Return a JSON description of the kernel, for `.eigenkit` bundles. A custom feature map is stored as its `CircuitSpec`. Secret fields of the provider, such as access tokens, are never written. Returns: | Type | Description | | ------ | --------------------------------- | | `dict` | The description, as a dictionary. | #### `with_weights(weights)` Return a copy of this kernel with different trainable weights. Parameters: | Name | Type | Description | Default | | --------- | --------- | --------------------------------------- | ---------- | | `weights` | `ndarray` | The new weights, of shape (n_weights,). | *required* | Returns: | Type | Description | | ----------------- | ---------------------------------------- | | `'QuantumKernel'` | A new kernel, which is not compiled yet. | ### `eigenkit.qml._kernels.KernelJob` A kernel matrix submitted to a backend, to be collected later. QuantumKernel.submit_kernel_matrix() returns it. The handle records which pair each submitted circuit measured, so `result()` returns the assembled matrix instead of a flat list of probabilities. `save()` writes this information together with the job id, so that the matrix can be collected from a new session with QuantumKernel.retrieve_kernel_matrix(). #### `n_circuits` The number of circuits that the job runs, one for each measured entry. #### `job_id()` Return the identifier of the job on the backend. #### `result()` Wait until the job finishes, and return the kernel matrix. Returns: | Type | Description | | --------- | ---------------------------------------------------------------------------------- | | `ndarray` | The kernel matrix, of shape (n_x, n_x) for a Gram matrix and (n_x, n_y) otherwise. | #### `save(path)` Write the job id and the layout of the pairs to a JSON file. The setting of `psd_projection` is written too. Parameters: | Name | Type | Description | Default | | ------ | ----- | ----------- | --------------------------------------------------------------------------------------------- | | `path` | \`str | Path\` | Where to write the file. Its suffix is replaced by .json, or .json is added when it has none. | Returns: | Type | Description | | ------ | ----------------------------- | | `Path` | The path of the written file. | #### `status()` Return the current status of the job, as a string from the backend. ### `eigenkit.qml._alignment.align_kernel(kernel, X, y, *, loss='svc', optimizer='spsa', maxiter=50, C=1.0, initial_weights=None, learning_rate=None, perturbation=0.2, random_state=0, callback=None)` Train the weights of a kernel so that it fits the labels. Parameters: | Name | Type | Description | Default | | ----------------- | ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `kernel` | `QuantumKernel` | A QuantumKernel whose feature map has trainable weights. | *required* | | `X` | `ndarray` | The training samples, of shape (n, n_features). | *required* | | `y` | `ndarray` | The training labels, of shape (n,). | *required* | | `loss` | \`str | Callable\[[ndarray, ndarray], float\]\` | The loss to minimise. It is "svc" (the dual objective of the support vector machine, which is the loss of quantum kernel alignment), "kta" (minus the kernel-target alignment), or a callable loss(K, y) -> float. | | `optimizer` | \`str | Callable\` | The optimiser. It can be "spsa", the default, which selects Simultaneous Perturbation Stochastic Approximation (SPSA). It can also be the name of a scipy.optimize.minimize method such as "COBYLA", or a callable optimizer(fun, x0) -> x. | | `maxiter` | `int` | The number of SPSA iterations, or the maxiter option of SciPy. | `50` | | `C` | `float` | The regularisation parameter of the support vector machine, for the "svc" loss. | `1.0` | | `initial_weights` | \`ndarray | None\` | The starting point. By default, it is the kernel's own weights, or zeros if the kernel has none. | | `learning_rate` | \`float | None\` | The initial step size of SPSA. None calibrates it so that the first step moves the weights by about 2π / 10. The "svc" loss grows with the number of samples, so a fixed step that suits one dataset size can be far too large for another. | | `perturbation` | `float` | The initial perturbation size of SPSA. The default, 0.2, is the value that qiskit-algorithms uses for its calibration. Much smaller values give estimates of the gradient that are too local to cross a flat region of the loss. | `0.2` | | `random_state` | \`int | None\` | The seed for the perturbation directions of SPSA. | | `callback` | \`Callable\[[int, ndarray, float], None\] | None\` | A function called as callback(iteration, weights, loss) after every SPSA iteration or SciPy evaluation. | Returns: | Type | Description | | ----------------------- | ------------------------ | | `KernelAlignmentResult` | A KernelAlignmentResult. | Raises: | Type | Description | | ------------ | ---------------------------------------------------------------------- | | `ValueError` | If the kernel has no trainable weights, or if loss is an unknown name. | Example ```python import numpy as np from eigenkit import QSVC, QuantumKernel, align_kernel def my_map(qc, x, w): for q in range(len(x)): qc.ry(w[0], q) qc.rz(x[q], q) rng = np.random.default_rng(0) X_train = rng.uniform(0, np.pi, (8, 2)) y_train = np.array([0, 1] * 4) kernel = QuantumKernel(n_features=2, feature_map=my_map, n_weights=1) result = align_kernel(kernel, X_train, y_train, maxiter=20) model = QSVC(kernel=result.kernel).fit(X_train, y_train) ``` ### `eigenkit.qml._alignment.KernelAlignmentResult` Outcome of align_kernel(). Attributes: | Name | Type | Description | | --------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `weights` | `ndarray` | The trained weights. | | `kernel` | `QuantumKernel` | A copy of the input kernel with weights, ready for QSVC(kernel=...). | | `loss` | `float` | The loss at weights. | | `initial_loss` | `float` | The loss at the starting weights. | | `loss_history` | `tuple[float, ...]` | The loss after every iteration of Simultaneous Perturbation Stochastic Approximation (SPSA), or at every evaluation for a SciPy optimiser. With SPSA, weights is the point with the lowest loss seen, including the starting weights. | | `n_evaluations` | `int` | The number of kernel matrices computed. | | `success` | `bool` | Whether the loss is lower than at the starting point. | ### `eigenkit.qml._alignment.kernel_target_alignment(K, y)` Return the kernel-target alignment `_F / (||K||_F ||Y||_F)`. `Y_ij = +1` when `y_i == y_j` and `-1` otherwise. For binary labels in `{-1, +1}`, this is `y y^T`. Parameters: | Name | Type | Description | Default | | ---- | --------- | --------------------------------- | ---------- | | `K` | `ndarray` | The Gram matrix, of shape (n, n). | *required* | | `y` | `ndarray` | The labels, of shape (n,). | *required* | Returns: | Type | Description | | ------- | --------------------------------------------------------------------------------------- | | `float` | A value in [-1, 1]. A higher value means that the kernel already separates the classes. | ### `eigenkit.qml._alignment.svc_dual_loss(K, y, C=1.0)` Return the optimal dual objective of a support vector machine (SVM). The SVM is fitted on a precomputed kernel. This is the `"svc"` loss. Parameters: | Name | Type | Description | Default | | ---- | --------- | -------------------------------------------------------------- | ---------- | | `K` | `ndarray` | The training Gram matrix, of shape (n, n). | *required* | | `y` | `ndarray` | The labels, of shape (n,). They must have exactly two classes. | *required* | | `C` | `float` | The regularisation parameter of the SVM. | `1.0` | Returns: | Type | Description | | ------- | ------------- | | `float` | The value sum | Raises: | Type | Description | | ------------ | ------------------------------------------------------------------------ | | `ValueError` | If y does not have exactly two classes. Use loss="kta" for more classes. | ### `eigenkit.qml._kernels.project_psd(K)` Return the nearest positive semidefinite matrix to a symmetric `K`. Shot noise can make a measured Gram matrix slightly indefinite, and a support vector machine solver handles such a matrix poorly. Setting the negative eigenvalues to zero gives the nearest positive semidefinite matrix in the Frobenius norm. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------- | ---------- | | `K` | `ndarray` | A square, symmetric kernel matrix. | *required* | Returns: | Type | Description | | --------- | ---------------------------------------- | | `ndarray` | The projected matrix, of the same shape. | # Layers Four `nn.Module` layers cover the usual patterns, and they can all be used inside `nn.Sequential`. Most of these layers expose `in_features` and `out_features`. `MinMaxNorm` uses `feature_dim`. The three layers that build a circuit, `Basic`, `Reupload` and `Ensemble`, take the same `provider=` argument as `Circuit`, and they pass it to the circuit unchanged. `MinMaxNorm` is classical arithmetic and has no circuit to run, so it does not take the argument, and passing it raises a `TypeError`. The same three layers return the provider on `.provider`, as the resolved backend, whichever form was passed in. `.provider.name` is its registered name: ```python from eigenkit import Basic, PennyLaneConfig layer = Basic(n_qubits=4, depth=2, provider=PennyLaneConfig()) layer.provider.name # "pennylane" ``` | Layer | Pattern | | ------------ | --------------------------------------------------------------------------- | | `Basic` | One encoding, one variational block, and a measurement of each qubit. | | `Reupload` | Standard data reuploading, optionally with a trainable scale. | | `Ensemble` | `N` independent circuits, one for each chunk of the input. | | `MinMaxNorm` | Min/max normalisation of each feature, in the style of batch normalisation. | ## `Basic` `Basic` is the thinnest wrapper around `Circuit`: one encoding, one variational block, and a measurement of each qubit. Use it for textbook variational classifiers and regressors, where `n_features == n_qubits`. ```python import torch from eigenkit import Basic # Defaults: RX encoding, RY variational, Z basis, which are three different axes. layer = Basic(n_qubits=4, depth=2) out = layer(torch.randn(8, 4)) # shape (8, 4) ``` The circuit that this call builds. `RX(x_i)` encodes one feature per qubit. Then the `basic` variational block, which is one `RY(θ)` per qubit followed by the ladder of controlled-NOT (CNOT) gates, repeats `depth` times, and every qubit is measured in `Z`. These three axes are the defaults. With `variational_type="strong"`, each `RY(θ)` box becomes the three rotations `RZ`, `RY` and `RZ`. Some combinations of axes make the circuit inert Some choices of `enc_rotation`, `var_rotation` and `basis` build a circuit whose weights, or whose input, provably cannot reach the output. All three layers check for this and issue a `UserWarning` that names the reason. The layer is still built, because you may want to build a degenerate circuit on purpose, as a baseline. There are three ways to get one: - **The weights commute with the observable.** With `variational_type="basic"` and `var_rotation` on the measurement axis (`RZ` with `basis="Z"`, or `RX` with `basis="X"`), the rotations and the CNOT ladder both preserve that group of Pauli operators. The weights then cancel out of the expectation value, and every gradient is exactly `0`. Rotate away from the measurement axis, or use `variational_type="strong"`, whose three rotations `RZ`, `RY` and `RZ` always include one away from the axis. - **`RZ` encoding.** The circuit starts in `|0...0⟩`, which every `RZ` leaves unchanged up to a phase, so the input never enters. `Reupload` escapes this once it encodes again, because the second encoding acts on a state that has already moved. `Basic` and `Ensemble` encode only once, so they do not escape it. - **A real circuit measured in `Y`.** `RY` and CNOT are real matrices, so an `RY` encoding with `RY` rotations keeps the statevector real, and `⟨Y⟩` is always `0` on a real state. Sharing an axis is not a problem in itself. `RY` rotations with `basis="Y"` train well on two qubits or more, because the CNOT ladder does not preserve strings of `Y` operators. ## `Reupload` `Reupload` is standard data reuploading in a single circuit. The input is encoded again `depth` times, with new variational weights between the encodings. Set `trainable_scale=True` to add a trainable scale for each pair of layer and feature (Schuld et al. 2021). Here, `in_features == n_qubits`. ```python from eigenkit import Reupload # Encoding on RX, variational on RY: same rule as Basic. layer = Reupload(n_qubits=4, depth=3, enc_rotation="RX", trainable_scale=True) out = layer(torch.randn(8, 4)) # shape (8, 4) ``` What distinguishes this layer from `Basic` is where the outer bracket starts. It covers the encoding as well as the variational block, so the input is encoded again at each repetition, with new weights. The circuit is drawn with `enc_rotation="RX"`, as in the call above, and without the trainable scale. With `trainable_scale=True`, each `RX(x_i)` becomes `RX(λ·x_i)`, with one trainable `λ` for each pair of layer and feature. Keep `enc_rotation` and `var_rotation` on different axes here too. When they share one, each `R(x)` and the `R(θ)` that follows it combine into a single `R(x + θ)`, and the variational weights of that layer become a plain offset of the data angle. The defaults of `Reupload` put both rotations on `RY`, so pass `enc_rotation` explicitly, as in the call above. ## `Ensemble` `Ensemble` runs `N` independent circuits, one for each chunk of the input, each with its own weights. The outputs are concatenated along the feature dimension. ```python from eigenkit import Ensemble layer = Ensemble(in_features=20, n_qubits=4, depth=5) out = layer(torch.randn(128, 20)) # shape (128, 20): 5 circuits * 4 qubits ``` The input is split into chunks of `n_qubits` features, and each chunk gets its own circuit, with its own weights and its own measurement. The outputs of the circuits are concatenated back into one feature vector. Nothing is shared between the branches: no statevector and no parameters. Four branches are drawn, while the call above has five. Note also that on this layer, the variational block defaults to `variational_type="strong"`, not `"basic"`. ## `MinMaxNorm` `MinMaxNorm` normalises each feature with its minimum and maximum, and keeps running statistics. It is useful before encoding rotations, which expect bounded angles. ```python import torch from eigenkit import MinMaxNorm norm = MinMaxNorm(feature_dim=16, min_val=0.0, max_val=torch.pi) ``` In training mode, it uses the minimum and maximum of each batch, and it updates an exponential moving average of them (`momentum=0.1` by default). In evaluation mode, it uses the running statistics. The output is clamped to `[min_val, max_val]`, to guard against outliers that were not seen in training. ## API reference ### `eigenkit.torch.layers` Quantum layers for the standard patterns. - `Basic`: one `Encoding`, one `Variational` block and a measurement. - `Reupload`: standard data reuploading, optionally with a trainable scale. - `Ensemble`: `N` independent circuits, one for each chunk of the input. - `MinMaxNorm`: min-max normalisation of each feature, in the style of `BatchNorm`. #### `Basic` Bases: `Module` One `Encoding`, one `Variational` block and a measurement on each qubit. Parameters: | Name | Type | Description | Default | | ------------------ | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `n_qubits` | `int` | The number of qubits, which is also the number of input features. | *required* | | `depth` | `int` | The number of variational layers. | *required* | | `enc_rotation` | `Literal['RX', 'RY', 'RZ']` | The encoding rotation: "RX", "RY" or "RZ". Default: "RX". | `'RX'` | | `var_rotation` | `Literal['RX', 'RY', 'RZ']` | The variational rotation. It is used only when variational_type="basic". The default, "RY", is off both the "RX" encoding axis and the "Z" measurement axis, and this is what lets the weights change the output. Default: "RY". | `'RY'` | | `variational_type` | `Literal['basic', 'strong']` | "basic", for one rotation and a controlled-NOT (CNOT) ladder in each layer, or "strong", for RZ, RY and RZ rotations and a CNOT ladder in each layer. Default: "basic". | `'basic'` | | `basis` | `Literal['X', 'Y', 'Z']` | The Pauli measurement basis: "X", "Y" or "Z". Default: "Z". | `'Z'` | | `provider` | `ProviderSpec` | The backend that runs the circuit. Default: "numpy". | `'numpy'` | Example ```python import torch from eigenkit import Basic layer = Basic(n_qubits=4, depth=2) out = layer(torch.randn(8, 4)) # shape (8, 4) ``` ##### `provider` The backend that the circuit of this layer was compiled for. The constructor takes `provider=` in the same forms as `Circuit` (a registered name, a configuration or a backend object) and turns it into one `Provider`. This property returns it. The registered name is `provider.name`. Returns: | Name | Type | Description | | ---------- | ---------- | ------------------------------------- | | `provider` | `Provider` | The backend that the circuit runs on. | ##### `forward(x)` Run the layer. Parameters: | Name | Type | Description | Default | | ---- | -------- | ----------------------------------------------- | ---------- | | `x` | `Tensor` | The input batch, of shape (batch, in_features). | *required* | Returns: | Name | Type | Description | | ----- | -------- | --------------------------------------- | | `out` | `Tensor` | The output of the layer for that batch. | #### `Ensemble` Bases: `Module` An ensemble of independent quantum circuits, one for each chunk of the input. Parameters: | Name | Type | Description | Default | | ------------------ | ---------------------------- | --------------------------------------------------------------------------------------- | ---------- | | `in_features` | `int` | The total number of input features. It must be a multiple of n_qubits. | *required* | | `n_qubits` | `int` | The number of qubits of each circuit. | *required* | | `depth` | `int` | The number of variational layers of each circuit. | *required* | | `enc_rotation` | `Literal['RX', 'RY', 'RZ']` | The encoding rotation: "RX", "RY" or "RZ". Default: "RX". | `'RX'` | | `var_rotation` | `Literal['RX', 'RY', 'RZ']` | The variational rotation. It is used only when variational_type="basic". Default: "RY". | `'RY'` | | `variational_type` | `Literal['basic', 'strong']` | "basic" or "strong". Default: "strong". | `'strong'` | | `basis` | `Literal['X', 'Y', 'Z']` | The Pauli measurement basis: "X", "Y" or "Z". Default: "Z". | `'Z'` | | `provider` | `ProviderSpec` | The backend that runs the circuits. Default: "numpy". | `'numpy'` | Example ```python import torch from eigenkit import Ensemble layer = Ensemble(in_features=20, n_qubits=4, depth=5) out = layer(torch.randn(128, 20)) # shape (128, 20) ``` ##### `provider` The backend that the circuits of this layer were compiled for. The constructor takes `provider=` in the same forms as `Circuit` (a registered name, a configuration or a backend object) and turns it into one `Provider`. This property returns it. The registered name is `provider.name`. Returns: | Name | Type | Description | | ---------- | ---------- | ------------------------------------- | | `provider` | `Provider` | The backend that the circuits run on. | ##### `forward(x)` Run the layer. Parameters: | Name | Type | Description | Default | | ---- | -------- | ----------------------------------------------- | ---------- | | `x` | `Tensor` | The input batch, of shape (batch, in_features). | *required* | Returns: | Name | Type | Description | | ----- | -------- | --------------------------------------- | | `out` | `Tensor` | The output of the layer for that batch. | #### `MinMaxNorm` Bases: `Module` Min-max normalisation of each feature, with running statistics. It rescales each feature to `[min_val, max_val]`. In training mode, it uses the minimum and maximum of each batch, and it updates an exponential moving average of these statistics. In evaluation mode, it uses the running statistics. A training batch of a single row has no range, so it uses the running statistics too, and if no batch with a range has been seen yet, it returns the row unchanged with a warning. Training and evaluation modes work as in the `BatchNorm` and `LayerNorm` layers of PyTorch, but the output is rescaled to a fixed interval instead of standardised. It is useful to prepare the inputs of quantum encoding rotations, which expect angles in a bounded range such as `[0, π]`. Parameters: | Name | Type | Description | Default | | ------------- | ------- | ----------------------------------------------------------------------------------------------------------- | ---------- | | `feature_dim` | `int` | The number of features in the last dimension of the input. The inputs have shape (batch_size, feature_dim). | *required* | | `min_val` | `float` | The lower bound of the target interval. Default: 0.0. | `0.0` | | `max_val` | `float` | The upper bound of the target interval. Default: π. | `pi` | | `momentum` | `float` | The momentum factor of the exponential moving average of the running statistics. Default: 0.1. | `0.1` | Example ```python import torch from eigenkit import Ensemble, MinMaxNorm norm = MinMaxNorm(feature_dim=16, min_val=0.0, max_val=torch.pi) layer = Ensemble(in_features=16, n_qubits=4, depth=5) out = layer(norm(torch.randn(32, 16))) ``` ##### `forward(x)` Run the layer. Parameters: | Name | Type | Description | Default | | ---- | -------- | ----------------------------------------------- | ---------- | | `x` | `Tensor` | The input batch, of shape (batch, feature_dim). | *required* | Returns: | Name | Type | Description | | ----- | -------- | --------------------------------------- | | `out` | `Tensor` | The output of the layer for that batch. | #### `Reupload` Bases: `Module` The data-reuploading layer (Perez-Salinas 2020; Schuld 2021). It supports two patterns: - Standard reuploading (the default). The same input is encoded `depth` times, with new variational weights between the encodings. - Trainable feature scaling (`trainable_scale=True`). Each encoding rotation becomes `R(lambda * x)`, with a trainable `lambda` for each pair of layer and feature. Parameters: | Name | Type | Description | Default | | ------------------ | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------- | | `n_qubits` | `int` | The number of qubits, which is also the number of input features. | *required* | | `depth` | `int` | The number of times the input is encoded. | *required* | | `enc_rotation` | `Literal['RX', 'RY', 'RZ']` | The encoding rotation: "RX", "RY" or "RZ". Default: "RY". | `'RY'` | | `var_rotation` | `Literal['RX', 'RY', 'RZ']` | The variational rotation. It is used only when variational_type="basic". Default: "RY". | `'RY'` | | `variational_type` | `Literal['basic', 'strong']` | "basic" or "strong". Default: "basic". | `'basic'` | | `basis` | `Literal['X', 'Y', 'Z']` | The Pauli measurement basis: "X", "Y" or "Z". Default: "Z". | `'Z'` | | `trainable_scale` | `bool` | If True, each encoding rotation gets a trainable scale for each pair of layer and feature. The CUDA Quantum backend does not support it, because its QuakeValue arithmetic is limited. Default: False. | `False` | | `provider` | `ProviderSpec` | The backend that runs the circuit. Default: "numpy". | `'numpy'` | Example ```python import torch from eigenkit import Reupload layer = Reupload(n_qubits=4, depth=3, trainable_scale=True) out = layer(torch.randn(8, 4)) # shape (8, 4) ``` ##### `provider` The backend that the circuit of this layer was compiled for. The constructor takes `provider=` in the same forms as `Circuit` (a registered name, a configuration or a backend object) and turns it into one `Provider`. This property returns it. The registered name is `provider.name`. Returns: | Name | Type | Description | | ---------- | ---------- | ------------------------------------- | | `provider` | `Provider` | The backend that the circuit runs on. | ##### `forward(x)` Run the layer. Parameters: | Name | Type | Description | Default | | ---- | -------- | ----------------------------------------------- | ---------- | | `x` | `Tensor` | The input batch, of shape (batch, in_features). | *required* | Returns: | Name | Type | Description | | ----- | -------- | --------------------------------------- | | `out` | `Tensor` | The output of the layer for that batch. | # Model persistence (`.eigenkit`) The supported quantum estimators can save a **fitted** model to disk, and load it again later without training it again. A saved model is a **`.eigenkit` bundle** of JSON, NumPy and PyTorch files. Models that hold scikit-learn objects (`QSVC`, `QSVR`, the reservoir estimators and the time-series estimators) add them as joblib pickles, and loading a pickle runs code. As with `torch.load` or `joblib.load`, **load only bundles that you trust.** ```python from eigenkit import HQNC, load_model from eigenkit.datasets import load_dataset data = load_dataset("iris") X_train, y_train, X_test = data["X_train"], data["y_train"], data["X_test"] model = HQNC(in_features=4, n_classes=3, depth=2, epochs=30) model.fit(X_train, y_train) # Save (adds .eigenkit if you omit the suffix) model.save("artifacts/my_hqnc") loaded = HQNC.load("artifacts/my_hqnc") # Or load without naming the class loaded = load_model("artifacts/my_hqnc") assert (loaded.predict(X_test) == model.predict(X_test)).all() ``` ______________________________________________________________________ ## The layout of a `.eigenkit` bundle A typical bundle contains these files: | Path | Contents | | ------------------- | -------------------------------------------------------------------------------------- | | `metadata.json` | The keys `class_name`, `library_version`, `format_version`, `saved_at` and `provider`. | | `params.json` | The hyperparameters of the constructor, in a form that JSON can store. | | `circuit_spec.json` | The list of blocks and the observables, for `CustomQuantum*` only. | | `state/*` | The fitted state: weights, labels, the training data of kernels and so on. | The state files use typed extensions: | Extension | Meaning | | --------- | ------------------------------------------------------------------------------------ | | `.pt` | A `state_dict` saved with `torch.save`, which holds the weights of the model. | | `.npy` | A NumPy array, for example `X_train`, or the joblib bytes of `QSVC` and `QSVR`. | | `.json` | Scalars or lists that JSON can store, for example `train_losses` or `label_classes`. | PyTorch is imported only when a `.pt` entry is read or written. The `QSVC` and `QSVR` bundles, which do not use PyTorch, have no `.pt` entry, so they can be saved and loaded with the `qml` extra alone, without PyTorch. Example listing for `HQNC`: ```text metadata.json params.json state/label_classes.json state/n_classes.json state/train_losses.json state/validation_losses.json state/weights.pt ``` Example for `QSVC`: ```text metadata.json params.json state/X_train.npy state/svc_joblib.npy # sklearn SVC bytes (joblib), not a .joblib file in the zip state/calibrated_joblib.npy # only with probability=True: the fitted calibration ``` `QSVC` and `QSVR` bundles include joblib bytes For `QSVC` and `QSVR`, the fitted `sklearn.svm` object is serialised with joblib in memory, and then stored as a `uint8` array in `state/*_joblib.npy`. Joblib uses pickle internally, so load these bundles only from sources that you trust. `metadata.json` includes `"format_version": 1`. `load()` refuses a bundle whose `format_version` is missing or different, with an error that names the version. ______________________________________________________________________ ## `save()` and `load()` ### Saving ```python model.save("path/to/model") # writes path/to/model.eigenkit model.save("path/to/model.eigenkit") # same ``` Call `save()` only **after** `fit()`. The method collects: - the hyperparameters, from `get_params()`; - the `circuit_spec`, for custom lists of blocks; - the state specific to the algorithm, from `_collect_state()`: the weights, the encoders, `train_losses_`, the training matrices of kernels and so on. ### Loading ```python # Explicit class loaded = HQNC.load("path/to/model.eigenkit") # Override backend at load time (does not change saved weights) loaded = HQNC.load("path/to/model.eigenkit", provider="qiskit") ``` ```python # Class inferred from metadata.json from eigenkit import load_model loaded = load_model("path/to/model.eigenkit") ``` After `load()`, the attributes that `fit()` sets are restored when the bundle holds them. This includes `train_losses_` on `HQNC`, `HQNR`, `RecurrentHQNC`, `RecurrentHQNR` and the `Custom*` estimators. ### Supported estimators | Class | Notes | | ----------------------------------------------------------- | ------------------------------------------------------------------------------------- | | `HQNC`, `HQNR` | The bundle holds the weights and the training metadata. | | `RecurrentHQNC`, `RecurrentHQNR` | The same, and the loaded model expects 3-D input. | | `QSVC`, `QSVR` | The bundle holds the quantum kernel, the state of the scikit-learn SVM and `X_train`. | | `CustomQuantumClassifier`, `CustomQuantumRegressor` | The blocks in `circuit_spec` must be serialisable. | | `QuantumTimeSeriesClassifier`, `QuantumTimeSeriesRegressor` | The bundle holds the wrapper and the state of the inner estimator. | | `GenericClassifier`, `GenericRegressor` | You must pass the architecture yourself when you load. See below. | A `QSVC` or `QSVR` built with `kernel=QuantumKernel(...)` stores that kernel in `params.json`. It stores the name of a built-in map, or a custom map as its `CircuitSpec`, together with its weights, `scale` and `psd_projection`, so a trained kernel is loaded trained. `load(provider=...)` also builds the kernel again on that provider. #### GenericClassifier and GenericRegressor The bundle stores the weights, the training metadata and, for `GenericClassifier`, the label encoder. It does **not** store the architecture of the `nn.Module`. You must pass the same architecture when you load: ```python from eigenkit import GenericClassifier, load_model # Direct class method clf_loaded = GenericClassifier.load("clf_hybrid.eigenkit", model=clf_net_fresh) # Or via load_model, pass model= explicitly clf_loaded = load_model("clf_hybrid.eigenkit", model=clf_net_fresh) ``` Calling `load_model()` or `.load()` without `model=` raises a `SerializationError`. **Not estimators:** `optimization.QAOA` and `chemistry.VQE` are solvers that return result objects. They are not fitted scikit-learn estimators, so the `.eigenkit` bundles do not apply to them. **Custom circuits:** a `CustomBlock` built with `CircuitBuilder` can be saved when it uses the supported gates and parameter expressions of the intermediate representation. Other block objects still need `to_dict()` and `from_dict()`, or must be built-in block types. ______________________________________________________________________ ## Base classes for custom algorithms > **Coming in a future release:** full `.eigenkit` support for saving and loading custom model classes (subclasses of `BaseQuantumClassifier`, `BaseQuantumRegressor` or `BaseQuantumEstimator`) is not available yet. This covers both `save()` and `load_model()` or `.load()` for estimators that you define. ______________________________________________________________________ ## Credentials are never saved Configuration fields marked as secret are left out of `.eigenkit` bundles, and of pickles and joblib checkpoints (including the `.joblib` files that `benchmark.compare()` writes). Today, these fields are `QiskitRuntimeConfig.token` and `IQMConfig.token`. They are also hidden from `repr()`, so the `provider` entry in `metadata.json` does not show them. After a model is loaded, a secret field takes its default value (`None`), and the backend falls back to its own way of finding credentials. Set the credential in the environment of the process that runs the model: `QISKIT_IBM_TOKEN` for IBM Quantum, and `IQM_TOKEN` for IQM. The worker processes of joblib and loky receive their models as pickles, so they need the variable too. Copies inside one process (`copy.copy`, `copy.deepcopy`, `sklearn.base.clone`, and so `GridSearchCV`) keep the credential. ```python from eigenkit import QSVC, QiskitRuntimeConfig model = QSVC( feature_map="Angle", reps=1, provider=QiskitRuntimeConfig( backend_name="ibm_fez", primitive="sampler", token="..." ), ) model.fit(X_train, y_train) model.save("artifacts/qsvc") # the token is not written # Later, possibly on another machine: export QISKIT_IBM_TOKEN first. loaded = QSVC.load("artifacts/qsvc") assert loaded.provider.token is None # resolved from the environment instead ``` With `QSVC(kernel=QuantumKernel(..., provider=config))`, the configuration belongs to the kernel, so the same check reads `loaded.kernel.provider.token`. The fields that decide where a provider connects (`url`, `service_kwargs`, `provider_kwargs` and `backend_instance`) are not saved either. If a bundle holds them anyway, they are ignored, with a warning. Otherwise, a shared bundle could send the token from your environment to a host of its choice. To load a model onto a custom endpoint, pass the full configuration: `QSVC.load(path, provider=QiskitRuntimeConfig(..., service_kwargs=...))`. ______________________________________________________________________ ## Benchmark integration `benchmark.compare()` can save the fitted models when `save_dir` is set: ```python from eigenkit import HQNC, QSVC from eigenkit.benchmark import compare from sklearn.svm import SVC compare( models=[HQNC(in_features=4, depth=2), SVC()], dataset="iris", n_seeds=2, save_dir="benchmark_runs/", ) ``` The output files are named as follows: | Model type | Filename pattern | | ----------------------------------------------- | ------------------------------ | | An eigenkit estimator (`BaseQuantumEstimator`) | `{ModelName}_seed{N}.eigenkit` | | A built-in scikit-learn estimator (`sklearn.*`) | `{ModelName}_seed{N}.joblib` | `ModelName` is the benchmark key. When two models have the same class name, they get an index (`SVC_0`, `SVC_1`). Pass `models` as a dictionary to choose the benchmark key: ```python compare( models={ "Quantum SVC": QSVC(feature_map="Angle", reps=1, probability=True), "Regularized SVC": SVC(C=0.3), "Flexible SVC": SVC(C=3.0), }, dataset="iris", metrics=["accuracy"], n_seeds=1, save_dir="benchmark_runs/", ) # benchmark_runs/Quantum SVC_seed0.eigenkit # benchmark_runs/Regularized SVC_seed0.joblib # benchmark_runs/Flexible SVC_seed0.joblib ``` Custom benchmark keys follow the naming rules in [Benchmark](https://docs.kit.eigenorbit.com/user-guide/benchmark/index.md). To load a quantum model saved by a benchmark: ```python from eigenkit import load_model model = load_model("benchmark_runs/HQNC_seed0.eigenkit") ``` Load the scikit-learn baselines with `joblib.load("benchmark_runs/SVC_seed0.joblib")`. `eigenkit.benchmark.load_checkpoint(...)` can load either type of file from a benchmark run. `.joblib` checkpoints use pickle internally, so load them only from sources that you trust. See [Benchmark](https://docs.kit.eigenorbit.com/user-guide/benchmark/index.md) for the metrics, the seeds and `BenchmarkResult`. ______________________________________________________________________ ## API reference ### `eigenkit.qml.load_model(path, *, provider=None, model=None)` Load any saved model. The class of the model is read from `metadata.json`. Load only bundles that you trust. A bundle can contain joblib pickles, and unpickling runs code, as with `torch.load` or `joblib.load`. Parameters: | Name | Type | Description | Default | | ---------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------- | | `path` | \`str | Path\` | The path to the .eigenkit bundle. | | `provider` | \`ProviderSpec | None\` | A provider that replaces the saved one. It is passed to the load() method of the estimator. | | `model` | `Any` | The nn.Module architecture, for checkpoints of GenericRegressor and GenericClassifier. It is ignored for the other estimators. | `None` | Returns: | Type | Description | | ---------------------- | ------------------------------------------------------------------- | | `BaseQuantumEstimator` | The loaded estimator, an instance of the class named in the bundle. | ### `eigenkit.qml.BaseQuantumEstimator` Bases: `BaseEstimator`, `ABC` Base class, compatible with scikit-learn, for all quantum algorithms. Parameters: | Name | Type | Description | Default | | -------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `provider` | `ProviderSpec` | The backend. It can be a registered name ("pennylane", "qiskit", "braket", or any name passed to register()), a configuration instance (for example PennyLaneConfig(device_name=...)), or a backend instance. | `'numpy'` | | `random_state` | \`int | None\` | The seed for reproducibility, applied inside fit() and load(). What it seeds depends on the estimator. The PyTorch estimators isolate the random number generator of PyTorch, so seeding one of them does not reseed the random stream of the caller. The kernel estimators are deterministic, and they take it only so that all the estimators have the same interface. | #### `fit(X, y)` Fit the estimator on `X` and `y`. Parameters: | Name | Type | Description | Default | | ---- | --------- | -------------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | | `y` | `ndarray` | One target for each sample, of shape (n_samples,). | *required* | Returns: | Type | Description | | ------------------------ | --------------------------------------------- | | `'BaseQuantumEstimator'` | This estimator, so that calls can be chained. | #### `load(path, *, provider=None)` Load a fitted model from a `.eigenkit` bundle. Load only bundles that you trust. A bundle can contain joblib pickles, and unpickling runs code, as with `torch.load` or `joblib.load`. Parameters: | Name | Type | Description | Default | | ---------- | -------------- | ----------- | --------------------------------------- | | `path` | \`str | Path\` | The path of the bundle. | | `provider` | \`ProviderSpec | None\` | A provider that replaces the saved one. | Returns: | Type | Description | | ------------------------ | --------------------- | | `'BaseQuantumEstimator'` | The loaded estimator. | Raises: | Type | Description | | ----------- | ---------------------------------------------------------------------------------------------------------------------------- | | `TypeError` | If the method is called on a subclass and the bundle holds a different class. BaseQuantumEstimator.load() accepts any class. | #### `predict(X)` Predict a value for every sample in `X`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | Returns: | Type | Description | | --------- | ------------------------------- | | `ndarray` | One prediction for each sample. | #### `save(path)` Save a fitted model to a `.eigenkit` bundle. The bundles of models that hold scikit-learn objects (`QSVC`, `QSVR`, the reservoir estimators and the time-series estimators) contain joblib pickles, so loading one can run code. Share them only with people who trust you. Parameters: | Name | Type | Description | Default | | ------ | ----- | ----------- | ------------------------------------------------------------------------- | | `path` | \`str | Path\` | The path of the bundle. The suffix .eigenkit is added when it is missing. | ### `eigenkit.qml._serialization.SerializationError` Bases: `Exception` Raised when a model cannot be written to a bundle, or a bundle cannot be read. # Observables What to measure is set with the `observables=` argument of `Circuit`. It is not a special block. There are three ways to build an observable, each suited to a different level of detail. ## Default: `Z` on every qubit If you do not pass `observables=`, the circuit measures `Z` on every qubit: ```python from eigenkit import Circuit, Encoding, Variational circuit = Circuit( blocks=[Encoding(in_features=4), Variational(depth=2)], # observables= defaults to all-Z; output is shape (..., n_qubits). ) ``` ## Typed constructors `Observable.x(qubit)`, `Observable.y(qubit)` and `Observable.z(qubit)` build observables made of a single Pauli operator: ```python from eigenkit import Observable, all_z # Measure Z on every qubit. observables = all_z(4) # Measure X on qubit 0 only. observables = [Observable.x(0)] ``` ## Pauli algebra The `X`, `Y` and `Z` constructors return a `PauliExpr`, which you can combine with arithmetic. Use this to write Hamiltonians: ```python from eigenkit import Circuit, Encoding, Variational, X, Y, Z H = 0.7 * Z(0) + 0.3 * X(1) * Z(2) - 0.5 * Y(0) * Y(1) # H acts on qubits 0-2, so the circuit needs at least three wires. circuit = Circuit( blocks=[Encoding(in_features=3), Variational(depth=2)], observables=H, ) ``` A `PauliExpr` passed as a single observable gives a scalar output, which is one expectation value. Pass a list of `PauliExpr` to get a vector output. A measured observable holds only Pauli terms. So an expression with a constant term, such as the cost of an edge in MaxCut, `0.5 * (1 - Z(0) * Z(1))`, raises a `ValueError`, instead of losing the constant without an error. Measure the Pauli part, and add the constant back: ```python cost = 0.5 * (1 - Z(0) * Z(1)) edge = Circuit( blocks=[Encoding(in_features=2), Variational(depth=1)], observables=cost - cost.constant, ) value = edge(edge.spec.n_weights * [0.1], [0.3, 0.4]) + cost.constant ``` ## Text format For quick experiments, you can write the observable as a string: ```python from eigenkit import from_string H = from_string("0.5 Z0 X1 + 0.3 Y2") # Equivalent to 0.5 * Z(0) * X(1) + 0.3 * Y(2) ``` ## Output shape The shape of the output depends on what you pass, **and on the number of dimensions of the input**. A 1-D `inputs` is one sample and has no batch axis. A 2-D input, of shape `(batch, n_inputs)`, has one. | `observables=` | 1-D input | 2-D `(batch, n_inputs)` | | ------------------------------------- | -------------- | ----------------------- | | `None` | `(n_qubits,)` | `(batch, n_qubits)` | | A single `Observable` or `PauliExpr` | `()`, a scalar | `(batch,)` | | A list of `Observable` or `PauliExpr` | `(len(list),)` | `(batch, len(list))` | These shapes were measured on a circuit with four qubits and a batch of shape `(32, 4)`. In both cases, `weights` stays 1-D, because the weights are shared across the batch. ## Measurements that are not Pauli operators For observables based on fidelity (quantum kernels, swap tests), use `circuit.zero_state_probability(weights, inputs)` instead. It returns the probability of measuring the all-zeros state, without going through the Pauli machinery. See [Quantum Kernels](https://docs.kit.eigenorbit.com/user-guide/kernels/index.md). ## API reference ### `eigenkit.core.ir.Observable` A Hermitian operator to measure, written as a sum of Pauli terms. You can build one with the `z`, `x` and `y` methods for a single Pauli operator on one qubit, with `from_string` for a whole sum, or with the `X`, `Y` and `Z` functions of `eigenkit.core.observables`, which support arithmetic. Attributes: | Name | Type | Description | | ------- | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `terms` | `tuple[PauliTerm, ...]` | The Pauli terms of the sum. It can be empty: jordan_wigner returns an empty observable, together with an offset, when the operator reduces to a multiple of the identity. However, Circuit refuses to measure an empty observable, because there is nothing to measure. | #### `from_string(text)` Parse a sum of Pauli terms written as a string. Examples of such strings are `"0.5 Z0 X1 + 0.3 Y2"` and `"-Z0 + Z1"`. The grammar is as follows. The terms are joined by `+` or `-`. Each term starts with an optional numeric coefficient, in plain or scientific notation (`1e-3` is accepted). The coefficient is followed by an optional `*` and by one or more `[XYZ]` operators on distinct qubits. Any amount of whitespace is accepted, including none. Every part of the string must be a valid term, and every term must contain Pauli operators. A constant term (`"0.5 + Z0"`, `"0.5 I"`) raises an error instead of being dropped from the sum: an `Observable` holds only Pauli terms, and the constant must be kept next to it as an offset. Parameters: | Name | Type | Description | Default | | ------ | ----- | -------------------- | ---------- | | `text` | `str` | The string to parse. | *required* | Returns: | Type | Description | | -------------- | ---------------------- | | `'Observable'` | The parsed observable. | Raises: | Type | Description | | ------------ | ------------------------------------------------------------------------------------------------------------------------------ | | `ValueError` | If text is empty, if it contains a constant term, if a part of it is not a valid term, or if a term uses the same qubit twice. | #### `x(qubit)` Return the observable `X` on the single qubit `qubit`. Parameters: | Name | Type | Description | Default | | ------- | ----- | ----------------------- | ---------- | | `qubit` | `int` | The index of the qubit. | *required* | #### `y(qubit)` Return the observable `Y` on the single qubit `qubit`. Parameters: | Name | Type | Description | Default | | ------- | ----- | ----------------------- | ---------- | | `qubit` | `int` | The index of the qubit. | *required* | #### `z(qubit)` Return the observable `Z` on the single qubit `qubit`. Parameters: | Name | Type | Description | Default | | ------- | ----- | ----------------------- | ---------- | | `qubit` | `int` | The index of the qubit. | *required* | ### `eigenkit.core.observables` Functions and classes that make observables easy to write. There are three ways to choose what a circuit measures: ```python # Pauli algebra observable = Z(0) * X(1) + 0.5 * Y(2) # class methods, or a string observable = Observable.from_string("Z0 X1 + 0.5 Y2") observable = Observable.z(0) # no observable at all: by default, Circuit measures Z on every qubit ``` `PauliExpr` is a simple builder. It collects terms, each made of a coefficient and a tensor product of Pauli operators, and it converts them to an `Observable` when needed. A product of two operators on the same qubit is simplified when the two operators are equal (`P * P = I`). A product of different Pauli operators on the same qubit (`X * Y = iZ` and so on) needs complex arithmetic and is not supported. #### `PauliExpr` A sum of terms, each a coefficient times a product of Pauli operators. In each term, the Pauli operators act on distinct qubits, so the product is a tensor product. ##### `constant` The coefficient of the identity term, or `0.0` when there is none. ##### `__init__(terms)` Build an expression from raw terms. Parameters: | Name | Type | Description | Default | | ------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------- | | `terms` | `Iterable[_Term]` | Internal records of the terms. Prefer the X, Y and Z functions and ordinary arithmetic, such as 0.5 * Z(0) + X(1). They build the same expression without creating these records directly. | *required* | ##### `single(operator, qubit, coeff=1.0)` Return `coeff` times a single Pauli `operator` on `qubit`. Parameters: | Name | Type | Description | Default | | ---------- | ------- | ------------------------------------ | ---------- | | `operator` | `str` | The Pauli operator: "X", "Y" or "Z". | *required* | | `qubit` | `int` | The index of the qubit. | *required* | | `coeff` | `float` | The coefficient. Default: 1.0. | `1.0` | Returns: | Type | Description | | ------------- | ---------------------------------------- | | `'PauliExpr'` | The expression, which has a single term. | ##### `to_observable()` Convert the expression to an `Observable`. The terms with the same Pauli string are combined, and the terms whose coefficient is `0` (at most `1e-15` in magnitude) are dropped. An `Observable` holds only Pauli terms, so a non-zero identity term raises an error. Such a term comes from a scalar offset, or from `P * P = I`. Dropping it without an error would shift every expectation value. To measure such an expression, measure `expr - expr.constant` and add `expr.constant` to the result. The chemistry entry points take this constant as `offset`. Returns: | Type | Description | | ------------ | --------------- | | `Observable` | The observable. | Raises: | Type | Description | | ------------ | ------------------------------------------------------------------------------------------------ | | `ValueError` | If the expression has a constant term that is not 0, or if no term that can be measured is left. | #### `X(qubit)` Return Pauli `X` on `qubit`, as an expression that can be added and scaled. Parameters: | Name | Type | Description | Default | | ------- | ----- | ----------------------- | ---------- | | `qubit` | `int` | The index of the qubit. | *required* | Returns: | Type | Description | | ----------- | --------------- | | `PauliExpr` | The expression. | #### `Y(qubit)` Return Pauli `Y` on `qubit`, as an expression that can be added and scaled. Parameters: | Name | Type | Description | Default | | ------- | ----- | ----------------------- | ---------- | | `qubit` | `int` | The index of the qubit. | *required* | Returns: | Type | Description | | ----------- | --------------- | | `PauliExpr` | The expression. | #### `Z(qubit)` Return Pauli `Z` on `qubit`, as an expression that can be added and scaled. Parameters: | Name | Type | Description | Default | | ------- | ----- | ----------------------- | ---------- | | `qubit` | `int` | The index of the qubit. | *required* | Returns: | Type | Description | | ----------- | --------------- | | `PauliExpr` | The expression. | #### `all_z(n_qubits)` Return one `Z` observable for each qubit. This is useful as a default measurement. Parameters: | Name | Type | Description | Default | | ---------- | ----- | ------------------------------------ | ---------- | | `n_qubits` | `int` | The number of qubits of the circuit. | *required* | Returns: | Type | Description | | ------------------ | ------------------------------ | | `list[Observable]` | One Observable for each qubit. | #### `from_string(text)` Parse an observable from a string. This function is a shortcut for `Observable.from_string`. Parameters: | Name | Type | Description | Default | | ------ | ----- | -------------------- | ---------- | | `text` | `str` | The string to parse. | *required* | Returns: | Type | Description | | ------------ | ---------------------- | | `Observable` | The parsed observable. | # Optimization `eigenkit.optimization` solves combinatorial problems. In these problems, the answer is an assignment of bits, and the cost is a quadratic function of the bits. You write the problem as a `QUBO` (quadratic unconstrained binary optimisation) or an `Ising` model, and pass it to `QAOA` (the Quantum Approximate Optimization Algorithm). You get back the best bitstring that it found, together with the distribution that it sampled from. Like `chemistry` and `reservoir`, this is a domain package of the core. It uses only NumPy and SciPy, without PyTorch or the package of any backend. `QAOA` also uses `provider="numpy"` by default, so nothing here needs an optional dependency. The theory is on the [QAOA](https://docs.kit.eigenorbit.com/algorithms/qaoa/index.md) page of Algorithms Explained. ## MaxCut in five lines MaxCut looks for the split of the nodes of a graph that cuts the most edges. `max_cut` builds the Ising model for you, written as an energy to minimise: ```python from eigenkit.optimization import QAOA, max_cut problem = max_cut([(0, 1), (1, 2), (2, 0), (2, 3)]) result = QAOA(layers=2, random_state=0).solve(problem) print(result.best_bitstring) # (1, 1, 0, 1) print(result.best_energy) # -3.0 print(result.success) # True ``` The graph has four nodes and four edges, and the best cut has three edges. On a problem this small, you can check the answer by trying every bitstring. It is worth doing once, to see what "the answer" means here: ```python import itertools best = min(itertools.product([0, 1], repeat=4), key=problem.energy) print(best) # (0, 0, 1, 0) print(problem.energy(best)) # -3.0 ``` The bitstring is different, but the energy is the same, and each bit of one bitstring is the opposite of the same bit in the other. Moving every node to the other side of the cut describes the same partition, so the solutions of MaxCut always come in pairs. Compare energies, not bitstrings. ## Stating the problem `QUBO` minimises `xᵀQx + constant` over `x ∈ {0,1}ⁿ`. `energy` gives the cost of any candidate. This is the quickest way to check a formulation before you spend time on a solve: ```python import numpy as np from eigenkit.optimization import QUBO Q = np.array([[-2.0, 1.0], [0.0, -2.0]]) qubo = QUBO(Q, constant=0.5) print(qubo.energy([0, 0])) # 0.5 print(qubo.energy([1, 0])) # -1.5 print(qubo.energy([1, 1])) # -2.5 the minimum print(QAOA(layers=2, random_state=0).solve(qubo).best_bitstring) # (1, 1) ``` `Ising` is the same problem in spin variables. It minimises `Σᵢhᵢsᵢ + Σᵢ<ⱼJᵢⱼsᵢsⱼ + constant` over `s ∈ {−1,+1}ⁿ`. The two forms are equivalent, and `QUBO.to_ising()` converts a `QUBO` to an `Ising` model: ```python from eigenkit.optimization import Ising ising = qubo.to_ising() print(ising.h) # [0.75 0.75] print(ising.constant) # -1.25 direct = Ising(np.array([0.5, -0.5]), np.array([[0.0, 1.0], [0.0, 0.0]])) print(direct.energy([0, 0])) # 1.0 print(direct.energy([1, 0])) # -2.0 ``` Note that `Ising.energy` takes **bits, not spins**: a `0` means the spin `+1`, and a `1` means the spin `−1`. This keeps `energy` the same for both types of problem, but it will surprise you if you pass `-1` directly. `max_cut(edges, weights=None, num_nodes=None)` is a shortcut that builds the Ising model of a weighted graph. Pass `weights` for weighted edges, and `num_nodes` when the graph has isolated nodes that the list of edges never mentions. ## Reading the result `solve` returns an `OptimizationResult`, which holds more than the best bitstring: | Field | What it holds | | -------------------- | ------------------------------------------------------------------------------------ | | `best_bitstring` | The best assignment found, as a tuple of `0` and `1` values. | | `best_energy` | Its exact classical cost. This is the answer you would act on. | | `expectation` | The mean cost over the whole distribution. This is the quantity that QAOA minimises. | | `probabilities` | The whole distribution over all `2**n` bitstrings. | | `optimal_parameters` | The optimised angles. There are `2 * layers` of them. | | `n_iterations` | The number of steps of the optimiser. | | `success` | Whether the optimiser reported convergence. | ```python result = QAOA(layers=2, random_state=0).solve(problem) print(result.best_energy) # -3.0 the best sample print(result.expectation) # -2.903547 the mean over the distribution print(result.probabilities.shape) # (16,) one entry per bitstring print(result.n_iterations) # 18 print(result.optimal_parameters) # 4 angles: gamma and beta per layer ``` The two energies answer different questions. `best_energy` is the cost of the best single bitstring among those that the optimised state makes most likely. This is the answer you would act on. `expectation` is the average cost over the whole distribution, and it is the quantity that QAOA actually minimises. The difference between them comes from the circuit not putting all its amplitude on one solution. More `layers` make it smaller. `probabilities` is the whole distribution over all `2ⁿ` bitstrings, so you can also look at the solutions that come after the best one. To choose `best_bitstring`, `solve` scores the most probable bitstrings with the `energy` of the problem. By default, it scores the `64` most probable ones, or all of them when there are fewer. Pass `top_k=` to `solve` to change how many bitstrings are scored, which matters once `n` is large. ## The circuit behind it `QAOA` builds the cost Hamiltonian and the ansatz itself, but you can also get both separately, to inspect or reuse them. `to_hamiltonian()` turns a problem into the pair `(Observable, offset)`, with the same convention as the chemistry mappings. `qaoa_ansatz` builds the standard circuit: `|+⟩ⁿ`, followed by `layers` repetitions of the cost unitary and the mixer. ```python from eigenkit import Circuit from eigenkit.optimization import qaoa_ansatz hamiltonian, offset = problem.to_hamiltonian() print(len(hamiltonian.terms)) # 4 print(offset) # -2.0 print(hamiltonian.terms[0]) # PauliTerm(coeff=0.5, paulis=(('Z', 0), ('Z', 1))) spec = qaoa_ansatz(hamiltonian, 4, layers=2) print(spec.n_weights) # 4 circuit = Circuit(spec, observables=hamiltonian, provider="numpy") print(float(circuit(result.optimal_parameters, [])) + offset) # -2.903547 ``` The last number is `result.expectation`, computed again from the parts. It is a useful check when you start to use your own mixer or a different schedule of parameters. One limitation: `qaoa_ansatz` expects an `Observable`, which is the type that `to_hamiltonian` returns. It does not accept a `PauliExpr` built with the `X`, `Y` and `Z` helpers, although `Circuit(observables=...)` does. Passing one raises an `AttributeError` about `terms`. ## API reference ### `eigenkit.optimization` Combinatorial optimisation with the core of the library. Write a problem as an `Ising` or `QUBO` model, then solve it with `QAOA`, the Quantum Approximate Optimization Algorithm. The ansatz uses the gates of the core, the angles are optimised with `minimize_expectation`, and the solution is read from the distribution of bitstrings of the optimised state. The package uses only NumPy and SciPy, needs no PyTorch, and works with any backend. Example ```python from eigenkit.optimization import max_cut, QAOA problem = max_cut([(0, 1), (1, 2), (2, 0), (2, 3)]) result = QAOA(layers=2).solve(problem) print(result.best_bitstring, result.best_energy) ``` #### `Ising` An Ising problem, which minimises `Σ h_i s_i + Σ_{i 50:50 input_state=(1, 1), ) compiled = PercevalBackend().compile(spec) # compile once dist = compiled.run(np.array([]), np.array([])) # {fock_tuple: probability} print(dist) # {(0, 2): 0.5, (2, 0): 0.5}: the photons bunch, and P(1, 1) = 0 ``` The photons *bunch*: `(1, 1)` has probability `0`. This result is not classical, because it comes from the permanent of the transfer matrix of the circuit. It is a good check that the executor computes real boson statistics. The components available today are `BeamSplitter(modes, theta, phi=0)`, `PhaseShifter(mode, phi)` and `Permutation(perm)`. An angle can be a plain number (stored as a `Constant`), a `WeightRef` (trainable), an `InputRef` (data), or any combination with `Scale`, `Sum` and `Multiply`. This is exactly the `Parameter` tree of the qubit circuits, and it accepts the same values as the qubit `CircuitBuilder`. ## Observables The readouts are observables that are **diagonal in the Fock basis**, which are the ones that photonic quantum machine learning uses in practice. A `FockProbability` target must hold as many photons as the input state, because linear optics conserves the number of photons. ```python from eigenkit.photonics import FockProbability, PhotonNumber w, x = np.array([]), np.array([]) # the circuit above has no weights and no inputs compiled.expectation(w, x, PhotonNumber(mode=0)) # , expected photons in mode 0 compiled.expectation(w, x, FockProbability(target=(2, 0))) # P(detect exactly |2,0>) ``` ## Gradients: the photonic parameter-shift rule The qubit rule with shifts of `±π/2` does **not** apply to linear optics. The generator of a phase shifter is the number operator, whose spectrum is made of integers. So an expectation that is diagonal in the Fock basis is a trigonometric polynomial in the phase, of degree at most `N`, where `N` is the total number of photons. Its exact derivative needs the general equidistant shift rule with `2N` terms. The rule with `±π/2` is the case `N = 1`. The angle `theta` of a beam splitter is generated by an operator of the special unitary group SU(2) on its two modes. The eigenvalues of this operator are integers or half-integers, depending on how many photons the two modes hold. In a circuit with two modes, this count is always `N`, and the same rule with `2N` terms applies. With more modes, the expectation also has half-integer frequencies in `theta`, so the rule is applied in `theta / 2` with order `2N`, which takes `4N` evaluations per beam-splitter angle. Both rules are exact. They reuse `chain_factor` from the qubit intermediate representation to map the derivatives with respect to the angles onto the weights and the inputs. ```python from eigenkit.core.ir import WeightRef from eigenkit.photonics import PhaseShifter # One photon through a Mach-Zehnder interferometer with a trainable phase. spec = PhotonicCircuitSpec( n_modes=2, n_weights=1, n_inputs=0, components=( BeamSplitter(modes=(0, 1), theta=math.pi / 2), PhaseShifter(mode=0, phi=WeightRef(0)), BeamSplitter(modes=(0, 1), theta=math.pi / 2), ), input_state=(1, 0), ) compiled = PercevalBackend().compile(spec) w = np.array([0.3]) weight_jac, input_jac = compiled.jacobian(w, x, PhotonNumber(0), method="parameter_shift") ``` `method="finite_difference"` gives a gradient by central differences, as a check. With two or more photons, the naive rule with `±π/2` is *wrong*, and only the rule with more terms matches the finite differences (see `tests/photonics/test_gradients.py`). Pass `need_input_grad=False`, or `need_weight_grad=False`, to skip the shift evaluations for a side that you do not need. That side is then returned as `None`. `PhotonicCircuit` does this automatically for the inputs that do not require a gradient. ## Training with PyTorch `PhotonicCircuit` is an `nn.Module` that holds the weights as an `nn.Parameter` and passes the photonic gradient to PyTorch autograd. So `loss.backward()` and any `torch.optim` optimiser work as usual: ```python import torch from eigenkit.photonics import FockProbability from eigenkit.photonics import PhotonicCircuit circuit = PhotonicCircuit(spec, FockProbability(target=(0, 1)), init_weights=[1.5]) opt = torch.optim.Adam(circuit.parameters(), lr=0.1) for _ in range(120): opt.zero_grad() loss = (1.0 - circuit()) ** 2 # for example, send the photon to mode 1 loss.backward() opt.step() ``` A complete worked classifier is in [`examples/photonics_classification.py`](https://github.com/eigenorbit/eigenkit/blob/main/examples/photonics_classification.py). It encodes 2-D features as phases, trains a shallow interferometer with Adam, and reaches an accuracy of about `0.97`. Place the encodings of a photonic feature map so that they do not commute When two features are encoded as phases on *adjacent* single modes of a one-photon state, they only change their *relative* phase. The model then sees `x0 − x1` and cannot see `x0 + x1`. Separate the two encodings with a beam splitter, which does not commute with them, so that both features enter independently. This is the photonic version of ordering the rotations in a qubit feature map. ## Reservoir computing Photonics also supports **reservoir computing**, in the form of a quantum extreme learning machine. A *fixed* random interferometer is the reservoir. Each input is encoded in its phases and read out as photon-number statistics, which gives a high-dimensional nonlinear map of the features. Only a classical linear head is trained. No gradient through the optics is needed, only the forward path of the executor. It reuses the `Stateless` windowing policy of the qubit reservoir package without changes, and the readout can be the closed-form `LinearReadout` of the same package. ```python import numpy as np from eigenkit.photonics.reservoir import random_reservoir_spec, run_photonic_reservoir from eigenkit.reservoir import LinearReadout, Stateless rng = np.random.default_rng(0) sequences = rng.uniform(0, np.pi, size=(8, 10, 1)) # (B, T, F), one feature per step targets = sequences[..., 0] * np.roll(sequences[..., 0], 1, axis=1) # x_t * x_{t-1} spec, weights = random_reservoir_spec(n_modes=4, n_inputs=2, input_state=(1, 1, 0, 0)) feats = run_photonic_reservoir( spec, weights, sequences, policy=Stateless(window=2) ) # (B, T, n_fock) head = LinearReadout(ridge=1e-6).fit( feats.reshape(-1, feats.shape[-1]), targets.reshape(-1) ) ``` A worked example is in [`examples/photonics_reservoir.py`](https://github.com/eigenorbit/eigenkit/blob/main/examples/photonics_reservoir.py). It learns the bilinear memory task `y_t = x_t·x_{t-1}`, which a linear readout on the raw inputs cannot learn. No memory yet Only reservoirs without a state, or with a classical window, are supported. A photonic carry-over of the state, which would be a quantum memory across time steps, needs Fock states to be injected into the executor, and it is not supported yet. ## Running on Quandela Cloud Besides local simulation, a circuit can run on **Quandela Cloud**, on a real photonic quantum processing unit (QPU) or on a cloud simulator, through `QuandelaCloudBackend`. The results are *sampled* (counts of shots), so `run` returns an estimated Fock distribution, and gradients are not available, as with the qubit QPU backends. The backend follows the QPU conventions of the library. It issues a `CostWarning` on the first submission, `submit()` returns a handle with `.result()` and `.status()` without waiting, and `dry_run_info()` works offline. ```python import numpy as np from eigenkit.photonics import QuandelaCloudBackend backend = QuandelaCloudBackend("qpu:ascella", shots=20000) # token from $QUANDELA_TOKEN compiled = backend.compile(spec) # `spec` and `weights` from the reservoir example above print(compiled.dry_run_info()) # inspect offline, before submitting handle = compiled.submit(weights, np.zeros(spec.n_inputs)) print(handle.status()) dist = handle.result() # blocks until the cloud job completes ``` The access token is read from the `token=` argument, then from `$QUANDELA_TOKEN` or `$PERCEVAL_TOKEN`, and then from the token that Perceval itself would use (saved with `pcvl.RemoteConfig`). It is never written in the code. The processor connects once per compiled handle, and the connection is reused for later runs. `run` and `result()` wait at most `poll_timeout_seconds` (`3600` by default). They raise an error if the job fails, or if the post-selection on the number of detected photons (`min_detected_photons`) discards every shot. See [`examples/quandela_cloud_hardware.py`](https://github.com/eigenorbit/eigenkit/blob/main/examples/quandela_cloud_hardware.py), and the live test marked `qpu` in `tests/photonics/test_quandela_cloud.py`, which you run with `pytest -m qpu` and a token set. ## How it maps to the qubit core The photonics package has the same layers as the core, for a different model of computation: | Qubit core | Photonics package | | -------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | | `core/ir.py` | `photonics/ir.py` | | `core/provider.py` (the protocol, and the general parameter-shift rule used when a backend has nothing faster) | the `photonics` executor and `photonics/gradients.py` | | `core/backends/*_backend.py` | the Perceval executor, which is a photonic backend | | `torch/` (the PyTorch interface) | `PhotonicCircuit` (the photonic PyTorch interface) | The Perceval executor is a *photonic* backend, not a qubit `Provider`. The PyTorch adapter belongs to the photonics package, not to the qubit `torch/` package. The `Parameter` tree, the separation between compiling and running, and the `autograd.Function` bridge are reused from the core without changes. ## Status and limitations - **Local simulation** uses the strong linear optical simulation (SLOS) algorithm of Perceval. It is exact within the sizes it can handle. Its cost grows with the numbers of photons and modes, and the sizes it can handle are smaller than for a qubit statevector of size `2ⁿ`. - **Execution on Quandela Cloud**, on a real QPU or a cloud simulator, goes through `QuandelaCloudBackend`. The results are sampled, so there are no gradients. - **One scalar observable is evaluated at a time, for one sample.** There is no batching yet. - **Only observables that are diagonal in the Fock basis are supported.** ## Roadmap These items are planned, in no fixed order: - A `PhotonicMeasurement` output type will allow several observables per evaluation and readouts that are not diagonal. It will also report the success probability of the post-selection with a sampled distribution. - `run`, `expectation` and `PhotonicCircuit` will accept a leading batch axis. - The Perceval circuit will be built once, with symbolic `pcvl.P` parameters that are bound at each call, in place of being rebuilt at every call. - Photonic reservoirs will get a quantum memory (an analogue of `Carry`), which needs Fock states to be injected between time steps. - `QuandelaCloudBackend` will move off `RemoteProcessor` and `RemoteJob`, which Perceval 1.3 keeps under `perceval.runtime.legacy` (they are still re-exported as `pcvl.*`). - **MerLin**, the PyTorch framework of Quandela for quantum machine learning, will be evaluated as an alternative to the parameter-shift bridge, with native autograd. # Providers Every entry point that builds a circuit accepts a `provider=` argument, in the same three forms: a string (a registered name), a configuration dataclass, or a backend instance. The same circuit can run on any backend without changes. ```python from eigenkit import Circuit, PennyLaneConfig # String: registered name lookup. Circuit(blocks=..., provider="pennylane") # Config: routes to the matching backend via `make_backend()`. Circuit(blocks=..., provider=PennyLaneConfig(device_name="lightning.qubit")) # Backend instance: used directly. from eigenkit import CudaQuantumBackend, register Circuit(blocks=..., provider=CudaQuantumBackend()) # Or register it once under a name, then use the name anywhere. register("cudaq", CudaQuantumBackend()) Circuit(blocks=..., provider="cudaq") ``` The built-in **NumPy** simulator is the default (`provider="numpy"`), and it is always available, with no extra. PennyLane, Qiskit, Braket, IQM, TensorCircuit, Qulacs and qsim register their names automatically once their extras are installed. CUDA Quantum and Qiskit Runtime do not register a name by themselves: pass an instance as `provider=`, or import the backend and call `register()`. A `QiskitRuntimeConfig` passed as `provider=` does not need registration either. Naming a backend whose package is not installed raises an `ImportError`, which tells you which extra to install. The name stays registered, so once the extra is installed, the same process finds it, without a restart: ```text ImportError: provider 'pennylane' is registered but its package is not installed (install with `pip install eigenkit[pennylane]`). ``` A name that the library does not know at all raises a `KeyError` instead, and lists the names that it knows. These are two different problems, and the two messages keep them apart. ## What "faster" is measured against When these docs call a backend *fast* or *faster* without naming a baseline, the comparison is with the built-in **NumPy** reference simulator. This is the default `provider="numpy"`, run on the central processing unit (CPU), analytically (`shots=None`) and in double precision. That simulator is written to be readable, and it is not tuned, which is what makes it the zero point. "Faster than NumPy" says that a backend is worth installing for a given workload. It does not say that the backend beats every other backend. The batching table further down uses a **different** baseline, and its heading says so. It compares the native batched call of a backend with the library's own Python loop over the samples, *on that same backend*. The speedup of about `30×` given for NumPy there is that same comparison between the loop and the batch, not a comparison between backends. All these figures are rough expectations from development runs, not published benchmarks with fixed hardware and versions. Time your own circuit before you choose a backend for its speed. ## Backend matrix | Backend | Registration | Gradients | Native batching | Notes | | --------------------- | ----------------------------------------------- | ----------------------------------- | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | NumPy | Built in (`"numpy"`). | Adjoint. | Yes, vectorised. | **The default.** The reference statevector simulator, with no extra dependencies. | | PennyLane | Lazy (`"pennylane"`). | Native. | Yes. | Any PennyLane device, through `PennyLaneConfig`. It needs the `pennylane` extra. | | Qiskit | Lazy (`"qiskit"`). | Parameter shift. | No, it loops. | A statevector simulator. Finite shots are not supported yet, and any value other than `shots=None` raises an error. | | Braket (local) | Lazy (`"braket"`). | Parameter shift. | No, it loops. | `LocalSimulator`. | | Braket (cloud or QPU) | The same backend, chosen by the configuration. | Parameter shift, or none. | No, it loops. | `BraketConfig(device_arn=...)`. On a quantum processing unit (QPU), gradients are turned off unless `enable_gradients=True`, and `shots > 0` is required. | | Qiskit Runtime | On request (`register("qiskit-runtime", ...)`). | None, or parameter shift. | Yes. | IBM Eagle and Heron devices, and the fake backends. Gradients only with `enable_gradients=True`. | | IQM | Lazy (`"iqm"`). | None, or parameter shift. | No, one job per sample. | IQM Resonance through iqm-client, and the fake backends of IQM, which model its noise. The hardware returns only counts, so every value is computed from histograms of bitstrings. Gradients only with `enable_gradients=True`. It needs the `iqm` extra. | | CUDA Quantum | On request (`register("cudaq", ...)`). | Parameter shift. | Partly. | The optional `cudaq` dependency. `Sum` and `Multiply` expressions are not supported. | | TensorCircuit | Lazy (`"tensorcircuit"`). | Automatic differentiation with JAX. | Yes, with `jax.vmap`. | A tensor-network simulator, through tensorcircuit-ng on JAX. It needs the `tensorcircuit` extra. It is best for workloads that are run many times (see the note below). | | Qulacs | Lazy (`"qulacs"`). | Adjoint. | No, it loops. | A fast statevector simulator written in C++. It has a native reverse-mode gradient through `backprop`, and it supports the full set of gates. It needs the `qulacs` extra. | | Qiskit Aer | Lazy (`"qiskit-aer"`). | Parameter shift. | No, it loops. | The dense statevector core of Aer. It comes with the `qiskit` extra, so it adds no dependency. | | Qiskit Aer MPS | Lazy (`"qiskit-aer-mps"`). | Parameter shift. | No, it loops. | The same backend, with `method="matrix_product_state"`. It is exact when the bond dimension is not limited. It is the natural point of comparison for a simulation based on the time-evolving block decimation (TEBD) algorithm. | | qsim | Lazy (`"qsim"`). | Parameter shift. | No, it loops. | The qsim simulator of Google, through Cirq. It works in **single precision** (`float32`), so it is fast but has a residual error of about `1e-6`. It needs the `qsim` extra. | ## NumPy (built in) This is the default: a reference statevector simulator in pure NumPy, with no extra dependencies, so that an install with no extra can run circuits immediately: ```python import numpy as np from eigenkit import Circuit, Encoding, Variational, all_z circuit = Circuit( blocks=[Encoding(in_features=2), Variational(depth=1)], observables=all_z(2), ) # provider="numpy", interface="numpy" by default circuit(np.zeros(circuit.spec.n_weights), np.array([0.3, -0.2])) ``` Gradients use the analytic **adjoint** method, which is exact and costs `O(1)` in the number of parameters on statevector simulators. The simulator takes the simple speed gains that NumPy allows. A batch of inputs evolves in one vectorised sweep, with a leading batch axis on the state tensor, which is about `30×` faster than a loop over the samples with a batch of `64`. A CNOT is applied as a swap of amplitudes (`np.flip`), not as a matrix product. It is still the readable reference and not a tuned simulator, and it is the baseline of every "faster" claim in these docs. Install `pennylane` for the fast path. The forward values and the gradients agree with PennyLane to machine precision. ## PennyLane ```python from eigenkit import Circuit, PennyLaneConfig config = PennyLaneConfig(device_name="lightning.qubit", shots=None) Circuit(blocks=..., provider=config) ``` `device_name` is the identifier of any PennyLane device. `shots=None` evaluates the circuit analytically. The PyTorch interface of PennyLane computes gradients natively. ## Qiskit ```python from eigenkit import Circuit, QiskitConfig config = QiskitConfig(shots=None) Circuit(blocks=..., provider=config) ``` The backend builds a parametric `QuantumCircuit` once. Each forward pass binds the parameters with `assign_parameters` and computes the expectation values with `Statevector`. Gradients use the parameter-shift rule. ## Braket `BraketConfig` chooses between the local simulator, a cloud simulator and a QPU, based on `device_arn`: ```python from eigenkit import BraketConfig # Local (default). local = BraketConfig() # Cloud simulator (SV1). sv1 = BraketConfig( device_arn="arn:aws:braket:::device/quantum-simulator/amazon/sv1", shots=1000, ) # QPU (gradients disabled unless enable_gradients=True, shots > 0 enforced). qpu = BraketConfig( device_arn="arn:aws:braket:us-east-1::device/qpu/ionq/Aria-1", shots=1000, ) ``` Runs on the cloud and on a QPU issue a single `CostWarning` when the circuit is compiled. See [QPU and cloud workflows](https://docs.kit.eigenorbit.com/user-guide/qpu/index.md) for submitting without waiting and for dry runs. Braket runs `run`, `sample`, `probabilities` and `zero_state_probability`, either waiting for the result or through `submit()`. `sample()` needs `shots > 0`. With `shots=0`, `probabilities()` and `zero_state_probability()` are exact on the local simulator, SV1 and DM1. The result type that each device uses, and the largest number of qubits for the exact results, are in the [table of Braket result types](https://docs.kit.eigenorbit.com/user-guide/qpu/#braket-shot-rules-and-result-types). ## Qiskit Runtime This backend targets real IBM hardware, and the fake backends of `qiskit_ibm_runtime`, which model the noise of the devices. The token is looked up in this order: an explicit token, then the `QISKIT_IBM_TOKEN` environment variable, then the saved account file. `channel` defaults to `"ibm_quantum_platform"`, because the older `"ibm_quantum"` channel was retired. ```python from eigenkit import Circuit, QiskitRuntimeConfig config = QiskitRuntimeConfig(backend_name="ibm_fez", shots=1024) Circuit(blocks=..., provider=config) # Or let the service choose its least busy operational device. config = QiskitRuntimeConfig(backend_name="least_busy", shots=1024) ``` `backend_name` is required unless `backend_instance` is given. There is no default device, because IBM retires devices. Passing the configuration does not need a `register()` call. For tests, pass a `FakeBackendV2` directly through `backend_instance`. No authentication is needed. `primitive="sampler"` uses `SamplerV2` in place of `EstimatorV2`, and computes the expectation values from the raw counts of bitstrings. This also makes `sample()` and `probabilities()` available. See [QPU and cloud workflows](https://docs.kit.eigenorbit.com/user-guide/qpu/index.md). ## IQM This backend targets IQM hardware through the software of IQM itself, and not through Braket. The difference is between a bill from Amazon Web Services (AWS) and the plans of IQM, and this is the only way to use the fake backends of IQM and its Star architectures, which use MOVE gates. The token is looked up in this order: an explicit token, then the `IQM_TOKEN` environment variable, then whatever iqm-client finds by itself. ```python from eigenkit import Circuit, IQMConfig config = IQMConfig( url="https://resonance.iqm.tech/", quantum_computer="garnet", shots=2048, ) Circuit(blocks=..., provider=config) ``` The service of IQM returns only histograms of bitstrings, so every expectation value is computed from counts, and `shots > 0` is required. For tests, pass an `IQMFakeAdonis()` or an `IQMFakeDeneb()` directly through `backend_instance`. They run locally on Aer, with a model of the noise of IQM, and they need no token and no network. ## CUDA Quantum ```python from eigenkit import ( Circuit, CudaQuantumConfig, CudaQuantumBackend, register, ) register("cudaq", CudaQuantumBackend()) config = CudaQuantumConfig(target="nvidia", shots=None) Circuit(blocks=..., provider=config) ``` Common targets are `"qpp-cpu"` (a statevector on the CPU, the default), `"nvidia"` (one graphics processing unit, or GPU), `"nvidia-mgpu"`, `"nvidia-mqpu"` and `"tensornet"`. The arithmetic of CUDA Quantum on parameters (`QuakeValue`) is limited: `Sum` and `Multiply` expressions are not supported. This rules out `Reuploading(trainable_scale=True)` on CUDA Quantum. ## TensorCircuit ```python from eigenkit import Circuit, TensorCircuitConfig # Auto-registered once the `tensorcircuit` extra is installed. Circuit(blocks=..., provider="tensorcircuit") # Equivalently, via the config. Circuit(blocks=..., provider=TensorCircuitConfig()) ``` This backend simulates circuits as tensor networks, with [tensorcircuit-ng](https://github.com/tensorcircuit/tensorcircuit-ng) on the JAX backend (the `tensorcircuit` extra). Expectation values are exact contractions of the tensor network. The batch of parameters is vectorised with `jax.vmap`, and the whole forward pass and gradient are compiled with `jax.jit`. Gradients use the native reverse-mode automatic differentiation of JAX (`jax.jacrev`), exposed as the `jacobian` of the provider. So both the NumPy variational optimisation tools (VQE and QAOA through `minimize_expectation`) and the PyTorch interface use them, without the parameter-shift rule. The software is imported only when it is needed, so `import eigenkit` does not import JAX until this provider is resolved. The supported gates are `RX`, `RY`, `RZ`, `H`, `X`, `Z`, `CNOT` and `SWAP`. **When it pays off.** The simulation is still an *exact* contraction, so what this backend gives is a constant factor, not access to larger circuits. It has a one-time compilation cost with `jax.jit` of a few seconds, which grows with the number of qubits and the size of the Hamiltonian. So it suits workloads that are **run many times** (training loops, searches over hyperparameters, VQE and QAOA with an ansatz of fixed shape), where this cost is spread over many evaluations. For single runs or exploration, the NumPy simulator is faster. The forward values and the gradients match the NumPy reference to machine precision. ## Qulacs ```python from eigenkit import Circuit, QulacsConfig, QulacsBackend, register register("qulacs", QulacsBackend()) # auto-registers when the extra is installed Circuit(blocks=..., provider="qulacs") # or provider=QulacsConfig() ``` [qulacs](https://github.com/qulacs/qulacs) is a statevector simulator with a compiled C++ core, where the NumPy reference is written in Python and NumPy. It has a **native adjoint gradient** (`ParametricQuantumCircuit.backprop`), so it declares `gradient_method="adjoint"`, like the NumPy reference simulator. One `backprop` sweep per observable gives the gradient of every gate angle in reverse mode, at a cost of `O(1)` in the number of parameters. These gradients are mapped onto the weights and the inputs with the chain factors of the intermediate representation, so `Reuploading(trainable_scale=True)` and other angles with `Scale` or `Multiply` are differentiated correctly. The forward values, the gradients, `zero_state_probability` (the primitive of quantum kernels) and the reading and writing of `statevector` all agree with the NumPy reference to machine precision. The full set of gates is supported. Gates for which qulacs has no parametric primitive are decomposed onto parametric rotations, so that they stay differentiable: `PhaseShift` becomes `RZ`, `CRY` becomes `RY·CX·RY·CX`, and `CPHASE` becomes three `RZ` and `CX` gates. `adjoint(AmplitudeEncoding)` is not supported, as with Qiskit, because a state preparation cannot be inverted as a gate. Install it with `pip install eigenkit[qulacs]`. ## Qiskit Aer ```python from eigenkit import Circuit, QiskitAerConfig Circuit(blocks=..., provider="qiskit-aer") # dense statevector core Circuit(blocks=..., provider="qiskit-aer-mps") # matrix-product-state core Circuit(blocks=..., provider=QiskitAerConfig(method="matrix_product_state")) ``` [Aer](https://github.com/Qiskit/qiskit-aer) is the high-performance simulator of Qiskit. It already comes with the `qiskit` extra, so this backend adds **no new dependency**. It reuses the translation of the plain Qiskit backend without changes (the lowering of gates, the binding of symbolic `Parameter` values, and the `SparsePauliOp` observables). Only the way the circuit is executed changes: the expectation values are read through `save_expectation_value` of Aer, in place of `Statevector.from_instruction`. Two methods are available, as two provider names. `qiskit-aer` is the dense statevector core, which is exact and matches the NumPy reference to about `1e-12`. `qiskit-aer-mps` is the matrix product state (MPS) core. It is exact when the bond dimension is not limited (the default of Aer), it is a fast approximation for circuits with little entanglement, and it is the natural point of comparison for a simulation with tensor networks. It agrees with NumPy to about `1e-9`, and the difference comes from the truncation of the singular value decomposition. The full set of gates is supported by both. Both methods translate the circuit to the native gates of Aer first (`optimization_level=0`, with no coupling map, so no routing and no permutation of the qubits). This is needed because the MPS core rejects `cry`, and because the statevector core cannot run the controlled unitary that a `ControlledQubitUnitary` becomes. Sampling with finite shots is not supported yet, so `shots` must be `None`. Install it with `pip install eigenkit[qiskit]`. ## qsim ```python from eigenkit import Circuit, QsimConfig Circuit(blocks=..., provider="qsim") # or provider=QsimConfig() Circuit(blocks=..., provider=QsimConfig(threads=8)) ``` [qsim](https://github.com/quantumlib/qsim) is the high-performance Schrödinger statevector simulator of Google, used through its `qsimcirq` bridge to Cirq. The intermediate representation is translated once into a parametrised `cirq.Circuit`, whose angles are SymPy symbols, and each call resolves the symbols and runs the circuit. **qsim works in single precision (`float32`)**, because the pip package has no build in double precision. The values are returned as `complex128`, but they carry an accumulated error of about `1e-6`, so qsim does not pass a parity test at `1e-9`. This residual comes from the precision and is not a bug, as with the single-precision path of CUDA Quantum. The parity tests check qsim at `1e-4`. What qsim gives is speed over the NumPy reference simulator, and the residual should be reported together with any timing, not hidden. The full set of gates of the circuit algebra is supported. The primitives that carry data (`AmplitudeEncoding`, `QubitUnitary` and `ControlledQubitUnitary`) are not, and they are rejected before the run. An `initial_state` is not supported, and passing one raises a `NotImplementedError`. The number of threads follows `OMP_NUM_THREADS` unless `QsimConfig(threads=...)` sets it, so qsim respects a benchmark setup that fixes the threads. Install it with `pip install eigenkit[qsim]`. ## Batched evaluation Batching works on **both interfaces** and on every backend. Pass a 2-D `inputs` array of shape `(B, n_inputs)`, and you get an output of shape `(B, n_obs)`, or `(B,)` when there is a single observable. `weights` stays 1-D, because the weights are shared across the batch. ```python import numpy as np from eigenkit import Circuit, Encoding, Variational, all_z # numpy interface (the default): weights are passed per call. circuit = Circuit( blocks=[Encoding(in_features=4), Variational(rotation="RY", depth=2)], observables=all_z(4), ) weights = np.zeros(circuit.spec.n_weights) # or a trained vector x_batch = np.random.uniform(0, np.pi, size=(128, 4)) y_batch = circuit(weights, x_batch) # shape (128, 4) ``` On the PyTorch interface, the module stores the weights, so you pass only the batch: ```python import torch from eigenkit import Circuit, Encoding, Variational, all_z circuit = Circuit( blocks=[Encoding(in_features=4), Variational(rotation="RY", depth=2)], observables=all_z(4), interface="torch", provider="pennylane", ) y_batch = circuit(torch.randn(128, 4, dtype=torch.float64)) # shape (128, 4) ``` ### What "native batching" means here When a call has the shape of the **fast path** (`weights.ndim == 1` and `inputs.ndim == 2`), the backends with native batching send the whole batch through one batched call of the provider. The Qiskit statevector backend and the local Braket backend use the shared Python loop `batch_call`. Any other combination of shapes (batched weights, a single sample of inputs in a 2-D wrapper and so on) falls back to a loop in Python, which gives the index of a sample that fails. The speedups below are **within one backend**: the native batched call of the backend against the library's own loop over the samples, on the same simulator. They are not comparisons between backends, and they are not measured against the NumPy reference. | Backend | How the fast path works | Speedup over the loop on the same backend | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **NumPy** | A leading batch axis on the state tensor, so the whole batch evolves in one sweep. The inputs are transposed to `(n_inputs, B)`, so an angle that depends on the inputs becomes a `(B,)` vector (a stack of `(B, 2, 2)` rotations), while angles that depend only on weights stay a single shared `(2, 2)` matrix. A CNOT is a swap of amplitudes with `np.flip`, not a matrix product. | About `30×` over the loop with a batch of `64`. | | **PennyLane** | Parameter broadcasting in the QNode. The `inputs` are transposed to `(n_inputs, B)`, so each `InputRef(j)` becomes a vector of shape `(B,)`, and every gate angle is broadcast through a single evaluation of the tape. | About `B×` over the loop. On `default.qubit` with inputs of shape `(128, 4)`, the overhead of the library is at most `20%` of the plain PennyLane QNode call. | | **Qiskit Runtime** | The `EstimatorV2` primitive accepts a `(B, n_params)` array of parameters in each Primitive Unified Bloc (PUB), so the whole batch is sent as one job, with one PUB per observable, in place of one job per sample. | Large. IBM Runtime is dominated by the overhead of each job, so collapsing one job per sample into one job is a big change. Check it on your own backend. | | **CUDA Quantum** | `cudaq.observe(kernel, ham, *2D_args)` uses the broadcast helper of CUDA Quantum. **Limitation**: this helper is itself a Python loop for kernels built with `cudaq.make_kernel(...)`, so the cost of just-in-time compilation is not shared. The behaviour is correct, and it is the usual way to batch in CUDA Quantum, but in practice the run time is currently within about `10%` of the explicit loop. Full vectorisation would require moving the backend to `@cudaq.kernel` declarations. | About the same as the loop. | | **TensorCircuit** | `jax.vmap` over the contraction of one sample, compiled with `jax.jit`, so the batch of parameters is a vectorised extra index of the tensor-network contraction. | About `B×` once it is compiled. The one-time compilation takes seconds, grows with the number of qubits, and is spread over the loop. | | **Qiskit (statevector)** | None. `Statevector.expectation_value` works on one state at a time, so the backend loops through the shared helper. | None. | | **Braket (local)** | None. It calls `LocalSimulator.run` once per task. Braket has `run_batch` to submit independent tasks in parallel, but it is not used yet. | None. | | **Qulacs** | None. `Observable.get_expectation_value` works on one state at a time, so the backend loops through the shared helper. The gradients also loop over `backprop`, one sample at a time, and each call is fast because it runs in C++. | None. | ### Gradients through batched inputs For the **backends with native automatic differentiation** (PennyLane), gradients through a 2-D `inputs` batch go through the same broadcasting as the forward pass, so the library does no extra work. For the **backends that use the parameter-shift rule** (Qiskit, local Braket, CUDA Quantum), the backward pass of the autograd function loops over the samples. It computes the jacobian of each sample, adds them into the shared gradient of the weights, and stacks the gradients of the inputs of each sample. This loop is part of the parameter-shift rule, because each sample has its own input vector, and therefore its own shifted expectation values. The forward pass still uses the batched path of the backend. Qiskit Runtime and IQM refuse gradients (`gradient_method="none"`) unless their configuration sets `enable_gradients=True`. Then they use the same loop, and every shifted evaluation is a paid forward call. ## Registering custom configs Use `register()` to give a configuration a name, so that you can reuse it: ```python from eigenkit import PennyLaneConfig, register register("lightning", PennyLaneConfig(device_name="lightning.qubit")) Circuit(blocks=..., provider="lightning") ``` ## Reusing the same circuit on another provider `with_provider` is a method of the **PyTorch** circuit (`interface="torch"`). If you already have one, and you want the **same** `CircuitSpec` and observables on a different backend, call it instead of building the list of blocks again. The `provider` argument accepts the same three forms as `Circuit(..., provider=...)`: a registered name, a configuration with `make_backend()`, or a backend instance. ```python import torch from eigenkit import Circuit, Encoding, Variational, all_z, PennyLaneConfig layer = Circuit( blocks=[Encoding(in_features=2), Variational(depth=1)], observables=all_z(2), provider="pennylane", interface="torch", ) x = torch.randn(2, dtype=torch.float64) layer_qiskit = layer.with_provider("qiskit") layer_lightning = layer.with_provider( PennyLaneConfig(device_name="lightning.qubit"), ) ``` By default, the weights are **cloned** from the original circuit. Pass `copy_weights=False` for new random weights, or `init=...` to set the weights explicitly (it takes precedence over `copy_weights`). ## API reference ### `eigenkit.core.provider.register(name, provider_or_config)` Register a backend, or a configuration that can build one, under `name`. Later calls to `Circuit(..., provider=name)` then use that backend. If a configuration is passed, its `make_backend()` method is called to build the backend. Parameters: | Name | Type | Description | Default | | -------------------- | ---------- | --------------------- | -------------------------------------------------------------------- | | `name` | `str` | The name to register. | *required* | | `provider_or_config` | \`Provider | object\` | A backend instance, or a configuration with a make_backend() method. | ### `eigenkit.core.provider.registered_names()` Return every provider name that `get` accepts, including the lazy ones. ### `eigenkit.torch.circuit.TorchCircuit.with_provider(provider, *, copy_weights=True, init=None)` Return a new `TorchCircuit` for another `provider`. The new circuit has the same `spec` and the same `observables` argument as this instance, and it is compiled for the given `provider`. `provider` accepts the same forms as in `__init__()`: a registered name (a `str`), a configuration object with a `make_backend()` method (for example `PennyLaneConfig`), or a backend instance. The gradients follow the `gradient_method` of the new backend. The forward values should match across simulators up to numerical tolerance, but not exactly. Parameters: | Name | Type | Description | Default | | -------------- | -------------- | ------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | `provider` | `ProviderSpec` | The backend for the new circuit. | *required* | | `copy_weights` | `bool` | If True, the new circuit starts from a copy of self.weights. It is ignored when init is given. Default: True. | `True` | | `init` | \`Tensor | None\` | An optional weight tensor of shape (spec.n_weights,). When it is given, it takes precedence over copy_weights. Default: None. | Returns: | Type | Description | | ---------------- | --------------------- | | `'TorchCircuit'` | The new TorchCircuit. | ### `eigenkit.core.provider.CostWarning` Bases: `UserWarning` Warning issued before a submission to a paid quantum backend. A paid backend is a quantum processing unit (QPU) or a cloud simulator that charges for use. The first compilation for such a device issues this warning, so that costs cannot build up without the user noticing. ### Config dataclasses ### `eigenkit.core.backends.numpy_backend.NumpyConfig` Configuration for the reference NumPy simulator. It has no settings yet. It exists so that `Circuit(provider=NumpyConfig())` works in the same way as with the configurations of the other backends. #### `make_backend()` Build the backend that this configuration describes. The provider layer calls it when a configuration is passed as `provider=`. There is no reason to call it directly. Returns: | Type | Description | | ------------------ | ------------------------------------------ | | `'NumpySimulator'` | The NumpySimulator for this configuration. | ### `eigenkit.core.backends.pennylane_backend.PennyLaneConfig` Device settings, used for every `qml.device()` call. Pass it directly to `Circuit(provider=PennyLaneConfig(...))`, or pass it to `register()` under a name to reuse it. Parameters: | Name | Type | Description | Default | | ------------- | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | | `device_name` | `str` | The identifier of the PennyLane simulator plugin: "default.qubit", "lightning.qubit", "lightning.gpu", or any other installed device. Default: "default.qubit". | `'default.qubit'` | | `shots` | \`int | None\` | None computes exact expectation values, and a positive integer samples that many shots for each expectation value. Default: None. | | `diff_method` | `str` | The PennyLane differentiation method. With "best", PennyLane chooses for each device, usually backpropagation on a simulator and the parameter-shift rule on hardware. Default: "best". | `'best'` | #### `make_backend()` Build the backend that this configuration describes. The provider layer calls it when a configuration is passed as `provider=`. There is no reason to call it directly. Returns: | Type | Description | | -------------------- | -------------------------------------------- | | `'PennyLaneBackend'` | The PennyLaneBackend for this configuration. | ### `eigenkit.core.backends.qiskit_backend.QiskitConfig` Device configuration for the Qiskit backend. Pass it to `Circuit(provider=QiskitConfig(...))`, or register it under a name with `register()` to use it again. Parameters: | Name | Type | Description | Default | | ------- | ----- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `shots` | \`int | None\` | None computes exact expectation values with Statevector. Finite-shot sampling is not supported on this backend yet, and any other value makes the compilation raise NotImplementedError. For sampling, use the Qiskit Runtime backend, which can also run locally on a fake device. Default: None. | #### `make_backend()` Build the backend that this configuration describes. The provider layer calls it when a configuration is passed as `provider=`. There is no reason to call it directly. Returns: | Type | Description | | ----------------- | ----------------------------------------- | | `'QiskitBackend'` | The QiskitBackend for this configuration. | ### `eigenkit.core.backends.qiskit_aer_backend.QiskitAerConfig` Device configuration for the Qiskit Aer backend. Parameters: | Name | Type | Description | Default | | -------- | ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `method` | `str` | The Aer simulation method. "statevector" selects the exact dense simulator. "matrix_product_state" selects the matrix product state (MPS) simulator, which is exact when the bond dimension is not bounded, and an approximation for circuits with little entanglement otherwise. The provider name "qiskit-aer-mps" is this configuration with the MPS method already set. Any other value raises ValueError. Default: "statevector". | `'statevector'` | | `shots` | \`int | None\` | None returns exact expectation values. Finite-shot sampling is not supported yet, and any other value makes the compilation raise NotImplementedError. Default: None. | #### `make_backend()` Build the backend that this configuration describes. The provider layer calls it when a configuration is passed as `provider=`. There is no reason to call it directly. Returns: | Type | Description | | -------------------- | -------------------------------------------- | | `'QiskitAerBackend'` | The QiskitAerBackend for this configuration. | ### `eigenkit.core.backends.braket_backend.BraketConfig` Device configuration for the Braket backend. Parameters: | Name | Type | Description | Default | | ---------------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `device_arn` | \`str | None\` | None (the default) for the local simulator, or the Amazon Resource Name (ARN) of an Amazon Web Services (AWS) device. An example for a cloud simulator is arn:aws:braket:::device/quantum-simulator/amazon/sv1. Quantum processing units (QPUs) have ARNs of the form arn:aws:braket:::device/qpu//. | | `local_backend` | `str` | The LocalSimulator backend to use when device_arn is None. Common values are "braket_sv" (state vector) and "braket_dm" (density matrix). | `'braket_sv'` | | `shots` | `int` | 0 means exact values (simulators only), and a positive integer turns on sampling with that number of shots. AWS QPUs require shots > 0. | `0` | | `s3_bucket` | \`str | None\` | The Amazon Simple Storage Service (S3) bucket for the results of cloud tasks. With None, the Braket package picks the default of the account. | | `s3_prefix` | `str` | The key prefix inside s3_bucket. | `'braket-results'` | | `poll_timeout_seconds` | `int` | How many seconds result() waits for a submitted task. | `3600` | | `enable_gradients` | `bool` | Whether to allow parameter-shift gradients on a QPU, for PyTorch .backward(), jacobian() and gradient-based optimisers. It is off by default, because one backward pass costs at least 2 * n_weights paid tasks. When it is set, compiling for a QPU issues a CostWarning that gives this count. Simulators compute gradients in either case. | `False` | #### `is_cloud` Whether `device_arn` is set. If it is, the circuit runs on Amazon Web Services (AWS), on a cloud simulator or a quantum processing unit (QPU), instead of on the local simulator. #### `is_qpu` Whether `device_arn` names real hardware (it contains `/qpu/`). #### `make_backend()` Build the backend that this configuration describes. The provider layer calls it when a configuration is passed as `provider=`. There is no reason to call it directly. Returns: | Type | Description | | ----------------- | ----------------------------------------- | | `'BraketBackend'` | The BraketBackend for this configuration. | ### `eigenkit.core.backends.qiskit_runtime_backend.QiskitRuntimeConfig` Bases: `SecretFieldsMixin` Device configuration for the Qiskit Runtime backend. There are two ways to choose the target backend: - `backend_name` is looked up with `QiskitRuntimeService`. It needs credentials: a token, an environment variable or a saved account. - `backend_instance` is a `BackendV2` that is used directly. It is useful for tests with a fake backend, or to pass any backend that is already configured. Parameters: | Name | Type | Description | Default | | ---------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `backend_name` | \`str | None\` | The IBM backend name, for example "ibm_fez", or "least_busy" for the least busy operational device that the account can use, chosen once per compiled circuit. It is required unless backend_instance is set, and it is ignored when it is. Default: None. | | `backend_instance` | `Any` | A backend that is already configured. It takes precedence over backend_name. Default: None. | `None` | | `shots` | `int` | The number of shots per circuit run. Default: 1024. | `1024` | | `optimization_level` | `int` | The optimisation level passed to qiskit.transpile, from 0 to 3. Default: 1. | `1` | | `seed_transpiler` | \`int | None\` | The seed of the random passes of the transpiler, mainly the SabreLayout and SabreSwap passes, which choose the layout and the routing. None, the default, draws a new seed for every compile, so a circuit that needs routing compiles to a different number of gates on every run. Within one compile, all the transpilations share the seed, including the shifted copies that a gradient on hardware compiles. Set it whenever two compilations must be comparable. Since compiling submits nothing, trying a few seeds and keeping the cheapest circuit is a cheap and fair way to choose a layout. | | `primitive` | `Primitive` | The Runtime primitive that computes the expectation values. "estimator", the default, uses EstimatorV2, with one Primitive Unified Bloc (PUB) per observable in each job. "sampler" uses SamplerV2, with one PUB per qubit-wise commuting measurement group, and reduces the raw counts locally. sample() and probabilities() use SamplerV2 with either value. | `'estimator'` | | `group_commuting` | `bool` | Whether the sampler puts qubit-wise commuting Pauli terms into shared measurement circuits. False runs one circuit per term, which runs more circuits but keeps the shot budget of each term independent. The estimator ignores it. Default: True. | `True` | | `token` | \`str | None\` | The token passed to QiskitRuntimeService when backend_instance is None. With None, the backend uses QISKIT_IBM_TOKEN, then the saved account file. It is secret: it is never shown by repr(), and it is never saved in .eigenkit bundles, pickles or joblib checkpoints. After a load it is None, so set QISKIT_IBM_TOKEN. | | `channel` | `str` | The service channel: "ibm_quantum_platform" (the default), "ibm_cloud" or "local". The legacy "ibm_quantum" channel was retired, and QiskitRuntimeService no longer accepts it. | `'ibm_quantum_platform'` | | `instance` | \`str | None\` | The service instance, as a Cloud Resource Name (CRN) or a name, when one applies. Default: None. | | `service_kwargs` | `dict` | Extra keyword arguments passed to QiskitRuntimeService, such as region, url or proxies. | `dict()` | | `poll_timeout_seconds` | `int` | How long result() waits before it gives up, in seconds. Default: 3600. | `3600` | | `enable_gradients` | `bool` | Whether to allow parameter-shift gradients on the device: PyTorch .backward(), jacobian() and gradient-based optimisers. It is off by default, because one backward pass costs at least 2 * n_weights times the jobs of a forward call. When it is on, compiling for a paid device gives a CostWarning that states this cost. | `False` | #### `__post_init__()` Reject an unknown primitive when the configuration is built. If the check waited for the first submission, the error would come as a confusing failure on a paid backend, after the transpilation and the cost warning. #### `make_backend()` Build the backend that this configuration describes. The provider layer calls it when a configuration is passed as `provider=`. There is no reason to call it directly. Returns: | Type | Description | | ------------------------ | ------------------------------------------------ | | `'QiskitRuntimeBackend'` | The QiskitRuntimeBackend for this configuration. | ### `eigenkit.core.backends.iqm_backend.IQMConfig` Bases: `SecretFieldsMixin` Device configuration for the IQM backend. There are two ways to choose a target: - `url`, and optionally `quantum_computer`, opens an `IQMProvider` for an IQM Server. This needs a token, from `token` or from the `IQM_TOKEN` environment variable. - `backend_instance` uses any IQM `BackendV2` directly: a fake backend for offline work, or a backend that you configured yourself. It takes precedence over `url`, and it needs no token and no network access. Parameters: | Name | Type | Description | Default | | ---------------------- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `url` | \`str | None\` | The address of the IQM Server, for example "https://resonance.iqm.tech/". It is ignored when backend_instance is set. With None, the backend reads the address from the IQM_SERVER_URL environment variable when the circuit is compiled. | | `quantum_computer` | \`str | None\` | The machine to use when the server hosts more than one, for example "garnet". With None, the server's default is used. | | `backend_instance` | `Any` | An IQM backend that is already configured. It takes precedence over url. | `None` | | `shots` | `int` | The number of shots for each circuit run. It must be positive, because hardware cannot compute exact values. | `1024` | | `optimization_level` | `int` | The optimisation level passed to qiskit.transpile, from 0 to 3. | `1` | | `seed_transpiler` | \`int | None\` | The seed of the random steps of the transpiler, mainly the SabreLayout and SabreSwap passes, which choose the layout and the routing. With None, the default, a new seed is drawn for every compilation, so a circuit that needs routing compiles to a different number of gates on every run. Within one compilation, all the transpilations share the seed, including those of the shifted copies that a hardware gradient compiles. Set it whenever two compilations must be comparable. Trying a few seeds and keeping the cheapest result is a cheap and fair way to choose a layout, because compiling submits nothing. | | `transpile_mode` | `TranspileMode` | "parametric" transpiles once and sets the parameter values at each call. "per_call" sets the values first and transpiles at every call, which gives shallower circuits but costs one transpilation per evaluation. | `'parametric'` | | `group_commuting` | `bool` | Whether to put Pauli terms that commute qubit by qubit into shared measurement circuits. With False, one circuit runs for each term: there are more jobs, but each term gets all the shots. | `True` | | `token` | \`str | None\` | The access token for the IQM Server. It is passed on only when it is set. With None, iqm-client handles authentication on its own, from IQM_TOKEN and from its own tokens file. Do not set both token and IQM_TOKEN, because iqm-client rejects a token given in two ways at once. This field is secret: repr() never shows it, and it is never saved in .eigenkit bundles, pickles or joblib checkpoints. After a load it is None, so set IQM_TOKEN. | | `calibration_set_id` | `Any` | The calibration set to use, as a UUID. With None, the server's default is used. | `None` | | `use_metrics` | `bool` | Whether to ask the server for the error rates of each qubit and each coupler, and load them into the InstructionProperties of the target. With False (the default of iqm-client itself), the target has no error rates, so the Qiskit layout pass does not see the noise. It then cannot prefer the better qubits, and a comparison with a backend that does have calibration data is not like for like. It costs one extra request at compile time, and it is ignored when backend_instance is set. | `False` | | `provider_kwargs` | `dict` | Extra keyword arguments passed to IQMProvider, for example tokens_file. | `dict()` | | `poll_timeout_seconds` | `int` | How many seconds result() waits before giving up. | `3600` | | `enable_gradients` | `bool` | Whether to allow parameter-shift gradients on the device, for PyTorch .backward(), jacobian() and gradient-based optimisers. It is off by default, because one backward pass runs the circuits of a forward call at least 2 * n_weights times. When it is set, compiling for a real device issues a CostWarning that gives this count. | `False` | #### `__post_init__()` Reject the settings that can never work, when the configuration is created. If they were left to the first submission, the error would appear after the transpilation and the cost warning, which on a paid device is the wrong time to find out. Only the settings that are wrong on their own are checked here. Whether a *target* can be found depends on the environment at compile time, so `_resolve_backend` checks that. #### `make_backend()` Build the backend that this configuration describes. The provider layer calls it when a configuration is passed as `provider=`. There is no reason to call it directly. Returns: | Type | Description | | -------------- | -------------------------------------- | | `'IQMBackend'` | The IQMBackend for this configuration. | ### `eigenkit.core.backends.cudaquantum_backend.CudaQuantumConfig` Device configuration for the CUDA Quantum backend. Parameters: | Name | Type | Description | Default | | -------- | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | | `target` | `str` | The cudaq target. Common values are "qpp-cpu", a statevector simulator on the central processing unit (CPU), which is the default; "nvidia", on one graphics processing unit (GPU); "nvidia-mgpu", on several GPUs; "nvidia-mqpu", with several quantum processing units (QPUs); and "tensornet". Default: "qpp-cpu". | `'qpp-cpu'` | | `shots` | \`int | None\` | None for exact expectation values, or a positive integer for a finite number of shots. Default: None. | #### `make_backend()` Build the backend that this configuration describes. The provider layer calls it when a configuration is passed as `provider=`. There is no reason to call it directly. Returns: | Type | Description | | ---------------------- | ---------------------------------------------- | | `'CudaQuantumBackend'` | The CudaQuantumBackend for this configuration. | ### `eigenkit.core.backends.tensorcircuit_backend.TensorCircuitConfig` Configuration for the tensorcircuit backend. It has no settings yet: the contraction path is the default of tensorcircuit, and the batch axis is vectorised with `jax.vmap`. It exists for consistency with the configurations of the other backends, and so that `provider=TensorCircuitConfig()` works without a `register` call. #### `make_backend()` Build the backend that this configuration describes. The provider layer calls it when a configuration is passed as `provider=`. There is no reason to call it directly. Returns: | Type | Description | | ------------------------ | ------------------------------------------------ | | `'TensorCircuitBackend'` | The TensorCircuitBackend for this configuration. | ### `eigenkit.core.backends.qulacs_backend.QulacsConfig` Device configuration for the Qulacs backend. The Qulacs statevector simulator for the central processing unit (CPU) needs no settings, so this configuration is currently only a marker. It makes `Circuit(provider=QulacsConfig())` select this backend, and it leaves room for future fields, for example a state on a graphics processing unit (GPU). Pass it to `Circuit(provider=QulacsConfig())`, or pass it to `register()` under a name to reuse it. #### `make_backend()` Build the backend that this configuration describes. The provider layer calls it when a configuration is passed as `provider=`. There is no reason to call it directly. Returns: | Type | Description | | ----------------- | ----------------------------------------- | | `'QulacsBackend'` | The QulacsBackend for this configuration. | ### `eigenkit.core.backends.qsim_backend.QsimConfig` Device configuration for the qsim backend. Parameters: | Name | Type | Description | Default | | --------- | ----- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `threads` | \`int | None\` | The size of the internal thread pool of qsim. None reads OMP_NUM_THREADS instead, which defaults to 1, so qsim uses one core unless you set it here or in the environment. Set it explicitly before you report any timing. Default: None. | #### `make_backend()` Build the backend that this configuration describes. The provider layer calls it when a configuration is passed as `provider=`. There is no reason to call it directly. Returns: | Type | Description | | --------------- | --------------------------------------- | | `'QsimBackend'` | The QsimBackend for this configuration. | # QML The `qml` package is the scikit-learn side of the library for quantum machine learning (QML). It holds kernel support vector machines (SVMs) and quantum reservoir estimators. Everything here follows the scikit-learn interface, with `fit(X, y)`, `predict(X)` and `score(X, y)`, and runs without PyTorch. **Install:** the `qml` extra (`pip install eigenkit[qml]`) installs scikit-learn, and not PyTorch. The estimators run on the built-in NumPy simulator. To run them on another backend, install its extra and pass `provider=`, for example `eigenkit[pennylane]` and `provider="pennylane"`. ## QSVC `QSVC` is the quantum support vector classifier. It passes the quantum kernel matrix to scikit-learn's `SVC(kernel="precomputed")`. ```python from eigenkit import QSVC from eigenkit.datasets import load_dataset data = load_dataset("iris") X_train, y_train = data["X_train"], data["y_train"] X_test, y_test = data["X_test"], data["y_test"] model = QSVC(feature_map="AngleCNOT", reps=2, C=1.0) model.fit(X_train, y_train) predictions = model.predict(X_test) ``` The accepted `feature_map` values, what `reps` does, and how the kernel value is computed are explained in [Quantum Kernels](https://docs.kit.eigenorbit.com/user-guide/kernels/index.md). The defaults of `QSVC`, `feature_map="Angle"` and `reps=1`, differ from those of `QuantumKernel` (`"AngleCNOT"` and `reps=2`). Any other kernel, such as a custom map, a trained map, or a kernel on hardware, is passed whole with `kernel=`. `feature_map`, `reps` and `provider` are then ignored, and the fitted copy of the kernel is `model.kernel_`. `scale` multiplies the inputs before they are encoded, so this bandwidth can be tuned like `C`: ```python from eigenkit import QuantumKernel kernel = QuantumKernel(n_features=X_train.shape[1], feature_map="AngleCNOT", reps=2) model = QSVC(kernel=kernel, scale=0.5).fit(X_train, y_train) margins = model.decision_function(X_test) ``` `decision_function(X)` returns the margins of the SVM. `probability=True` enables `predict_proba` through a sigmoid calibration, as in `CalibratedClassifierCV(SVC(), ensemble=False)`. The predictions still come from the plain `SVC`. ## QSVR `QSVR` is the quantum support vector regressor. It works like `QSVC`, but it passes the kernel matrix to scikit-learn's `SVR(kernel="precomputed")`. ```python from eigenkit import QSVR from eigenkit.datasets import load_dataset data = load_dataset("concrete") X_reg, y_reg = data["X_train"][:100], data["y_train"][:100] model = QSVR(feature_map="Angle", reps=1, C=1.0, epsilon=0.1) model.fit(X_reg, y_reg) ``` ## Use inside a scikit-learn Pipeline The kernel estimators can also be the last step of a scikit-learn `Pipeline`. When you tune them with `GridSearchCV`, prefix each parameter with the name of the step, including the bandwidth `scale`: ```python from sklearn.datasets import make_moons from sklearn.model_selection import GridSearchCV from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from eigenkit import QSVC X, y = make_moons(n_samples=40, noise=0.1, random_state=0) search = GridSearchCV( Pipeline( [ ("scale", StandardScaler()), ( "qsvc", QSVC( feature_map="Angle", reps=1, C=1.0, probability=False, ), ), ] ), param_grid={ "qsvc__C": [0.1, 1.0, 10.0], "qsvc__feature_map": ["Angle", "AngleCNOT"], "qsvc__scale": [0.5, 1.0], }, scoring="accuracy", cv=3, ) search.fit(X, y) predictions = search.predict(X[:5]) ``` ## Quantum reservoir computing `QuantumReservoirClassifier` and `QuantumReservoirRegressor` use a quantum circuit as a fixed dynamical system that is *not trained*, called the **reservoir**. They train only a cheap linear **readout** on the features that the reservoir produces. The weights of the circuit are drawn once from `random_state` and then frozen, so there is no gradient descent and no PyTorch. Fitting solves a ridge or logistic regression in closed form. The data is a batch of sequences. `X` has shape `(n_sequences, n_timesteps, n_features)`, and `y` has shape `(n_sequences, n_timesteps)`, with one target per time step. ```python import numpy as np from eigenkit import Encoding, Variational from eigenkit import QuantumReservoirRegressor rng = np.random.default_rng(0) X = rng.uniform(-1, 1, size=(40, 50, 3)) y = np.zeros((40, 50)) y[:, 1:] = X[:, :-1, 0] # predict the previous step's first channel reservoir = QuantumReservoirRegressor( blocks=[Encoding(in_features=3, rotation="RY"), Variational(depth=3, rotation="RY")], state_policy="leaky", leak=0.5, # fading memory features="probabilities", washout=5, ) reservoir.fit(X, y) print(reservoir.predict(X).shape) # (40, 50) ``` `QuantumReservoirClassifier` takes the same arguments, with one exception. It accepts integer or label targets, reads out through a logistic model, and adds `predict_proba`. The exception is the regularisation of the readout: the `ridge` of the regressor (`float`, default `1e-06`) becomes `C` on the classifier (`float`, default `1.0`). `C` is the inverse of the regularisation strength, so a larger `C` means *less* regularisation. | Parameter | Type | Default | Description | | -------------- | ------------------ | --------- | --------------------------------------------------------------------------------------------------------------------------------- | | `blocks` | `list[Block]` | Required | The frozen reservoir circuit. | | `state_policy` | `str` | `"carry"` | One of `"carry"`, `"leaky"` or `"stateless"`. | | `window` | `int` | `1` | The number of time steps encoded per step with `"stateless"`. The circuit must then encode `window * n_features` inputs. | | `leak` | `float` | `1.0` | The mixing rate for `"leaky"`. At `1.0`, it is the same as `"carry"`. | | `features` | `str \| list[str]` | `"z"` | One of `"z"`, `"probabilities"` or `"amplitudes"`, a Hamiltonian string such as `"0.5 Z0 X1 - 0.3 Y0"`, or a list of observables. | | `ridge` | `float` | `1e-06` | The regularisation of the linear readout. | | `washout` | `int` | `0` | The number of first time steps dropped when the readout is fitted. Predictions are still produced for every time step. | | `weight_scale` | `float` | `1.0` | The scale of the frozen weights that are drawn. Larger values give more energetic dynamics. | | `provider` | `ProviderSpec` | `"numpy"` | The backend, with the same options as `Circuit(provider=...)`. | | `random_state` | `int \| None` | `42` | The seed of the frozen weights. The reservoir is random, but fixed. | Fading memory matters A purely unitary carry (`state_policy="carry"`, the default) preserves the norm, but it tends to scramble the history into a form that a linear readout cannot recover. So it often has little memory that can be used. Some dissipation is what makes a reservoir work in practice: `"leaky"` with a `leak` of about `0.3` to `0.7` is usually the right starting point for temporal tasks. The [memory capacity example](https://docs.kit.eigenorbit.com/examples/reservoir-memory/index.md) shows this directly. The layers under these wrappers, which are the driver, the policy classes, the feature extractors and the readout, are documented in [Reservoir Computing](https://docs.kit.eigenorbit.com/user-guide/reservoir/index.md). ## API reference ### `eigenkit.qml.QSVC` Bases: `BaseQuantumClassifier` Quantum support vector classifier (QSVC). It computes a quantum kernel matrix from a fidelity circuit, and then uses `SVC(kernel="precomputed")` from scikit-learn. Parameters: | Name | Type | Description | Default | | -------------- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `feature_map` | `str` | The feature map. It is "Angle" (angle encoding into a product state) or "AngleCNOT" (angle encoding with ladders of controlled-NOT (CNOT) gates). Default: "Angle". It is ignored when kernel is given. | `'Angle'` | | `reps` | `int` | The number of repetitions of the feature map. Default: 1. It is ignored when kernel is given. | `1` | | `C` | `float` | The regularisation parameter of the support vector machine. Default: 1.0. | `1.0` | | `probability` | `bool` | If True, the model gives Platt-scaled probabilities. If False (the default), predict_proba is unavailable. The calibration is a sigmoid fitted by cross-validation on the training kernel, as in CalibratedClassifierCV(SVC(), ensemble=False). The predictions still come from the plain SVC. | `False` | | `provider` | `ProviderSpec` | The backend. It takes the same options as Circuit(provider=...). Any backend with a statevector or a zero_state_probability method works, including Qiskit Runtime and IQM hardware. Default: "numpy". It is ignored when kernel is given. | `'numpy'` | | `random_state` | \`int | None\` | The seed. | | `kernel` | \`QuantumKernel | None\` | A QuantumKernel to use instead of building one from feature_map, reps and provider. It can be a custom kernel, a trained kernel or a hardware kernel. Default: None. | | `scale` | `float` | The bandwidth. The inputs are multiplied by it before they reach the kernel, so it can be tuned with GridSearchCV. Default: 1.0. | `1.0` | Example ```python import numpy as np from eigenkit import QSVC, Encoding, QuantumKernel, Variational rng = np.random.default_rng(0) X_train, X_test = rng.uniform(0, np.pi, (8, 4)), rng.uniform(0, np.pi, (3, 4)) y_train = np.array([0, 1] * 4) model = QSVC(feature_map="AngleCNOT", reps=2) model.fit(X_train, y_train) predictions = model.predict(X_test) # Any kernel works, including custom and trained ones. my_map = [Encoding(in_features=4), Variational(depth=1)] model = QSVC(kernel=QuantumKernel(n_features=4, feature_map=my_map)) ``` #### `__sklearn_is_fitted__()` Return True when the wrapped scikit-learn `SVC` has been fitted. #### `decision_function(X)` Return the signed distance of each sample to the separating hyperplane. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | Returns: | Type | Description | | --------- | ------------------------------------------------------------------------------------- | | `ndarray` | An array of shape (n_samples,) for two classes, and (n_samples, n_classes) otherwise. | #### `fit(X, y)` Fit the classifier on training data. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | | `y` | `ndarray` | The class labels, of shape (n_samples,). | *required* | Returns: | Type | Description | | ------ | --------------------------------------------- | | `QSVC` | This estimator, so that calls can be chained. | #### `predict(X)` Predict the class labels of `X`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | Returns: | Type | Description | | --------- | ------------------------------- | | `ndarray` | One prediction for each sample. | #### `predict_proba(X)` Predict the probability of each class. It requires `probability=True`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | Returns: | Type | Description | | --------- | ----------------------------------------------------------------------------------------------- | | `ndarray` | The probabilities, of shape (n_samples, n_classes), with the columns in the order of classes\_. | Raises: | Type | Description | | ------------ | ------------------------------------------------ | | `ValueError` | If the model was created with probability=False. | ### `eigenkit.qml.QSVR` Bases: `BaseQuantumRegressor` Quantum support vector regressor (QSVR). It is the regression counterpart of QSVC. It uses `SVR(kernel="precomputed")` from scikit-learn on the quantum kernel matrix. Parameters: | Name | Type | Description | Default | | -------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | | `feature_map` | `str` | The feature map. It is "Angle" (angle encoding into a product state) or "AngleCNOT" (angle encoding with ladders of controlled-NOT (CNOT) gates). Default: "Angle". It is ignored when kernel is given. | `'Angle'` | | `reps` | `int` | The number of repetitions of the feature map. Default: 1. It is ignored when kernel is given. | `1` | | `C` | `float` | The regularisation parameter of the support vector regressor. Default: 1.0. | `1.0` | | `epsilon` | `float` | The width of the epsilon-insensitive tube. Default: 0.1. | `0.1` | | `provider` | `ProviderSpec` | The backend. It takes the same options as Circuit(provider=...). Default: "numpy". It is ignored when kernel is given. | `'numpy'` | | `random_state` | \`int | None\` | The seed. | | `kernel` | \`QuantumKernel | None\` | A QuantumKernel to use instead of building one from feature_map, reps and provider. Default: None. | | `scale` | `float` | The bandwidth. The inputs are multiplied by it before they reach the kernel. Default: 1.0. | `1.0` | Example ```python import numpy as np from eigenkit import QSVR rng = np.random.default_rng(0) X_train, X_test = rng.uniform(0, np.pi, (8, 2)), rng.uniform(0, np.pi, (3, 2)) y_train = np.sin(X_train).sum(axis=1) model = QSVR(feature_map="Angle", reps=1) model.fit(X_train, y_train) predictions = model.predict(X_test) ``` #### `__sklearn_is_fitted__()` Return True when the wrapped scikit-learn `SVR` has been fitted. #### `fit(X, y)` Fit the regressor on training data. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | | `y` | `ndarray` | The float targets, of shape (n_samples,). | *required* | Returns: | Type | Description | | ------ | --------------------------------------------- | | `QSVR` | This estimator, so that calls can be chained. | #### `predict(X)` Predict continuous targets for `X`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | Returns: | Type | Description | | --------- | ------------------------------- | | `ndarray` | One prediction for each sample. | ### `eigenkit.qml.QuantumReservoirClassifier` Bases: `_ReservoirMixin`, `BaseQuantumClassifier` Quantum reservoir classifier: a fixed random reservoir and a logistic readout. It uses the same reservoir as QuantumReservoirRegressor. The readout is a scikit-learn `LogisticRegression` on the features of each timestep. `y` holds integer or label targets, of shape `(n_sequences, n_timesteps)`. Parameters: | Name | Type | Description | Default | | -------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | `blocks` | `list` | The architecture of the reservoir circuit, a list of blocks that is assembled once. Its n_inputs must match the policy: F for the policies that carry the state, and window * F for state_policy="stateless", where F is the number of features per timestep. | *required* | | `state_policy` | `str` | "carry" (the unitary of the circuit carries the state from one step to the next), "stateless" (each step starts from | 0…0⟩ and encodes a sliding window of inputs), or "leaky" (the carried state is mixed with a reset state and renormalised). | | `window` | `int` | The length of the sliding window, for state_policy="stateless". | `1` | | `leak` | `float` | The mixing coefficient alpha, for state_policy="leaky". | `1.0` | | `features` | `FeaturesSpec` | The features read from the reservoir. It is "z" (the value of ⟨Z⟩ on each qubit, the default), "probabilities", "amplitudes", a Pauli or Hamiltonian string (for example "0.5 Z0 X1 - 0.3 Y0"), or a list of such strings, to read out any observables. | `'z'` | | `C` | `float` | The inverse regularisation strength of the logistic readout, passed to scikit-learn. Smaller values regularise more. This is the only argument that the regressor does not have: there, the readout is a ridge regression controlled by ridge. | `1.0` | | `washout` | `int` | The number of timesteps dropped from the fit of the readout. They are not dropped from the predictions. | `0` | | `weight_scale` | `float` | The standard deviation of the sampled reservoir weights. | `1.0` | | `provider` | `ProviderSpec` | The backend, which must give access to the statevector. Default: "numpy". | `'numpy'` | | `random_state` | \`int | None\` | The seed of the fixed reservoir weights. | #### `__sklearn_is_fitted__()` Return True when the readout has been fitted. #### `fit(X, y)` Fit the estimator on `X` and `y`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------------------------------------------------- | ---------- | | `X` | `ndarray` | The sequences, of shape (n_sequences, n_timesteps, n_features). A 2-D array is rejected. | *required* | | `y` | `ndarray` | One target for each timestep, of shape (n_sequences, n_timesteps). | *required* | Returns: | Type | Description | | ------------------------------ | --------------------------------------------- | | `'QuantumReservoirClassifier'` | This estimator, so that calls can be chained. | #### `predict(X)` Predict a label for every timestep of every sequence in `X`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------------------------------------------------- | ---------- | | `X` | `ndarray` | The sequences, of shape (n_sequences, n_timesteps, n_features). A 2-D array is rejected. | *required* | Returns: | Type | Description | | --------- | ---------------------------------------------------------- | | `ndarray` | The predicted labels, of shape (n_sequences, n_timesteps). | #### `predict_proba(X)` Predict the class probabilities for every timestep of every sequence in `X`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------------------------------------------------- | ---------- | | `X` | `ndarray` | The sequences, of shape (n_sequences, n_timesteps, n_features). A 2-D array is rejected. | *required* | Returns: | Type | Description | | --------- | ---------------------------------------------------------------------------------------------------------------- | | `ndarray` | The probabilities, of shape (n_sequences, n_timesteps, n_classes), with the last axis in the order of classes\_. | #### `score(X, y)` Score the estimator on `X` against `y`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------------------------------------------------- | ---------- | | `X` | `ndarray` | The sequences, of shape (n_sequences, n_timesteps, n_features). A 2-D array is rejected. | *required* | | `y` | `ndarray` | One target for each timestep, of shape (n_sequences, n_timesteps). | *required* | Returns: | Type | Description | | ------- | ----------------------------------------------------------------------- | | `float` | The accuracy of the predictions, over every timestep of every sequence. | ### `eigenkit.qml.QuantumReservoirRegressor` Bases: `_ReservoirMixin`, `BaseQuantumRegressor` Quantum reservoir regressor: a fixed random reservoir and a ridge readout. Parameters: | Name | Type | Description | Default | | -------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | `blocks` | `list` | The architecture of the reservoir circuit, a list of blocks that is assembled once. Its n_inputs must match the policy: F for the policies that carry the state, and window * F for state_policy="stateless", where F is the number of features per timestep. | *required* | | `state_policy` | `str` | "carry" (the unitary of the circuit carries the state from one step to the next), "stateless" (each step starts from | 0…0⟩ and encodes a sliding window of inputs), or "leaky" (the carried state is mixed with a reset state and renormalised). | | `window` | `int` | The length of the sliding window, for state_policy="stateless". | `1` | | `leak` | `float` | The mixing coefficient alpha, for state_policy="leaky". | `1.0` | | `features` | `FeaturesSpec` | The features read from the reservoir. It is "z" (the value of ⟨Z⟩ on each qubit, the default), "probabilities", "amplitudes", a Pauli or Hamiltonian string (for example "0.5 Z0 X1 - 0.3 Y0"), or a list of such strings, to read out any observables. | `'z'` | | `ridge` | `float` | The strength of the L2 penalty of the linear readout, which is solved in closed form. | `1e-06` | | `washout` | `int` | The number of timesteps dropped from the fit of the readout. They are not dropped from the predictions. | `0` | | `weight_scale` | `float` | The standard deviation of the sampled reservoir weights. | `1.0` | | `provider` | `ProviderSpec` | The backend, which must give access to the statevector. Default: "numpy". | `'numpy'` | | `random_state` | \`int | None\` | The seed of the fixed reservoir weights. | #### `__sklearn_is_fitted__()` Return True when the readout has been fitted. #### `fit(X, y)` Fit the estimator on `X` and `y`. Parameters: | Name | Type | Description | Default | | ---- | --------- | -------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `X` | `ndarray` | The sequences, of shape (n_sequences, n_timesteps, n_features). A 2-D array is rejected. | *required* | | `y` | `ndarray` | One target for each timestep, of shape (n_sequences, n_timesteps), or (n_sequences, n_timesteps, n_outputs) for several outputs. | *required* | Returns: | Type | Description | | ----------------------------- | --------------------------------------------- | | `'QuantumReservoirRegressor'` | This estimator, so that calls can be chained. | #### `predict(X)` Predict a value for every timestep of every sequence in `X`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------------------------------------------------- | ---------- | | `X` | `ndarray` | The sequences, of shape (n_sequences, n_timesteps, n_features). A 2-D array is rejected. | *required* | Returns: | Type | Description | | --------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `ndarray` | The predictions, of shape (n_sequences, n_timesteps), or (n_sequences, n_timesteps, n_outputs) when the targets had several outputs. | #### `score(X, y)` Score the estimator on `X` against `y`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------------------------------------------------- | ---------- | | `X` | `ndarray` | The sequences, of shape (n_sequences, n_timesteps, n_features). A 2-D array is rejected. | *required* | | `y` | `ndarray` | One target for each timestep, of shape (n_sequences, n_timesteps). | *required* | Returns: | Type | Description | | ------- | ---------------------------------------------------------------------------------------------- | | `float` | The coefficient of determination R² of the predictions, over every timestep of every sequence. | # QPU and Cloud Workflows Three backends support real hardware: Braket (IonQ, Rigetti, IQM, AQT and others), Qiskit Runtime (IBM Eagle and Heron), and IQM through the software of IQM itself. All three support calls that wait for the result and calls that do not, inspection before a run, and a warning before anything runs on paid hardware. The hardware devices are called quantum processing units (QPUs). | | Braket | Qiskit Runtime | IQM | | ---------------------------- | ------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | | Registration | Lazy, `provider="braket"`. | Pass a `QiskitRuntimeConfig`. `register("qiskit-runtime", ...)` is needed only for the string name. | Lazy, `provider="iqm"`. | | Hardware reached | IonQ, Rigetti, IQM and AQT, through `device_arn`. | IBM Eagle and Heron, through `backend_name`. | IQM Crystal and Star devices, through `url` and `quantum_computer`. | | Simulator without an account | Yes: `device_arn=None` runs `LocalSimulator`. | No. Use a `FakeBackendV2` through `backend_instance`. | No. Use an `IQMFake*` backend through `backend_instance`. | | Selects the device by | `device_arn`. | `backend_name` (or `"least_busy"`), or a backend object. | `url` and `quantum_computer`, or a backend object. | | Analytic evaluation | With `shots=0`, on simulators only. | Never: `shots` defaults to `1024`. | Never: the hardware only samples. | | Measurement | Expectation result types, and counts and probability result types for `sample()` and `probabilities()`. | `EstimatorV2` or `SamplerV2`, chosen with `primitive=`. | Counts only. The expectation values are computed from them. | | The job handle has | `result()`, `status()`, `cancel()` and `task_id()`. | `result()`, `status()`, `cancel()` and `job_id()`. | `result()`, `status()`, `cancel()`, `job_id()` and `counts()`. | | Extra | `braket` | `qiskit` | `iqm` | There is a second way to reach IQM hardware: a Braket `device_arn` that points at an IQM QPU, which is billed through Amazon Web Services (AWS). The `iqm` provider talks to IQM Resonance directly instead. It is also the only way to use the fake backends of IQM and its Star architectures, which use MOVE gates. The examples below use the **PyTorch** interface (`interface="torch"`). The supported pattern is to train a differentiable `nn.Module` circuit on a simulator, and then to use its weights on the QPU, because QPU backends refuse gradients unless you [turn them on](#gradients-on-hardware). The NumPy interface also works on QPU backends: pass the weights explicitly, with `circuit(weights, x)` or `circuit.submit(weights, x)`. ## Cost warnings The first compilation for a paid backend (a cloud simulator or a QPU) issues a single `CostWarning`. You can catch it or filter it with the standard `warnings` module: ```python import warnings from eigenkit import CostWarning with warnings.catch_warnings(): warnings.simplefilter("ignore", CostWarning) circuit = Circuit(blocks=..., provider=qpu_config, interface="torch") ``` QPU configurations also do the following: - They require `shots > 0`, because analytic evaluation is not possible on hardware. - They set `gradient_method="none"`. Calling `.backward()` on a QPU result, `circuit.jacobian()` or a gradient-based optimiser raises a clear error, which points to the workflow of training on a simulator and then running on the device. This does not happen when the configuration sets `enable_gradients=True` (see [Gradients on hardware](#gradients-on-hardware)). ## Calls that wait for the result `Circuit(...)(x)` works on QPU backends in the same way as on simulators, but it waits until the job is finished: ```python from eigenkit import Circuit, BraketConfig config = BraketConfig( device_arn="arn:aws:braket:us-east-1::device/qpu/ionq/Aria-1", shots=1000, ) circuit = Circuit(blocks=..., provider=config, interface="torch") y = circuit(x) # blocks until the QPU returns ``` ## Submitting without waiting For long jobs, use `circuit.submit(x)`, which returns a `JobHandle` immediately: ```python handle = circuit.submit(x) print(handle.status()) # "QUEUED", "RUNNING", "COMPLETED", ... # Do other work... result = handle.result() # blocks until the job is done ``` `JobHandle.result()` returns the expectation value of each observable, as a NumPy array. The protocol itself declares `result()` and `status()`. The three handles that exist also implement `cancel()`. The Braket handle has `task_id()`, while the Qiskit Runtime and IQM handles have `job_id()`. The IQM handle also has `counts()`, which returns the raw histograms behind the expectation values, one for each measurement group. A custom handle can implement only the two methods that the protocol names. `submit()` accepts only a single 1-D input, and no batches. For a batch, loop in Python. ## Dry runs The backends that can run in the cloud have `Circuit.dry_run_info()`. It returns a dictionary that describes the prepared circuit, without submitting anything. Use it to check the circuit before you submit it: ```python info = circuit.dry_run_info() print(info["n_circuit_runs_per_call"]) ``` The backends return **different keys**. They do not share a common set with additions, and only three keys appear in all of them. | Key | Braket | Qiskit Runtime | IQM | Meaning | | ------------------------- | ------ | -------------- | --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `shots` | ✓ | ✓ | ✓ | The number of shots per circuit run. | | `n_observables` | ✓ | ✓ | ✓ | The number of observables measured. | | `n_circuit_runs_per_call` | ✓ | ✓ | ✓ | **The key number**: how many circuits the device runs for one forward call. | | `device_arn` | ✓ | | | The chosen Braket device, or `None` for the local simulator. | | `local_backend` | ✓ | | | The `LocalSimulator` that runs when `device_arn` is `None`. | | `n_qubits` | ✓ | | | The number of qubits of the circuit. | | `n_gates` | ✓ | | | The number of gates, as built. | | `n_unique_pauli_terms` | ✓ | | | The number of distinct Pauli terms. With shots, they are measured in one task for each group of terms that commute qubit-wise. Exact runs measure them all in one task. | | `is_cloud`, `is_qpu` | ✓ | | | Whether the target is billed, and whether it is hardware. | | `backend_name` | | ✓ | ✓ | The device that the job would go to. | | `is_fake` | | ✓ | | `True` for a `FakeBackendV2`, so nothing is billed. | | `is_local` | | | ✓ | `True` for an `IQMFake*` backend, so nothing is charged. | | `primitive` | | ✓ | | `"estimator"` or `"sampler"`. | | `transpile_mode` | | | ✓ | `"parametric"` or `"per_call"`. | | `n_measurement_groups` | | ✓ | ✓ | The number of groups of observables that commute qubit-wise. This sets the number of circuit runs per call when sampling. | | `optimization_level` | | ✓ | ✓ | The effort of the transpiler that produced the numbers below. | | `n_logical_qubits` | | ✓ | ✓ | The number of qubits before transpilation. | | `n_physical_qubits` | | ✓ | ✓ | The number of qubits of the target device. | | `transpiled_gate_counts` | | ✓ | ✓ | The gates after transpilation, counted by gate name. IQM reports one entry for each measurement basis. | | `transpiled_depth` | | ✓ | ✓ | The depth of the circuit after transpilation. IQM reports one entry for each measurement basis, and leaves out both keys in `"per_call"` mode, which transpiles when the circuit is called. | `n_circuit_runs_per_call` is the key to read before you submit. It is the number of circuits that the device runs for a single forward call. ## Resources after transpilation On Qiskit Runtime and IQM, `Circuit.resources()` reports the resources that the circuit uses on its target once it is transpiled. These are the numbers that decide whether the circuit survives the noise of the device. Nothing is submitted, and a fake `backend_instance` gives the same report locally: ```python from iqm.qiskit_iqm.fake_backends.fake_garnet import IQMFakeGarnet from qiskit_ibm_runtime.fake_provider import FakeFez from eigenkit import Circuit, IQMConfig, QiskitRuntimeConfig for config in ( QiskitRuntimeConfig( backend_instance=FakeFez(), primitive="sampler", optimization_level=3, seed_transpiler=0, ), IQMConfig(backend_instance=IQMFakeGarnet(), optimization_level=3, seed_transpiler=0), ): report = Circuit(blocks=..., provider=config).resources() print( report["n_2q_gates"], report["depth_2q"], report["routing_overhead_2q"], report["estimated_success_probability"], ) ``` | Key | Meaning | | ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `gate_counts` | The native gates after transpilation, including the measurements. | | `n_2q_gates`, `two_qubit_gate_counts` | The two-qubit operations, in total and by name (`cz`, `ecr`, or `move` on IQM). | | `depth`, `depth_2q` | The depth of the circuit, and the depth that counts only the layers of two-qubit gates. | | `n_2q_gates_unrouted` | The same count for the circuit on the native gates of the device, with every qubit connected to every other. | | `routing_overhead_2q` | `n_2q_gates - n_2q_gates_unrouted`, which is the number of two-qubit gates that the coupling map adds. | | `physical_qubits` | Where the layout placed each logical qubit. | | `estimated_success_probability` | The product of `1 - error` over every operation and readout, from the error rates of the target (for the IQM fake backends, from their error profile). Idle time between operations is not counted. On the fake IBM devices, it matched the measured loss of contrast of a kernel within `0.02`. On the IQM fake backends, it overestimates deep circuits: `0.85` against a measured `0.42` for a 4-qubit ZZ kernel with `64` two-qubit gates on Garnet. | `Circuit.transpiled_circuit()` returns the Qiskit circuit itself, for `.draw("mpl")`. Set `seed_transpiler` to compare two configurations fairly, because routing without a seed varies from run to run. Quantum kernels use the same machinery. See [Quantum Kernels, On hardware](https://docs.kit.eigenorbit.com/user-guide/kernels/#on-hardware). A kernel matrix is one job, and `QuantumKernel.submit_kernel_matrix()` and `retrieve_kernel_matrix()` let a long queue finish in another session. ## Sampling: counts, probabilities, and QAOA on hardware Hardware does not return expectation values. It returns a histogram of measured bitstrings. Braket, Qiskit Runtime and IQM give access to this histogram directly: ```python counts = circuit.sample(weights, x) # {"01": 4913, "00": 3020, "11": 1584, "10": 483} ``` In the keys, qubit 0 is the most significant bit, so `"100"` means that qubit 0 is excited and the others are in the ground state. This is the same convention as everywhere else in the library. The counts add up to the `shots` configured for the backend. `circuit.probabilities(weights, x)` turns the same histogram into the full distribution over `2**n_qubits` states. On hardware, it is an *estimate from the frequencies of the shots*, not the exact distribution: basis states that were not sampled come back as exact zeros, without smoothing. It is limited to `24` qubits. Above that, a budget of shots cannot fill a dense distribution anyway, so use `sample()` instead. This is what lets the Quantum Approximate Optimization Algorithm (QAOA) read a solution from real hardware, because it reads its answer from `probabilities()`. The optimisation loop still needs a method without gradients, because hardware backends declare `gradient_method="none"` unless the configuration sets `enable_gradients=True`: ```python from eigenkit.optimization import QAOA from eigenkit import IQMConfig solver = QAOA( layers=1, provider=IQMConfig(url="https://resonance.iqm.tech/", shots=4000), optimizer="COBYLA", ) ``` `optimizer="spsa"` is also accepted. Simultaneous perturbation stochastic approximation (SPSA) estimates the gradient from two evaluations per iteration, whatever the number of parameters, and makes a third evaluation to record the loss. This keeps the number of jobs low on hardware. ### How an expectation value is computed from a histogram When a backend samples, the Pauli terms of the observables are sorted into **measurement groups**. Two terms share a group when no qubit needs two different bases for them, which means that they commute qubit-wise. Each group is one circuit: it rotates the qubits (`H` for `X`, and `Sdg` then `H` for `Y`), measures them, and then computes the value from the parity of the counts. `all_z(4)` is one group, not four circuits, and the `Z` and `ZZ` terms of a QAOA cost Hamiltonian also make one group. `n_measurement_groups` in `dry_run_info()` is this number, and on a sampling backend, `n_circuit_runs_per_call` is equal to it. Set `group_commuting=False` to run one circuit per term instead. This runs more circuits, but each term gets the full budget of shots for itself, which is the point of comparison when you want to attribute the shot noise to each term. ## Recommended workflow: train on a simulator, run on the QPU QPU calls are too expensive for parameter-shift gradients, because one backward pass costs at least `2 * n_weights` jobs. The supported workflow is: 1. Train on a simulator (`provider="numpy"` or `provider="pennylane"`). 1. Save the weights with `torch.save(circuit.state_dict(), ...)`. 1. Build the same circuit with a QPU provider, and load the weights. 1. Run the inference with `circuit(x)` or `circuit.submit(x)`. ```python # Train. sim_circuit = Circuit(blocks=blocks, interface="torch") # ... training loop ... torch.save(sim_circuit.state_dict(), "weights.pt") # Deploy. qpu_circuit = Circuit( blocks=blocks, provider=BraketConfig(device_arn=..., shots=1000), interface="torch", ) qpu_circuit.load_state_dict(torch.load("weights.pt")) y = qpu_circuit(x_test) ``` ## Gradients on hardware To train or fine-tune on the device itself, set `enable_gradients=True` in the device configuration (`BraketConfig`, `QiskitRuntimeConfig` or `IQMConfig`). The backend then reports `gradient_method="parameter_shift"`, and `.backward()`, `circuit.jacobian()` and gradient-based optimisers run the parameter-shift rule on the hardware: ```python qpu_circuit = Circuit( blocks=blocks, provider=IQMConfig( url="https://resonance.iqm.tech/", shots=2000, enable_gradients=True, ), interface="torch", ) loss = qpu_circuit(x).sum() loss.backward() # 2 * n_weights forward calls on the device ``` Compiling for a paid device with this flag set issues a second `CostWarning`, which states how many evaluations one backward pass needs there, for example *"one backward pass costs at least 24 circuits per sample (12 parameter-shift evaluations x 2 circuits each) at shots=2000"*. The unit is the one that the cost warning of the device counts: circuits on IQM, Primitive Unified Blocs (PUBs) on Qiskit Runtime, and tasks on Braket. This figure is a minimum. A batch multiplies it by the batch size, and input gradients, or weights wrapped in `Scale`, `Multiply` or `Sum` angles, add more. Local fake backends issue neither warning. On Braket, the flag matters only for QPU ARNs, because the local and cloud simulators compute gradients in either case. Gradients estimated from shots are noisy: the standard deviation of each entry scales as `1/sqrt(shots)`. If the cost is the concern, SPSA (`optimizer="spsa"`) needs two evaluations per step to estimate the gradient, whatever the number of weights, and it needs no flag. ## Notes for each backend ### Braket - `device_arn=None` runs on the `LocalSimulator`, in the same process. - An Amazon Resource Name (ARN) that contains `/qpu/` (Rigetti, IonQ, IQM, AQT and others) turns on the QPU mode automatically. - Amazon Web Services bills the tasks on its cloud simulators and QPUs. eigenkit does not estimate the price. - The AWS credentials are read from the environment, through the standard boto3 chain. - The `submit()` of the compiled circuit takes two keyword arguments. `method=` chooses what the task computes: `"run"` (the default, which measures the observables), `"sample"`, `"probabilities"` or `"zero_state_probability"` (`BraketCompiled.submit_methods` lists them). Each returns what the waiting method of the same name returns. `tags={...}` puts AWS tags on a cloud task. The tags follow the AWS rules (no key that starts with `aws:`, and only letters, digits, spaces and `_ . : / = + - @`), and they are checked before anything is submitted. The local simulator ignores tags. - `circuit.compiled.attach(task_arn)` builds a `BraketJobHandle` again for a cloud task submitted earlier, for example after the process that submitted it has restarted. Pass the same `method` as the one the task was submitted with. Values with the shape of another method or circuit raise a `ValueError`. - `handle.result()` raises a `RuntimeError`, which names the task and its state, when the task ended as `FAILED` or `CANCELLED`, or when the polling timed out. #### Braket shot rules and result types `sample()` reads the measurement counts of the task, so it needs `shots > 0` everywhere, and a QPU needs a number of shots within the range of the device. In the keys, qubit 0 is the most significant bit, over the whole circuit, and a qubit that no gate acts on reads `0`. `probabilities()` and `zero_state_probability()` read the first result type that the device publishes for the configured shots, in the order `Probability`, `StateVector`, `Amplitude`, `DensityMatrix`. The capabilities of the device (GetDevice) decide, so the choice is fixed for each device and number of shots: | Device | `sample` | `probabilities`, `shots=0` | `probabilities`, `shots > 0` | `zero_state_probability`, `shots=0` | `zero_state_probability`, `shots > 0` | | --------------- | -------- | ----------------------------------------------------------------- | --------------------------------- | -------------------------------------------------------------------------- | ------------------------------------- | | Local simulator | Counts. | `Probability`. | `Probability`. | `Probability`. | `Probability`. | | SV1 | Counts. | The `Amplitude` of every basis state, squared, up to `17` qubits. | `Probability`, up to `15` qubits. | The `Amplitude` of the all-zeros state, squared, for any number of qubits. | `Probability`, up to `15` qubits. | | DM1 | Counts. | The diagonal of the `DensityMatrix`, up to `8` qubits. | `Probability`, up to `15` qubits. | The `[0, 0]` entry of the `DensityMatrix`, up to `8` qubits. | `Probability`, up to `15` qubits. | | QPU | Counts. | Refused, because QPUs need `shots > 0`. | `Probability`, up to `15` qubits. | Refused, because QPUs need `shots > 0`. | `Probability`, up to `15` qubits. | "Counts" means no result type: the task returns its measurement counts, which needs `shots > 0` on every device. SV1 and DM1 publish `Probability` only for `1` to `100,000` shots, so their answers with `shots=0` come from the exact result types instead, and they match the local simulator. With `shots > 0`, `probabilities()` is the estimate from the frequencies of the shots. A device that publishes none of the four result types for the requested shots raises a `ValueError` before anything is submitted. The limits on the number of qubits in the table are the limits of Braket itself. They are checked before anything is submitted, so a wider circuit fails with a `ValueError` and not at AWS: - The managed simulators and QPUs of Braket return `Probability` for at most `15` qubits ([List of result types](https://docs.aws.amazon.com/braket/latest/developerguide/braket-result-types.html)), and both methods read it over every qubit. So every cloud device stops at `15` qubits with `shots > 0`, which is below the limit of `24` qubits of the dense `probabilities()`. On the local simulator, `Probability` is limited only by memory, and `sample()` has no limit anywhere. - The exact `probabilities()` of SV1 names all the `2**n_qubits` basis states inside the program, and Braket limits the program of a quantum task to 5 megabytes ([Amazon Braket quotas](https://docs.aws.amazon.com/braket/latest/developerguide/braket-quotas.html)). The list alone takes 3.0 megabytes at `17` qubits, and 6.3 megabytes at `18`. `zero_state_probability()` names one state, so it has no limit. - DM1 returns a density matrix of at most `8` qubits ([DM1 results](https://docs.aws.amazon.com/braket/latest/developerguide/braket-submit-tasks-simulators.html#braket-simulator-dm1)), and both exact methods need the density matrix over every qubit, which is `4**8`, or `65,536`, entries at the limit. Above the limit of `Probability`, use `sample()` for the histogram, which has no limit on the number of qubits. `zero_state_probability()` is then the count of the all-zeros state divided by the number of shots. Above an exact limit, use `shots > 0` for the estimate from the frequencies of the shots (up to `15` qubits), or `sample()`. ### Qiskit Runtime - The token is looked up in this order: an explicit `token`, then the `QISKIT_IBM_TOKEN` environment variable, then the saved account file. - `channel` defaults to `"ibm_quantum_platform"`. The older `"ibm_quantum"` channel was retired with the old IBM Quantum Platform, and `QiskitRuntimeService` no longer accepts that string. - Pass a `FakeBackendV2` instance through `backend_instance` for tests, in continuous integration or locally. No authentication is needed. - There are two primitives, chosen with `QiskitRuntimeConfig(primitive=...)`. Each call submits one job, whichever primitive is used. - `"estimator"` (the default) uses `EstimatorV2`. Each observable becomes one Primitive Unified Bloc (PUB) in the job, so `n_circuit_runs_per_call` equals `n_observables`. - `"sampler"` uses `SamplerV2`, and computes the expectation values from the raw counts locally. The job holds one PUB for each measurement group, in place of one for each observable, and the budget of shots is explicit. This is what you want when the shot cost of a result is itself what you are measuring. - `sample()` and `probabilities()` go through `SamplerV2`, whichever primitive is configured, because `EstimatorV2` has no bitstrings to return. `primitive` only chooses how the expectation values are obtained. - Both primitives process a batch at once. The PUBs of `EstimatorV2` and `SamplerV2` both accept a `(B, n_params)` array, so a batch of `B` inputs stays one job, with one PUB for each observable (or for each group), in place of one job for each sample. - The `submit()` of the compiled circuit takes two keyword arguments. `method=` chooses what the job computes: `"run"` (the default, which measures the observables), `"sample"` or `"probabilities"` (`QiskitRuntimeCompiled.submit_methods` lists them). Each returns what the waiting method of the same name returns. `"sample"` and `"probabilities"` are one `SamplerV2` job in the `Z` basis, whichever primitive is used. `zero_state_probability` is not a method of `submit()`. Quantum kernels call `submit_zero_state_probabilities()`, which sends a whole batch as one job, and `retrieve_zero_state_probabilities(job_id)` attaches to it again (see [Resources after transpilation](#resources-after-transpilation) and [Quantum Kernels, On hardware](https://docs.kit.eigenorbit.com/user-guide/kernels/#on-hardware)). `tags={...}` puts IBM job tags on the job, each as `"key=value"`, so that you can find it again with `QiskitRuntimeService.jobs(job_tags=[...])`. IBM allows at most `8` tags of at most `86` characters. Keys and values must not be empty, and a key must not contain `=`. The tags are checked before anything is submitted. `submit()` takes one sample: 2-D `weights` or `inputs` raise a `ValueError`. - `circuit.compiled.attach(job_id)` builds a `QiskitRuntimeJobHandle` again for a job submitted earlier, looked up through the `QiskitRuntimeService` of the backend, for example after the process that submitted it has restarted. Pass the same `method` as the one the job was submitted with. Results with the shape of another method or circuit raise a `ValueError`. A fake backend has no service, so `attach()` needs a real IBM backend. ### IQM ```python from eigenkit import IQMConfig config = IQMConfig( url="https://resonance.iqm.tech/", quantum_computer="garnet", # omit to take the server's default shots=2048, ) ``` - The token is looked up in this order: an explicit `token`, then the `IQM_TOKEN` environment variable, then whatever iqm-client finds by itself. The address of the server can come from `IQM_SERVER_URL` in place of `url`. - Pass an `IQMFakeAdonis()`, `IQMFakeApollo()` or `IQMFakeDeneb()` instance through `backend_instance` for tests, in continuous integration or locally. They run on Aer with a model of the noise of IQM, with no token and no network, and the tests of this backend use them. - The service of IQM returns **only counts**. There is no estimator primitive, so every expectation value is computed from a histogram of bitstrings. `shots > 0` is required, and `sample()` and `probabilities()` are fully supported, not added as an afterthought. - The native gates are `r` (an `RX` rotation with a phase) and `cz`. Devices with the Star topology route two-qubit gates through a resonator with `move` gates, which the transpiler adds automatically. - `transpile_mode` balances the cost of compilation against the depth of the circuit: - `"parametric"` (the default) transpiles once for each measurement group, and binds the values at each call. The transpiler cannot merge rotations whose angles it cannot see, so the circuit is longer than it needs to be. A 2-qubit block of an encoding and a variational layer gave `6` `r` gates in parametric mode, against `3` when the values were bound first. - `"per_call"` binds the values first and transpiles at every call. This gives shallower circuits and a better fidelity, at the cost of one transpilation per evaluation. This cost is small compared with the time in the queue, but not compared with a local simulator. - A batch is one job per sample. The measurement groups of a single sample do share a job. Quantum kernels are the exception: `zero_state_probabilities()` binds every row into the one transpiled circuit, and submits them as a single job. `submit_zero_state_probabilities()` returns without waiting for it, and `retrieve_zero_state_probabilities(job_id)` attaches to it again. ## API reference ### `eigenkit.core.provider.JobHandle` Bases: `Protocol` A handle to a job running on a quantum processing unit (QPU) or in the cloud. `compiled.submit(...)` returns it without waiting for the job. Call `.result()` to wait for the job and combine its results. #### `result()` Wait until the job finishes, then return its expectation values. The shape follows the same rule as CompiledCircuit.run(): `(n_obs,)` for a single sample, and `(B, n_obs)` for a batch. #### `status()` Return the state of the job as the provider reports it, without waiting. The strings are the provider's own. For example, Braket returns `"CREATED"`, `"QUEUED"`, `"RUNNING"`, `"COMPLETED"`, `"FAILED"` or `"CANCELLED"`. Compare them within one provider, not across providers. ### `eigenkit.core.provider.CostWarning` Bases: `UserWarning` Warning issued before a submission to a paid quantum backend. A paid backend is a quantum processing unit (QPU) or a cloud simulator that charges for use. The first compilation for such a device issues this warning, so that costs cannot build up without the user noticing. # Quantum Reservoir Computing Quantum reservoir computing (QRC) uses a quantum circuit as a fixed dynamical system, called the **reservoir**, that is never trained. Only a classical model on top of the features it produces, called the **readout**, is trained, and it is inexpensive to train. The weights of the reservoir are sampled once and then frozen, so all of the learning happens in the readout. This page covers `eigenkit.reservoir`, the part of the library that uses only NumPy and needs no other framework. It contains the driver, the state policies, the feature extractors and a ridge-regression readout. Use these building blocks when you want full control, for example to process the features in your own way or to use your own readout from scikit-learn or PyTorch. The scikit-learn-style estimators built on them, `QuantumReservoirClassifier` and `QuantumReservoirRegressor`, are described on the [quantum machine learning (QML)](https://docs.kit.eigenorbit.com/user-guide/qml/index.md) page. Everything relies on `Circuit.statevector(weights, inputs, initial_state=)`. The state is carried from one timestep to the next by reading the statevector out and injecting it back as the initial state of the following step. ## The driver `run_reservoir(circuit, policy, sequences, weights)` evolves the circuit over a batch of sequences under a policy, and returns the trajectory of raw statevectors. The driver does not compute features or fit a readout, so the three layers can be combined freely: the driver first, then a feature extractor, then a readout. ```python import numpy as np from eigenkit import Circuit, Encoding, Variational, all_z from eigenkit.reservoir import ( run_reservoir, LeakyCarry, expectation_features, LinearReadout, ) circuit = Circuit( blocks=[Encoding(in_features=3, rotation="RY"), Variational(depth=3, rotation="RY")], observables=all_z(3), provider="numpy", ) weights = np.random.default_rng(0).standard_normal(circuit.spec.n_weights) sequences = np.random.default_rng(1).uniform(-1, 1, size=(8, 40, 3)) states = run_reservoir( circuit, LeakyCarry(alpha=0.5), sequences, weights ) # (8, 40, 8) complex feats = expectation_features(states, "Z0 + Z1 + 0.5 X0 X1") # (8, 40, 1) readout = LinearReadout(ridge=1e-4) ``` ## State policies A *policy* decides, at each timestep, which state the circuit starts from and which inputs drive it. Two policies carry the quantum state from one timestep to the next, so the timesteps run one after another. The third policy carries no state, so all timesteps can run in parallel. | Policy | Constructor | What it does | Memory | Execution | | ------------ | ------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------- | ---------------------- | | `Stateless` | `Stateless(window=1)` | Starts each step from \` | 0…0⟩`and encodes the last`window\` inputs as rotation angles. | classical (the window) | | `Carry` | `Carry()` | Applies the circuit to the previous state, `ψ_t = U(x_t) ψ_{t-1}`. | quantum state | sequential | | `LeakyCarry` | `LeakyCarry(alpha, reset=None)` | Mixes the carried state with a reset state and renormalises the result. | quantum, fading | sequential | With the carry policies, the circuit takes `n_inputs = F` inputs per step, where `F` is the number of features per step. With `Stateless`, it takes the whole window, so the circuit must encode `n_inputs = window * F`. The driver checks this and raises a clear error otherwise. Fading memory matters A `Carry` policy preserves the norm of the state, but it tends to mix the history into a form that a linear readout cannot recover, so it often has little usable memory. A memory that fades over time is what makes a reservoir work in practice, so `LeakyCarry` with `alpha` between `0.3` and `0.7` is usually the right starting point for temporal tasks. The [memory-capacity example](https://docs.kit.eigenorbit.com/examples/reservoir-memory/index.md) shows this. ## Feature extractors The readout trains on real-valued features extracted from the trajectory of statevectors. Each extractor takes the trajectory returned by the driver and returns real-valued features for each timestep. | Extractor | Features per step | Notes | | ------------------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `pauli_z_features(states)` | `n_qubits` | The expectation value of `Z` on each qubit. This is the usual default in QRC; it is inexpensive and can be measured on hardware. | | `probability_features(states)` | `2**n_qubits` | The measurement distribution over all basis states. It carries more information, and it is quadratic in the amplitudes. | | `amplitude_features(states)` | `2 * 2**n_qubits` | The real and imaginary parts of the state. Only a simulator can provide them; they cannot be measured on hardware. | | `expectation_features(states, observables)` | one per observable | The expectation values of any Pauli observables. Pass the same `Observable` or `PauliExpr` objects, or Hamiltonian strings, that the rest of the library uses. A single Hamiltonian string is one observable. | ## Readout `LinearReadout(ridge=1e-06)` performs ridge regression in closed form, with a bias term and no iterative optimiser. It is only a convenience: the features are a plain NumPy array, so any scikit-learn or PyTorch model can replace it. ## Backends and limitations - Quantum reservoir computing runs on any backend that provides a statevector **and** accepts an injected initial state: NumPy, PennyLane, the Qiskit statevector simulator and the CUDA-Q simulators. Backends without access to the statevector, such as the Braket cloud services and Qiskit Runtime, raise a clear `NotImplementedError`. The default `provider="numpy"` needs no extra dependencies. - The carry policies use state injection, which cannot be differentiated, so the reservoir is **frozen** and only the classical readout is trained. This is the standard setup in QRC, and it avoids computing gradients through the carried state. - `LeakyCarry` approximates a dissipative reservoir with pure states. True dissipation needs density matrices, which the pure-state backends do not provide. The renormalised mixing used here approximates that behaviour, but it is not a physical quantum channel. See the [short-term memory example](https://docs.kit.eigenorbit.com/examples/reservoir-memory/index.md) for a worked benchmark that compares the policies. ## API reference ### `eigenkit.reservoir` Quantum reservoir computing (QRC). A circuit with fixed weights, usually random, is driven over input sequences. The quantum state is carried from one timestep to the next, and classical features are read out from it. The reservoir circuit is never trained; only a classical readout is. The driver, the policies, the feature extractors and the ridge-regression readout use only NumPy. Like `optimization` and `chemistry`, this package belongs to the core of the library, so it needs neither PyTorch nor a backend package. Three policies cover the common variants of QRC: - Stateless starts every step from `|0…0⟩` and encodes a sliding window of inputs. All timesteps run in parallel, in one batched evaluation. - Carry carries the state from one step to the next with the circuit's unitary: `ψ_t = U(x_t) ψ_{t-1}`. - LeakyCarry mixes the carried state with a reset state, and renormalises the result. Example ```python from eigenkit import Circuit from eigenkit.reservoir import ( run_reservoir, Carry, expectation_features, LinearReadout, ) states = run_reservoir(circuit, Carry(), sequences, weights) # (B, T, 2**n) feats = expectation_features(states, "Z0 + Z1 + 0.5 X0 X1") # (B, T, 1) readout = LinearReadout(ridge=1e-4).fit(feats.reshape(B * T, -1), targets) ``` #### `Carry` Policy that applies the circuit to the carried state: `ψ_t = U(x_t) ψ_{t-1}`. The quantum state is the memory of the reservoir, and the first step starts from `|0…0⟩`. This policy is equivalent to `LeakyCarry(alpha=1.0)`. ##### `advance(x_t, evolved, state)` Build the state for the next timestep from the result of this step. Parameters: | Name | Type | Description | Default | | --------- | ---------------- | -------------------------------------------- | ---------- | | `x_t` | `ndarray` | The input of this timestep, of shape (B, F). | *required* | | `evolved` | `ndarray` | The statevector that the circuit produced. | *required* | | `state` | `ReservoirState` | The state that went into this step. | *required* | Returns: | Type | Description | | ---------------- | -------------------------------- | | `ReservoirState` | The state for the next timestep. | ##### `initial(batch)` Return the state carried into the first timestep. Parameters: | Name | Type | Description | Default | | ------- | ----- | --------------------------------------------- | ---------- | | `batch` | `int` | The number of sequences that run in parallel. | *required* | Returns: | Type | Description | | ---------------- | --------------------------- | | `ReservoirState` | The initial ReservoirState. | ##### `inject(x_t, state)` Turn the input of this timestep into the arguments of the circuit call. Parameters: | Name | Type | Description | Default | | ------- | ---------------- | ---------------------------------------------- | ---------- | | `x_t` | `ndarray` | The input of this timestep, of shape (B, F). | *required* | | `state` | `ReservoirState` | The state carried over from the previous step. | *required* | Returns: | Name | Type | Description | | --------------- | --------- | ----------------------------------------------- | | `initial_state` | \`ndarray | None\` | | `gate_inputs` | `ndarray` | The values to pass to the gates of the circuit. | #### `LeakyCarry` Policy that mixes the carried state with a reset state and renormalises it. It approximates a leaky, dissipative reservoir with pure states: `ψ_t = normalise(α U(x_t) ψ_{t-1} + (1 − α) reset)`. `reset` defaults to `|0…0⟩` and should be a normalised statevector; this is not checked. With `alpha=1`, the policy is exactly Carry. This is not a true dissipative channel, which would need density matrices that the pure-state backends do not provide. It is an approximation that uses pure states only. Attributes: | Name | Type | Description | | ------- | --------- | ------------------------------------------------------------------------------------------------------------------------------ | | `alpha` | `float` | The mixing weight of the carried state, in [0, 1]. Lower values forget faster, and 1 keeps everything, which is exactly Carry. | | `reset` | \`ndarray | None\` | ##### `advance(x_t, evolved, state)` Build the state for the next timestep from the result of this step. Parameters: | Name | Type | Description | Default | | --------- | ---------------- | -------------------------------------------- | ---------- | | `x_t` | `ndarray` | The input of this timestep, of shape (B, F). | *required* | | `evolved` | `ndarray` | The statevector that the circuit produced. | *required* | | `state` | `ReservoirState` | The state that went into this step. | *required* | Returns: | Type | Description | | ---------------- | -------------------------------- | | `ReservoirState` | The state for the next timestep. | ##### `initial(batch)` Return the state carried into the first timestep. Parameters: | Name | Type | Description | Default | | ------- | ----- | --------------------------------------------- | ---------- | | `batch` | `int` | The number of sequences that run in parallel. | *required* | Returns: | Type | Description | | ---------------- | --------------------------- | | `ReservoirState` | The initial ReservoirState. | ##### `inject(x_t, state)` Turn the input of this timestep into the arguments of the circuit call. Parameters: | Name | Type | Description | Default | | ------- | ---------------- | ---------------------------------------------- | ---------- | | `x_t` | `ndarray` | The input of this timestep, of shape (B, F). | *required* | | `state` | `ReservoirState` | The state carried over from the previous step. | *required* | Returns: | Name | Type | Description | | --------------- | --------- | ----------------------------------------------- | | `initial_state` | \`ndarray | None\` | | `gate_inputs` | `ndarray` | The values to pass to the gates of the circuit. | #### `LinearReadout` Ridge regression in closed form, with a bias term. It solves `W = (XᵀX + λI)⁻¹ XᵀY`, and the bias column is not regularised. `Y` can have one output or several. `ridge` is the strength `λ` of the L2 penalty, which adds `λ‖W‖²` to the squared error of the fit. A small positive value also keeps the solution numerically stable when the reservoir features are collinear. Parameters: | Name | Type | Description | Default | | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | | `ridge` | `float` | The strength λ of the L2 penalty. Raise it when the readout overfits or the feature matrix is nearly singular, and lower it when the readout underfits. The default is 1e-06. | `1e-06` | ##### `fit(X, y)` Fit the readout to `X` and `y`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ----------------------------------------------------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | | `y` | `ndarray` | The targets, of shape (n_samples,) for one output, or (n_samples, n_outputs) for several. | *required* | Returns: | Type | Description | | ----------------- | ------------------------------------------- | | `'LinearReadout'` | This readout, so that calls can be chained. | ##### `predict(X)` Predict the target for every sample in `X`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | Returns: | Type | Description | | --------- | ----------------------------------------------------------------------------------------------------- | | `ndarray` | One prediction per sample, with one column per output when the readout was fitted on several outputs. | #### `ReservoirPolicy` Bases: `Protocol` A reservoir policy that carries state from one timestep to the next. Parallel policies, which carry no state, do not implement this protocol. They provide `expand_inputs(sequences)`, which returns the inputs of every timestep with shape `(B, T, n_inputs)`, and the driver evaluates all timesteps at once. See Stateless. ##### `advance(x_t, evolved, state)` Build the state for the next timestep from the result of this step. Parameters: | Name | Type | Description | Default | | --------- | ---------------- | -------------------------------------------- | ---------- | | `x_t` | `ndarray` | The input of this timestep, of shape (B, F). | *required* | | `evolved` | `ndarray` | The statevector that the circuit produced. | *required* | | `state` | `ReservoirState` | The state that went into this step. | *required* | Returns: | Type | Description | | ---------------- | -------------------------------- | | `ReservoirState` | The state for the next timestep. | ##### `initial(batch)` Return the state carried into the first timestep. Parameters: | Name | Type | Description | Default | | ------- | ----- | --------------------------------------------- | ---------- | | `batch` | `int` | The number of sequences that run in parallel. | *required* | Returns: | Type | Description | | ---------------- | --------------------------- | | `ReservoirState` | The initial ReservoirState. | ##### `inject(x_t, state)` Turn the input of this timestep into the arguments of the circuit call. Parameters: | Name | Type | Description | Default | | ------- | ---------------- | ---------------------------------------------- | ---------- | | `x_t` | `ndarray` | The input of this timestep, of shape (B, F). | *required* | | `state` | `ReservoirState` | The state carried over from the previous step. | *required* | Returns: | Name | Type | Description | | --------------- | --------- | ----------------------------------------------- | | `initial_state` | \`ndarray | None\` | | `gate_inputs` | `ndarray` | The values to pass to the gates of the circuit. | #### `ReservoirState` The state that sequential policies carry from one timestep to the next. `vector` is the `(B, 2**n)` statevector to inject at the next step, and `None` means that the step starts from `|0…0⟩`. `history` holds the recent raw inputs, of shape `(B, k, F)`, for a sequential policy that encodes a window of inputs. Attributes: | Name | Type | Description | | --------- | --------- | ----------- | | `vector` | \`ndarray | None\` | | `history` | \`ndarray | None\` | #### `Stateless` Parallel policy that carries no state. Every step starts from `|0…0⟩`, and the last `window` inputs are encoded as rotation angles of the circuit's gates. The memory is purely classical (the window), and the quantum state carries nothing from one step to the next. Every timestep is therefore independent, and the driver evaluates all of them in one batched run. With `window=1`, the policy has no memory: it encodes only the current input. The circuit's `n_inputs` must equal `window * F`, where `F` is the number of features per step, because each step passes the whole flattened window to the encoding gates. Attributes: | Name | Type | Description | | -------- | ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `window` | `int` | The number of consecutive inputs to encode at each step. 1 means no memory: only the current input is encoded. Whatever the value, the circuit's n_inputs must equal window * F. The default is 1. | ##### `expand_inputs(sequences)` Return the gate inputs of every timestep, with shape `(B, T, window * F)`. Step `t` encodes the inputs `t - window + 1` to `t`. The first `window - 1` steps are padded with zeros at the front. No quantum state is involved, so this is only a reshaping of the input sequence. Parameters: | Name | Type | Description | Default | | ----------- | --------- | ------------------------------------------ | ---------- | | `sequences` | `ndarray` | The whole input batch, of shape (B, T, F). | *required* | Returns: | Type | Description | | --------- | ---------------------------------------------------------------- | | `ndarray` | The gate inputs for every timestep, of shape (B, T, window * F). | #### `amplitude_features(states)` The raw statevector as real features: its real and imaginary parts, concatenated. `states` is a complex array of shape `(B, T, 2**n)`, and the result is a real array of shape `(B, T, 2 * 2**n)`. It exposes the full state, which only a simulator can provide; it cannot be measured on hardware. Parameters: | Name | Type | Description | Default | | -------- | --------- | -------------------------------------------------------- | ---------- | | `states` | `ndarray` | The trajectory of statevectors, of shape (B, T, 2\*\*n). | *required* | Returns: | Type | Description | | --------- | -------------------------------------------------------------- | | `ndarray` | The real and imaginary parts, concatenated, for each timestep. | #### `expectation_features(states, observables)` Expectation values `⟨ψ|H_k|ψ⟩` of each observable, over the whole trajectory. `states` is a complex array of shape `(B, T, 2**n)`, and the result is a real array of shape `(B, T, n_obs)`. `observables` can be an `Observable`, a `PauliExpr` or a Hamiltonian string, or a sequence of these. These are the same types that the rest of the library uses, so measuring an arbitrary Hamiltonian, as QRC experiments often require, needs no manual handling of Pauli terms. A single Hamiltonian string is one observable, and gives one feature. Parameters: | Name | Type | Description | Default | | ------------- | ---------------------- | -------------------------------------------------------- | ---------- | | `states` | `ndarray` | The trajectory of statevectors, of shape (B, T, 2\*\*n). | *required* | | `observables` | `Sequence[Observable]` | The observables to measure. | *required* | Returns: | Type | Description | | --------- | -------------------------------------------- | | `ndarray` | One feature per observable and per timestep. | #### `pauli_z_features(states)` The expectation value `⟨Z_q⟩` on each qubit, the usual default readout in QRC. `states` has shape `(B, T, 2**n)`, and the result has shape `(B, T, n)`. The values are computed with expectation_features(), so both functions share the same tested measurement code. Parameters: | Name | Type | Description | Default | | -------- | --------- | -------------------------------------------------------- | ---------- | | `states` | `ndarray` | The trajectory of statevectors, of shape (B, T, 2\*\*n). | *required* | Returns: | Type | Description | | --------- | --------------------------------------- | | `ndarray` | One feature per qubit and per timestep. | #### `probability_features(states)` The measurement distribution over all `2**n` basis states, `|ψ|²`. `states` is a complex array of shape `(B, T, 2**n)`, and the result is a real array of the same shape. Parameters: | Name | Type | Description | Default | | -------- | --------- | -------------------------------------------------------- | ---------- | | `states` | `ndarray` | The trajectory of statevectors, of shape (B, T, 2\*\*n). | *required* | Returns: | Type | Description | | --------- | ---------------------------------------------------------- | | `ndarray` | The distribution over the basis states, for each timestep. | #### `run_reservoir(circuit, policy, sequences, weights)` Evolve `circuit` over `sequences` under `policy`, with frozen `weights`. The result is the trajectory of statevectors, a complex array of shape `(B, T, 2**n)`. `sequences` has shape `(B, T, F)`: `B` independent series, `T` timesteps and `F` features per step. `weights` is the fixed 1-D parameter vector of the reservoir, shared by the whole batch. Features are extracted separately from the returned trajectory (see the feature extractors), so the same run can produce expectation, probability or amplitude features without evolving the circuit again. Parameters: | Name | Type | Description | Default | | ----------- | --------- | --------------------------------------------------- | ---------- | | `circuit` | `Circuit` | The reservoir circuit, run with frozen weights. | *required* | | `policy` | `Policy` | How the state passes from one timestep to the next. | *required* | | `sequences` | `ndarray` | The inputs, of shape (B, T, F). | *required* | | `weights` | `ndarray` | The frozen weights of the reservoir. | *required* | Returns: | Type | Description | | --------- | -------------------------------------------------------- | | `ndarray` | The trajectory of statevectors, of shape (B, T, 2\*\*n). | # Reuploading The `Reuploading` block alternates encodings of the data with variational blocks, and repeats this pattern `depth` times within a single circuit. For non-trivial classification and regression tasks, reuploading is usually more expressive than encoding the data only once. ```python from eigenkit import Reuploading reuploading = Reuploading( in_features=4, depth=3, encoding_rotation="RY", variational_rotation="RY", ) ``` ## Two patterns `Reuploading` covers two known patterns: 1. **Standard reuploading** (the default; Pérez-Salinas et al. 2020). The same input is encoded `depth` times, with new variational weights between the encodings. 1. **Trainable feature scaling** (`trainable_scale=True`; Schuld et al. 2021). Each encoding rotation takes the angle `λ * x`, where `λ` is a trainable weight for each pair of layer and feature. At the same depth, this is strictly more expressive than pattern 1. ## Parameters | Parameter | Type | Default | Description | | ---------------------- | --------- | ---------- | ----------------------------------------------------------------------------------------- | | `in_features` | `int` | Required | The total number of input features. | | `depth` | `int` | Required | The number of full repetitions. | | `encoding_rotation` | `"RX"` | `"RY"` | `"RZ"` | | `variational_rotation` | `"RX"` | `"RY"` | `"RZ"` | | `variational_type` | `"basic"` | `"strong"` | `"basic"` | | `trainable_scale` | `bool` | `False` | Whether each encoding rotation gets a trainable scale for each pair of layer and feature. | ## Number of weights | Setup | Trainable weights | | --------------------------- | ------------------------------------------ | | `"basic"`, without a scale | `depth * in_features` | | `"strong"`, without a scale | `3 * depth * in_features` | | With `trainable_scale=True` | `depth * in_features` more, for the scales | ## Trainable scale `trainable_scale=True` adds the parameter expression `Multiply(weight, input)`. The default NumPy simulator differentiates it with its analytic adjoint method. Backends without their own automatic differentiation use the parameter-shift jacobian, which takes the chain rule into account. The gradients agree to machine precision on every backend that supports `Multiply`. CUDA Quantum does not support `Multiply` at the moment. Use a different backend, or set `trainable_scale=False` if you need CUDA Quantum. ## API reference ### `eigenkit.configs.Reuploading` Data reuploading: the input is encoded several times, between trainable layers. The block repeats an encoding of the input followed by a variational layer, `depth` times. Each encoding can have a trainable scale for each feature and repetition. The arguments give two patterns: 1. Standard reuploading (the default; Pérez-Salinas et al. 2020). The same input is encoded `depth` times, with new variational weights between the encodings. 1. Trainable feature scaling (`trainable_scale=True`; Schuld et al. 2021). Each encoding rotation takes the angle `λ_{d,i} * x_i`, where each pair of repetition `d` and feature `i` has its own trainable `λ_{d,i}`. Parameters: | Name | Type | Description | Default | | ---------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `in_features` | `int` | The total number of input features. It must be positive. | *required* | | `depth` | `int` | The number of times the whole sequence is repeated. It must be positive. | *required* | | `encoding_rotation` | `GateName` | The rotation gate of the encoding: "RX", "RY" or "RZ". Default: "RY". | `'RY'` | | `variational_rotation` | `GateName` | The rotation gate of the variational layer: "RX", "RY" or "RZ". It is used only when variational_type="basic". Default: "RY". | `'RY'` | | `variational_type` | `str` | "basic", for one rotation per qubit followed by a ladder of controlled-NOT (CNOT) gates, or "strong", for RZ, RY and RZ rotations on every qubit followed by a ladder of CNOT gates. Default: "basic". | `'basic'` | | `trainable_scale` | `bool` | If True, each encoding rotation gets a trainable scale for each repetition and each feature. The CUDA Quantum backend does not support it, because it allows only limited arithmetic on parameters. Default: False. | `False` | Weight count - With `variational_type="basic"` and no trainable scale, the block has `depth * in_features` weights. - With `variational_type="strong"` and no trainable scale, it has `3 * depth * in_features` weights. - `trainable_scale=True` adds `depth * in_features` weights. #### `build(n_qubits)` Build the gates of this block for a circuit with `n_qubits` qubits. Parameters: | Name | Type | Description | Default | | ---------- | ----- | ------------------------------------------------------------------------------------------------------------------- | ---------- | | `n_qubits` | `int` | The number of qubits of the circuit being assembled, which is the number of qubits of the widest block in the list. | *required* | Returns: | Type | Description | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `IRFragment` | The gates of the block, as an IRFragment. Its weight and input indices start at 0, and they are shifted when the blocks are combined into a circuit. | #### `from_dict(d)` Rebuild a block from the output of `to_dict()`. Parameters: | Name | Type | Description | Default | | ---- | ------ | ----------------------------------- | ---------- | | `d` | `dict` | A dictionary returned by to_dict(). | *required* | Returns: | Type | Description | | --------------- | ------------------------ | | `'Reuploading'` | The rebuilt Reuploading. | #### `required_qubits()` Return `in_features`: one qubit for each feature. #### `to_dict()` Return a description of this block that can be saved as JSON. When a saved model is loaded, the `type` key selects the block class whose `from_dict()` rebuilds the block. Returns: | Type | Description | | ------ | -------------------------------------- | | `dict` | The fields of the block, and its type. | # Variational These blocks are trainable, parametrised layers. There are two kinds: - `Variational` applies rotations about one axis and a ladder of controlled-NOT (CNOT) gates, repeated `depth` times. - `StrongVariational` applies the rotations `RZ`, `RY` and `RZ` and a ladder of CNOT gates, repeated `depth` times. ```python from eigenkit import Variational, StrongVariational basic = Variational(depth=2, rotation="RY") strong = StrongVariational(depth=2) ``` ## `Variational` | Parameter | Type | Default | Description | | ---------- | ------ | -------- | -------------------------- | | `depth` | `int` | Required | The number of repetitions. | | `rotation` | `"RX"` | `"RY"` | `"RZ"` | Each repetition applies one rotation to each qubit, followed by a ladder of CNOT gates (`CNOT(0, 1), CNOT(1, 2), ...`). The total number of trainable weights is `depth * n_qubits`. The block is the middle box: one `RY(θ)` on every qubit, then `CNOT(0, 1), CNOT(1, 2), ...`, and the bracket shows how many times this repeats. The encoding on the left and the measurement of each qubit on the right are not part of the block. A layer such as [`Basic`](https://docs.kit.eigenorbit.com/user-guide/layers/#basic) adds them around it. ## `StrongVariational` | Parameter | Type | Default | Description | | --------- | ----- | -------- | -------------------------- | | `depth` | `int` | Required | The number of repetitions. | Each repetition applies `RZ`, `RY` and `RZ` to every qubit, followed by a ladder of CNOT gates. The total number of trainable weights is `3 * depth * n_qubits`. The same picture with `strong` in place of `basic`, on two qubits. Each `Rot` box is the three rotations `RZ`, `RY` and `RZ`, so it has three weights where `Variational` has one, and the CNOT ladder after it is unchanged. It is drawn after an `RY` encoding, with a single expectation value measured on the first qubit. `StrongVariational` is more expressive than `Variational` at the same depth, but it has three times as many parameters. The block adds all the `RZ` gates, then all the `RY` gates, then all the `RZ` gates, in place of three rotations per qubit in turn. This lets the backends that work layer by layer apply each layer of rotations in one fast call. ## Choosing the depth Each repetition uses a new set of trainable weights. A larger depth gives more expressivity, but it adds parameters and makes training slower. ```python # Minimal expressivity, fast training. Variational(depth=1, rotation="RY") # More expressive; useful for harder classification tasks. Variational(depth=3, rotation="RY") ``` `depth` must be positive Both blocks check it when they are created: `Variational(depth=0)` and `StrongVariational(depth=0)` raise `ValueError: depth must be positive, got 0`, and so does any negative value. `Variational` also checks `rotation` at that point. `Reuploading` makes the same checks on `depth`, `in_features` and both of its rotation arguments. ## API reference ### `eigenkit.configs.Variational` `depth` trainable layers of single-qubit rotations and entangling gates. Each layer applies one trainable rotation to every qubit, then a controlled-NOT (CNOT) gate from each qubit to the next one. The block takes `depth * n_qubits` weights and no inputs. Parameters: | Name | Type | Description | Default | | ---------- | ---------- | -------------------------------------------------------------------------------- | ---------- | | `depth` | `int` | The number of layers. It must be positive, and a ValueError is raised otherwise. | *required* | | `rotation` | `GateName` | The rotation gate of the trainable layer: "RX", "RY" or "RZ". Default: "RY". | `'RY'` | #### `build(n_qubits)` Build the gates of this block for a circuit with `n_qubits` qubits. The layers act on every qubit of the circuit. Parameters: | Name | Type | Description | Default | | ---------- | ----- | ------------------------------------------------------------------------------------------------------------------- | ---------- | | `n_qubits` | `int` | The number of qubits of the circuit being assembled, which is the number of qubits of the widest block in the list. | *required* | Returns: | Type | Description | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `IRFragment` | The gates of the block, as an IRFragment. Its weight and input indices start at 0, and they are shifted when the blocks are combined into a circuit. | #### `from_dict(d)` Rebuild a block from the output of `to_dict()`. Parameters: | Name | Type | Description | Default | | ---- | ------ | ----------------------------------- | ---------- | | `d` | `dict` | A dictionary returned by to_dict(). | *required* | Returns: | Type | Description | | -------------------- | ----------------------------- | | `'BasicVariational'` | The rebuilt BasicVariational. | #### `required_qubits()` Return `1`, because the block extends to all the qubits of the circuit. #### `to_dict()` Return a description of this block that can be saved as JSON. When a saved model is loaded, the `type` key selects the block class whose `from_dict()` rebuilds the block. Returns: | Type | Description | | ------ | -------------------------------------- | | `dict` | The fields of the block, and its type. | ### `eigenkit.configs.StrongVariational` `depth` trainable layers of `RZ`, `RY` and `RZ` rotations and entangling gates. Each layer applies the rotations `RZ`, `RY` and `RZ` to every qubit, then a controlled-NOT (CNOT) gate from each qubit to the next one. The block is modelled on PennyLane's `StronglyEntanglingLayers`. The gates are added one layer at a time: all the `RZ` rotations, then all the `RY` rotations, then all the `RZ` rotations, then the CNOT gates. They are not added as three rotations per qubit. The result is the same, because rotations on different qubits commute. The layered order lets the backends that support it apply each layer of rotations in one fast call, in place of `3 * n_qubits` calls on single qubits. The block takes `3 * depth * n_qubits` weights and no inputs. Parameters: | Name | Type | Description | Default | | ------- | ----- | -------------------------------------------------------------------------------- | ---------- | | `depth` | `int` | The number of layers. It must be positive, and a ValueError is raised otherwise. | *required* | #### `build(n_qubits)` Build the gates of this block for a circuit with `n_qubits` qubits. The layers act on every qubit of the circuit. Parameters: | Name | Type | Description | Default | | ---------- | ----- | ------------------------------------------------------------------------------------------------------------------- | ---------- | | `n_qubits` | `int` | The number of qubits of the circuit being assembled, which is the number of qubits of the widest block in the list. | *required* | Returns: | Type | Description | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `IRFragment` | The gates of the block, as an IRFragment. Its weight and input indices start at 0, and they are shifted when the blocks are combined into a circuit. | #### `from_dict(d)` Rebuild a block from the output of `to_dict()`. Parameters: | Name | Type | Description | Default | | ---- | ------ | ----------------------------------- | ---------- | | `d` | `dict` | A dictionary returned by to_dict(). | *required* | Returns: | Type | Description | | --------------------- | ------------------------------ | | `'StrongVariational'` | The rebuilt StrongVariational. | #### `required_qubits()` Return `1`, because the block extends to all the qubits of the circuit. #### `to_dict()` Return a description of this block that can be saved as JSON. When a saved model is loaded, the `type` key selects the block class whose `from_dict()` rebuilds the block. Returns: | Type | Description | | ------ | -------------------------------------- | | `dict` | The fields of the block, and its type. | # Algorithms Explained # Algorithms explained These pages explain how each algorithm in eigenkit works: what happens between the data that you put in and the result that it gives back. For the interface itself (the arguments of the constructors, the return types, saving a model), see the [Estimators guide](https://docs.kit.eigenorbit.com/user-guide/estimators/index.md) and the API Reference, which is generated from the code. | Page | What it computes | What it gives back | | ------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | | [IQAE](https://docs.kit.eigenorbit.com/algorithms/iqae/index.md) | An unknown probability amplitude `a`, to a target precision. | A number and a confidence interval. | | [HHL](https://docs.kit.eigenorbit.com/algorithms/hhl/index.md) | The solution `x` of a linear system `Ax = b`, with a fast `"reference"` mode and an `"honest"` mode that is faithful to hardware. | A classical solution vector. | | [QFT](https://docs.kit.eigenorbit.com/algorithms/qft/index.md) | The Fourier transform of the amplitudes of a register. | A change of basis that other algorithms read from. | | [QAOA](https://docs.kit.eigenorbit.com/algorithms/qaoa/index.md) | A good bitstring for a combinatorial problem. | A bitstring and its cost. | | [VQE](https://docs.kit.eigenorbit.com/algorithms/vqe/index.md) | The lowest energy that a molecule or a model Hamiltonian can have. | An energy, and the angles that reached it. | | [QSVC / QSVR](https://docs.kit.eigenorbit.com/algorithms/qsvc-qsvr/index.md) | A similarity (kernel) matrix for a support vector model. | scikit-learn predictions. | | [HQNN](https://docs.kit.eigenorbit.com/algorithms/hqnn/index.md) | A trained hybrid quantum-classical neural network. | Labels, probabilities or values. | | [Reservoir computing](https://docs.kit.eigenorbit.com/algorithms/reservoir/index.md) | Features from a circuit that nobody trains. | A linear readout over a trajectory of states. | The names in the first column are acronyms, and each page defines its own: iterative quantum amplitude estimation (IQAE), the Harrow-Hassidim-Lloyd algorithm (HHL), the quantum Fourier transform (QFT), the Quantum Approximate Optimization Algorithm (QAOA), the Variational Quantum Eigensolver (VQE), the quantum support vector classifier and regressor (QSVC and QSVR), and hybrid quantum neural networks (HQNN). ## Glossary These terms come up on several pages. Each is also defined where it first appears. - **Qubit**: the quantum version of a bit. `n` qubits describe a state with `2ⁿ` complex amplitudes. - **Statevector**: the full list of these `2ⁿ` amplitudes. - **Observable**: a quantity that can be measured, here a Hermitian sum of the Pauli operators `X`, `Y` and `Z`. Its **expectation value** `⟨O⟩` is the average outcome over many measurements, a real number that is often in `[-1, 1]`. - **Ansatz**: a circuit template with free (trainable) parameters, such as a `Variational` block. "Variational" means that these parameters are tuned by optimisation. - **Amplitude amplification, or Grover operator**: a unitary that repeatedly rotates probability towards a chosen "good" subspace, so that a property becomes easier to read. It is the engine of [IQAE](https://docs.kit.eigenorbit.com/algorithms/iqae/index.md). - **Quantum phase estimation (QPE)**: a part of a circuit that reads the eigenvalue of a unitary into a register of "clock" qubits. It is the engine of [HHL](https://docs.kit.eigenorbit.com/algorithms/hhl/index.md). - **Hamiltonian simulation, or Trotterisation**: building the evolution `e^{iAt}` from elementary gates, by splitting `A` into a sum of simple terms and interleaving their evolutions over short times. The `"honest"` mode of HHL uses it. More `trotter_steps` lower the approximation error, and `trotter_steps="auto"` chooses a number of steps that keeps this error below the resolution of the clock register. The error is not only in the eigenvalues: a coarse grid mostly rotates the *eigenvectors*, which changes the direction of the answer while the spectrum still looks correct. - **Gershgorin bound**: a cheap upper bound on the largest magnitude of the eigenvalues of a matrix, which costs `O(nnz)`: the largest absolute row sum. The honest mode of HHL uses it to choose the evolution time without diagonalising `A`. - **Condition number** `κ`: for a matrix, the ratio of its largest to its smallest eigenvalue, in absolute value. A large `κ` means that the system is hard to solve accurately. - **Feature map**: a circuit that embeds a classical sample `x` into a quantum state `|φ(x)⟩`. It is the basis of [quantum kernels](https://docs.kit.eigenorbit.com/algorithms/qsvc-qsvr/index.md). - **Angle encoding**: the usual feature map here. One input value becomes one rotation angle, so `n` features need `n` qubits. This is what the `Encoding` block does. - **Amplitude encoding**: the other way to load data. A vector of `2ⁿ` numbers is written directly into the amplitudes of a state of `n` qubits, so `n` qubits hold exponentially more values than with angle encoding. The vector is padded to the next power of two and scaled to a norm of `1`, because a statevector must have a norm of `1`. It is cheap in qubits and expensive in gates: preparing an arbitrary state is not an operation of fixed depth, which is why the `AmplitudeEncoding` block adds one gate to the intermediate representation and leaves each backend to use its own primitive for state preparation. - **Kernel, or Gram matrix**: a table of the similarities `K(x_i, x_j)` between pairs of samples. Classical support vector machines train directly on this table. - **Barren plateau**: a failure where the training gradients of a circuit shrink exponentially as the circuit gets wider or deeper, so it stops learning. It is a risk for the [HQNN](https://docs.kit.eigenorbit.com/algorithms/hqnn/index.md) family. # HHL: solving a linear system ## The problem Given a square matrix `A` and a vector `b`, find `x` such that `Ax = b`. This is a standard numerical task. The original [Harrow-Hassidim-Lloyd paper](https://arxiv.org/abs/0811.3171), which gives the algorithm its name (HHL), prepares a quantum state `|x⟩` proportional to `A⁻¹|b⟩`. Reading the full vector back as numbers would remove the speedup. The speedup also assumes that the matrix is sparse and well conditioned, and that it can be accessed efficiently through an oracle. The [primer by Dervovic et al.](https://arxiv.org/abs/1802.08227) is a short reference for these assumptions. In eigenkit, HHL is a solver for an exact simulator. It builds the structure of the HHL circuit, reads the full statevector, and returns a classical NumPy vector. ## Input and output `HHL(...).solve(A, b)` takes a square invertible matrix and a vector that is not zero. The settings are: - `n_eigenvalue_qubits` is the number of clock qubits used to estimate eigenvalues. It is the main setting for resolution. - `mode` is `"reference"` (the default) or `"honest"` (see below). - `trotter_steps`, `trotter_order` and `trotter_tolerance` set the accuracy of the Trotter product, in `"honest"` mode only. The defaults (`"auto"`, `2` and `0.25`) are safe. See [Choosing the Trotter grid](#choosing-the-trotter-grid). It returns a classical solution vector, its norm, the residual, and two post-selection probabilities. The first, `success_probability`, is the probability that the ancilla is in `|1⟩`, summed over all states of the clock. The second, `readout_probability`, is the probability of the joint event `ancilla = 1 and clock = 0`, which is the event that the solution is actually read from. Use the second one to plan the number of shots. It is never the larger of the two. Across the systems in the [worked example](https://docs.kit.eigenorbit.com/examples/hhl-honest/index.md), the ratio between them goes from `1.0` to `1.8`. It is exactly `1.0` when every eigenvalue lies on the grid of buckets, because then no clock state other than `|0…0⟩` carries any weight. ## How it works HHL works in the **eigenbasis** of `A`. If `A` has eigenvalues `λ` and eigenvectors `|u⟩`, solving `Ax = b` means multiplying each eigen-component of `b` by `1/λ`. The circuit does this in five steps: 1. It makes `A` Hermitian. A non-Hermitian system is converted to `A†Ax = A†b`. 1. It loads the normalised vector `b` as amplitudes. 1. It uses quantum phase estimation (QPE) to write information about the eigenvalues into the clock qubits. This is the step where the Hamiltonian evolution `e^{iAt}` is applied. 1. It rotates an ancilla by an angle that encodes the reciprocal `1/λ`. This is a controlled `RY` rotation whose angle is a fixed function of the clock register. No classical division happens in this step, in either mode. 1. It uncomputes the clock, post-selects the ancilla on `1`, and rescales the readout to physical units. Step 3 is the step that differs between the two modes. ## Reference and honest mode The `mode` argument selects how the circuit is built. Both modes solve the same system, but they do not return the same numbers: `"reference"` mode is exact up to the resolution of the clock, and `"honest"` mode adds a Trotter error on top of that. The difference between them is whether the circuit could run on hardware. | | `"reference"` (default) | `"honest"` | | ------------------------------------------------ | ---------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | | Eigenvalues | They are found classically with `eigvalsh`, at a cost of `O(N³)`. | They are not computed. | | Evolution `e^{iAt}` | It is a dense matrix from `expm`, applied as one `QubitUnitary` block. | It is Trotterised into elementary gates (`HamiltonianQPEBlock`). | | Evolution time `t` | It is tuned to the known spectrum. | It is fixed from a Gershgorin bound on `‖A‖`. | | Rotation constant `C` | It is the true `λ_min`. | It is the resolution of the clock register, `2π/(t · 2^n_l)`, where `n_l` is the number of clock qubits. | | The evolution can be built from elementary gates | No, it is a unitary given only as a matrix. | Yes, it uses elementary gates only. | | Dense unitary gate in the circuit | There is one. | There is none. | | Extra settings | There are none. | They are `trotter_steps`, `trotter_order` and `trotter_tolerance`. | `"reference"` mode is fast and exact. However, it does `O(N³)` classical work, and it applies a `2ⁿ × 2ⁿ` unitary given only as a matrix, with no decomposition into elementary gates, so it gives no quantum speedup. Use it to validate results and for quick experiments. `"honest"` mode removes the three shortcuts that set the parameters of the circuit. It decomposes `A` into a sum of Pauli strings, and simulates `e^{iAt}` with a Trotter product built only from `H`, `RX`, `RZ`, controlled-NOT (`CNOT`), `CPHASE`, `SWAP` and `PhaseShift` gates. The evolution time comes from a Gershgorin bound, which costs `O(nnz)`, where `nnz` is the number of non-zero entries of `A`. The rotation constant comes from the resolution of the clock. The condition number `κ` is never an input. It only bounds the success probability that can be reached. `"honest"` mode needs `n_eigenvalue_qubits >= 2`, so that the clock register is two-sided and can resolve the sign of an eigenvalue. It provides `trotter_steps`, `trotter_order` and `trotter_tolerance` to trade circuit depth for accuracy. Two things stay classical in `"honest"` mode: - **Input validation** runs one singular value decomposition (SVD) of `A`, at a cost of `O(N³)`. It rejects a singular matrix with a clear message, and it gives a condition number for the warnings. No spectral quantity from it reaches `t`, `C`, the rotation angles or the number of Trotter steps, and changing it leaves the circuit byte-identical. Even so, `solve()` itself still does `O(N³)` work. The SVD takes 1.2 ms at `N = 128`, compared with 20 ms for the Pauli decomposition. - **The inversion of the eigenvalues uses a table of angles**, with one entry for each clock bucket. The gates are elementary: a uniformly controlled rotation ([Möttönen et al.](https://arxiv.org/abs/quant-ph/0407010)) adds exactly `2^n_l` `RY` gates and `2^n_l` `CNOT` gates, with no ancillas and no multi-controlled gate. The assembled circuit in `"honest"` mode therefore contains no unitary-matrix gate of any kind. The `2^n_l` angles are listed bucket by bucket, so the classical description stays of size `Θ(2^n_l)`. The circuit can be built from elementary gates, but its description is not compact. ```python import numpy as np from eigenkit import HHL # A = I + X + Z. X and Z anticommute, so the Trotter product is really # approximate and "auto" has real work to do. (The tempting [[2,1],[1,2]] is 2I + X: # one non-identity term, so its Trotterisation is exact and trotter_steps is a no-op.) A = np.array([[2.0, 1.0], [1.0, 0.0]]) b = np.array([1.0, 1.0]) # hardware-faithful path; trotter_steps/order default to "auto" and 2 result = HHL(n_eigenvalue_qubits=4, mode="honest", provider="numpy").solve(A, b) print(result.solution) # x ≈ A⁻¹b print(result.residual) # ‖Ax − b‖ / ‖b‖ print(result.readout_probability) # the post-selection rate a device would see ``` This example uses four clock qubits, and not more. This system reaches a residual of `0.012` at `n_l = 4` and `0.020` at `n_l = 6`, while the post-selection rate falls from one preparation in six to one in 99. Each extra clock qubit costs about four times more shots, for a gain that the residual does not always deliver. The register to choose is therefore the smallest one that passes the resolution check. See [Linear systems with HHL](https://docs.kit.eigenorbit.com/examples/hhl-honest/index.md), which chooses it from the residual instead of by hand, and explains the rest. ## Choosing the Trotter grid The Trotter step size `τ = t / trotter_steps` is **fixed** for every controlled power. QPE is therefore an *exact* phase estimation of the unitary `W` of a single step, and the whole circuit is ideal HHL for an effective Hamiltonian `A_eff := log(W) / (iτ)`. The whole Trotter error is this one change of spectrum, `A → A_eff`, and it falls mostly on the **eigenvectors**, not on the eigenvalues. This is why a coarse grid is dangerous: an error in the eigenvectors cannot be seen by the alignment of buckets, by the Nyquist guard bands, or by the Gershgorin bound. Take the Heisenberg Hamiltonian `XX + YY + ZZ + ZI + IZ`, with `κ = 3` and five Pauli terms, which is the favourable case for this method. A single first-order Trotter step reproduces its spectrum to `1e-15` at every `n_l`, but rotates the eigenvectors by 59°. At some register sizes this puts two of the four signs of the answer wrong. The second way to fail is a drift of an eigenvalue across the Nyquist bucket, whose sign is ambiguous. This flips the sign of `1/λ` for the extreme eigenvalue. `trotter_steps="auto"`, the default, prevents both. It bounds `‖A_eff − A‖` from the Pauli coefficients alone. It uses the standard leading-order bounds for product formulas, in the commutator-scaling form of [Childs et al. 2021](https://arxiv.org/abs/1912.08854): `τ Γ₁` at order 1 and `τ² Γ₂` at order 2, written for Pauli terms, where every commutator norm is `0` or `2`. It then picks the smallest number of steps that keeps the bound below `trotter_tolerance × C`, which is a quarter of the eigenvalue resolution of the clock register. The bound costs `O(L²)`, where `L` is the number of Pauli terms, and the test suite checks it against `‖A_eff − A‖` computed exactly. On the Heisenberg example above, with `b` a vector of ones, the default grid returns a solution whose cosine with the exact answer is `0.9999` or more. Several things follow from this: - **The cost is real.** To keep the drift below the resolution, the number of steps must grow as `steps ∝ √(2^n_l)` at `trotter_order=2`, so the Trotter part of the circuit grows as `Θ(2^(1.5 n_l))`, that is, `Θ(κ^1.5)`. At `trotter_order=1`, the number of steps grows as `steps ∝ 2^n_l` and the cost as `Θ(4^n_l)`. The automatic choice is limited by a gate budget. When the limit applies, the solver warns and gives the number of steps that the criterion asked for. - **`trotter_order=2` is the default for a structural reason.** The symmetric product is a palindrome, so the first-order commutator cancels, and `A_eff` stays real for a real `A`. At order 1, `A_eff` picks up `Y`-type terms, and a real system returns a complex state. Both orders uncompute exactly: the inverse QPE ladder applies the first-order factors in reverse order. This is what makes it the adjoint of the forward pass, and not only its product for a negative time. - **An explicit integer turns off the automatic choice.** The solver then warns when the drift bound exceeds `C/2`, the threshold at which the Nyquist bucket can capture an eigenvalue. - **Real inputs converge one order faster at `trotter_order=1`.** The readout discards the imaginary part, and at order 1 the leading error in the eigenvectors is exactly what this step removes. A real `A` and `b` therefore converge as `steps⁻²`, while a complex Hermitian `A` or a complex `b` keeps that error and converges as `steps⁻¹`. At `trotter_order=2`, the first-order commutator has already cancelled, so discarding the imaginary part has nothing left to remove, and both converge as `steps⁻²`. The default is therefore the order where this difference does not appear, not the order that causes it. - **The order of the terms matters, and it is fixed.** The first-order Trotter error depends on the order in which the Pauli factors are applied, which is the order that `pauli_decompose` returns. A regression test fixes this order, because changing it would change every result in `"honest"` mode at a fixed `trotter_steps`. ## Performance notes The timings come from one developer workstation and are only indicative. The ratios between them are the part that lasts. The Pauli decomposition runs once per call to `solve()` and costs `O(N² log N)`. A Pauli string has exactly one non-zero entry per row, at column `r ⊕ x`, so the Hilbert-Schmidt trace reduces to a single sum over rows. That sum, taken over every `z` at once for a fixed `x`, is a Walsh-Hadamard transform of the diagonal of `A` shifted by `x`. On a dense complex Hermitian matrix, it takes 0.3, 1.0, 4.3, 20 and 119 ms at dimensions 16, 32, 64, 128 and 256, and 1.6 s at `N = 1024`. The `O(N³)` SVD in input validation is of the same order, 1.2 ms at `N = 128`. The cost that limits the method is the circuit depth, `#terms × steps × 2^n_l`. This is the real exponential cost, and no optimisation can remove it. Because the exponential cost is in the circuit *depth*, and not in the dimension of the Hilbert space, `lightning.gpu` runs about 20 % slower than `lightning.qubit` at every size tried. The number of qubits, `1 + n_l + n_b`, stays small (13 qubits, or 8192 amplitudes, in the largest useful run), while the number of gates reaches hundreds of thousands. The time is therefore spent on the overhead of launching kernels, not on arithmetic. A graphics processing unit (GPU) starts to be faster at about 20 qubits, which needs `n_b ≈ 10`. At that size the decomposition costs 1.6 s, and the circuit depth is the real obstacle. ## Guarantees and limits For an invertible `A` on a statevector backend, you get a classical `x` read from the post-selected state of the circuit. The accuracy is measured, not proven: increasing `n_eigenvalue_qubits` lowers the residual **provided that the Trotter grid keeps up**, which is what `trotter_steps="auto"` is for. On a fixed coarse grid the pattern can reverse, because `t` grows with `N`, and so does `τ = t / steps`. A small irregular variation in the residual remains by construction, even on a fine grid. The projection on `clock = |0⟩` weights each eigenvalue by how well it fits the grid of buckets, and this pattern changes as eigenvalues move on and off the grid when `n_l` changes. Two limits are separate, and both must be respected. The **register** must satisfy `2^n_l ≳ 2κ + 2`, because a two-sided register resolves `κ` only up to `(N - 2)/2`, and a one-sided register up to `N - 1`. Below that, the smallest eigenvalue falls into bucket `0`, which has no rotation, and its component, which carries the largest weight `1/λ`, is reduced or lost. The **Trotter grid** must keep the drift below the resolution. Raising `n_eigenvalue_qubits` alone fixes the first limit and makes the second worse. `"auto"` handles the second limit for you. The readout is the main caveat. HHL natively produces the quantum *state* `|x⟩`. The full classical vector is recovered from the statevector of the simulator, which is only possible on a simulator. On real hardware, only expectation values `⟨x|M|x⟩` are available, and reading all `N` components would take `O(N)` measurements, which cancels the speedup. On hardware, HHL is used to pass `|x⟩` to a later quantum routine, or to estimate a single observable. In addition, the returned vector is **completed by two classical steps** that a device cannot perform. For real inputs, the imaginary part is discarded. The magnitude *and the sign* come from a least-squares projection, `x = r ⟨Ar, b⟩ / ‖Ar‖²`, which uses the full classical `A` and `b`. Only the *direction* of the raw post-selected vector comes from the quantum circuit. The rescaling can improve the reported residual by more than an order of magnitude, and for an indefinite `A` its correction of the sign is not reliable, because `⟨Ax, b⟩ ≥ 0` does not imply `⟨x, A⁻¹b⟩ ≥ 0`. Read `residual` as the quality of the best classical rescaling of a quantum direction. The unit-norm complex vector that the circuit actually produced, before either classical step, is returned as `raw_solution`, so you can check all of this yourself. **Scope of the speedup.** HHL is efficient only for an `A` that is **a short sum of Pauli terms and well conditioned**, with an efficient way to prepare the state for `b`. The cost that matters is the number of Pauli terms, and this number depends on sparsity **in the Pauli basis**, which is *not* the same property as sparsity of the matrix: | `A` | Non-zero entries per row | Terms at `N = 128` | | ------------------------------------------------------------------ | ------------------------ | ------------------ | | `diag(1..N)`, the Ising model `Σ ZZ + Σ X`, and local spin chains. | From 1 to 3. | `O(n)` ✅. | | A Toeplitz tridiagonal matrix. | 3. | `N = 128`. | | A real symmetric tridiagonal matrix, with variable coefficients. | 3. | `576 = Θ(n 2ⁿ)`. | | **A random diagonal matrix.** | **1.** | `128 = Θ(N)`. | | **The tridiagonal cycle, with its vertices relabelled at random.** | **3.** | **5440.** | | A random dense real symmetric matrix. | 128. | 8256. | | A random dense complex Hermitian matrix. | 128. | `16384 = 4ⁿ`. | Two rows show the main point. A random *diagonal* matrix, which is as sparse as an invertible matrix can be, needs `N` terms, because the coefficients of the `Z` strings are the Walsh-Hadamard transform of the diagonal, and this transform is dense for a generic diagonal. Also, the number of non-zero entries is not the right variable: a tridiagonal matrix and a randomly relabelled cycle graph both have three non-zero entries per row, but one needs 128 terms and the other needs 5440, which is 66 % of a fully dense matrix. A non-zero entry at `(i, j)` contributes to the Pauli strings whose `X`/`Y` support is the bit pattern `i ⊕ j`, so the number of terms follows the number of *distinct* offsets `i ⊕ j`. A band has only the `log₂ N` patterns `2^k - 1`, which is why the real tridiagonal row with variable coefficients above costs `576 = 64 · 7 + 128` at `n = 7`, while a scattered pattern uses them all. For generic coefficients, the number of terms is exactly `2^(n-1)` for each distinct non-zero offset when `A` is real symmetric, or `2ⁿ` when `A` is complex Hermitian, plus the number of non-zero Walsh-Hadamard coefficients of the diagonal. This predicts every row of the table. So "sparse means cheap" is false here. What holds is "built as a short Pauli sum means cheap". The efficiency assumption of the 2009 HHL paper uses a **different input model**: the matrix is `s`-sparse, its rows can be computed efficiently, and it is simulated through a sparse-access oracle. That cost analysis does not carry over to a decomposition into Pauli terms or into a linear combination of unitaries. `"reference"` mode hides this cost inside `expm`, so it stays fast in simulation but does not represent hardware. Non-Hermitian inputs go through the normal equations, which square the condition number and can reduce accuracy. `"honest"` mode knows that the converted system is positive semidefinite, and uses the whole phase window for it. This is worth about one extra bit of eigenvalue resolution, and was measured as a clear gain in accuracy (38 of 75 A/B pairs better, and 4 worse). The cost is a post-selection rate 2 to 4 times lower, and about twice the Trotter depth, because the ancilla amplitude is `C/λ` and this choice halves `C`. Eigen-directions below the `epsilon` cutoff are removed before the circuit is built, and `"honest"` mode warns when this happens. With the default `ε = 0.01`, it starts to remove buckets at `n_l ≥ 8`, and these are the buckets with the largest weights `1/λ`. If post-selection is almost impossible, the solver returns a zero vector with `residual = 1.0`. A rough answer (`‖Ax - b‖ / ‖b‖ > 0.25`) comes back with a warning. **Noisy backends are not supported.** The solution is read from a slice of the amplitudes, which a mixed state does not provide, so density-matrix devices (for example PennyLane `default.mixed`) raise `NotImplementedError`. Coherent and calibration errors can be studied by perturbing the gate angles, and the circuit tolerates them well: a random error of 1 milliradian on each of 20 000 gates costs `2e-5` in the cosine of the solution, and 10 milliradians cost 0.2 %. Compared with a coarse Trotter grid, calibration noise is negligible. ## Next The details of the interface are in the [API reference](https://docs.kit.eigenorbit.com/api/algorithms/index.md). If you change only one parameter while you experiment, change `n_eigenvalue_qubits`: it controls the resolution of the phase estimation. The papers are listed in [References](https://docs.kit.eigenorbit.com/algorithms/references/#hhl-linear-systems). # HQNN: hybrid quantum neural networks ## A family of estimators In this library, a hybrid quantum neural network (HQNN) is an estimator in the style of sklearn, built around a hybrid neural model. You call `fit`, `predict` and, for classifiers, `predict_proba`. Inside the estimator, the data passes through classical layers, a trainable quantum circuit, and a classical head. The name covers several designs. > An HQNN encodes the data, runs a trainable quantum circuit, measures it, and passes the result to a classical head. All of it is trained end-to-end, with one optimiser and a standard loss. The quantum circuit gives the model a different way to shape the features. The accuracy still depends on the data, on the training setup, and on a comparison with classical baselines. ## The shared pipeline The base estimators use the same structure: an optional linear adapter, a `MinMaxNorm` layer that rescales the inputs into `[0, π]`, the quantum layer, a cast to float32, and a linear head. The quantum layer encodes the data as rotation angles, runs a variational circuit, and measures one Pauli expectation value per qubit. The training is the usual PyTorch loop, with one optimiser that updates the classical and the quantum parameters together. ## Input and output `fit(X, y)` trains the model. `predict(X)` returns classical NumPy labels or floats. `predict_proba(X)` returns a softmax over the logits of the classical head. `train_losses_`, and `validation_losses_` when you give validation data, store the loss curves. ## The catalogue All of these estimators share the pipeline above. They differ in what surrounds the quantum layer. | Estimator | Task | What makes it different | | ------------------------------------------------------------ | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `HQNC` / `HQNR` | They classify or regress tabular data. | They are the base hybrid models: the hybrid quantum neural network classifier (HQNC) and regressor (HQNR). | | `RecurrentHQNC` / `RecurrentHQNR` | They handle sequences (3-D input). | A classical recurrent neural network (an RNN, a gated recurrent unit (GRU) or a long short-term memory (LSTM) network) encodes the sequence first. By default, its last time step goes to the quantum layer. | | `CustomQuantumClassifier` / `CustomQuantumRegressor` | They classify or regress. | You give the circuit as an explicit list of blocks, together with the observables. | | `GenericClassifier` / `GenericRegressor` | They classify or regress. | They wrap any `torch.nn.Module` in the sklearn interface. They are quantum only if you put a quantum layer inside. | | `QuantumTimeSeriesClassifier` / `QuantumTimeSeriesRegressor` | They handle time series. | They are a purely classical wrapper with sliding windows around an inner estimator. | Inside `HQNC` and `HQNR`, `layer_type` chooses the quantum feature extractor: | `layer_type` | Quantum layer | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | `"Basic"` (default) | It is one `Encoding` block and one variational block. | | `"Reupload"` | It uses data reuploading: the input is encoded again `depth` times, which gives a richer class of (Fourier) functions. | | `"Ensemble"` | It splits the features into chunks the size of the number of qubits, runs one independent circuit on each chunk, and concatenates their outputs. | `variational_type` sets the ansatz. `"basic"` uses one rotation and a ladder of controlled-NOT (CNOT) gates per layer. `"strong"` uses `RZ`, `RY` and `RZ` rotations and a ladder of CNOT gates per layer, and has more parameters. ## Guarantees and limits You get an estimator that is compatible with scikit-learn, and whose classical and quantum parameters are trained end-to-end by one optimiser. The output is classical. On a statevector simulator evaluated analytically (with `shots=None` in the configuration of the provider, which is the default), the expectation values of each qubit are exact. The randomness comes from the initialisation of the weights and from the order of the mini-batches. Any claimed advantage has to be shown against a baseline that is a classical network. Deep or wide variational circuits can reach barren plateaus, where the gradients shrink and the learning stalls ([McClean et al.](https://arxiv.org/abs/1803.11173)). The encoding also limits what the model can represent ([Schuld, Sweke, and Meyer](https://arxiv.org/abs/2008.08605)), and `MinMaxNorm` clips the values that fall outside the fitted range. `GenericClassifier` and `GenericRegressor` are quantum only if the module that you pass contains a quantum layer. The simulation stays small, because the number of features sets the number of qubits. ## Next The interfaces of the estimators are in the [HQNC reference](https://docs.kit.eigenorbit.com/api/torch/estimators/hqnc/index.md) and the [HQNR reference](https://docs.kit.eigenorbit.com/api/torch/estimators/hqnr/index.md). See [Quantum Classification](https://docs.kit.eigenorbit.com/examples/algorithms-classification/index.md) for `HQNC`, [Quantum Regression & Time Series](https://docs.kit.eigenorbit.com/examples/algorithms-regression/index.md) for `HQNR`, and [Generic Estimators](https://docs.kit.eigenorbit.com/examples/generic-estimators/index.md) for custom PyTorch modules. Other HQNN examples focus on one dataset each: [breast cancer](https://docs.kit.eigenorbit.com/examples/breast-cancer-hqnc/index.md), [circles](https://docs.kit.eigenorbit.com/examples/circles-hqnc/index.md), and [concrete strength](https://docs.kit.eigenorbit.com/examples/cement-hqnr/index.md). The papers are listed in [References](https://docs.kit.eigenorbit.com/algorithms/references/#hqnn-hybrid-quantum-neural-networks). # IQAE: iterative quantum amplitude estimation ## The problem Suppose that a circuit `A` prepares a state where the last qubit, the **ancilla**, reads `1` with an unknown probability `a`. Amplitude estimation is the task of estimating this `a`. Formally, `A` acts on `n` qubits so that ```text A|0…0⟩ = √(1 - a) |ψ_0⟩|0⟩ + √a |ψ_1⟩|1⟩ ``` where the last qubit marks the "good" outcome, and `a = sin²θ`. When you count shots directly, the error shrinks like `1/√N`. Iterative quantum amplitude estimation (IQAE) amplifies the angle before counting, following the construction of [Grinko et al.](https://arxiv.org/abs/1912.05559), which does not need quantum phase estimation. The number `a` is useful when the circuit marks an event that has a meaning outside the circuit. A risk model can mark "the loss of the portfolio is above this threshold", and use IQAE to estimate the probability of this tail. The output is then a probability at the level of the application: a number that the caller can compare, use as a threshold, or pass into a larger estimate of risk. ## Input and output `IQAE(...).solve(oracle)` takes an oracle `A` that prepares a state and has no parameters, either as a PennyLane callable or as a `CircuitSpec`. The last qubit is the ancilla. The result is classical: an estimate, a confidence interval, and the numbers of rounds and of oracle calls. ## How it works Applying the Grover operator `Q` exactly `m` times changes the probability that the ancilla reads `1` to ```text P(ancilla = 1 after A then Q^m) = sin²((2m + 1)θ) ``` So `Q` multiplies the angle `θ` by the odd factor `2m + 1`, and a small probability becomes easier to distinguish. IQAE then runs an adaptive loop: 1. It chooses a Grover power `m` that keeps `(2m + 1)θ` within a half-period where it is not ambiguous. 1. It runs `A` followed by `Q^m`, and counts the outcomes where the ancilla reads `1`. 1. It builds a Clopper-Pearson confidence interval for this amplified probability. 1. It maps the interval back through `P = sin²((2m + 1)θ)`, to narrow the interval for `a`. ## Guarantees and limits With its adaptive schedule, the interval is built to contain the true `a` with a probability of at least `1 - α`. If the loop converges within its budget of rounds, the width of the interval is at most `2ε`. In theory, the number of queries grows like `1/ε`, up to logarithmic factors, instead of `1/ε²` for a direct Monte Carlo estimate ([Grinko et al.](https://arxiv.org/abs/1912.05559); [Brassard et al.](https://arxiv.org/abs/quant-ph/0005055)). In the default path, the library computes the exact probability of the ancilla from the statevector, and then draws a seeded binomial sample to imitate shots. Use `shot_mode="provider"` with `provider="pennylane"` for real sampling by the backend. If too few shots prevent convergence, IQAE returns the wider interval that it has, and issues a warning. `epsilon` must be in `(0, π/8)`. ## Next The details of the interface are in the [API reference](https://docs.kit.eigenorbit.com/api/algorithms/index.md). The same idea also appears in `PortfolioRiskEstimator`, where the ancilla marks a tail loss. The papers are listed in [References](https://docs.kit.eigenorbit.com/algorithms/references/#iqae-amplitude-estimation). # QAOA: quantum approximate optimization algorithm ## The problem Many optimisation problems ask for a bitstring `b ∈ {0, 1}ⁿ` with a low cost. MaxCut is the standard example: you split the nodes of a graph into two groups, so that many edges cross from one group to the other. The Quantum Approximate Optimization Algorithm (QAOA) is a hybrid version of this search. A quantum circuit proposes bitstrings, and a classical optimiser tunes the angles of the circuit. The method alternates a cost layer and a mixer layer, as in the construction of [Farhi, Goldstone, and Gutmann](https://arxiv.org/abs/1411.4028). ## Input and output `QAOA(...).solve(problem)` accepts an `Ising`, a `QUBO`, or the output of `max_cut(...)`. `layers` is the depth `p` of QAOA. The method returns the decoded bitstring, its exact classical cost, the optimised expectation value, the tuned angles, and the probability distribution. ## How it works First, the problem is rewritten as a diagonal **cost Hamiltonian**, whose energy is equal to the cost of the bitstring: ```text H_C = Σ_i h_i Z_i + Σ_{i k`, it applies a controlled phase rotation `CPHASE` of angle `2π/2^{j-k+1}`, so that each bit of lower order shifts the phase by its own weight. 1. When every qubit has been through step 1, it reverses the register with `SWAP` gates, because the construction produces the output bits in the opposite order. This gives `n` Hadamard gates, `n(n-1)/2` controlled phases and `⌊n/2⌋` swaps. A transform on three qubits has seven gates, and `assemble([QFTBlock(n_qubits=3)])` builds exactly `H, CPHASE, CPHASE, H, CPHASE, H, SWAP`. The inverse runs the same construction backwards, with the phase angles negated. The cost is what makes the transform useful: `O(n²)` gates, against the `O(2ⁿ log 2ⁿ)` operations that a classical fast Fourier transform (FFT) needs on the same `2ⁿ` amplitudes. This speedup can only be used when the answer can be read from a few measurements rather than from the whole transformed vector, which is exactly the case of phase estimation. This construction comes from [Kitaev 1995](https://arxiv.org/abs/quant-ph/9511026) and [Cleve, Ekert, Macchiavello and Mosca 1998](https://arxiv.org/abs/quant-ph/9708016). ## Guarantees and limits The transform is exact. It is a unitary built from exact gates, with no approximation and no setting to tune. Every gate in it (`H`, `CPHASE` and `SWAP`) is a gate that the intermediate representation (IR) of the circuit defines, so the transform compiles on any backend whose gate set includes these three gates. The first limit is that the QFT alone answers no question. It is a change of basis, and what makes it useful is the algorithm around it, which here means `QPEBlock` and the [Harrow-Hassidim-Lloyd algorithm (HHL)](https://docs.kit.eigenorbit.com/algorithms/hhl/index.md). `QPEBlock` uses `QFTBlock` directly. The `"honest"` mode of HHL reaches the same gates through the helper function on which `QFTBlock` is built, so the transform is shared and not written twice. The other limit is hardware. The number of gates grows quadratically with the width of the register, while the phase angles shrink exponentially, so on a real device the smallest rotations eventually fall below the resolution of the device. [Barenco, Ekert, Suominen and Törmä](https://arxiv.org/abs/quant-ph/9601018) showed that removing them costs little and can help under decoherence, which is why approximate QFTs exist. This implementation keeps every rotation. ## Next The block is in the [blocks reference](https://docs.kit.eigenorbit.com/api/core/blocks/index.md), together with `QPEBlock`, which is its main user. [HHL](https://docs.kit.eigenorbit.com/algorithms/hhl/index.md) shows phase estimation in use. The papers are listed in [References](https://docs.kit.eigenorbit.com/algorithms/references/#cross-cutting-foundations). # QSVC / QSVR: quantum kernel machines ## Start with the kernel trick A support vector machine (SVM) trains from a table of similarities between pairs of samples, `K(x_i, x_j)`, which is also called a kernel or Gram matrix. The quantum support vector classifier (QSVC) and the quantum support vector regressor (QSVR) change one thing: a quantum circuit computes this similarity. The fit step is still the training of an SVM by scikit-learn. This is the idea of a quantum feature space from [Havlíček et al.](https://arxiv.org/abs/1804.11326). - `QSVC` does classification, and wraps `SVC` from scikit-learn. - `QSVR` does regression, and wraps `SVR` from scikit-learn. ## Input and output With the built-in feature maps, `QSVC.fit(X, y)` and `QSVR.fit(X, y)` use one qubit per feature. A custom map passed with `kernel=QuantumKernel(...)` sets its own number of qubits. `QSVC` returns class labels, and also probabilities when `probability=True`. `QSVR` returns floats. You can also call `QuantumKernel.compute_kernel_matrix(X)` directly, to inspect the values of the fidelity. ## How it works The circuit computes a **fidelity kernel**. A feature map `U(x)` encodes a sample as the state `|φ(x)⟩ = U(x)|0⟩`. The kernel is the squared overlap of two encoded states: ```text K(x_i, x_j) = |⟨φ(x_j)|φ(x_i)⟩|² ``` On hardware and on the other backends that use shots, this value is read from an inversion circuit: 1. It applies `U(x_i)`. 1. It applies `U(x_j)†`. 1. It reads the probability of returning to `|0…0⟩`, which is the value of the kernel. 1. It fills the Gram matrix, and passes it to `SVC` or `SVR` with `kernel="precomputed"`. Simulators that give access to the statevector do not use the inversion circuit. They prepare each `|φ(x)⟩` once, and take every overlap in one matrix product. The two built-in feature maps are simple on purpose. `"Angle"` is an angle embedding (`RX` rotations, with `RZ` rotations on the extra repetitions) with **no entanglement**. `"AngleCNOT"` adds a fixed ladder of controlled-NOT (CNOT) gates between neighbouring qubits. Any other map goes in through `QSVC(kernel=QuantumKernel(feature_map=...))`: for example a ZZ map, an entangling pattern that is native to the hardware, or a map with trainable weights tuned by kernel alignment ([Glick et al.](https://arxiv.org/abs/2105.03406)). See the [Quantum Kernels guide](https://docs.kit.eigenorbit.com/user-guide/kernels/index.md). ## Guarantees and limits On the simulator, each entry of the kernel is the exact fidelity without noise, `|⟨φ(x_j)|φ(x_i)⟩|²`. The matrix `K(X, X)` is symmetric, and its diagonal entries equal `1.0` within floating-point precision. After that, the guarantee is the one from scikit-learn: `SVC` and `SVR` solve their usual convex problem on the precomputed matrix. This matches the view of kernel methods in [Schuld and Killoran](https://arxiv.org/abs/1803.07128). The learning is classical. The circuit only produces similarities. Real hardware that uses shots would add sampling noise, which the exact path of the simulator leaves out. The original paper uses a `ZZ` feature map. The built-in `"Angle"` map is a plain angle embedding, and a ZZ map takes a few lines with `QuantumKernel(feature_map=...)`. Very expressive maps can also suffer from the concentration of kernel values ([Thanasilp et al.](https://arxiv.org/abs/2208.11060)). On a statevector simulator, a Gram matrix costs `O(n_samples)` state evolutions and one matrix product, and the state grows exponentially with the number of qubits. On hardware, it costs `n_samples(n_samples - 1)/2` inversion circuits. Qiskit Runtime and IQM send all of them in one job, and the other backends send them in smaller calls. ## Next The interfaces of the estimators are in the [QSVC reference](https://docs.kit.eigenorbit.com/api/qml/qsvc/index.md) and the [QSVR reference](https://docs.kit.eigenorbit.com/api/qml/qsvr/index.md). See [Quantum Classification](https://docs.kit.eigenorbit.com/examples/algorithms-classification/index.md) for `QSVC`, [Quantum Regression & Time Series](https://docs.kit.eigenorbit.com/examples/algorithms-regression/index.md) for `QSVR`, and the [Quantum Kernels guide](https://docs.kit.eigenorbit.com/user-guide/kernels/index.md) to inspect the kernel matrix. The Iris example of hyperparameter optimisation uses `QSVC` inside [GridSearchCV](https://docs.kit.eigenorbit.com/examples/hpo-qsvc-iris/index.md). The papers are listed in [References](https://docs.kit.eigenorbit.com/algorithms/references/#qsvc-qsvr-quantum-kernels). # References This page collects the papers that the algorithm pages cite. The original papers state the formal guarantees. The reviews and tutorials are there to build intuition. ## IQAE: amplitude estimation These papers cover iterative quantum amplitude estimation (IQAE). - **The original paper.** D. Grinko, J. Gacon, C. Zoufal, S. Woerner, *Iterative Quantum Amplitude Estimation*, npj Quantum Information (2021), [arXiv:1912.05559](https://arxiv.org/abs/1912.05559). This library follows its adaptive schedule and its construction of the confidence interval. - **The background.** G. Brassard, P. Høyer, M. Mosca, A. Tapp, *Quantum Amplitude Amplification and Estimation* (2000), [arXiv:quant-ph/0005055](https://arxiv.org/abs/quant-ph/0005055). This paper gives the Grover operator and the formulation `a = sin²θ`. - **The limitations.** Y. Suzuki et al., *Amplitude estimation without phase estimation* (2019), [arXiv:1904.10246](https://arxiv.org/abs/1904.10246); K. Miyamoto, *On the bias in iterative quantum amplitude estimation* (2023), [arXiv:2311.16560](https://arxiv.org/abs/2311.16560). These two papers discuss the limitations of the method. ## HHL: linear systems These papers cover the Harrow-Hassidim-Lloyd (HHL) algorithm for systems of linear equations. - **The original paper.** A. Harrow, A. Hassidim, S. Lloyd, *Quantum algorithm for linear systems of equations*, Phys. Rev. Lett. (2009), [arXiv:0811.3171](https://arxiv.org/abs/0811.3171). - **A review.** D. Dervovic et al., *Quantum linear systems algorithms: a primer* (2018), [arXiv:1802.08227](https://arxiv.org/abs/1802.08227). - **A walkthrough.** A. Morrell et al., *Step-by-Step HHL Algorithm Walkthrough* (2021), [arXiv:2108.09004](https://arxiv.org/abs/2108.09004). It works through a numerical example at the level of detail that this implementation uses. - **The limitations and later work.** A. Childs, R. Kothari, R. Somma, *Quantum algorithm for systems of linear equations with exponentially improved dependence on precision* (2017), [arXiv:1511.02306](https://arxiv.org/abs/1511.02306); M. E. S. Morales et al., *Quantum Linear System Solvers: A Survey of Algorithms and Applications* (2024), [arXiv:2411.02522](https://arxiv.org/abs/2411.02522). - **Hamiltonian simulation.** These papers concern the evolution `e^{iAt}` that the `"honest"` mode builds. S. Lloyd, *Universal Quantum Simulators*, Science (1996), [DOI:10.1126/science.273.5278.1073](https://doi.org/10.1126/science.273.5278.1073); A. M. Childs, Y. Su, M. C. Tran, N. Wiebe, S. Zhu, *Theory of Trotter Error with Commutator Scaling*, Phys. Rev. X 11, 011020 (2021), [DOI:10.1103/PhysRevX.11.011020](https://doi.org/10.1103/PhysRevX.11.011020). The preprint of the second paper, [arXiv:1912.08854](https://arxiv.org/abs/1912.08854), has the title *A Theory of Trotter Error*. Its Theorem 11 establishes the form of the error bound with commutator scaling, which `trotter_steps` and `trotter_order` rely on. This library writes that bound for Pauli terms, where every commutator norm is `0` or `2`. The theorem is stated only up to an order of magnitude, so the explicit constants that this library uses do not come from it. Instead, tests fix them by comparing the bound with `‖A_eff - A‖`, computed exactly. - **The cost of the Pauli decomposition.** There are `4ⁿ` Pauli strings on `n` qubits, so a generic matrix has `Θ(4ⁿ)` terms. For the decomposition itself, L. Hantzko, L. Binkowski, S. Gupta, *Tensorized Pauli decomposition algorithm*, Phys. Scr. 99, 085128 (2024), [arXiv:2310.13421](https://arxiv.org/abs/2310.13421), works by slicing and adding matrices instead of multiplying them. For tridiagonal matrices, the exact count is `(n+1)2ⁿ` in general (Proposition 1) and `(n+2)2ⁿ⁻¹` when the matrix is real and symmetric (Corollary 2). Both counts are in B. Arseniev, D. Guskov, R. Sengupta, J. Biamonte, I. Zacharov, *Tridiagonal matrix decomposition for Hamiltonian simulation on a quantum computer*, Phys. Rev. A 109, 052629 (2024), [arXiv:2310.00121](https://arxiv.org/abs/2310.00121). The second count gives the 576 terms that the [scaling table](https://docs.kit.eigenorbit.com/algorithms/hhl/#guarantees-and-limits) reports at `n = 7`. The following paper explains why the Pauli input model, which is a linear combination of unitaries (LCU), and the sparse-access input model are *distinct*: A. Gilyén, Y. Su, G. H. Low, N. Wiebe, *Quantum singular value transformation and beyond: exponential improvements for quantum matrix arithmetics*, STOC 2019, [arXiv:1806.01838](https://arxiv.org/abs/1806.01838). - **The eigenvalue inversion as arithmetic.** The inversion would become a uniform family of circuits if the angle were computed from the clock register with reversible arithmetic, instead of being tabulated bucket by bucket. Circuits for the functions involved, including the arcsine, are in T. Häner, M. Roetteler, K. M. Svore, *Optimizing Quantum Circuits for Arithmetic* (2018), [arXiv:1805.12445](https://arxiv.org/abs/1805.12445). - **The eigenvalue inversion as elementary gates.** M. Möttönen, J. J. Vartiainen, V. Bergholm, M. M. Salomaa, *Transformation of quantum states using uniformly controlled rotations*, Quantum Inf. Comput. 5, 467 (2005), [quant-ph/0407010](https://arxiv.org/abs/quant-ph/0407010). This paper gives the uniformly controlled rotation and its transform of the angles. The inversion stage of the `"honest"` mode is built from them, with `2^(n_l)` `RY` gates and `2^(n_l)` controlled-NOT (`CNOT`) gates. It needs no ancillas, and the assembled circuit contains no multi-controlled primitive anywhere. - **The fine print.** S. Aaronson, *Read the fine print*, Nature Physics (2015), [DOI:10.1038/nphys3272](https://doi.org/10.1038/nphys3272). The preprint has the title *Quantum Machine Learning Algorithms: Read the Fine Print*. It names four caveats: preparing `|b⟩`, applying `e^{-iAt}`, the condition number, and the fact that the output is a state and not a vector. - **Dense matrices.** L. Wossnig, Z. Zhao, A. Prakash, *A quantum linear system algorithm for dense matrices*, Phys. Rev. Lett. 120, 050502 (2018), [arXiv:1704.06174](https://arxiv.org/abs/1704.06174). For a dense matrix with a bounded spectral norm, it reports a quadratic speedup over the earlier quantum approaches. The method is built on singular value estimation and a specialised memory architecture. That machinery is out of the scope of this library. ## QFT: the quantum Fourier transform These papers cover the quantum Fourier transform (QFT) and quantum phase estimation. - **The original paper.** D. Coppersmith, *An approximate Fourier transform useful in quantum factoring*, IBM Research Report RC19642 (1994), [arXiv:quant-ph/0201067](https://arxiv.org/abs/quant-ph/0201067). It gives the circuit of Hadamard and controlled-phase gates that this block builds, and the approximate variant that drops the smallest rotations. - **The paper that made it known.** P. W. Shor, *Polynomial-Time Algorithms for Prime Factorization and Discrete Logarithms on a Quantum Computer*, SIAM J. Comput. (1997), [arXiv:quant-ph/9508027](https://arxiv.org/abs/quant-ph/9508027). - **Phase estimation.** A. Yu. Kitaev, *Quantum measurements and the Abelian Stabilizer Problem* (1995), [arXiv:quant-ph/9511026](https://arxiv.org/abs/quant-ph/9511026); R. Cleve, A. Ekert, C. Macchiavello, M. Mosca, *Quantum Algorithms Revisited*, Proc. R. Soc. Lond. A (1998), [arXiv:quant-ph/9708016](https://arxiv.org/abs/quant-ph/9708016). These papers give the construction that `QPEBlock` implements. - **The limitations.** A. Barenco, A. Ekert, K.-A. Suominen, P. Törmä, *Approximate Quantum Fourier Transform and Decoherence* (1996), [arXiv:quant-ph/9601018](https://arxiv.org/abs/quant-ph/9601018), which studies what happens to the smallest phase rotations; Y. Nam, Y. Su, D. Maslov, *Approximate Quantum Fourier Transform with `O(n log n)` T gates* (2020), [arXiv:1803.04933](https://arxiv.org/abs/1803.04933). ## QAOA: combinatorial optimisation These papers cover the Quantum Approximate Optimization Algorithm (QAOA). - **The original paper.** E. Farhi, J. Goldstone, S. Gutmann, *A Quantum Approximate Optimization Algorithm* (2014), [arXiv:1411.4028](https://arxiv.org/abs/1411.4028). - **A tutorial and a review.** A. Sturm, *Theory and Implementation of the Quantum Approximate Optimization Algorithm* (2023), [arXiv:2301.09535](https://arxiv.org/abs/2301.09535); K. Blekos et al., *A Review on Quantum Approximate Optimization Algorithm and its Variants* (2023), [arXiv:2306.09198](https://arxiv.org/abs/2306.09198). - **The limitations.** M. Goh, *The Overlap Gap Property limits limit swapping in the QAOA* (2024), [arXiv:2404.06087](https://arxiv.org/abs/2404.06087). ## VQE: the variational quantum eigensolver These papers cover the Variational Quantum Eigensolver (VQE). - **The original paper.** A. Peruzzo, J. McClean et al., *A variational eigenvalue solver on a quantum processor*, Nature Communications (2014), [arXiv:1304.3061](https://arxiv.org/abs/1304.3061). - **The hybrid framework.** J. R. McClean, J. Romero, R. Babbush, A. Aspuru-Guzik, *The theory of variational hybrid quantum-classical algorithms*, New J. Phys. (2016), [arXiv:1509.04279](https://arxiv.org/abs/1509.04279). - **The mappings from fermions to qubits.** P. Jordan, E. Wigner, *Über das Paulische Äquivalenzverbot*, Zeitschrift für Physik 47 (1928), 631–651, which gives the original transformation; S. Bravyi, A. Kitaev, *Fermionic quantum computation* (2000), [arXiv:quant-ph/0003137](https://arxiv.org/abs/quant-ph/0003137); J. T. Seeley, M. J. Richard, P. J. Love, *The Bravyi-Kitaev transformation for quantum computation of electronic structure* (2012), [arXiv:1208.5986](https://arxiv.org/abs/1208.5986). - **The ansatze.** J. Romero et al., *Strategies for quantum computing molecular energies using the unitary coupled cluster ansatz* (2018), [arXiv:1701.02691](https://arxiv.org/abs/1701.02691), is the reference for the unitary coupled cluster ansatz with single and double excitations (UCCSD). A. Kandala et al., *Hardware-efficient Variational Quantum Eigensolver for Small Molecules and Quantum Magnets*, Nature (2017), [arXiv:1704.05018](https://arxiv.org/abs/1704.05018), is the reference for the hardware-efficient family. - **A review and the limitations.** J. Tilly et al., *The Variational Quantum Eigensolver: a review of methods and best practices*, Physics Reports (2022), [arXiv:2111.05176](https://arxiv.org/abs/2111.05176). The papers on barren plateaus are listed under HQNN below. ## QSVC / QSVR: quantum kernels These papers cover the quantum support vector classifier (QSVC) and the quantum support vector regressor (QSVR), which use quantum kernels. - **The original paper.** V. Havlíček et al., *Supervised learning with quantum enhanced feature spaces*, Nature (2019), [arXiv:1804.11326](https://arxiv.org/abs/1804.11326). - **The kernel point of view.** M. Schuld, N. Killoran, *Quantum machine learning in feature Hilbert spaces* (2018), [arXiv:1803.07128](https://arxiv.org/abs/1803.07128). - **The limitations.** S. Thanasilp et al., *Exponential concentration in quantum kernel methods* (2022), [arXiv:2208.11060](https://arxiv.org/abs/2208.11060); S. Jerbi et al., *Quantum machine learning beyond kernel methods* (2023), [Nature Communications](https://www.nature.com/articles/s41467-023-36159-y). - **Kernel alignment.** J. R. Glick et al., *Covariant quantum kernels for data with group structure*, Nature Physics (2024), [arXiv:2105.03406](https://arxiv.org/abs/2105.03406), is the reference for the `"svc"` loss of `align_kernel`. N. Cristianini et al., *On kernel-target alignment*, NeurIPS (2001), is the reference for the `"kta"` loss. - **The optimiser.** J. C. Spall, *Multivariate stochastic approximation using a simultaneous perturbation gradient approximation*, IEEE Transactions on Automatic Control (1992), is the reference for simultaneous perturbation stochastic approximation (SPSA). ## HQNN: hybrid quantum neural networks These papers cover hybrid quantum neural networks (HQNN). - **A recent paper.** A. Abbas et al., *The power of quantum neural networks* (2021), [arXiv:2011.00027](https://arxiv.org/abs/2011.00027). - **A historical review.** M. Schuld, I. Sinayskiy, F. Petruccione, *The quest for a Quantum Neural Network* (2014), [arXiv:1408.7005](https://arxiv.org/abs/1408.7005). - **Trainability and data encoding.** J. McClean et al., *Barren plateaus in quantum neural network training landscapes* (2018), [arXiv:1803.11173](https://arxiv.org/abs/1803.11173); M. Schuld, R. Sweke, J. Meyer, *Effect of data encoding on the expressive power of variational quantum machine learning models* (2021), [arXiv:2008.08605](https://arxiv.org/abs/2008.08605). ## Quantum reservoir computing - **The original paper.** K. Fujii, K. Nakajima, *Harnessing disordered quantum dynamics for machine learning*, Phys. Rev. Applied (2017), [arXiv:1602.08159](https://arxiv.org/abs/1602.08159). It uses quantum dynamics as the reservoir, with a trained linear readout. - **The classical origins.** H. Jaeger, *The "echo state" approach to analysing and training recurrent neural networks*, GMD Report 148 (2001); W. Maass, T. Natschläger, H. Markram, *Real-time computing without stable states*, Neural Computation 14 (2002), 2531–2560. Neither of these has an arXiv link, and this is not by accident: both are reports, not preprints. - **Capacity and multiplexing.** K. Nakajima, K. Fujii, M. Negoro, K. Mitarai, M. Kitagawa, *Boosting computational power through spatial multiplexing in quantum reservoir computing*, Phys. Rev. Applied (2019), [arXiv:1803.04574](https://arxiv.org/abs/1803.04574). - **A review.** P. Mujal, R. Martínez-Peña et al., *Opportunities in Quantum Reservoir Computing and Extreme Learning Machines*, Adv. Quantum Technol. (2021), [arXiv:2102.11831](https://arxiv.org/abs/2102.11831). It explains why dissipation, and not unitarity, is what gives a reservoir a memory that can be used. ## Cross-cutting foundations These papers are used across several pages. - **Data re-uploading.** A. Pérez-Salinas et al., *Data re-uploading for a universal quantum classifier* (2020), [arXiv:1907.02085](https://arxiv.org/abs/1907.02085). This paper is the basis of the `Reupload` layer. - **Gradients by parameter shift.** K. Mitarai et al., *Quantum circuit learning* (2018), [arXiv:1803.00745](https://arxiv.org/abs/1803.00745); M. Schuld et al., *Evaluating analytic gradients on quantum hardware* (2019), [arXiv:1811.11184](https://arxiv.org/abs/1811.11184). These papers give the gradient method used when a backend has no native automatic differentiation. # Quantum reservoir computing: learning on a circuit nobody trains ## The problem Training a quantum circuit is expensive. Every parameter needs a gradient, every gradient needs extra evaluations of the circuit, and deep circuits run into barren plateaus, where the gradients vanish altogether. Reservoir computing avoids this problem entirely. You drive a **fixed, random** circuit with your input sequence, observe how its state evolves, and fit an ordinary linear model to what comes out. The circuit is never trained. Only the readout is trained, and the readout has a closed-form solution. The idea comes from classical echo state networks and liquid state machines, and [Fujii and Nakajima](https://arxiv.org/abs/1602.08159) carried it over to quantum dynamics. The assumption is that a dynamical system that is rich enough does the hard part without any training. It mixes the history of the input into a state with many dimensions, and a linear readout is then enough to extract whatever the task needs. ## Input and output Two ready-made estimators wrap the whole pipeline: ```python import numpy as np from eigenkit import Encoding, Variational, QuantumReservoirRegressor rng = np.random.default_rng(0) X = rng.uniform(-1, 1, size=(20, 40, 5)) y = np.zeros((20, 40)) y[:, 1:] = X[:, :-1, 0] # predict the first feature of the previous step blocks = [Encoding(in_features=5, rotation="RY"), Variational(depth=4, rotation="RY")] model = QuantumReservoirRegressor( blocks, state_policy="leaky", leak=0.5, features="probabilities", washout=10, ) model.fit(X, y) # X is (n_sequences, n_timesteps, n_features) ``` `X` has three dimensions: the sequences, the timesteps and the features. A 2-D array raises an error. `QuantumReservoirClassifier` is the same estimator with a logistic readout. Underneath, `eigenkit.reservoir` holds the parts separately. `run_reservoir` drives the circuit, the policies decide what carries over between timesteps, the feature functions turn statevectors into numbers, and `LinearReadout` fits the ridge regression. All of it is pure NumPy: it needs no PyTorch, and no software development kit (SDK) of a backend. ## How it works ### Step 1: drive the reservoir At each timestep, the input is encoded as angles of the gates of the frozen circuit, and the state advances. The **policy** defines what "advances" means, and it is the one choice that decides whether the reservoir remembers anything: - **`Carry`** is a purely unitary carry-over, `ψ_t = U(x_t) ψ_{t-1}`. The norm is preserved, and nothing is forgotten. - **`LeakyCarry`** mixes the carried state with a reset state, and then divides by the norm: `ψ̃_t = α U(x_t) ψ_{t-1} + (1 - α) ψ_reset` and `ψ_t = ψ̃_t / ‖ψ̃_t‖`. At `α = 1`, it is exactly `Carry`. It stands in for a dissipative reservoir with pure states, and it is not a true quantum channel. A true channel would need density matrices, which the pure-state backends do not provide. - **`Stateless`** starts every step from `|0…0⟩`, and encodes a sliding window of the last `window` inputs. The memory is entirely classical. Because no state crosses from one timestep to the next, the driver evaluates all the timesteps in one batched run. ### Step 2: read out features The trajectory of statevectors becomes a classical matrix of features, with one row per timestep and one column per number that the extractor produces. | `features` | What it takes from the state | | -------------------- | -------------------------------------------------------------------------- | | `"z"` | It takes `⟨Z_q⟩` on each qubit, which gives `n` values. | | `"probabilities"` | It takes \` | | `"amplitudes"` | It takes the real and imaginary parts of `ψ`, which gives `2 · 2ⁿ` values. | | a Hamiltonian string | It takes \`⟨ψ | More numbers give the linear readout more to fit on, and this is usually what makes a reservoir work. However, the middle two grow as `2ⁿ`, so a wide reservoir becomes expensive on the classical side rather than on the quantum side. ### Step 3: fit the readout This is the part that learns. The readout is a ridge regression with a bias term. It is fitted on the matrix of features in one step, by solving ```text W = (XᵀX + λI)⁻¹ XᵀY ``` where the bias column is not penalised, and `λ` is the `ridge` argument. It is a linear solve rather than an iterative loop, so there is no optimiser and no number of epochs to set. However, the weights are fitted to the data exactly as in any other supervised model. What stays fixed is the quantum side. The weights of the circuit are drawn once from `random_state`, and they never change. The training only changes the classical readout. `washout` drops the first timesteps, before the reservoir has settled, from the fit. The model still produces predictions for every timestep. ## Guarantees and limits Nothing here converges, so there is no guarantee of convergence. A reservoir is judged instead on its **memory capacity**: how far back it can still reproduce its own input. Dissipation gives the reservoir this memory, and unitarity does not. The `leaky` policy gives the curve of fading memory that a reservoir is expected to have. `stateless` remembers only the current step. The purely unitary `carry` scores nothing at all on a delay task, even though it is the default. Use `state_policy="leaky"` with a `leak` of about `0.3` to `0.7` on any temporal task, and treat the default as the first thing to change. The [memory-capacity example](https://docs.kit.eigenorbit.com/examples/reservoir-memory/index.md) measures all three. The other limits are the usual ones. The wider sets of features grow as `2ⁿ`, so the cost falls on the classical side. The readout is linear, so whatever it cannot express has to come from the dynamics of the reservoir itself. The frozen weights are one random draw, so a poor result may come from the particular `random_state` rather than from the configuration. Vary the seed before you draw any conclusion about the setup. ## Next The estimators are covered in the [quantum machine learning (QML) guide](https://docs.kit.eigenorbit.com/user-guide/qml/index.md), and the parts in [Reservoir Computing](https://docs.kit.eigenorbit.com/user-guide/reservoir/index.md). The [memory-capacity example](https://docs.kit.eigenorbit.com/examples/reservoir-memory/index.md) is where the results above come from. The papers are listed in [References](https://docs.kit.eigenorbit.com/algorithms/references/#quantum-reservoir-computing). # VQE: the variational quantum eigensolver ## The problem Chemistry asks for the lowest energy that a molecule can have, which is the energy of its ground state. This energy is the smallest eigenvalue of a Hamiltonian. The matrix of this Hamiltonian grows as `2ⁿ` with the number `n` of spin-orbitals, so exact diagonalisation runs out of memory quickly. The Variational Quantum Eigensolver (VQE) replaces this diagonalisation with a search. A circuit prepares a trial state, the quantum side measures its energy, a classical optimiser adjusts the angles of the circuit, and the loop repeats. The variational principle guarantees that the measured energy can never fall below the true energy of the ground state, so a lower number is always a better answer. The method is due to [Peruzzo et al.](https://arxiv.org/abs/1304.3061), and the hybrid framework was set out by [McClean et al.](https://arxiv.org/abs/1509.04279). ## Input and output `VQE(...).solve(hamiltonian, n_qubits, offset=..., ansatz=...)` takes a qubit `Observable`, and returns `energy`, the tuned `optimal_parameters`, `n_iterations` and `success`. Going from a molecule to this `Observable` takes two steps: ```python from eigenkit.chemistry import ( VQE, exact_ground_state_energy, fermi_hubbard_dimer, jordan_wigner, ) hamiltonian = fermi_hubbard_dimer(t=1.0, u=2.0) # 6 fermionic terms observable, offset = jordan_wigner(hamiltonian) # 10 Pauli terms, offset 1.0 result = VQE(layers=3, restarts=5).solve(observable, n_qubits=4, offset=offset) print(result.energy) # -1.236068 print(exact_ground_state_energy(observable, 4, offset=offset)) # -1.236068 ``` `offset` is the constant term. The Pauli backends reject a term that holds only the identity, so the mapping returns the constant separately, and the solver adds it to `⟨H⟩` at the end. If you forget to pass it, every energy is wrong by this amount. ## How it works ### Step 1: a fermionic Hamiltonian Electrons are fermions: when you swap two of them, the sign of the wavefunction changes. A `FermionOperator` stores the Hamiltonian in terms of creation and annihilation operators, which follow this rule. ### Step 2: map fermions onto qubits Qubits do not antisymmetrise on their own, so the mapping must impose this rule. **Jordan-Wigner** does it with a string of `Z` operators: ```text a_j = (Π_{k (batch, N_QUBITS) expectation values nn.Linear(N_QUBITS, N_CLASSES), ) print(clf_net) ``` ```text Sequential( (0): Linear(in_features=2, out_features=5, bias=True) (1): ReLU() (2): Basic( (circuit): TorchCircuit() ) (3): Linear(in_features=5, out_features=2, bias=True) ) ``` ### Train with `GenericClassifier` `GenericClassifier` wraps the network in a standard sklearn estimator. Pass the complete `nn.Module` as `model`. Set `in_features`, so that the wrapper checks the shape of the input in `fit()`. Configure the training loop with `epochs`, `lr` and `batch_size`. The wrapper encodes the labels automatically, so `y_train` can hold integers, strings or any other hashable labels. Pre-trained weights and the behaviour of `fit()` `fit()` does **not** start from the weights that the `nn.Module` holds. It copies the module, and then calls `reset_parameters()` on every submodule that has this method (such as `nn.Linear` and the quantum layers of eigenkit), seeded with `random_state`. So the weights of these layers are drawn again before training, even if the module that you pass already holds trained weights. The module that you pass is not changed. `clone()` copies the *hyperparameters* of the estimator, not the trained weights, and each later call to `fit()` (for example after `clone()`, or inside cross-validation) also starts again from new weights. To keep the trained weights, use the fitted estimator, or save it with `save()` and load it again with `load()`, and do not call `fit()` on it again. ```python clf = GenericClassifier( model=clf_net, in_features=2, epochs=150, lr=0.01, batch_size=16, random_state=42, ) clf.fit(X_fit, y_fit, X_val=X_val, y_val=y_val) print(f"Test accuracy: {clf.score(X_test, y_test):.3f}") ``` ```text GenericClassifier training | Epoch 1/150 | loss=0.668630 | val_loss=0.624434 | elapsed=0.2s | eta=32.6s GenericClassifier training | Epoch 2/150 | loss=0.583259 | val_loss=0.552643 | elapsed=0.4s | eta=33.1s GenericClassifier training | Epoch 3/150 | loss=0.512898 | val_loss=0.487117 | elapsed=0.7s | eta=34.2s ... GenericClassifier training | Epoch 149/150 | loss=0.009535 | val_loss=0.067237 | elapsed=56.7s | eta=0.4s GenericClassifier training | Epoch 150/150 | loss=0.009276 | val_loss=0.070270 | elapsed=56.9s | eta=0.0s Test accuracy: 1.000 ``` ### Training curve ```python for theme in ("light", "dark"): with matplotlib.rc_context(rc_params(theme=theme)): fig, ax = plt.subplots(figsize=(7, 4)) ax.plot(clf.train_losses_, color=STYLE_COLORS[0], label="train loss") ax.plot(clf.validation_losses_, color=STYLE_COLORS[1], label="validation loss") ax.set_xlabel("Epoch") ax.set_ylabel("Cross-entropy loss") ax.set_title("GenericClassifier training curve") ax.legend() fig.tight_layout() fig.savefig(f"generic_classifier_training_curve_{theme}.png") if theme == "light": plt.show() else: plt.close(fig) ``` ### Predict and inspect outputs ```python labels = clf.predict(X_test) probs = clf.predict_proba(X_test) print("Predicted labels (first 8):", labels[:8].tolist()) print("Class probabilities (first 3):") print(probs[:3]) ``` ```text Predicted labels (first 8): [0, 1, 1, 1, 0, 1, 0, 1] Class probabilities (first 3): [[9.9979252e-01 2.0752900e-04] [1.8817510e-02 9.8118252e-01] [3.3264074e-01 6.6735923e-01]] ``` ### Save and load Call `save()` after training. The bundle stores the trained weights, the label encoder and the training metadata. The architecture cannot be rebuilt automatically from the bundle, so you must pass a module with the **same `nn.Module` structure** to `GenericClassifier.load()`. The saved weights are loaded into a copy of this module, and the module that you pass is not changed. So the module used in training and the module passed to `load()` do not need to be the same Python object. They only need to have the same structure of layers. ```python clf.save("clf_hybrid.eigenkit") # Rebuild the architecture (same structure, fresh weights) clf_net_fresh = nn.Sequential( nn.Linear(2, N_QUBITS), nn.ReLU(), Basic(n_qubits=N_QUBITS, depth=3, variational_type="strong"), nn.Linear(N_QUBITS, N_CLASSES), ) clf_loaded = GenericClassifier.load("clf_hybrid.eigenkit", model=clf_net_fresh) # Predictions must match the original model assert (clf_loaded.predict(X_test) == clf.predict(X_test)).all() print(f"Loaded model test accuracy: {clf_loaded.score(X_test, y_test):.3f}") ``` ```text Loaded model test accuracy: 1.000 ``` ______________________________________________________________________ ## Part 2: Regression ### Data The diabetes dataset has `10` features, and its target is a continuous measure of the progression of the disease. We normalise both the features and the target with `StandardScaler`. ```python from sklearn.preprocessing import StandardScaler data = load_dataset("diabetes") X_train_r_full, y_train_r_full = data["X_train"], data["y_train"] X_test_r, y_test_r_raw = data["X_test"], data["y_test"] X_train_r_raw, X_val_r_raw, y_train_r_raw, y_val_r_raw = train_test_split( X_train_r_full, y_train_r_full, test_size=0.2, random_state=42, ) x_scaler = StandardScaler() X_train_r = x_scaler.fit_transform(X_train_r_raw).astype(np.float32) X_val_r = x_scaler.transform(X_val_r_raw).astype(np.float32) X_test_r = x_scaler.transform(X_test_r).astype(np.float32) y_scaler = StandardScaler() y_train_r = ( y_scaler.fit_transform(y_train_r_raw.reshape(-1, 1)).ravel().astype(np.float32) ) y_val_r = y_scaler.transform(y_val_r_raw.reshape(-1, 1)).ravel().astype(np.float32) y_test_r = y_scaler.transform(y_test_r_raw.reshape(-1, 1)).ravel().astype(np.float32) print(f"Train: {X_train_r.shape}, Validation: {X_val_r.shape}, Test: {X_test_r.shape}") print(f"Target mean: {y_train_r.mean():.3f}, std: {y_train_r.std():.3f}") ``` ```text Train: (282, 10), Validation: (71, 10), Test: (89, 10) Target mean: -0.000, std: 1.000 ``` ### Build a hybrid regression model The architecture has three parts: 1. **A classical encoder**, which compresses the `10` features to the number of qubits. 1. **A `Basic` quantum layer**, which is a variational layer that extracts features. 1. **A classical regression head**, which is a linear output with a single neuron. `GenericRegressor` expects the `forward()` method of the model to return a tensor of shape `(batch,)` or `(batch, 1)`. The last layer, `nn.Linear(N_QUBITS_R, 1)`, meets this requirement. ```python N_QUBITS_R = 6 IN_FEATURES_R = X_train_r.shape[1] # 10 torch.manual_seed(42) quantum_layer_r = Basic(n_qubits=N_QUBITS_R, depth=3, variational_type="strong") reg_net = nn.Sequential( nn.Linear(IN_FEATURES_R, N_QUBITS_R), nn.ReLU(), quantum_layer_r, nn.Linear(N_QUBITS_R, 1), ) print(reg_net) ``` ```text Sequential( (0): Linear(in_features=10, out_features=6, bias=True) (1): ReLU() (2): Basic( (circuit): TorchCircuit() ) (3): Linear(in_features=6, out_features=1, bias=True) ) ``` ### Train with `GenericRegressor` The default loss is the mean squared error (MSE). Pass `loss_func="mae"` (the mean absolute error) or `loss_func="huber"` (the Huber loss) to use another loss, or pass any callable `(output, target) -> scalar_tensor` for a custom loss. ```python reg = GenericRegressor( model=reg_net, in_features=IN_FEATURES_R, loss_func="mse", epochs=100, lr=0.001, batch_size=32, random_state=42, ) reg.fit(X_train_r, y_train_r, X_val=X_val_r, y_val=y_val_r) print(f"Test R²: {reg.score(X_test_r, y_test_r):.3f}") ``` ```text GenericRegressor training | Epoch 1/100 | loss=1.222554 | val_loss=0.968641 | elapsed=0.2s | eta=23.3s GenericRegressor training | Epoch 2/100 | loss=1.199555 | val_loss=0.949176 | elapsed=0.5s | eta=22.2s GenericRegressor training | Epoch 3/100 | loss=1.166938 | val_loss=0.930964 | elapsed=0.7s | eta=21.9s ... GenericRegressor training | Epoch 99/100 | loss=0.429658 | val_loss=0.401382 | elapsed=27.2s | eta=0.3s GenericRegressor training | Epoch 100/100 | loss=0.426311 | val_loss=0.400020 | elapsed=27.4s | eta=0.0s Test R²: 0.489 ``` ### Training curve and predictions ```python preds = reg.predict(X_test_r) for theme in ("light", "dark"): with matplotlib.rc_context(rc_params(theme=theme)): fig, axes = plt.subplots(1, 2, figsize=(12, 4)) axes[0].plot(reg.train_losses_, color=STYLE_COLORS[0], label="train loss") axes[0].plot( reg.validation_losses_, color=STYLE_COLORS[1], label="validation loss" ) axes[0].set_xlabel("Epoch") axes[0].set_ylabel("MSE loss") axes[0].set_title("GenericRegressor training curve") axes[0].legend() lims = [min(y_test_r.min(), preds.min()), max(y_test_r.max(), preds.max())] axes[1].scatter(y_test_r, preds, alpha=0.6, color=STYLE_COLORS[0]) axes[1].plot(lims, lims, "--", linewidth=1.2, color=STYLE_COLORS[5]) axes[1].set_xlabel("True value (normalised)") axes[1].set_ylabel("Predicted value (normalised)") axes[1].set_title("Predicted vs True") plt.tight_layout() fig.savefig(f"generic_regressor_training_curve_{theme}.png") if theme == "light": plt.show() else: plt.close(fig) ``` ### Save and load Saving and loading work as for the classifier. Call `save()` after training, and pass the architecture again when you load. The bundle stores the weights and the training metadata, but not the architecture. ```python reg.save("reg_hybrid.eigenkit") # Rebuild the architecture (same structure, fresh weights) reg_net_fresh = nn.Sequential( nn.Linear(IN_FEATURES_R, N_QUBITS_R), nn.ReLU(), Basic(n_qubits=N_QUBITS_R, depth=3, variational_type="strong"), nn.Linear(N_QUBITS_R, 1), ) reg_loaded = GenericRegressor.load("reg_hybrid.eigenkit", model=reg_net_fresh) np.testing.assert_allclose( reg_loaded.predict(X_test_r), reg.predict(X_test_r), atol=1e-5, ) print(f"Loaded model R²: {reg_loaded.score(X_test_r, y_test_r):.3f}") ``` ```text Loaded model R²: 0.489 ``` ### Custom loss function Any callable `(output, target) -> scalar_tensor` can be used directly as the loss. Note that a model trained with a callable loss cannot be saved: `save()` raises a `SerializationError`. Use a loss given by its name (`"mse"`, `"mae"` or `"huber"`) when you need to save the model. The log-cosh loss is a smooth approximation of the MAE, and it is less sensitive to outliers than the MSE. The direct formula `torch.log(torch.cosh(x))` overflows for `|x| > 88` in float32, so we use the identity `log(cosh(x)) = |x| + log(1 + exp(-2|x|)) - log(2)`, which stays finite for any input. ```python def log_cosh_loss(output, target): """Numerically stable log-cosh loss. Uses the identity log(cosh(x)) = |x| + log(1 + exp(-2|x|)) - log(2). """ diff = output.squeeze(-1) - target abs_diff = torch.abs(diff) return ( abs_diff + torch.log1p(torch.exp(-2.0 * abs_diff)) - torch.log(torch.tensor(2.0)) ).mean() torch.manual_seed(42) reg_custom = GenericRegressor( model=nn.Sequential( nn.Linear(IN_FEATURES_R, N_QUBITS_R), nn.ReLU(), Basic(n_qubits=N_QUBITS_R, depth=3, variational_type="strong"), nn.Linear(N_QUBITS_R, 1), ), in_features=IN_FEATURES_R, loss_func=log_cosh_loss, epochs=30, lr=0.01, ) reg_custom.fit(X_train_r, y_train_r, X_val=X_val_r, y_val=y_val_r) print(f"Log-cosh model R²: {reg_custom.score(X_test_r, y_test_r):.3f}") ``` ```text GenericRegressor training | Epoch 1/30 | loss=0.407534 | val_loss=0.307214 | elapsed=0.2s | eta=7.2s GenericRegressor training | Epoch 2/30 | loss=0.317115 | val_loss=0.247753 | elapsed=0.5s | eta=6.6s GenericRegressor training | Epoch 3/30 | loss=0.252423 | val_loss=0.213302 | elapsed=0.7s | eta=6.3s ... GenericRegressor training | Epoch 29/30 | loss=0.158274 | val_loss=0.195311 | elapsed=6.0s | eta=0.2s GenericRegressor training | Epoch 30/30 | loss=0.157571 | val_loss=0.191838 | elapsed=6.2s | eta=0.0s Log-cosh model R²: 0.492 ``` # Linear Systems with HHL The **Harrow-Hassidim-Lloyd (HHL)** algorithm solves a linear system `Ax = b` on a quantum computer. The `eigenkit` implementation has **two modes**: | `mode` | How the circuit is built | Runs on hardware? | | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | | `"reference"` (default) | The eigenvalues are found classically (`eigvalsh`), `e^{iAt}` is applied as a dense matrix from `expm`, and the evolution time is tuned to the known spectrum. | ❌ No, it runs on a simulator only. | | `"honest"` | There is **no** eigendecomposition, `e^{iAt}` is **Trotterised into elementary gates**, and the time is fixed from a Gershgorin bound that is cheap to compute. | ✅ Yes, the circuit can be built from elementary gates. | `"reference"` mode is fast and exact. However, it does `O(N³)` classical work, and it applies a `2ⁿ × 2ⁿ` unitary that is given only as a matrix, so it gives **no** quantum speedup. `"honest"` mode removes all three shortcuts, so its circuit is one that you could, *in principle*, run on real hardware. This example runs on `provider="numpy"`, which is the default of `HHL`, so it needs no optional extra. The figures need `matplotlib`. The whole page runs in about ten seconds. See the [HHL chapter](https://docs.kit.eigenorbit.com/algorithms/hhl/index.md) for the construction, and the [Algorithms guide](https://docs.kit.eigenorbit.com/user-guide/algorithms/index.md) for the interface. ```python import warnings import matplotlib import matplotlib.pyplot as plt import numpy as np from eigenkit import HHL from eigenkit.datasets import STYLE_COLORS, rc_params # Honest mode is a pure-gate simulation; the numpy backend keeps it fast. Each # figure below is drawn twice, so the site can serve whichever theme matches the # reader's. PROVIDER = "numpy" ``` ## Solving small systems in honest mode We solve a few textbook systems with `mode="honest"`, and compare each answer with the classical one. `"honest"` mode requires `n_eigenvalue_qubits >= 2`, because the clock register must be two-sided to represent negative eigenvalues. From here on, every number comes from the circuit of `"honest"` mode. `"reference"` mode appears only once more, in the section on how `"honest"` mode works below. There, its gate counts are the baseline that `"honest"` mode is defined against. It is not shown as an alternative to run. None of the examples below chooses `n_eigenvalue_qubits` by hand. In the printed rows, `n_l` is the number of clock qubits. `P_readout` is the rate at which a device post-selects, and it falls by about four times for each added clock qubit. The helper below therefore chooses the register: it grows the register until the solver stops warning and the residual is good enough, and then it stops. The section on precision below is the sweep that shows why it should stop there. ```python def cosine(u, v): u, v = np.asarray(u).ravel(), np.asarray(v).ravel() return abs(np.vdot(u, v)) / (np.linalg.norm(u) * np.linalg.norm(v) + 1e-15) def solve_at_chosen_clock(A, b, target=0.03, n_l_max=7, **kwargs): """Solve in honest mode, letting the answer choose the clock register. `n_eigenvalue_qubits` is the one knob honest mode cannot derive for itself. The resolution floor `C` has to clear the smallest eigenvalue of `A`, and the smallest eigenvalue is precisely what the honest path refuses to compute. Two things it may read instead: the warnings the solver raises for itself, and the residual the result already carries: `‖Ax − b‖/‖b‖` is an `O(N²)` check on the answer, not an `O(N³)` look at the spectrum. So grow the register from 2 and stop at the first size that raises no warning and lands inside `target`. Stopping matters as much as starting: each extra clock qubit costs about fourfold in post-selection rate, and the precision section shows the residual does not fall monotonically to pay for it. The smallest register that is good enough is the right one, not the largest that fits. """ A, b = np.asarray(A, float), np.asarray(b, float) x_true = np.linalg.solve(A, b) for n_l in range(2, n_l_max + 1): # A warning here is the solver telling us this register is too coarse, # which is the answer to the question being asked, not a problem to # report, so it is caught rather than printed. with warnings.catch_warnings(record=True) as caught: warnings.simplefilter("always") result = HHL( n_eigenvalue_qubits=n_l, provider=PROVIDER, mode="honest", **kwargs ).solve(A, b) good = not caught and result.residual <= target note = f"{len(caught)} warning(s)" if caught else "" line = ( f" n_l={n_l}: resid={result.residual:.4f} " f"cos={cosine(result.solution, x_true):.4f} " f"P_readout={result.readout_probability:.2e} " f"1 in {round(1 / max(result.readout_probability, 1e-12)):<4d} {note}" + (" <- chosen" if good else "") ) # Rstrip, because `trim trailing whitespace` in pre-commit strips the # page and cannot strip the cell, and the two then never match again. print(line.rstrip()) if good: if x_true.size <= 8: # a wider vector wraps and reads as noise print(f" classical x = {np.round(x_true, 4)}") print(f" honest x = {np.round(result.solution, 4)}") return n_l, result raise RuntimeError( f"no register up to n_l={n_l_max} met target={target}; the system is " "worse conditioned than this notebook's examples" ) ``` ### A 2×2 diagonal system ```text A = [[2, 0], [0, 1]] = (3/2)·I + (1/2)·Z, b = [1, 1] ⇒ x* = [0.5, 1] ``` A diagonal `A` decomposes into `Z` strings only, and these commute with each other, so its Trotterisation is **exact**: even `trotter_steps=1` gives a clean answer. The residual that is left comes from the resolution of the clock, not from Trotter error. This is why the selector below can trade it against the post-selection rate at all. ```python _ = solve_at_chosen_clock(np.diag([2.0, 1.0]), np.array([1.0, 1.0]), trotter_steps=1) ``` ```text n_l=2: resid=0.5586 cos=0.7417 P_readout=5.76e-01 1 in 2 1 warning(s) n_l=3: resid=0.0508 cos=0.9991 P_readout=2.37e-01 1 in 4 n_l=4: resid=0.0125 cos=0.9999 P_readout=4.90e-02 1 in 20 <- chosen classical x = [0.5 1. ] honest x = [0.5062 0.9874] ``` ### A system whose Trotterisation is only approximate ```text A = [[2, 1], [1, 0]] = I + X + Z, b = [1, 0] ⇒ x* = [0, 1] ``` The eigenvalues are `1 ± √2`, so the condition number is `κ(A) ≈ 5.83`, and the spectrum is indefinite (it has eigenvalues of both signs). What matters here is that `X` and `Z` **anticommute**: the Trotter product `e^{iXτ} e^{iZτ}` is not `e^{i(X+Z)τ}`. This is therefore the first example on this page where `trotter_steps` changes the answer at all. The more obvious choice, `[[2, 1], [1, 2]]`, would *not* work as an example. It equals `2I + X`, which has exactly one Pauli term that is not the identity. With a single term, the Trotter product is the exact exponential for any number of steps, so `trotter_steps` has no effect there. The cell below prints both decompositions, so that you can see the difference. An off-diagonal entry is not the same thing as Pauli terms that do not commute. We then let the selector choose a register at the default `trotter_steps="auto"`, and solve again on that same register with a single Trotter step, so that the Trotter grid is the only thing that changes. ```python from eigenkit.algorithms._hamiltonian_sim import pauli_decompose A_1b = np.array([[2.0, 1.0], [1.0, 0.0]]) b_1b = np.array([1.0, 0.0]) # The two-term decomposition that disqualifies [[2,1],[1,2]] as an example here, # and the three-term one that makes [[2,1],[1,0]] a real test of trotter_steps. for M in (np.array([[2.0, 1.0], [1.0, 2.0]]), A_1b): print( f"{M.tolist()} = " + " + ".join(f"{c.real:+.3g}*{s}" for c, s in pauli_decompose(M)) ) # The register is chosen at the default trotter_steps="auto" ... print('\ntrotter_steps="auto" (the default)') n_l_1b, auto_1b = solve_at_chosen_clock(A_1b, b_1b) # ... and then held fixed, so the only thing that changes below is the grid. print("\ntrotter_steps=1 (one step for the whole evolution), same register") one_step = HHL( n_eigenvalue_qubits=n_l_1b, provider=PROVIDER, mode="honest", trotter_steps=1 ).solve(A_1b, b_1b) print( f" n_l={n_l_1b}: resid={one_step.residual:.4f} " f"cos={cosine(one_step.solution, np.linalg.solve(A_1b, b_1b)):.4f} " f"P_readout={one_step.readout_probability:.2e}" ) print(f" honest x = {np.round(one_step.solution, 4)}") ``` ```text [[2.0, 1.0], [1.0, 2.0]] = +2*I + +1*X [[2.0, 1.0], [1.0, 0.0]] = +1*I + +1*X + +1*Z trotter_steps="auto" (the default) n_l=2: resid=0.3827 cos=0.3825 P_readout=6.64e-01 1 in 2 2 warning(s) n_l=3: resid=0.3457 cos=0.5810 P_readout=1.48e-01 1 in 7 2 warning(s) n_l=4: resid=0.0003 cos=1.0000 P_readout=1.67e-01 1 in 6 <- chosen classical x = [0. 1.] honest x = [-3.0000e-04 1.0006e+00] trotter_steps=1 (one step for the whole evolution), same register RuntimeWarning: HHL(mode='honest'): the Trotter drift bound ‖A_eff − A‖ ≲ 0.42 exceeds C/2 = 0.214 at trotter_steps=1, trotter_order=2. The circuit is then exact HHL for a perturbed A_eff: an extremal eigenvalue can be captured by the sign-ambiguous Nyquist bucket, and the eigenvectors rotate, corrupting the direction of the solution without changing its spectrum. Use trotter_steps=6 or 'auto'. n_l=4: resid=0.2412 cos=0.8964 P_readout=7.07e-02 honest x = [0.2341 0.4736] ``` At `trotter_steps=1`, the residual is `0.2412`, and the cosine with `x*` drops to `0.8964`. `trotter_steps="auto"` picks 6 steps, and reaches a residual of `0.0003` with a cosine of `1.0000`. The single-step run also raises a `RuntimeWarning`. The drift bound `‖A_eff − A‖ ≲ 0.42`, where `A_eff` is the Hamiltonian that the Trotter circuit actually simulates, exceeds `C/2 = 0.214`, where `C` is the rotation constant. Quantum phase estimation (QPE) then measures a perturbed Hamiltonian, and not `A`. This warning is the reason for `"auto"`. The selector solves the same bound for the smallest number of steps that stays below `trotter_tolerance · C` (the default tolerance is `0.25`). When you set the number of steps by hand, the warning gives the number that the selector would need (6). ### An indefinite system, with a negative eigenvalue ```text A = [[0, 1], [1, 0]], b = [1, 1] ⇒ x* = [1, 1] ``` The eigenvalues are `±1`. The two-sided clock register resolves the sign of each eigenvalue, and the controlled `RY` inversion gives `1/λ` the correct sign. Both eigenvalues have modulus `1`, so two clock qubits already place them exactly on the grid of buckets. This is the best case on the whole page, and it is worth seeing once. ```python _ = solve_at_chosen_clock( np.array([[0.0, 1.0], [1.0, 0.0]]), np.array([1.0, 1.0]), trotter_steps=1 ) ``` ```text n_l=2: resid=0.0000 cos=1.0000 P_readout=1.00e+00 1 in 1 <- chosen classical x = [1. 1.] honest x = [1. 1.] ``` ### Beyond 2×2: a 4×4 and an 8×8 Nothing so far requires the system to be small. The three cases above are `2 × 2` only so that their Pauli decompositions fit in a line of text. Here is the finite-difference matrix at `N = 4` and `N = 8`, solved from start to finish in `"honest"` mode, with the register chosen in the same way. ```python def tridiagonal_system(dim): """The finite-difference matrix, with b a unit load on the first node.""" A = 2.0 * np.eye(dim) - np.eye(dim, k=1) - np.eye(dim, k=-1) b = np.zeros(dim) b[0] = 1.0 return A, b for dim in (4, 8): A_n, b_n = tridiagonal_system(dim) print( f"{dim}x{dim} tridiagonal: kappa={np.linalg.cond(A_n):.1f}, " f"{len(pauli_decompose(A_n))} Pauli terms, " f"{dim.bit_length() - 1} state qubits" ) solve_at_chosen_clock(A_n, b_n) ``` ```text 4x4 tridiagonal: kappa=9.5, 4 Pauli terms, 2 state qubits n_l=2: resid=0.7017 cos=0.2905 P_readout=2.98e-01 1 in 3 2 warning(s) n_l=3: resid=0.3643 cos=0.5031 P_readout=4.66e-01 1 in 2 2 warning(s) n_l=4: resid=0.1959 cos=0.9339 P_readout=1.45e-01 1 in 7 n_l=5: resid=0.0319 cos=0.9996 P_readout=7.89e-02 1 in 13 n_l=6: resid=0.0086 cos=1.0000 P_readout=1.94e-02 1 in 51 <- chosen classical x = [0.8 0.6 0.4 0.2] honest x = [0.7965 0.593 0.3972 0.2006] 8x8 tridiagonal: kappa=32.2, 8 Pauli terms, 3 state qubits n_l=2: resid=0.7026 cos=0.2008 P_readout=2.97e-01 1 in 3 2 warning(s) n_l=3: resid=0.3671 cos=0.3729 P_readout=3.73e-01 1 in 3 2 warning(s) n_l=4: resid=0.1829 cos=0.5478 P_readout=1.62e-01 1 in 6 1 warning(s) n_l=5: resid=0.1450 cos=0.7137 P_readout=4.64e-02 1 in 22 1 warning(s) n_l=6: resid=0.0190 cos=0.9997 P_readout=3.67e-02 1 in 27 <- chosen classical x = [0.8889 0.7778 0.6667 0.5556 0.4444 0.3333 0.2222 0.1111] honest x = [0.8694 0.7391 0.6207 0.515 0.4105 0.3047 0.2055 0.1027] ``` Both systems stop at six clock qubits, with a cosine of `1.0000` for the `4 × 4` and `0.9997` for the `8 × 8`. The `8 × 8` has `κ = 32`, which is five times the condition number of any of the `2 × 2` examples, and it reaches a residual of `0.019` with one usable preparation in 27. The `8 × 8` system is then a circuit of 10 qubits (3 state qubits, 6 clock qubits and 1 ancilla), and the whole search takes seconds of simulation. Read the rejected rows, because they show why the warning check is needed. The `8 × 8` at `n_l = 5` has a residual of `0.145`, which is bad but not obviously fatal, while its cosine is `0.71`. This means that the *direction* of the answer is wrong, and not only its scale, which is exactly what a residual cannot tell you. The solver flags this row itself: the smallest eigenvalue, `0.121`, is below half the resolution of the clock. These rows are also *cheaper* (1 in 22 against 1 in 27), so a selector based on the post-selection rate alone would have taken one of them. The warning is what rules them out. ## Under the hood: what makes honest mode different Three things distinguish `"honest"` mode from `"reference"` mode. We inspect each of them on the following matrix: ```text A = [[2, 1], [1, 3]] ``` ```python from eigenkit.algorithms._hamiltonian_sim import pauli_decompose A = np.array([[2.0, 1.0], [1.0, 3.0]]) # (i) A is written as a sum of Pauli strings, the building block of gate-level # Hamiltonian simulation. No eigendecomposition involved. terms = pauli_decompose(A) print("Pauli decomposition of A:") for coeff, pstring in terms: print(f" {coeff.real:+.3f} · {pstring}") ``` ```text Pauli decomposition of A: +2.500 · I +1.000 · X -0.500 · Z ``` **(i) Parameters without the spectrum.** The `"honest"` path never calls `eigvalsh`. The evolution time comes from a Gershgorin upper bound on `‖A‖`, which is the largest sum of absolute values over the rows. It costs `O(nnz)`, where `nnz` is the number of non-zero entries of `A`. The rotation constant `C` comes from the resolution of the clock register. No eigenvalues are needed. ```python from eigenkit.algorithms.hhl import ( _gershgorin_spectral_radius, _honest_qpe_params, ) n_l = 5 lam_max_bound = _gershgorin_spectral_radius(A) t, C, _ = _honest_qpe_params(A, n_l) print( f"Gershgorin bound on |λ|_max : {lam_max_bound:.4f} " f"(true = {abs(np.linalg.eigvalsh(A)).max():.4f})" ) print( f"evolution time t : {t:.4f} (fixed; t·bound = {t * lam_max_bound:.4f} < π)" ) print( f"rotation constant C : {C:.4f} (= register resolution, no spectrum used)" ) ``` ```text Gershgorin bound on |λ|_max : 4.0000 (true = 3.6180) evolution time t : 0.7363 (fixed; t·bound = 2.9452 < π) rotation constant C : 0.2667 (= register resolution, no spectrum used) ``` **(ii) Elementary gates throughout.** In `"reference"` mode, `e^{iAt}` enters QPE as a dense `ControlledQubitUnitary` that carries the full `2ⁿ × 2ⁿ` matrix. It is a black box with no decomposition into hardware gates. `"honest"` mode replaces it with a `HamiltonianQPEBlock` built from `H`, `RZ`, controlled-NOT (`CNOT`), `CPHASE`, `PhaseShift` and `SWAP` gates (plus `RX` when the decomposition contains `Y` terms, which this `A` does not). In both modes, the eigenvalue inversion is a uniformly controlled rotation: `2^n_l` `RY` gates and `2^n_l` `CNOT` gates, with no ancillas and no multi-controlled gate. The cell below counts the gates twice: first in the QPE sub-block on its own, and then in the **assembled** HHL circuit. The second count describes the circuit that you would send to a device. The reason to check it separately is that a clean sub-block says nothing about the whole circuit. ```python from collections import Counter import scipy.linalg from eigenkit.algorithms.hhl import _compute_rotation_angles from eigenkit.core.blocks import ( HamiltonianQPEBlock, QPEBlock, QubitUnitary, assemble, ) # (a) the QPE sub-block on its own # Reference QPE: dense oracle e^{iAt} raised to powers. U = scipy.linalg.expm(1j * A * t) ref_oracle = assemble([QubitUnitary(U, wires=(0,))]) ref_gates = QPEBlock(ref_oracle, n_clock=n_l).build(n_l + 1).gates # Honest QPE: Trotterised into elementary gates. hon_gates = HamiltonianQPEBlock(terms, n_clock=n_l, time=t, steps=6).build(n_l + 1).gates print("reference QPE gate counts:", dict(Counter(g.name for g in ref_gates))) print("honest QPE gate counts:", dict(Counter(g.name for g in hon_gates))) dense = [g for g in ref_gates if g.name == "ControlledQubitUnitary"][0] print( f"\nreference evolution oracle is a dense {len(dense.data[0])}×{len(dense.data[0])} " f"controlled unitary (a hardware black box)" ) print( "honest QPE *block* contains ControlledQubitUnitary:", any(g.name == "ControlledQubitUnitary" for g in hon_gates), ) # (b) the assembled HHL circuit # The block above is only the evolution stage. The inversion is the other half, and # a sub-block being free of dense gates says nothing about the whole circuit, so # assemble both modes and count what actually ships. n_b = 1 b_unit = np.array([1.0, 0.0]) angles = _compute_rotation_angles(2**n_l, t, C, lam_max_bound, 0.01) specs = {} for mode, kwargs in (("reference", {}), ("honest", {"trotter_steps": 6})): solver = HHL(n_eigenvalue_qubits=n_l, provider=PROVIDER, mode=mode, **kwargs) specs[mode] = solver._build_circuit_spec(A, b_unit, n_b, n_l, t, angles) counts = dict(Counter(g.name for g in specs[mode].gates)) print(f"\nassembled {mode:9s} HHL circuit: {counts}") dense_names = {"QubitUnitary", "ControlledQubitUnitary"} for mode, spec in specs.items(): n_dense = sum(1 for g in spec.gates if g.name in dense_names) print(f"{mode:>9s}: {n_dense} dense unitary-matrix gates") n_ry = sum(1 for g in specs["honest"].gates if g.name == "RY") print( f"\nhonest inversion: {n_ry} RY gates = 2^n_l = {2**n_l}, each elementary, " f"paired with {n_ry} CNOTs" ) ``` ```text reference QPE gate counts: {'H': 10, 'ControlledQubitUnitary': 5, 'SWAP': 2, 'CPHASE': 10} honest QPE gate counts: {'H': 382, 'PhaseShift': 186, 'RZ': 744, 'CNOT': 744, 'SWAP': 2, 'CPHASE': 10} reference evolution oracle is a dense 2×2 controlled unitary (a hardware black box) honest QPE *block* contains ControlledQubitUnitary: False assembled reference HHL circuit: {'AmplitudeEncoding': 1, 'H': 20, 'ControlledQubitUnitary': 10, 'SWAP': 4, 'CPHASE': 20, 'RY': 32, 'CNOT': 32} assembled honest HHL circuit: {'AmplitudeEncoding': 1, 'H': 1508, 'PhaseShift': 744, 'RZ': 2976, 'CNOT': 3008, 'SWAP': 4, 'CPHASE': 20, 'RY': 32} reference: 10 dense unitary-matrix gates honest: 0 dense unitary-matrix gates honest inversion: 32 RY gates = 2^n_l = 32, each elementary, paired with 32 CNOTs ``` The two counts show that both stages are elementary. The assembled circuit is the claim that matters: it has **no `ControlledQubitUnitary` and no `QubitUnitary` at all**, against 10 in the `"reference"` circuit. These 10 gates are exactly the oracle powers `e^{iAt·2ᵏ}`. The inversion is shared between the two modes, so it is elementary in both, and the dense evolution is the only thing that still separates them. The evolution is the larger part. The QPE block of `"honest"` mode adds only `H`, `RZ`, `CNOT`, `PhaseShift`, `CPHASE` and `SWAP` gates, against the five `2 × 2` oracle powers of the `"reference"` block, each of which is a black box with no decomposition into hardware gates. The inversion then adds `2^n_l = 32` `RY` gates and 32 `CNOT` gates. This is the uniformly controlled decomposition of [Möttönen et al.](https://arxiv.org/abs/quant-ph/0407010), which applies a different `RY(θ_j)` exactly for each of the 32 clock buckets, with one `RY` and one `CNOT` for each step of a Gray-code walk. Written out literally, as one rotation controlled by `n_l` qubits for each bucket, the same stage would be 31 multi-controlled gates. Two claims about this are easy to confuse: - The **circuit** can be built from elementary gates. Every gate above is one that a device implements directly, and the count is `Θ(2^n_l)`. The ladder of controlled evolutions in QPE already costs this much, and by the no-fast-forwarding theorem, no HHL based on QPE avoids it. - The **description** is not compact. The `2^n_l` angles `θ_j = 2 arcsin(C/λ̃_j)` are listed bucket by bucket instead of computed, so the classical description of the circuit has size `Θ(2^n_l)`, and this is not a uniform family of circuits of polynomial size. Making it uniform means evaluating `arcsin(1/λ)` with quantum arithmetic on the clock register. Circuits for the functions that this needs, including the arcsine, are given by Häner et al. 2018 ([arXiv:1805.12445](https://arxiv.org/abs/1805.12445)). ## Trotter accuracy: `trotter_steps` and `trotter_order` For an `A` whose Pauli terms do not commute, the Trotter product approximates `e^{iAt}` with an error that shrinks as `trotter_steps` grows, and that shrinks *faster* for the symmetric second order. With the default `trotter_steps="auto"`, the number of steps is chosen so that this error stays below the limit set by the finite resolution of the clock. This is why the anticommuting example above needed `trotter_steps=1` to show the effect of this setting at all. To see how the two orders scale, we measure the Trotter error of the evolution `e^{iAt}` **directly**, because it is the quantity that ultimately governs the accuracy of `"honest"` mode. We use a 2-qubit `A` whose terms are strongly non-commuting. ```python import scipy.linalg from eigenkit import Circuit from eigenkit.algorithms._hamiltonian_sim import pauli_decompose from eigenkit.core.blocks import TrotterBlock from eigenkit.core.ir import CircuitSpec def block_unitary(gates, n): """Dense unitary of a gate list via the statevector backend (qubit-0-MSB).""" spec = CircuitSpec(n_qubits=n, n_weights=0, n_inputs=0, gates=tuple(gates)) circ = Circuit(spec, provider=PROVIDER) dim = 2**n cols = [ circ.statevector(np.zeros(0), initial_state=np.eye(dim)[j].astype(complex)) for j in range(dim) ] return np.column_stack(cols) A_t = np.array( [ [1.0, 0.7, 0.0, 0.3], [0.7, -0.5, 0.4, 0.0], [0.0, 0.4, 0.8, 0.6], [0.3, 0.0, 0.6, -1.0], ] ) n_state, t_t = 2, 2.0 terms_t = pauli_decompose(A_t) # TrotterBlock skips the identity term (an unobservable global phase), so we # compare against the traceless part of A. A_traceless = A_t - np.trace(A_t) / A_t.shape[0] * np.eye(A_t.shape[0]) exact = scipy.linalg.expm(1j * A_traceless * t_t) step_grid = [1, 2, 4, 8, 16, 32, 64] err = {1: [], 2: []} for order in (1, 2): for s in step_grid: U = block_unitary( TrotterBlock(terms_t, time=t_t, steps=s, order=order).build(n_state).gates, n_state, ) err[order].append(float(np.linalg.norm(U - exact, 2))) print( f"order {order}: " + " ".join(f"{s}:{v:.1e}" for s, v in zip(step_grid, err[order])) ) ``` ```text order 1: 1:1.7e+00 2:9.6e-01 4:4.9e-01 8:2.5e-01 16:1.2e-01 32:6.1e-02 64:3.1e-02 order 2: 1:1.0e+00 2:2.8e-01 4:7.0e-02 8:1.7e-02 16:4.3e-03 32:1.1e-03 64:2.7e-04 ``` The figure shows the two rates on a log-log axis, where a power law is a straight line whose slope is its exponent: ```python # Text inside matplotlib artists needs care here: the docs theme renders serif text # in cmr10, TeX's OT1-encoded font. It has no glyph at all for en dash, U+2212 # minus, or unicode sub/superscripts, and it maps ASCII "|", "_" and "^" to em # dash, dot accent and circumflex. So keep artist text to plain words with ASCII # hyphens, and put every mathematical symbol inside $...$ mathtext, which uses the # correctly encoded cm fonts. for theme in ("light", "dark"): with matplotlib.rc_context(rc_params(theme=theme)): fig, ax = plt.subplots(figsize=(7, 4)) ax.plot( step_grid, err[1], "o-", label="order 1 (Lie-Trotter)", color=STYLE_COLORS[0], ) ax.plot( step_grid, err[2], "s-", label="order 2 (symmetric)", color=STYLE_COLORS[3], ) ax.set_xscale("log", base=2) ax.set_yscale("log") ax.set_xlabel("trotter steps") ax.set_ylabel(r"$\| U_{\mathrm{trotter}} - e^{iAt} \|_2$") ax.set_title(r"Honest HHL: Trotter error of the evolution $e^{iAt}$") ax.legend() fig.tight_layout() fig.savefig(f"trotter_error_{theme}.png", bbox_inches="tight") if theme == "light": plt.show() else: plt.close(fig) ``` The order-1 error falls as `1/steps` (each doubling halves it), while the order-2 error falls as `1/steps²` (each doubling divides it by four). `trotter_order=2` therefore reaches a target accuracy with far fewer steps. At `trotter_steps=64`, the two errors are `3.1e-02` and `2.7e-04`, a factor of 115. You do not normally set `trotter_steps` by hand. `"auto"` solves a drift bound for the number of steps that keeps `‖A_eff − A‖` below `trotter_tolerance × C`. Lower `trotter_tolerance` to get a more accurate and deeper circuit. At order 2, the number of steps grows only as `1/√tolerance`. ## Precision, and what post-selection costs `n_eigenvalue_qubits` (`n_l`) sets the resolution of the clock. In `"honest"` mode, it also sets the rotation constant `C = 2π/(t · 2^n_l)`, which is the smallest eigenvalue that the register can resolve. A smaller `C` handles systems with worse conditioning, but it lowers the post-selection probability, which scales as `(C/λ)²`. This section makes the trade-off explicit. Two probabilities come back, and they are not the same number. `success_probability` is the weight on `ancilla = |1⟩`, summed over **all** clock states. `readout_probability` is the weight on the joint event `ancilla = |1⟩` **and** `clock = |0…0⟩`. This is the slice that the solution is actually read from, and the rate at which a device would post-select. Use the second one to plan the number of shots; the first one overstates it. We sweep `n_l` on `A = [[3, 1], [1, 2]]` (`κ ≈ 2.62`) with the default `trotter_steps="auto"`. Each solve is repeated once as a control, at `trotter_tolerance=0.01`, which is a Trotter target 25 times tighter. If the control follows the series, the error that remains is not Trotter error, and the `off-grid` diagnostic shows what it is instead. ```python A_p = np.array([[3.0, 1.0], [1.0, 2.0]]) b_p = np.array([1.0, 0.0]) x_true_p = np.linalg.solve(A_p, b_p) n_l_grid = [2, 3, 4, 5, 6] res_p, cos_p, prob_p, read_p, res_fine = [], [], [], [], [] for n_l in n_l_grid: kw = dict(n_eigenvalue_qubits=n_l, provider=PROVIDER) # The series itself. This is the one solve left free to warn. r = HHL(**kw, mode="honest").solve(A_p, b_p) # Diagnostic only: the solver never calls eigvalsh in honest mode. How far # off the clock's bucket grid does each eigenvalue land? 0 = exactly on grid, # 0.5 = worst case. t_h, _, _ = _honest_qpe_params(A_p, n_l) grid_pos = 2**n_l * np.abs(np.linalg.eigvalsh(A_p)) * t_h / (2 * np.pi) off_grid = np.abs(grid_pos - np.round(grid_pos)) # The control: the same solve against a 25x tighter Trotter target. It # would repeat the warnings the series has already raised, so it is # silenced: two copies of one warning read as two problems. with warnings.catch_warnings(): warnings.simplefilter("ignore") r_fine = HHL(**kw, mode="honest", trotter_tolerance=0.01).solve(A_p, b_p) res_p.append(r.residual) cos_p.append(cosine(r.solution, x_true_p)) prob_p.append(r.success_probability) read_p.append(r.readout_probability) res_fine.append(r_fine.residual) print( f"n_l={n_l}: residual={r.residual:.4f} cos={cos_p[-1]:.4f} " f"P_ancilla={r.success_probability:.2e} P_readout={r.readout_probability:.2e}" f" | off-grid: {np.round(off_grid, 2)}" ) print(f" control: trotter_tolerance=0.01 -> {r_fine.residual:.4f}") ``` ```text RuntimeWarning: HHL(mode='honest'): σ_min ≈ 1.38 is below half the resolution floor, C/2 = 2, so it rounds into the rotation-free bucket 0, so that component of b is attenuated or dropped — and it carries the largest 1/λ weight, so it is what the direction of x depends on most. At n_eigenvalue_qubits=2: raise n_eigenvalue_qubits to ≥ 3. RuntimeWarning: HHL: relative residual ‖Ax−b‖/‖b‖ = 0.491 — a rough answer, and with κ ≈ 3 the direction of x can be off by κ× that (a small residual bounds the backward error, not the forward one). Raise n_eigenvalue_qubits; in honest mode a finer Trotter grid is the other usual cause. n_l=2: residual=0.4913 cos=0.6116 P_ancilla=7.87e-01 P_readout=6.91e-01 | off-grid: [0.35 0.1 ] control: trotter_tolerance=0.01 -> 0.4913 n_l=3: residual=0.0640 cos=0.9982 P_ancilla=3.89e-01 P_readout=3.53e-01 | off-grid: [0.04 0.29] control: trotter_tolerance=0.01 -> 0.0640 n_l=4: residual=0.0099 cos=1.0000 P_ancilla=9.01e-02 P_readout=6.40e-02 | off-grid: [0.42 0.33] control: trotter_tolerance=0.01 -> 0.0099 n_l=5: residual=0.0371 cos=0.9994 P_ancilla=1.82e-02 P_readout=1.43e-02 | off-grid: [0.18 0.43] control: trotter_tolerance=0.01 -> 0.0371 n_l=6: residual=0.0079 cos=1.0000 P_ancilla=3.80e-03 P_readout=3.25e-03 | off-grid: [0.29 0.04] control: trotter_tolerance=0.01 -> 0.0078 ``` The figure has three panels: what the answer is worth, whether its direction is right, and what reading it costs. ```python # Plain words plus $...$ mathtext only; see the note above the Trotter figure. # The residual label is also set below the rc default size: at 16 pt it is # taller than its panel and spills off the saved figure. for theme in ("light", "dark"): with matplotlib.rc_context(rc_params(theme=theme)): fig, ax = plt.subplots(1, 3, figsize=(14, 4)) ax[0].plot(n_l_grid, res_p, "o-", color=STYLE_COLORS[0], lw=2, label="default") ax[0].plot( n_l_grid, res_fine, "x:", color=STYLE_COLORS[2], lw=2, label="tighter Trotter target", ) ax[0].set_xlabel(r"clock qubits $n_l$") ax[0].set_ylabel(r"residual $\|Ax - b\| / \|b\|$", fontsize=13) ax[0].set_title("Residual, and it wobbles") ax[0].set_yscale("log") ax[0].legend(fontsize=9) ax[1].plot(n_l_grid, cos_p, "s-", color=STYLE_COLORS[5], lw=2) ax[1].set_xlabel(r"clock qubits $n_l$") ax[1].set_ylabel(r"cosine similarity with $x^*$", fontsize=13) ax[1].set_title("Direction accuracy") ax[1].set_ylim(0, 1.05) ax[2].plot( n_l_grid, prob_p, "^-", color=STYLE_COLORS[1], lw=2, label="ancilla only" ) ax[2].plot( n_l_grid, read_p, "v--", color=STYLE_COLORS[6], lw=2, label="ancilla+clock" ) ax[2].set_xlabel(r"clock qubits $n_l$") ax[2].set_ylabel("post-selection probability", fontsize=13) ax[2].set_title("Post-selection cost") ax[2].set_yscale("log") ax[2].legend(fontsize=11) fig.suptitle("Honest HHL: precision and the post-selection trade-off", y=1.02) fig.tight_layout() fig.savefig(f"precision_{theme}.png", bbox_inches="tight") if theme == "light": plt.show() else: plt.close(fig) ``` The residual improves overall, but not monotonically. The measured residuals across `n_l = 2…6` are `0.4913`, `0.0640`, `0.0099`, `0.0371` and `0.0079`. Two clock qubits are not enough to represent the spectrum at all: the residual is `0.49`, the cosine is `0.61`, and `solve()` says so twice. The first warning reports that four clock buckets cannot resolve this `κ`, and names the `n_eigenvalue_qubits` that can. The second warning flags the residual itself as rough. By `n_l = 6`, the residual is 62 times smaller. However, `n_l = 5` is **3.7 times worse than `n_l = 4`**, and the plot shows this dip instead of a straight line. This is not Trotter error, and the control shows it. At `trotter_tolerance=0.01`, a Trotter target 25 times tighter, every point of the sweep comes back within `0.0001` of the default (`n_l = 5` returns the same `0.0371`). The Trotter grid that the selector chose was therefore never the limiting factor. The cause is the readout projection. The solution is read from the `clock = |0…0⟩` slice, and this slice weights each eigenvalue by how well it fits the grid of buckets. The weight is exact on the grid, and up to about 9 % low at an offset of half a bucket (the shortfall is largest for an eigenvalue in the lowest buckets). The pattern changes as eigenvalues move on and off the grid when `n_l` changes, and the `off-grid` column tracks it: `0.42`/`0.33` at `n_l = 4`, `0.18`/**`0.43`** at `n_l = 5` (close to the worst case), and `0.29`/**`0.04`** at `n_l = 6` (almost on the grid). A small irregular variation of the residual as `n_l` grows is therefore expected by construction. The right way to read the trend is its envelope, not the difference from one point to the next. The variation comes from the evolution time. With no spectrum to look at, the `"honest"` path takes `t` from the Gershgorin bound, which is `4.0` here against a true `|λ|_max` of `3.618`. Where the eigenvalues fall between the centres of the buckets is therefore not chosen: it is whatever this `t` and this `n_l` produce. The `off-grid` column shows where they fall, and it changes with `n_l` for reasons that have nothing to do with an improvement in accuracy. This is the one real cost of not diagonalising `A`, and it is a cost in *the alignment of the buckets*, not in the evolution. The `trotter_tolerance` control above has already shown that the Trotterisation is not what limits the answer. The post-selection panel shows the price. The ancilla weight falls from `0.79` to `0.0038` across the sweep. The joint weight of ancilla and clock, which is the rate that matters on hardware, falls from `0.69` to `0.0033`. At `n_l = 4`, the two differ by a factor of `1.4` (`0.090` against `0.064`), so if you quoted `success_probability` as the post-selection rate, you would underestimate the number of shots by that factor. ## Which matrices are actually cheap The one classical cost that the `"honest"` path *cannot* avoid is the decomposition of `A` into Pauli strings, and the number of terms that it finds decides whether a speedup exists at all. The property that matters is sparsity **in the Pauli basis**. This is not the same property as sparsity of the matrix, and confusing the two is the easiest way to claim too much here. Below, `n` is the number of state qubits and `N = 2ⁿ` is the dimension of `A`. - Matrices *built* as a short Pauli sum have `O(poly n)` terms. Examples are local spin Hamiltonians, Ising models and `diag(1…N)`. These matrices are really cheap. - A matrix that is sparse can be dense in the Pauli basis. A random **diagonal** matrix has one non-zero entry per row, and still needs `N` terms. The coefficients of its `Z` strings are the Walsh-Hadamard transform of the diagonal, which is dense for a generic diagonal. - The tridiagonal finite-difference matrix is the standard example of a cheap system, and on a log axis next to `4ⁿ` it looks like one. It needs exactly `N = 2ⁿ` terms, which is exponential in the number of qubits. It is cheap *compared with* a dense matrix, not cheap in absolute terms. Keep one thing separate. The efficiency assumption of the 2009 HHL paper uses a **different input model**. It assumes an `A` that is `s`-sparse, whose rows can be computed efficiently, and that is accessed through a sparse-access oracle. The cost analysis of that model does not carry over to a decomposition into Pauli terms or into a linear combination of unitaries. The `"honest"` path pays the Pauli cost, so that is the cost that this section measures. We count the Pauli terms for four families of matrices, across sizes. ```python def tridiagonal(dim): return 2.0 * np.eye(dim) - np.eye(dim, k=1) - np.eye(dim, k=-1) def random_diagonal(dim, seed): """One non-zero per row: as matrix-sparse as an invertible matrix can be.""" return np.diag(np.random.default_rng(seed).standard_normal(dim)) def sparse_symmetric(dim, seed): """Symmetric with exactly 3 non-zeros per row: a diagonal plus one random cycle.""" r = np.random.default_rng(seed) A = np.diag(r.standard_normal(dim)) order = r.permutation(dim) for k in range(dim): i, j = order[k], order[(k + 1) % dim] A[i, j] = A[j, i] = r.standard_normal() return A def dense_symmetric(dim, seed): m = np.random.default_rng(seed).standard_normal((dim, dim)) return m + m.T def dense_hermitian(dim, seed): r = np.random.default_rng(seed) m = r.standard_normal((dim, dim)) + 1j * r.standard_normal((dim, dim)) return m + m.conj().T sizes = [2, 4, 8, 16, 32, 64] tri_terms, diag_terms, sp3_terms, dense_terms = [], [], [], [] for dim in sizes: n = dim.bit_length() - 1 sp3 = sparse_symmetric(dim, seed=dim) tri_terms.append(len(pauli_decompose(tridiagonal(dim)))) diag_terms.append(len(pauli_decompose(random_diagonal(dim, seed=dim)))) sp3_terms.append(len(pauli_decompose(sp3))) dense_terms.append(len(pauli_decompose(dense_hermitian(dim, seed=dim)))) print( f"dim={dim:3d} (n={n}): tridiagonal={tri_terms[-1]:4d} " f"rand-diagonal={diag_terms[-1]:4d} " f"symmetric({int((sp3 != 0).sum(axis=1).max())} per row)={sp3_terms[-1]:5d} " f"dense={dense_terms[-1]:5d} (N={dim}, 4^n={4**n})" ) # Headline, compared like for like: both matrices below are real symmetric, for # which the Y-odd Pauli strings vanish, so the ceiling is 4^n/2 + N/2 rather # than 4^n. dim = 128 sp3_128 = len(pauli_decompose(sparse_symmetric(dim, seed=dim))) dense_128 = len(pauli_decompose(dense_symmetric(dim, seed=dim))) print( f"\nN={dim}: symmetric with 3 non-zeros per row -> {sp3_128} Pauli terms, " f"against {dense_128} for fully dense real symmetric " f"({100 * sp3_128 / dense_128:.0f}% of dense)" ) ``` ```text dim= 2 (n=1): tridiagonal= 2 rand-diagonal= 2 symmetric(2 per row)= 3 dense= 4 (N=2, 4^n=4) dim= 4 (n=2): tridiagonal= 4 rand-diagonal= 4 symmetric(3 per row)= 8 dense= 16 (N=4, 4^n=16) dim= 8 (n=3): tridiagonal= 8 rand-diagonal= 8 symmetric(3 per row)= 24 dense= 64 (N=8, 4^n=64) dim= 16 (n=4): tridiagonal= 16 rand-diagonal= 16 symmetric(3 per row)= 96 dense= 256 (N=16, 4^n=256) dim= 32 (n=5): tridiagonal= 32 rand-diagonal= 32 symmetric(3 per row)= 368 dense= 1024 (N=32, 4^n=1024) dim= 64 (n=6): tridiagonal= 64 rand-diagonal= 64 symmetric(3 per row)= 1472 dense= 4096 (N=64, 4^n=4096) N=128: symmetric with 3 non-zeros per row -> 5440 Pauli terms, against 8256 for fully dense real symmetric (66% of dense) ``` The figure shows the four families against the two reference curves, `4ⁿ` and `N = 2ⁿ`: ```python ns = [d.bit_length() - 1 for d in sizes] # Plain words plus $...$ mathtext only; see the note above the Trotter figure. for theme in ("light", "dark"): with matplotlib.rc_context(rc_params(theme=theme)): fig, ax = plt.subplots(figsize=(7.5, 4.6)) ax.plot( ns, dense_terms, "o-", color=STYLE_COLORS[0], lw=2, label="dense Hermitian" ) ax.plot( ns, sp3_terms, "s-", color=STYLE_COLORS[1], lw=2, label="symmetric, 3 non-zeros per row", ) ax.plot(ns, tri_terms, "^-", color=STYLE_COLORS[3], lw=2, label="tridiagonal") ax.plot( ns, diag_terms, "v--", color=STYLE_COLORS[5], lw=2, label="random diagonal" ) ax.plot( ns, [4**n for n in ns], ":", color="0.5", lw=2, label=r"$4^n$ (worst case)" ) ax.plot(ns, [2**n for n in ns], "-.", color="0.5", lw=1.5, label=r"$N = 2^n$") ax.set_yscale("log") ax.set_xlabel(r"state qubits $n = \log_2 N$") ax.set_ylabel("number of Pauli terms") ax.set_title("Pauli-basis sparsity is not matrix sparsity") ax.legend(fontsize=11, loc="upper left") fig.tight_layout() fig.savefig(f"scaling_{theme}.png", bbox_inches="tight") if theme == "light": plt.show() else: plt.close(fig) ``` The tridiagonal and random diagonal matrices lie exactly on the `N = 2ⁿ` line, with 2, 4, 8, 16, 32 and 64 terms across the sweep, and only the dense Hermitian matrix follows `4ⁿ`. The plot therefore does not show a cheap family and an expensive one. It shows one family that is exponential with base 2, and one that is exponential with base 4. On a log axis, this gap looks like a difference in kind, and it is not. The third curve is the main point of the section. `sparse_symmetric` is a cycle graph with its vertices relabelled at random. It has exactly 3 non-zero entries per row, which is the number that a tridiagonal matrix has in its interior rows. The relabelling changes nothing about the sparsity of the matrix, and everything about the number of terms: 1472 terms at `N = 64`, against 64 for the tridiagonal matrix. This is a factor of 23, and 36 % of the 4096 terms that a dense Hermitian matrix of that size needs. At `N = 128`, it reaches 5440 terms, against 8256 for a fully dense real symmetric matrix. This is 66 % of a dense matrix, from three non-zero entries per row. The mechanism is the following. A non-zero entry at `(i, j)` contributes to the Pauli strings whose `X`/`Y` support is the bit pattern `i ⊕ j`. The number of terms is therefore set by how many *distinct* offsets `i ⊕ j` the pattern contains, not by how many non-zero entries it has. A band has only the `log₂ N` offsets `2ᵏ − 1`, which gives 7 of them at `N = 128`. This is exactly why a real symmetric tridiagonal matrix with variable coefficients costs `576 = 64 · 7 + 128` terms. A random pattern uses all the offsets. For generic coefficients, the number of terms is exactly `2^(n−1)` for each distinct non-zero offset when `A` is real symmetric, or `2ⁿ` when `A` is complex Hermitian, plus the number of non-zero Walsh-Hadamard coefficients of the diagonal. The `"honest"` path makes this cost *visible*: it is a number of terms that you can print before you build a circuit, while a dense `expm` hides it. The speedup of HHL is real for a well-conditioned `A` that is a short Pauli sum, with an efficient way to prepare `b`. Sparsity of the matrix on its own provides none of these three properties. ### The cheap family, solved The first bullet above is the constructive case: a matrix *written down* as a short Pauli sum, rather than as an array of numbers. A transverse-field Ising Hamiltonian has `2n` terms: the identity shift, a nearest-neighbour `ZZ` term for each pair of neighbouring sites, and a local `X` term on each site. This is linear in the number of qubits, so the number of terms is no longer the obstacle. Once it is shifted to be positive definite, it is a linear system like any other, and `"honest"` mode solves it. ```python from eigenkit.algorithms._hamiltonian_sim import pauli_string_matrix def ising_terms(n, coupling=0.5, field=0.4, shift=3.0): """A transverse-field Ising Hamiltonian, shifted positive definite. Written down as Pauli strings rather than as a matrix, which is what makes it cheap: nearest-neighbour ZZ plus a local X on each site is 2n terms, linear in the qubit count, however large N gets. """ terms = [(shift, "I" * n)] for k in range(n - 1): s = ["I"] * n s[k] = s[k + 1] = "Z" terms.append((coupling, "".join(s))) for k in range(n): s = ["I"] * n s[k] = "X" terms.append((field, "".join(s))) return terms for n in (2, 3, 4): terms_i = ising_terms(n) A_i = sum(c * pauli_string_matrix(s) for c, s in terms_i).real b_i = np.zeros(2**n) b_i[0] = 1.0 print( f"n={n} (N={2**n:2d}): {len(terms_i)} terms written, " f"{len(pauli_decompose(A_i))} found by pauli_decompose, " f"kappa={np.linalg.cond(A_i):.2f}" ) solve_at_chosen_clock(A_i, b_i) ``` ```text n=2 (N= 4): 4 terms written, 4 found by pauli_decompose, kappa=1.92 n_l=2: resid=0.2877 cos=0.9005 P_readout=7.80e-01 1 in 1 2 warning(s) n_l=3: resid=0.0275 cos=0.9997 P_readout=1.70e-01 1 in 6 <- chosen classical x = [ 0.297 -0.0493 -0.0493 0.0113] honest x = [ 0.2973 -0.0518 -0.0518 0.0048] n=3 (N= 8): 6 terms written, 6 found by pauli_decompose, kappa=3.01 n_l=2: resid=0.3071 cos=0.8593 P_readout=7.04e-01 1 in 1 2 warning(s) n_l=3: resid=0.0436 cos=0.9981 P_readout=1.89e-01 1 in 5 n_l=4: resid=0.0361 cos=0.9994 P_readout=4.14e-02 1 in 24 n_l=5: resid=0.0048 cos=1.0000 P_readout=9.31e-03 1 in 107 <- chosen classical x = [ 0.2636 -0.0392 -0.0582 0.0136 -0.0392 0.0165 0.0136 -0.0044] honest x = [ 0.2639 -0.0397 -0.0599 0.0146 -0.0397 0.0174 0.0146 -0.0054] n=4 (N=16): 8 terms written, 8 found by pauli_decompose, kappa=5.47 n_l=2: resid=0.3213 cos=0.8280 P_readout=6.44e-01 1 in 2 2 warning(s) n_l=3: resid=0.0805 cos=0.9494 P_readout=2.19e-01 1 in 5 1 warning(s) n_l=4: resid=0.0228 cos=0.9993 P_readout=4.70e-02 1 in 21 <- chosen ``` `pauli_decompose` finds exactly the terms that were written, so nothing is hidden: 8 terms for the `16 × 16` matrix, against the 256 that a dense Hermitian matrix of that size needs, and the 16 that a random diagonal one needs. The number of terms grows by two for each added qubit, not by a factor of four. The three sizes stop at three different registers, with 3, 5 and 4 clock qubits, and their cosines are `0.9997`, `1.0000` and `0.9993`. The register at which the selector stops is therefore not a function of `N`. It depends on where the eigenvalues of that system happen to fall on the grid of buckets, which is the subject of the precision section. The `16 × 16` system stops at `n_l = 4`, a circuit of 9 qubits, with one usable preparation in 21. This is the cheapest readout of any system of size `8 × 8` or larger on this page, and it is the largest system. This last number is the one to plan the number of shots against, and it comes from `κ` and the resolution of the clock, not from the Pauli decomposition. Nothing about a short Pauli sum makes post-selection cheaper. What it makes cheaper is the circuit that has to be built in the first place. ## Summary | | `"reference"` | `"honest"` | | ---------------------------------------------------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------- | | Eigenvalues | They are found classically with `eigvalsh` (`O(N³)`). | They are **not computed**. | | Evolution `e^{iAt}` | It is a dense black box from `expm`. | It is **Trotterised into elementary gates**. | | Evolution time `t` | It is tuned to the known spectrum. | It is fixed from a Gershgorin bound. | | Rotation constant `C` | It is the true `λ_min`. | It is the resolution of the clock register. | | The evolution `e^{iAt}` can be built from elementary gates | ❌ No, it is a black-box unitary. | ✅ Yes, it uses elementary gates. | | Accuracy settings | The only one is `n_eigenvalue_qubits`. | They are `n_eigenvalue_qubits`, plus `trotter_steps`, `trotter_order` and `trotter_tolerance`. | ```python from eigenkit import HHL # The system from under the hood, so its Pauli decomposition is on the page. A = np.array([[2.0, 1.0], [1.0, 3.0]]) b = np.array([1.0, 0.0]) result = HHL( n_eigenvalue_qubits=4, # clock resolution; the selector above picks this mode="honest", # hardware-faithful path trotter_steps="auto", # default: step count from the drift bound trotter_order=2, # default: symmetric splitting trotter_tolerance=0.25, # default: drift bound target, as a fraction of C provider="numpy", ).solve(A, b) # x ≈ A⁻¹b, read from the statevector; see the read-out caveat below. print(f"solution = {np.round(result.solution, 4)}") # ‖Ax − b‖ / ‖b‖, against the original A and b. print(f"residual = {result.residual:.4f}") # Weight on ancilla = |1⟩ across all clock states … print(f"success_probability = {result.success_probability:.4f}") # … and on ancilla = |1⟩ *and* clock = |0…0⟩, the physical rate: use this one. print(f"readout_probability = {result.readout_probability:.4f}") ``` ```text solution = [ 0.6018 -0.2036] residual = 0.0091 success_probability = 0.1772 readout_probability = 0.1294 ``` The exact answer is `[0.6, -0.2]`, so four clock qubits come within one per cent of it, at one usable preparation in eight. Four is the register that the selector from the start of the page chooses for this system. Six clock qubits would reach a residual of `0.0085` instead of `0.0091`, and cost one preparation in 154. `"honest"` mode does not change one thing. In both modes, the eigenvalue inversion still uses a table of `2^n_l − 1` angles, one for each clock bucket. It is built from elementary gates (the uniformly controlled rotation above), so it costs nothing asymptotically, because the ladder of controlled evolutions in QPE is already `Θ(2^n_l)`. However, it means that the classical description of the circuit grows as `Θ(2^n_l)`, so this is not a uniform family of circuits of polynomial size. There is also a caveat about the readout. HHL natively produces the quantum *state* `|x⟩`. The full `solution` vector here is read from the statevector of the simulator. On real hardware, only expectation values `⟨x|M|x⟩` are available, and reading all `N` components would take `O(N)` measurements. On hardware, HHL is therefore used to pass `|x⟩` to a later quantum routine, or to estimate a single observable. ## Next - [MaxCut with QAOA](https://docs.kit.eigenorbit.com/examples/qaoa-max-cut/index.md), which uses the quantum approximate optimisation algorithm (QAOA), is the other worked example of an algorithm that reads its answer from a full statevector. - The [HHL chapter](https://docs.kit.eigenorbit.com/algorithms/hhl/index.md) explains the two modes, the Trotter settings, and where the speedup does and does not survive. - The [Algorithms guide](https://docs.kit.eigenorbit.com/user-guide/algorithms/index.md) describes `HHL`, `IQAE` and `PortfolioRiskEstimator`, and what each result contains. # Hyperparameter search with QSVC on Iris This example runs a small hyperparameter optimisation (HPO) for an `eigenkit` estimator. It loads a dataset, builds a sklearn pipeline, chooses a search range, runs `GridSearchCV`, and evaluates the selected model. The grid is small on purpose, so that the example finishes quickly. Make it larger once this first quick test passes. ## Imports The example uses tools from sklearn, and `QSVC` (the quantum support vector classifier) and the dataset loader from `eigenkit`. ```python import numpy as np from sklearn.metrics import accuracy_score, confusion_matrix from sklearn.model_selection import GridSearchCV from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from eigenkit import QSVC from eigenkit.datasets import load_dataset ``` ## Load data `load_dataset("iris")` returns a stratified split into a training set and a test set. Iris is small enough for a quick HPO run, and it is still a real classification task with 3 classes. ```python data = load_dataset("iris", split=True, test_size=0.25, random_state=42) X_train = np.asarray(data["X_train"], dtype=np.float32) y_train = np.asarray(data["y_train"], dtype=np.int64) X_test = np.asarray(data["X_test"], dtype=np.float32) y_test = np.asarray(data["y_test"], dtype=np.int64) metadata = data["metadata"] labels, counts = np.unique(y_train, return_counts=True) class_counts = {int(label): int(count) for label, count in zip(labels, counts)} print(f"Dataset: {metadata.name}") print(f"Train shape: {X_train.shape}, test shape: {X_test.shape}") print("Train class counts:", class_counts) ``` ```text Dataset: iris Train shape: (112, 4), test shape: (38, 4) Train class counts: {0: 38, 1: 37, 2: 37} ``` ## Build the pipeline and search range `StandardScaler` gives the features comparable ranges. `GridSearchCV` tunes `QSVC` through the name of its step in the pipeline, so `C` becomes `qsvc__C`. `feature_map="Angle"` is the angle encoding without entanglement. The first grid changes only `C`. Very small values usually underfit, and larger values should give a useful decision boundary. Once this quick run is stable, add another dimension, such as `qsvc__feature_map`. ```python model = Pipeline( [ ("scale", StandardScaler()), ( "qsvc", QSVC( feature_map="Angle", reps=1, random_state=42, ), ), ] ) param_grid = { "qsvc__C": [0.001, 0.01, 0.1, 1.0, 10.0], } print("Search space:", param_grid) ``` ```text Search space: {'qsvc__C': [0.001, 0.01, 0.1, 1.0, 10.0]} ``` ## Run HPO Here, `GridSearchCV` uses 3-fold cross-validation (`cv=3`) on the training split. In the output, "CV" stands for cross-validation. With `refit=True`, it trains the selected pipeline again on the whole training split. ```python search = GridSearchCV( model, param_grid=param_grid, scoring="accuracy", cv=3, n_jobs=1, refit=True, ) search.fit(X_train, y_train) print("Best parameters:", search.best_params_) print(f"Best CV accuracy: {search.best_score_:.3f}") results = search.cv_results_ for mean, std, params in zip( results["mean_test_score"], results["std_test_score"], results["params"], ): print(f"{mean:.3f} +/- {std:.3f} {params}") ``` ```text Best parameters: {'qsvc__C': 1.0} Best CV accuracy: 0.946 0.616 +/- 0.223 {'qsvc__C': 0.001} 0.616 +/- 0.223 {'qsvc__C': 0.01} 0.866 +/- 0.037 {'qsvc__C': 0.1} 0.946 +/- 0.022 {'qsvc__C': 1.0} 0.946 +/- 0.022 {'qsvc__C': 10.0} ``` ## Evaluate the selected model The search does not use the held-out test split. Use it once, at the end, for a fairer check of the selected pipeline. ```python best_model = search.best_estimator_ y_pred = best_model.predict(X_test) test_accuracy = accuracy_score(y_test, y_pred) print(f"Test accuracy: {test_accuracy:.3f}") print("Confusion matrix:") print(confusion_matrix(y_test, y_pred)) print("Predictions for the first five test samples:", y_pred[:5].tolist()) ``` ```text Test accuracy: 0.947 Confusion matrix: [[12 0 0] [ 0 12 1] [ 0 1 12]] Predictions for the first five test samples: [0, 1, 1, 1, 0] ``` # Hybrid Classical-Quantum Model This example places an `Ensemble` layer inside a classical `nn.Sequential` pipeline, and trains the pipeline on the moons dataset. ## Data preparation ```python import torch from sklearn.datasets import make_moons from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler X, y = make_moons(n_samples=1000, noise=0.2, random_state=42) X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, ) scaler = StandardScaler() X_train = scaler.fit_transform(X_train) X_test = scaler.transform(X_test) X_train_t = torch.tensor(X_train, dtype=torch.float32) y_train_t = torch.tensor(y_train, dtype=torch.long) X_test_t = torch.tensor(X_test, dtype=torch.float32) y_test_t = torch.tensor(y_test, dtype=torch.long) ``` ## Model definition The quantum layer is `Ensemble`, placed between two classical linear layers. The first linear layer maps the two features of the moons data to an input with more dimensions, which the quantum circuit can use. ```python import torch from torch import nn from eigenkit import Ensemble class HybridModel(nn.Module): def __init__(self, hidden_size: int, n_qubits: int, depth: int): super().__init__() self.layers = nn.Sequential( nn.Linear(2, hidden_size), nn.ReLU(), Ensemble( in_features=hidden_size, n_qubits=n_qubits, depth=depth, ), nn.ReLU(), nn.Linear(hidden_size, 2), ) def forward(self, x: torch.Tensor) -> torch.Tensor: return self.layers(x) ``` `hidden_size` must be divisible by `n_qubits`, because `Ensemble` splits its input into chunks of size `n_qubits` and runs one independent circuit on each chunk. The outputs of the circuits are concatenated, so the layer returns `hidden_size` values. ## Training loop ```python from torch import optim from torch.utils.data import TensorDataset, DataLoader def train( model, X_train, y_train, X_test, y_test, loss_fn, optimizer, num_epochs=10, batch_size=32, ): loader = DataLoader( TensorDataset(X_train, y_train), batch_size=batch_size, shuffle=True, ) for epoch in range(num_epochs): model.train() running_loss = 0.0 for xb, yb in loader: preds = model(xb) loss = loss_fn(preds, yb) optimizer.zero_grad() loss.backward() optimizer.step() running_loss += loss.item() * xb.size(0) model.eval() with torch.no_grad(): acc = (model(X_test).argmax(1) == y_test).float().mean().item() print( f"Epoch {epoch + 1}/{num_epochs} | " f"loss: {running_loss / len(X_train):.4f} | " f"test_acc: {acc:.4f}" ) ``` ## Run training ```python torch.manual_seed(42) model = HybridModel(hidden_size=12, n_qubits=4, depth=2) loss_fn = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=0.005) train( model, X_train_t, y_train_t, X_test_t, y_test_t, loss_fn, optimizer, num_epochs=15, batch_size=32, ) ``` ```text Epoch 1/15 | loss: 0.6283 | test_acc: 0.8200 ... Epoch 15/15 | loss: 0.1134 | test_acc: 0.9850 ``` ## Visualise predictions ```python import matplotlib.pyplot as plt import matplotlib from matplotlib.colors import ListedColormap from eigenkit.datasets import STYLE_COLORS, rc_params def plot_predictions(model, X_test, y_test): model.eval() with torch.no_grad(): pred = model(X_test).argmax(1) acc = (pred == y_test).float().mean().item() correct = pred.eq(y_test) class_cmap = ListedColormap([STYLE_COLORS[0], STYLE_COLORS[3], STYLE_COLORS[5]]) with matplotlib.rc_context(rc_params(theme="light")): plt.figure(figsize=(8, 6)) for cls in torch.unique(pred).tolist(): color = class_cmap(int(cls) % class_cmap.N) mask = pred.eq(cls) plt.scatter( X_test[mask & correct, 0], X_test[mask & correct, 1], s=28, marker="o", alpha=0.9, label=f"pred={cls} (correct {int((mask & correct).sum())})", color=color, ) if (mask & ~correct).any(): plt.scatter( X_test[mask & ~correct, 0], X_test[mask & ~correct, 1], s=70, marker="x", alpha=0.9, label=f"pred={cls} (errors {int((mask & ~correct).sum())})", color=color, ) plt.title(f"Test predictions - acc={acc:.3f}") plt.legend(fontsize=9) plt.tight_layout() plt.show() plot_predictions(model, X_test_t, y_test_t) ``` See the [Layers guide](https://docs.kit.eigenorbit.com/user-guide/layers/index.md) for the full application programming interface (API). # Pre-built Layers The pre-built layers wrap `Circuit` with reasonable defaults. They are all subclasses of `torch.nn.Module`, and they can be used directly inside `nn.Sequential`. ## `Basic`: one encoding and one variational block ```python import torch from eigenkit import Basic layer = Basic(n_qubits=4, depth=2) out = layer(torch.randn(8, 4)) print(out.shape) # torch.Size([8, 4]) ``` ## `Reupload`: standard data reuploading ```python import torch from eigenkit import Reupload layer = Reupload(n_qubits=4, depth=3, trainable_scale=True) out = layer(torch.randn(8, 4)) print(out.shape) # torch.Size([8, 4]) ``` Here, `in_features == n_qubits`. Set `trainable_scale=True` for the trainable feature scaling of Schuld et al. (2021). ## `Ensemble`: `N` independent circuits over chunks ```python import torch from eigenkit import Ensemble layer = Ensemble(in_features=20, n_qubits=4, depth=5) out = layer(torch.randn(128, 20)) print(out.shape) # torch.Size([128, 20]): 5 circuits * 4 qubits ``` ## Inspecting the underlying circuit The `Basic`, `Reupload` and `Ensemble` layers expose `.circuit` (or `.circuits` for `Ensemble`), so that you can inspect the circuit or save its weights: ```python layer = Basic(n_qubits=4, depth=2) print(layer.circuit.spec.n_weights) # 8 = depth * n_qubits print(list(layer.parameters())) # one Parameter of shape (8,), float64, # randomly initialised unless you seed torch ``` To draw the circuit, call `layer.circuit.draw()`, which returns a drawing as text. For a drawing from a backend, build a circuit for a single call, without a batch, and use the drawing method of that backend (for example `qml.draw_mpl` for PennyLane). # Time series regression with eigenkit: HQNR and RecurrentHQNR compared ## Why this problem matters Forecasts of air quality support public health alerts, decisions on traffic and industry, and early warnings when the level of a pollutant rises sharply. An hourly series with several variables combines the weather, the seasons and human activity. The target at the next step (for example, the concentration of fine particulate matter, PM2.5) is **ordered in time**, so the split into training and test data must keep the order of time instead of shuffling the rows at random. ## Dataset This example uses the **Beijing PM2.5** series: hourly measurements of fine particulate matter in Beijing, with several weather variables. We load the cleaned comma-separated values (CSV) file of eigenkit. Its last column, the PM2.5 concentration, is the regression target. The rows with a missing target were already removed from the file, and the wind direction was already encoded as one-hot columns. - The source of the dataset is the UC Irvine Machine Learning Repository: [Beijing PM2.5](https://archive.ics.uci.edu/dataset/381/beijing+pm2+5+data), by Song Xi Chen. - Its digital object identifier (DOI) is [10.24432/C5JS49](https://doi.org/10.24432/C5JS49). - `load_dataset("beijing_pm25")` downloads the hosted CSV file [kit.eigenorbit.com/datasets/beijing_pm25.csv](https://kit.eigenorbit.com/datasets/beijing_pm25.csv). - The licence is [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) (Creative Commons Attribution 4.0). ## Our approach We compare two hybrid quantum and classical regressors of **`eigenkit`**. Each one is wrapped in **`QuantumTimeSeriesRegressor`**: 1. **`HQNR`** (the hybrid quantum neural network regressor): each sliding window is **flattened** to a single vector per sample. With `autoregressive=True`, each window also holds the past values of the target, as an extra channel (see `QuantumTimeSeriesRegressor` in the library documentation). 1. **`RecurrentHQNR`**: a gated recurrent unit (**GRU**) reads each window as a sequence, and then the quantum block predicts the target at the next step. Both models use the same length of window, the same shift of the horizon and the same held-out test segment, which comes **after** the training data in time. We use only the first rows of each split, so that the example finishes in a reasonable time. For runs on the full data, increase `MAX_TRAIN_ROWS`, `MAX_TEST_ROWS` and `epochs`. ## Import libraries ```python import matplotlib import matplotlib.pyplot as plt import numpy as np import pandas as pd from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score from sklearn.preprocessing import StandardScaler from eigenkit import ( HQNR, QuantumTimeSeriesRegressor, RecurrentHQNR, ) from eigenkit.datasets import STYLE_COLORS, load_dataset, rc_params ``` ## Loading the selected dataset ```python data = load_dataset("beijing_pm25") X_train_full, y_train_full = data["X_train"], data["y_train"] X_test_full, y_test_full = data["X_test"], data["y_test"] # Subsample for notebook runtime (remove or raise limits for full data) MAX_TRAIN_ROWS = 8000 MAX_TEST_ROWS = 2500 X_train = X_train_full[:MAX_TRAIN_ROWS] y_train = y_train_full[:MAX_TRAIN_ROWS] X_test = X_test_full[:MAX_TEST_ROWS] y_test = y_test_full[:MAX_TEST_ROWS] scaler = StandardScaler() X_train = scaler.fit_transform(X_train).astype(np.float32) X_test = scaler.transform(X_test).astype(np.float32) print("Train shape:", X_train.shape, y_train.shape) print("Test shape:", X_test.shape, y_test.shape) ``` ```text Train shape: (8000, 13) (8000,) Test shape: (2500, 13) (2500,) ``` ## Windowing parameters `in_size` is the number of past hours in each window, and `shift=1` means that we predict the step just after the window. With `autoregressive=True`, the wrapper adds the past values of the **target** inside each window, so the number of channels is `n_features + 1`. ```python in_size = 24 shift = 1 n_chan = X_train.shape[1] + 1 # exogenous features + autoregressive target channel flat_dim = in_size * n_chan N_QUBITS = 4 RANDOM_STATE = 42 # Targets aligned with windowed predictions (same for both models) y_test_aligned = y_test[in_size + shift - 1 :] print("Channels per time step (with AR):", n_chan) print("Flattened HQNR input dim:", flat_dim) print("Aligned test targets:", len(y_test_aligned)) ``` ```text Channels per time step (with AR): 14 Flattened HQNR input dim: 336 Aligned test targets: 2476 ``` ## Model 1: HQNR inside `QuantumTimeSeriesRegressor` ```python hqnr_base = HQNR( in_features=flat_dim, in_quantum=N_QUBITS, n_qubits=N_QUBITS, depth=2, layer_type="Basic", variational_type="strong", basis="Y", lr=0.01, optimizer="adam", epochs=500, batch_size=128, loss_func="mse", show_progress=True, random_state=RANDOM_STATE, ) hqnr_ts = QuantumTimeSeriesRegressor( estimator=hqnr_base, in_size=in_size, shift=shift, autoregressive=True, ) hqnr_ts.fit(X_train, y_train) print( "Training complete (HQNR). Last epoch loss:", round(float(hqnr_base.train_losses_[-1]), 6), ) ``` ```text HQNR training | Epoch 1/500 | loss=19248.546565 | elapsed=3.3s | eta=1647.4s HQNR training | Epoch 2/500 | loss=18833.893043 | elapsed=6.5s | eta=1626.9s HQNR training | Epoch 3/500 | loss=17989.033854 | elapsed=9.8s | eta=1626.7s ... HQNR training | Epoch 498/500 | loss=1050.977663 | elapsed=1497.2s | eta=6.0s HQNR training | Epoch 499/500 | loss=1314.121002 | elapsed=1500.4s | eta=3.0s HQNR training | Epoch 500/500 | loss=1180.751416 | elapsed=1503.5s | eta=0.0s Training complete (HQNR). Last epoch loss: 1180.751416 ``` ## Model 2: RecurrentHQNR inside `QuantumTimeSeriesRegressor` ```python seq_base = RecurrentHQNR( seq_feature_dim=n_chan, rnn_type="gru", rnn_hidden_dim=16, rnn_num_layers=1, in_quantum=N_QUBITS, n_qubits=N_QUBITS, depth=2, layer_type="Basic", variational_type="strong", basis="Y", lr=0.005, optimizer="adam", epochs=400, batch_size=128, loss_func="mse", show_progress=True, random_state=RANDOM_STATE, ) seq_ts = QuantumTimeSeriesRegressor( estimator=seq_base, in_size=in_size, shift=shift, autoregressive=True, ) seq_ts.fit(X_train, y_train) print( "Training complete (RecurrentHQNR). Last epoch loss:", round(float(seq_base.train_losses_[-1]), 6), ) ``` ```text RecurrentHQNR training | Epoch 1/400 | loss=19419.129154 | elapsed=4.1s | eta=1643.2s RecurrentHQNR training | Epoch 2/400 | loss=19135.513052 | elapsed=8.6s | eta=1707.3s RecurrentHQNR training | Epoch 3/400 | loss=18939.542535 | elapsed=12.9s | eta=1712.4s ... RecurrentHQNR training | Epoch 398/400 | loss=844.827900 | elapsed=618.8s | eta=3.1s RecurrentHQNR training | Epoch 399/400 | loss=915.601318 | elapsed=620.3s | eta=1.6s RecurrentHQNR training | Epoch 400/400 | loss=793.070036 | elapsed=621.8s | eta=0.0s Training complete (RecurrentHQNR). Last epoch loss: 793.070036 ``` ## Evaluation ```python y_pred_hqnr = hqnr_ts.predict(X_test, y_history=y_test) y_pred_seq = seq_ts.predict(X_test, y_history=y_test) assert len(y_pred_hqnr) == len(y_test_aligned) assert len(y_pred_seq) == len(y_test_aligned) def _report(name: str, y_true: np.ndarray, y_pred: np.ndarray) -> dict: mae = float(mean_absolute_error(y_true, y_pred)) rmse = float(np.sqrt(mean_squared_error(y_true, y_pred))) r2 = float(r2_score(y_true, y_pred)) print(f"{name}") print(f" R2: {r2:.4f}") print(f" MAE: {mae:.4f}") print(f" RMSE: {rmse:.4f}") return {"model": name, "r2": r2, "mae": mae, "rmse": rmse} rows = [ _report("HQNR + QuantumTimeSeriesRegressor", y_test_aligned, y_pred_hqnr), _report("RecurrentHQNR + QuantumTimeSeriesRegressor", y_test_aligned, y_pred_seq), ] ``` ```text HQNR + QuantumTimeSeriesRegressor R2: 0.9184 MAE: 18.3597 RMSE: 33.0781 RecurrentHQNR + QuantumTimeSeriesRegressor R2: 0.8679 MAE: 28.6717 RMSE: 42.0891 ``` ## Plotting results ```python for theme in ("light", "dark"): with matplotlib.rc_context(rc_params(theme=theme)): fig, axes = plt.subplots(2, 2, figsize=(12, 8)) axes[0, 0].plot( hqnr_base.train_losses_, color=STYLE_COLORS[0], label="train loss" ) axes[0, 0].set_title("HQNR training loss") axes[0, 0].set_xlabel("Epoch") axes[0, 0].set_ylabel("MSE") axes[0, 0].legend() axes[0, 1].plot(seq_base.train_losses_, color=STYLE_COLORS[0], label="train loss") axes[0, 1].set_title("RecurrentHQNR training loss") axes[0, 1].set_xlabel("Epoch") axes[0, 1].set_ylabel("MSE") axes[0, 1].legend() n_plot = min(400, len(y_test_aligned)) idx = np.arange(n_plot) axes[1, 0].plot( idx, y_test_aligned[:n_plot], color=STYLE_COLORS[3], label="true", alpha=0.85 ) axes[1, 0].plot( idx, y_pred_hqnr[:n_plot], color=STYLE_COLORS[0], linestyle="--", label="pred", alpha=0.85, ) axes[1, 0].set_title("HQNR forecast (test prefix)") axes[1, 0].set_xlabel("Window index") axes[1, 0].set_ylabel("PM2.5") axes[1, 0].legend() axes[1, 1].plot( idx, y_test_aligned[:n_plot], color=STYLE_COLORS[3], label="true", alpha=0.85 ) axes[1, 1].plot( idx, y_pred_seq[:n_plot], color=STYLE_COLORS[0], linestyle="--", label="pred", alpha=0.85, ) axes[1, 1].set_title("RecurrentHQNR forecast (test prefix)") axes[1, 1].set_xlabel("Window index") axes[1, 1].set_ylabel("PM2.5") axes[1, 1].legend() plt.tight_layout() fig.savefig(f"pollution_hqnr_{theme}.png") if theme == "light": plt.show() else: plt.close(fig) ``` ## Conclusion In this run, both models follow the PM2.5 curve. `HQNR` is a little better: its coefficient of determination (`R²`) is `0.918`, and its root mean squared error (RMSE) is `33.08`. `RecurrentHQNR` reaches `R² = 0.868` and an RMSE of `42.09`. This is not a strict comparison of the two architectures. Both models use the same window and the same training data, but the two architectures behave differently during optimisation. Treat the plot as a worked example, and not as proof that one estimator is always better. The practical lesson is simple: look at the fit of the time series on a plot, and tune the inner estimator. A fair comparison would tune the learning rate of each model separately, use several random seeds, and give both models the same training budget. # MaxCut with QAOA MaxCut asks for the partition of the nodes of a graph into two sets that cuts the most edges. In general, it is NP-hard, where NP stands for nondeterministic polynomial time: no known algorithm solves every case in polynomial time. With six nodes, it is small enough to check by hand, and it is the standard first problem for the Quantum Approximate Optimization Algorithm (QAOA). QAOA is a variational algorithm: its circuit alternates a cost layer and a mixer layer, and a classical optimiser then tunes the two families of angles. This example runs on `provider="numpy"`, the default of `QAOA`, so it needs no optional extra. The whole page runs in about two seconds. See the [QAOA chapter](https://docs.kit.eigenorbit.com/algorithms/qaoa/index.md) for the construction, and the [Optimization guide](https://docs.kit.eigenorbit.com/user-guide/optimization/index.md) for the application programming interface (API). ## The graph, and the answer we are looking for The graph has six nodes in a ring, plus one chord across the ring. It is small enough to list all `64` partitions, so we know the true optimum before we run anything quantum. ```python import itertools import numpy as np from eigenkit.optimization import QAOA, max_cut EDGES = [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 0), (0, 3)] N_NODES = 6 def cut_size(bits): """Edges whose two endpoints land on opposite sides of the partition.""" return sum(1 for u, v in EDGES if bits[u] != bits[v]) best = max(itertools.product([0, 1], repeat=N_NODES), key=cut_size) print("edges ", EDGES) print("best partition", best) print("its cut ", cut_size(best)) ``` ```text edges [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 0), (0, 3)] best partition (0, 1, 0, 1, 0, 1) its cut 7 ``` All seven edges can be cut at once. Flipping every bit gives the same cut, so `(1, 0, 1, 0, 1, 0)` is also optimal. The solutions of MaxCut always come in such complementary pairs. ## From a graph to an Ising model `max_cut()` turns a list of edges into the `Ising` model whose ground state is the maximum cut. ```python problem = max_cut(EDGES) print("type ", type(problem).__name__) print("h ", problem.h) print("J ") print(problem.J) print("constant", problem.constant) ``` ```text type Ising h [0. 0. 0. 0. 0. 0.] J [[0. 0.5 0. 0.5 0. 0.5] [0. 0. 0.5 0. 0. 0. ] [0. 0. 0. 0.5 0. 0. ] [0. 0. 0. 0. 0.5 0. ] [0. 0. 0. 0. 0. 0.5] [0. 0. 0. 0. 0. 0. ]] constant -3.5 ``` The numbers show how the mapping works. There is no local field, so `h` is all zeros: MaxCut does not prefer either side for a node on its own. Each edge adds `0.5` to the upper triangle of `J`, and the constant is `-|E|/2 = -3.5`, where `|E|` is the number of edges. Together, they make the Ising energy equal to minus the size of the cut, so *minimising* the energy *maximises* the cut. `QAOA` also accepts a `QUBO` (quadratic unconstrained binary optimisation problem) if the problem is stated with binary variables instead of spin variables. See the [Optimization guide](https://docs.kit.eigenorbit.com/user-guide/optimization/index.md). ## Solve it ```python result = QAOA(layers=1, random_state=0).solve(problem) print("best_bitstring ", result.best_bitstring) print("best_energy ", result.best_energy) print("expectation ", round(result.expectation, 6)) print("n_iterations ", result.n_iterations) print("success ", result.success) print("optimal_parameters", np.round(result.optimal_parameters, 6)) ``` ```text best_bitstring (1, 0, 1, 0, 1, 0) best_energy -7.0 expectation -5.050979 n_iterations 5 success True optimal_parameters [ 0.708303 -0.392699] ``` `OptimizationResult` has seven fields. These are the ones that you read first: - **`best_bitstring`** is the partition with the lowest energy among the most probable bitstrings of the final state. - **`best_energy`** is its Ising energy, `-7.0`, so it is a cut of `7`. - **`expectation`** is the mean energy of the final state, `-5.05`. It is the quantity that QAOA minimises. - **`optimal_parameters`** holds one `γ` and one `β` for `layers=1`. With more layers, the array grows to `2 * layers` entries. - **`n_iterations`** and **`success`** come from the classical optimiser that tuned the angles. ## Check the answer ```python print("cut of best_bitstring:", cut_size(result.best_bitstring)) print("optimal cut :", cut_size(best)) ``` ```text cut of best_bitstring: 7 optimal cut : 7 ``` One layer returns an optimal partition. To choose `best_bitstring`, `solve` scores the `64` most probable bitstrings with the energy of the problem (or all of them, when there are fewer), and keeps the best one. Six nodes give only `64` bitstrings, so here every partition is scored, and `best_bitstring` does not show the average quality of the state. That quality shows in `expectation`, the quantity that QAOA minimises. ## What depth buys The approximation ratio is the mean cut that the circuit produces, divided by the best cut that exists. ```python optimum = cut_size(best) print("layers expectation approx ratio iterations") for p in (1, 2, 3, 4, 5): r = QAOA(layers=p, random_state=0).solve(problem) print( f"{p:6d} {r.expectation:11.4f} {-r.expectation / optimum:12.4f} {r.n_iterations:10d}" ) ``` ```text layers expectation approx ratio iterations 1 -5.0510 0.7216 5 2 -5.9967 0.8567 11 3 -6.5026 0.9289 25 4 -6.7074 0.9582 69 5 -6.9087 0.9870 61 ``` With one layer, the cuts sampled are `5.05` on average, which is 72 % of the optimum. Five layers reach 98.7 %. The classical optimiser works on `2 * layers` angles, and its number of iterations rises from `5` to about `60` over the same range. ## Reading the distribution `probabilities` is the full distribution over the `2 ** n_nodes` partitions. Its index is the bitstring read as a binary integer, with node `0` as the most significant bit. ```python r3 = QAOA(layers=3, random_state=0).solve(problem) probs = r3.probabilities print("bitstring probability cut") for idx in np.argsort(probs)[::-1][:6]: bits = tuple(int(b) for b in format(int(idx), f"0{N_NODES}b")) print(f"{str(bits):16s} {probs[idx]:11.4f} {cut_size(bits)}") ``` ```text bitstring probability cut (0, 1, 0, 1, 0, 1) 0.4155 7 (1, 0, 1, 0, 1, 0) 0.4155 7 (1, 0, 1, 0, 0, 1) 0.0146 5 (0, 1, 0, 1, 1, 0) 0.0146 5 (0, 0, 1, 1, 0, 1) 0.0146 5 (1, 1, 0, 0, 1, 0) 0.0146 5 ``` The two optimal partitions have a probability of `0.4155` each. Each one is the other with every bit flipped. The QAOA circuit does not change when every qubit is flipped, so their amplitudes are equal by construction. Together, they hold 83 % of the distribution at three layers. The next partitions form pairs for the same reason, and all of them cut `5` edges: this graph has no partition that cuts exactly `6`. ## Next - [Ground-state energy with VQE](https://docs.kit.eigenorbit.com/examples/vqe-fermi-hubbard/index.md) shows the other variational solver, the Variational Quantum Eigensolver (VQE), on a chemistry problem with an exact reference. - The [QAOA chapter](https://docs.kit.eigenorbit.com/algorithms/qaoa/index.md) explains the cost and mixer layers behind `solve()`. - The [Optimization guide](https://docs.kit.eigenorbit.com/user-guide/optimization/index.md) covers `QUBO`, `Ising`, `max_cut()` and the full `OptimizationResult`. # Short-Term Memory with a Quantum Reservoir The **short-term memory capacity** task is the standard test of a reservoir. You feed it a random scalar signal, and you ask the readout to rebuild the signal delayed by `τ` steps. A reservoir with memory rebuilds recent values well, and it gets slowly worse as `τ` grows. A model without memory can only reproduce the present (`τ = 0`). This example compares the three state policies, `"stateless"`, `"carry"` (the default) and `"leaky"`, on that task. It shows *why* a memory that fades over time, and not just any state carried from one step to the next, is what makes a quantum reservoir work. See the [user guide](https://docs.kit.eigenorbit.com/user-guide/reservoir/index.md) for the application programming interface (API). ## The task A scalar signal `s_t ~ Uniform(-1, 1)` drives every qubit of the reservoir (we repeat it across the input slots). The target is the same signal delayed by `τ` steps: `y_t = s_{t-τ}`. ```python import numpy as np from eigenkit import Encoding, Variational from eigenkit import QuantumReservoirRegressor N_QUBITS, MAX_LAG = 5, 5 def delay_data(seed, B, T, tau): rng = np.random.default_rng(seed) signal = rng.uniform(-1.0, 1.0, size=(B, T, 1)) inputs = np.repeat(signal, N_QUBITS, axis=2) # drive every qubit with the signal target = np.zeros((B, T)) target[:, tau:] = signal[:, : T - tau, 0] if tau else signal[:, :, 0] return inputs, target ``` ## The reservoir The reservoir is a circuit of `5` qubits. It encodes the signal as rotation angles, and then applies a fixed, random, entangling variational block. The weights are sampled once (with a seed) and never trained. We read out the full measurement distribution (`features="probabilities"`). ```python blocks = [ Encoding(in_features=N_QUBITS, rotation="RY"), Variational(depth=4, rotation="RY"), ] def memory_curve(**kw): r2 = [] for tau in range(MAX_LAG + 1): Xtr, ytr = delay_data(0, 60, 80, tau) Xte, yte = delay_data(1, 30, 80, tau) model = QuantumReservoirRegressor( blocks, features="probabilities", washout=10, random_state=2, **kw ).fit(Xtr, ytr) r2.append(max(model.score(Xte, yte), 0.0)) return r2 ``` ## Compare the policies ```python configs = { "memoryless (stateless)": dict(state_policy="stateless", window=1), "carry (unitary)": dict(state_policy="carry"), "leaky (alpha=0.5)": dict(state_policy="leaky", leak=0.5), } print( "lag tau: " + " ".join(f"{t:>4d}" for t in range(MAX_LAG + 1)) + " MC" ) for name, kw in configs.items(): curve = memory_curve(**kw) print( f"{name:22s} " + " ".join(f"{v:4.2f}" for v in curve) + f" {sum(curve):4.2f}" ) ``` ```text lag tau: 0 1 2 3 4 5 MC memoryless (stateless) 1.00 0.00 0.00 0.00 0.00 0.00 1.00 carry (unitary) 0.00 0.00 0.00 0.00 0.00 0.00 0.00 leaky (alpha=0.5) 0.96 0.91 0.74 0.53 0.31 0.18 3.63 ``` The last column is the **memory capacity** (MC): the sum of `R²` (the coefficient of determination) over the lags. This single number sums up how much of the history of the input the reservoir keeps. ## What the numbers say - **Memoryless** rebuilds the present perfectly (`R² = 1.00` at `τ = 0`), and nothing else. With only the current input, it has no information about the past, as expected. `MC = 1.0`. - **Pure unitary carry** scores about `0` *everywhere*, even at `τ = 0`. The carry is correct (the state really is `ψ_t = U(x_t)ψ_{t-1}`), but without dissipation, the recurrence mixes the whole history into the state in a way that a *linear* readout cannot separate. There is no clean fading memory to read. `MC ≈ 0`. - **Leaky carry** shows the standard curve of short-term memory: it recalls the recent steps almost perfectly, and the recall fades smoothly with the lag (`0.96 → 0.91 → 0.74 → …`). The leak of weight `(1 - α)` toward the reset state lets old inputs decay, so the recent ones can still be recovered linearly. `MC = 3.63`, which is more than three times the baseline without memory. ## Forecasting the next step Memory is only useful if it helps to *predict*. Here, a fast quasiperiodic signal drives the reservoir, and the readout forecasts the **next** value, `y_t = s_{t+1}`. A fast oscillation moves a lot between steps, so the current sample alone cannot tell where the signal goes next. The forecast needs the context of the previous steps. There are two ways to give the reservoir that context. The classical one is a sliding **window**, which embeds the last `window` values as a time delay (`state_policy="stateless"`, with one qubit for each input in the window). The quantum one is a memory in the state that fades over time (`state_policy="leaky"`, which drives all `5` qubits with the scalar and carries the state). We compare both on the prediction of one step ahead. ```python def signal(seed, B, T): rng = np.random.default_rng(seed) t = np.arange(T + 1) phase = rng.uniform(0, 2 * np.pi, size=(B, 1)) return (np.sin(0.9 * t + phase) + 0.5 * np.sin(1.4 * t + 2 * phase)) / 1.5 T = 120 s_tr, s_te = signal(0, 60, T), signal(1, 30, T) ytr, yte = s_tr[:, 1:], s_te[:, 1:] # predict s_{t+1} raw_tr, raw_te = s_tr[:, :T, None], s_te[:, :T, None] # Classical memory: a sliding window of the last W values (W qubits). for W in (1, 3, 5): bw = [Encoding(in_features=W, rotation="RY"), Variational(depth=4, rotation="RY")] model = QuantumReservoirRegressor( bw, state_policy="stateless", window=W, features="probabilities", washout=20, random_state=2, ).fit(raw_tr, ytr) print(f"stateless window={W} ({W}q) R^2 = {model.score(raw_te, yte):.2f}") # Quantum fading memory: carry state, no explicit window. tiled_tr, tiled_te = np.repeat(raw_tr, N_QUBITS, 2), np.repeat(raw_te, N_QUBITS, 2) leaky = QuantumReservoirRegressor( blocks, state_policy="leaky", leak=0.5, features="probabilities", washout=20, random_state=2, ).fit(tiled_tr, ytr) print(f"leaky carry (5q) R^2 = {leaky.score(tiled_te, yte):.2f}") ``` ```text stateless window=1 (1q) R^2 = 0.01 stateless window=3 (3q) R^2 = 0.87 stateless window=5 (5q) R^2 = 0.98 leaky carry (5q) R^2 = 0.91 ``` With no window (`window=1`), the readout sees only the current value, and it cannot forecast a fast oscillation at all. A short window recovers the local dynamics: three past values already reach `R² = 0.87`, and five reach `0.98`. The leaky reservoir gets close too (`0.91`), by carrying a fading state instead of an explicit window. ## Tuning notes - **`leak`** balances the length of the memory against its clarity. A smaller `leak` (more decay) gives a clearer recall of the recent steps, but a shorter memory. A larger `leak` remembers longer, but with more noise. - **`features`** controls how much information the readout gets. `"probabilities"` (used here) is quadratic in the amplitudes, and it captures more than `"z"`, which gives one value per qubit. For structure specific to a task, pass a Hamiltonian string or a list of observables. - **The size of the reservoir and `weight_scale`** set how rich the dynamics are. More qubits and larger weights generally raise the memory capacity, up to a point. # Ground-State Energy with VQE This example finds the ground-state energy of a small model with the variational quantum eigensolver (VQE). The model is the Fermi-Hubbard dimer: two sites, two electrons, a hopping term `t` and an on-site repulsion `u`. Its ground-state energy is known in closed form, so the result can be checked exactly. The example runs on `provider="numpy"`, which is the default of `VQE`, and it uses only the fermionic tools built into the library, so it does not need `openfermion` or `pyscf`. The whole page runs in under two seconds. See the [VQE chapter](https://docs.kit.eigenorbit.com/algorithms/vqe/index.md) for the algorithm, and the [Quantum Chemistry guide](https://docs.kit.eigenorbit.com/user-guide/chemistry/index.md) for the interface. ## The model ```python import numpy as np from eigenkit.chemistry import ( VQE, exact_ground_state_energy, fermi_hubbard_dimer, fermi_hubbard_dimer_ground_energy, jordan_wigner, uccsd_ansatz, ) T, U, N_QUBITS = 1.0, 2.0, 4 fermionic = fermi_hubbard_dimer(t=T, u=U) analytic = fermi_hubbard_dimer_ground_energy(t=T, u=U) print("operator type :", type(fermionic).__name__) print("analytic ground:", analytic) ``` ```text operator type : FermionOperator analytic ground: -1.2360679774997898 ``` The model has four spin-orbitals (two sites, each with two spins), so it needs four qubits after the mapping. ## Mapping fermions to qubits `jordan_wigner()` returns two values: the qubit `Observable`, and a scalar offset that holds the identity part of the Hamiltonian. ```python hamiltonian, offset = jordan_wigner(fermionic) print("pauli terms:", len(hamiltonian.terms)) print("offset :", offset) for term in hamiltonian.terms[:4]: print(" ", term.coeff, term.paulis) ``` ```text pauli terms: 10 offset : 1.0 -0.5 (('Y', 0), ('Z', 1), ('Y', 2)) -0.5 (('X', 0), ('Z', 1), ('X', 2)) -0.5 (('Y', 1), ('Z', 2), ('Y', 3)) -0.5 (('X', 1), ('Z', 2), ('X', 3)) ``` The first four terms are the hopping terms. Each one moves an electron from one site to the other, and the `Z` between the two ends gives the sign that fermions need. The `Z` and `ZZ` terms that follow are the on-site repulsion. The offset must be included in every energy calculation. Here it is exactly `1.0`, and if you leave it out, every result is shifted by this amount. ```python without = exact_ground_state_energy(hamiltonian, N_QUBITS) with_off = exact_ground_state_energy(hamiltonian, N_QUBITS, offset) print("without the offset:", without) print("with the offset :", with_off) print("analytic :", analytic) ``` ```text without the offset: -2.23606797749979 with the offset : -1.2360679774997898 analytic : -1.2360679774997898 ``` `exact_ground_state_energy()` diagonalises the Hamiltonian directly. At four qubits, this is a `16 × 16` matrix. ## Running VQE ```python result = VQE(layers=2, random_state=0).solve(hamiltonian, N_QUBITS, offset=offset) print("energy ", result.energy) print("error vs exact", abs(result.energy - analytic)) print("n_iterations ", result.n_iterations) print("success ", result.success) print("n parameters ", len(result.optimal_parameters)) ``` ```text energy -0.9999999991069795 error vs exact 0.23606797839281035 n_iterations 20 success True n parameters 20 ``` `success` only reports that the classical optimiser converged. Here it converged to `-1.0000`, which is `0.236` above the ground-state energy. `-1.0` is itself an eigenvalue of this Hamiltonian. It is the lowest energy with one electron, and the default hardware-efficient ansatz with two layers cannot reach the lower two-electron state. ## Depth of the default ansatz `layers` sets the depth of the hardware-efficient ansatz that `VQE` builds when no `ansatz` is given. ```python print("layers params energy error") for layers in (1, 2, 3, 4): r = VQE(layers=layers, random_state=0).solve(hamiltonian, N_QUBITS, offset=offset) print( f"{layers:6d} {len(r.optimal_parameters):6d} {r.energy:.10f} {abs(r.energy - analytic):.3e}" ) ``` ```text layers params energy error 1 12 -0.8535533903 3.825e-01 2 20 -0.9999999991 2.361e-01 3 28 -0.9999999893 2.361e-01 4 36 -1.2360679394 3.806e-08 ``` Two and three layers both stop at `-1.0`. Four layers, with `36` parameters, reach the ground-state energy with an error of `3.8e-8`. ## A chemistry-aware ansatz `uccsd_ansatz()` builds the unitary coupled cluster circuit with single and double excitations (UCCSD), approximated by Trotterisation, on the Hartree-Fock reference state for a given number of electrons. `reps` is the number of Trotter repetitions of the product of excitations. ```python print("reps params energy error") for reps in (1, 2, 3): ansatz = uccsd_ansatz(n_qubits=N_QUBITS, n_electrons=2, reps=reps) r = VQE(random_state=0).solve(hamiltonian, N_QUBITS, offset=offset, ansatz=ansatz) print( f"{reps:4d} {len(r.optimal_parameters):6d} {r.energy:.10f} {abs(r.energy - analytic):.3e}" ) ``` ```text reps params energy error 1 5 -0.9999999971 2.361e-01 2 10 -1.2360679758 1.657e-09 3 15 -1.2360679775 2.675e-12 ``` One repetition stops at `-1.0`, the same value where the shallow hardware-efficient circuits stopped. The first-order Trotter error is the limit here. Two repetitions, with `10` parameters, give an error of `1.7e-9`, compared with `3.8e-8` for the hardware-efficient ansatz with `36` parameters. Three repetitions reach an error of `2.7e-12`. ## Next - [MaxCut with QAOA](https://docs.kit.eigenorbit.com/examples/qaoa-max-cut/index.md) shows the other variational solver, the Quantum Approximate Optimization Algorithm (QAOA), on a combinatorial problem. - The [VQE chapter](https://docs.kit.eigenorbit.com/algorithms/vqe/index.md) explains the ansätze and the mappings. - The [Quantum Chemistry guide](https://docs.kit.eigenorbit.com/user-guide/chemistry/index.md) covers `Molecule`, `bravyi_kitaev()` and the rest of the package. Real molecules need the `chemistry` extra. # API Reference - [algorithms](https://docs.kit.eigenorbit.com/api/algorithms/index.md) - [hhl](https://docs.kit.eigenorbit.com/api/algorithms/hhl/index.md) - [iqae](https://docs.kit.eigenorbit.com/api/algorithms/iqae/index.md) - [portfolio_risk](https://docs.kit.eigenorbit.com/api/algorithms/portfolio_risk/index.md) - [benchmark](https://docs.kit.eigenorbit.com/api/benchmark/index.md) - [chemistry](https://docs.kit.eigenorbit.com/api/chemistry/index.md) - [ansatz](https://docs.kit.eigenorbit.com/api/chemistry/ansatz/index.md) - [exact](https://docs.kit.eigenorbit.com/api/chemistry/exact/index.md) - [fermion](https://docs.kit.eigenorbit.com/api/chemistry/fermion/index.md) - [interop](https://docs.kit.eigenorbit.com/api/chemistry/interop/index.md) - [mappings](https://docs.kit.eigenorbit.com/api/chemistry/mappings/index.md) - [models](https://docs.kit.eigenorbit.com/api/chemistry/models/index.md) - [molecule](https://docs.kit.eigenorbit.com/api/chemistry/molecule/index.md) - [vqe](https://docs.kit.eigenorbit.com/api/chemistry/vqe/index.md) - [circuit](https://docs.kit.eigenorbit.com/api/circuit/index.md) - [configs](https://docs.kit.eigenorbit.com/api/configs/index.md) - [core](https://docs.kit.eigenorbit.com/api/core/index.md) - [backends](https://docs.kit.eigenorbit.com/api/core/backends/index.md) - [braket_backend](https://docs.kit.eigenorbit.com/api/core/backends/braket_backend/index.md) - [cudaquantum_backend](https://docs.kit.eigenorbit.com/api/core/backends/cudaquantum_backend/index.md) - [iqm_backend](https://docs.kit.eigenorbit.com/api/core/backends/iqm_backend/index.md) - [numpy_backend](https://docs.kit.eigenorbit.com/api/core/backends/numpy_backend/index.md) - [pennylane_backend](https://docs.kit.eigenorbit.com/api/core/backends/pennylane_backend/index.md) - [qiskit_aer_backend](https://docs.kit.eigenorbit.com/api/core/backends/qiskit_aer_backend/index.md) - [qiskit_backend](https://docs.kit.eigenorbit.com/api/core/backends/qiskit_backend/index.md) - [qiskit_runtime_backend](https://docs.kit.eigenorbit.com/api/core/backends/qiskit_runtime_backend/index.md) - [qsim_backend](https://docs.kit.eigenorbit.com/api/core/backends/qsim_backend/index.md) - [qulacs_backend](https://docs.kit.eigenorbit.com/api/core/backends/qulacs_backend/index.md) - [tensorcircuit_backend](https://docs.kit.eigenorbit.com/api/core/backends/tensorcircuit_backend/index.md) - [blocks](https://docs.kit.eigenorbit.com/api/core/blocks/index.md) - [builder](https://docs.kit.eigenorbit.com/api/core/builder/index.md) - [circuit](https://docs.kit.eigenorbit.com/api/core/circuit/index.md) - [draw](https://docs.kit.eigenorbit.com/api/core/draw/index.md) - [draw_mpl](https://docs.kit.eigenorbit.com/api/core/draw_mpl/index.md) - [interfaces](https://docs.kit.eigenorbit.com/api/core/interfaces/index.md) - [ir](https://docs.kit.eigenorbit.com/api/core/ir/index.md) - [observables](https://docs.kit.eigenorbit.com/api/core/observables/index.md) - [optimizers](https://docs.kit.eigenorbit.com/api/core/optimizers/index.md) - [provider](https://docs.kit.eigenorbit.com/api/core/provider/index.md) - [secret_fields](https://docs.kit.eigenorbit.com/api/core/secret_fields/index.md) - [solver](https://docs.kit.eigenorbit.com/api/core/solver/index.md) - [variational](https://docs.kit.eigenorbit.com/api/core/variational/index.md) - [wire](https://docs.kit.eigenorbit.com/api/core/wire/index.md) - [datasets](https://docs.kit.eigenorbit.com/api/datasets/index.md) - [eda](https://docs.kit.eigenorbit.com/api/datasets/eda/index.md) - [optimization](https://docs.kit.eigenorbit.com/api/optimization/index.md) - [problems](https://docs.kit.eigenorbit.com/api/optimization/problems/index.md) - [qaoa](https://docs.kit.eigenorbit.com/api/optimization/qaoa/index.md) - [photonics](https://docs.kit.eigenorbit.com/api/photonics/index.md) - [backends](https://docs.kit.eigenorbit.com/api/photonics/backends/index.md) - [perceval_backend](https://docs.kit.eigenorbit.com/api/photonics/backends/perceval_backend/index.md) - [perceval_remote](https://docs.kit.eigenorbit.com/api/photonics/backends/perceval_remote/index.md) - [executor](https://docs.kit.eigenorbit.com/api/photonics/executor/index.md) - [gradients](https://docs.kit.eigenorbit.com/api/photonics/gradients/index.md) - [ir](https://docs.kit.eigenorbit.com/api/photonics/ir/index.md) - [reservoir](https://docs.kit.eigenorbit.com/api/photonics/reservoir/index.md) - [torch](https://docs.kit.eigenorbit.com/api/photonics/torch/index.md) - [qml](https://docs.kit.eigenorbit.com/api/qml/index.md) - [qrc](https://docs.kit.eigenorbit.com/api/qml/qrc/index.md) - [qsvc](https://docs.kit.eigenorbit.com/api/qml/qsvc/index.md) - [qsvr](https://docs.kit.eigenorbit.com/api/qml/qsvr/index.md) - [reservoir](https://docs.kit.eigenorbit.com/api/reservoir/index.md) - [driver](https://docs.kit.eigenorbit.com/api/reservoir/driver/index.md) - [features](https://docs.kit.eigenorbit.com/api/reservoir/features/index.md) - [policies](https://docs.kit.eigenorbit.com/api/reservoir/policies/index.md) - [readout](https://docs.kit.eigenorbit.com/api/reservoir/readout/index.md) - [torch](https://docs.kit.eigenorbit.com/api/torch/index.md) - [circuit](https://docs.kit.eigenorbit.com/api/torch/circuit/index.md) - [estimators](https://docs.kit.eigenorbit.com/api/torch/estimators/index.md) - [custom](https://docs.kit.eigenorbit.com/api/torch/estimators/custom/index.md) - [generic](https://docs.kit.eigenorbit.com/api/torch/estimators/generic/index.md) - [hqnc](https://docs.kit.eigenorbit.com/api/torch/estimators/hqnc/index.md) - [hqnr](https://docs.kit.eigenorbit.com/api/torch/estimators/hqnr/index.md) - [recurrent_hqnc](https://docs.kit.eigenorbit.com/api/torch/estimators/recurrent_hqnc/index.md) - [recurrent_hqnr](https://docs.kit.eigenorbit.com/api/torch/estimators/recurrent_hqnr/index.md) - [timeseries](https://docs.kit.eigenorbit.com/api/torch/estimators/timeseries/index.md) - [interface](https://docs.kit.eigenorbit.com/api/torch/interface/index.md) - [layers](https://docs.kit.eigenorbit.com/api/torch/layers/index.md) - [basic](https://docs.kit.eigenorbit.com/api/torch/layers/basic/index.md) - [ensemble](https://docs.kit.eigenorbit.com/api/torch/layers/ensemble/index.md) - [minmax](https://docs.kit.eigenorbit.com/api/torch/layers/minmax/index.md) - [reupload](https://docs.kit.eigenorbit.com/api/torch/layers/reupload/index.md) # algorithms ## `eigenkit.algorithms.HHL` Bases: `BaseQuantumSolver` Quantum linear solver based on the algorithm of Harrow, Hassidim and Lloyd (HHL). Solves `Ax = b` for any invertible square matrix `A`. If `A` is Hermitian, it is used directly, even when it is not positive semidefinite. Any other matrix is converted to the normal equations `A†Ax = A†b`, which have the same solution and a Hermitian, positive semidefinite system matrix. The cost of this conversion is that the condition number `κ` is squared. The circuit itself inverts the eigenvalues. It applies controlled `RY` rotations to the ancilla, with one fixed angle for each state of the clock register, so the reciprocal `1/λ` of each eigenvalue is encoded in the amplitudes without any classical division by the eigenvalues. The readout is different: it uses the simulator's full statevector and keeps only the part where the ancilla is in `|1⟩` and the clock register is in `|0⟩`. HHL produces the quantum state `|x⟩`, so reading a classical vector from it is only possible on a simulator. See HHLResult. Warning The backend must provide `statevector()` on its compiled circuit. The built-in numpy simulator, PennyLane `default.qubit` and `lightning.qubit`, and Qiskit Aer all do. A custom backend registered with register() must implement it, otherwise `solve()` raises `NotImplementedError`. **Scope and scaling.** HHL is efficient only when three conditions hold: `A` is a sum of a few short Pauli terms, `A` is well conditioned, and the state `|b⟩` can be prepared efficiently. - *Hamiltonian simulation.* `e^{iAt}` is Trotterised from the Pauli decomposition of `A`, so its cost follows the number of Pauli terms. This means sparsity in the Pauli basis, which is different from sparsity of the matrix. The module `algorithms/_hamiltonian_sim.py` explains what this means in practice. In short, a sparse matrix is not necessarily cheap to simulate here. - *State preparation.* `|b⟩` is prepared with AmplitudeEncoding, which is treated as an oracle of unit cost. Loading general amplitudes costs `O(N)` gates, so a real speedup assumes that `b` comes from a formula, from an earlier quantum routine, or from a quantum random-access memory (qRAM). - *Conditioning.* The condition number `κ` is never an input. It only bounds the achievable success probability and the size of the clock register. - *Inversion angles.* The gates are elementary (see `_uniformly_controlled_ry()`), so the circuit built in `"honest"` mode contains no unitary-matrix gate. However, the `2^n_l` angles, where `n_l` is the number of clock qubits, are listed one by one instead of being computed by the circuit. The classical description of the circuit therefore has size `Θ(2^n_l)`. The circuit can be built from elementary gates, but its description is not compact. Solvers for dense matrices, for example the one of Wossnig, Zhao and Prakash, reach a quadratic speedup when the spectral norm is bounded. They need a specialised memory architecture that this package does not provide. **Real and complex inputs.** For real `A` and `b`, the readout discards the imaginary part. The leading error of the Trotter eigenvectors enters the raw vector as a purely imaginary component, so discarding the imaginary part removes it. This matters at `trotter_order=1`, where it improves the convergence by one order: the measured error falls as `steps⁻²` for real inputs, and as `steps⁻¹` for a complex Hermitian `A` or a complex `b`, which keep that error. At `trotter_order=2`, the default, the first-order commutator already cancels by construction, so there is nothing left to remove, and real and complex inputs both converge as `steps⁻²`. This advantage was not designed. It depends on the test for real inputs in solve(), so a change to that test changes the convergence rate at order 1. Parameters: | Name | Type | Description | Default | | --------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `n_eigenvalue_qubits` | `int` | The number of qubits in the clock register. Eigenvalues are resolved into 2\*\*n_eigenvalue_qubits buckets. The value must be at least 1, and at least 2 in "honest" mode. More qubits give a finer resolution of the spectrum and a larger circuit. The default is 4. | `4` | | `t` | \`float | None\` | The evolution time for U = e^{iAt}. If given, it must be positive and finite. If None, it is chosen automatically: from the spectrum in "reference" mode, and from a Gershgorin bound in "honest" mode. The default is None. | | `epsilon` | `float` | The relative eigenvalue cutoff, in \[0, 1). For a Hermitian A, buckets whose estimated eigenvalue is below epsilon * λ_max are treated as zero. For the normal equations, the cutoff applies to the singular values instead, and it is implemented as epsilon² on the squared spectrum. If no eigenvalue is above the cutoff, solve() raises an error. The default is 0.01. | `0.01` | | `provider` | `ProviderSpec` | The backend to use: a registered name ("numpy", "pennylane", "qiskit" and so on), a config instance, or a backend object. It must provide statevector(). The default is "numpy". | `'numpy'` | | `mode` | `str` | How the circuit is built. This argument is keyword-only. "reference" (the default): the eigenvalues are computed classically with eigvalsh, e^{iAt} is a dense QubitUnitary computed with expm, and t is tuned to the known spectrum. This mode is fast and exact, but the O(N³) precomputation and the dense unitary mean that its cost does not follow the scaling of HHL. "honest": the circuit is built as it would be on hardware. No eigendecomposition sets any circuit parameter: t comes from a Gershgorin bound, the scaling constant C of the inversion comes from the resolution of the clock register (see \_honest_qpe_params()), and e^{iAt} is Trotterised into elementary gates. This mode requires n_eigenvalue_qubits >= 2. Input validation still runs an O(N³) rank check. This check makes the solver safer to use and is not part of the algorithm; see solve(). | `'reference'` | | `trotter_steps` | \`int | Literal['auto']\` | The number of Trotter steps for the base evolution time in "honest" mode. It is ignored in "reference" mode. With "auto", the default, the solver picks the smallest number of steps for which a computable bound on the Trotter drift ‖A_eff − A‖ stays below trotter_tolerance × C, which is below the eigenvalue resolution of the clock register. This matters because, with a fixed step size, the circuit is exact HHL for an effective matrix A_eff whose eigenvectors are rotated even when its eigenvalues are exact. That changes the direction of the answer while every check based on the spectrum reports success. An integer turns off the automatic choice, and the solver warns if the drift bound then exceeds C/2. Above that value, an extreme eigenvalue can be captured by the Nyquist bucket or wrap around the branch cut. The automatic choice is limited by a gate budget, and the solver says so when the limit is reached. The default is "auto". | | `trotter_order` | `int` | The Trotter order, 1 or 2. The default is 2. The symmetric product of order 2 is a palindrome, so the first-order commutator cancels, the error falls from O(τ) to O(τ²), and A_eff stays real for a real A. At order 1, A_eff picks up Y-type terms, and a real system returns a complex state. Order 1 is kept for cost comparisons. | `2` | | `trotter_tolerance` | `float` | The target for trotter_steps="auto". This argument is keyword-only. The drift bound must not exceed trotter_tolerance × C. The default, 0.25, keeps the drift at a quarter of a bucket, below the C/2 threshold at which the Nyquist bucket can capture an eigenvalue. At order 2, the cost grows as steps ∝ 1/√tolerance. | `_AUTO_TROTTER_TOLERANCE` | Raises: | Type | Description | | ------------ | ---------------------------------------------------------------------------------------------------------- | | `ValueError` | If n_eigenvalue_qubits is below 1, if t is not a positive finite number, or if epsilon is outside \[0, 1). | Example This example solves a `2 × 2` Hermitian system: ```python import numpy as np from eigenkit import HHL A = np.array([[2, 1], [1, 2]], dtype=float) b = np.array([1, 0], dtype=float) result = HHL(n_eigenvalue_qubits=6).solve(A, b) print(result.solution) print(result.residual) ``` This example solves the same system on a faster simulator: ```python from eigenkit import HHL, PennyLaneConfig solver = HHL( n_eigenvalue_qubits=6, provider=PennyLaneConfig(device_name="lightning.qubit"), ) result = solver.solve(A, b) ``` ### `solve(A, b)` Solve the linear system `Ax = b`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ----------------------------------------------------------------------------------------------------------------------- | ---------- | | `A` | `ndarray` | A real or complex square matrix, which must be invertible. A non-Hermitian matrix is converted to the normal equations. | *required* | | `b` | `ndarray` | The right-hand side, of length A.shape[0]. | *required* | Returns: | Type | Description | | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | `HHLResult` | An HHLResult. Its solution is read from the simulator's statevector and completed with a classical least-squares rescaling. See the notes on HHLResult. | Raises: | Type | Description | | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | `ValueError` | If A is not square or is singular, if b has the wrong length, is zero or is not finite, or if t causes aliasing in quantum phase estimation (QPE). | | `NotImplementedError` | If the backend does not implement statevector(), or if it returns something else, such as a density matrix. | Note Validation runs one `O(N³)` singular value decomposition (SVD) to reject a singular `A` and to compute a condition number for the warnings. No quantity derived from it reaches `t`, `C`, the rotation angles or the number of Trotter steps, and changing it would leave the circuit exactly the same. `mode="honest"` therefore keeps its defining property, but `solve()` itself still does `O(N³)` work. ## `eigenkit.algorithms.HHLResult` Bases: `ValueEquality` Output of HHL.solve(). The result is read-only. Assigning to a field raises `FrozenInstanceError`, and writing into an array field raises `ValueError`. This prevents a mistyped `=` (instead of `==`) or an in-place edit from leaving a result whose scalar fields describe a different run from the arrays it holds. To get a writable copy of an array, use `np.array(result.field)`. Results compare by value, including their arrays. They are not hashable, because a result is a collection of numbers and not a key, and a correct hash of a float array is expensive to compute. Attributes: | Name | Type | Description | | --------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | `solution` | `ndarray` | The best-fit x with Ax ≈ b. It is float64 for real inputs, and complex128 when A or b has a non-zero imaginary part. | | `euclidean_norm` | `float` | The norm ‖solution‖ of the returned solution. It is computed after the classical least-squares rescaling, so it is not a norm that HHL itself measures. | | `residual` | `float` | The relative error ‖Ax − b‖ / ‖b‖, computed with the original A and b. | | `success_probability` | `float` | The probability that the ancilla is in | | `readout_probability` | `float` | The probability of the joint event from which the solution is read: the ancilla in | | `raw_solution` | \`ndarray | None\` | Note HHL produces the quantum state `|x⟩`, not a vector of numbers. The full array in this result is read from the simulator's statevector, which only a simulator can provide. On hardware, only expectation values can be measured, and extracting all `N` components of `|x⟩` costs `O(N)` measurements, which cancels the speedup. In practice, `|x⟩` is passed to another quantum routine, or used to estimate one observable. Recovering the sign of `1/λ` needs a separate protocol. All of this applies to both values of `mode`. Warning `solution` is completed by two classical steps that hardware cannot perform. For real inputs, the imaginary part is discarded. The magnitude and the sign then come from a least-squares projection, `x = raw · ⟨A·raw, b⟩ / ‖A·raw‖²`, which uses the full classical `A` and `b`. Only the direction of `raw` comes from the quantum computation. The rescaling can change `residual` by an order of magnitude, and for an indefinite `A` its sign correction is not reliable, because `⟨Ax, b⟩ ≥ 0` does not imply `⟨x, A⁻¹b⟩ ≥ 0`. ### `__post_init__()` Make the array fields read-only. Freezing the dataclass prevents a field from being reassigned, but it does not prevent writing into the array that the field holds, and a result that has been written into still reports the scalar values of the original run. The result therefore keeps its own copy, so the array it was built from can still be changed by the caller. To get a writable copy, use `np.array(result.solution)`. Both array fields are frozen. `raw_solution` is optional, and `freeze_arrays` would turn a `None` into a zero-dimensional object array, so it is only frozen when the run recorded one. ## `eigenkit.algorithms.IQAE` Bases: `BaseQuantumSolver` Iterative Quantum Amplitude Estimation (IQAE). Estimates `a = |⟨ψ₁|A|0⟩|²` to an additive precision `epsilon`, with a failure probability of at most `alpha`. It uses the Grover operator `Q = A S₀ A† S_ψ₁`, built from the oracle `A` that you provide. The oracle can be given in two forms: - A **PennyLane callable** `oracle(wires)`, which applies the circuit `A` in place. The last wire (`wires[-1]`) must be the ancilla qubit: ```python def my_oracle(wires): qml.RY(theta, wires=wires[0]) qml.CNOT(wires=[wires[0], wires[-1]]) ``` The matrix of the circuit is extracted internally with `qml.matrix`, so this form requires `provider="pennylane"` and the `pennylane` extra. - A CircuitSpec, which works on any backend that supports the `QubitUnitary`, `X`, `MCZ` and `Z` gates. With a provider other than PennyLane, pass `shot_mode="synthetic"` or `shot_mode="auto"`, because `shot_mode="provider"` works only with PennyLane. Parameters: | Name | Type | Description | Default | | -------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | `epsilon` | `float` | The additive precision to reach, that is, the half-width of the final confidence interval for a. It must be in (0, π/8). | *required* | | `alpha` | `float` | The total failure probability. The algorithm applies a Bonferroni correction, so the confidence interval of each round is valid at level alpha / T, where T = ceil(log2(π / (8 * epsilon))) is the maximum number of rounds. It must be in (0, 1). | *required* | | `n_qubits` | `int` | The total number of qubits, including the ancilla. | *required* | | `provider` | `ProviderSpec` | The backend to use: a registered name ("numpy", "pennylane", "qiskit" and so on), a config instance, or a backend object. The default is "numpy", the built-in simulator. | `'numpy'` | | `n_shots` | `int` | The number of measurement shots per round. With shot_mode="synthetic", which is used on analytic backends, it sets the size of a seeded binomial draw on top of the exact probability of the ancilla. With shot_mode="provider" or "auto", it is the number of samples per round on the hardware or simulator, unless the provider config sets its own shots, which takes precedence. For example, a device configured for 50 shots samples 50 times, whatever the value of n_shots. To control sampling on those backends, set shots on the provider config. The default is 100. | `100` | | `shot_mode` | `ShotMode` | How the ancilla measurements are obtained. With "auto", the default, the provider's shot count is used when the backend config sets a finite shots, and synthetic binomial noise is used otherwise. "provider" always samples from the backend. "synthetic" always uses exact expectation values plus seeded binomial noise. | `'auto'` | | `min_ratio` | `float` | The minimum ratio K\_{i+1} / K_i between the Grover powers of consecutive rounds. It must be greater than 1. | `2.0` | | `random_state` | \`int | None\` | The seed of the random number generator used for the shot noise. An integer gives reproducible results, and None uses a non-deterministic seed. | Example ```python import numpy as np from eigenkit import CircuitBuilder from eigenkit.algorithms import IQAE # RY(pi/3) on the ancilla prepares an amplitude of sin^2(pi/6) = 0.25. qc = CircuitBuilder(n_qubits=2) qc.ry(np.pi / 3, qubit=1) oracle = qc.build() iqae = IQAE(epsilon=0.02, alpha=0.05, n_qubits=2, random_state=0) result = iqae.solve(oracle) print(result.estimation) # 0.246 print(result.confidence_interval) # (0.2407, 0.2513), which contains 0.25 print(result.n_oracle_calls) # 2600 ``` ### `solve(oracle)` Estimate the amplitude `a = |⟨ψ₁|A|0⟩|²`. Parameters: | Name | Type | Description | Default | | -------- | ---------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `oracle` | \`Callable | CircuitSpec\` | Either a PennyLane callable oracle(wires) that builds the state-preparation circuit A, or a CircuitSpec produced by CircuitBuilder. In both cases, the last qubit is treated as the ancilla. | Returns: | Type | Description | | ------------ | --------------------------------------------------------------------------------------- | | `IQAEResult` | An IQAEResult with the point estimate, the confidence interval and the resource counts. | Raises: | Type | Description | | ------------ | ------------------------------------------------------------------------------------------------- | | `ValueError` | If the selected provider does not support the gates that IQAE needs (QubitUnitary, X, MCZ and Z). | ## `eigenkit.algorithms.IQAEResult` Bases: `ValueEquality` Outcome of a completed Iterative Quantum Amplitude Estimation (IQAE) run. The result is read-only. Assigning to a field raises `FrozenInstanceError`, so a mistyped `=` (instead of `==`) cannot change a result. This type holds no arrays, so nothing else needs protecting. Results compare by value. They are not hashable, so that all result types behave in the same way: the types that hold arrays cannot be hashed both correctly and efficiently. Attributes: | Name | Type | Description | | --------------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `estimation` | `float` | The point estimate of the amplitude a. | | `confidence_interval` | `tuple[float, float]` | The (lower, upper) Clopper-Pearson interval at the requested confidence level 1 - alpha. | | `n_rounds` | `int` | The number of rounds that the run executed. It can also be read as n_iterations, the name that the other result types use. | | `n_oracle_calls` | `int` | The total number of oracle calls over all rounds. | | `success` | `bool` | Whether the interval reached the requested precision. This is the same test that ends the loop over rounds, and the one reported by the warning that the run did not converge. False means that the run stopped at max_rounds with an interval still wider than 2 * epsilon. | ### `n_iterations` The number of rounds, under the name that the other results use. `n_rounds` is the name that IQAE uses, and it stays available. This property lets one loop read the number of iterations from any result. Returns: | Type | Description | | ----- | ------------------------------------------- | | `int` | The number of rounds that the run executed. | ## `eigenkit.algorithms.PortfolioRiskEstimator` Bases: `BaseQuantumEstimator` Quantum estimator of portfolio risk. It estimates the Value at Risk (VaR) and the Conditional Value at Risk (CVaR) of a portfolio. The estimator can use IQAE to estimate the tail probability on a quantum circuit. The oracle for IQAE is built from the fitted return distribution with amplitude encoding. Note This is not a standard scikit-learn regressor. The risk at a confidence level comes from `estimate()` and `var_at()`, which take confidence levels and not feature vectors, and the estimator has no meaningful scikit-learn `score()`. It inherits only from BaseQuantumEstimator. Not financial advice The estimates come from a model fitted to the price history you pass in, and they carry no guarantee. They are not investment advice. Parameters: | Name | Type | Description | Default | | -------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- | | `n_qubits` | `int` | The total number of qubits, including the ancilla. The loss distribution is discretised into 2^(n_qubits - 1) bins. The minimum is 2: one data qubit and the ancilla. | `6` | | `epsilon` | `float` | The additive precision of the IQAE estimate of the tail probability. | `0.01` | | `alpha` | `float` | The failure probability of IQAE. The confidence level is 1 − alpha. | `0.05` | | `horizon_days` | `int` | The investment horizon, in trading days, used to scale daily log-returns to the target horizon. The default is 1, which gives daily risk measures. | `1` | | `provider` | `ProviderSpec` | The backend that IQAE uses: a registered name ("pennylane", "qiskit", "braket" and so on), a config instance, or a backend object. The default is "numpy". Note The Grover operator built internally uses the adjoint of an AmplitudeEncoding block, so the provider must support the adjoint of a state preparation and provide a statevector. The built-in "numpy" simulator and PennyLane both do, and their results agree to the last digit. A backend that does not raises an error when the operator is built. Calling IQAE directly with a CircuitSpec based on QubitUnitary avoids this requirement. | `'numpy'` | | `n_shots` | `int` | The number of shots per IQAE round. The default is 100. | `100` | | `random_state` | \`int | None\` | The seed that makes the results reproducible. | | `n_bisection_steps` | `int` | The number of binary-search steps used to locate the VaR with quantum estimates of the tail probability. With 0, the default, the VaR and CVaR are computed classically from the fitted bins. A positive value calls \_quantum_var_bisection(), which refines the threshold by running IQAE at each step of the bisection. | `0` | | `measure_tail_probability` | `bool` | When True, the default, estimate() runs IQAE once after the VaR and CVaR are computed, and reports the quantum estimate of the tail in tail_probability and confidence_interval. Set it to False to skip that final IQAE call when only the classical VaR and CVaR are needed and n_bisection_steps=0. | `True` | | `distribution` | `str` | The model of the loss distribution that fit() uses. "gaussian" (the default) fits the mean μ and the standard deviation σ of the scaled log-returns, and integrates the Gaussian cumulative distribution function (CDF) over each bin. The distribution is truncated at ±6σ. A VaR beyond that point is capped at the limit, and a RuntimeWarning is issued. "empirical" places each historical return in the nearest bin of equal width, without assuming a parametric form. "t" fits a Student-t distribution by maximum-likelihood estimation, and integrates its CDF over each bin. It has heavier tails than the Gaussian. | `'gaussian'` | Example ```python import numpy as np from eigenkit.algorithms import PortfolioRiskEstimator rng = np.random.default_rng(0) returns = rng.normal(0.0005, 0.02, size=(250, 3)) prices = 100 * np.exp(np.cumsum(returns, axis=0)) estimator = PortfolioRiskEstimator( n_qubits=5, provider="numpy", random_state=0 ) estimator.fit(prices, weights=np.array([0.5, 0.3, 0.2])) risk = estimator.estimate(confidence_level=0.95) print(risk.var) # 0.0237: at most a 2.37% loss on 95% of days print(risk.cvar) # 0.0262: the average loss on the worst 5% print(risk.tail_probability) # 0.0659: the IQAE estimate of the tail mass ``` ### `fit(X, y=None, *, weights=None)` Fit the return distribution to historical price data. Parameters: | Name | Type | Description | Default | | --------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------- | | `X` | `ndarray` | A 2-D array of shape (T, n_assets) with daily closing prices, where T must be at least 2. A 1-D array of length T is accepted for a portfolio with a single asset. | *required* | | `y` | \`ndarray | None\` | Ignored, because this estimator is unsupervised. It is present for compatibility with the scikit-learn interface. | | `weights` | \`ndarray | None\` | A 1-D array of portfolio weights that sum to 1. If it is not given, all assets have equal weights. | Raises: | Type | Description | | ------------ | -------------------------------------------------------------------------------------------------------------------------------------- | | `ValueError` | If distribution is not one of the supported values. This can happen when distribution is changed with set_params() after construction. | ### `var_at(confidence_levels)` Return the VaR at each confidence level. This is the array version of `estimate()`. It runs IQAE once for each level and returns only the `var` of each result. Parameters: | Name | Type | Description | Default | | ------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ---------- | | `confidence_levels` | `ndarray` | A 1-D array of confidence levels in (0, 1), for example [0.95, 0.99]. A scalar or an (n, 1) column is also accepted, and it is read as n levels. | *required* | Returns: | Type | Description | | --------- | ------------------------------------------------------------------------------------------- | | `ndarray` | An array of VaR values, as fractions of the portfolio value lost, with one value per level. | Raises: | Type | Description | | ---------------- | ------------------------------------------------------------- | | `ValueError` | If the argument is not 1-D after a single column is squeezed. | | `NotFittedError` | If fit() has not been called. | Warns: | Type | Description | | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `UserWarning` | If any level is below 0.5. Such a level is valid, but values below 0.5 are also what a price series or a feature matrix looks like, and this warning catches the mistake of passing one of those. | Warning Each level runs IQAE once, so `n` levels need `n` separate quantum circuits. For a single risk measure, call `estimate()` instead. Example ```python estimator.var_at([0.90, 0.95, 0.99]) ``` ### `predict(X)` Not supported. Use `var_at()` instead. The estimator base class requires a `predict` method. In scikit-learn, `predict` takes samples from the same feature space that `fit` received, but the risk at a confidence level is a different question, which `var_at()` answers. Parameters: | Name | Type | Description | Default | | ---- | --------- | ----------- | ---------- | | `X` | `ndarray` | Ignored. | *required* | Raises: | Type | Description | | --------------------- | --------------------------------------- | | `NotImplementedError` | Always. The message points to var_at(). | ### `estimate(confidence_level=0.95, *, _seed_seq=None)` Estimate the VaR and CVaR at `confidence_level`. The VaR and CVaR come from the discretised loss distribution. When `n_bisection_steps > 0`, the VaR is refined by a bisection that uses IQAE. Otherwise, the classical quantiles of the bins are used. Unless `measure_tail_probability=False`, a final IQAE run estimates `P(loss ≥ VaR)`, for checking and diagnostics. Parameters: | Name | Type | Description | Default | | ------------------ | -------------- | ---------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `confidence_level` | `float` | The probability level of the risk measures, for example 0.95 for the 95 % VaR. It must be in (0, 1). | `0.95` | | `_seed_seq` | \`SeedSequence | None\` | A private override of the random seed, used by var_at() so that the shot noise of the IQAE runs for different confidence levels is independent. The default is None, which derives the seed from random_state. | Returns: | Type | Description | | --------------------- | ---------------------- | | `PortfolioRiskResult` | A PortfolioRiskResult. | Raises: | Type | Description | | -------------- | -------------------------------------------------------------------------------- | | `RuntimeError` | If fit() has not been called. | | `ValueError` | If the selected provider cannot compile the oracle for the tail that IQAE needs. | ### `save(path)` Save a fitted model to a `.eigenkit` bundle. The bundles of models that hold scikit-learn objects (`QSVC`, `QSVR`, the reservoir estimators and the time-series estimators) contain joblib pickles, so loading one can run code. Share them only with people who trust you. Parameters: | Name | Type | Description | Default | | ------ | ----- | ----------- | ------------------------------------------------------------------------- | | `path` | \`str | Path\` | The path of the bundle. The suffix .eigenkit is added when it is missing. | ### `load(path, *, provider=None)` Load a fitted model from a `.eigenkit` bundle. Load only bundles that you trust. A bundle can contain joblib pickles, and unpickling runs code, as with `torch.load` or `joblib.load`. Parameters: | Name | Type | Description | Default | | ---------- | -------------- | ----------- | --------------------------------------- | | `path` | \`str | Path\` | The path of the bundle. | | `provider` | \`ProviderSpec | None\` | A provider that replaces the saved one. | Returns: | Type | Description | | ------------------------ | --------------------- | | `'BaseQuantumEstimator'` | The loaded estimator. | Raises: | Type | Description | | ----------- | ---------------------------------------------------------------------------------------------------------------------------- | | `TypeError` | If the method is called on a subclass and the bundle holds a different class. BaseQuantumEstimator.load() accepts any class. | ## `eigenkit.algorithms.PortfolioRiskResult` Bases: `ValueEquality` Outcome of a PortfolioRiskEstimator run. The result is read-only. Assigning to a field raises `FrozenInstanceError`, so a mistyped `=` (instead of `==`) cannot change a result. This type holds no arrays, so nothing else needs protecting. Results compare by value. They are not hashable, so that all result types behave in the same way: the types that hold arrays cannot be hashed both correctly and efficiently. Attributes: | Name | Type | Description | | --------------------- | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | `var` | `float` | The Value at Risk (VaR) at the requested confidence_level, as a fraction of the portfolio value lost. For example, 0.12 means a loss of 12 %. | | `cvar` | `float` | The Conditional Value at Risk (CVaR), also called Expected Shortfall, at the same confidence level. | | `tail_probability` | `float` | The IQAE point estimate of P(loss ≥ VaR). | | `confidence_interval` | `tuple[float, float]` | The (lower, upper) Clopper-Pearson interval for the tail probability, at level 1 - alpha. | | `iqae_result` | \`IQAEResult | None\` | ## `eigenkit.benchmark` Comparison of quantum and classical estimators. The compare() function trains models that follow the scikit-learn interface on a dataset with several random seeds. It returns a BenchmarkResult with the aggregated metrics. Example ```python from sklearn.svm import SVC from eigenkit.qml import QSVC from eigenkit.benchmark import compare results = compare( models={ "Quantum SVC": QSVC( feature_map="Angle", reps=1, probability=True ), "Regularized SVC": SVC(C=0.3), "Flexible SVC": SVC(C=3.0), }, dataset="iris", metrics=["accuracy"], n_seeds=1, ) print(list(results.fitted_models)) # ['Quantum SVC', 'Regularized SVC', 'Flexible SVC'] ``` ### `BenchmarkResult` Results of a benchmark comparison. It stores the raw metric values of every seed, and aggregates them when you display them with `table()`, plot them with `plot()`, or analyse them with `to_dataframe()`. Each entry in `raw` is a dict with the keys `"model"`, `"seed"`, `"metric"` and `"value"`. `fitted_models` holds every fitted copy produced by compare(), keyed by the name of the model in the benchmark. The names usually match the class names, and repeated names are numbered (for example, `SVC_0` and `SVC_1`). If `compare(models={...})` receives a dict, its keys are used instead. Example ```python from sklearn.svm import SVC from eigenkit.qml import QSVC from eigenkit.benchmark import compare results = compare( models={ "Quantum SVC": QSVC( feature_map="Angle", reps=1, probability=True ), "Regularized SVC": SVC(C=0.3), "Flexible SVC": SVC(C=3.0), }, dataset="iris", metrics=["accuracy"], n_seeds=1, ) print(list(results.fitted_models)) # ['Quantum SVC', 'Regularized SVC', 'Flexible SVC'] results.plot(save_path="svc_comparison.png") df = results.to_dataframe() X_new = [[5.1, 3.5, 1.4, 0.2]] # Access the fitted estimators winner = results.best_model(metric="accuracy") preds = results.predict(X_new) quantum_fits = results.fitted_models["Quantum SVC"] # Run inference on new data, with all models or a subset out = results.inference(X_new) out = results.inference(X_new, models=["Quantum SVC"], seed=0) ``` #### `best_model(metric=None, seed='last')` Return the fitted estimator with the best mean score on `metric`. When `metric` is `None`, the default, the first metric recorded in `raw` that is not a timing metric is used. With the default metrics, this is usually `"accuracy"` for classification and `"r2"` for regression. If custom metrics were passed in a different order, the first of them is used. Parameters: | Name | Type | Description | Default | | -------- | ----- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `metric` | \`str | None\` | The metric used for ranking, for example "accuracy" or "r2". None selects the primary metric of the task. Built-in metrics use their registered direction, and custom metrics use the direction given to custom_metric(...). | | `seed` | \`str | int\` | The seed whose estimator is returned for the winning model. "last", the default, returns the estimator of the last seed. An int is a 0-based index into the list of seeds, in the order they ran. | Returns: | Type | Description | | ----- | --------------------- | | `Any` | The fitted estimator. | Raises: | Type | Description | | ------------ | ------------------------------------------------------------------- | | `ValueError` | If no fitted models are stored, or if metric is not in the results. | | `IndexError` | If seed is out of range. | #### `confusion_matrix(model_name=None, **kwargs)` Return a scikit-learn confusion matrix computed from the stored predictions. Parameters: | Name | Type | Description | Default | | ------------ | ----- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------ | | `model_name` | \`str | None\` | The model whose predictions are used. None, the default, uses the first model with stored predictions. | | `**kwargs` | `Any` | Passed on to sklearn.metrics.confusion_matrix. | `{}` | Returns: | Type | Description | | --------- | --------------------------------------- | | `ndarray` | The confusion matrix, as a NumPy array. | Raises: | Type | Description | | ------------- | ------------------------------------------------------------------------------------------------------------- | | `ValueError` | If no predictions are stored (run compare(..., store_predictions=True)), or if there are none for model_name. | | `ImportError` | If scikit-learn is not installed. | #### `inference(X, models=None, seed='last')` Run every fitted model, or a subset of them, on new data. Parameters: | Name | Type | Description | Default | | -------- | ----------- | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `X` | `Any` | The input features to predict on. | *required* | | `models` | \`list[str] | None\` | The names of the models to run. None, the default, runs all fitted models. Use the exact keys of fitted_models: the names based on the class for a list input (for example, "SVC" or "SVC_0"), or your own keys for a dict input. | | `seed` | \`str | int\` | The seed whose estimator is used for each model. "last", the default, uses the estimator of the last seed. An int is a 0-based index into the list of seeds, in the order they ran. | Returns: | Type | Description | | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `dict[str, dict[str, Any]]` | A dict keyed by model name. Each value contains two entries: "predictions": the array returned by estimator.predict(X). "probabilities": the array returned by estimator.predict_proba(X) for classifiers that support it, and None for regressors and for classifiers without predict_proba. | Raises: | Type | Description | | ------------ | ------------------------------------------------------------------------- | | `ValueError` | If no fitted models are stored, or if an unknown model name is requested. | | `IndexError` | If seed is out of range for any model. | Example ```python out = results.inference(X_new) out = results.inference(X_new, models=["Flexible SVC"], seed=0) preds = out["Flexible SVC"]["predictions"] probs = out["Flexible SVC"]["probabilities"] # None for regressors ``` #### `load(path)` Load numeric benchmark results saved by `BenchmarkResult.save()`. Parameters: | Name | Type | Description | Default | | ------ | ----- | ----------- | ---------------------- | | `path` | \`str | Path\` | The JSON file to read. | Returns: | Type | Description | | ------------------- | ------------------------------------------------------------------------------------------------------ | | `'BenchmarkResult'` | A BenchmarkResult with the stored values. It has no fitted models, because save() does not store them. | #### `plot(save_path=None, theme='light')` Plot a grouped bar chart with error bars. Parameters: | Name | Type | Description | Default | | ----------- | ----- | ------------------------------------------------ | ------------------------------------------------------------------ | | `save_path` | \`str | None\` | If given, the figure is saved to this path instead of being shown. | | `theme` | `str` | The plot theme, "light" (the default) or "dark". | `'light'` | Raises: | Type | Description | | ------------- | ------------------------------- | | `ImportError` | If matplotlib is not installed. | #### `plot_history(save_path=None, theme='light', seed='last')` Plot the stored training and validation loss curves. Models without a loss history are left out, and a warning names them. Parameters: | Name | Type | Description | Default | | ----------- | ----- | ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `save_path` | \`str | None\` | If given, the figure is saved to this path instead of being shown. | | `theme` | `str` | The plot theme, "light" (the default) or "dark". | `'light'` | | `seed` | \`str | int\` | The run to draw: "last" (the default), or an integer index into the list of seeds. With "last", a model whose last seed failed shows its most recent recorded curve. | Raises: | Type | Description | | ------------- | ---------------------------------------------------- | | `TypeError` | If seed is neither "last" nor an int. | | `ValueError` | If no model has a training or validation loss curve. | | `ImportError` | If matplotlib is not installed. | #### `predict(X, metric=None)` Predict with the best model ranked by `metric`. This is a shortcut for `best_model()`. It calls `predict()` on the estimator returned by `best_model(metric=metric)`. Parameters: | Name | Type | Description | Default | | -------- | ----- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | `X` | `Any` | The input features. | *required* | | `metric` | \`str | None\` | The metric used to select the best model. None, the default, selects the primary metric of the task: "accuracy" for classification and "r2" for regression. | Returns: | Type | Description | | ----- | --------------------------------------------------------- | | `Any` | The predictions of the best estimator's predict() method. | #### `save(path)` Save the numeric results of the benchmark to a JSON file. The fitted estimators are not saved, by design. Parameters: | Name | Type | Description | Default | | ------ | ----- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | `path` | \`str | Path\` | The file to write. If it is an existing directory, a file named bench\_.json is created inside it. If it has no extension, .json is added. | Returns: | Type | Description | | ------ | -------------------------------------- | | `Path` | The path of the file that was written. | #### `significance_test(metric, method='wilcoxon')` Return a matrix of p-values for each pair of models. The p-values compare the values of `metric` on the seeds that both models share. Parameters: | Name | Type | Description | Default | | -------- | ----- | ----------------------------------------------------------------------------------------- | ------------ | | `metric` | `str` | The metric to compare. | *required* | | `method` | `str` | "wilcoxon" (the default) for a Wilcoxon signed-rank test, or "ttest" for a paired t-test. | `'wilcoxon'` | Returns: | Type | Description | | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | `'pd.DataFrame'` | A pandas DataFrame with one row and one column per model. The diagonal is 1, and a pair with fewer than two shared seeds has the value NaN. | Raises: | Type | Description | | ------------- | ------------------------------------------------------------------------------- | | `ImportError` | If pandas or scipy is not installed. | | `ValueError` | If method is not supported, or if fewer than two models have values for metric. | #### `table(show_significance=False)` Return the results as a text table. The table is drawn with Unicode box-drawing characters and needs no optional dependency. Parameters: | Name | Type | Description | Default | | ------------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | | `show_significance` | `bool` | If True, mark the best model with * or \*\* when a Wilcoxon signed-rank test against another model gives p < 0.05 or p < 0.01. The test needs at least 6 seeds shared by both models, so with fewer seeds no model is marked, and a warning explains why. The test needs pandas, from the benchmark extra. | `False` | Returns: | Type | Description | | ----- | --------------------------------------------------------------------------------------------- | | `str` | A string with one row per model and one column per metric. Each value is shown as mean ± std. | Raises: | Type | Description | | ------------- | --------------------------------------------------------- | | `ImportError` | If show_significance is True and pandas is not installed. | #### `to_dataframe()` Return the raw results of every seed as a pandas DataFrame. Returns: | Type | Description | | ---------------- | ---------------------------------------------------------------------------------------------- | | `'pd.DataFrame'` | A DataFrame with the columns model, seed, metric and value, and one row for each entry of raw. | Raises: | Type | Description | | ------------- | --------------------------- | | `ImportError` | If pandas is not installed. | ### `compare(models, dataset='breast_cancer_wisconsin', metrics=None, n_seeds=5, test_size=0.15, show_progress=False, save_dir=None, store_predictions=False, validation_size=0.15)` Compare eigenkit models and built-in scikit-learn models on a dataset. Each model must be a built-in scikit-learn estimator (from the `sklearn` package), or a subclass of BaseQuantumEstimator, BaseQuantumClassifier or BaseQuantumRegressor. For every seed, each model is copied with `sklearn.base.clone()`. Parameters: | Name | Type | Description | Default | | ------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `models` | \`list | dict[str, Any]\` | The estimator instances to compare. If a list is passed, the display names come from type(model).__name__, and repeated names are numbered (for example, SVC_0 and SVC_1). If a dict is passed, its keys are used as display names. Dict keys must be non-empty strings, with no whitespace at the start or the end and no control characters. Use a different key for each model. If the same key appears twice in a Python dict literal, Python keeps only the last model before compare() receives the dict. When save_dir is set, the keys must also be valid prefixes for file names. | | `dataset` | \`str | dict\` | A dataset name, or a dict that is already split, with X_train, y_train, X_test and y_test. Such a dict can also include X_val and y_val. | | `metrics` | \`list | None\` | The metrics to compute. For classification, the options are accuracy, f1, precision, recall, roc_auc, fit_time and predict_time. For regression, they are r2, mae, rmse, mse, fit_time and predict_time. A custom function must be wrapped with custom_metric(...), so that its direction (higher or lower is better) is explicit. The default depends on the type of task of the first model. | | `n_seeds` | `int` | The number of seeds to average over. The default is 5. | `5` | | `test_size` | `float` | The fraction of the data kept for the final test set. | `0.15` | | `show_progress` | `bool` | Whether to print the progress of the benchmark, with the current model and an estimate of the remaining time. The default is False, which runs silently. | `False` | | `save_dir` | \`str | Path | None\` | | `store_predictions` | `bool` | If True, the result keeps y_true, y_pred and y_prob for the last seed of each model. | `False` | | `validation_size` | `float` | The fraction of the data kept for validation. | `0.15` | Returns: | Type | Description | | ----------------- | ----------------------------------------------------------------------------------- | | `BenchmarkResult` | An object with the results, and methods such as table(), plot() and to_dataframe(). | Raises: | Type | Description | | ------------ | ------------------------------------------------------------------------------------ | | `ValueError` | If models is empty, if a model name is not valid, or if dataset is not well formed. | | `TypeError` | If a model name is not a string, or if a model is not a supported type of estimator. | ### `custom_metric(fn, *, direction, name=None)` Define a custom metric and the direction in which it improves. Parameters: | Name | Type | Description | Default | | ----------- | ------------------------------------- | ---------------------------------------------------------------------------- | -------------------------------------------------------------- | | `fn` | `Callable[[ndarray, ndarray], float]` | The metric function. It receives (y_true, y_pred) and returns a number. | *required* | | `direction` | `MetricDirection` | "higher" if higher values are better, or "lower" if lower values are better. | *required* | | `name` | \`str | None\` | The column label of the metric. The default is the name of fn. | Returns: | Type | Description | | -------------- | --------------------------------------------------------- | | `CustomMetric` | A metric specification to pass to compare(metrics=[...]). | ### `load_checkpoint(path, *, model=None)` Load a model checkpoint written by `compare(..., save_dir=...)`. Parameters: | Name | Type | Description | Default | | ------- | ----- | ----------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | | `path` | \`str | Path\` | The path to the checkpoint file (\*.eigenkit or \*.joblib). | | `model` | `Any` | The nn.Module architecture for GenericRegressor or GenericClassifier checkpoints. It is ignored for other types of estimator. | `None` | Returns: | Type | Description | | ----- | --------------------- | | `Any` | The loaded estimator. | Raises: | Type | Description | | ------------ | ------------------------------------------------------------ | | `TypeError` | If a .joblib file does not contain a scikit-learn estimator. | | `ValueError` | If the file extension is not supported. | ### `quick_baselines()` Return a list of common scikit-learn classifiers with sensible defaults. The list contains a `SVC` with a radial basis function (RBF) kernel and probabilities, so that `roc_auc` can be computed, a `RandomForestClassifier`, a `LogisticRegression` and a `KNeighborsClassifier`. `XGBClassifier` and `LGBMClassifier` are added when their packages can be imported. scikit-learn 1.9 deprecates `SVC(probability=True)`. From that version, the support vector classifier is wrapped as `CalibratedClassifierCV(SVC(), ensemble=False)`, the replacement that scikit-learn documents, so the results table calls it `CalibratedClassifierCV`. Returns: | Type | Description | | ----------- | ------------------------------------------------------------------------------------- | | `list[Any]` | A list of classifiers that follow the scikit-learn interface, created but not fitted. | Example ```python from eigenkit.benchmark import compare, quick_baselines results = compare(models=quick_baselines(), dataset="iris", n_seeds=2) ``` ### `timer()` Measure the wall-clock time of a block of code. Yields: | Type | Description | | ------ | -------------------------------------------------------------------------------------- | | `dict` | A dict. When the with block ends, its "elapsed" key holds the elapsed time in seconds. | Example ```python with timer() as t: do_work() print(t["elapsed"]) ``` ## `eigenkit.chemistry` Quantum chemistry with the core of the library. Build a fermionic Hamiltonian (`FermionOperator`), map it to a qubit `Observable` (`jordan_wigner`), and find its ground-state energy with the Variational Quantum Eigensolver (`VQE`), which uses the variational optimisation tools of the core. The package uses only NumPy and SciPy, needs no PyTorch, and works with any backend. For small systems, `exact_ground_state_energy` gives the classical reference value. Example ```python from eigenkit.chemistry import ( fermi_hubbard_dimer, jordan_wigner, VQE, exact_ground_state_energy, ) hamiltonian, offset = jordan_wigner(fermi_hubbard_dimer(t=1.0, u=2.0)) result = VQE(layers=3, restarts=5).solve(hamiltonian, n_qubits=4, offset=offset) exact = exact_ground_state_energy(hamiltonian, n_qubits=4, offset=offset) print(result.energy, exact) ``` ### `FermionOperator` A complex linear combination of fermionic terms, stored as `{term: coeff}`. #### `__init__(terms=None)` Build an operator from a mapping of terms to coefficients. Parameters: | Name | Type | Description | Default | | ------- | --------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `terms` | \`dict[Term, complex] | None\` | Maps each term to its coefficient. A term is a tuple of (mode, action) pairs, where action is 1 for creation and 0 for annihilation, applied from left to right. The empty tuple is the identity. The coefficients of repeated terms are added together. Leave it out for the zero operator. The default is None. | #### `annihilation(mode)` The annihilation operator `a_mode`. Parameters: | Name | Type | Description | Default | | ------ | ----- | ---------------------------------------------------------------- | ---------- | | `mode` | `int` | The index of the spin-orbital from which an electron is removed. | *required* | Returns: | Type | Description | | ------------------- | ----------------------------------- | | `'FermionOperator'` | The operator, as a FermionOperator. | #### `creation(mode)` The creation operator `a†_mode`. Parameters: | Name | Type | Description | Default | | ------ | ----- | -------------------------------------------------------------- | ---------- | | `mode` | `int` | The index of the spin-orbital in which an electron is created. | *required* | Returns: | Type | Description | | ------------------- | ----------------------------------- | | `'FermionOperator'` | The operator, as a FermionOperator. | #### `hermitian_conjugate()` The adjoint of the operator. The order of each term is reversed, creation and annihilation are swapped, and each coefficient is replaced by its complex conjugate. Returns: | Type | Description | | ------------------- | ---------------------------------- | | `'FermionOperator'` | The adjoint, as a FermionOperator. | #### `identity(coefficient=1.0)` The identity operator, optionally scaled. Parameters: | Name | Type | Description | Default | | ------------- | -------- | ------------------------------------------------------------ | ------- | | `coefficient` | `Scalar` | The factor that multiplies the identity. The default is 1.0. | `1.0` | Returns: | Type | Description | | ------------------- | ------------------------------------------ | | `'FermionOperator'` | The scaled identity, as a FermionOperator. | #### `number(mode)` The number operator `a†_mode a_mode`. It counts the electrons in one spin-orbital. Parameters: | Name | Type | Description | Default | | ------ | ----- | ------------------------------ | ---------- | | `mode` | `int` | The index of the spin-orbital. | *required* | Returns: | Type | Description | | ------------------- | ----------------------------------- | | `'FermionOperator'` | The operator, as a FermionOperator. | #### `zero()` The zero operator, which has no terms. Returns: | Type | Description | | ------------------- | ------------------------- | | `'FermionOperator'` | The zero FermionOperator. | ### `Molecule` A molecular Hamiltonian in second quantisation. `fermion_operator` already includes the constant term (the nuclear repulsion), so `jordan_wigner(fermion_operator)` returns it as the offset. `hf_energy` and `fci_energy`, when computed, are the reference energies from PySCF, which are useful targets for the Variational Quantum Eigensolver (VQE). Attributes: | Name | Type | Description | | ------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `fermion_operator` | `FermionOperator` | The Hamiltonian in second quantisation, including the constant term. | | `n_qubits` | `int` | The number of qubits needed after the mapping, one per spin-orbital with Jordan-Wigner. | | `n_electrons` | `int` | The number of electrons, which sets the Hartree-Fock reference state and the excitations that a unitary coupled-cluster singles and doubles (UCCSD) ansatz can use. | | `nuclear_repulsion` | `float` | The constant nuclear term, in Hartree. It is already included in fermion_operator, and it is kept separately so that it can be reported. | | `hf_energy` | `float` | The Hartree-Fock energy from PySCF, in Hartree. A VQE run should reach a lower energy. | | `fci_energy` | \`float | None\` | ### `VQE` Bases: `BaseQuantumSolver` Variational Quantum Eigensolver (VQE). Parameters: | Name | Type | Description | Default | | -------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------- | | `layers` | `int` | The depth of the default hardware-efficient ansatz. The default is 2. | `2` | | `provider` | `ProviderSpec` | The backend to use. The default is "numpy", the built-in reference simulator. | `'numpy'` | | `optimizer` | `Optimizer` | The optimiser: the name of a scipy.optimize.minimize method, "spsa", or a custom callable (see minimize_expectation). The default is "L-BFGS-B". | `'L-BFGS-B'` | | `restarts` | `int` | The number of runs from random starting points. The lowest energy is kept, because variational landscapes have local minima. The default is 1. | `1` | | `random_state` | \`int | None\` | The base seed. Restart r uses random_state + r, and None leaves every restart unseeded. The default is 42. | #### `solve(hamiltonian, n_qubits, *, offset=0.0, ansatz=None)` Minimise the expectation value of the Hamiltonian over the ansatz. It runs `restarts` independent optimisations from different random starting points and keeps the lowest energy, because a variational landscape has local minima. Parameters: | Name | Type | Description | Default | | ------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | | `hamiltonian` | `Observable` | The Hamiltonian to minimise, as a qubit Observable, usually the one returned by jordan_wigner. | *required* | | `n_qubits` | `int` | The number of qubits of the circuit. It must match the Hamiltonian. | *required* | | `offset` | `float` | The constant added to the minimised value. This is where the identity term returned by the mapping goes, which for a molecule includes the nuclear repulsion, in Hartree. If it is left out, the energy is wrong by exactly that constant. The default is 0.0. | `0.0` | | `ansatz` | \`CircuitSpec | None\` | The circuit to optimise. The default, None, uses the built-in hardware-efficient ansatz with layers layers. | Returns: | Type | Description | | ----------- | ------------------------------------------------------------------------------------- | | `VQEResult` | A VQEResult with the best energy, its parameters, and whether that restart converged. | ### `VQEResult` Bases: `ValueEquality` Outcome of a run of the Variational Quantum Eigensolver (VQE). The result is read-only. Assigning to a field raises `FrozenInstanceError`, and writing into an array field raises `ValueError`. This prevents a mistyped `=` (instead of `==`) or an in-place edit from leaving a result whose scalar fields describe a different run from the array it holds. To get a writable copy of the array, use `np.array(result.field)`. Results compare by value, including their arrays. They are not hashable, because a result is a collection of numbers and not a key, and a correct hash of a float array is expensive to compute. Attributes: | Name | Type | Description | | -------------------- | --------- | --------------------------------------------------------------------------------------- | | `energy` | `float` | The lowest energy found, with the offset added back. For a molecule, it is in Hartree. | | `optimal_parameters` | `ndarray` | The ansatz weights that produced this energy. | | `n_iterations` | `int` | The number of optimiser iterations in the best restart. It is not summed over restarts. | | `success` | `bool` | Whether the optimiser of that restart reported convergence. | #### `__post_init__()` Make the array field read-only. Freezing the dataclass prevents a field from being reassigned, but it does not prevent writing into the array that the field holds, and a result that has been written into still reports the scalar values of the original run. The result therefore keeps its own copy, so the array it was built from can still be changed by the caller. To get a writable copy, use `np.array(result.optimal_parameters)`. ### `apply_hartree_fock(circuit, n_electrons)` Prepare the Hartree-Fock reference state on an existing builder. It applies an `X` gate to each of the lowest `n_electrons` spin-orbitals, which marks them as occupied (`|1⟩`). The convention is the one of the Jordan-Wigner mapping: mode `j` is qubit `j`, and the spin-orbitals are ordered so that the lowest `n_electrons` of them are occupied in the reference state, for example `|1…1 0…0⟩`. Parameters: | Name | Type | Description | Default | | ------------- | ---------------- | --------------------------------------------------------------------- | ---------- | | `circuit` | `CircuitBuilder` | The builder to add the gates to. | *required* | | `n_electrons` | `int` | The number of electrons, which sets the Hartree-Fock reference state. | *required* | Returns: | Type | Description | | ---------------- | ----------------------------------------------- | | `CircuitBuilder` | The same builder, so that calls can be chained. | ### `bravyi_kitaev(operator, n_qubits=None)` Map a `FermionOperator` to qubits with the Bravyi-Kitaev transformation. The transformation itself is done by OpenFermion (the `chemistry` extra), and the result is converted to an eigenkit `Observable`. The Jordan-Wigner mapping, in contrast, is implemented in eigenkit. `n_qubits` defaults to the largest mode index plus one. Parameters: | Name | Type | Description | Default | | ---------- | ----------------- | ------------------------------ | ---------- | | `operator` | `FermionOperator` | The fermionic operator to map. | *required* | | `n_qubits` | `int` | The number of qubits. | `None` | Returns: | Name | Type | Description | | ------------ | ------------ | ---------------------------------------------------------------------------- | | `observable` | `Observable` | The operator on qubits, without its identity part. | | `offset` | `float` | The coefficient of the identity part, which the Observable does not contain. | ### `dense_hamiltonian(observable, n_qubits, offset=0.0)` The `2**n_qubits × 2**n_qubits` matrix of `observable + offset * I`. Qubit 0 is the most significant factor of the tensor product, which matches the ordering of basis states in the simulators. Parameters: | Name | Type | Description | Default | | ------------ | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `observable` | `Observable` | The qubit operator. | *required* | | `n_qubits` | `int` | The number of qubits. | *required* | | `offset` | `float` | The constant (identity) term that the mapping returned separately, added back to the energy. For a molecule, it includes the nuclear repulsion, in Hartree. | `0.0` | Returns: | Type | Description | | --------- | ------------------------------------- | | `ndarray` | The matrix, as a complex NumPy array. | ### `exact_ground_state_energy(observable, n_qubits, offset=0.0)` The lowest eigenvalue of `observable + offset * I`, by dense diagonalisation. Parameters: | Name | Type | Description | Default | | ------------ | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `observable` | `Observable` | The qubit operator. | *required* | | `n_qubits` | `int` | The number of qubits. | *required* | | `offset` | `float` | The constant (identity) term that the mapping returned separately, added back to the energy. For a molecule, it includes the nuclear repulsion, in Hartree. | `0.0` | Returns: | Type | Description | | ------- | ------------------------ | | `float` | The ground-state energy. | ### `fermi_hubbard_dimer(t, u)` The two-site Fermi-Hubbard model with spin `1/2`. It has four spin-orbital modes, indexed by `mode = 2 * site + spin`, with spin `0` for up and `1` for down. Site `0` has modes `0` and `1`, and site `1` has modes `2` and `3`. The Hamiltonian is: ```text H = -t Σ_σ (a†_{0σ} a_{1σ} + a†_{1σ} a_{0σ}) + U (n_{0↑} n_{0↓} + n_{1↑} n_{1↓}) ``` The ground-state energy at half filling has the closed form fermi_hubbard_dimer_ground_energy(). This makes the model an exact check of the fermion-to-qubit mapping and of VQE, with no external dependency. Parameters: | Name | Type | Description | Default | | ---- | ------- | -------------------------------------------- | ---------- | | `t` | `float` | The hopping amplitude between the two sites. | *required* | | `u` | `float` | The on-site Coulomb repulsion. | *required* | Returns: | Type | Description | | ----------------- | -------------------------------------- | | `FermionOperator` | The Hamiltonian, as a FermionOperator. | ### `fermi_hubbard_dimer_ground_energy(t, u)` The ground-state energy of the Fermi-Hubbard dimer at half filling. The energy of fermi_hubbard_dimer() with two electrons in a singlet state is `(U - sqrt(U² + 16t²)) / 2`. This value is the ground state over all particle numbers only when `U < 3t`. For a larger `U`, double occupancy costs so much energy that the state with a single electron, of energy `-t`, is lower, and the ground state over all particle numbers is `min(-t, (U - sqrt(U² + 16t²)) / 2)`. An ansatz that conserves the number of particles, such as UCCSD, targets the half-filling sector directly. The hardware-efficient ansatz explores all sectors, so use it only where the half-filling state is the overall ground state. Parameters: | Name | Type | Description | Default | | ---- | ------- | --------------------------------------- | ---------- | | `t` | `float` | The hopping amplitude, as in the model. | *required* | | `u` | `float` | The on-site repulsion, as in the model. | *required* | Returns: | Type | Description | | ------- | -------------------------------------------------------------- | | `float` | The ground-state energy at half filling, from the closed form. | ### `fermion_operator_from_openfermion(operator)` Convert an `openfermion.FermionOperator` to an eigenkit `FermionOperator`. Parameters: | Name | Type | Description | Default | | ---------- | ----------------- | ---------------------------------- | ---------- | | `operator` | `FermionOperator` | The fermionic operator to convert. | *required* | Returns: | Type | Description | | ----------------- | -------------------------------------------------- | | `FermionOperator` | The same operator, as an eigenkit FermionOperator. | ### `hardware_efficient_ansatz(n_qubits, layers)` A standard hardware-efficient ansatz. It starts with a layer of `RY` rotations, then repeats `layers` times a linear chain of `CNOT` gates followed by an `RY` and an `RZ` rotation on each qubit. It has `n_qubits + 2 * layers * n_qubits` trainable weights. Parameters: | Name | Type | Description | Default | | ---------- | ----- | -------------------------- | ---------- | | `n_qubits` | `int` | The number of qubits. | *required* | | `layers` | `int` | The number of repetitions. | *required* | Returns: | Type | Description | | ------------- | ----------------------------- | | `CircuitSpec` | The ansatz, as a CircuitSpec. | ### `hartree_fock_state(n_qubits, n_electrons)` A circuit that prepares the Hartree-Fock reference state. The state is `|1…1 0…0⟩` on `n_qubits` qubits, with `n_electrons` occupied spin-orbitals. Parameters: | Name | Type | Description | Default | | ------------- | ----- | --------------------------------------------------------------------- | ---------- | | `n_qubits` | `int` | The number of qubits. | *required* | | `n_electrons` | `int` | The number of electrons, which sets the Hartree-Fock reference state. | *required* | Returns: | Type | Description | | ------------- | -------------------------------------------------------- | | `CircuitSpec` | The circuit, as a CircuitSpec with no trainable weights. | ### `hydrogen_molecule(bond_length=0.7414, *, basis='sto-3g', run_fci=False)` The hydrogen molecule `H₂` at the given bond length, in angstroms. In the minimal STO-3G basis set, this is the standard 4-qubit benchmark for VQE. Its FCI ground-state energy is about `-1.137` Hartree at equilibrium. Parameters: | Name | Type | Description | Default | | ------------- | ------- | --------------------------------------------------------------------------------------------- | ---------- | | `bond_length` | `float` | The distance between the nuclei, in angstroms. The default, 0.7414, is close to equilibrium. | `0.7414` | | `basis` | `str` | The name of the PySCF basis set. | `'sto-3g'` | | `run_fci` | `bool` | Whether to also compute the exact FCI energy, the target against which a VQE run is measured. | `False` | Returns: | Type | Description | | ---------- | ---------------------------------------------------------- | | `Molecule` | The molecule, with its Hamiltonian and reference energies. | ### `jordan_wigner(operator, *, atol=1e-10)` Map a `FermionOperator` to qubits with the Jordan-Wigner transformation. The operator must be Hermitian, as a Hamiltonian is. If a resulting coefficient is not real, a `ValueError` is raised. Parameters: | Name | Type | Description | Default | | ---------- | ----------------- | ---------------------------------------------------------------------------- | ---------- | | `operator` | `FermionOperator` | The fermionic operator to map. | *required* | | `atol` | `float` | Terms whose coefficient is smaller than this value in magnitude are dropped. | `1e-10` | Returns: | Name | Type | Description | | ------------ | ------------ | ---------------------------------------------------------------------------- | | `observable` | `Observable` | The operator on qubits, without its identity part. | | `offset` | `float` | The coefficient of the identity part, which the Observable does not contain. | ### `molecular_hamiltonian(geometry, *, basis='sto-3g', charge=0, multiplicity=1, run_fci=False)` Build a `Molecule` from a geometry, with PySCF and OpenFermion. Parameters: | Name | Type | Description | Default | | -------------- | ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `geometry` | `list[tuple[str, tuple[float, float, float]]]` | A list of (element, (x, y, z)) entries, with the coordinates in angstroms. | *required* | | `basis` | `str` | The name of the PySCF basis set. The default is the minimal basis set "sto-3g". | `'sto-3g'` | | `charge` | `int` | The total charge of the molecule. | `0` | | `multiplicity` | `int` | The spin multiplicity, 2S + 1. | `1` | | `run_fci` | `bool` | Whether to also compute the FCI energy, which is exact in this basis set and a useful target for VQE. It needs an exact diagonalisation, so it is only possible for small systems. | `False` | Returns: | Type | Description | | ---------- | ---------------------------------------------------------- | | `Molecule` | The Molecule, with its Hamiltonian and reference energies. | ### `observable_from_openfermion(qubit_operator, *, atol=1e-10)` Convert an `openfermion.QubitOperator` to an `(Observable, offset)` pair. The identity term becomes the real `offset`. The coefficients must be real, because the operator must be Hermitian, and a `ValueError` is raised otherwise. Parameters: | Name | Type | Description | Default | | ---------------- | --------------- | ---------------------------------------------------------------------------- | ---------- | | `qubit_operator` | `QubitOperator` | The operator to convert. | *required* | | `atol` | `float` | Terms whose coefficient is smaller than this value in magnitude are dropped. | `1e-10` | Returns: | Name | Type | Description | | ------------ | ------------ | ---------------------------------------- | | `observable` | `Observable` | The operator, without its identity term. | | `offset` | `float` | The coefficient of the identity term. | ### `to_openfermion_fermion_operator(operator)` Convert an eigenkit `FermionOperator` to an `openfermion.FermionOperator`. Parameters: | Name | Type | Description | Default | | ---------- | ----------------- | ---------------------------------- | ---------- | | `operator` | `FermionOperator` | The fermionic operator to convert. | *required* | Returns: | Type | Description | | ----------------- | ---------------------------------- | | `FermionOperator` | The same operator, in OpenFermion. | ### `uccsd_ansatz(n_qubits, n_electrons, reps=1)` The UCCSD ansatz on the Hartree-Fock reference, with a Trotter approximation. UCCSD is the unitary coupled-cluster ansatz with single and double excitations. The circuit prepares `(∏_k exp(θ_k (T_k − T_k†)))^reps |HF⟩`, where `|HF⟩` is the Hartree-Fock state. Each generator `G = T − T†` is anti-Hermitian. The Jordan-Wigner mapping turns `H = −iG` into a Hermitian qubit operator, and `exp(θG) = exp(iθH)` is then a product of `pauli_exponential` gates. The Pauli terms of one generator commute, so that product is exact. The product over different generators is a first-order Trotter approximation. `reps` repeats the whole sequence with independent weights, which reduces the Trotter error: more repetitions bring the circuit closer to the exact UCCSD unitary. The ansatz has `reps * n_excitations` weights in total. Parameters: | Name | Type | Description | Default | | ------------- | ----- | -------------------------------------------------------------------------------------------- | ---------- | | `n_qubits` | `int` | The number of qubits, one per spin-orbital. | *required* | | `n_electrons` | `int` | The number of electrons, which sets the Hartree-Fock reference and the possible excitations. | *required* | | `reps` | `int` | The number of Trotter repetitions of the product of excitations. The default is 1. | `1` | Returns: | Type | Description | | ------------- | --------------------------------------------------------------------------------------- | | `CircuitSpec` | The ansatz as a CircuitSpec, with one trainable angle per generator and per repetition. | ### `uccsd_excitation_generators(n_qubits, n_electrons)` The excitation generators `T - T†` of the UCCSD ansatz. UCCSD is the unitary coupled-cluster ansatz with single and double excitations. The excitations go from the occupied spin-orbitals (the lowest `n_electrons`) to the unoccupied ones. Single and double excitations are taken over all choices of occupied and unoccupied indices, with no restriction from spin symmetry, which keeps the construction simple and correct. A variant restricted by spin symmetry, which would have fewer parameters, is not implemented. Parameters: | Name | Type | Description | Default | | ------------- | ----- | --------------------------------------------------------------------- | ---------- | | `n_qubits` | `int` | The number of qubits. | *required* | | `n_electrons` | `int` | The number of electrons, which sets the Hartree-Fock reference state. | *required* | Returns: | Type | Description | | ----------------------- | ----------------------------------------------------- | | `list[FermionOperator]` | The generators, as a list of FermionOperator objects. | ## `eigenkit.circuit` The top-level `Circuit` function, which builds a circuit for the chosen interface. `Circuit(...)` returns a different type of circuit depending on `interface`, as in PennyLane: - With `interface="numpy"`, the default, it returns the core NumPy `Circuit`. This circuit does not store weights: you pass the `weights` at each call. The variational optimisation tools and the `chemistry` and `optimization` packages use this form. With the default `provider="numpy"`, the circuit runs on the built-in reference simulator, so `pip install eigenkit` with no extra is enough to build and run circuits. - With `interface="torch"`, it returns a TorchCircuit. This is a PyTorch `nn.Module` that stores trainable weights and can be used inside PyTorch models. It comes from `eigenkit.torch`, which needs the `torch` extra. PyTorch is imported only when this interface is requested, so an environment without PyTorch never imports it. Both `interface` and `provider` default to NumPy. To use PyTorch, pass `interface="torch"`. To use a faster simulator or hardware, pass a provider such as `provider="pennylane"` or `provider="qiskit"`. Example ```python import numpy as np from eigenkit import Circuit, Encoding, Variational, all_z blocks = [Encoding(in_features=4), Variational(depth=2, rotation="RY")] circuit = Circuit(blocks=blocks, observables=all_z(4)) weights = np.zeros(circuit.spec.n_weights) values = circuit(weights, np.array([0.1, 0.2, 0.3, 0.4])) # one value per qubit # A differentiable PyTorch module on PennyLane (torch and pennylane extras): layer = Circuit( blocks=blocks, observables=all_z(4), interface="torch", provider="pennylane" ) ``` ### `Circuit(blocks, observables=None, provider='numpy', interface='numpy', init=None)` Build a circuit and compile it for a backend. This is the entry point for the rest of the library: you give it blocks, and it returns a circuit that you can call. The type of the circuit depends on `interface`. The NumPy interface returns a core `Circuit`, and the PyTorch interface returns a `TorchCircuit`. Parameters: | Name | Type | Description | Default | | ------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `blocks` | `_BlocksOrSpec` | The circuit. It can be a list of blocks to assemble, a CircuitSpec that is already assembled, or a CircuitBuilder. A builder is built when Circuit is called, so gates added to it afterwards are not part of this circuit. A builder can also take the place of a block in the list. | *required* | | `observables` | `ObservableLike` | What to measure. None measures Z on every qubit and returns one value per qubit. A single Observable or PauliExpr returns a single value, and a list of them returns one value for each. Default: None. | `None` | | `provider` | `ProviderSpec` | The backend that runs the circuit. It can be a registered name ("numpy", "pennylane", "qiskit" and others), a configuration instance such as PennyLaneConfig(...), or a backend object. Default: "numpy". | `'numpy'` | | `interface` | `_Interface` | "numpy" returns a circuit that takes the weights at each call and does not depend on any framework. "torch" returns a TorchCircuit, which is a PyTorch nn.Module that stores the weights. Default: "numpy". | `'numpy'` | | `init` | \`Tensor | None\` | The initial weights of the PyTorch circuit, of shape (n_weights,). They are random when init is not given. The NumPy interface stores no weights, so passing init with it raises a ValueError. Default: None. | Returns: | Name | Type | Description | | --------- | --------- | -------------- | | `circuit` | \`Circuit | TorchCircuit\` | Raises: | Type | Description | | ------------ | ------------------------------------------------------------------------------------------- | | `ValueError` | If interface is neither "numpy" nor "torch", or if init is passed with the NumPy interface. | Example ```python import numpy as np from eigenkit import Circuit, Encoding, Variational, all_z circuit = Circuit( blocks=[Encoding(in_features=4), Variational(depth=2)], observables=all_z(4), ) weights = np.zeros(circuit.spec.n_weights) values = circuit(weights, np.random.uniform(0, np.pi, size=4)) ``` ## `eigenkit.configs` The blocks that make up a circuit, and the configurations of the backends. Blocks are combined into a circuit with `Circuit(blocks=[...])`: - `Encoding` encodes classical features as rotation angles. - `Variational` applies, in each layer, one trainable rotation to every qubit, followed by a ladder of controlled-NOT (CNOT) gates. - `StrongVariational` applies, in each layer, trainable `RZ`, `RY` and `RZ` rotations to every qubit, followed by a ladder of CNOT gates. - `Reuploading` encodes the input several times, between trainable layers. The encoding can have a trainable scale. A configuration selects a backend and holds its settings. Pass it to `Circuit(provider=...)`, or register a name for it. `NumpyConfig` is documented below, and the other configurations are documented with their backends: - `NumpyConfig`: the reference simulator, which needs no extra dependencies (`provider="numpy"`). - PennyLaneConfig - QiskitConfig - QiskitAerConfig: the Qiskit Aer simulator, with the statevector and the matrix product state methods. - QiskitRuntimeConfig - IQMConfig - BraketConfig - CudaQuantumConfig - TensorCircuitConfig - QulacsConfig - QsimConfig The blocks and `NumpyConfig` are imported with this module, because they only need NumPy. The other configurations are imported when they are first used, so importing this module does not import PyTorch, PennyLane, Qiskit or Braket. The quantities to measure are set with `Circuit(observables=...)`. They are not a block. Example ```python import numpy as np from eigenkit import Circuit, all_z from eigenkit.configs import Encoding, Variational, NumpyConfig circuit = Circuit( blocks=[Encoding(in_features=3), Variational(depth=2)], observables=all_z(3), provider=NumpyConfig(), ) print(circuit.spec.n_qubits, circuit.spec.n_weights) # 3 6 print(circuit(np.zeros(circuit.spec.n_weights), np.zeros(3))) # [1. 1. 1.] ``` To change the backend, you change one argument. `provider=PennyLaneConfig(...)` runs the same blocks on PennyLane, and `provider="numpy"` selects the same reference simulator by its registered name. ### `Encoding` Encode `in_features` data values as single-qubit rotations. Feature `i` sets the angle of one rotation on qubit `i`, so the block uses the qubits `0` to `in_features - 1`. This matters only when the block is combined with others. A circuit has as many qubits as its widest block, and when another block in the list needs more qubits, this block leaves the extra qubits unchanged. The block takes `in_features` inputs and no weights. Parameters: | Name | Type | Description | Default | | ------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `in_features` | `int` | The number of features to encode, one per qubit. It must be positive, and a ValueError is raised otherwise. | *required* | | `rotation` | `GateName` | The rotation gate: "RX", "RY" or "RZ". Default: "RY". | `'RY'` | | `scale` | `float` | A constant factor applied to every input angle. It is useful when the data is normalised to [0, 1] and the circuit needs angles in [0, 2π]. It must be finite. Default: 1.0. | `1.0` | #### `build(n_qubits)` Build the gates of this block for a circuit with `n_qubits` qubits. The qubits with an index of `in_features` or more are left unchanged. Parameters: | Name | Type | Description | Default | | ---------- | ----- | ------------------------------------------------------------------------------------------------------------------- | ---------- | | `n_qubits` | `int` | The number of qubits of the circuit being assembled, which is the number of qubits of the widest block in the list. | *required* | Returns: | Type | Description | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `IRFragment` | The gates of the block, as an IRFragment. Its weight and input indices start at 0, and they are shifted when the blocks are combined into a circuit. | #### `from_dict(d)` Rebuild a block from the output of `to_dict()`. Parameters: | Name | Type | Description | Default | | ---- | ------ | ----------------------------------- | ---------- | | `d` | `dict` | A dictionary returned by to_dict(). | *required* | Returns: | Type | Description | | ----------------- | -------------------------- | | `'AngleEncoding'` | The rebuilt AngleEncoding. | #### `required_qubits()` Return `in_features`, because the block uses one qubit per feature. #### `to_dict()` Return a description of this block that can be saved as JSON. When a saved model is loaded, the `type` key selects the block class whose `from_dict()` rebuilds the block. Returns: | Type | Description | | ------ | -------------------------------------- | | `dict` | The fields of the block, and its type. | ### `NumpyConfig` Configuration for the reference NumPy simulator. It has no settings yet. It exists so that `Circuit(provider=NumpyConfig())` works in the same way as with the configurations of the other backends. #### `make_backend()` Build the backend that this configuration describes. The provider layer calls it when a configuration is passed as `provider=`. There is no reason to call it directly. Returns: | Type | Description | | ------------------ | ------------------------------------------ | | `'NumpySimulator'` | The NumpySimulator for this configuration. | ### `Reuploading` Data reuploading: the input is encoded several times, between trainable layers. The block repeats an encoding of the input followed by a variational layer, `depth` times. Each encoding can have a trainable scale for each feature and repetition. The arguments give two patterns: 1. Standard reuploading (the default; Pérez-Salinas et al. 2020). The same input is encoded `depth` times, with new variational weights between the encodings. 1. Trainable feature scaling (`trainable_scale=True`; Schuld et al. 2021). Each encoding rotation takes the angle `λ_{d,i} * x_i`, where each pair of repetition `d` and feature `i` has its own trainable `λ_{d,i}`. Parameters: | Name | Type | Description | Default | | ---------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `in_features` | `int` | The total number of input features. It must be positive. | *required* | | `depth` | `int` | The number of times the whole sequence is repeated. It must be positive. | *required* | | `encoding_rotation` | `GateName` | The rotation gate of the encoding: "RX", "RY" or "RZ". Default: "RY". | `'RY'` | | `variational_rotation` | `GateName` | The rotation gate of the variational layer: "RX", "RY" or "RZ". It is used only when variational_type="basic". Default: "RY". | `'RY'` | | `variational_type` | `str` | "basic", for one rotation per qubit followed by a ladder of controlled-NOT (CNOT) gates, or "strong", for RZ, RY and RZ rotations on every qubit followed by a ladder of CNOT gates. Default: "basic". | `'basic'` | | `trainable_scale` | `bool` | If True, each encoding rotation gets a trainable scale for each repetition and each feature. The CUDA Quantum backend does not support it, because it allows only limited arithmetic on parameters. Default: False. | `False` | Weight count - With `variational_type="basic"` and no trainable scale, the block has `depth * in_features` weights. - With `variational_type="strong"` and no trainable scale, it has `3 * depth * in_features` weights. - `trainable_scale=True` adds `depth * in_features` weights. #### `build(n_qubits)` Build the gates of this block for a circuit with `n_qubits` qubits. Parameters: | Name | Type | Description | Default | | ---------- | ----- | ------------------------------------------------------------------------------------------------------------------- | ---------- | | `n_qubits` | `int` | The number of qubits of the circuit being assembled, which is the number of qubits of the widest block in the list. | *required* | Returns: | Type | Description | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `IRFragment` | The gates of the block, as an IRFragment. Its weight and input indices start at 0, and they are shifted when the blocks are combined into a circuit. | #### `from_dict(d)` Rebuild a block from the output of `to_dict()`. Parameters: | Name | Type | Description | Default | | ---- | ------ | ----------------------------------- | ---------- | | `d` | `dict` | A dictionary returned by to_dict(). | *required* | Returns: | Type | Description | | --------------- | ------------------------ | | `'Reuploading'` | The rebuilt Reuploading. | #### `required_qubits()` Return `in_features`: one qubit for each feature. #### `to_dict()` Return a description of this block that can be saved as JSON. When a saved model is loaded, the `type` key selects the block class whose `from_dict()` rebuilds the block. Returns: | Type | Description | | ------ | -------------------------------------- | | `dict` | The fields of the block, and its type. | ### `StrongVariational` `depth` trainable layers of `RZ`, `RY` and `RZ` rotations and entangling gates. Each layer applies the rotations `RZ`, `RY` and `RZ` to every qubit, then a controlled-NOT (CNOT) gate from each qubit to the next one. The block is modelled on PennyLane's `StronglyEntanglingLayers`. The gates are added one layer at a time: all the `RZ` rotations, then all the `RY` rotations, then all the `RZ` rotations, then the CNOT gates. They are not added as three rotations per qubit. The result is the same, because rotations on different qubits commute. The layered order lets the backends that support it apply each layer of rotations in one fast call, in place of `3 * n_qubits` calls on single qubits. The block takes `3 * depth * n_qubits` weights and no inputs. Parameters: | Name | Type | Description | Default | | ------- | ----- | -------------------------------------------------------------------------------- | ---------- | | `depth` | `int` | The number of layers. It must be positive, and a ValueError is raised otherwise. | *required* | #### `build(n_qubits)` Build the gates of this block for a circuit with `n_qubits` qubits. The layers act on every qubit of the circuit. Parameters: | Name | Type | Description | Default | | ---------- | ----- | ------------------------------------------------------------------------------------------------------------------- | ---------- | | `n_qubits` | `int` | The number of qubits of the circuit being assembled, which is the number of qubits of the widest block in the list. | *required* | Returns: | Type | Description | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `IRFragment` | The gates of the block, as an IRFragment. Its weight and input indices start at 0, and they are shifted when the blocks are combined into a circuit. | #### `from_dict(d)` Rebuild a block from the output of `to_dict()`. Parameters: | Name | Type | Description | Default | | ---- | ------ | ----------------------------------- | ---------- | | `d` | `dict` | A dictionary returned by to_dict(). | *required* | Returns: | Type | Description | | --------------------- | ------------------------------ | | `'StrongVariational'` | The rebuilt StrongVariational. | #### `required_qubits()` Return `1`, because the block extends to all the qubits of the circuit. #### `to_dict()` Return a description of this block that can be saved as JSON. When a saved model is loaded, the `type` key selects the block class whose `from_dict()` rebuilds the block. Returns: | Type | Description | | ------ | -------------------------------------- | | `dict` | The fields of the block, and its type. | ### `Variational` `depth` trainable layers of single-qubit rotations and entangling gates. Each layer applies one trainable rotation to every qubit, then a controlled-NOT (CNOT) gate from each qubit to the next one. The block takes `depth * n_qubits` weights and no inputs. Parameters: | Name | Type | Description | Default | | ---------- | ---------- | -------------------------------------------------------------------------------- | ---------- | | `depth` | `int` | The number of layers. It must be positive, and a ValueError is raised otherwise. | *required* | | `rotation` | `GateName` | The rotation gate of the trainable layer: "RX", "RY" or "RZ". Default: "RY". | `'RY'` | #### `build(n_qubits)` Build the gates of this block for a circuit with `n_qubits` qubits. The layers act on every qubit of the circuit. Parameters: | Name | Type | Description | Default | | ---------- | ----- | ------------------------------------------------------------------------------------------------------------------- | ---------- | | `n_qubits` | `int` | The number of qubits of the circuit being assembled, which is the number of qubits of the widest block in the list. | *required* | Returns: | Type | Description | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `IRFragment` | The gates of the block, as an IRFragment. Its weight and input indices start at 0, and they are shifted when the blocks are combined into a circuit. | #### `from_dict(d)` Rebuild a block from the output of `to_dict()`. Parameters: | Name | Type | Description | Default | | ---- | ------ | ----------------------------------- | ---------- | | `d` | `dict` | A dictionary returned by to_dict(). | *required* | Returns: | Type | Description | | -------------------- | ----------------------------- | | `'BasicVariational'` | The rebuilt BasicVariational. | #### `required_qubits()` Return `1`, because the block extends to all the qubits of the circuit. #### `to_dict()` Return a description of this block that can be saved as JSON. When a saved model is loaded, the `type` key selects the block class whose `from_dict()` rebuilds the block. Returns: | Type | Description | | ------ | -------------------------------------- | | `dict` | The fields of the block, and its type. | ## `eigenkit.core` The internal circuit layer of eigenkit. This package re-exports the classes and functions that users need, and it registers the default backends when it is imported. The entry points are: - `Circuit` creates a circuit that can be run. - `CircuitBuilder` builds a circuit one gate at a time. - `AngleEncoding`, `BasicVariational`, `StrongVariational`, `Reuploading`, `CustomBlock` and `assemble` combine blocks into a circuit. - `X`, `Y`, `Z`, `all_z`, `from_string` and `Observable` build observables. - `PennyLaneConfig`, `QiskitConfig` and the other configurations hold the settings of each backend. - `register` gives a configuration a name, so that it can be used again. - `BaseQuantumSolver` is the base class of every solver that computes its result in a single call. The top-level `provider=` argument accepts a registered name, a configuration dataclass, or a backend instance. ### `AmplitudeEncoding` Encode a classical vector as the amplitudes of a quantum state. The vector is padded with zeros to the next power of two. With `normalize=True`, the default, it is also rescaled to norm `1`. The number of qubits follows from the length of the vector: it is `max(1, ceil(log2(len(amplitudes))))`. The block adds a single `AmplitudeEncoding` gate. The `data` field of the gate holds the prepared amplitudes as a flat tuple, and each backend loads them with its own state preparation method. Parameters: | Name | Type | Description | Default | | ------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------- | | `amplitudes` | `object` | The vector to load, as an array-like. Its length sets the number of qubits, and it is padded with zeros to the next power of two. A ValueError is raised if the vector is empty, if all its values are zero, or if it holds a not-a-number (NaN) or infinite value. | *required* | | `normalize` | `bool` | If True, the vector is rescaled to norm 1 before it is loaded. Pass False when the vector is already a state. The block then stores it unchanged and checks it: a ValueError is raised if its norm differs from 1 by more than 1e-10. This check exists because, without it, the same mistake would be caught or not depending on the backend: the NumPy simulator rescales the vector without a warning, and PennyLane refuses it. The same circuit would then behave differently on different backends. Default: True. | `True` | Example ```python from eigenkit.core.blocks import AmplitudeEncoding, assemble spec = assemble([AmplitudeEncoding([0.6, 0.8], normalize=False)]) ``` #### `build(n_qubits)` Build the gates of this block for a circuit with `n_qubits` qubits. Parameters: | Name | Type | Description | Default | | ---------- | ----- | ------------------------------------------------------------------------------------------------------------------- | ---------- | | `n_qubits` | `int` | The number of qubits of the circuit being assembled, which is the number of qubits of the widest block in the list. | *required* | Returns: | Type | Description | | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `'IRFragment'` | The gates of the block, as an IRFragment. Its weight and input indices start at 0, and they are shifted when the blocks are combined into a circuit. | #### `from_dict(d)` Rebuild a block from the output of `to_dict()`. Parameters: | Name | Type | Description | Default | | ---- | ------ | ----------------------------------- | ---------- | | `d` | `dict` | A dictionary returned by to_dict(). | *required* | Returns: | Type | Description | | --------------------- | ------------------------------ | | `'AmplitudeEncoding'` | The rebuilt AmplitudeEncoding. | #### `required_qubits()` Return the number of qubits that the padded vector needs. It is `ceil(log2(len(amplitudes)))`, and at least `1`. #### `to_dict()` Return a description of this block that can be saved as JSON. When a saved model is loaded, the `type` key selects the block class whose `from_dict()` rebuilds the block. Returns: | Type | Description | | ------ | -------------------------------------- | | `dict` | The fields of the block, and its type. | ### `AngleEncoding` Encode `in_features` data values as single-qubit rotations. Feature `i` sets the angle of one rotation on qubit `i`, so the block uses the qubits `0` to `in_features - 1`. This matters only when the block is combined with others. A circuit has as many qubits as its widest block, and when another block in the list needs more qubits, this block leaves the extra qubits unchanged. The block takes `in_features` inputs and no weights. Parameters: | Name | Type | Description | Default | | ------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `in_features` | `int` | The number of features to encode, one per qubit. It must be positive, and a ValueError is raised otherwise. | *required* | | `rotation` | `GateName` | The rotation gate: "RX", "RY" or "RZ". Default: "RY". | `'RY'` | | `scale` | `float` | A constant factor applied to every input angle. It is useful when the data is normalised to [0, 1] and the circuit needs angles in [0, 2π]. It must be finite. Default: 1.0. | `1.0` | #### `build(n_qubits)` Build the gates of this block for a circuit with `n_qubits` qubits. The qubits with an index of `in_features` or more are left unchanged. Parameters: | Name | Type | Description | Default | | ---------- | ----- | ------------------------------------------------------------------------------------------------------------------- | ---------- | | `n_qubits` | `int` | The number of qubits of the circuit being assembled, which is the number of qubits of the widest block in the list. | *required* | Returns: | Type | Description | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `IRFragment` | The gates of the block, as an IRFragment. Its weight and input indices start at 0, and they are shifted when the blocks are combined into a circuit. | #### `from_dict(d)` Rebuild a block from the output of `to_dict()`. Parameters: | Name | Type | Description | Default | | ---- | ------ | ----------------------------------- | ---------- | | `d` | `dict` | A dictionary returned by to_dict(). | *required* | Returns: | Type | Description | | ----------------- | -------------------------- | | `'AngleEncoding'` | The rebuilt AngleEncoding. | #### `required_qubits()` Return `in_features`, because the block uses one qubit per feature. #### `to_dict()` Return a description of this block that can be saved as JSON. When a saved model is loaded, the `type` key selects the block class whose `from_dict()` rebuilds the block. Returns: | Type | Description | | ------ | -------------------------------------- | | `dict` | The fields of the block, and its type. | ### `BaseQuantumSolver` Bases: `ABC` Base class for quantum solvers that compute a result in a single call. Unlike BaseQuantumEstimator, a solver has no training phase and no fitted state, and it does not follow the scikit-learn estimator interface. It receives a mathematical problem directly in `solve()` and returns a structured result object. Four classes inherit from it: HHL, IQAE, QAOA and VQE. PortfolioRiskEstimator does not, by design. It is a scikit-learn-style estimator with `fit()`, `estimate()` and `var_at()`, and it has no `solve()` method. As a result, `isinstance(x, BaseQuantumSolver)` tells you whether an object can be given a problem directly, and it returns `False` for the estimator. Each subclass sets its own `provider` default in its signature and passes it to this class. All of them use `"numpy"`. Two of the modes of `IQAE`, a callable oracle and `shot_mode="provider"`, need `provider="pennylane"`. Parameters: | Name | Type | Description | Default | | ---------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | | `provider` | `ProviderSpec` | The backend to use: a registered name ("numpy", "pennylane", "qiskit" and so on), a config instance, or a backend object. The default is "numpy", the built-in simulator that the core installation includes. | `'numpy'` | Example ```python from eigenkit.core import BaseQuantumSolver from eigenkit.optimization import QAOA isinstance(QAOA(), BaseQuantumSolver) # True ``` #### `solve(*args, **kwargs)` Compute and return the solution. Each subclass declares the signature that its problem needs. `HHL` takes a matrix and a right-hand side, `IQAE` takes an oracle, `QAOA` takes a problem, and `VQE` takes a Hamiltonian and the number of qubits. ### `BasicVariational` `depth` trainable layers of single-qubit rotations and entangling gates. Each layer applies one trainable rotation to every qubit, then a controlled-NOT (CNOT) gate from each qubit to the next one. The block takes `depth * n_qubits` weights and no inputs. Parameters: | Name | Type | Description | Default | | ---------- | ---------- | -------------------------------------------------------------------------------- | ---------- | | `depth` | `int` | The number of layers. It must be positive, and a ValueError is raised otherwise. | *required* | | `rotation` | `GateName` | The rotation gate of the trainable layer: "RX", "RY" or "RZ". Default: "RY". | `'RY'` | #### `build(n_qubits)` Build the gates of this block for a circuit with `n_qubits` qubits. The layers act on every qubit of the circuit. Parameters: | Name | Type | Description | Default | | ---------- | ----- | ------------------------------------------------------------------------------------------------------------------- | ---------- | | `n_qubits` | `int` | The number of qubits of the circuit being assembled, which is the number of qubits of the widest block in the list. | *required* | Returns: | Type | Description | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `IRFragment` | The gates of the block, as an IRFragment. Its weight and input indices start at 0, and they are shifted when the blocks are combined into a circuit. | #### `from_dict(d)` Rebuild a block from the output of `to_dict()`. Parameters: | Name | Type | Description | Default | | ---- | ------ | ----------------------------------- | ---------- | | `d` | `dict` | A dictionary returned by to_dict(). | *required* | Returns: | Type | Description | | -------------------- | ----------------------------- | | `'BasicVariational'` | The rebuilt BasicVariational. | #### `required_qubits()` Return `1`, because the block extends to all the qubits of the circuit. #### `to_dict()` Return a description of this block that can be saved as JSON. When a saved model is loaded, the `type` key selects the block class whose `from_dict()` rebuilds the block. Returns: | Type | Description | | ------ | -------------------------------------- | | `dict` | The fields of the block, and its type. | ### `Circuit` A compiled circuit, ready to evaluate. It holds a `CircuitSpec`, the observables to measure and a compiled backend handle. The circuit is compiled once, when the instance is created, and every call reuses the compiled handle. The instance does not store weights: you pass the `weights` at each call. This is the NumPy interface. The top-level `Circuit(...)` function of the package returns this class for `interface="numpy"` and a `TorchCircuit` for `interface="torch"`. Parameters: | Name | Type | Description | Default | | ------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------- | | `spec` | `CircuitSpec` | The circuit to run, usually built with assemble(blocks). | *required* | | `observables` | `ObservableLike` | What to measure. None measures Z on every qubit and returns one value per qubit. A single Observable or PauliExpr returns a single value, and a list of them returns one value for each. Default: None. | `None` | | `provider` | `ProviderSpec` | The backend that runs the circuit. It can be a registered name ("numpy", "pennylane" and others), a configuration instance, or a provider object. Default: "numpy". | `'numpy'` | | `interface` | `Interface` | How a call is evaluated. With "numpy", a call runs the NumPy path and returns NumPy values. With "torch", each call is passed to the registered PyTorch interface, and TorchCircuit builds its core circuit in this way. The top-level Circuit function always passes "numpy". Default: "numpy". | `'numpy'` | Example ```python import numpy as np from eigenkit import Circuit, Encoding, Variational, all_z circuit = Circuit( blocks=[Encoding(in_features=2), Variational(depth=1)], observables=all_z(2), ) # The circuit stores no weights, so you pass them at every call. weights = np.zeros(circuit.spec.n_weights) values = circuit(weights, np.array([0.1, 0.2])) # Gradients, without PyTorch. grad = circuit.jacobian(weights, np.array([0.1, 0.2])) ``` #### `draw(*, decimals=3)` Return a text drawing of the circuit, over several lines. The drawing does not depend on the provider: it is built from the `CircuitSpec`, not from the compiled backend object. See `eigenkit.core.draw` for the details of the layout. Parameters: | Name | Type | Description | Default | | ---------- | ----- | ---------------------------------------------------------------------------- | ------- | | `decimals` | `int` | The number of significant digits shown for a numeric gate angle. Default: 3. | `3` | Returns: | Type | Description | | ----- | ------------------------------------------------------- | | `str` | The drawing, as a single string that contains newlines. | #### `draw_mpl(**kwargs)` Draw the circuit as a matplotlib figure, and return `(fig, ax)`. Gates are drawn as labelled boxes and controls as filled dots, and each measured wire (from `self.observables`) ends with a measurement symbol. As with `draw()`, the picture is built from the `CircuitSpec` and the observables, not from the compiled backend, so it does not depend on the provider. Pass `style="eigen"` for the brand colours (each gate is coloured by its kind: rotations, `H` and block gates, and measurements), or `style="roles"` to colour the outline of each gate by its role (encoding, trainable or fixed). The default is black on white. Each style has a `"-dark"` variant (for example `"eigen-dark"`) for dark backgrounds, and `"eigen-universal"` is a single image that works on both. matplotlib is an optional dependency. Install it with `eigenkit[viz]`. Parameters: | Name | Type | Description | Default | | ---------- | ----- | --------------------------------------------------------------------------------------------------------- | ------- | | `**kwargs` | `Any` | Arguments passed to draw_mpl(): decimals, show_params, show_measurements, fontsize, style, legend and ax. | `{}` | Returns: | Name | Type | Description | | -------- | ------- | ------------------------------ | | `fig_ax` | `tuple` | The matplotlib (fig, ax) pair. | #### `dry_run_info()` Describe the prepared circuit in a dictionary, without submitting it. It is available only on backends whose `CompiledCircuit` provides a `dry_run_info` method. Currently, these are Braket (on its local simulator as well as on cloud devices), Qiskit Runtime and IQM. It is useful to check the size of the circuit, the gate counts after transpilation, the number of shots and the number of circuits that each call runs, before you submit to a cloud simulator or a quantum processing unit (QPU). It does not estimate a price. #### `jacobian(weights, inputs=None, *, input_grad=False)` Return the gradient of the expectation values with respect to the weights. It returns `d/d(weights)`, of shape `(n_weights,)` for a single observable or `(n_observables, n_weights)` for a list of observables, like the output of `__call__()`. Pass a single 1-D `inputs` sample. For a batch, call it once for each row. The NumPy interface can compute every gradient without PyTorch. This method uses the same sequence of gradient methods as the variational optimisation tools and the PyTorch interface. It uses the backend's own differentiation when the backend has one (the analytic adjoint method of the NumPy simulator, the differentiation of PennyLane, and so on), and otherwise the parameter-shift rule, which works on any backend. With `input_grad=True`, it returns `(weight_jacobian, input_jacobian)`, where `input_jacobian` is `d/d(inputs)`, with the same leading shape. Parameters: | Name | Type | Description | Default | | ------------ | --------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `weights` | `ndarray` | The weight vector, of shape (n_weights,). | *required* | | `inputs` | \`ndarray | None\` | One 1-D sample, of shape (n_inputs,). This method takes a single sample, so for a batch, call it once for each row. None works only for a circuit that takes no inputs, and it raises a ValueError for any other circuit. Default: None. | | `input_grad` | `bool` | If True, also return the jacobian with respect to the inputs. Default: False. | `False` | Returns: | Name | Type | Description | | ---------- | --------- | ------------------------- | | `jacobian` | \`ndarray | tuple[ndarray, ndarray]\` | Raises: | Type | Description | | -------------- | ----------------------------------------------------------------------------------------------- | | `RuntimeError` | On a hardware backend whose configuration leaves enable_gradients off (gradient_method="none"). | #### `probabilities(weights, inputs=None)` Return the measurement distribution over all `2**n_qubits` basis states. The basis states are ordered with qubit `0` as the most significant bit. The distribution is used to decode samples. For example, the quantum approximate optimisation algorithm (QAOA) reads the most probable bitstrings from the optimised state. Parameters: | Name | Type | Description | Default | | --------- | --------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `weights` | `ndarray` | The weight vector, of shape (n_weights,). | *required* | | `inputs` | \`ndarray | None\` | One 1-D sample. None works only for a circuit that takes no inputs, and it raises a ValueError for any other circuit. Default: None. | Returns: | Type | Description | | --------- | ----------------------------------------------------------------- | | `ndarray` | The probabilities of all basis states, of shape (2\*\*n_qubits,). | Raises: | Type | Description | | --------------------- | ------------------------------------------------------------------------------------------------------------ | | `NotImplementedError` | On a backend that has neither a probabilities() method nor a state vector to compute the probabilities from. | #### `resources(**kwargs)` Report the resources that the circuit uses once transpiled for its target. It reports the native gate counts, the number of two-qubit gates and the two-qubit depth, the physical qubits chosen by the layout, the number of two-qubit gates added by routing (compared with the same circuit on the native gates with all-to-all connectivity), and the success probability predicted by the error rates of the target. Nothing is submitted. Parameters: | Name | Type | Description | Default | | ---------- | ----- | ------------------------------------------------------------------------------------------------------------- | ------- | | `**kwargs` | `Any` | Arguments passed to the backend, for example measured=False on Qiskit Runtime for the bare estimator circuit. | `{}` | Returns: | Type | Description | | ------ | ----------------------------------------------------------------- | | `dict` | A dictionary. See QiskitRuntimeCompiled.resources() for its keys. | Raises: | Type | Description | | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `NotImplementedError` | On backends that do not transpile for a device, which includes every simulator. To inspect a device locally, use Qiskit Runtime or IQM with a fake backend_instance. | #### `sample(weights, inputs=None)` Return the raw measurement histogram, which maps each bitstring to a count. It is what a sampling backend measured, before any averaging. These are the counts behind `probabilities()`, and it is the only form that stays usable when the circuit has too many qubits for a dense distribution. In each bitstring, qubit `0` is the most significant bit, so `"100"` means that qubit `0` was measured in state `1`. Only sampling backends can return a histogram, because it needs a finite number of shots. Exact statevector simulators raise an error instead of making up a histogram. Use `probabilities()` with those. Parameters: | Name | Type | Description | Default | | --------- | --------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `weights` | `ndarray` | The weight vector, of shape (n_weights,). | *required* | | `inputs` | \`ndarray | None\` | One 1-D sample. None works only for a circuit that takes no inputs, and it raises a ValueError for any other circuit. Default: None. | Returns: | Name | Type | Description | | -------- | ---------------- | -------------------------------------------------------------------------------------------------------------- | | `counts` | `dict[str, int]` | The number of shots for each measured bitstring. The counts add up to the number of shots set for the backend. | Raises: | Type | Description | | --------------------- | -------------------------------------------------------------------------------------------------- | | `NotImplementedError` | On backends that do not sample. This includes every exact simulator, which has no shots to report. | #### `statevector(weights, inputs=None, initial_state=None)` Return the full complex statevector after the circuit runs. It is a flat array of length `2**n_qubits`, ordered with qubit `0` as the most significant bit. A 2-D `inputs` batch returns an array of shape `(B, 2**n_qubits)`. With `initial_state`, a normalised flat statevector in the same order, the evolution starts from that state instead of `|0…0⟩`. Reading a state out and passing it back in this way is the basic operation behind the state carried over in quantum reservoirs and behind amplitude encoding. Parameters: | Name | Type | Description | Default | | --------------- | --------- | ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | `weights` | `ndarray` | The weight vector, of shape (n_weights,). | *required* | | `inputs` | \`ndarray | None\` | A 1-D sample, or a 2-D batch. None works only for a circuit that takes no inputs, and it raises a ValueError for any other circuit. Default: None. | | `initial_state` | \`ndarray | None\` | The state to start the evolution from, instead of | Returns: | Type | Description | | --------- | ------------------------------------------------------------------------------ | | `ndarray` | The statevector, of shape (2\*\*n_qubits,), or (B, 2\*\*n_qubits) for a batch. | Raises: | Type | Description | | --------------------- | --------------------------------------------------------------------------------------------------------------------------- | | `NotImplementedError` | On backends without access to the statevector, which is the case for most quantum processing unit (QPU) and cloud backends. | #### `submit(weights, inputs=None)` Submit a job without waiting for it, and return a `JobHandle`. `handle.result()` returns the expectation value of each observable. Parameters: | Name | Type | Description | Default | | --------- | --------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `weights` | `ndarray` | The weight vector, of shape (n_weights,). | *required* | | `inputs` | \`ndarray | None\` | One 1-D sample. None works only for a circuit that takes no inputs, and it raises a ValueError for any other circuit. Default: None. | Returns: | Name | Type | Description | | -------- | ----------- | ------------------------------- | | `handle` | `JobHandle` | A handle for the submitted job. | Raises: | Type | Description | | --------------------- | -------------------------------------------------------------------------------------------------------------------- | | `NotImplementedError` | If the CompiledCircuit of the backend does not provide submit. Currently, Braket, Qiskit Runtime and IQM provide it. | #### `transpiled_circuit(**kwargs)` Return the `qiskit.QuantumCircuit` that a hardware backend submits. The circuit is transpiled for the target of the backend. Use it to draw the circuit (`.draw("mpl")`) or to inspect it. Raises: | Type | Description | | --------------------- | ----------------------------------------------- | | `NotImplementedError` | On backends that do not transpile for a device. | #### `zero_state_probabilities(weights, inputs)` Compute `zero_state_probability()` for every row of a batch of inputs. `inputs` has shape `(B, n_inputs)`, and the result is an array of shape `(B,)` of all-zeros probabilities. Backends that provide a batched `zero_state_probabilities` (such as the NumPy simulator) compute the whole batch at once. The other backends call the single-sample method once for each row. This is what lets QuantumKernel build a fidelity matrix without a Python loop over pairs of samples. Parameters: | Name | Type | Description | Default | | --------- | --------- | ----------------------------------------- | ---------- | | `weights` | `ndarray` | The weight vector, of shape (n_weights,). | *required* | | `inputs` | `ndarray` | A batch, of shape (B, n_inputs). | *required* | Returns: | Type | Description | | --------- | ------------------------------------------------------------------ | | `ndarray` | One all-zeros probability for each row, as an array of shape (B,). | #### `zero_state_probability(weights, inputs=None)` Return the probability of measuring the all-zeros state after the circuit. It is useful for quantities based on fidelity: quantum kernels built from inversion circuits, swap tests and the Bures distance. Each backend computes it in its own way. PennyLane returns `qml.probs(...)[0]`, the Qiskit and NumPy simulators return the squared amplitude of the all-zeros basis state, Braket uses its own probability result, and IQM and Qiskit Runtime estimate it from shots. This method does not go through the `Observable` path, because the value is not the expectation value of a Pauli operator. Parameters: | Name | Type | Description | Default | | --------- | --------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `weights` | `ndarray` | The weight vector, of shape (n_weights,). | *required* | | `inputs` | \`ndarray | None\` | One 1-D sample. None works only for a circuit that takes no inputs, and it raises a ValueError for any other circuit. Default: None. | Returns: | Type | Description | | ------- | ----------------------------------- | | `float` | The probability, a float in [0, 1]. | Raises: | Type | Description | | --------------------- | -------------------------------------------------------------- | | `NotImplementedError` | If the active backend does not provide zero_state_probability. | ### `CircuitBuilder` Build a `CircuitSpec` one gate at a time. Example ```python qc = CircuitBuilder(n_qubits=3) weights = qc.add_weights(6) inputs = qc.add_inputs(3) for qubit in range(3): qc.ry(inputs[qubit], qubit=qubit) for depth in range(2): for qubit in range(3): qc.ry(weights[depth * 3 + qubit], qubit=qubit) for qubit in range(2): qc.cnot(qubit, qubit + 1) spec = qc.build() ``` Parameters: | Name | Type | Description | Default | | ---------- | ----- | ----------------------------------------------------------------------------------------------------------------------- | ---------- | | `n_qubits` | `int` | The number of qubits of the circuit being built. Every qubit index that a gate uses must be between 0 and n_qubits - 1. | *required* | Raises: | Type | Description | | ------------ | ---------------------------- | | `ValueError` | If n_qubits is not positive. | #### `n_inputs` The number of input features that `add_inputs()` has reserved so far. The built CircuitSpec has the same number. #### `n_qubits` The number of qubits of the circuit, as given to the constructor. Every qubit index that a gate uses must be smaller than this number, and the number never changes. #### `n_weights` The number of trainable weights that `add_weights()` has reserved so far. The built CircuitSpec has the same number. #### `add_inputs(count)` Reserve `count` input features and return references to them. This is the counterpart of `add_weights()` for data instead of trainable parameters. Parameters: | Name | Type | Description | Default | | ------- | ----- | ---------------------------------------- | ---------- | | `count` | `int` | The number of input features to reserve. | *required* | Returns: | Type | Description | | ---------------- | ------------------------------------------------- | | `list[InputRef]` | One InputRef for each reserved feature, in order. | #### `add_weights(count)` Reserve `count` trainable weights and return references to them. Pass a returned `WeightRef` wherever a gate takes an angle. The indices are local to this builder and start at `0`. When the block is combined with others, `assemble` shifts them to their positions in the whole circuit. Parameters: | Name | Type | Description | Default | | ------- | ----- | --------------------------------- | ---------- | | `count` | `int` | The number of weights to reserve. | *required* | Returns: | Type | Description | | ----------------- | ------------------------------------------------- | | `list[WeightRef]` | One WeightRef for each reserved weight, in order. | #### `build()` Return the current state of the builder as an immutable `CircuitSpec`. #### `cnot(control, target)` Add a controlled-NOT (CNOT) gate. Parameters: | Name | Type | Description | Default | | --------- | ----- | --------------------------------- | ---------- | | `control` | `int` | The qubit that controls the flip. | *required* | | `target` | `int` | The qubit that is flipped. | *required* | Returns: | Type | Description | | ------------------ | ------------------------------------------- | | `'CircuitBuilder'` | This builder, so that calls can be chained. | Raises: | Type | Description | | ------------ | ----------------------------------------- | | `ValueError` | If control and target are the same qubit. | #### `cphase(angle, control, target)` Add a controlled phase shift gate. The gate is symmetric: the control and the target play the same role. Parameters: | Name | Type | Description | Default | | --------- | --------------- | ------------------------------------------------------------------------------- | ---------- | | `angle` | `ParameterLike` | The phase. It can be a number or a symbolic reference. | *required* | | `control` | `int` | One of the two qubits. | *required* | | `target` | `int` | The other qubit. The gate acts in the same way if the two qubits are exchanged. | *required* | Returns: | Type | Description | | ------------------ | ------------------------------------------- | | `'CircuitBuilder'` | This builder, so that calls can be chained. | #### `cry(angle, control, target)` Add a controlled `RY` gate. Parameters: | Name | Type | Description | Default | | --------- | --------------- | --------------------------------------------------------------- | ---------- | | `angle` | `ParameterLike` | The rotation angle. It can be a number or a symbolic reference. | *required* | | `control` | `int` | The qubit that controls the rotation. | *required* | | `target` | `int` | The qubit that is rotated. | *required* | Returns: | Type | Description | | ------------------ | ------------------------------------------- | | `'CircuitBuilder'` | This builder, so that calls can be chained. | #### `h(qubit)` Add a Hadamard gate. Parameters: | Name | Type | Description | Default | | ------- | ----- | ----------------- | ---------- | | `qubit` | `int` | The target qubit. | *required* | Returns: | Type | Description | | ------------------ | ------------------------------------------- | | `'CircuitBuilder'` | This builder, so that calls can be chained. | #### `mcx(*qubits)` Add a multi-controlled `X` gate. All the qubits except the last one are controls. Parameters: | Name | Type | Description | Default | | --------- | ----- | ------------------------------------------------------------------------------------- | ------- | | `*qubits` | `int` | Two or more distinct qubits. The controls come first, and the target is the last one. | `()` | Returns: | Type | Description | | ------------------ | ------------------------------------------- | | `'CircuitBuilder'` | This builder, so that calls can be chained. | Raises: | Type | Description | | ------------ | -------------------------------------------------------------- | | `ValueError` | If fewer than two qubits are given, or if a qubit is repeated. | #### `mcz(*qubits)` Add a multi-controlled `Z` gate. All the qubits except the last one are controls. Parameters: | Name | Type | Description | Default | | --------- | ----- | ------------------------------------------------------------------------------------- | ------- | | `*qubits` | `int` | Two or more distinct qubits. The controls come first, and the target is the last one. | `()` | Returns: | Type | Description | | ------------------ | ------------------------------------------- | | `'CircuitBuilder'` | This builder, so that calls can be chained. | Raises: | Type | Description | | ------------ | -------------------------------------------------------------- | | `ValueError` | If fewer than two qubits are given, or if a qubit is repeated. | #### `pauli_exponential(paulis, angle, coefficient=1.0)` Add the exponential of a product of Pauli operators. The gate is `exp(-i * coefficient * angle * P)`, where `P` is the product of `paulis`. Each element of `paulis` is a pair `(op, qubit)`, where `op` is `"X"`, `"Y"` or `"Z"`, and the qubits are distinct. This is the Trotter building block that the quantum approximate optimisation algorithm (QAOA) needs for its cost and mixer layers, and that the unitary coupled-cluster singles and doubles (UCCSD) ansatz needs for its excitation operators. It is built from the core gates only. First, `H` gates (for `X`) and `RX(π/2)` gates (for `Y`) change each factor to the `Z` basis. Then a ladder of controlled-NOT (CNOT) gates accumulates the parity on the last qubit, and a central `RZ(2 * coefficient * angle)` is applied (because `RZ(φ) = exp(-i φ Z / 2)`). Finally, the inverse of the first steps is applied. Parameters: | Name | Type | Description | Default | | ------------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `paulis` | `Sequence[tuple[str, int]]` | The Pauli product, as (operator, qubit) pairs, where the operator is "X", "Y" or "Z" and the qubits are distinct. An empty sequence is the identity, and nothing is added. | *required* | | `angle` | `ParameterLike` | The rotation angle. It can be a number or a symbolic reference. | *required* | | `coefficient` | `float` | A number that multiplies the angle. This is where the weight of a Hamiltonian term goes. Default: 1.0. | `1.0` | Returns: | Type | Description | | ------------------ | ------------------------------------------- | | `'CircuitBuilder'` | This builder, so that calls can be chained. | #### `phase_shift(angle, qubit)` Add a phase shift gate `R(φ)`. The gate multiplies `|1⟩` by `e^(iφ)` and leaves `|0⟩` unchanged. Parameters: | Name | Type | Description | Default | | ------- | --------------- | -------------------------------------------------------- | ---------- | | `angle` | `ParameterLike` | The phase φ. It can be a number or a symbolic reference. | *required* | | `qubit` | `int` | The target qubit. | *required* | Returns: | Type | Description | | ------------------ | ------------------------------------------- | | `'CircuitBuilder'` | This builder, so that calls can be chained. | #### `rx(angle, qubit)` Add a rotation about the `X` axis. Parameters: | Name | Type | Description | Default | | ------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ---------- | | `angle` | `ParameterLike` | The rotation angle. It can be a number, or a WeightRef, an InputRef or an expression, which is evaluated when the circuit is called. | *required* | | `qubit` | `int` | The target qubit. | *required* | Returns: | Type | Description | | ------------------ | ------------------------------------------- | | `'CircuitBuilder'` | This builder, so that calls can be chained. | #### `ry(angle, qubit)` Add a rotation about the `Y` axis. Parameters: | Name | Type | Description | Default | | ------- | --------------- | ------------------------------------------------------------------------------------- | ---------- | | `angle` | `ParameterLike` | The rotation angle. It can be a number, or a WeightRef, an InputRef or an expression. | *required* | | `qubit` | `int` | The target qubit. | *required* | Returns: | Type | Description | | ------------------ | ------------------------------------------- | | `'CircuitBuilder'` | This builder, so that calls can be chained. | #### `rz(angle, qubit)` Add a rotation about the `Z` axis. Parameters: | Name | Type | Description | Default | | ------- | --------------- | ------------------------------------------------------------------------------------- | ---------- | | `angle` | `ParameterLike` | The rotation angle. It can be a number, or a WeightRef, an InputRef or an expression. | *required* | | `qubit` | `int` | The target qubit. | *required* | Returns: | Type | Description | | ------------------ | ------------------------------------------- | | `'CircuitBuilder'` | This builder, so that calls can be chained. | #### `s(qubit)` Add an `S` gate, which is a quarter turn about the `Z` axis. Its inverse is `sdg()`. Parameters: | Name | Type | Description | Default | | ------- | ----- | ----------------- | ---------- | | `qubit` | `int` | The target qubit. | *required* | Returns: | Type | Description | | ------------------ | ------------------------------------------- | | `'CircuitBuilder'` | This builder, so that calls can be chained. | #### `sdg(qubit)` Add an S-dagger gate (`Sdg`), which is the inverse of `s()`. Parameters: | Name | Type | Description | Default | | ------- | ----- | ----------------- | ---------- | | `qubit` | `int` | The target qubit. | *required* | Returns: | Type | Description | | ------------------ | ------------------------------------------- | | `'CircuitBuilder'` | This builder, so that calls can be chained. | #### `swap(qubit0, qubit1)` Add a `SWAP` gate, which exchanges the states of two qubits. Parameters: | Name | Type | Description | Default | | -------- | ----- | ----------------- | ---------- | | `qubit0` | `int` | The first qubit. | *required* | | `qubit1` | `int` | The second qubit. | *required* | Returns: | Type | Description | | ------------------ | ------------------------------------------- | | `'CircuitBuilder'` | This builder, so that calls can be chained. | #### `to_block()` Wrap the current state of the builder in a `CustomBlock`. This is useful to build a part of a circuit one gate at a time, and then to combine it with other blocks. When the blocks are assembled, the indices of the `WeightRef` and `InputRef` objects of this block are shifted to their positions in the whole circuit. #### `x(qubit)` Add a Pauli `X` gate, which is a bit flip. Parameters: | Name | Type | Description | Default | | ------- | ----- | ----------------- | ---------- | | `qubit` | `int` | The target qubit. | *required* | Returns: | Type | Description | | ------------------ | ------------------------------------------- | | `'CircuitBuilder'` | This builder, so that calls can be chained. | #### `z(qubit)` Add a Pauli `Z` gate, which is a phase flip. Parameters: | Name | Type | Description | Default | | ------- | ----- | ----------------- | ---------- | | `qubit` | `int` | The target qubit. | *required* | Returns: | Type | Description | | ------------------ | ------------------------------------------- | | `'CircuitBuilder'` | This builder, so that calls can be chained. | ### `CircuitSpec` A whole circuit as data. It holds the list of gates, the number of qubits, and the lengths of the weight and input vectors that the gates refer to. Every provider compiles this object, and `assemble` builds it from a list of blocks. It holds no weights and no observables: the weights are passed at each call, and the observables are set by the `Circuit` that wraps the spec. Attributes: | Name | Type | Description | | ----------- | ------------------ | -------------------------------------------------------------------------- | | `n_qubits` | `int` | The number of qubits of the circuit. | | `n_weights` | `int` | The length of the weight vector that the gates refer to through WeightRef. | | `n_inputs` | `int` | The length of the input vector that the gates refer to through InputRef. | | `gates` | `tuple[Gate, ...]` | The gates, in the order in which they are applied. | ### `CostWarning` Bases: `UserWarning` Warning issued before a submission to a paid quantum backend. A paid backend is a quantum processing unit (QPU) or a cloud simulator that charges for use. The first compilation for such a device issues this warning, so that costs cannot build up without the user noticing. ### `CustomBlock` A block made from the gates of a `CircuitBuilder`. `CircuitBuilder.to_block()` creates it. It is useful when you want to write a part of a circuit gate by gate and combine it with the standard blocks. Parameters: | Name | Type | Description | Default | | ----------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `n_qubits` | `int` | The number of qubits that the builder was given, which is also the number of qubits that this block needs. It must be positive. | *required* | | `gates` | `tuple[Gate, ...]` | The gates that the builder added, in the order in which they are applied. | *required* | | `n_weights` | `int` | The number of trainable weights that these gates use. It must not be negative. | *required* | | `n_inputs` | `int` | The number of input features that these gates use. It must not be negative. | *required* | #### `build(n_qubits)` Build the gates of this block for a circuit with `n_qubits` qubits. Parameters: | Name | Type | Description | Default | | ---------- | ----- | ------------------------------------------------------------------------------------------------------------------- | ---------- | | `n_qubits` | `int` | The number of qubits of the circuit being assembled, which is the number of qubits of the widest block in the list. | *required* | Returns: | Type | Description | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `IRFragment` | The gates of the block, as an IRFragment. Its weight and input indices start at 0, and they are shifted when the blocks are combined into a circuit. | #### `required_qubits()` Return `n_qubits`, the number of qubits that the builder was given. ### `Grover` The Grover operator `Q = A · S₀ · A† · S_ψ₁` used for amplitude estimation. This is the operator that iterative quantum amplitude estimation (IQAE) uses. The block builds it from a state-preparation oracle `A`, given as a `CircuitSpec`, and from these standard parts: - **`S_ψ₁`** flips the phase of the ancilla qubit, which is the last qubit. It is the gate `Z(n-1)`. - **`A†`** is the adjoint of the oracle. It uses the same weights and inputs as `A`. - **`S₀`** is the reflection about the all-zero state: `X^⊗n → MCZ → X^⊗n`, where `MCZ` is a multi-controlled `Z` gate. - **`A`** is the oracle. The gates are applied to the state in this order, from left to right: ```text Z(ancilla) → A† → S₀ → A ``` `A` and `A†` share their weights and inputs, so `fragment.n_weights == a_circuit.n_weights`: the count is not doubled. Parameters: | Name | Type | Description | Default | | ----------- | ------------- | ---------------------------------------------------------------------------------------------------------------- | ---------- | | `a_circuit` | `CircuitSpec` | The state-preparation oracle A, as a CircuitSpec with at least one qubit. Its last qubit is used as the ancilla. | *required* | Example ```python from eigenkit.core.blocks import Grover, assemble from eigenkit.core.builder import CircuitBuilder qb = CircuitBuilder(n_qubits=2) qb.h(0).cnot(0, 1) spec = assemble([Grover(a_circuit=qb.build())]) ``` #### `build(n_qubits)` Build the gates of this block for a circuit with `n_qubits` qubits. The oracle `A` and its adjoint `A†` share their weights and inputs, so these are not counted twice. Parameters: | Name | Type | Description | Default | | ---------- | ----- | ------------------------------------------------------------------------------------------------------------------- | ---------- | | `n_qubits` | `int` | The number of qubits of the circuit being assembled, which is the number of qubits of the widest block in the list. | *required* | Returns: | Type | Description | | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `'IRFragment'` | The gates of the block, as an IRFragment. Its weight and input indices start at 0, and they are shifted when the blocks are combined into a circuit. | #### `from_dict(d)` Rebuild a block from the output of `to_dict()`. Parameters: | Name | Type | Description | Default | | ---- | ------ | ----------------------------------- | ---------- | | `d` | `dict` | A dictionary returned by to_dict(). | *required* | Returns: | Type | Description | | ---------- | ------------------- | | `'Grover'` | The rebuilt Grover. | #### `required_qubits()` Return the number of qubits of the oracle. The operator adds no qubits of its own. #### `to_dict()` Return a description of this block that can be saved as JSON. When a saved model is loaded, the `type` key selects the block class whose `from_dict()` rebuilds the block. Returns: | Type | Description | | ------ | -------------------------------------- | | `dict` | The fields of the block, and its type. | ### `JobHandle` Bases: `Protocol` A handle to a job running on a quantum processing unit (QPU) or in the cloud. `compiled.submit(...)` returns it without waiting for the job. Call `.result()` to wait for the job and combine its results. #### `result()` Wait until the job finishes, then return its expectation values. The shape follows the same rule as CompiledCircuit.run(): `(n_obs,)` for a single sample, and `(B, n_obs)` for a batch. #### `status()` Return the state of the job as the provider reports it, without waiting. The strings are the provider's own. For example, Braket returns `"CREATED"`, `"QUEUED"`, `"RUNNING"`, `"COMPLETED"`, `"FAILED"` or `"CANCELLED"`. Compare them within one provider, not across providers. ### `NumpyConfig` Configuration for the reference NumPy simulator. It has no settings yet. It exists so that `Circuit(provider=NumpyConfig())` works in the same way as with the configurations of the other backends. #### `make_backend()` Build the backend that this configuration describes. The provider layer calls it when a configuration is passed as `provider=`. There is no reason to call it directly. Returns: | Type | Description | | ------------------ | ------------------------------------------ | | `'NumpySimulator'` | The NumpySimulator for this configuration. | ### `NumpySimulator` The provider for the reference NumPy statevector simulator. It is registered automatically as `"numpy"`. It uses only NumPy and no optional dependency, so it is always available. It declares `gradient_method = "adjoint"`, so the gradient code of the core package uses NumpyCompiled.jacobian() (analytic, in reverse mode, with one forward pass and one backward pass whatever the number of parameters) instead of the parameter-shift rule, whose cost is `O(n_params)`. #### `capabilities` What this backend supports. It runs every gate of the library, evaluates a batch of inputs in one call, returns exact expectation values only (it does not sample shots), and has no limit on the number of qubits. See Capabilities. #### `__init__(config=None, name='numpy')` Build the backend from a device configuration. Parameters: | Name | Type | Description | Default | | -------- | ------------- | ------------------------------------------------------------- | --------- | | `config` | `NumpyConfig` | The device settings. Default: None, which uses NumpyConfig(). | `None` | | `name` | `str` | The registry name, used in error messages. Default: "numpy". | `'numpy'` | #### `compile(circuit, observables)` Prepare `circuit` for repeated evaluation of `observables`. A `Circuit` calls it once and reuses the result for every call, so the expensive work belongs here. Parameters: | Name | Type | Description | Default | | ------------- | ---------------------- | --------------------------- | ---------- | | `circuit` | `CircuitSpec` | The circuit to prepare. | *required* | | `observables` | `Sequence[Observable]` | The observables to measure. | *required* | Returns: | Type | Description | | --------------- | ------------------------------------------------ | | `NumpyCompiled` | The prepared circuit, as a NumpyCompiled object. | ### `Observable` A Hermitian operator to measure, written as a sum of Pauli terms. You can build one with the `z`, `x` and `y` methods for a single Pauli operator on one qubit, with `from_string` for a whole sum, or with the `X`, `Y` and `Z` functions of `eigenkit.core.observables`, which support arithmetic. Attributes: | Name | Type | Description | | ------- | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `terms` | `tuple[PauliTerm, ...]` | The Pauli terms of the sum. It can be empty: jordan_wigner returns an empty observable, together with an offset, when the operator reduces to a multiple of the identity. However, Circuit refuses to measure an empty observable, because there is nothing to measure. | #### `from_string(text)` Parse a sum of Pauli terms written as a string. Examples of such strings are `"0.5 Z0 X1 + 0.3 Y2"` and `"-Z0 + Z1"`. The grammar is as follows. The terms are joined by `+` or `-`. Each term starts with an optional numeric coefficient, in plain or scientific notation (`1e-3` is accepted). The coefficient is followed by an optional `*` and by one or more `[XYZ]` operators on distinct qubits. Any amount of whitespace is accepted, including none. Every part of the string must be a valid term, and every term must contain Pauli operators. A constant term (`"0.5 + Z0"`, `"0.5 I"`) raises an error instead of being dropped from the sum: an `Observable` holds only Pauli terms, and the constant must be kept next to it as an offset. Parameters: | Name | Type | Description | Default | | ------ | ----- | -------------------- | ---------- | | `text` | `str` | The string to parse. | *required* | Returns: | Type | Description | | -------------- | ---------------------- | | `'Observable'` | The parsed observable. | Raises: | Type | Description | | ------------ | ------------------------------------------------------------------------------------------------------------------------------ | | `ValueError` | If text is empty, if it contains a constant term, if a part of it is not a valid term, or if a term uses the same qubit twice. | #### `x(qubit)` Return the observable `X` on the single qubit `qubit`. Parameters: | Name | Type | Description | Default | | ------- | ----- | ----------------------- | ---------- | | `qubit` | `int` | The index of the qubit. | *required* | #### `y(qubit)` Return the observable `Y` on the single qubit `qubit`. Parameters: | Name | Type | Description | Default | | ------- | ----- | ----------------------- | ---------- | | `qubit` | `int` | The index of the qubit. | *required* | #### `z(qubit)` Return the observable `Z` on the single qubit `qubit`. Parameters: | Name | Type | Description | Default | | ------- | ----- | ----------------------- | ---------- | | `qubit` | `int` | The index of the qubit. | *required* | ### `QubitUnitary` Apply any unitary matrix to the given qubits. The matrix is stored in the `data` field of the gate as a nested tuple, so that the `Gate` stays hashable. Parameters: | Name | Type | Description | Default | | --------- | ----------------- | ------------------------------------------------------------------------------------------------------------------- | ---------- | | `unitary` | `object` | A 2^n × 2^n matrix, as an array-like, where n is len(wires). It is checked to be unitary when the block is created. | *required* | | `wires` | `tuple[int, ...]` | The target qubits, in the order that the matrix expects. | *required* | Example ```python import numpy as np from eigenkit.core.blocks import QubitUnitary, assemble U = np.array([[0, 1], [1, 0]]) # the Pauli X matrix spec = assemble([QubitUnitary(U, wires=(0,))]) ``` #### `build(n_qubits)` Build the gates of this block for a circuit with `n_qubits` qubits. Parameters: | Name | Type | Description | Default | | ---------- | ----- | ------------------------------------------------------------------------------------------------------------------- | ---------- | | `n_qubits` | `int` | The number of qubits of the circuit being assembled, which is the number of qubits of the widest block in the list. | *required* | Returns: | Type | Description | | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `'IRFragment'` | The gates of the block, as an IRFragment. Its weight and input indices start at 0, and they are shifted when the blocks are combined into a circuit. | #### `from_dict(d)` Rebuild a block from the output of `to_dict()`. Parameters: | Name | Type | Description | Default | | ---- | ------ | ----------------------------------- | ---------- | | `d` | `dict` | A dictionary returned by to_dict(). | *required* | Returns: | Type | Description | | ---------------- | ------------------------- | | `'QubitUnitary'` | The rebuilt QubitUnitary. | #### `required_qubits()` Return `max(wires) + 1`, one more than the highest target qubit. #### `to_dict()` Return a description of this block that can be saved as JSON. When a saved model is loaded, the `type` key selects the block class whose `from_dict()` rebuilds the block. Returns: | Type | Description | | ------ | -------------------------------------- | | `dict` | The fields of the block, and its type. | ### `Reuploading` Data reuploading: the input is encoded several times, between trainable layers. The block repeats an encoding of the input followed by a variational layer, `depth` times. Each encoding can have a trainable scale for each feature and repetition. The arguments give two patterns: 1. Standard reuploading (the default; Pérez-Salinas et al. 2020). The same input is encoded `depth` times, with new variational weights between the encodings. 1. Trainable feature scaling (`trainable_scale=True`; Schuld et al. 2021). Each encoding rotation takes the angle `λ_{d,i} * x_i`, where each pair of repetition `d` and feature `i` has its own trainable `λ_{d,i}`. Parameters: | Name | Type | Description | Default | | ---------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `in_features` | `int` | The total number of input features. It must be positive. | *required* | | `depth` | `int` | The number of times the whole sequence is repeated. It must be positive. | *required* | | `encoding_rotation` | `GateName` | The rotation gate of the encoding: "RX", "RY" or "RZ". Default: "RY". | `'RY'` | | `variational_rotation` | `GateName` | The rotation gate of the variational layer: "RX", "RY" or "RZ". It is used only when variational_type="basic". Default: "RY". | `'RY'` | | `variational_type` | `str` | "basic", for one rotation per qubit followed by a ladder of controlled-NOT (CNOT) gates, or "strong", for RZ, RY and RZ rotations on every qubit followed by a ladder of CNOT gates. Default: "basic". | `'basic'` | | `trainable_scale` | `bool` | If True, each encoding rotation gets a trainable scale for each repetition and each feature. The CUDA Quantum backend does not support it, because it allows only limited arithmetic on parameters. Default: False. | `False` | Weight count - With `variational_type="basic"` and no trainable scale, the block has `depth * in_features` weights. - With `variational_type="strong"` and no trainable scale, it has `3 * depth * in_features` weights. - `trainable_scale=True` adds `depth * in_features` weights. #### `build(n_qubits)` Build the gates of this block for a circuit with `n_qubits` qubits. Parameters: | Name | Type | Description | Default | | ---------- | ----- | ------------------------------------------------------------------------------------------------------------------- | ---------- | | `n_qubits` | `int` | The number of qubits of the circuit being assembled, which is the number of qubits of the widest block in the list. | *required* | Returns: | Type | Description | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `IRFragment` | The gates of the block, as an IRFragment. Its weight and input indices start at 0, and they are shifted when the blocks are combined into a circuit. | #### `from_dict(d)` Rebuild a block from the output of `to_dict()`. Parameters: | Name | Type | Description | Default | | ---- | ------ | ----------------------------------- | ---------- | | `d` | `dict` | A dictionary returned by to_dict(). | *required* | Returns: | Type | Description | | --------------- | ------------------------ | | `'Reuploading'` | The rebuilt Reuploading. | #### `required_qubits()` Return `in_features`: one qubit for each feature. #### `to_dict()` Return a description of this block that can be saved as JSON. When a saved model is loaded, the `type` key selects the block class whose `from_dict()` rebuilds the block. Returns: | Type | Description | | ------ | -------------------------------------- | | `dict` | The fields of the block, and its type. | ### `S0` Reflection about the all-zero state `|0…0⟩`, built as `X^⊗n → MCZ → X^⊗n`. The block applies `X` to every qubit, then a multi-controlled `Z` gate (`MCZ`) on all of them, then `X` to every qubit again. It acts on the qubits `0` to `n_qubits - 1`, which are all the qubits of the circuit unless another block in the list needs more. This reflection is the `S₀` part of the Grover operator used by iterative quantum amplitude estimation (IQAE). The Grover block adds the same gates itself. Parameters: | Name | Type | Description | Default | | ---------- | ----- | ------------------------------------------------------------------------------------------ | ---------- | | `n_qubits` | `int` | The number of qubits of the reflection. Every one of them takes part. It must be positive. | *required* | Example ```python from eigenkit.core.blocks import S0, assemble spec = assemble([S0(n_qubits=3)]) ``` #### `build(n_qubits)` Build the gates of this block for a circuit with `n_qubits` qubits. Parameters: | Name | Type | Description | Default | | ---------- | ----- | ------------------------------------------------------------------------------------------------------------------- | ---------- | | `n_qubits` | `int` | The number of qubits of the circuit being assembled, which is the number of qubits of the widest block in the list. | *required* | Returns: | Type | Description | | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `'IRFragment'` | The gates of the block, as an IRFragment. Its weight and input indices start at 0, and they are shifted when the blocks are combined into a circuit. | #### `from_dict(d)` Rebuild a block from the output of `to_dict()`. Parameters: | Name | Type | Description | Default | | ---- | ------ | ----------------------------------- | ---------- | | `d` | `dict` | A dictionary returned by to_dict(). | *required* | Returns: | Type | Description | | ------ | --------------- | | `'S0'` | The rebuilt S0. | #### `required_qubits()` Return `n_qubits`, the number of qubits that the reflection acts on. #### `to_dict()` Return a description of this block that can be saved as JSON. When a saved model is loaded, the `type` key selects the block class whose `from_dict()` rebuilds the block. Returns: | Type | Description | | ------ | -------------------------------------- | | `dict` | The fields of the block, and its type. | ### `StrongVariational` `depth` trainable layers of `RZ`, `RY` and `RZ` rotations and entangling gates. Each layer applies the rotations `RZ`, `RY` and `RZ` to every qubit, then a controlled-NOT (CNOT) gate from each qubit to the next one. The block is modelled on PennyLane's `StronglyEntanglingLayers`. The gates are added one layer at a time: all the `RZ` rotations, then all the `RY` rotations, then all the `RZ` rotations, then the CNOT gates. They are not added as three rotations per qubit. The result is the same, because rotations on different qubits commute. The layered order lets the backends that support it apply each layer of rotations in one fast call, in place of `3 * n_qubits` calls on single qubits. The block takes `3 * depth * n_qubits` weights and no inputs. Parameters: | Name | Type | Description | Default | | ------- | ----- | -------------------------------------------------------------------------------- | ---------- | | `depth` | `int` | The number of layers. It must be positive, and a ValueError is raised otherwise. | *required* | #### `build(n_qubits)` Build the gates of this block for a circuit with `n_qubits` qubits. The layers act on every qubit of the circuit. Parameters: | Name | Type | Description | Default | | ---------- | ----- | ------------------------------------------------------------------------------------------------------------------- | ---------- | | `n_qubits` | `int` | The number of qubits of the circuit being assembled, which is the number of qubits of the widest block in the list. | *required* | Returns: | Type | Description | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `IRFragment` | The gates of the block, as an IRFragment. Its weight and input indices start at 0, and they are shifted when the blocks are combined into a circuit. | #### `from_dict(d)` Rebuild a block from the output of `to_dict()`. Parameters: | Name | Type | Description | Default | | ---- | ------ | ----------------------------------- | ---------- | | `d` | `dict` | A dictionary returned by to_dict(). | *required* | Returns: | Type | Description | | --------------------- | ------------------------------ | | `'StrongVariational'` | The rebuilt StrongVariational. | #### `required_qubits()` Return `1`, because the block extends to all the qubits of the circuit. #### `to_dict()` Return a description of this block that can be saved as JSON. When a saved model is loaded, the `type` key selects the block class whose `from_dict()` rebuilds the block. Returns: | Type | Description | | ------ | -------------------------------------- | | `dict` | The fields of the block, and its type. | ### `VariationalResult` Bases: `ValueEquality` Outcome of minimize_expectation(). The result is read-only. Assigning to a field raises `FrozenInstanceError`, and writing into an array field raises `ValueError`. This prevents a mistyped `=` (instead of `==`) or an in-place edit from leaving a result whose scalar fields describe a different run from the array it holds. To get a writable copy of the array, use `np.array(result.field)`. Results compare by value, including their arrays. They are not hashable, because a result is a collection of numbers and not a key, and a correct hash of a float array is expensive to compute. Attributes: | Name | Type | Description | | -------------------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `optimal_value` | `float` | The lowest expectation value reached. When the observable is a Hamiltonian, it is the estimated ground-state energy. | | `optimal_parameters` | `ndarray` | The weights that produced this value, of shape (n_weights,). | | `n_iterations` | `int` | The number of iterations reported by the SciPy optimiser. When there is no such count (a custom callable, "spsa", or a SciPy method that does not report one), it is the number of cost evaluations, as in n_evaluations. | | `n_evaluations` | `int` | The number of circuit evaluations that the optimiser made. With a SciPy method it is usually higher than n_iterations, because a line search costs several evaluations per step. Evaluations made only for bookkeeping are not counted, so a custom optimiser that never called the cost function reports 0. | | `success` | `bool` | Whether the run converged. A SciPy optimiser reports it. A custom callable reports it only if it returns (parameters, converged), and otherwise it is inferred from whether the final cost is lower than the starting cost. A False with a plausible energy usually means that the maximum number of iterations was reached. | | `message` | `str` | The termination message of the optimiser. For a custom callable that reported nothing, it gives the cost before and after the run. | | `history` | `tuple[float, ...]` | The expectation value at every evaluation, in order. Plot it to see whether the run converged or stalled. It is a tuple, so nothing can be appended to it after the run. | #### `__post_init__()` Make the array field read-only. Freezing the dataclass prevents a field from being reassigned, but it does not prevent writing into the array that the field holds, and a result that has been written into still reports the scalar values of the original run. The result therefore keeps its own copy, so the array it was built from can still be changed by the caller. To get a writable copy, use `np.array(result.optimal_parameters)`. ### `X(qubit)` Return Pauli `X` on `qubit`, as an expression that can be added and scaled. Parameters: | Name | Type | Description | Default | | ------- | ----- | ----------------------- | ---------- | | `qubit` | `int` | The index of the qubit. | *required* | Returns: | Type | Description | | ----------- | --------------- | | `PauliExpr` | The expression. | ### `Y(qubit)` Return Pauli `Y` on `qubit`, as an expression that can be added and scaled. Parameters: | Name | Type | Description | Default | | ------- | ----- | ----------------------- | ---------- | | `qubit` | `int` | The index of the qubit. | *required* | Returns: | Type | Description | | ----------- | --------------- | | `PauliExpr` | The expression. | ### `Z(qubit)` Return Pauli `Z` on `qubit`, as an expression that can be added and scaled. Parameters: | Name | Type | Description | Default | | ------- | ----- | ----------------------- | ---------- | | `qubit` | `int` | The index of the qubit. | *required* | Returns: | Type | Description | | ----------- | --------------- | | `PauliExpr` | The expression. | ### `__getattr__(name)` Return the objects that are re-exported only when they are first accessed. These are the backend classes and configurations. Because of this, importing `core` does not import any framework until one is needed. ### `adjoint(circuit)` Reverse the order of the gates and invert each gate. The transform does not depend on the provider. It returns another `CircuitSpec` that any backend can compile, so the transform does not have to be written again for each backend. Each type of gate is inverted as follows: - **Rotations with parameters** (`RX`, `RY`, `RZ`, `PhaseShift`, `CRY`, `CPHASE`): the angle parameters are negated. - **Gates without parameters** (`H`, `X`, `Z`, `CNOT`, `SWAP`, `MCZ`, `MCX`): they are copied unchanged, because applying them twice gives the identity. `S` and `Sdg` are not their own inverse, so each one is replaced by the other. - **`QubitUnitary(U)`**: the stored matrix is replaced by its conjugate transpose `U†`. It is computed without NumPy, so that the intermediate representation stays pure Python. - **`ControlledQubitUnitary(U)`**: the target unitary (`data[0]`) is replaced by its conjugate transpose, and the control values (`data[1]`) are kept unchanged. - **`AmplitudeEncoding`**: the transform flips `gate.adjoint` and leaves the inversion to the backend. PennyLane, for example, applies the conjugate transpose of the state-preparation unitary. The Qiskit backend does not support this adjoint: `Circuit` rejects such a circuit with a `ValueError`, and the backend itself raises `NotImplementedError`. Parameters: | Name | Type | Description | Default | | --------- | ------------- | ---------------------- | ---------- | | `circuit` | `CircuitSpec` | The circuit to invert. | *required* | Returns: | Type | Description | | ------------- | ------------------------------------------------------ | | `CircuitSpec` | The inverted circuit, with the gates in reverse order. | ### `all_z(n_qubits)` Return one `Z` observable for each qubit. This is useful as a default measurement. Parameters: | Name | Type | Description | Default | | ---------- | ----- | ------------------------------------ | ---------- | | `n_qubits` | `int` | The number of qubits of the circuit. | *required* | Returns: | Type | Description | | ------------------ | ------------------------------ | | `list[Observable]` | One Observable for each qubit. | ### `assemble(blocks)` Combine blocks into a single `CircuitSpec`. A `CircuitBuilder` can take the place of a block. It is replaced by the result of its `to_block()`, so a part of a circuit written gate by gate can go directly in the list. The blocks are combined as follows: - `n_qubits` is the largest `required_qubits()` of the blocks. - The weights and the inputs of the blocks are placed one after the other, in the order of the blocks. - The local `WeightRef` and `InputRef` indices of each block are shifted by the number of weights and inputs of the blocks before it. Parameters: | Name | Type | Description | Default | | -------- | ----------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `blocks` | \`Iterable\[Block | CircuitBuilder\]\` | The blocks to combine, in the order in which they are applied. Any of them can be a CircuitBuilder, which is taken as it is when assemble is called. | Returns: | Type | Description | | ------------- | --------------------------------------------- | | `CircuitSpec` | One CircuitSpec that contains all the blocks. | Raises: | Type | Description | | ------------ | ------------------- | | `ValueError` | If blocks is empty. | Example ```python from eigenkit import Encoding, Variational from eigenkit.core import assemble spec = assemble([Encoding(in_features=4), Variational(depth=2)]) print(spec.n_qubits, spec.n_weights, spec.n_inputs) # 4 8 4 ``` ### `from_string(text)` Parse an observable from a string. This function is a shortcut for `Observable.from_string`. Parameters: | Name | Type | Description | Default | | ------ | ----- | -------------------- | ---------- | | `text` | `str` | The string to parse. | *required* | Returns: | Type | Description | | ------------ | ---------------------- | | `Observable` | The parsed observable. | ### `minimize_expectation(circuit, *, initial_parameters=None, optimizer='L-BFGS-B', max_iterations=None, tol=None, random_state=42, callback=None)` Minimise `(θ)` for a circuit whose parameters are all variational. Parameters: | Name | Type | Description | Default | | -------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | `circuit` | `Circuit` | A core NumPy Circuit whose single observable is the Hamiltonian H and whose spec.n_inputs == 0 (every parameter is a variational weight). It works with any provider. | *required* | | `initial_parameters` | \`ndarray | None\` | The starting θ, of shape (n_weights,). When it is not given, it is drawn at random with the seed random_state. | | `optimizer` | `Optimizer` | The name of a scipy.optimize.minimize method (the default is "L-BFGS-B"), "spsa" for spsa(), or a callable optimizer(cost, theta0, gradient) -> parameters for a custom or framework optimiser. SPSA needs no gradient: each iteration evaluates the circuit twice for the gradient estimate and once more to record the history. A callable can instead return (parameters, converged) to report convergence itself. Otherwise, success is inferred from whether the final cost is lower than the starting cost, because a vector alone says nothing about convergence. | `'L-BFGS-B'` | | `max_iterations` | \`int | None\` | The maximum number of iterations. It is passed to SciPy as options={"maxiter": ...}, and to spsa() as maxiter, which is 100 when max_iterations is None. | | `tol` | \`float | None\` | The tolerance, passed to scipy.optimize.minimize. | | `random_state` | \`int | None\` | The seed for the random starting point, which also seeds the perturbations of "spsa". None draws an unseeded starting point. | | `callback` | \`Callable\[[ndarray], None\] | None\` | An optional function called after every iteration with the current parameters. It is passed to SciPy, and "spsa" also calls it. | Returns: | Type | Description | | ------------------- | ------------------------------------------------------------------------------------------------------------ | | `VariationalResult` | A VariationalResult with the optimal value, the parameters, and the history of the cost at every evaluation. | Example ```python from eigenkit import Circuit, Variational from eigenkit.core.ir import Observable from eigenkit.core.variational import minimize_expectation # One qubit, one trainable rotation, H = Z. The minimum is -1. circuit = Circuit( blocks=[Variational(depth=1)], observables=Observable.from_string("Z0"), provider="numpy", ) result = minimize_expectation(circuit, random_state=42) print(round(result.optimal_value, 6)) # -1.0 print(result.success, result.n_iterations) # True 4 ``` ### `register(name, provider_or_config)` Register a backend, or a configuration that can build one, under `name`. Later calls to `Circuit(..., provider=name)` then use that backend. If a configuration is passed, its `make_backend()` method is called to build the backend. Parameters: | Name | Type | Description | Default | | -------------------- | ---------- | --------------------- | -------------------------------------------------------------------- | | `name` | `str` | The name to register. | *required* | | `provider_or_config` | \`Provider | object\` | A backend instance, or a configuration with a make_backend() method. | ### `register_lazy(name, loader)` Register a backend under `name` without importing its package yet. `loader` is called the first time the name is looked up, and its result is kept for later lookups. The optional backends are registered in this way, so that naming them imports nothing. It replaces whatever `name` currently refers to, as `register` does. If the registration were skipped when the name was already taken, a caller could not replace a built-in backend with a lazy one, and would get no sign that the loader had been ignored. Parameters: | Name | Type | Description | Default | | -------- | ------------------------ | --------------------------------------------------------------------------------------------------------- | ---------- | | `name` | `str` | The name to register. | *required* | | `loader` | `Callable[[], Provider]` | A function with no arguments that returns the backend. It is called the first time the name is looked up. | *required* | ### `registered_names()` Return every provider name that `get` accepts, including the lazy ones. ## `eigenkit.datasets` The datasets of eigenkit: the registry of their metadata, and the loaders. ### `FONTSIZE_AXES = 20` The font size, in points, that `rc_params()` gives the axis labels and titles. ### `FONTSIZE_LEGEND = 18` The font size, in points, that `rc_params()` gives the legend entries. ### `FONTSIZE_TICKS = 16` The font size, in points, of the tick labels, which is also the base `font.size`. ### `STYLE_COLORS = ('#CE2173', '#AA5EFF', '#7D4ECF', '#43CDFF', '#3333FB', '#11E57A', '#E6C541', '#94A3B8')` The plot palette of the library, in the order of the colour cycle. `rc_params()` sets it as `axes.prop_cycle`, so a plot drawn inside that context uses these colours without naming them. Index it when a series needs a fixed colour, and not the next one in the cycle. For example, use `STYLE_COLORS[0]` for a training curve and `STYLE_COLORS[1]` for validation, so that the two curves keep their colours across figures. It is a tuple, so that an `append` by mistake cannot change how every later plot in the process looks. ### `DatasetInfo` Bases: `BaseModel` The metadata of a dataset that eigenkit provides. Parameters: | Name | Type | Description | Default | | --------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `name` | `str` | The unique name of the dataset, which is also its key in REGISTRY. | *required* | | `source` | `str` | Where the dataset comes from. It is one of these values: 'sklearn': the dataset is loaded with a function of sklearn.datasets. 'url': the dataset is downloaded from a web address. 'synthetic': the dataset is generated by a function, for example make_moons. | *required* | | `url` | `str` | The web address of the file to download. It is used only when source='url'. | `None` | | `sha256` | `str` | The SHA-256 hash, in hexadecimal, that the downloaded file must match. A downloaded file that does not match is deleted, and a ValueError is raised. A cached file that does not match is downloaded again. It is used only when source='url'. | `None` | | `sklearn_loader` | `str` | The name of the sklearn.datasets function, for example 'load_iris'. It is used only when source='sklearn'. | `None` | | `synthetic_generator` | `str` | The name of the generator function, for example 'make_moons'. It is used only when source='synthetic'. | `None` | | `task` | `str` | The type of task: 'classification', 'regression', 'timeseries', 'optimization' or 'chemistry'. | *required* | | `n_features` | `int` | The number of input features. It must be at least 1. | *required* | | `n_samples` | `int` | The total number of samples. It is None when the number is not recorded, for example for the synthetic datasets, whose size is chosen when they are generated. | `None` | | `n_classes` | `int` | The number of classes. It is None for regression and time-series tasks. | `None` | | `feature_names` | `list[str]` | Readable names of the input features. | `None` | | `target_names` | `list[str]` | Readable names of the classes, or of the output. | `None` | | `window_size` | `int` | The recommended size of the sliding window, for time-series datasets. | `None` | | `description` | `str` | A short description of the dataset. | *required* | Example ```python info = DatasetInfo( name="iris", source="sklearn", sklearn_loader="load_iris", task="classification", n_features=4, n_samples=150, n_classes=3, description="Classic multi-class classification dataset.", ) ``` ### `DatasetReport` An exploratory data analysis (EDA) report for a built-in or custom dataset. Parameters: | Name | Type | Description | Default | | --------------- | ----------- | ---------------------------------------------------------------------------------------------- | ---------- | | `X` | `ndarray` | The feature matrix, of shape (n_samples, n_features). | *required* | | `y` | `ndarray` | The target vector, of shape (n_samples,). | *required* | | `feature_names` | `list[str]` | Readable names of the features, in the order of the columns of X. | *required* | | `target_name` | `str` | The name of the target in the plots and the summary. | *required* | | `task` | `str` | The type of task, usually "classification", "regression" or "timeseries". | *required* | | `metadata` | `Any` | The metadata of the dataset, for example a DatasetInfo. | `None` | | `time_index` | `Any` | The time axis for the time-series plots. It can be numeric (array-like) or a pd.DatetimeIndex. | `None` | | `theme` | `str` | The theme of the plots, "light" or "dark". | `'light'` | Notes The recommended way to build a report is `from_data()`, which accepts the dictionaries returned by `load_dataset(...)` as well as your own `X` and `y`. Example ```python from eigenkit.datasets import DatasetReport, load_dataset data = load_dataset("iris", split=False) report = DatasetReport.from_data(data) figs = report.plot_overview() summary = report.summarize() ``` #### `from_data(data=None, *, X=None, y=None, feature_names=None, target_name='target', task=None, time_index=None, theme='light')` Build a report from a dataset dictionary, or from your own arrays or tensors. Parameters: | Name | Type | Description | Default | | --------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `data` | `dict[str, Any]` | A dataset dictionary in the format of eigenkit, either {"X", "y"} or {"X_train", "y_train", "X_test", "y_test"}. The training and test sets are combined. | `None` | | `X` | `Any` | Your own feature matrix, as a NumPy array, a pandas object, a PyTorch tensor or another array-like. | `None` | | `y` | `Any` | Your own target vector, in the same formats as X. | `None` | | `feature_names` | `list[str]` | The names of the features. If they are not given, the names come from the metadata, or they are feature_0, feature_1 and so on. | `None` | | `target_name` | `str` | The name of the target in the plots and the summary. | `'target'` | | `task` | `str` | The type of task. If it is not given, it is taken from the metadata, or guessed from y. | `None` | | `time_index` | `Any` | The time axis for the time-series analysis. | `None` | | `theme` | `str` | The theme of the plots, "light" or "dark". | `'light'` | Returns: | Type | Description | | ----------------- | --------------------------------------------------- | | `'DatasetReport'` | The report, ready for plotting and for the summary. | Raises: | Type | Description | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------ | | `ValueError` | If the inputs are missing or do not agree, for example if the shapes do not match, or if feature_names does not have one name per feature. | #### `plot(kind, **kwargs)` Build one plot, chosen by its kind. Parameters: | Name | Type | Description | Default | | ---------- | ----- | ------------------------------------------------------------------------------------------------------------------- | ---------- | | `kind` | `str` | The kind of plot: "data_health", "target", "distributions", "correlation", "feature_target" or "timeseries_target". | *required* | | `**kwargs` | `Any` | Options of the plot, such as bins, max_features and max_points. | `{}` | Returns: | Type | Description | | ----- | ---------------------- | | `Any` | The matplotlib figure. | Raises: | Type | Description | | ------------ | -------------------------------------- | | `ValueError` | If kind is not one of the kinds above. | #### `plot_overview(*, max_features=None, bins=30, max_points=2000, path=None)` Build the default set of plots of the exploratory data analysis. Parameters: | Name | Type | Description | Default | | -------------- | ----- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | | `max_features` | `int` | The largest number of features shown in the panels about features. None shows every feature. | `None` | | `bins` | `int` | The number of histogram bins in the distribution plots. | `30` | | `max_points` | `int` | The largest number of points in the scatter plots. Larger datasets are subsampled. | `2000` | | `path` | \`str | Path\` | A directory where every overview plot is saved as a PNG file. With None, the plots are not saved. | Returns: | Type | Description | | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `dict[str, Any]` | A dictionary from each kind of plot to its matplotlib figure. The "timeseries_target" plot is included only for a time-series task. | #### `summarize()` Build a reproducible summary of the dataset, with recommendations. Returns: | Type | Description | | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `dict[str, Any]` | A dictionary with statistics about the shape and the quality of the data, signals specific to the task when there are any, and a readable summary under the "text" key. | ### `generate_ad_hoc(n_samples=200, n_qubits=2, gap=0.3, random_state=42)` Generate the "ad hoc" dataset of Havlíček et al., Nature 567, 209 (2019). The dataset is designed so that a quantum kernel separates it. Each point `x` in `[0, 2π)^n` is encoded with the ZZ feature map, with two repetitions as in Qiskit's `ZZFeatureMap`. Its label is the sign of `⟨ψ(x)| V† Z^{⊗n} V |ψ(x)⟩` for a fixed random unitary `V`. The points closer than `gap` to the decision boundary are dropped, and the two classes are balanced. A kernel built from the same ZZ feature map classifies the data almost perfectly, and classical kernels usually do not. Parameters: | Name | Type | Description | Default | | -------------- | ------- | -------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | | `n_samples` | `int` | The number of points, split evenly between the two classes. | `200` | | `n_qubits` | `int` | The number of features, which is also the number of qubits of the circuit that generates the data. | `2` | | `gap` | `float` | The margin around the decision boundary, in \[0, 1). | `0.3` | | `random_state` | \`int | None\` | The seed of the unitary V and of the sampled points. | Returns: | Type | Description | | ------------------------- | ----------------------------------------------------------------------------------------------------------- | | `tuple[ndarray, ndarray]` | The pair (X, y), with X of shape (n_samples, n_qubits) and y of shape (n_samples,). The labels are 0 and 1. | Raises: | Type | Description | | ------------ | --------------------------------------------- | | `ValueError` | If gap is not in \[0, 1), or if n_qubits < 1. | ### `generate_checkerboard(n_samples=200, grid_size=2, random_state=42)` Generate a dataset with a checkerboard pattern. The points are drawn uniformly in `[0, grid_size]²`. The label is `(floor(x) + floor(y)) % 2`, which gives regions of alternating classes. Parameters: | Name | Type | Description | Default | | -------------- | ----- | ------------------------------------ | ------------------------------------------ | | `n_samples` | `int` | The number of points to generate. | `200` | | `grid_size` | `int` | The number of cells along each axis. | `2` | | `random_state` | \`int | None\` | The random seed, for reproducible results. | Returns: | Type | Description | | ------------------------- | ---------------------------------------------------------------------------- | | `tuple[ndarray, ndarray]` | The pair (X, y), with X of shape (n_samples, 2) and y of shape (n_samples,). | ### `generate_circles(n_samples=200, noise=0.05, factor=0.5, random_state=42)` Generate two concentric circles. Parameters: | Name | Type | Description | Default | | -------------- | ------- | ------------------------------------------------------------------- | ------------------------------------------ | | `n_samples` | `int` | The number of points to generate. | `200` | | `noise` | `float` | The standard deviation of the Gaussian noise added to the data. | `0.05` | | `factor` | `float` | The ratio of the inner circle to the outer circle, between 0 and 1. | `0.5` | | `random_state` | \`int | None\` | The random seed, for reproducible results. | Returns: | Type | Description | | ------------------------- | ---------------------------------------------------------------------------- | | `tuple[ndarray, ndarray]` | The pair (X, y), with X of shape (n_samples, 2) and y of shape (n_samples,). | ### `generate_moons(n_samples=200, noise=0.1, random_state=42)` Generate two half-circles that interleave, called moons. Parameters: | Name | Type | Description | Default | | -------------- | ------- | --------------------------------------------------------------- | ------------------------------------------ | | `n_samples` | `int` | The number of points to generate. | `200` | | `noise` | `float` | The standard deviation of the Gaussian noise added to the data. | `0.1` | | `random_state` | \`int | None\` | The random seed, for reproducible results. | Returns: | Type | Description | | ------------------------- | ---------------------------------------------------------------------------- | | `tuple[ndarray, ndarray]` | The pair (X, y), with X of shape (n_samples, 2) and y of shape (n_samples,). | ### `generate_regression(n_samples=200, n_features=4, noise=0.1, random_state=42)` Generate a synthetic regression dataset with scikit-learn's `make_regression`. Parameters: | Name | Type | Description | Default | | -------------- | ------- | ----------------------------------------------------------- | ------------------------------------------ | | `n_samples` | `int` | The number of samples to generate. | `200` | | `n_features` | `int` | The number of input features. | `4` | | `noise` | `float` | The standard deviation of the Gaussian noise on the output. | `0.1` | | `random_state` | \`int | None\` | The random seed, for reproducible results. | Returns: | Type | Description | | ------------------------- | ----------------------------------------------------------------------------------------------------------- | | `tuple[ndarray, ndarray]` | The pair (X, y), with X of shape (n_samples, n_features) and y of shape (n_samples,), both of type float32. | ### `generate_timeseries(n_samples=300, kind='sine', noise=0.05, random_state=42)` Generate a 1-D time series, as the pair `(t, y)`. Parameters: | Name | Type | Description | Default | | -------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | | `n_samples` | `int` | The number of time steps. | `300` | | `kind` | `str` | The shape of the series: "sine" for a sine wave with noise, or "ar" for an autoregressive process of order 1, written AR(1). | `'sine'` | | `noise` | `float` | The standard deviation of the Gaussian noise. | `0.05` | | `random_state` | \`int | None\` | The random seed, for reproducible results. | Returns: | Type | Description | | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `tuple[ndarray, ndarray]` | The pair (t, y), two arrays of shape (n_samples,) and type float32. t holds time values spaced evenly in [0, 1], and y is the signal. The arrays are not cut into windows, because the algorithms do that. | ### `get_dataset_info(name)` Return the `DatasetInfo` of a dataset. Parameters: | Name | Type | Description | Default | | ------ | ----- | ----------------------------------- | ---------- | | `name` | `str` | The key of the dataset in REGISTRY. | *required* | Returns: | Type | Description | | ------------- | ---------------------------- | | `DatasetInfo` | The metadata of the dataset. | Raises: | Type | Description | | ------------ | ----------------------------------------------------------------------- | | `ValueError` | If name is not in the registry. The message lists every available name. | Example ```python info = get_dataset_info("iris") ``` ### `list_datasets()` Return the metadata of every available dataset. Returns: | Type | Description | | ------------------- | ------------------------------------------------- | | `list[DatasetInfo]` | One DatasetInfo for each dataset in the registry. | Example ```python datasets = list_datasets() print(datasets[0].name) # 'breast_cancer_wisconsin' ``` ### `load_dataset(name, *, split=True, test_size=0.2, validation_size=0.0, random_state=42, cache_dir=None)` Load a dataset by name. Parameters: | Name | Type | Description | Default | | ----------------- | ------- | --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | | `name` | `str` | The name of the dataset in REGISTRY, for example "iris". | *required* | | `split` | `bool` | If True, the default, the data is split into a training set and a test set. | `True` | | `test_size` | `float` | The fraction of the samples used for the test set. | `0.2` | | `validation_size` | `float` | The fraction of the samples used for the validation set. With 0, the default, there is no validation set. | `0.0` | | `random_state` | \`int | None\` | The random seed of the split, and of the generator for the synthetic datasets. | | `cache_dir` | \`str | None\` | A cache directory for the downloaded datasets, in place of the default one. | Returns: | Type | Description | | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `dict` | A dictionary with these keys: With split=True: "X_train", "y_train", "X_test", "y_test" and "metadata", and also "X_val" and "y_val" when validation_size is above 0. With split=False: "X", "y" and "metadata". The synthetic time-series datasets also have a "sequence" key, which holds the whole series. "metadata" is the DatasetInfo of the dataset. | Raises: | Type | Description | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------- | | `ValueError` | If name is not in the registry, if validation_size is not in \[0, 1), or if test_size + validation_size is 1 or more with split=True. | Example ```python data = load_dataset("iris") X_train, y_train = data["X_train"], data["y_train"] ``` ### `rc_params(theme='light')` Return the matplotlib settings of the library for one plot, as rcParams. Every figure that the library draws uses these settings: `DatasetReport.plot_overview()`, the benchmark curves and the plots in the worked examples. This is what makes them look like one set. Pass the result to `matplotlib.rc_context()` and draw as usual. The settings apply only inside that block, because the returned dictionary is a new copy and the global `matplotlib.rcParams` is never changed. The two themes differ only in what has to change for a dark page. `"dark"` makes the backgrounds of the figure, the axes and the legend transparent, and makes the text, the ticks and the spines white. The sizes, the spines, the grid and STYLE_COLORS are the same in both themes, so a figure saved with each theme differs only in its background and in the colour of its text and lines. Parameters: | Name | Type | Description | Default | | ------- | ----- | ------------------------------------------------------------------- | --------- | | `theme` | `str` | "light" or "dark". The case and any surrounding spaces are ignored. | `'light'` | Returns: | Type | Description | | ------ | ------------------------------------------------------------- | | `dict` | The rcParams to draw with, ready for matplotlib.rc_context(). | Raises: | Type | Description | | ------------ | --------------------------------------- | | `ValueError` | If theme is neither "light" nor "dark". | Example ```python import matplotlib import matplotlib.pyplot as plt from eigenkit.datasets import STYLE_COLORS, rc_params with matplotlib.rc_context(rc_params(theme="light")): fig, ax = plt.subplots(figsize=(7, 4)) ax.plot(history, color=STYLE_COLORS[0], label="train loss") ax.legend() fig.savefig("loss.png", bbox_inches="tight") ``` ## `eigenkit.optimization` Combinatorial optimisation with the core of the library. Write a problem as an `Ising` or `QUBO` model, then solve it with `QAOA`, the Quantum Approximate Optimization Algorithm. The ansatz uses the gates of the core, the angles are optimised with `minimize_expectation`, and the solution is read from the distribution of bitstrings of the optimised state. The package uses only NumPy and SciPy, needs no PyTorch, and works with any backend. Example ```python from eigenkit.optimization import max_cut, QAOA problem = max_cut([(0, 1), (1, 2), (2, 0), (2, 3)]) result = QAOA(layers=2).solve(problem) print(result.best_bitstring, result.best_energy) ``` ### `Ising` An Ising problem, which minimises `Σ h_i s_i + Σ_{i = P(target)`. ### `PercevalBackend` The photonic executor that runs on a local Perceval simulator. `compile(spec)` returns a `PercevalCompiled`. Parameters: | Name | Type | Description | Default | | -------------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | | `backend_name` | `str` | A Perceval backend that computes the full output distribution. "SLOS" (the default) and "Naive" are exact, while "MPS" and "NaiveApprox" approximate the distribution, and so the gradients too. Backends that only sample, such as "CliffordClifford2017", are rejected, because run returns probabilities, not samples. | `'SLOS'` | Raises: | Type | Description | | ------------ | ------------------------------------------- | | `ValueError` | If backend_name is unknown or only samples. | #### `compile(spec)` Wrap `spec` in a reusable handle. Perceval runs at each call of the handle. Parameters: | Name | Type | Description | Default | | ------ | --------------------- | ----------------------- | ---------- | | `spec` | `PhotonicCircuitSpec` | The circuit to compile. | *required* | Returns: | Type | Description | | ------------------ | ------------------------------------- | | `PercevalCompiled` | The PercevalCompiled handle for spec. | ### `PercevalCompiled` A reusable handle to a compiled photonic circuit, cheap to call many times. It is the linear-optics counterpart of `CompiledCircuit`. `PercevalBackend.compile` builds it. Parameters: | Name | Type | Description | Default | | -------------- | --------------------- | -------------------------------------------------------------- | ---------- | | `spec` | `PhotonicCircuitSpec` | The circuit that this handle evaluates. | *required* | | `backend_name` | `str` | The Perceval backend, as for PercevalBackend. Default: "SLOS". | `'SLOS'` | Raises: | Type | Description | | ------------ | ------------------------------------------- | | `ValueError` | If backend_name is unknown or only samples. | #### `expectation(weights, inputs, observable)` Return the expectation value of a `PhotonicObservable`, as a single number. The observable is diagonal in the Fock basis. Parameters: | Name | Type | Description | Default | | ------------ | -------------------- | ------------------------------------ | ---------- | | `weights` | `ndarray` | The 1-D weight vector. | *required* | | `inputs` | `ndarray` | The 1-D input vector. | *required* | | `observable` | `PhotonicObservable` | A PhotonNumber or a FockProbability. | *required* | Raises: | Type | Description | | ------------ | -------------------------------------------------------- | | `ValueError` | If observable can never match the output of the circuit. | #### `jacobian(weights, inputs, observable, *, method='parameter_shift', need_weight_grad=True, need_input_grad=True)` Return `(d/d weights, d/d inputs)` for an observable. The observable is diagonal in the Fock basis. Only one sample is accepted (1-D weights and inputs). Parameters: | Name | Type | Description | Default | | ------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | | `weights` | `ndarray` | The 1-D weight vector. | *required* | | `inputs` | `ndarray` | The 1-D input vector. | *required* | | `observable` | `PhotonicObservable` | A PhotonNumber or a FockProbability. | *required* | | `method` | `str` | "parameter_shift" for the exact photonic parameter-shift rule (in gradients.py), or "finite_difference" for central finite differences, the reference method. Default: "parameter_shift". | `'parameter_shift'` | | `need_weight_grad` | `bool` | If True, compute the gradient with respect to the weights. If False, it is skipped, and None is returned in its place. | `True` | | `need_input_grad` | `bool` | If True, compute the gradient with respect to the inputs. If False, it is skipped, and None is returned in its place. | `True` | Raises: | Type | Description | | ------------ | ----------------------------------------- | | `ValueError` | If method is neither of these two values. | #### `run(weights, inputs)` Return the output distribution of photon numbers. The distribution is a dictionary `{photon_counts_tuple: probability}`. Fock states with probability zero may be left out, because Perceval's `prob_distribution` is sparse. Parameters: | Name | Type | Description | Default | | --------- | --------- | ------------------------------------------------ | ---------- | | `weights` | `ndarray` | The 1-D weight vector, of length spec.n_weights. | *required* | | `inputs` | `ndarray` | The 1-D input vector, of length spec.n_inputs. | *required* | Raises: | Type | Description | | ------------ | ----------------------------------------- | | `ValueError` | If weights or inputs has the wrong shape. | ### `Permutation` A reordering of the modes, which is a network of mode swaps given as one component. `perm[i]` is the output mode to which input mode `i` is sent. The component acts on the modes `0` to `len(perm) - 1`, so `perm` must reorder exactly these modes. ### `PhaseShifter` A phase shift `exp(i·phi·n̂)` on a single mode. `n̂` is the photon-number operator of the mode. `phi` is a `Parameter` expression or a plain number, and a plain number is stored as a `Constant`. ### `PhotonNumber` The expected number of photons in a mode: ` = `. ### `PhotonicCircuitSpec` A description of a linear-optical circuit that does not depend on the backend. It is the photonic counterpart of `core.ir.CircuitSpec`. It cannot be changed after it is built, and it can be hashed and pickled. `input_state` is the Fock state sent into the circuit, as the number of photons in each mode. For example, `(1, 0, 1, 0)` means one photon in mode `0` and one in mode `2`. What is measured (the full distribution of photon numbers, the expected photon number of each mode, or a function of the detection pattern) is left to the executor for now. A `PhotonicMeasurement` type is planned. Every component must act on the modes `0` to `n_modes - 1`, and `input_state` must hold non-negative integers. Both are checked when the object is built, so a wrong circuit fails at that point, and not inside Perceval. #### `n_photons` The total number of photons sent into the circuit. This number sets the cost of the simulation in Fock space. ### `QuandelaCloudBackend` An executor that runs on a Quandela Cloud platform. The platform is a real quantum processing unit (QPU) or a cloud simulator. Parameters: | Name | Type | Description | Default | | ---------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | `platform` | `str` | The name of a Quandela Cloud platform, for example "qpu:ascella" for hardware or "sim:slos" for a cloud simulator. Default: "sim:slos". | `'sim:slos'` | | `token` | \`str | None\` | The access token. Without it, the token comes from $QUANDELA_TOKEN or $PERCEVAL_TOKEN, then from the token that Perceval has saved. Default: None. | | `shots` | `int` | The number of shots sampled in each run. Default: 10000. | `10000` | | `url` | \`str | None\` | The web address of the Quandela Cloud service, if it is not Perceval's default. Default: None. | | `min_detected_photons` | \`int | None\` | Keep only the shots that detect at least this many photons (post-selection). The default is the number of photons in the input state. | | `poll_timeout_seconds` | `float` | How long, in seconds, run and result() wait for a job before they raise TimeoutError. Default: 3600. | `3600` | #### `compile(spec)` Tie `spec` to this platform, offline, with no network connection or token. Parameters: | Name | Type | Description | Default | | ------ | --------------------- | ----------------------- | ---------- | | `spec` | `PhotonicCircuitSpec` | The circuit to compile. | *required* | Returns: | Type | Description | | ---------------- | ----------------------------------- | | `RemoteCompiled` | The RemoteCompiled handle for spec. | ## `eigenkit.qml` Quantum machine learning: kernels, support vector machines, the estimator base classes compatible with scikit-learn, and saving and loading models. This package needs scikit-learn, NumPy and the core of the library, but not PyTorch: the quantum kernels run on the NumPy interface. The hybrid estimators based on PyTorch (`HQNC`, `HQNR` and others) are in `eigenkit.torch.estimators`, and they build on the base classes exported here. ### `BaseQuantumClassifier` Bases: `ClassifierMixin`, `BaseQuantumEstimator` Base class for quantum classifiers. Its `score()` returns the accuracy. #### `predict_proba(X)` Predict the class probabilities. Subclasses should return the probabilities in the order of `classes_`, with shape `(n_samples, n_classes)`. The default implementation raises a `NotImplementedError`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | Returns: | Type | Description | | --------- | ----------------------------------------------------------------------------------------------- | | `ndarray` | The probabilities, of shape (n_samples, n_classes), with the columns in the order of classes\_. | ### `BaseQuantumEstimator` Bases: `BaseEstimator`, `ABC` Base class, compatible with scikit-learn, for all quantum algorithms. Parameters: | Name | Type | Description | Default | | -------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `provider` | `ProviderSpec` | The backend. It can be a registered name ("pennylane", "qiskit", "braket", or any name passed to register()), a configuration instance (for example PennyLaneConfig(device_name=...)), or a backend instance. | `'numpy'` | | `random_state` | \`int | None\` | The seed for reproducibility, applied inside fit() and load(). What it seeds depends on the estimator. The PyTorch estimators isolate the random number generator of PyTorch, so seeding one of them does not reseed the random stream of the caller. The kernel estimators are deterministic, and they take it only so that all the estimators have the same interface. | #### `fit(X, y)` Fit the estimator on `X` and `y`. Parameters: | Name | Type | Description | Default | | ---- | --------- | -------------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | | `y` | `ndarray` | One target for each sample, of shape (n_samples,). | *required* | Returns: | Type | Description | | ------------------------ | --------------------------------------------- | | `'BaseQuantumEstimator'` | This estimator, so that calls can be chained. | #### `load(path, *, provider=None)` Load a fitted model from a `.eigenkit` bundle. Load only bundles that you trust. A bundle can contain joblib pickles, and unpickling runs code, as with `torch.load` or `joblib.load`. Parameters: | Name | Type | Description | Default | | ---------- | -------------- | ----------- | --------------------------------------- | | `path` | \`str | Path\` | The path of the bundle. | | `provider` | \`ProviderSpec | None\` | A provider that replaces the saved one. | Returns: | Type | Description | | ------------------------ | --------------------- | | `'BaseQuantumEstimator'` | The loaded estimator. | Raises: | Type | Description | | ----------- | ---------------------------------------------------------------------------------------------------------------------------- | | `TypeError` | If the method is called on a subclass and the bundle holds a different class. BaseQuantumEstimator.load() accepts any class. | #### `predict(X)` Predict a value for every sample in `X`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | Returns: | Type | Description | | --------- | ------------------------------- | | `ndarray` | One prediction for each sample. | #### `save(path)` Save a fitted model to a `.eigenkit` bundle. The bundles of models that hold scikit-learn objects (`QSVC`, `QSVR`, the reservoir estimators and the time-series estimators) contain joblib pickles, so loading one can run code. Share them only with people who trust you. Parameters: | Name | Type | Description | Default | | ------ | ----- | ----------- | ------------------------------------------------------------------------- | | `path` | \`str | Path\` | The path of the bundle. The suffix .eigenkit is added when it is missing. | ### `BaseQuantumRegressor` Bases: `RegressorMixin`, `BaseQuantumEstimator` Base class for quantum regressors. Its `score()` returns `R²`. ### `KernelAlignmentResult` Outcome of align_kernel(). Attributes: | Name | Type | Description | | --------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `weights` | `ndarray` | The trained weights. | | `kernel` | `QuantumKernel` | A copy of the input kernel with weights, ready for QSVC(kernel=...). | | `loss` | `float` | The loss at weights. | | `initial_loss` | `float` | The loss at the starting weights. | | `loss_history` | `tuple[float, ...]` | The loss after every iteration of Simultaneous Perturbation Stochastic Approximation (SPSA), or at every evaluation for a SciPy optimiser. With SPSA, weights is the point with the lowest loss seen, including the starting weights. | | `n_evaluations` | `int` | The number of kernel matrices computed. | | `success` | `bool` | Whether the loss is lower than at the starting point. | ### `KernelJob` A kernel matrix submitted to a backend, to be collected later. QuantumKernel.submit_kernel_matrix() returns it. The handle records which pair each submitted circuit measured, so `result()` returns the assembled matrix instead of a flat list of probabilities. `save()` writes this information together with the job id, so that the matrix can be collected from a new session with QuantumKernel.retrieve_kernel_matrix(). #### `n_circuits` The number of circuits that the job runs, one for each measured entry. #### `job_id()` Return the identifier of the job on the backend. #### `result()` Wait until the job finishes, and return the kernel matrix. Returns: | Type | Description | | --------- | ---------------------------------------------------------------------------------- | | `ndarray` | The kernel matrix, of shape (n_x, n_x) for a Gram matrix and (n_x, n_y) otherwise. | #### `save(path)` Write the job id and the layout of the pairs to a JSON file. The setting of `psd_projection` is written too. Parameters: | Name | Type | Description | Default | | ------ | ----- | ----------- | --------------------------------------------------------------------------------------------- | | `path` | \`str | Path\` | Where to write the file. Its suffix is replaced by .json, or .json is added when it has none. | Returns: | Type | Description | | ------ | ----------------------------- | | `Path` | The path of the written file. | #### `status()` Return the current status of the job, as a string from the backend. ### `QSVC` Bases: `BaseQuantumClassifier` Quantum support vector classifier (QSVC). It computes a quantum kernel matrix from a fidelity circuit, and then uses `SVC(kernel="precomputed")` from scikit-learn. Parameters: | Name | Type | Description | Default | | -------------- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `feature_map` | `str` | The feature map. It is "Angle" (angle encoding into a product state) or "AngleCNOT" (angle encoding with ladders of controlled-NOT (CNOT) gates). Default: "Angle". It is ignored when kernel is given. | `'Angle'` | | `reps` | `int` | The number of repetitions of the feature map. Default: 1. It is ignored when kernel is given. | `1` | | `C` | `float` | The regularisation parameter of the support vector machine. Default: 1.0. | `1.0` | | `probability` | `bool` | If True, the model gives Platt-scaled probabilities. If False (the default), predict_proba is unavailable. The calibration is a sigmoid fitted by cross-validation on the training kernel, as in CalibratedClassifierCV(SVC(), ensemble=False). The predictions still come from the plain SVC. | `False` | | `provider` | `ProviderSpec` | The backend. It takes the same options as Circuit(provider=...). Any backend with a statevector or a zero_state_probability method works, including Qiskit Runtime and IQM hardware. Default: "numpy". It is ignored when kernel is given. | `'numpy'` | | `random_state` | \`int | None\` | The seed. | | `kernel` | \`QuantumKernel | None\` | A QuantumKernel to use instead of building one from feature_map, reps and provider. It can be a custom kernel, a trained kernel or a hardware kernel. Default: None. | | `scale` | `float` | The bandwidth. The inputs are multiplied by it before they reach the kernel, so it can be tuned with GridSearchCV. Default: 1.0. | `1.0` | Example ```python import numpy as np from eigenkit import QSVC, Encoding, QuantumKernel, Variational rng = np.random.default_rng(0) X_train, X_test = rng.uniform(0, np.pi, (8, 4)), rng.uniform(0, np.pi, (3, 4)) y_train = np.array([0, 1] * 4) model = QSVC(feature_map="AngleCNOT", reps=2) model.fit(X_train, y_train) predictions = model.predict(X_test) # Any kernel works, including custom and trained ones. my_map = [Encoding(in_features=4), Variational(depth=1)] model = QSVC(kernel=QuantumKernel(n_features=4, feature_map=my_map)) ``` #### `__sklearn_is_fitted__()` Return True when the wrapped scikit-learn `SVC` has been fitted. #### `decision_function(X)` Return the signed distance of each sample to the separating hyperplane. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | Returns: | Type | Description | | --------- | ------------------------------------------------------------------------------------- | | `ndarray` | An array of shape (n_samples,) for two classes, and (n_samples, n_classes) otherwise. | #### `fit(X, y)` Fit the classifier on training data. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | | `y` | `ndarray` | The class labels, of shape (n_samples,). | *required* | Returns: | Type | Description | | ------ | --------------------------------------------- | | `QSVC` | This estimator, so that calls can be chained. | #### `predict(X)` Predict the class labels of `X`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | Returns: | Type | Description | | --------- | ------------------------------- | | `ndarray` | One prediction for each sample. | #### `predict_proba(X)` Predict the probability of each class. It requires `probability=True`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | Returns: | Type | Description | | --------- | ----------------------------------------------------------------------------------------------- | | `ndarray` | The probabilities, of shape (n_samples, n_classes), with the columns in the order of classes\_. | Raises: | Type | Description | | ------------ | ------------------------------------------------ | | `ValueError` | If the model was created with probability=False. | ### `QSVR` Bases: `BaseQuantumRegressor` Quantum support vector regressor (QSVR). It is the regression counterpart of QSVC. It uses `SVR(kernel="precomputed")` from scikit-learn on the quantum kernel matrix. Parameters: | Name | Type | Description | Default | | -------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | | `feature_map` | `str` | The feature map. It is "Angle" (angle encoding into a product state) or "AngleCNOT" (angle encoding with ladders of controlled-NOT (CNOT) gates). Default: "Angle". It is ignored when kernel is given. | `'Angle'` | | `reps` | `int` | The number of repetitions of the feature map. Default: 1. It is ignored when kernel is given. | `1` | | `C` | `float` | The regularisation parameter of the support vector regressor. Default: 1.0. | `1.0` | | `epsilon` | `float` | The width of the epsilon-insensitive tube. Default: 0.1. | `0.1` | | `provider` | `ProviderSpec` | The backend. It takes the same options as Circuit(provider=...). Default: "numpy". It is ignored when kernel is given. | `'numpy'` | | `random_state` | \`int | None\` | The seed. | | `kernel` | \`QuantumKernel | None\` | A QuantumKernel to use instead of building one from feature_map, reps and provider. Default: None. | | `scale` | `float` | The bandwidth. The inputs are multiplied by it before they reach the kernel. Default: 1.0. | `1.0` | Example ```python import numpy as np from eigenkit import QSVR rng = np.random.default_rng(0) X_train, X_test = rng.uniform(0, np.pi, (8, 2)), rng.uniform(0, np.pi, (3, 2)) y_train = np.sin(X_train).sum(axis=1) model = QSVR(feature_map="Angle", reps=1) model.fit(X_train, y_train) predictions = model.predict(X_test) ``` #### `__sklearn_is_fitted__()` Return True when the wrapped scikit-learn `SVR` has been fitted. #### `fit(X, y)` Fit the regressor on training data. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | | `y` | `ndarray` | The float targets, of shape (n_samples,). | *required* | Returns: | Type | Description | | ------ | --------------------------------------------- | | `QSVR` | This estimator, so that calls can be chained. | #### `predict(X)` Predict continuous targets for `X`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | Returns: | Type | Description | | --------- | ------------------------------- | | `ndarray` | One prediction for each sample. | ### `QuantumKernel` Quantum kernel matrix `K[i, j] = fidelity(U(x_i), U(x_j))`. Parameters: | Name | Type | Description | Default | | ---------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- | | `n_features` | `int` | The number of features. For the built-in maps, it is also the number of qubits. | *required* | | `feature_map` | `FeatureMapLike` | The feature map. It is "Angle", "AngleCNOT", or a custom forward map U(x; w). A custom map is a CircuitSpec, a CircuitBuilder, a block, a list of blocks, or a callable emit(builder, x, w) that writes the map onto a CircuitBuilder, given the input references x and the weight references w. A custom map must read exactly n_features inputs. Default: "AngleCNOT". | `'AngleCNOT'` | | `reps` | `int` | The number of repetitions of a built-in feature map. reps=1 uses only the RX encoding, and reps>=2 adds repetitions of the RZ encoding (with ladders of controlled-NOT (CNOT) gates for "AngleCNOT"). It is ignored for a custom map. Default: 2. | `2` | | `provider` | `ProviderSpec` | The backend. It takes the same options as Circuit(provider=...). Default: "numpy", which needs no extra. | `'numpy'` | | `weights` | \`Sequence[float] | ndarray | None\` | | `scale` | `float` | The bandwidth. Every input is multiplied by it before it reaches the circuit, so scale sets how far apart two samples are in Hilbert space. Default: 1.0. | `1.0` | | `n_weights` | \`int | None\` | The number of weights that a callable feature_map reserves. For any other map, it is read from the circuit and, if given, checked. | | `n_qubits` | \`int | None\` | The number of qubits of a callable feature_map. Default: n_features. | | `psd_projection` | `bool` | If True, a sampled Gram matrix is projected onto the nearest positive semidefinite matrix. It only affects backends that use shots. Default: False. | `False` | Example ```python import numpy as np from eigenkit import QuantumKernel rng = np.random.default_rng(0) X_train, X_test = rng.uniform(0, np.pi, (8, 4)), rng.uniform(0, np.pi, (3, 4)) qk = QuantumKernel(n_features=4, feature_map="AngleCNOT", reps=2) K_train = qk.compute_kernel_matrix(X_train) K_test = qk.compute_kernel_matrix(X_test, X_train) # A custom feature map with a trainable weight. def zz_map(qc, x, w): for q in range(len(x)): qc.ry(w[0], q) qc.h(q) qc.rz(x[q], q) qc.pauli_exponential([("Z", 0), ("Z", 1)], x[0]) trainable = QuantumKernel(n_features=2, feature_map=zz_map, n_weights=1) K = trainable.compute_kernel_matrix(X_train[:, :2], weights=[0.3]) ``` #### `circuit` The compiled inversion circuit. A backend with shots runs it for each kernel entry. It is compiled when it is first accessed. #### `feature_map_circuit` The compiled forward feature map `U(x; w)`. It is compiled when it is first accessed. #### `feature_map_spec` The forward feature map `U(x; w)`, as a `CircuitSpec`. #### `kernel_spec` The inversion circuit `U(x_1; w) U(x_2; w)^dagger`, as a `CircuitSpec`. It has `2 * n_features` inputs. #### `n_qubits` The number of qubits of the feature-map circuit. #### `n_weights` The number of trainable weights in the feature map. #### `__sklearn_clone__()` Return a new copy of the kernel, not compiled yet, for `sklearn.base.clone`. `GridSearchCV`, `cross_val_score` and `Pipeline` use it. The copy has the same feature map, provider, weights, scale and `psd_projection`. #### `compute_kernel_matrix(X, Y=None, *, weights=None)` Return the kernel matrix between the rows of `X` and the rows of `Y`. Parameters: | Name | Type | Description | Default | | --------- | --------- | ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | | `X` | `ndarray` | The samples, of shape (n_X, n_features). | *required* | | `Y` | \`ndarray | None\` | The second set of samples, of shape (n_Y, n_features). If None, the method returns K(X, X). | | `weights` | \`ndarray | None\` | The feature-map weights for this evaluation. By default, they are the kernel's own weights, or zeros if it has none. | Returns: | Type | Description | | --------- | ----------------------------- | | `ndarray` | An array of shape (n_X, n_Y). | #### `draw(*, inversion=False, decimals=3)` Return a text drawing of the feature map, or of the inversion circuit. Parameters: | Name | Type | Description | Default | | ----------- | ------ | ---------------------------------------------------- | ------- | | `inversion` | `bool` | If True, draw U(x_1) U(x_2)^dagger instead of U(x). | `False` | | `decimals` | `int` | The number of significant digits of constant angles. | `3` | Returns: | Type | Description | | ----- | ------------------------------------------ | | `str` | The drawing, as a string of several lines. | #### `draw_mpl(*, inversion=False, **kwargs)` Draw the feature map, or the inversion circuit, with Matplotlib. The inversion circuit is drawn with a measurement on every qubit, and the forward map without one. This method needs the `viz` extra. Parameters: | Name | Type | Description | Default | | ----------- | ------ | ---------------------------------------------------------- | ------- | | `inversion` | `bool` | If True, draw U(x_1) U(x_2)^dagger instead of U(x). | `False` | | `**kwargs` | `Any` | Arguments passed to draw_mpl(), for example style="eigen". | `{}` | Returns: | Type | Description | | ----------------------- | ------------------------------------------------------ | | `'tuple[Figure, Axes]'` | The Matplotlib figure and axes, as the pair (fig, ax). | #### `estimate_cost(X, Y=None)` Return what computing `K(X, Y)` would need on the backend of this kernel. Nothing is submitted. On a sampling backend, the inversion circuit is compiled (and transpiled, for hardware) to report its size. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------- | ------------------------------------------------------------------------------------------- | | `X` | `ndarray` | The samples, of shape (n_X, n_features). | *required* | | `Y` | \`ndarray | None\` | The second set of samples, of shape (n_Y, n_features), or None for the Gram matrix K(X, X). | Returns: | Type | Description | | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `dict` | A dictionary with the keys method ("statevector" or "inversion"), n_circuits, n_jobs, shots and total_shots. With the "statevector" method, it also has n_state_evolutions. When the backend reports them, it also has the transpiled two-qubit gate count n_2q_gates, the depth and the backend_name. | #### `from_json_dict(data, provider=None)` Rebuild a kernel written by `to_json_dict()`. Parameters: | Name | Type | Description | Default | | ---------- | -------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | `data` | `dict` | The stored description. | *required* | | `provider` | \`ProviderSpec | None\` | A provider that replaces the stored one. It is needed when the stored provider held a secret, or to move a saved model onto another backend. | Returns: | Type | Description | | ----------------- | ------------------- | | `'QuantumKernel'` | The rebuilt kernel. | #### `kernel_value(x1, x2, *, weights=None)` Return the kernel value of one pair `(x1, x2)`, from the inversion circuit. #### `retrieve_kernel_matrix(job, n_x=None, n_y=None)` Reconnect to a kernel job submitted earlier, possibly in another session. Parameters: | Name | Type | Description | Default | | ----- | ----- | ----------- | ------------------------------------------------------------------------------------ | | `job` | \`str | Path\` | The JSON file written by KernelJob.save(), or a job id on its own. | | `n_x` | \`int | None\` | The number of rows of the matrix. It is required with a job id on its own. | | `n_y` | \`int | None\` | The number of columns, for a rectangular matrix. Leave it as None for a Gram matrix. | Returns: | Type | Description | | ----------- | ---------------------------------------------------- | | `KernelJob` | A KernelJob. Call .result() on it to get the matrix. | #### `submit_kernel_matrix(X, Y=None, *, weights=None)` Submit `K(X, Y)` as one asynchronous job, and return its handle. This is for the queues of a quantum processing unit (QPU), which can take minutes or hours. Print the job id of the handle or call its `save()`, and collect the matrix later with `job.result()` or, from a new session, with `retrieve_kernel_matrix()`. Parameters: | Name | Type | Description | Default | | --------- | --------- | ---------------------------------------- | ----------------------------------------------------------------------------------- | | `X` | `ndarray` | The samples, of shape (n_X, n_features). | *required* | | `Y` | \`ndarray | None\` | The second set of samples, of shape (n_Y, n_features), or None for the Gram matrix. | | `weights` | \`ndarray | None\` | The feature-map weights. By default, they are the kernel's own. | Returns: | Type | Description | | ----------- | ------------ | | `KernelJob` | A KernelJob. | Raises: | Type | Description | | --------------------- | ----------------------------------------------------------------------------------------------------------------------- | | `NotImplementedError` | On backends without asynchronous submission, which includes every local simulator. Use compute_kernel_matrix() on them. | #### `to_json_dict()` Return a JSON description of the kernel, for `.eigenkit` bundles. A custom feature map is stored as its `CircuitSpec`. Secret fields of the provider, such as access tokens, are never written. Returns: | Type | Description | | ------ | --------------------------------- | | `dict` | The description, as a dictionary. | #### `with_weights(weights)` Return a copy of this kernel with different trainable weights. Parameters: | Name | Type | Description | Default | | --------- | --------- | --------------------------------------- | ---------- | | `weights` | `ndarray` | The new weights, of shape (n_weights,). | *required* | Returns: | Type | Description | | ----------------- | ---------------------------------------- | | `'QuantumKernel'` | A new kernel, which is not compiled yet. | ### `QuantumReservoirClassifier` Bases: `_ReservoirMixin`, `BaseQuantumClassifier` Quantum reservoir classifier: a fixed random reservoir and a logistic readout. It uses the same reservoir as QuantumReservoirRegressor. The readout is a scikit-learn `LogisticRegression` on the features of each timestep. `y` holds integer or label targets, of shape `(n_sequences, n_timesteps)`. Parameters: | Name | Type | Description | Default | | -------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | `blocks` | `list` | The architecture of the reservoir circuit, a list of blocks that is assembled once. Its n_inputs must match the policy: F for the policies that carry the state, and window * F for state_policy="stateless", where F is the number of features per timestep. | *required* | | `state_policy` | `str` | "carry" (the unitary of the circuit carries the state from one step to the next), "stateless" (each step starts from | 0…0⟩ and encodes a sliding window of inputs), or "leaky" (the carried state is mixed with a reset state and renormalised). | | `window` | `int` | The length of the sliding window, for state_policy="stateless". | `1` | | `leak` | `float` | The mixing coefficient alpha, for state_policy="leaky". | `1.0` | | `features` | `FeaturesSpec` | The features read from the reservoir. It is "z" (the value of ⟨Z⟩ on each qubit, the default), "probabilities", "amplitudes", a Pauli or Hamiltonian string (for example "0.5 Z0 X1 - 0.3 Y0"), or a list of such strings, to read out any observables. | `'z'` | | `C` | `float` | The inverse regularisation strength of the logistic readout, passed to scikit-learn. Smaller values regularise more. This is the only argument that the regressor does not have: there, the readout is a ridge regression controlled by ridge. | `1.0` | | `washout` | `int` | The number of timesteps dropped from the fit of the readout. They are not dropped from the predictions. | `0` | | `weight_scale` | `float` | The standard deviation of the sampled reservoir weights. | `1.0` | | `provider` | `ProviderSpec` | The backend, which must give access to the statevector. Default: "numpy". | `'numpy'` | | `random_state` | \`int | None\` | The seed of the fixed reservoir weights. | #### `__sklearn_is_fitted__()` Return True when the readout has been fitted. #### `fit(X, y)` Fit the estimator on `X` and `y`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------------------------------------------------- | ---------- | | `X` | `ndarray` | The sequences, of shape (n_sequences, n_timesteps, n_features). A 2-D array is rejected. | *required* | | `y` | `ndarray` | One target for each timestep, of shape (n_sequences, n_timesteps). | *required* | Returns: | Type | Description | | ------------------------------ | --------------------------------------------- | | `'QuantumReservoirClassifier'` | This estimator, so that calls can be chained. | #### `predict(X)` Predict a label for every timestep of every sequence in `X`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------------------------------------------------- | ---------- | | `X` | `ndarray` | The sequences, of shape (n_sequences, n_timesteps, n_features). A 2-D array is rejected. | *required* | Returns: | Type | Description | | --------- | ---------------------------------------------------------- | | `ndarray` | The predicted labels, of shape (n_sequences, n_timesteps). | #### `predict_proba(X)` Predict the class probabilities for every timestep of every sequence in `X`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------------------------------------------------- | ---------- | | `X` | `ndarray` | The sequences, of shape (n_sequences, n_timesteps, n_features). A 2-D array is rejected. | *required* | Returns: | Type | Description | | --------- | ---------------------------------------------------------------------------------------------------------------- | | `ndarray` | The probabilities, of shape (n_sequences, n_timesteps, n_classes), with the last axis in the order of classes\_. | #### `score(X, y)` Score the estimator on `X` against `y`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------------------------------------------------- | ---------- | | `X` | `ndarray` | The sequences, of shape (n_sequences, n_timesteps, n_features). A 2-D array is rejected. | *required* | | `y` | `ndarray` | One target for each timestep, of shape (n_sequences, n_timesteps). | *required* | Returns: | Type | Description | | ------- | ----------------------------------------------------------------------- | | `float` | The accuracy of the predictions, over every timestep of every sequence. | ### `QuantumReservoirRegressor` Bases: `_ReservoirMixin`, `BaseQuantumRegressor` Quantum reservoir regressor: a fixed random reservoir and a ridge readout. Parameters: | Name | Type | Description | Default | | -------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | `blocks` | `list` | The architecture of the reservoir circuit, a list of blocks that is assembled once. Its n_inputs must match the policy: F for the policies that carry the state, and window * F for state_policy="stateless", where F is the number of features per timestep. | *required* | | `state_policy` | `str` | "carry" (the unitary of the circuit carries the state from one step to the next), "stateless" (each step starts from | 0…0⟩ and encodes a sliding window of inputs), or "leaky" (the carried state is mixed with a reset state and renormalised). | | `window` | `int` | The length of the sliding window, for state_policy="stateless". | `1` | | `leak` | `float` | The mixing coefficient alpha, for state_policy="leaky". | `1.0` | | `features` | `FeaturesSpec` | The features read from the reservoir. It is "z" (the value of ⟨Z⟩ on each qubit, the default), "probabilities", "amplitudes", a Pauli or Hamiltonian string (for example "0.5 Z0 X1 - 0.3 Y0"), or a list of such strings, to read out any observables. | `'z'` | | `ridge` | `float` | The strength of the L2 penalty of the linear readout, which is solved in closed form. | `1e-06` | | `washout` | `int` | The number of timesteps dropped from the fit of the readout. They are not dropped from the predictions. | `0` | | `weight_scale` | `float` | The standard deviation of the sampled reservoir weights. | `1.0` | | `provider` | `ProviderSpec` | The backend, which must give access to the statevector. Default: "numpy". | `'numpy'` | | `random_state` | \`int | None\` | The seed of the fixed reservoir weights. | #### `__sklearn_is_fitted__()` Return True when the readout has been fitted. #### `fit(X, y)` Fit the estimator on `X` and `y`. Parameters: | Name | Type | Description | Default | | ---- | --------- | -------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `X` | `ndarray` | The sequences, of shape (n_sequences, n_timesteps, n_features). A 2-D array is rejected. | *required* | | `y` | `ndarray` | One target for each timestep, of shape (n_sequences, n_timesteps), or (n_sequences, n_timesteps, n_outputs) for several outputs. | *required* | Returns: | Type | Description | | ----------------------------- | --------------------------------------------- | | `'QuantumReservoirRegressor'` | This estimator, so that calls can be chained. | #### `predict(X)` Predict a value for every timestep of every sequence in `X`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------------------------------------------------- | ---------- | | `X` | `ndarray` | The sequences, of shape (n_sequences, n_timesteps, n_features). A 2-D array is rejected. | *required* | Returns: | Type | Description | | --------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `ndarray` | The predictions, of shape (n_sequences, n_timesteps), or (n_sequences, n_timesteps, n_outputs) when the targets had several outputs. | #### `score(X, y)` Score the estimator on `X` against `y`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------------------------------------------------- | ---------- | | `X` | `ndarray` | The sequences, of shape (n_sequences, n_timesteps, n_features). A 2-D array is rejected. | *required* | | `y` | `ndarray` | One target for each timestep, of shape (n_sequences, n_timesteps). | *required* | Returns: | Type | Description | | ------- | ---------------------------------------------------------------------------------------------- | | `float` | The coefficient of determination R² of the predictions, over every timestep of every sequence. | ### `align_kernel(kernel, X, y, *, loss='svc', optimizer='spsa', maxiter=50, C=1.0, initial_weights=None, learning_rate=None, perturbation=0.2, random_state=0, callback=None)` Train the weights of a kernel so that it fits the labels. Parameters: | Name | Type | Description | Default | | ----------------- | ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `kernel` | `QuantumKernel` | A QuantumKernel whose feature map has trainable weights. | *required* | | `X` | `ndarray` | The training samples, of shape (n, n_features). | *required* | | `y` | `ndarray` | The training labels, of shape (n,). | *required* | | `loss` | \`str | Callable\[[ndarray, ndarray], float\]\` | The loss to minimise. It is "svc" (the dual objective of the support vector machine, which is the loss of quantum kernel alignment), "kta" (minus the kernel-target alignment), or a callable loss(K, y) -> float. | | `optimizer` | \`str | Callable\` | The optimiser. It can be "spsa", the default, which selects Simultaneous Perturbation Stochastic Approximation (SPSA). It can also be the name of a scipy.optimize.minimize method such as "COBYLA", or a callable optimizer(fun, x0) -> x. | | `maxiter` | `int` | The number of SPSA iterations, or the maxiter option of SciPy. | `50` | | `C` | `float` | The regularisation parameter of the support vector machine, for the "svc" loss. | `1.0` | | `initial_weights` | \`ndarray | None\` | The starting point. By default, it is the kernel's own weights, or zeros if the kernel has none. | | `learning_rate` | \`float | None\` | The initial step size of SPSA. None calibrates it so that the first step moves the weights by about 2π / 10. The "svc" loss grows with the number of samples, so a fixed step that suits one dataset size can be far too large for another. | | `perturbation` | `float` | The initial perturbation size of SPSA. The default, 0.2, is the value that qiskit-algorithms uses for its calibration. Much smaller values give estimates of the gradient that are too local to cross a flat region of the loss. | `0.2` | | `random_state` | \`int | None\` | The seed for the perturbation directions of SPSA. | | `callback` | \`Callable\[[int, ndarray, float], None\] | None\` | A function called as callback(iteration, weights, loss) after every SPSA iteration or SciPy evaluation. | Returns: | Type | Description | | ----------------------- | ------------------------ | | `KernelAlignmentResult` | A KernelAlignmentResult. | Raises: | Type | Description | | ------------ | ---------------------------------------------------------------------- | | `ValueError` | If the kernel has no trainable weights, or if loss is an unknown name. | Example ```python import numpy as np from eigenkit import QSVC, QuantumKernel, align_kernel def my_map(qc, x, w): for q in range(len(x)): qc.ry(w[0], q) qc.rz(x[q], q) rng = np.random.default_rng(0) X_train = rng.uniform(0, np.pi, (8, 2)) y_train = np.array([0, 1] * 4) kernel = QuantumKernel(n_features=2, feature_map=my_map, n_weights=1) result = align_kernel(kernel, X_train, y_train, maxiter=20) model = QSVC(kernel=result.kernel).fit(X_train, y_train) ``` ### `get_estimator_class(name)` Return the estimator class registered under `name`. Parameters: | Name | Type | Description | Default | | ------ | ----- | ----------------------------------------------------------- | ---------- | | `name` | `str` | The class name, as stored in the metadata.json of a bundle. | *required* | Returns: | Type | Description | | ------ | --------------------- | | `type` | The registered class. | Raises: | Type | Description | | ------------ | ----------------------------------------------------------------------------------------------------------------- | | `ValueError` | If no class is registered under name. The module that defines the class must be imported before load() is called. | ### `kernel_target_alignment(K, y)` Return the kernel-target alignment `_F / (||K||_F ||Y||_F)`. `Y_ij = +1` when `y_i == y_j` and `-1` otherwise. For binary labels in `{-1, +1}`, this is `y y^T`. Parameters: | Name | Type | Description | Default | | ---- | --------- | --------------------------------- | ---------- | | `K` | `ndarray` | The Gram matrix, of shape (n, n). | *required* | | `y` | `ndarray` | The labels, of shape (n,). | *required* | Returns: | Type | Description | | ------- | --------------------------------------------------------------------------------------- | | `float` | A value in [-1, 1]. A higher value means that the kernel already separates the classes. | ### `load_model(path, *, provider=None, model=None)` Load any saved model. The class of the model is read from `metadata.json`. Load only bundles that you trust. A bundle can contain joblib pickles, and unpickling runs code, as with `torch.load` or `joblib.load`. Parameters: | Name | Type | Description | Default | | ---------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------- | | `path` | \`str | Path\` | The path to the .eigenkit bundle. | | `provider` | \`ProviderSpec | None\` | A provider that replaces the saved one. It is passed to the load() method of the estimator. | | `model` | `Any` | The nn.Module architecture, for checkpoints of GenericRegressor and GenericClassifier. It is ignored for the other estimators. | `None` | Returns: | Type | Description | | ---------------------- | ------------------------------------------------------------------- | | `BaseQuantumEstimator` | The loaded estimator, an instance of the class named in the bundle. | ### `project_psd(K)` Return the nearest positive semidefinite matrix to a symmetric `K`. Shot noise can make a measured Gram matrix slightly indefinite, and a support vector machine solver handles such a matrix poorly. Setting the negative eigenvalues to zero gives the nearest positive semidefinite matrix in the Frobenius norm. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------- | ---------- | | `K` | `ndarray` | A square, symmetric kernel matrix. | *required* | Returns: | Type | Description | | --------- | ---------------------------------------- | | `ndarray` | The projected matrix, of the same shape. | ### `register_estimator(cls)` Register an estimator class under its `__name__`. It is used as a class decorator. Parameters: | Name | Type | Description | Default | | ----- | ------ | -------------------------------- | ---------- | | `cls` | `type` | The estimator class to register. | *required* | Returns: | Type | Description | | ------ | -------------------------- | | `type` | The same class, unchanged. | ### `svc_dual_loss(K, y, C=1.0)` Return the optimal dual objective of a support vector machine (SVM). The SVM is fitted on a precomputed kernel. This is the `"svc"` loss. Parameters: | Name | Type | Description | Default | | ---- | --------- | -------------------------------------------------------------- | ---------- | | `K` | `ndarray` | The training Gram matrix, of shape (n, n). | *required* | | `y` | `ndarray` | The labels, of shape (n,). They must have exactly two classes. | *required* | | `C` | `float` | The regularisation parameter of the SVM. | `1.0` | Returns: | Type | Description | | ------- | ------------- | | `float` | The value sum | Raises: | Type | Description | | ------------ | ------------------------------------------------------------------------ | | `ValueError` | If y does not have exactly two classes. Use loss="kta" for more classes. | ## `eigenkit.reservoir` Quantum reservoir computing (QRC). A circuit with fixed weights, usually random, is driven over input sequences. The quantum state is carried from one timestep to the next, and classical features are read out from it. The reservoir circuit is never trained; only a classical readout is. The driver, the policies, the feature extractors and the ridge-regression readout use only NumPy. Like `optimization` and `chemistry`, this package belongs to the core of the library, so it needs neither PyTorch nor a backend package. Three policies cover the common variants of QRC: - Stateless starts every step from `|0…0⟩` and encodes a sliding window of inputs. All timesteps run in parallel, in one batched evaluation. - Carry carries the state from one step to the next with the circuit's unitary: `ψ_t = U(x_t) ψ_{t-1}`. - LeakyCarry mixes the carried state with a reset state, and renormalises the result. Example ```python from eigenkit import Circuit from eigenkit.reservoir import ( run_reservoir, Carry, expectation_features, LinearReadout, ) states = run_reservoir(circuit, Carry(), sequences, weights) # (B, T, 2**n) feats = expectation_features(states, "Z0 + Z1 + 0.5 X0 X1") # (B, T, 1) readout = LinearReadout(ridge=1e-4).fit(feats.reshape(B * T, -1), targets) ``` ### `Carry` Policy that applies the circuit to the carried state: `ψ_t = U(x_t) ψ_{t-1}`. The quantum state is the memory of the reservoir, and the first step starts from `|0…0⟩`. This policy is equivalent to `LeakyCarry(alpha=1.0)`. #### `advance(x_t, evolved, state)` Build the state for the next timestep from the result of this step. Parameters: | Name | Type | Description | Default | | --------- | ---------------- | -------------------------------------------- | ---------- | | `x_t` | `ndarray` | The input of this timestep, of shape (B, F). | *required* | | `evolved` | `ndarray` | The statevector that the circuit produced. | *required* | | `state` | `ReservoirState` | The state that went into this step. | *required* | Returns: | Type | Description | | ---------------- | -------------------------------- | | `ReservoirState` | The state for the next timestep. | #### `initial(batch)` Return the state carried into the first timestep. Parameters: | Name | Type | Description | Default | | ------- | ----- | --------------------------------------------- | ---------- | | `batch` | `int` | The number of sequences that run in parallel. | *required* | Returns: | Type | Description | | ---------------- | --------------------------- | | `ReservoirState` | The initial ReservoirState. | #### `inject(x_t, state)` Turn the input of this timestep into the arguments of the circuit call. Parameters: | Name | Type | Description | Default | | ------- | ---------------- | ---------------------------------------------- | ---------- | | `x_t` | `ndarray` | The input of this timestep, of shape (B, F). | *required* | | `state` | `ReservoirState` | The state carried over from the previous step. | *required* | Returns: | Name | Type | Description | | --------------- | --------- | ----------------------------------------------- | | `initial_state` | \`ndarray | None\` | | `gate_inputs` | `ndarray` | The values to pass to the gates of the circuit. | ### `LeakyCarry` Policy that mixes the carried state with a reset state and renormalises it. It approximates a leaky, dissipative reservoir with pure states: `ψ_t = normalise(α U(x_t) ψ_{t-1} + (1 − α) reset)`. `reset` defaults to `|0…0⟩` and should be a normalised statevector; this is not checked. With `alpha=1`, the policy is exactly Carry. This is not a true dissipative channel, which would need density matrices that the pure-state backends do not provide. It is an approximation that uses pure states only. Attributes: | Name | Type | Description | | ------- | --------- | ------------------------------------------------------------------------------------------------------------------------------ | | `alpha` | `float` | The mixing weight of the carried state, in [0, 1]. Lower values forget faster, and 1 keeps everything, which is exactly Carry. | | `reset` | \`ndarray | None\` | #### `advance(x_t, evolved, state)` Build the state for the next timestep from the result of this step. Parameters: | Name | Type | Description | Default | | --------- | ---------------- | -------------------------------------------- | ---------- | | `x_t` | `ndarray` | The input of this timestep, of shape (B, F). | *required* | | `evolved` | `ndarray` | The statevector that the circuit produced. | *required* | | `state` | `ReservoirState` | The state that went into this step. | *required* | Returns: | Type | Description | | ---------------- | -------------------------------- | | `ReservoirState` | The state for the next timestep. | #### `initial(batch)` Return the state carried into the first timestep. Parameters: | Name | Type | Description | Default | | ------- | ----- | --------------------------------------------- | ---------- | | `batch` | `int` | The number of sequences that run in parallel. | *required* | Returns: | Type | Description | | ---------------- | --------------------------- | | `ReservoirState` | The initial ReservoirState. | #### `inject(x_t, state)` Turn the input of this timestep into the arguments of the circuit call. Parameters: | Name | Type | Description | Default | | ------- | ---------------- | ---------------------------------------------- | ---------- | | `x_t` | `ndarray` | The input of this timestep, of shape (B, F). | *required* | | `state` | `ReservoirState` | The state carried over from the previous step. | *required* | Returns: | Name | Type | Description | | --------------- | --------- | ----------------------------------------------- | | `initial_state` | \`ndarray | None\` | | `gate_inputs` | `ndarray` | The values to pass to the gates of the circuit. | ### `LinearReadout` Ridge regression in closed form, with a bias term. It solves `W = (XᵀX + λI)⁻¹ XᵀY`, and the bias column is not regularised. `Y` can have one output or several. `ridge` is the strength `λ` of the L2 penalty, which adds `λ‖W‖²` to the squared error of the fit. A small positive value also keeps the solution numerically stable when the reservoir features are collinear. Parameters: | Name | Type | Description | Default | | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | | `ridge` | `float` | The strength λ of the L2 penalty. Raise it when the readout overfits or the feature matrix is nearly singular, and lower it when the readout underfits. The default is 1e-06. | `1e-06` | #### `fit(X, y)` Fit the readout to `X` and `y`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ----------------------------------------------------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | | `y` | `ndarray` | The targets, of shape (n_samples,) for one output, or (n_samples, n_outputs) for several. | *required* | Returns: | Type | Description | | ----------------- | ------------------------------------------- | | `'LinearReadout'` | This readout, so that calls can be chained. | #### `predict(X)` Predict the target for every sample in `X`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | Returns: | Type | Description | | --------- | ----------------------------------------------------------------------------------------------------- | | `ndarray` | One prediction per sample, with one column per output when the readout was fitted on several outputs. | ### `ReservoirPolicy` Bases: `Protocol` A reservoir policy that carries state from one timestep to the next. Parallel policies, which carry no state, do not implement this protocol. They provide `expand_inputs(sequences)`, which returns the inputs of every timestep with shape `(B, T, n_inputs)`, and the driver evaluates all timesteps at once. See Stateless. #### `advance(x_t, evolved, state)` Build the state for the next timestep from the result of this step. Parameters: | Name | Type | Description | Default | | --------- | ---------------- | -------------------------------------------- | ---------- | | `x_t` | `ndarray` | The input of this timestep, of shape (B, F). | *required* | | `evolved` | `ndarray` | The statevector that the circuit produced. | *required* | | `state` | `ReservoirState` | The state that went into this step. | *required* | Returns: | Type | Description | | ---------------- | -------------------------------- | | `ReservoirState` | The state for the next timestep. | #### `initial(batch)` Return the state carried into the first timestep. Parameters: | Name | Type | Description | Default | | ------- | ----- | --------------------------------------------- | ---------- | | `batch` | `int` | The number of sequences that run in parallel. | *required* | Returns: | Type | Description | | ---------------- | --------------------------- | | `ReservoirState` | The initial ReservoirState. | #### `inject(x_t, state)` Turn the input of this timestep into the arguments of the circuit call. Parameters: | Name | Type | Description | Default | | ------- | ---------------- | ---------------------------------------------- | ---------- | | `x_t` | `ndarray` | The input of this timestep, of shape (B, F). | *required* | | `state` | `ReservoirState` | The state carried over from the previous step. | *required* | Returns: | Name | Type | Description | | --------------- | --------- | ----------------------------------------------- | | `initial_state` | \`ndarray | None\` | | `gate_inputs` | `ndarray` | The values to pass to the gates of the circuit. | ### `ReservoirState` The state that sequential policies carry from one timestep to the next. `vector` is the `(B, 2**n)` statevector to inject at the next step, and `None` means that the step starts from `|0…0⟩`. `history` holds the recent raw inputs, of shape `(B, k, F)`, for a sequential policy that encodes a window of inputs. Attributes: | Name | Type | Description | | --------- | --------- | ----------- | | `vector` | \`ndarray | None\` | | `history` | \`ndarray | None\` | ### `Stateless` Parallel policy that carries no state. Every step starts from `|0…0⟩`, and the last `window` inputs are encoded as rotation angles of the circuit's gates. The memory is purely classical (the window), and the quantum state carries nothing from one step to the next. Every timestep is therefore independent, and the driver evaluates all of them in one batched run. With `window=1`, the policy has no memory: it encodes only the current input. The circuit's `n_inputs` must equal `window * F`, where `F` is the number of features per step, because each step passes the whole flattened window to the encoding gates. Attributes: | Name | Type | Description | | -------- | ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `window` | `int` | The number of consecutive inputs to encode at each step. 1 means no memory: only the current input is encoded. Whatever the value, the circuit's n_inputs must equal window * F. The default is 1. | #### `expand_inputs(sequences)` Return the gate inputs of every timestep, with shape `(B, T, window * F)`. Step `t` encodes the inputs `t - window + 1` to `t`. The first `window - 1` steps are padded with zeros at the front. No quantum state is involved, so this is only a reshaping of the input sequence. Parameters: | Name | Type | Description | Default | | ----------- | --------- | ------------------------------------------ | ---------- | | `sequences` | `ndarray` | The whole input batch, of shape (B, T, F). | *required* | Returns: | Type | Description | | --------- | ---------------------------------------------------------------- | | `ndarray` | The gate inputs for every timestep, of shape (B, T, window * F). | ### `amplitude_features(states)` The raw statevector as real features: its real and imaginary parts, concatenated. `states` is a complex array of shape `(B, T, 2**n)`, and the result is a real array of shape `(B, T, 2 * 2**n)`. It exposes the full state, which only a simulator can provide; it cannot be measured on hardware. Parameters: | Name | Type | Description | Default | | -------- | --------- | -------------------------------------------------------- | ---------- | | `states` | `ndarray` | The trajectory of statevectors, of shape (B, T, 2\*\*n). | *required* | Returns: | Type | Description | | --------- | -------------------------------------------------------------- | | `ndarray` | The real and imaginary parts, concatenated, for each timestep. | ### `expectation_features(states, observables)` Expectation values `⟨ψ|H_k|ψ⟩` of each observable, over the whole trajectory. `states` is a complex array of shape `(B, T, 2**n)`, and the result is a real array of shape `(B, T, n_obs)`. `observables` can be an `Observable`, a `PauliExpr` or a Hamiltonian string, or a sequence of these. These are the same types that the rest of the library uses, so measuring an arbitrary Hamiltonian, as QRC experiments often require, needs no manual handling of Pauli terms. A single Hamiltonian string is one observable, and gives one feature. Parameters: | Name | Type | Description | Default | | ------------- | ---------------------- | -------------------------------------------------------- | ---------- | | `states` | `ndarray` | The trajectory of statevectors, of shape (B, T, 2\*\*n). | *required* | | `observables` | `Sequence[Observable]` | The observables to measure. | *required* | Returns: | Type | Description | | --------- | -------------------------------------------- | | `ndarray` | One feature per observable and per timestep. | ### `pauli_z_features(states)` The expectation value `⟨Z_q⟩` on each qubit, the usual default readout in QRC. `states` has shape `(B, T, 2**n)`, and the result has shape `(B, T, n)`. The values are computed with expectation_features(), so both functions share the same tested measurement code. Parameters: | Name | Type | Description | Default | | -------- | --------- | -------------------------------------------------------- | ---------- | | `states` | `ndarray` | The trajectory of statevectors, of shape (B, T, 2\*\*n). | *required* | Returns: | Type | Description | | --------- | --------------------------------------- | | `ndarray` | One feature per qubit and per timestep. | ### `probability_features(states)` The measurement distribution over all `2**n` basis states, `|ψ|²`. `states` is a complex array of shape `(B, T, 2**n)`, and the result is a real array of the same shape. Parameters: | Name | Type | Description | Default | | -------- | --------- | -------------------------------------------------------- | ---------- | | `states` | `ndarray` | The trajectory of statevectors, of shape (B, T, 2\*\*n). | *required* | Returns: | Type | Description | | --------- | ---------------------------------------------------------- | | `ndarray` | The distribution over the basis states, for each timestep. | ### `run_reservoir(circuit, policy, sequences, weights)` Evolve `circuit` over `sequences` under `policy`, with frozen `weights`. The result is the trajectory of statevectors, a complex array of shape `(B, T, 2**n)`. `sequences` has shape `(B, T, F)`: `B` independent series, `T` timesteps and `F` features per step. `weights` is the fixed 1-D parameter vector of the reservoir, shared by the whole batch. Features are extracted separately from the returned trajectory (see the feature extractors), so the same run can produce expectation, probability or amplitude features without evolving the circuit again. Parameters: | Name | Type | Description | Default | | ----------- | --------- | --------------------------------------------------- | ---------- | | `circuit` | `Circuit` | The reservoir circuit, run with frozen weights. | *required* | | `policy` | `Policy` | How the state passes from one timestep to the next. | *required* | | `sequences` | `ndarray` | The inputs, of shape (B, T, F). | *required* | | `weights` | `ndarray` | The frozen weights of the reservoir. | *required* | Returns: | Type | Description | | --------- | -------------------------------------------------------- | | `ndarray` | The trajectory of statevectors, of shape (B, T, 2\*\*n). | ## `eigenkit.torch` The PyTorch package: the PyTorch interface and the `nn.Module` circuit. Importing this package registers the `"torch"` differentiation interface with the core, so that `Circuit(..., interface="torch")` works, and it makes the `nn.Module` circuit, `TorchCircuit`, available. This package is an optional layer on top of the NumPy core, and everything in it needs PyTorch. ### `TorchCircuit` Bases: `Module` A quantum layer that stores trainable weights and runs on any registered backend. It runs through the PyTorch interface, so that PyTorch can compute the gradients of its output. Parameters: | Name | Type | Description | Default | | ------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | | `blocks` | `_BlocksOrSpec` | The circuit. It can be a list of blocks to assemble, a CircuitSpec that is already assembled, or a CircuitBuilder. A builder is built when the layer is created, so gates added to it afterwards are not part of this layer. | *required* | | `observables` | `ObservableLike` | What to measure. None measures Z on every qubit and returns one value per qubit. A single Observable or PauliExpr returns a single value, and a list of them returns one value for each. Default: None. | `None` | | `provider` | `ProviderSpec` | The backend that runs the circuit. It can be a registered name, a configuration instance, or a provider object. Any backend works. A backend that computes gradients itself avoids the fallback to the parameter-shift rule. Default: "numpy". | `'numpy'` | | `init` | \`Tensor | None\` | The initial weights, of shape (n_weights,). They are random when init is not given. Default: None. | #### `n_qubits` The number of qubits of the circuit in `spec`. #### `out_features` The number of observables, or `1` for a single observable. #### `provider` The backend that this circuit was compiled for. It is the same as Circuit.provider, which turns the `provider=` argument (a registered name, a configuration or a backend object) into one `Provider`. The registered name is `provider.name`. To run the same circuit on another backend, use `with_provider()`, which returns a new instance and leaves this one unchanged. #### `spec` The circuit that this layer runs. It is the CircuitSpec assembled from `blocks`, or the one that was passed in already assembled. #### `draw(*, decimals=3)` Return a text drawing of the circuit, over several lines. The drawing does not depend on the provider: it is built from the `CircuitSpec`, so it is the same whatever the active backend is. Print the returned string directly. Example ```python from eigenkit import Circuit, Encoding, Variational blocks = [Encoding(in_features=2), Variational(depth=1)] circuit = Circuit(blocks=blocks, interface="torch") print(circuit.draw()) # q_0: ──RY(x_0)──RY(w_0)──●── # │ # q_1: ──RY(x_1)──RY(w_1)──X── ``` Parameters: | Name | Type | Description | Default | | ---------- | ----- | ---------------------------------------------------------------------------- | ------- | | `decimals` | `int` | The number of significant digits shown for a numeric gate angle. Default: 3. | `3` | Returns: | Type | Description | | ----- | ------------------------------------------------------- | | `str` | The drawing, as a single string that contains newlines. | #### `draw_mpl(**kwargs)` Draw the circuit as a matplotlib figure, and return `(fig, ax)`. The picture does not depend on the provider, because it is built from the `CircuitSpec` and the observables. matplotlib is an optional dependency. Install it with `eigenkit[viz]`. The keyword arguments are passed to draw_mpl(). #### `dry_run_info()` Describe the prepared circuit in a dictionary, without submitting it. It is useful to check the size of the circuit, the gate counts after transpilation, the number of shots and the number of circuits that each call runs, before you submit to a cloud simulator or a quantum processing unit (QPU). It does not estimate a price. It is available only on the backends that provide it. Currently, these are Braket (on its local simulator as well as on cloud devices), Qiskit Runtime and IQM. Other backends raise a `NotImplementedError`. #### `forward(x=None)` Run the layer. Parameters: | Name | Type | Description | Default | | ---- | -------- | ----------------------------------------------- | ------- | | `x` | `Tensor` | The input batch, of shape (batch, in_features). | `None` | Returns: | Type | Description | | -------- | --------------------------------------- | | `Tensor` | The output of the layer for that batch. | #### `reset_parameters()` Draw new circuit weights from the current PyTorch random number generator. Every `nn.Module` that PyTorch provides defines this method. The estimators call `model.apply(reset_parameters)` after setting the seed, so that `random_state` controls the whole initialisation. Without this method, the estimators would skip the layer: two fits with different values of `random_state` would give different classical weights but exactly the same quantum weights, and a user who restarts with new seeds to escape a barren plateau would change only the part of the model that is not the circuit. #### `sample(x=None)` Return the raw measurement histogram for the current weights and input. The histogram maps each bitstring to its number of shots. In each bitstring, qubit `0` is the most significant bit. It is the second step of the workflow in which you train on a simulator and then run on hardware: you train the layer, and then you read what the device measured. It takes one sample, and its result is not differentiable, because the counts are integers that PyTorch does not record for automatic differentiation. Parameters: | Name | Type | Description | Default | | ---- | -------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `x` | \`Tensor | None\` | The input features of one sample, as a 1-D tensor. None works only for a circuit that takes no inputs, and it raises a ValueError for any other circuit. Default: None. | Returns: | Name | Type | Description | | -------- | ---------------- | ------------------------------------------------ | | `counts` | `dict[str, int]` | The number of shots for each measured bitstring. | Raises: | Type | Description | | --------------------- | ------------------------------- | | `NotImplementedError` | On backends that do not sample. | | `ValueError` | If x is 2-D. | #### `submit(x=None)` Submit a job without waiting for it, and return a `JobHandle`. Only the backends that provide submission support it (Braket, Qiskit Runtime and IQM). Parameters: | Name | Type | Description | Default | | ---- | -------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `x` | \`Tensor | None\` | One 1-D input sample. None works only for a circuit that takes no inputs, and it raises a ValueError for any other circuit. Default: None. | Returns: | Name | Type | Description | | -------- | ----------- | ------------------------------- | | `handle` | `JobHandle` | A handle for the submitted job. | #### `with_provider(provider, *, copy_weights=True, init=None)` Return a new `TorchCircuit` for another `provider`. The new circuit has the same `spec` and the same `observables` argument as this instance, and it is compiled for the given `provider`. `provider` accepts the same forms as in `__init__()`: a registered name (a `str`), a configuration object with a `make_backend()` method (for example `PennyLaneConfig`), or a backend instance. The gradients follow the `gradient_method` of the new backend. The forward values should match across simulators up to numerical tolerance, but not exactly. Parameters: | Name | Type | Description | Default | | -------------- | -------------- | ------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | `provider` | `ProviderSpec` | The backend for the new circuit. | *required* | | `copy_weights` | `bool` | If True, the new circuit starts from a copy of self.weights. It is ignored when init is given. Default: True. | `True` | | `init` | \`Tensor | None\` | An optional weight tensor of shape (spec.n_weights,). When it is given, it takes precedence over copy_weights. Default: None. | Returns: | Type | Description | | ---------------- | --------------------- | | `'TorchCircuit'` | The new TorchCircuit. | #### `zero_state_probability(x=None)` Return the probability of measuring the all-zeros state. It uses the current weights and the given input. It is useful for fidelity kernels and swap tests. It takes one sample, and its result is not differentiable. The core reads the amplitude through the backend's access to the state and returns a plain `float`, so PyTorch does not record it for automatic differentiation. A batch raises an error, because a single number cannot hold a batch axis. For a batch, call this method once for each row, or use `Circuit.zero_state_probabilities`. Parameters: | Name | Type | Description | Default | | ---- | -------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `x` | \`Tensor | None\` | The input features of one sample, as a 1-D tensor. None works only for a circuit that takes no inputs, and it raises a ValueError for any other circuit. Default: None. | Returns: | Type | Description | | ------- | --------------------------------------------- | | `float` | The all-zeros probability, a float in [0, 1]. | Raises: | Type | Description | | ------------ | ------------ | | `ValueError` | If x is 2-D. | ## `eigenkit.algorithms.hhl` ### `HHL` Bases: `BaseQuantumSolver` Quantum linear solver based on the algorithm of Harrow, Hassidim and Lloyd (HHL). Solves `Ax = b` for any invertible square matrix `A`. If `A` is Hermitian, it is used directly, even when it is not positive semidefinite. Any other matrix is converted to the normal equations `A†Ax = A†b`, which have the same solution and a Hermitian, positive semidefinite system matrix. The cost of this conversion is that the condition number `κ` is squared. The circuit itself inverts the eigenvalues. It applies controlled `RY` rotations to the ancilla, with one fixed angle for each state of the clock register, so the reciprocal `1/λ` of each eigenvalue is encoded in the amplitudes without any classical division by the eigenvalues. The readout is different: it uses the simulator's full statevector and keeps only the part where the ancilla is in `|1⟩` and the clock register is in `|0⟩`. HHL produces the quantum state `|x⟩`, so reading a classical vector from it is only possible on a simulator. See HHLResult. Warning The backend must provide `statevector()` on its compiled circuit. The built-in numpy simulator, PennyLane `default.qubit` and `lightning.qubit`, and Qiskit Aer all do. A custom backend registered with register() must implement it, otherwise `solve()` raises `NotImplementedError`. **Scope and scaling.** HHL is efficient only when three conditions hold: `A` is a sum of a few short Pauli terms, `A` is well conditioned, and the state `|b⟩` can be prepared efficiently. - *Hamiltonian simulation.* `e^{iAt}` is Trotterised from the Pauli decomposition of `A`, so its cost follows the number of Pauli terms. This means sparsity in the Pauli basis, which is different from sparsity of the matrix. The module `algorithms/_hamiltonian_sim.py` explains what this means in practice. In short, a sparse matrix is not necessarily cheap to simulate here. - *State preparation.* `|b⟩` is prepared with AmplitudeEncoding, which is treated as an oracle of unit cost. Loading general amplitudes costs `O(N)` gates, so a real speedup assumes that `b` comes from a formula, from an earlier quantum routine, or from a quantum random-access memory (qRAM). - *Conditioning.* The condition number `κ` is never an input. It only bounds the achievable success probability and the size of the clock register. - *Inversion angles.* The gates are elementary (see `_uniformly_controlled_ry()`), so the circuit built in `"honest"` mode contains no unitary-matrix gate. However, the `2^n_l` angles, where `n_l` is the number of clock qubits, are listed one by one instead of being computed by the circuit. The classical description of the circuit therefore has size `Θ(2^n_l)`. The circuit can be built from elementary gates, but its description is not compact. Solvers for dense matrices, for example the one of Wossnig, Zhao and Prakash, reach a quadratic speedup when the spectral norm is bounded. They need a specialised memory architecture that this package does not provide. **Real and complex inputs.** For real `A` and `b`, the readout discards the imaginary part. The leading error of the Trotter eigenvectors enters the raw vector as a purely imaginary component, so discarding the imaginary part removes it. This matters at `trotter_order=1`, where it improves the convergence by one order: the measured error falls as `steps⁻²` for real inputs, and as `steps⁻¹` for a complex Hermitian `A` or a complex `b`, which keep that error. At `trotter_order=2`, the default, the first-order commutator already cancels by construction, so there is nothing left to remove, and real and complex inputs both converge as `steps⁻²`. This advantage was not designed. It depends on the test for real inputs in solve(), so a change to that test changes the convergence rate at order 1. Parameters: | Name | Type | Description | Default | | --------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `n_eigenvalue_qubits` | `int` | The number of qubits in the clock register. Eigenvalues are resolved into 2\*\*n_eigenvalue_qubits buckets. The value must be at least 1, and at least 2 in "honest" mode. More qubits give a finer resolution of the spectrum and a larger circuit. The default is 4. | `4` | | `t` | \`float | None\` | The evolution time for U = e^{iAt}. If given, it must be positive and finite. If None, it is chosen automatically: from the spectrum in "reference" mode, and from a Gershgorin bound in "honest" mode. The default is None. | | `epsilon` | `float` | The relative eigenvalue cutoff, in \[0, 1). For a Hermitian A, buckets whose estimated eigenvalue is below epsilon * λ_max are treated as zero. For the normal equations, the cutoff applies to the singular values instead, and it is implemented as epsilon² on the squared spectrum. If no eigenvalue is above the cutoff, solve() raises an error. The default is 0.01. | `0.01` | | `provider` | `ProviderSpec` | The backend to use: a registered name ("numpy", "pennylane", "qiskit" and so on), a config instance, or a backend object. It must provide statevector(). The default is "numpy". | `'numpy'` | | `mode` | `str` | How the circuit is built. This argument is keyword-only. "reference" (the default): the eigenvalues are computed classically with eigvalsh, e^{iAt} is a dense QubitUnitary computed with expm, and t is tuned to the known spectrum. This mode is fast and exact, but the O(N³) precomputation and the dense unitary mean that its cost does not follow the scaling of HHL. "honest": the circuit is built as it would be on hardware. No eigendecomposition sets any circuit parameter: t comes from a Gershgorin bound, the scaling constant C of the inversion comes from the resolution of the clock register (see \_honest_qpe_params()), and e^{iAt} is Trotterised into elementary gates. This mode requires n_eigenvalue_qubits >= 2. Input validation still runs an O(N³) rank check. This check makes the solver safer to use and is not part of the algorithm; see solve(). | `'reference'` | | `trotter_steps` | \`int | Literal['auto']\` | The number of Trotter steps for the base evolution time in "honest" mode. It is ignored in "reference" mode. With "auto", the default, the solver picks the smallest number of steps for which a computable bound on the Trotter drift ‖A_eff − A‖ stays below trotter_tolerance × C, which is below the eigenvalue resolution of the clock register. This matters because, with a fixed step size, the circuit is exact HHL for an effective matrix A_eff whose eigenvectors are rotated even when its eigenvalues are exact. That changes the direction of the answer while every check based on the spectrum reports success. An integer turns off the automatic choice, and the solver warns if the drift bound then exceeds C/2. Above that value, an extreme eigenvalue can be captured by the Nyquist bucket or wrap around the branch cut. The automatic choice is limited by a gate budget, and the solver says so when the limit is reached. The default is "auto". | | `trotter_order` | `int` | The Trotter order, 1 or 2. The default is 2. The symmetric product of order 2 is a palindrome, so the first-order commutator cancels, the error falls from O(τ) to O(τ²), and A_eff stays real for a real A. At order 1, A_eff picks up Y-type terms, and a real system returns a complex state. Order 1 is kept for cost comparisons. | `2` | | `trotter_tolerance` | `float` | The target for trotter_steps="auto". This argument is keyword-only. The drift bound must not exceed trotter_tolerance × C. The default, 0.25, keeps the drift at a quarter of a bucket, below the C/2 threshold at which the Nyquist bucket can capture an eigenvalue. At order 2, the cost grows as steps ∝ 1/√tolerance. | `_AUTO_TROTTER_TOLERANCE` | Raises: | Type | Description | | ------------ | ---------------------------------------------------------------------------------------------------------- | | `ValueError` | If n_eigenvalue_qubits is below 1, if t is not a positive finite number, or if epsilon is outside \[0, 1). | Example This example solves a `2 × 2` Hermitian system: ```python import numpy as np from eigenkit import HHL A = np.array([[2, 1], [1, 2]], dtype=float) b = np.array([1, 0], dtype=float) result = HHL(n_eigenvalue_qubits=6).solve(A, b) print(result.solution) print(result.residual) ``` This example solves the same system on a faster simulator: ```python from eigenkit import HHL, PennyLaneConfig solver = HHL( n_eigenvalue_qubits=6, provider=PennyLaneConfig(device_name="lightning.qubit"), ) result = solver.solve(A, b) ``` #### `solve(A, b)` Solve the linear system `Ax = b`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ----------------------------------------------------------------------------------------------------------------------- | ---------- | | `A` | `ndarray` | A real or complex square matrix, which must be invertible. A non-Hermitian matrix is converted to the normal equations. | *required* | | `b` | `ndarray` | The right-hand side, of length A.shape[0]. | *required* | Returns: | Type | Description | | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | `HHLResult` | An HHLResult. Its solution is read from the simulator's statevector and completed with a classical least-squares rescaling. See the notes on HHLResult. | Raises: | Type | Description | | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | `ValueError` | If A is not square or is singular, if b has the wrong length, is zero or is not finite, or if t causes aliasing in quantum phase estimation (QPE). | | `NotImplementedError` | If the backend does not implement statevector(), or if it returns something else, such as a density matrix. | Note Validation runs one `O(N³)` singular value decomposition (SVD) to reject a singular `A` and to compute a condition number for the warnings. No quantity derived from it reaches `t`, `C`, the rotation angles or the number of Trotter steps, and changing it would leave the circuit exactly the same. `mode="honest"` therefore keeps its defining property, but `solve()` itself still does `O(N³)` work. ### `HHLResult` Bases: `ValueEquality` Output of HHL.solve(). The result is read-only. Assigning to a field raises `FrozenInstanceError`, and writing into an array field raises `ValueError`. This prevents a mistyped `=` (instead of `==`) or an in-place edit from leaving a result whose scalar fields describe a different run from the arrays it holds. To get a writable copy of an array, use `np.array(result.field)`. Results compare by value, including their arrays. They are not hashable, because a result is a collection of numbers and not a key, and a correct hash of a float array is expensive to compute. Attributes: | Name | Type | Description | | --------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | `solution` | `ndarray` | The best-fit x with Ax ≈ b. It is float64 for real inputs, and complex128 when A or b has a non-zero imaginary part. | | `euclidean_norm` | `float` | The norm ‖solution‖ of the returned solution. It is computed after the classical least-squares rescaling, so it is not a norm that HHL itself measures. | | `residual` | `float` | The relative error ‖Ax − b‖ / ‖b‖, computed with the original A and b. | | `success_probability` | `float` | The probability that the ancilla is in | | `readout_probability` | `float` | The probability of the joint event from which the solution is read: the ancilla in | | `raw_solution` | \`ndarray | None\` | Note HHL produces the quantum state `|x⟩`, not a vector of numbers. The full array in this result is read from the simulator's statevector, which only a simulator can provide. On hardware, only expectation values can be measured, and extracting all `N` components of `|x⟩` costs `O(N)` measurements, which cancels the speedup. In practice, `|x⟩` is passed to another quantum routine, or used to estimate one observable. Recovering the sign of `1/λ` needs a separate protocol. All of this applies to both values of `mode`. Warning `solution` is completed by two classical steps that hardware cannot perform. For real inputs, the imaginary part is discarded. The magnitude and the sign then come from a least-squares projection, `x = raw · ⟨A·raw, b⟩ / ‖A·raw‖²`, which uses the full classical `A` and `b`. Only the direction of `raw` comes from the quantum computation. The rescaling can change `residual` by an order of magnitude, and for an indefinite `A` its sign correction is not reliable, because `⟨Ax, b⟩ ≥ 0` does not imply `⟨x, A⁻¹b⟩ ≥ 0`. #### `__post_init__()` Make the array fields read-only. Freezing the dataclass prevents a field from being reassigned, but it does not prevent writing into the array that the field holds, and a result that has been written into still reports the scalar values of the original run. The result therefore keeps its own copy, so the array it was built from can still be changed by the caller. To get a writable copy, use `np.array(result.solution)`. Both array fields are frozen. `raw_solution` is optional, and `freeze_arrays` would turn a `None` into a zero-dimensional object array, so it is only frozen when the run recorded one. ## `eigenkit.algorithms.iqae` ### `IQAE` Bases: `BaseQuantumSolver` Iterative Quantum Amplitude Estimation (IQAE). Estimates `a = |⟨ψ₁|A|0⟩|²` to an additive precision `epsilon`, with a failure probability of at most `alpha`. It uses the Grover operator `Q = A S₀ A† S_ψ₁`, built from the oracle `A` that you provide. The oracle can be given in two forms: - A **PennyLane callable** `oracle(wires)`, which applies the circuit `A` in place. The last wire (`wires[-1]`) must be the ancilla qubit: ```python def my_oracle(wires): qml.RY(theta, wires=wires[0]) qml.CNOT(wires=[wires[0], wires[-1]]) ``` The matrix of the circuit is extracted internally with `qml.matrix`, so this form requires `provider="pennylane"` and the `pennylane` extra. - A CircuitSpec, which works on any backend that supports the `QubitUnitary`, `X`, `MCZ` and `Z` gates. With a provider other than PennyLane, pass `shot_mode="synthetic"` or `shot_mode="auto"`, because `shot_mode="provider"` works only with PennyLane. Parameters: | Name | Type | Description | Default | | -------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | `epsilon` | `float` | The additive precision to reach, that is, the half-width of the final confidence interval for a. It must be in (0, π/8). | *required* | | `alpha` | `float` | The total failure probability. The algorithm applies a Bonferroni correction, so the confidence interval of each round is valid at level alpha / T, where T = ceil(log2(π / (8 * epsilon))) is the maximum number of rounds. It must be in (0, 1). | *required* | | `n_qubits` | `int` | The total number of qubits, including the ancilla. | *required* | | `provider` | `ProviderSpec` | The backend to use: a registered name ("numpy", "pennylane", "qiskit" and so on), a config instance, or a backend object. The default is "numpy", the built-in simulator. | `'numpy'` | | `n_shots` | `int` | The number of measurement shots per round. With shot_mode="synthetic", which is used on analytic backends, it sets the size of a seeded binomial draw on top of the exact probability of the ancilla. With shot_mode="provider" or "auto", it is the number of samples per round on the hardware or simulator, unless the provider config sets its own shots, which takes precedence. For example, a device configured for 50 shots samples 50 times, whatever the value of n_shots. To control sampling on those backends, set shots on the provider config. The default is 100. | `100` | | `shot_mode` | `ShotMode` | How the ancilla measurements are obtained. With "auto", the default, the provider's shot count is used when the backend config sets a finite shots, and synthetic binomial noise is used otherwise. "provider" always samples from the backend. "synthetic" always uses exact expectation values plus seeded binomial noise. | `'auto'` | | `min_ratio` | `float` | The minimum ratio K\_{i+1} / K_i between the Grover powers of consecutive rounds. It must be greater than 1. | `2.0` | | `random_state` | \`int | None\` | The seed of the random number generator used for the shot noise. An integer gives reproducible results, and None uses a non-deterministic seed. | Example ```python import numpy as np from eigenkit import CircuitBuilder from eigenkit.algorithms import IQAE # RY(pi/3) on the ancilla prepares an amplitude of sin^2(pi/6) = 0.25. qc = CircuitBuilder(n_qubits=2) qc.ry(np.pi / 3, qubit=1) oracle = qc.build() iqae = IQAE(epsilon=0.02, alpha=0.05, n_qubits=2, random_state=0) result = iqae.solve(oracle) print(result.estimation) # 0.246 print(result.confidence_interval) # (0.2407, 0.2513), which contains 0.25 print(result.n_oracle_calls) # 2600 ``` #### `solve(oracle)` Estimate the amplitude `a = |⟨ψ₁|A|0⟩|²`. Parameters: | Name | Type | Description | Default | | -------- | ---------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `oracle` | \`Callable | CircuitSpec\` | Either a PennyLane callable oracle(wires) that builds the state-preparation circuit A, or a CircuitSpec produced by CircuitBuilder. In both cases, the last qubit is treated as the ancilla. | Returns: | Type | Description | | ------------ | --------------------------------------------------------------------------------------- | | `IQAEResult` | An IQAEResult with the point estimate, the confidence interval and the resource counts. | Raises: | Type | Description | | ------------ | ------------------------------------------------------------------------------------------------- | | `ValueError` | If the selected provider does not support the gates that IQAE needs (QubitUnitary, X, MCZ and Z). | ### `IQAEResult` Bases: `ValueEquality` Outcome of a completed Iterative Quantum Amplitude Estimation (IQAE) run. The result is read-only. Assigning to a field raises `FrozenInstanceError`, so a mistyped `=` (instead of `==`) cannot change a result. This type holds no arrays, so nothing else needs protecting. Results compare by value. They are not hashable, so that all result types behave in the same way: the types that hold arrays cannot be hashed both correctly and efficiently. Attributes: | Name | Type | Description | | --------------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `estimation` | `float` | The point estimate of the amplitude a. | | `confidence_interval` | `tuple[float, float]` | The (lower, upper) Clopper-Pearson interval at the requested confidence level 1 - alpha. | | `n_rounds` | `int` | The number of rounds that the run executed. It can also be read as n_iterations, the name that the other result types use. | | `n_oracle_calls` | `int` | The total number of oracle calls over all rounds. | | `success` | `bool` | Whether the interval reached the requested precision. This is the same test that ends the loop over rounds, and the one reported by the warning that the run did not converge. False means that the run stopped at max_rounds with an interval still wider than 2 * epsilon. | #### `n_iterations` The number of rounds, under the name that the other results use. `n_rounds` is the name that IQAE uses, and it stays available. This property lets one loop read the number of iterations from any result. Returns: | Type | Description | | ----- | ------------------------------------------- | | `int` | The number of rounds that the run executed. | ## `eigenkit.algorithms.portfolio_risk` ### `PortfolioRiskEstimator` Bases: `BaseQuantumEstimator` Quantum estimator of portfolio risk. It estimates the Value at Risk (VaR) and the Conditional Value at Risk (CVaR) of a portfolio. The estimator can use IQAE to estimate the tail probability on a quantum circuit. The oracle for IQAE is built from the fitted return distribution with amplitude encoding. Note This is not a standard scikit-learn regressor. The risk at a confidence level comes from `estimate()` and `var_at()`, which take confidence levels and not feature vectors, and the estimator has no meaningful scikit-learn `score()`. It inherits only from BaseQuantumEstimator. Not financial advice The estimates come from a model fitted to the price history you pass in, and they carry no guarantee. They are not investment advice. Parameters: | Name | Type | Description | Default | | -------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- | | `n_qubits` | `int` | The total number of qubits, including the ancilla. The loss distribution is discretised into 2^(n_qubits - 1) bins. The minimum is 2: one data qubit and the ancilla. | `6` | | `epsilon` | `float` | The additive precision of the IQAE estimate of the tail probability. | `0.01` | | `alpha` | `float` | The failure probability of IQAE. The confidence level is 1 − alpha. | `0.05` | | `horizon_days` | `int` | The investment horizon, in trading days, used to scale daily log-returns to the target horizon. The default is 1, which gives daily risk measures. | `1` | | `provider` | `ProviderSpec` | The backend that IQAE uses: a registered name ("pennylane", "qiskit", "braket" and so on), a config instance, or a backend object. The default is "numpy". Note The Grover operator built internally uses the adjoint of an AmplitudeEncoding block, so the provider must support the adjoint of a state preparation and provide a statevector. The built-in "numpy" simulator and PennyLane both do, and their results agree to the last digit. A backend that does not raises an error when the operator is built. Calling IQAE directly with a CircuitSpec based on QubitUnitary avoids this requirement. | `'numpy'` | | `n_shots` | `int` | The number of shots per IQAE round. The default is 100. | `100` | | `random_state` | \`int | None\` | The seed that makes the results reproducible. | | `n_bisection_steps` | `int` | The number of binary-search steps used to locate the VaR with quantum estimates of the tail probability. With 0, the default, the VaR and CVaR are computed classically from the fitted bins. A positive value calls \_quantum_var_bisection(), which refines the threshold by running IQAE at each step of the bisection. | `0` | | `measure_tail_probability` | `bool` | When True, the default, estimate() runs IQAE once after the VaR and CVaR are computed, and reports the quantum estimate of the tail in tail_probability and confidence_interval. Set it to False to skip that final IQAE call when only the classical VaR and CVaR are needed and n_bisection_steps=0. | `True` | | `distribution` | `str` | The model of the loss distribution that fit() uses. "gaussian" (the default) fits the mean μ and the standard deviation σ of the scaled log-returns, and integrates the Gaussian cumulative distribution function (CDF) over each bin. The distribution is truncated at ±6σ. A VaR beyond that point is capped at the limit, and a RuntimeWarning is issued. "empirical" places each historical return in the nearest bin of equal width, without assuming a parametric form. "t" fits a Student-t distribution by maximum-likelihood estimation, and integrates its CDF over each bin. It has heavier tails than the Gaussian. | `'gaussian'` | Example ```python import numpy as np from eigenkit.algorithms import PortfolioRiskEstimator rng = np.random.default_rng(0) returns = rng.normal(0.0005, 0.02, size=(250, 3)) prices = 100 * np.exp(np.cumsum(returns, axis=0)) estimator = PortfolioRiskEstimator( n_qubits=5, provider="numpy", random_state=0 ) estimator.fit(prices, weights=np.array([0.5, 0.3, 0.2])) risk = estimator.estimate(confidence_level=0.95) print(risk.var) # 0.0237: at most a 2.37% loss on 95% of days print(risk.cvar) # 0.0262: the average loss on the worst 5% print(risk.tail_probability) # 0.0659: the IQAE estimate of the tail mass ``` #### `distribution_params` Summary statistics of the fitted return distribution. The value is `None` if `fit()` has not been called. Otherwise it always contains `"mu"` (the mean) and `"sigma"` (the standard deviation) of the scaled log-returns, whatever the `distribution` model. For `"t"`, the degrees of freedom are not stored here, because they are an intermediate result of the maximum-likelihood fit and not part of the state of the estimator. #### `is_fitted` Whether `fit()` has completed successfully. #### `estimate(confidence_level=0.95, *, _seed_seq=None)` Estimate the VaR and CVaR at `confidence_level`. The VaR and CVaR come from the discretised loss distribution. When `n_bisection_steps > 0`, the VaR is refined by a bisection that uses IQAE. Otherwise, the classical quantiles of the bins are used. Unless `measure_tail_probability=False`, a final IQAE run estimates `P(loss ≥ VaR)`, for checking and diagnostics. Parameters: | Name | Type | Description | Default | | ------------------ | -------------- | ---------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `confidence_level` | `float` | The probability level of the risk measures, for example 0.95 for the 95 % VaR. It must be in (0, 1). | `0.95` | | `_seed_seq` | \`SeedSequence | None\` | A private override of the random seed, used by var_at() so that the shot noise of the IQAE runs for different confidence levels is independent. The default is None, which derives the seed from random_state. | Returns: | Type | Description | | --------------------- | ---------------------- | | `PortfolioRiskResult` | A PortfolioRiskResult. | Raises: | Type | Description | | -------------- | -------------------------------------------------------------------------------- | | `RuntimeError` | If fit() has not been called. | | `ValueError` | If the selected provider cannot compile the oracle for the tail that IQAE needs. | #### `fit(X, y=None, *, weights=None)` Fit the return distribution to historical price data. Parameters: | Name | Type | Description | Default | | --------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------- | | `X` | `ndarray` | A 2-D array of shape (T, n_assets) with daily closing prices, where T must be at least 2. A 1-D array of length T is accepted for a portfolio with a single asset. | *required* | | `y` | \`ndarray | None\` | Ignored, because this estimator is unsupervised. It is present for compatibility with the scikit-learn interface. | | `weights` | \`ndarray | None\` | A 1-D array of portfolio weights that sum to 1. If it is not given, all assets have equal weights. | Raises: | Type | Description | | ------------ | -------------------------------------------------------------------------------------------------------------------------------------- | | `ValueError` | If distribution is not one of the supported values. This can happen when distribution is changed with set_params() after construction. | #### `predict(X)` Not supported. Use `var_at()` instead. The estimator base class requires a `predict` method. In scikit-learn, `predict` takes samples from the same feature space that `fit` received, but the risk at a confidence level is a different question, which `var_at()` answers. Parameters: | Name | Type | Description | Default | | ---- | --------- | ----------- | ---------- | | `X` | `ndarray` | Ignored. | *required* | Raises: | Type | Description | | --------------------- | --------------------------------------- | | `NotImplementedError` | Always. The message points to var_at(). | #### `var_at(confidence_levels)` Return the VaR at each confidence level. This is the array version of `estimate()`. It runs IQAE once for each level and returns only the `var` of each result. Parameters: | Name | Type | Description | Default | | ------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ---------- | | `confidence_levels` | `ndarray` | A 1-D array of confidence levels in (0, 1), for example [0.95, 0.99]. A scalar or an (n, 1) column is also accepted, and it is read as n levels. | *required* | Returns: | Type | Description | | --------- | ------------------------------------------------------------------------------------------- | | `ndarray` | An array of VaR values, as fractions of the portfolio value lost, with one value per level. | Raises: | Type | Description | | ---------------- | ------------------------------------------------------------- | | `ValueError` | If the argument is not 1-D after a single column is squeezed. | | `NotFittedError` | If fit() has not been called. | Warns: | Type | Description | | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `UserWarning` | If any level is below 0.5. Such a level is valid, but values below 0.5 are also what a price series or a feature matrix looks like, and this warning catches the mistake of passing one of those. | Warning Each level runs IQAE once, so `n` levels need `n` separate quantum circuits. For a single risk measure, call `estimate()` instead. Example ```python estimator.var_at([0.90, 0.95, 0.99]) ``` ### `PortfolioRiskResult` Bases: `ValueEquality` Outcome of a PortfolioRiskEstimator run. The result is read-only. Assigning to a field raises `FrozenInstanceError`, so a mistyped `=` (instead of `==`) cannot change a result. This type holds no arrays, so nothing else needs protecting. Results compare by value. They are not hashable, so that all result types behave in the same way: the types that hold arrays cannot be hashed both correctly and efficiently. Attributes: | Name | Type | Description | | --------------------- | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | `var` | `float` | The Value at Risk (VaR) at the requested confidence_level, as a fraction of the portfolio value lost. For example, 0.12 means a loss of 12 %. | | `cvar` | `float` | The Conditional Value at Risk (CVaR), also called Expected Shortfall, at the same confidence level. | | `tail_probability` | `float` | The IQAE point estimate of P(loss ≥ VaR). | | `confidence_interval` | `tuple[float, float]` | The (lower, upper) Clopper-Pearson interval for the tail probability, at level 1 - alpha. | | `iqae_result` | \`IQAEResult | None\` | ## `eigenkit.chemistry.ansatz` Ansatz building blocks for chemistry circuits: Hartree-Fock reference-state preparation and the UCCSD ansatz. ### `apply_hartree_fock(circuit, n_electrons)` Prepare the Hartree-Fock reference state on an existing builder. It applies an `X` gate to each of the lowest `n_electrons` spin-orbitals, which marks them as occupied (`|1⟩`). The convention is the one of the Jordan-Wigner mapping: mode `j` is qubit `j`, and the spin-orbitals are ordered so that the lowest `n_electrons` of them are occupied in the reference state, for example `|1…1 0…0⟩`. Parameters: | Name | Type | Description | Default | | ------------- | ---------------- | --------------------------------------------------------------------- | ---------- | | `circuit` | `CircuitBuilder` | The builder to add the gates to. | *required* | | `n_electrons` | `int` | The number of electrons, which sets the Hartree-Fock reference state. | *required* | Returns: | Type | Description | | ---------------- | ----------------------------------------------- | | `CircuitBuilder` | The same builder, so that calls can be chained. | ### `hartree_fock_state(n_qubits, n_electrons)` A circuit that prepares the Hartree-Fock reference state. The state is `|1…1 0…0⟩` on `n_qubits` qubits, with `n_electrons` occupied spin-orbitals. Parameters: | Name | Type | Description | Default | | ------------- | ----- | --------------------------------------------------------------------- | ---------- | | `n_qubits` | `int` | The number of qubits. | *required* | | `n_electrons` | `int` | The number of electrons, which sets the Hartree-Fock reference state. | *required* | Returns: | Type | Description | | ------------- | -------------------------------------------------------- | | `CircuitSpec` | The circuit, as a CircuitSpec with no trainable weights. | ### `uccsd_ansatz(n_qubits, n_electrons, reps=1)` The UCCSD ansatz on the Hartree-Fock reference, with a Trotter approximation. UCCSD is the unitary coupled-cluster ansatz with single and double excitations. The circuit prepares `(∏_k exp(θ_k (T_k − T_k†)))^reps |HF⟩`, where `|HF⟩` is the Hartree-Fock state. Each generator `G = T − T†` is anti-Hermitian. The Jordan-Wigner mapping turns `H = −iG` into a Hermitian qubit operator, and `exp(θG) = exp(iθH)` is then a product of `pauli_exponential` gates. The Pauli terms of one generator commute, so that product is exact. The product over different generators is a first-order Trotter approximation. `reps` repeats the whole sequence with independent weights, which reduces the Trotter error: more repetitions bring the circuit closer to the exact UCCSD unitary. The ansatz has `reps * n_excitations` weights in total. Parameters: | Name | Type | Description | Default | | ------------- | ----- | -------------------------------------------------------------------------------------------- | ---------- | | `n_qubits` | `int` | The number of qubits, one per spin-orbital. | *required* | | `n_electrons` | `int` | The number of electrons, which sets the Hartree-Fock reference and the possible excitations. | *required* | | `reps` | `int` | The number of Trotter repetitions of the product of excitations. The default is 1. | `1` | Returns: | Type | Description | | ------------- | --------------------------------------------------------------------------------------- | | `CircuitSpec` | The ansatz as a CircuitSpec, with one trainable angle per generator and per repetition. | ### `uccsd_excitation_generators(n_qubits, n_electrons)` The excitation generators `T - T†` of the UCCSD ansatz. UCCSD is the unitary coupled-cluster ansatz with single and double excitations. The excitations go from the occupied spin-orbitals (the lowest `n_electrons`) to the unoccupied ones. Single and double excitations are taken over all choices of occupied and unoccupied indices, with no restriction from spin symmetry, which keeps the construction simple and correct. A variant restricted by spin symmetry, which would have fewer parameters, is not implemented. Parameters: | Name | Type | Description | Default | | ------------- | ----- | --------------------------------------------------------------------- | ---------- | | `n_qubits` | `int` | The number of qubits. | *required* | | `n_electrons` | `int` | The number of electrons, which sets the Hartree-Fock reference state. | *required* | Returns: | Type | Description | | ----------------------- | ----------------------------------------------------- | | `list[FermionOperator]` | The generators, as a list of FermionOperator objects. | ## `eigenkit.chemistry.exact` Exact classical reference: dense matrix and ground-state energy of a qubit `Observable`. For validating VQE on small systems (exponential cost — keep `n_qubits` small). ### `dense_hamiltonian(observable, n_qubits, offset=0.0)` The `2**n_qubits × 2**n_qubits` matrix of `observable + offset * I`. Qubit 0 is the most significant factor of the tensor product, which matches the ordering of basis states in the simulators. Parameters: | Name | Type | Description | Default | | ------------ | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `observable` | `Observable` | The qubit operator. | *required* | | `n_qubits` | `int` | The number of qubits. | *required* | | `offset` | `float` | The constant (identity) term that the mapping returned separately, added back to the energy. For a molecule, it includes the nuclear repulsion, in Hartree. | `0.0` | Returns: | Type | Description | | --------- | ------------------------------------- | | `ndarray` | The matrix, as a complex NumPy array. | ### `exact_ground_state_energy(observable, n_qubits, offset=0.0)` The lowest eigenvalue of `observable + offset * I`, by dense diagonalisation. Parameters: | Name | Type | Description | Default | | ------------ | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `observable` | `Observable` | The qubit operator. | *required* | | `n_qubits` | `int` | The number of qubits. | *required* | | `offset` | `float` | The constant (identity) term that the mapping returned separately, added back to the energy. For a molecule, it includes the nuclear repulsion, in Hartree. | `0.0` | Returns: | Type | Description | | ------- | ------------------------ | | `float` | The ground-state energy. | ## `eigenkit.chemistry.fermion` Fermionic operators: linear combinations of products of creation / annihilation operators on spin-orbital modes. Conventions (OpenFermion-compatible): - A *mode* is a spin-orbital, indexed `0 .. M-1`. - A *ladder operator* is `(mode, action)` with `action = 1` for creation `$a_{\text{mode}}^\dagger$` and `action = 0` for annihilation `a_mode`. - A *term* is a tuple of ladder operators applied left-to-right, e.g. `$a_2^\dagger a_0$` is `((2, 1), (0, 0))`. Operators are NOT normal-ordered (the Jordan-Wigner map handles raw products directly), so `$a_p^\dagger a_q$` and `$a_q a_p^\dagger$` are stored as written. ### `FermionOperator` A complex linear combination of fermionic terms, stored as `{term: coeff}`. #### `__init__(terms=None)` Build an operator from a mapping of terms to coefficients. Parameters: | Name | Type | Description | Default | | ------- | --------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `terms` | \`dict[Term, complex] | None\` | Maps each term to its coefficient. A term is a tuple of (mode, action) pairs, where action is 1 for creation and 0 for annihilation, applied from left to right. The empty tuple is the identity. The coefficients of repeated terms are added together. Leave it out for the zero operator. The default is None. | #### `annihilation(mode)` The annihilation operator `a_mode`. Parameters: | Name | Type | Description | Default | | ------ | ----- | ---------------------------------------------------------------- | ---------- | | `mode` | `int` | The index of the spin-orbital from which an electron is removed. | *required* | Returns: | Type | Description | | ------------------- | ----------------------------------- | | `'FermionOperator'` | The operator, as a FermionOperator. | #### `creation(mode)` The creation operator `a†_mode`. Parameters: | Name | Type | Description | Default | | ------ | ----- | -------------------------------------------------------------- | ---------- | | `mode` | `int` | The index of the spin-orbital in which an electron is created. | *required* | Returns: | Type | Description | | ------------------- | ----------------------------------- | | `'FermionOperator'` | The operator, as a FermionOperator. | #### `hermitian_conjugate()` The adjoint of the operator. The order of each term is reversed, creation and annihilation are swapped, and each coefficient is replaced by its complex conjugate. Returns: | Type | Description | | ------------------- | ---------------------------------- | | `'FermionOperator'` | The adjoint, as a FermionOperator. | #### `identity(coefficient=1.0)` The identity operator, optionally scaled. Parameters: | Name | Type | Description | Default | | ------------- | -------- | ------------------------------------------------------------ | ------- | | `coefficient` | `Scalar` | The factor that multiplies the identity. The default is 1.0. | `1.0` | Returns: | Type | Description | | ------------------- | ------------------------------------------ | | `'FermionOperator'` | The scaled identity, as a FermionOperator. | #### `number(mode)` The number operator `a†_mode a_mode`. It counts the electrons in one spin-orbital. Parameters: | Name | Type | Description | Default | | ------ | ----- | ------------------------------ | ---------- | | `mode` | `int` | The index of the spin-orbital. | *required* | Returns: | Type | Description | | ------------------- | ----------------------------------- | | `'FermionOperator'` | The operator, as a FermionOperator. | #### `zero()` The zero operator, which has no terms. Returns: | Type | Description | | ------------------- | ------------------------- | | `'FermionOperator'` | The zero FermionOperator. | ## `eigenkit.chemistry.interop` OpenFermion interop (the optional `chemistry` extra). OpenFermion's `FermionOperator` and `QubitOperator` share our term structure exactly — a `FermionOperator` term is a tuple of `(mode, action)` with `action` 1=creation / 0=annihilation, and a `QubitOperator` term is a tuple of `(qubit, 'X'|'Y'|'Z')` — so the conversions are direct. These adapters let you (a) bring an OpenFermion-built Hamiltonian onto the spine, and (b) reach mappings OpenFermion provides (e.g. Bravyi-Kitaev) via our `Observable`. The `->` conversions read only the `.terms` mapping (no OpenFermion import needed); the `to_openfermion` direction imports OpenFermion lazily. ### `fermion_operator_from_openfermion(operator)` Convert an `openfermion.FermionOperator` to an eigenkit `FermionOperator`. Parameters: | Name | Type | Description | Default | | ---------- | ----------------- | ---------------------------------- | ---------- | | `operator` | `FermionOperator` | The fermionic operator to convert. | *required* | Returns: | Type | Description | | ----------------- | -------------------------------------------------- | | `FermionOperator` | The same operator, as an eigenkit FermionOperator. | ### `observable_from_openfermion(qubit_operator, *, atol=1e-10)` Convert an `openfermion.QubitOperator` to an `(Observable, offset)` pair. The identity term becomes the real `offset`. The coefficients must be real, because the operator must be Hermitian, and a `ValueError` is raised otherwise. Parameters: | Name | Type | Description | Default | | ---------------- | --------------- | ---------------------------------------------------------------------------- | ---------- | | `qubit_operator` | `QubitOperator` | The operator to convert. | *required* | | `atol` | `float` | Terms whose coefficient is smaller than this value in magnitude are dropped. | `1e-10` | Returns: | Name | Type | Description | | ------------ | ------------ | ---------------------------------------- | | `observable` | `Observable` | The operator, without its identity term. | | `offset` | `float` | The coefficient of the identity term. | ### `to_openfermion_fermion_operator(operator)` Convert an eigenkit `FermionOperator` to an `openfermion.FermionOperator`. Parameters: | Name | Type | Description | Default | | ---------- | ----------------- | ---------------------------------- | ---------- | | `operator` | `FermionOperator` | The fermionic operator to convert. | *required* | Returns: | Type | Description | | ----------------- | ---------------------------------- | | `FermionOperator` | The same operator, in OpenFermion. | ## `eigenkit.chemistry.mappings` Fermion-to-qubit mappings: build a qubit `Observable` from a `FermionOperator`. Jordan-Wigner convention (OpenFermion-compatible): ```math a_j = \Big(\prod_{k`. ### `bravyi_kitaev(operator, n_qubits=None)` Map a `FermionOperator` to qubits with the Bravyi-Kitaev transformation. The transformation itself is done by OpenFermion (the `chemistry` extra), and the result is converted to an eigenkit `Observable`. The Jordan-Wigner mapping, in contrast, is implemented in eigenkit. `n_qubits` defaults to the largest mode index plus one. Parameters: | Name | Type | Description | Default | | ---------- | ----------------- | ------------------------------ | ---------- | | `operator` | `FermionOperator` | The fermionic operator to map. | *required* | | `n_qubits` | `int` | The number of qubits. | `None` | Returns: | Name | Type | Description | | ------------ | ------------ | ---------------------------------------------------------------------------- | | `observable` | `Observable` | The operator on qubits, without its identity part. | | `offset` | `float` | The coefficient of the identity part, which the Observable does not contain. | ### `jordan_wigner(operator, *, atol=1e-10)` Map a `FermionOperator` to qubits with the Jordan-Wigner transformation. The operator must be Hermitian, as a Hamiltonian is. If a resulting coefficient is not real, a `ValueError` is raised. Parameters: | Name | Type | Description | Default | | ---------- | ----------------- | ---------------------------------------------------------------------------- | ---------- | | `operator` | `FermionOperator` | The fermionic operator to map. | *required* | | `atol` | `float` | Terms whose coefficient is smaller than this value in magnitude are dropped. | `1e-10` | Returns: | Name | Type | Description | | ------------ | ------------ | ---------------------------------------------------------------------------- | | `observable` | `Observable` | The operator on qubits, without its identity part. | | `offset` | `float` | The coefficient of the identity part, which the Observable does not contain. | ## `eigenkit.chemistry.models` Built-in fermionic models with known properties — for validation and demos without any external chemistry dependency. ### `fermi_hubbard_dimer(t, u)` The two-site Fermi-Hubbard model with spin `1/2`. It has four spin-orbital modes, indexed by `mode = 2 * site + spin`, with spin `0` for up and `1` for down. Site `0` has modes `0` and `1`, and site `1` has modes `2` and `3`. The Hamiltonian is: ```text H = -t Σ_σ (a†_{0σ} a_{1σ} + a†_{1σ} a_{0σ}) + U (n_{0↑} n_{0↓} + n_{1↑} n_{1↓}) ``` The ground-state energy at half filling has the closed form fermi_hubbard_dimer_ground_energy(). This makes the model an exact check of the fermion-to-qubit mapping and of VQE, with no external dependency. Parameters: | Name | Type | Description | Default | | ---- | ------- | -------------------------------------------- | ---------- | | `t` | `float` | The hopping amplitude between the two sites. | *required* | | `u` | `float` | The on-site Coulomb repulsion. | *required* | Returns: | Type | Description | | ----------------- | -------------------------------------- | | `FermionOperator` | The Hamiltonian, as a FermionOperator. | ### `fermi_hubbard_dimer_ground_energy(t, u)` The ground-state energy of the Fermi-Hubbard dimer at half filling. The energy of fermi_hubbard_dimer() with two electrons in a singlet state is `(U - sqrt(U² + 16t²)) / 2`. This value is the ground state over all particle numbers only when `U < 3t`. For a larger `U`, double occupancy costs so much energy that the state with a single electron, of energy `-t`, is lower, and the ground state over all particle numbers is `min(-t, (U - sqrt(U² + 16t²)) / 2)`. An ansatz that conserves the number of particles, such as UCCSD, targets the half-filling sector directly. The hardware-efficient ansatz explores all sectors, so use it only where the half-filling state is the overall ground state. Parameters: | Name | Type | Description | Default | | ---- | ------- | --------------------------------------- | ---------- | | `t` | `float` | The hopping amplitude, as in the model. | *required* | | `u` | `float` | The on-site repulsion, as in the model. | *required* | Returns: | Type | Description | | ------- | -------------------------------------------------------------- | | `float` | The ground-state energy at half filling, from the closed form. | ## `eigenkit.chemistry.molecule` Real-molecule Hamiltonians via PySCF + OpenFermion (the `chemistry` extra). We use OpenFermion + openfermion-pyscf for the *classical* chemistry — molecular integrals and the fermionic Hamiltonian — and keep the quantum mapping ours: the returned `Molecule.fermion_operator` is our `FermionOperator`, ready for `jordan_wigner` (or `bravyi_kitaev`). The constant (nuclear repulsion + core energy) is included as the identity term, so `jordan_wigner`'s `offset` is the full energy shift and `min + offset` is the total molecular energy. Optional deps are imported lazily; install with `pip install eigenkit[chemistry]`. ### `Molecule` A molecular Hamiltonian in second quantisation. `fermion_operator` already includes the constant term (the nuclear repulsion), so `jordan_wigner(fermion_operator)` returns it as the offset. `hf_energy` and `fci_energy`, when computed, are the reference energies from PySCF, which are useful targets for the Variational Quantum Eigensolver (VQE). Attributes: | Name | Type | Description | | ------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `fermion_operator` | `FermionOperator` | The Hamiltonian in second quantisation, including the constant term. | | `n_qubits` | `int` | The number of qubits needed after the mapping, one per spin-orbital with Jordan-Wigner. | | `n_electrons` | `int` | The number of electrons, which sets the Hartree-Fock reference state and the excitations that a unitary coupled-cluster singles and doubles (UCCSD) ansatz can use. | | `nuclear_repulsion` | `float` | The constant nuclear term, in Hartree. It is already included in fermion_operator, and it is kept separately so that it can be reported. | | `hf_energy` | `float` | The Hartree-Fock energy from PySCF, in Hartree. A VQE run should reach a lower energy. | | `fci_energy` | \`float | None\` | ### `hydrogen_molecule(bond_length=0.7414, *, basis='sto-3g', run_fci=False)` The hydrogen molecule `H₂` at the given bond length, in angstroms. In the minimal STO-3G basis set, this is the standard 4-qubit benchmark for VQE. Its FCI ground-state energy is about `-1.137` Hartree at equilibrium. Parameters: | Name | Type | Description | Default | | ------------- | ------- | --------------------------------------------------------------------------------------------- | ---------- | | `bond_length` | `float` | The distance between the nuclei, in angstroms. The default, 0.7414, is close to equilibrium. | `0.7414` | | `basis` | `str` | The name of the PySCF basis set. | `'sto-3g'` | | `run_fci` | `bool` | Whether to also compute the exact FCI energy, the target against which a VQE run is measured. | `False` | Returns: | Type | Description | | ---------- | ---------------------------------------------------------- | | `Molecule` | The molecule, with its Hamiltonian and reference energies. | ### `molecular_hamiltonian(geometry, *, basis='sto-3g', charge=0, multiplicity=1, run_fci=False)` Build a `Molecule` from a geometry, with PySCF and OpenFermion. Parameters: | Name | Type | Description | Default | | -------------- | ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `geometry` | `list[tuple[str, tuple[float, float, float]]]` | A list of (element, (x, y, z)) entries, with the coordinates in angstroms. | *required* | | `basis` | `str` | The name of the PySCF basis set. The default is the minimal basis set "sto-3g". | `'sto-3g'` | | `charge` | `int` | The total charge of the molecule. | `0` | | `multiplicity` | `int` | The spin multiplicity, 2S + 1. | `1` | | `run_fci` | `bool` | Whether to also compute the FCI energy, which is exact in this basis set and a useful target for VQE. It needs an exact diagonalisation, so it is only possible for small systems. | `False` | Returns: | Type | Description | | ---------- | ---------------------------------------------------------- | | `Molecule` | The Molecule, with its Hamiltonian and reference energies. | ## `eigenkit.chemistry.vqe` VQE — the Variational Quantum Eigensolver, on the core spine. Given a qubit Hamiltonian (an `Observable` plus its constant offset), VQE minimizes `` over a parametrized ansatz to estimate the ground-state energy. It is a thin layer over `minimize_expectation`: build an ansatz, wrap it in a numpy `Circuit` whose observable is the Hamiltonian, optimize. Pure core (numpy/scipy), torch-free, backend-agnostic. The default ansatz is a hardware-efficient one (RY/RZ rotations + a linear CNOT entangler) starting from |0...0>, so it needs no Hartree-Fock preparation. For a chemistry-specific ansatz, pass `uccsd_ansatz(...)` as `ansatz`; it starts from the Hartree-Fock reference itself. ## `eigenkit.chemistry.vqe.VQE` Bases: `BaseQuantumSolver` Variational Quantum Eigensolver (VQE). Parameters: | Name | Type | Description | Default | | -------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------- | | `layers` | `int` | The depth of the default hardware-efficient ansatz. The default is 2. | `2` | | `provider` | `ProviderSpec` | The backend to use. The default is "numpy", the built-in reference simulator. | `'numpy'` | | `optimizer` | `Optimizer` | The optimiser: the name of a scipy.optimize.minimize method, "spsa", or a custom callable (see minimize_expectation). The default is "L-BFGS-B". | `'L-BFGS-B'` | | `restarts` | `int` | The number of runs from random starting points. The lowest energy is kept, because variational landscapes have local minima. The default is 1. | `1` | | `random_state` | \`int | None\` | The base seed. Restart r uses random_state + r, and None leaves every restart unseeded. The default is 42. | ### `solve(hamiltonian, n_qubits, *, offset=0.0, ansatz=None)` Minimise the expectation value of the Hamiltonian over the ansatz. It runs `restarts` independent optimisations from different random starting points and keeps the lowest energy, because a variational landscape has local minima. Parameters: | Name | Type | Description | Default | | ------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | | `hamiltonian` | `Observable` | The Hamiltonian to minimise, as a qubit Observable, usually the one returned by jordan_wigner. | *required* | | `n_qubits` | `int` | The number of qubits of the circuit. It must match the Hamiltonian. | *required* | | `offset` | `float` | The constant added to the minimised value. This is where the identity term returned by the mapping goes, which for a molecule includes the nuclear repulsion, in Hartree. If it is left out, the energy is wrong by exactly that constant. The default is 0.0. | `0.0` | | `ansatz` | \`CircuitSpec | None\` | The circuit to optimise. The default, None, uses the built-in hardware-efficient ansatz with layers layers. | Returns: | Type | Description | | ----------- | ------------------------------------------------------------------------------------- | | `VQEResult` | A VQEResult with the best energy, its parameters, and whether that restart converged. | ## `eigenkit.chemistry.vqe.VQEResult` Bases: `ValueEquality` Outcome of a run of the Variational Quantum Eigensolver (VQE). The result is read-only. Assigning to a field raises `FrozenInstanceError`, and writing into an array field raises `ValueError`. This prevents a mistyped `=` (instead of `==`) or an in-place edit from leaving a result whose scalar fields describe a different run from the array it holds. To get a writable copy of the array, use `np.array(result.field)`. Results compare by value, including their arrays. They are not hashable, because a result is a collection of numbers and not a key, and a correct hash of a float array is expensive to compute. Attributes: | Name | Type | Description | | -------------------- | --------- | --------------------------------------------------------------------------------------- | | `energy` | `float` | The lowest energy found, with the offset added back. For a molecule, it is in Hartree. | | `optimal_parameters` | `ndarray` | The ansatz weights that produced this energy. | | `n_iterations` | `int` | The number of optimiser iterations in the best restart. It is not summed over restarts. | | `success` | `bool` | Whether the optimiser of that restart reported convergence. | ### `__post_init__()` Make the array field read-only. Freezing the dataclass prevents a field from being reassigned, but it does not prevent writing into the array that the field holds, and a result that has been written into still reports the scalar values of the original run. The result therefore keeps its own copy, so the array it was built from can still be changed by the caller. To get a writable copy, use `np.array(result.optimal_parameters)`. ## `eigenkit.chemistry.vqe.hardware_efficient_ansatz(n_qubits, layers)` A standard hardware-efficient ansatz. It starts with a layer of `RY` rotations, then repeats `layers` times a linear chain of `CNOT` gates followed by an `RY` and an `RZ` rotation on each qubit. It has `n_qubits + 2 * layers * n_qubits` trainable weights. Parameters: | Name | Type | Description | Default | | ---------- | ----- | -------------------------- | ---------- | | `n_qubits` | `int` | The number of qubits. | *required* | | `layers` | `int` | The number of repetitions. | *required* | Returns: | Type | Description | | ------------- | ----------------------------- | | `CircuitSpec` | The ansatz, as a CircuitSpec. | ## `eigenkit.core.backends` Registration of the backends. The NumPy reference simulator only needs NumPy, and it is registered when this package is imported, because it is the default and is always available. The other backends, apart from CUDA Quantum, are registered *lazily*: naming one imports nothing, and its software development kit (PyTorch, PennyLane, Qiskit or Braket) is imported only when a circuit first uses that provider. This keeps `import eigenkit` and `import eigenkit.core` free of these large dependencies. `"pennylane"` is always registered lazily. `"qiskit"`, `"braket"` and the other optional backends are registered only when their extra is installed. `cudaq` is not registered automatically: once it is installed, register it explicitly: ```python from eigenkit import CudaQuantumBackend, register register("cudaq", CudaQuantumBackend()) ``` The backend classes and the configuration dataclasses are re-exported lazily through `__getattr__`. For example, `from eigenkit.core import PennyLaneConfig` imports the PennyLane adapter only when the name is accessed. ### `NumpyConfig` Configuration for the reference NumPy simulator. It has no settings yet. It exists so that `Circuit(provider=NumpyConfig())` works in the same way as with the configurations of the other backends. #### `make_backend()` Build the backend that this configuration describes. The provider layer calls it when a configuration is passed as `provider=`. There is no reason to call it directly. Returns: | Type | Description | | ------------------ | ------------------------------------------ | | `'NumpySimulator'` | The NumpySimulator for this configuration. | ### `NumpySimulator` The provider for the reference NumPy statevector simulator. It is registered automatically as `"numpy"`. It uses only NumPy and no optional dependency, so it is always available. It declares `gradient_method = "adjoint"`, so the gradient code of the core package uses NumpyCompiled.jacobian() (analytic, in reverse mode, with one forward pass and one backward pass whatever the number of parameters) instead of the parameter-shift rule, whose cost is `O(n_params)`. #### `capabilities` What this backend supports. It runs every gate of the library, evaluates a batch of inputs in one call, returns exact expectation values only (it does not sample shots), and has no limit on the number of qubits. See Capabilities. #### `__init__(config=None, name='numpy')` Build the backend from a device configuration. Parameters: | Name | Type | Description | Default | | -------- | ------------- | ------------------------------------------------------------- | --------- | | `config` | `NumpyConfig` | The device settings. Default: None, which uses NumpyConfig(). | `None` | | `name` | `str` | The registry name, used in error messages. Default: "numpy". | `'numpy'` | #### `compile(circuit, observables)` Prepare `circuit` for repeated evaluation of `observables`. A `Circuit` calls it once and reuses the result for every call, so the expensive work belongs here. Parameters: | Name | Type | Description | Default | | ------------- | ---------------------- | --------------------------- | ---------- | | `circuit` | `CircuitSpec` | The circuit to prepare. | *required* | | `observables` | `Sequence[Observable]` | The observables to measure. | *required* | Returns: | Type | Description | | --------------- | ------------------------------------------------ | | `NumpyCompiled` | The prepared circuit, as a NumpyCompiled object. | ## `eigenkit.core.blocks` Blocks: reusable parts of a circuit that do not depend on the provider. Given the total number of qubits of the circuit, a block builds its gates and states how many weights and inputs it uses. Blocks are combined with `assemble`: ```python from eigenkit.core.blocks import AngleEncoding, BasicVariational, assemble spec = assemble([AngleEncoding(in_features=3), BasicVariational(depth=2)]) ``` `assemble` goes through the blocks in order and chooses the total number of qubits. It shifts the local `WeightRef` and `InputRef` indices of each block so that they point to the right place in the weight and input vectors of the whole circuit, and it returns a single `CircuitSpec`. Blocks do not depend on a provider or on PyTorch, and most of them do not use NumPy. Only the blocks that check or compute a matrix (`QubitUnitary`, `ControlledUnitaryBlock` and `QPEBlock`) use NumPy. The blocks use `CircuitBuilder` internally, because it makes their code easier to read. ### `AmplitudeEncoding` Encode a classical vector as the amplitudes of a quantum state. The vector is padded with zeros to the next power of two. With `normalize=True`, the default, it is also rescaled to norm `1`. The number of qubits follows from the length of the vector: it is `max(1, ceil(log2(len(amplitudes))))`. The block adds a single `AmplitudeEncoding` gate. The `data` field of the gate holds the prepared amplitudes as a flat tuple, and each backend loads them with its own state preparation method. Parameters: | Name | Type | Description | Default | | ------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------- | | `amplitudes` | `object` | The vector to load, as an array-like. Its length sets the number of qubits, and it is padded with zeros to the next power of two. A ValueError is raised if the vector is empty, if all its values are zero, or if it holds a not-a-number (NaN) or infinite value. | *required* | | `normalize` | `bool` | If True, the vector is rescaled to norm 1 before it is loaded. Pass False when the vector is already a state. The block then stores it unchanged and checks it: a ValueError is raised if its norm differs from 1 by more than 1e-10. This check exists because, without it, the same mistake would be caught or not depending on the backend: the NumPy simulator rescales the vector without a warning, and PennyLane refuses it. The same circuit would then behave differently on different backends. Default: True. | `True` | Example ```python from eigenkit.core.blocks import AmplitudeEncoding, assemble spec = assemble([AmplitudeEncoding([0.6, 0.8], normalize=False)]) ``` #### `build(n_qubits)` Build the gates of this block for a circuit with `n_qubits` qubits. Parameters: | Name | Type | Description | Default | | ---------- | ----- | ------------------------------------------------------------------------------------------------------------------- | ---------- | | `n_qubits` | `int` | The number of qubits of the circuit being assembled, which is the number of qubits of the widest block in the list. | *required* | Returns: | Type | Description | | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `'IRFragment'` | The gates of the block, as an IRFragment. Its weight and input indices start at 0, and they are shifted when the blocks are combined into a circuit. | #### `from_dict(d)` Rebuild a block from the output of `to_dict()`. Parameters: | Name | Type | Description | Default | | ---- | ------ | ----------------------------------- | ---------- | | `d` | `dict` | A dictionary returned by to_dict(). | *required* | Returns: | Type | Description | | --------------------- | ------------------------------ | | `'AmplitudeEncoding'` | The rebuilt AmplitudeEncoding. | #### `required_qubits()` Return the number of qubits that the padded vector needs. It is `ceil(log2(len(amplitudes)))`, and at least `1`. #### `to_dict()` Return a description of this block that can be saved as JSON. When a saved model is loaded, the `type` key selects the block class whose `from_dict()` rebuilds the block. Returns: | Type | Description | | ------ | -------------------------------------- | | `dict` | The fields of the block, and its type. | ### `AngleEncoding` Encode `in_features` data values as single-qubit rotations. Feature `i` sets the angle of one rotation on qubit `i`, so the block uses the qubits `0` to `in_features - 1`. This matters only when the block is combined with others. A circuit has as many qubits as its widest block, and when another block in the list needs more qubits, this block leaves the extra qubits unchanged. The block takes `in_features` inputs and no weights. Parameters: | Name | Type | Description | Default | | ------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `in_features` | `int` | The number of features to encode, one per qubit. It must be positive, and a ValueError is raised otherwise. | *required* | | `rotation` | `GateName` | The rotation gate: "RX", "RY" or "RZ". Default: "RY". | `'RY'` | | `scale` | `float` | A constant factor applied to every input angle. It is useful when the data is normalised to [0, 1] and the circuit needs angles in [0, 2π]. It must be finite. Default: 1.0. | `1.0` | #### `build(n_qubits)` Build the gates of this block for a circuit with `n_qubits` qubits. The qubits with an index of `in_features` or more are left unchanged. Parameters: | Name | Type | Description | Default | | ---------- | ----- | ------------------------------------------------------------------------------------------------------------------- | ---------- | | `n_qubits` | `int` | The number of qubits of the circuit being assembled, which is the number of qubits of the widest block in the list. | *required* | Returns: | Type | Description | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `IRFragment` | The gates of the block, as an IRFragment. Its weight and input indices start at 0, and they are shifted when the blocks are combined into a circuit. | #### `from_dict(d)` Rebuild a block from the output of `to_dict()`. Parameters: | Name | Type | Description | Default | | ---- | ------ | ----------------------------------- | ---------- | | `d` | `dict` | A dictionary returned by to_dict(). | *required* | Returns: | Type | Description | | ----------------- | -------------------------- | | `'AngleEncoding'` | The rebuilt AngleEncoding. | #### `required_qubits()` Return `in_features`, because the block uses one qubit per feature. #### `to_dict()` Return a description of this block that can be saved as JSON. When a saved model is loaded, the `type` key selects the block class whose `from_dict()` rebuilds the block. Returns: | Type | Description | | ------ | -------------------------------------- | | `dict` | The fields of the block, and its type. | ### `BasicVariational` `depth` trainable layers of single-qubit rotations and entangling gates. Each layer applies one trainable rotation to every qubit, then a controlled-NOT (CNOT) gate from each qubit to the next one. The block takes `depth * n_qubits` weights and no inputs. Parameters: | Name | Type | Description | Default | | ---------- | ---------- | -------------------------------------------------------------------------------- | ---------- | | `depth` | `int` | The number of layers. It must be positive, and a ValueError is raised otherwise. | *required* | | `rotation` | `GateName` | The rotation gate of the trainable layer: "RX", "RY" or "RZ". Default: "RY". | `'RY'` | #### `build(n_qubits)` Build the gates of this block for a circuit with `n_qubits` qubits. The layers act on every qubit of the circuit. Parameters: | Name | Type | Description | Default | | ---------- | ----- | ------------------------------------------------------------------------------------------------------------------- | ---------- | | `n_qubits` | `int` | The number of qubits of the circuit being assembled, which is the number of qubits of the widest block in the list. | *required* | Returns: | Type | Description | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `IRFragment` | The gates of the block, as an IRFragment. Its weight and input indices start at 0, and they are shifted when the blocks are combined into a circuit. | #### `from_dict(d)` Rebuild a block from the output of `to_dict()`. Parameters: | Name | Type | Description | Default | | ---- | ------ | ----------------------------------- | ---------- | | `d` | `dict` | A dictionary returned by to_dict(). | *required* | Returns: | Type | Description | | -------------------- | ----------------------------- | | `'BasicVariational'` | The rebuilt BasicVariational. | #### `required_qubits()` Return `1`, because the block extends to all the qubits of the circuit. #### `to_dict()` Return a description of this block that can be saved as JSON. When a saved model is loaded, the `type` key selects the block class whose `from_dict()` rebuilds the block. Returns: | Type | Description | | ------ | -------------------------------------- | | `dict` | The fields of the block, and its type. | ### `Block` Bases: `Protocol` The interface that every circuit block follows. A block is a reusable part of a circuit, such as an encoding, a variational layer or a fixed subroutine. `assemble` asks each block in turn how many qubits it needs, gives the circuit as many qubits as the widest block needs, and then asks every block to build its gates for that number of qubits. Any object with these two methods works as a block. It does not need to inherit from this class. #### `build(n_qubits)` Build the gates of this block for a circuit with `n_qubits` qubits. Parameters: | Name | Type | Description | Default | | ---------- | ----- | ---------------------------------------------------- | ---------- | | `n_qubits` | `int` | The number of qubits of the circuit being assembled. | *required* | Returns: | Type | Description | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `IRFragment` | The gates of the block, as an IRFragment. Its weight and input indices start at 0, and they are shifted when the blocks are combined into a circuit. | #### `required_qubits()` Return the minimum number of qubits that this block needs. ### `ControlledUnitaryBlock` Apply a unitary matrix `U` to `target_wires`, controlled by `control_wires`. Parameters: | Name | Type | Description | Default | | ---------------- | ----------------- | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | | `unitary` | `object` | A 2^n_target × 2^n_target matrix, as an array-like, where n_target is the number of target qubits. | *required* | | `control_wires` | `tuple[int, ...]` | The qubits that control the gate. | *required* | | `target_wires` | `tuple[int, ...]` | The qubits that the matrix acts on. | *required* | | `control_values` | \`tuple[int, ...] | None\` | The bit value that activates each control, one for each entry of control_wires. Default: 1 for every control. | Example ```python import numpy as np import scipy.linalg from eigenkit.core.blocks import ControlledUnitaryBlock, assemble A = np.diag([1.0, 2.0, 3.0, 4.0]) # a Hermitian matrix on two qubits t = 0.5 U = scipy.linalg.expm(1j * A * t) block = ControlledUnitaryBlock(U, control_wires=(0,), target_wires=(1, 2)) spec = assemble([block]) ``` #### `build(n_qubits)` Build the gates of this block for a circuit with `n_qubits` qubits. Parameters: | Name | Type | Description | Default | | ---------- | ----- | ------------------------------------------------------------------------------------------------------------------- | ---------- | | `n_qubits` | `int` | The number of qubits of the circuit being assembled, which is the number of qubits of the widest block in the list. | *required* | Returns: | Type | Description | | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `'IRFragment'` | The gates of the block, as an IRFragment. Its weight and input indices start at 0, and they are shifted when the blocks are combined into a circuit. | #### `from_dict(d)` Rebuild a block from the output of `to_dict()`. Parameters: | Name | Type | Description | Default | | ---- | ------ | ----------------------------------- | ---------- | | `d` | `dict` | A dictionary returned by to_dict(). | *required* | Returns: | Type | Description | | -------------------------- | ----------------------------------- | | `'ControlledUnitaryBlock'` | The rebuilt ControlledUnitaryBlock. | #### `required_qubits()` Return one more than the highest control or target qubit. #### `to_dict()` Return a description of this block that can be saved as JSON. When a saved model is loaded, the `type` key selects the block class whose `from_dict()` rebuilds the block. Returns: | Type | Description | | ------ | -------------------------------------- | | `dict` | The fields of the block, and its type. | ### `CustomBlock` A block made from the gates of a `CircuitBuilder`. `CircuitBuilder.to_block()` creates it. It is useful when you want to write a part of a circuit gate by gate and combine it with the standard blocks. Parameters: | Name | Type | Description | Default | | ----------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `n_qubits` | `int` | The number of qubits that the builder was given, which is also the number of qubits that this block needs. It must be positive. | *required* | | `gates` | `tuple[Gate, ...]` | The gates that the builder added, in the order in which they are applied. | *required* | | `n_weights` | `int` | The number of trainable weights that these gates use. It must not be negative. | *required* | | `n_inputs` | `int` | The number of input features that these gates use. It must not be negative. | *required* | #### `build(n_qubits)` Build the gates of this block for a circuit with `n_qubits` qubits. Parameters: | Name | Type | Description | Default | | ---------- | ----- | ------------------------------------------------------------------------------------------------------------------- | ---------- | | `n_qubits` | `int` | The number of qubits of the circuit being assembled, which is the number of qubits of the widest block in the list. | *required* | Returns: | Type | Description | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `IRFragment` | The gates of the block, as an IRFragment. Its weight and input indices start at 0, and they are shifted when the blocks are combined into a circuit. | #### `required_qubits()` Return `n_qubits`, the number of qubits that the builder was given. ### `Grover` The Grover operator `Q = A · S₀ · A† · S_ψ₁` used for amplitude estimation. This is the operator that iterative quantum amplitude estimation (IQAE) uses. The block builds it from a state-preparation oracle `A`, given as a `CircuitSpec`, and from these standard parts: - **`S_ψ₁`** flips the phase of the ancilla qubit, which is the last qubit. It is the gate `Z(n-1)`. - **`A†`** is the adjoint of the oracle. It uses the same weights and inputs as `A`. - **`S₀`** is the reflection about the all-zero state: `X^⊗n → MCZ → X^⊗n`, where `MCZ` is a multi-controlled `Z` gate. - **`A`** is the oracle. The gates are applied to the state in this order, from left to right: ```text Z(ancilla) → A† → S₀ → A ``` `A` and `A†` share their weights and inputs, so `fragment.n_weights == a_circuit.n_weights`: the count is not doubled. Parameters: | Name | Type | Description | Default | | ----------- | ------------- | ---------------------------------------------------------------------------------------------------------------- | ---------- | | `a_circuit` | `CircuitSpec` | The state-preparation oracle A, as a CircuitSpec with at least one qubit. Its last qubit is used as the ancilla. | *required* | Example ```python from eigenkit.core.blocks import Grover, assemble from eigenkit.core.builder import CircuitBuilder qb = CircuitBuilder(n_qubits=2) qb.h(0).cnot(0, 1) spec = assemble([Grover(a_circuit=qb.build())]) ``` #### `build(n_qubits)` Build the gates of this block for a circuit with `n_qubits` qubits. The oracle `A` and its adjoint `A†` share their weights and inputs, so these are not counted twice. Parameters: | Name | Type | Description | Default | | ---------- | ----- | ------------------------------------------------------------------------------------------------------------------- | ---------- | | `n_qubits` | `int` | The number of qubits of the circuit being assembled, which is the number of qubits of the widest block in the list. | *required* | Returns: | Type | Description | | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `'IRFragment'` | The gates of the block, as an IRFragment. Its weight and input indices start at 0, and they are shifted when the blocks are combined into a circuit. | #### `from_dict(d)` Rebuild a block from the output of `to_dict()`. Parameters: | Name | Type | Description | Default | | ---- | ------ | ----------------------------------- | ---------- | | `d` | `dict` | A dictionary returned by to_dict(). | *required* | Returns: | Type | Description | | ---------- | ------------------- | | `'Grover'` | The rebuilt Grover. | #### `required_qubits()` Return the number of qubits of the oracle. The operator adds no qubits of its own. #### `to_dict()` Return a description of this block that can be saved as JSON. When a saved model is loaded, the `type` key selects the block class whose `from_dict()` rebuilds the block. Returns: | Type | Description | | ------ | -------------------------------------- | | `dict` | The fields of the block, and its type. | ### `HamiltonianQPEBlock` Quantum phase estimation (QPE) of `e^{iAt}`, built with Trotter products. This block replaces QPEBlock in the clock register of the Harrow-Hassidim-Lloyd (HHL) solver with `mode="honest"`, where the circuit is built as hardware would run it. It turns the Hermitian matrix `A = Σ_k c_k P_k` into elementary gates with Trotter products. It does not exponentiate a dense oracle and raise it to powers with `matrix_power`: both of these cost `O(N³)` for an `N × N` matrix, and neither gives a circuit of elementary gates. The controlled evolution `controlled-e^{iA·2^m·t}` that a clock qubit needs is a Trotter product with a total time of `2^m·t`, and never a matrix power. The step size `t/steps` is fixed, so the power `2^m` uses `steps·2^m` steps. Only the single `RZ` of each Trotter term is controlled. The controlled `RZ` is built from `RZ` and controlled-NOT (CNOT) gates, as `CRZ = RZ·CNOT·RZ·CNOT`. The change of basis and the ladder gates cancel out when the control is `|0⟩`. The identity term of `A` becomes a `PhaseShift` on the clock qubit, which is a controlled global phase. As a result, the estimated phase reflects the full eigenvalue and not only its traceless part, which matches what QPEBlock gives from `expm(1j·A·t)`. The qubits are laid out as follows, with indices local to the block: ```text [clock(0 … n_clock-1) | state(n_clock … n_clock+n_state-1)] ``` Clock qubit `k` controls `e^{iA·2^(n_clock-1-k)·t}`. This is the big-endian convention of QPE, the same as in QPEBlock. The caller maps these local indices to the qubits of the full circuit, for example to skip the HHL ancilla qubit. Parameters: | Name | Type | Description | Default | | ------------- | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ---------- | | `pauli_terms` | `tuple[tuple[float, str], ...]` | The (coeff, pauli_string) terms of the Hermitian matrix A, for example from pauli_decompose. Every string has length n_state. | *required* | | `n_clock` | `int` | The number of clock qubits, which sets the resolution of QPE. | *required* | | `time` | `float` | The base evolution time t. | *required* | | `steps` | `int` | The number of Trotter steps for the base time t. Default: 1. | `1` | | `order` | `int` | The Trotter order, 1 or 2. Default: 1. | `1` | | `inverse` | `bool` | If True, the block builds the inverse QPE, which undoes the phase estimation. Default: False. | `False` | Example ```python import numpy as np from eigenkit.algorithms._hamiltonian_sim import pauli_decompose from eigenkit.core.blocks import HamiltonianQPEBlock, assemble A = np.array([[1.0, 0.5], [0.5, -1.0]]) # a Hermitian matrix on one qubit terms = pauli_decompose(A) spec = assemble([HamiltonianQPEBlock(terms, n_clock=4, time=1.0, steps=8)]) ``` #### `build(n_qubits)` Build the phase estimation, with Trotter products, for `n_qubits` qubits. The qubit indices are local to the block, and the caller maps them to the qubits of the full circuit. Parameters: | Name | Type | Description | Default | | ---------- | ----- | ------------------------------------------------------------------------------------------------------------------- | ---------- | | `n_qubits` | `int` | The number of qubits of the circuit being assembled, which is the number of qubits of the widest block in the list. | *required* | Returns: | Type | Description | | ------------ | ----------------------------------------------------------------------------------------- | | `IRFragment` | The gates of the block, as an IRFragment. It holds elementary gates only, and no weights. | #### `from_dict(d)` Rebuild a block from the output of `to_dict()`. Parameters: | Name | Type | Description | Default | | ---- | ------ | ----------------------------------- | ---------- | | `d` | `dict` | A dictionary returned by to_dict(). | *required* | Returns: | Type | Description | | ----------------------- | -------------------------------- | | `'HamiltonianQPEBlock'` | The rebuilt HamiltonianQPEBlock. | #### `required_qubits()` Return `n_clock` plus the number of state qubits. The clock qubits come first, then the state qubits. #### `to_dict()` Return a description of this block that can be saved as JSON. When a saved model is loaded, the `type` key selects the block class whose `from_dict()` rebuilds the block. Returns: | Type | Description | | ------ | -------------------------------------- | | `dict` | The fields of the block, and its type. | ### `IRFragment` The gates that one block adds to a circuit. The `WeightRef` and `InputRef` indices of a fragment are local to it, and they start at `0`. When the blocks are combined into a circuit, `assemble` shifts them so that they point to the right place in the weight and input vectors of the whole circuit. Attributes: | Name | Type | Description | | ----------- | ------------------ | --------------------------------------------------------------- | | `gates` | `tuple[Gate, ...]` | The gates of the block, in the order in which they are applied. | | `n_weights` | `int` | The number of trainable weights that the block uses. | | `n_inputs` | `int` | The number of input features that the block uses. | ### `QFTBlock` Quantum Fourier transform (QFT) on `n_qubits` qubits. The transform is built from `H`, `CPHASE` and `SWAP` gates only, which are all gates that the intermediate representation (IR) of the circuit defines. Parameters: | Name | Type | Description | Default | | ---------- | ------ | ------------------------------------------------------------------------------------------------------------------------- | ---------- | | `n_qubits` | `int` | The number of qubits of the transform. It must be positive. | *required* | | `inverse` | `bool` | If True, the block builds the inverse QFT, which quantum phase estimation (QPE) applies as its last step. Default: False. | `False` | Example ```python from eigenkit.core.blocks import AmplitudeEncoding, QFTBlock, assemble b = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0] # eight values for three qubits spec = assemble([AmplitudeEncoding(b), QFTBlock(n_qubits=3, inverse=True)]) ``` #### `build(n_qubits)` Build the gates of this block for a circuit with `n_qubits` qubits. Parameters: | Name | Type | Description | Default | | ---------- | ----- | ------------------------------------------------------------------------------------------------------------------- | ---------- | | `n_qubits` | `int` | The number of qubits of the circuit being assembled, which is the number of qubits of the widest block in the list. | *required* | Returns: | Type | Description | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `IRFragment` | The gates of the block, as an IRFragment. Its weight and input indices start at 0, and they are shifted when the blocks are combined into a circuit. | #### `from_dict(d)` Rebuild a block from the output of `to_dict()`. Parameters: | Name | Type | Description | Default | | ---- | ------ | ----------------------------------- | ---------- | | `d` | `dict` | A dictionary returned by to_dict(). | *required* | Returns: | Type | Description | | ------------ | --------------------- | | `'QFTBlock'` | The rebuilt QFTBlock. | #### `required_qubits()` Return `n_qubits`, the number of qubits that the transform acts on. #### `to_dict()` Return a description of this block that can be saved as JSON. When a saved model is loaded, the `type` key selects the block class whose `from_dict()` rebuilds the block. Returns: | Type | Description | | ------ | -------------------------------------- | | `dict` | The fields of the block, and its type. | ### `QPEBlock` Quantum phase estimation (QPE) of a unitary oracle. The block applies `H` to every clock qubit, then the controlled powers `U^{2^k}` of the oracle, then the inverse quantum Fourier transform (`QFT†`): `H^⊗n_clock → controlled-U^{2^k} → QFT†`. Set `inverse=True` for the adjoint: `QFT → controlled-U†^{2^k} → H^⊗n_clock`. The oracle must be a `CircuitSpec` whose unitary can be read as a dense matrix. In practice, this means a circuit made of a single QubitUnitary gate. Parametrised oracles made of several gates are not supported. To use an AmplitudeEncoding or a ControlledUnitaryBlock as an oracle, first compute its unitary matrix and wrap it in a QubitUnitary. The qubits are laid out as follows, with indices local to the block: ```text [clock(0 … n_clock-1) | state(n_clock … n_clock+n_state-1)] ``` The caller is responsible for mapping these local indices to the qubits of the full circuit, for example to skip the ancilla qubit of the Harrow-Hassidim-Lloyd (HHL) algorithm. Parameters: | Name | Type | Description | Default | | ---------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `oracle_circuit` | `CircuitSpec` | A CircuitSpec whose single QubitUnitary gate defines the unitary U that acts on the state qubits. The single-gate requirement is checked when the block is built, and the type when the block is created. | *required* | | `n_clock` | `int` | The number of clock qubits, which sets the resolution of QPE. It must be at least 1. | *required* | | `inverse` | `bool` | If True, the block builds the inverse QPE, which undoes the phase estimation. Default: False. | `False` | Example ```python import numpy as np import scipy.linalg from eigenkit.core.blocks import QPEBlock, QubitUnitary, assemble A = np.array([[2, 0], [0, 1]], dtype=float) U = scipy.linalg.expm(1j * A * np.pi) oracle = assemble([QubitUnitary(U, wires=(0,))]) spec = assemble([QPEBlock(oracle, n_clock=3)]) ``` #### `build(n_qubits)` Build the gates of this block for a circuit with `n_qubits` qubits. The qubit indices are local to the block, and the caller maps them to the qubits of the full circuit. Parameters: | Name | Type | Description | Default | | ---------- | ----- | ------------------------------------------------------------------------------------------------------------------- | ---------- | | `n_qubits` | `int` | The number of qubits of the circuit being assembled, which is the number of qubits of the widest block in the list. | *required* | Returns: | Type | Description | | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `'IRFragment'` | The gates of the block, as an IRFragment. Its weight and input indices start at 0, and they are shifted when the blocks are combined into a circuit. | #### `from_dict(d)` Rebuild a block from the output of `to_dict()`. Parameters: | Name | Type | Description | Default | | ---- | ------ | ----------------------------------- | ---------- | | `d` | `dict` | A dictionary returned by to_dict(). | *required* | Returns: | Type | Description | | ------------ | --------------------- | | `'QPEBlock'` | The rebuilt QPEBlock. | #### `required_qubits()` Return `n_clock` plus the number of qubits of the oracle. The clock qubits come first, then the state qubits. #### `to_dict()` Return a description of this block that can be saved as JSON. When a saved model is loaded, the `type` key selects the block class whose `from_dict()` rebuilds the block. Returns: | Type | Description | | ------ | -------------------------------------- | | `dict` | The fields of the block, and its type. | ### `QubitUnitary` Apply any unitary matrix to the given qubits. The matrix is stored in the `data` field of the gate as a nested tuple, so that the `Gate` stays hashable. Parameters: | Name | Type | Description | Default | | --------- | ----------------- | ------------------------------------------------------------------------------------------------------------------- | ---------- | | `unitary` | `object` | A 2^n × 2^n matrix, as an array-like, where n is len(wires). It is checked to be unitary when the block is created. | *required* | | `wires` | `tuple[int, ...]` | The target qubits, in the order that the matrix expects. | *required* | Example ```python import numpy as np from eigenkit.core.blocks import QubitUnitary, assemble U = np.array([[0, 1], [1, 0]]) # the Pauli X matrix spec = assemble([QubitUnitary(U, wires=(0,))]) ``` #### `build(n_qubits)` Build the gates of this block for a circuit with `n_qubits` qubits. Parameters: | Name | Type | Description | Default | | ---------- | ----- | ------------------------------------------------------------------------------------------------------------------- | ---------- | | `n_qubits` | `int` | The number of qubits of the circuit being assembled, which is the number of qubits of the widest block in the list. | *required* | Returns: | Type | Description | | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `'IRFragment'` | The gates of the block, as an IRFragment. Its weight and input indices start at 0, and they are shifted when the blocks are combined into a circuit. | #### `from_dict(d)` Rebuild a block from the output of `to_dict()`. Parameters: | Name | Type | Description | Default | | ---- | ------ | ----------------------------------- | ---------- | | `d` | `dict` | A dictionary returned by to_dict(). | *required* | Returns: | Type | Description | | ---------------- | ------------------------- | | `'QubitUnitary'` | The rebuilt QubitUnitary. | #### `required_qubits()` Return `max(wires) + 1`, one more than the highest target qubit. #### `to_dict()` Return a description of this block that can be saved as JSON. When a saved model is loaded, the `type` key selects the block class whose `from_dict()` rebuilds the block. Returns: | Type | Description | | ------ | -------------------------------------- | | `dict` | The fields of the block, and its type. | ### `Reuploading` Data reuploading: the input is encoded several times, between trainable layers. The block repeats an encoding of the input followed by a variational layer, `depth` times. Each encoding can have a trainable scale for each feature and repetition. The arguments give two patterns: 1. Standard reuploading (the default; Pérez-Salinas et al. 2020). The same input is encoded `depth` times, with new variational weights between the encodings. 1. Trainable feature scaling (`trainable_scale=True`; Schuld et al. 2021). Each encoding rotation takes the angle `λ_{d,i} * x_i`, where each pair of repetition `d` and feature `i` has its own trainable `λ_{d,i}`. Parameters: | Name | Type | Description | Default | | ---------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `in_features` | `int` | The total number of input features. It must be positive. | *required* | | `depth` | `int` | The number of times the whole sequence is repeated. It must be positive. | *required* | | `encoding_rotation` | `GateName` | The rotation gate of the encoding: "RX", "RY" or "RZ". Default: "RY". | `'RY'` | | `variational_rotation` | `GateName` | The rotation gate of the variational layer: "RX", "RY" or "RZ". It is used only when variational_type="basic". Default: "RY". | `'RY'` | | `variational_type` | `str` | "basic", for one rotation per qubit followed by a ladder of controlled-NOT (CNOT) gates, or "strong", for RZ, RY and RZ rotations on every qubit followed by a ladder of CNOT gates. Default: "basic". | `'basic'` | | `trainable_scale` | `bool` | If True, each encoding rotation gets a trainable scale for each repetition and each feature. The CUDA Quantum backend does not support it, because it allows only limited arithmetic on parameters. Default: False. | `False` | Weight count - With `variational_type="basic"` and no trainable scale, the block has `depth * in_features` weights. - With `variational_type="strong"` and no trainable scale, it has `3 * depth * in_features` weights. - `trainable_scale=True` adds `depth * in_features` weights. #### `build(n_qubits)` Build the gates of this block for a circuit with `n_qubits` qubits. Parameters: | Name | Type | Description | Default | | ---------- | ----- | ------------------------------------------------------------------------------------------------------------------- | ---------- | | `n_qubits` | `int` | The number of qubits of the circuit being assembled, which is the number of qubits of the widest block in the list. | *required* | Returns: | Type | Description | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `IRFragment` | The gates of the block, as an IRFragment. Its weight and input indices start at 0, and they are shifted when the blocks are combined into a circuit. | #### `from_dict(d)` Rebuild a block from the output of `to_dict()`. Parameters: | Name | Type | Description | Default | | ---- | ------ | ----------------------------------- | ---------- | | `d` | `dict` | A dictionary returned by to_dict(). | *required* | Returns: | Type | Description | | --------------- | ------------------------ | | `'Reuploading'` | The rebuilt Reuploading. | #### `required_qubits()` Return `in_features`: one qubit for each feature. #### `to_dict()` Return a description of this block that can be saved as JSON. When a saved model is loaded, the `type` key selects the block class whose `from_dict()` rebuilds the block. Returns: | Type | Description | | ------ | -------------------------------------- | | `dict` | The fields of the block, and its type. | ### `S0` Reflection about the all-zero state `|0…0⟩`, built as `X^⊗n → MCZ → X^⊗n`. The block applies `X` to every qubit, then a multi-controlled `Z` gate (`MCZ`) on all of them, then `X` to every qubit again. It acts on the qubits `0` to `n_qubits - 1`, which are all the qubits of the circuit unless another block in the list needs more. This reflection is the `S₀` part of the Grover operator used by iterative quantum amplitude estimation (IQAE). The Grover block adds the same gates itself. Parameters: | Name | Type | Description | Default | | ---------- | ----- | ------------------------------------------------------------------------------------------ | ---------- | | `n_qubits` | `int` | The number of qubits of the reflection. Every one of them takes part. It must be positive. | *required* | Example ```python from eigenkit.core.blocks import S0, assemble spec = assemble([S0(n_qubits=3)]) ``` #### `build(n_qubits)` Build the gates of this block for a circuit with `n_qubits` qubits. Parameters: | Name | Type | Description | Default | | ---------- | ----- | ------------------------------------------------------------------------------------------------------------------- | ---------- | | `n_qubits` | `int` | The number of qubits of the circuit being assembled, which is the number of qubits of the widest block in the list. | *required* | Returns: | Type | Description | | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `'IRFragment'` | The gates of the block, as an IRFragment. Its weight and input indices start at 0, and they are shifted when the blocks are combined into a circuit. | #### `from_dict(d)` Rebuild a block from the output of `to_dict()`. Parameters: | Name | Type | Description | Default | | ---- | ------ | ----------------------------------- | ---------- | | `d` | `dict` | A dictionary returned by to_dict(). | *required* | Returns: | Type | Description | | ------ | --------------- | | `'S0'` | The rebuilt S0. | #### `required_qubits()` Return `n_qubits`, the number of qubits that the reflection acts on. #### `to_dict()` Return a description of this block that can be saved as JSON. When a saved model is loaded, the `type` key selects the block class whose `from_dict()` rebuilds the block. Returns: | Type | Description | | ------ | -------------------------------------- | | `dict` | The fields of the block, and its type. | ### `StrongVariational` `depth` trainable layers of `RZ`, `RY` and `RZ` rotations and entangling gates. Each layer applies the rotations `RZ`, `RY` and `RZ` to every qubit, then a controlled-NOT (CNOT) gate from each qubit to the next one. The block is modelled on PennyLane's `StronglyEntanglingLayers`. The gates are added one layer at a time: all the `RZ` rotations, then all the `RY` rotations, then all the `RZ` rotations, then the CNOT gates. They are not added as three rotations per qubit. The result is the same, because rotations on different qubits commute. The layered order lets the backends that support it apply each layer of rotations in one fast call, in place of `3 * n_qubits` calls on single qubits. The block takes `3 * depth * n_qubits` weights and no inputs. Parameters: | Name | Type | Description | Default | | ------- | ----- | -------------------------------------------------------------------------------- | ---------- | | `depth` | `int` | The number of layers. It must be positive, and a ValueError is raised otherwise. | *required* | #### `build(n_qubits)` Build the gates of this block for a circuit with `n_qubits` qubits. The layers act on every qubit of the circuit. Parameters: | Name | Type | Description | Default | | ---------- | ----- | ------------------------------------------------------------------------------------------------------------------- | ---------- | | `n_qubits` | `int` | The number of qubits of the circuit being assembled, which is the number of qubits of the widest block in the list. | *required* | Returns: | Type | Description | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `IRFragment` | The gates of the block, as an IRFragment. Its weight and input indices start at 0, and they are shifted when the blocks are combined into a circuit. | #### `from_dict(d)` Rebuild a block from the output of `to_dict()`. Parameters: | Name | Type | Description | Default | | ---- | ------ | ----------------------------------- | ---------- | | `d` | `dict` | A dictionary returned by to_dict(). | *required* | Returns: | Type | Description | | --------------------- | ------------------------------ | | `'StrongVariational'` | The rebuilt StrongVariational. | #### `required_qubits()` Return `1`, because the block extends to all the qubits of the circuit. #### `to_dict()` Return a description of this block that can be saved as JSON. When a saved model is loaded, the `type` key selects the block class whose `from_dict()` rebuilds the block. Returns: | Type | Description | | ------ | -------------------------------------- | | `dict` | The fields of the block, and its type. | ### `TrotterBlock` First-order or second-order Trotter approximation of `e^{iAt}`. The block builds the time evolution under a Hermitian matrix `A = Σ_k c_k P_k`, given as `(coeff, pauli_string)` terms (for example from `pauli_decompose()`). It uses elementary gates only (`H`, `RX`, controlled-NOT (CNOT) and `RZ`), and no dense `QubitUnitary`. It replaces applying `scipy.linalg.expm(1j*A*t)` as a single opaque unitary, and it builds the evolution as hardware would run it. Each term `e^{i·c·P·τ}` is built as follows. Every factor of `P` that is not `Z` is rotated into the eigenbasis of `Z` (with `H` for `X`, and `RX(+π/2)` for `Y`). A ladder of CNOT gates collects the parity of the active qubits on the last one, a single `RZ(-2cτ)` is applied there, and then the ladder and the change of basis are undone. With the convention `RZ(θ) = e^{-iθZ/2}` of the library, this gives `e^{+iAt}` up to a global phase, which matches the sign of the reference Harrow-Hassidim-Lloyd (HHL) solver. The identity term is only a global phase, so it is skipped: it has no observable effect here, and it becomes a phase on the clock qubit in the controlled version used for quantum phase estimation (QPE). Parameters: | Name | Type | Description | Default | | ------------- | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `pauli_terms` | `tuple[tuple[float, str], ...]` | A sequence of (coeff, pauli_string) pairs. All the strings have the same length n, which is the number of qubits. Each coeff must be real up to numerical tolerance, because A is Hermitian. | *required* | | `time` | `float` | The evolution time t. | *required* | | `steps` | `int` | The number of Trotter steps. The approximation error decreases as steps increases. Default: 1. | `1` | | `order` | `int` | 1 (Lie-Trotter) or 2 (symmetric Strang splitting, which has a smaller error for the same steps). Default: 1. | `1` | Example ```python import numpy as np from eigenkit.algorithms._hamiltonian_sim import pauli_decompose from eigenkit.core.blocks import TrotterBlock, assemble A = np.array([[1.0, 0.5], [0.5, -1.0]]) # a Hermitian matrix on one qubit terms = pauli_decompose(A) spec = assemble([TrotterBlock(terms, time=1.0, steps=20, order=2)]) ``` #### `build(n_qubits)` Build the Trotter product for a circuit with `n_qubits` qubits. The qubit indices are local to the block, and the caller maps them to the qubits of the full circuit. Parameters: | Name | Type | Description | Default | | ---------- | ----- | ------------------------------------------------------------------------------------------------------------------- | ---------- | | `n_qubits` | `int` | The number of qubits of the circuit being assembled, which is the number of qubits of the widest block in the list. | *required* | Returns: | Type | Description | | ------------ | ----------------------------------------------------------------------------------------- | | `IRFragment` | The gates of the block, as an IRFragment. It holds elementary gates only, and no weights. | #### `from_dict(d)` Rebuild a block from the output of `to_dict()`. Parameters: | Name | Type | Description | Default | | ---- | ------ | ----------------------------------- | ---------- | | `d` | `dict` | A dictionary returned by to_dict(). | *required* | Returns: | Type | Description | | ---------------- | ------------------------- | | `'TrotterBlock'` | The rebuilt TrotterBlock. | #### `required_qubits()` Return the length of the Pauli strings, one qubit for each factor. #### `to_dict()` Return a description of this block that can be saved as JSON. When a saved model is loaded, the `type` key selects the block class whose `from_dict()` rebuilds the block. Returns: | Type | Description | | ------ | -------------------------------------- | | `dict` | The fields of the block, and its type. | ### `assemble(blocks)` Combine blocks into a single `CircuitSpec`. A `CircuitBuilder` can take the place of a block. It is replaced by the result of its `to_block()`, so a part of a circuit written gate by gate can go directly in the list. The blocks are combined as follows: - `n_qubits` is the largest `required_qubits()` of the blocks. - The weights and the inputs of the blocks are placed one after the other, in the order of the blocks. - The local `WeightRef` and `InputRef` indices of each block are shifted by the number of weights and inputs of the blocks before it. Parameters: | Name | Type | Description | Default | | -------- | ----------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `blocks` | \`Iterable\[Block | CircuitBuilder\]\` | The blocks to combine, in the order in which they are applied. Any of them can be a CircuitBuilder, which is taken as it is when assemble is called. | Returns: | Type | Description | | ------------- | --------------------------------------------- | | `CircuitSpec` | One CircuitSpec that contains all the blocks. | Raises: | Type | Description | | ------------ | ------------------- | | `ValueError` | If blocks is empty. | Example ```python from eigenkit import Encoding, Variational from eigenkit.core import assemble spec = assemble([Encoding(in_features=4), Variational(depth=2)]) print(spec.n_qubits, spec.n_weights, spec.n_inputs) # 4 8 4 ``` ## `eigenkit.core.builder` A builder that creates a `CircuitSpec` one gate at a time. The style follows Qiskit, Amazon Braket and CUDA Quantum: `qc.rx(angle, qubit)` adds a gate to `qc` itself and returns `qc`, so that calls can be chained. `CircuitBuilder` sits between the ready-made blocks and writing the circuit data by hand. You can write a circuit gate by gate when that is the natural way, and then, if you want, call `.to_block()` to combine the result with other blocks. ### `CircuitBuilder` Build a `CircuitSpec` one gate at a time. Example ```python qc = CircuitBuilder(n_qubits=3) weights = qc.add_weights(6) inputs = qc.add_inputs(3) for qubit in range(3): qc.ry(inputs[qubit], qubit=qubit) for depth in range(2): for qubit in range(3): qc.ry(weights[depth * 3 + qubit], qubit=qubit) for qubit in range(2): qc.cnot(qubit, qubit + 1) spec = qc.build() ``` Parameters: | Name | Type | Description | Default | | ---------- | ----- | ----------------------------------------------------------------------------------------------------------------------- | ---------- | | `n_qubits` | `int` | The number of qubits of the circuit being built. Every qubit index that a gate uses must be between 0 and n_qubits - 1. | *required* | Raises: | Type | Description | | ------------ | ---------------------------- | | `ValueError` | If n_qubits is not positive. | #### `n_inputs` The number of input features that `add_inputs()` has reserved so far. The built CircuitSpec has the same number. #### `n_qubits` The number of qubits of the circuit, as given to the constructor. Every qubit index that a gate uses must be smaller than this number, and the number never changes. #### `n_weights` The number of trainable weights that `add_weights()` has reserved so far. The built CircuitSpec has the same number. #### `add_inputs(count)` Reserve `count` input features and return references to them. This is the counterpart of `add_weights()` for data instead of trainable parameters. Parameters: | Name | Type | Description | Default | | ------- | ----- | ---------------------------------------- | ---------- | | `count` | `int` | The number of input features to reserve. | *required* | Returns: | Type | Description | | ---------------- | ------------------------------------------------- | | `list[InputRef]` | One InputRef for each reserved feature, in order. | #### `add_weights(count)` Reserve `count` trainable weights and return references to them. Pass a returned `WeightRef` wherever a gate takes an angle. The indices are local to this builder and start at `0`. When the block is combined with others, `assemble` shifts them to their positions in the whole circuit. Parameters: | Name | Type | Description | Default | | ------- | ----- | --------------------------------- | ---------- | | `count` | `int` | The number of weights to reserve. | *required* | Returns: | Type | Description | | ----------------- | ------------------------------------------------- | | `list[WeightRef]` | One WeightRef for each reserved weight, in order. | #### `build()` Return the current state of the builder as an immutable `CircuitSpec`. #### `cnot(control, target)` Add a controlled-NOT (CNOT) gate. Parameters: | Name | Type | Description | Default | | --------- | ----- | --------------------------------- | ---------- | | `control` | `int` | The qubit that controls the flip. | *required* | | `target` | `int` | The qubit that is flipped. | *required* | Returns: | Type | Description | | ------------------ | ------------------------------------------- | | `'CircuitBuilder'` | This builder, so that calls can be chained. | Raises: | Type | Description | | ------------ | ----------------------------------------- | | `ValueError` | If control and target are the same qubit. | #### `cphase(angle, control, target)` Add a controlled phase shift gate. The gate is symmetric: the control and the target play the same role. Parameters: | Name | Type | Description | Default | | --------- | --------------- | ------------------------------------------------------------------------------- | ---------- | | `angle` | `ParameterLike` | The phase. It can be a number or a symbolic reference. | *required* | | `control` | `int` | One of the two qubits. | *required* | | `target` | `int` | The other qubit. The gate acts in the same way if the two qubits are exchanged. | *required* | Returns: | Type | Description | | ------------------ | ------------------------------------------- | | `'CircuitBuilder'` | This builder, so that calls can be chained. | #### `cry(angle, control, target)` Add a controlled `RY` gate. Parameters: | Name | Type | Description | Default | | --------- | --------------- | --------------------------------------------------------------- | ---------- | | `angle` | `ParameterLike` | The rotation angle. It can be a number or a symbolic reference. | *required* | | `control` | `int` | The qubit that controls the rotation. | *required* | | `target` | `int` | The qubit that is rotated. | *required* | Returns: | Type | Description | | ------------------ | ------------------------------------------- | | `'CircuitBuilder'` | This builder, so that calls can be chained. | #### `h(qubit)` Add a Hadamard gate. Parameters: | Name | Type | Description | Default | | ------- | ----- | ----------------- | ---------- | | `qubit` | `int` | The target qubit. | *required* | Returns: | Type | Description | | ------------------ | ------------------------------------------- | | `'CircuitBuilder'` | This builder, so that calls can be chained. | #### `mcx(*qubits)` Add a multi-controlled `X` gate. All the qubits except the last one are controls. Parameters: | Name | Type | Description | Default | | --------- | ----- | ------------------------------------------------------------------------------------- | ------- | | `*qubits` | `int` | Two or more distinct qubits. The controls come first, and the target is the last one. | `()` | Returns: | Type | Description | | ------------------ | ------------------------------------------- | | `'CircuitBuilder'` | This builder, so that calls can be chained. | Raises: | Type | Description | | ------------ | -------------------------------------------------------------- | | `ValueError` | If fewer than two qubits are given, or if a qubit is repeated. | #### `mcz(*qubits)` Add a multi-controlled `Z` gate. All the qubits except the last one are controls. Parameters: | Name | Type | Description | Default | | --------- | ----- | ------------------------------------------------------------------------------------- | ------- | | `*qubits` | `int` | Two or more distinct qubits. The controls come first, and the target is the last one. | `()` | Returns: | Type | Description | | ------------------ | ------------------------------------------- | | `'CircuitBuilder'` | This builder, so that calls can be chained. | Raises: | Type | Description | | ------------ | -------------------------------------------------------------- | | `ValueError` | If fewer than two qubits are given, or if a qubit is repeated. | #### `pauli_exponential(paulis, angle, coefficient=1.0)` Add the exponential of a product of Pauli operators. The gate is `exp(-i * coefficient * angle * P)`, where `P` is the product of `paulis`. Each element of `paulis` is a pair `(op, qubit)`, where `op` is `"X"`, `"Y"` or `"Z"`, and the qubits are distinct. This is the Trotter building block that the quantum approximate optimisation algorithm (QAOA) needs for its cost and mixer layers, and that the unitary coupled-cluster singles and doubles (UCCSD) ansatz needs for its excitation operators. It is built from the core gates only. First, `H` gates (for `X`) and `RX(π/2)` gates (for `Y`) change each factor to the `Z` basis. Then a ladder of controlled-NOT (CNOT) gates accumulates the parity on the last qubit, and a central `RZ(2 * coefficient * angle)` is applied (because `RZ(φ) = exp(-i φ Z / 2)`). Finally, the inverse of the first steps is applied. Parameters: | Name | Type | Description | Default | | ------------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `paulis` | `Sequence[tuple[str, int]]` | The Pauli product, as (operator, qubit) pairs, where the operator is "X", "Y" or "Z" and the qubits are distinct. An empty sequence is the identity, and nothing is added. | *required* | | `angle` | `ParameterLike` | The rotation angle. It can be a number or a symbolic reference. | *required* | | `coefficient` | `float` | A number that multiplies the angle. This is where the weight of a Hamiltonian term goes. Default: 1.0. | `1.0` | Returns: | Type | Description | | ------------------ | ------------------------------------------- | | `'CircuitBuilder'` | This builder, so that calls can be chained. | #### `phase_shift(angle, qubit)` Add a phase shift gate `R(φ)`. The gate multiplies `|1⟩` by `e^(iφ)` and leaves `|0⟩` unchanged. Parameters: | Name | Type | Description | Default | | ------- | --------------- | -------------------------------------------------------- | ---------- | | `angle` | `ParameterLike` | The phase φ. It can be a number or a symbolic reference. | *required* | | `qubit` | `int` | The target qubit. | *required* | Returns: | Type | Description | | ------------------ | ------------------------------------------- | | `'CircuitBuilder'` | This builder, so that calls can be chained. | #### `rx(angle, qubit)` Add a rotation about the `X` axis. Parameters: | Name | Type | Description | Default | | ------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ---------- | | `angle` | `ParameterLike` | The rotation angle. It can be a number, or a WeightRef, an InputRef or an expression, which is evaluated when the circuit is called. | *required* | | `qubit` | `int` | The target qubit. | *required* | Returns: | Type | Description | | ------------------ | ------------------------------------------- | | `'CircuitBuilder'` | This builder, so that calls can be chained. | #### `ry(angle, qubit)` Add a rotation about the `Y` axis. Parameters: | Name | Type | Description | Default | | ------- | --------------- | ------------------------------------------------------------------------------------- | ---------- | | `angle` | `ParameterLike` | The rotation angle. It can be a number, or a WeightRef, an InputRef or an expression. | *required* | | `qubit` | `int` | The target qubit. | *required* | Returns: | Type | Description | | ------------------ | ------------------------------------------- | | `'CircuitBuilder'` | This builder, so that calls can be chained. | #### `rz(angle, qubit)` Add a rotation about the `Z` axis. Parameters: | Name | Type | Description | Default | | ------- | --------------- | ------------------------------------------------------------------------------------- | ---------- | | `angle` | `ParameterLike` | The rotation angle. It can be a number, or a WeightRef, an InputRef or an expression. | *required* | | `qubit` | `int` | The target qubit. | *required* | Returns: | Type | Description | | ------------------ | ------------------------------------------- | | `'CircuitBuilder'` | This builder, so that calls can be chained. | #### `s(qubit)` Add an `S` gate, which is a quarter turn about the `Z` axis. Its inverse is `sdg()`. Parameters: | Name | Type | Description | Default | | ------- | ----- | ----------------- | ---------- | | `qubit` | `int` | The target qubit. | *required* | Returns: | Type | Description | | ------------------ | ------------------------------------------- | | `'CircuitBuilder'` | This builder, so that calls can be chained. | #### `sdg(qubit)` Add an S-dagger gate (`Sdg`), which is the inverse of `s()`. Parameters: | Name | Type | Description | Default | | ------- | ----- | ----------------- | ---------- | | `qubit` | `int` | The target qubit. | *required* | Returns: | Type | Description | | ------------------ | ------------------------------------------- | | `'CircuitBuilder'` | This builder, so that calls can be chained. | #### `swap(qubit0, qubit1)` Add a `SWAP` gate, which exchanges the states of two qubits. Parameters: | Name | Type | Description | Default | | -------- | ----- | ----------------- | ---------- | | `qubit0` | `int` | The first qubit. | *required* | | `qubit1` | `int` | The second qubit. | *required* | Returns: | Type | Description | | ------------------ | ------------------------------------------- | | `'CircuitBuilder'` | This builder, so that calls can be chained. | #### `to_block()` Wrap the current state of the builder in a `CustomBlock`. This is useful to build a part of a circuit one gate at a time, and then to combine it with other blocks. When the blocks are assembled, the indices of the `WeightRef` and `InputRef` objects of this block are shifted to their positions in the whole circuit. #### `x(qubit)` Add a Pauli `X` gate, which is a bit flip. Parameters: | Name | Type | Description | Default | | ------- | ----- | ----------------- | ---------- | | `qubit` | `int` | The target qubit. | *required* | Returns: | Type | Description | | ------------------ | ------------------------------------------- | | `'CircuitBuilder'` | This builder, so that calls can be chained. | #### `z(qubit)` Add a Pauli `Z` gate, which is a phase flip. Parameters: | Name | Type | Description | Default | | ------- | ----- | ----------------- | ---------- | | `qubit` | `int` | The target qubit. | *required* | Returns: | Type | Description | | ------------------ | ------------------------------------------- | | `'CircuitBuilder'` | This builder, so that calls can be chained. | ## `eigenkit.core.circuit` The core `Circuit`, which needs no framework, compiles once and accepts batches. - The circuit is compiled once, in `__init__`, and every call (`__call__`) reuses the compiled handle. - `inputs` can be 1-D (one sample) or 2-D (a batch, with the samples along the first axis). The array is always passed with its shape unchanged to `compiled.run` or `compiled.run_torch`, and the backend handles the shape itself. Backends that cannot run a batch at once use `batch_call`. - The observables can be given in several forms. `None` measures `Z` on every qubit. Otherwise, pass an `Observable`, a list of them, or a `PauliExpr`. This module holds the NumPy path: forward values computed with NumPy only, and analytic jacobians from the parameter-shift rule or the chain rule. Automatic differentiation in a framework (PyTorch now, JAX later) is handled by a registered *interface* (see `interfaces.py`), so this module imports no framework and stays in the lightweight core. The PyTorch `nn.Module` wrapper, `TorchCircuit`, is in the PyTorch package (`eigenkit.torch`). ### `Circuit` A compiled circuit, ready to evaluate. It holds a `CircuitSpec`, the observables to measure and a compiled backend handle. The circuit is compiled once, when the instance is created, and every call reuses the compiled handle. The instance does not store weights: you pass the `weights` at each call. This is the NumPy interface. The top-level `Circuit(...)` function of the package returns this class for `interface="numpy"` and a `TorchCircuit` for `interface="torch"`. Parameters: | Name | Type | Description | Default | | ------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------- | | `spec` | `CircuitSpec` | The circuit to run, usually built with assemble(blocks). | *required* | | `observables` | `ObservableLike` | What to measure. None measures Z on every qubit and returns one value per qubit. A single Observable or PauliExpr returns a single value, and a list of them returns one value for each. Default: None. | `None` | | `provider` | `ProviderSpec` | The backend that runs the circuit. It can be a registered name ("numpy", "pennylane" and others), a configuration instance, or a provider object. Default: "numpy". | `'numpy'` | | `interface` | `Interface` | How a call is evaluated. With "numpy", a call runs the NumPy path and returns NumPy values. With "torch", each call is passed to the registered PyTorch interface, and TorchCircuit builds its core circuit in this way. The top-level Circuit function always passes "numpy". Default: "numpy". | `'numpy'` | Example ```python import numpy as np from eigenkit import Circuit, Encoding, Variational, all_z circuit = Circuit( blocks=[Encoding(in_features=2), Variational(depth=1)], observables=all_z(2), ) # The circuit stores no weights, so you pass them at every call. weights = np.zeros(circuit.spec.n_weights) values = circuit(weights, np.array([0.1, 0.2])) # Gradients, without PyTorch. grad = circuit.jacobian(weights, np.array([0.1, 0.2])) ``` #### `draw(*, decimals=3)` Return a text drawing of the circuit, over several lines. The drawing does not depend on the provider: it is built from the `CircuitSpec`, not from the compiled backend object. See `eigenkit.core.draw` for the details of the layout. Parameters: | Name | Type | Description | Default | | ---------- | ----- | ---------------------------------------------------------------------------- | ------- | | `decimals` | `int` | The number of significant digits shown for a numeric gate angle. Default: 3. | `3` | Returns: | Type | Description | | ----- | ------------------------------------------------------- | | `str` | The drawing, as a single string that contains newlines. | #### `draw_mpl(**kwargs)` Draw the circuit as a matplotlib figure, and return `(fig, ax)`. Gates are drawn as labelled boxes and controls as filled dots, and each measured wire (from `self.observables`) ends with a measurement symbol. As with `draw()`, the picture is built from the `CircuitSpec` and the observables, not from the compiled backend, so it does not depend on the provider. Pass `style="eigen"` for the brand colours (each gate is coloured by its kind: rotations, `H` and block gates, and measurements), or `style="roles"` to colour the outline of each gate by its role (encoding, trainable or fixed). The default is black on white. Each style has a `"-dark"` variant (for example `"eigen-dark"`) for dark backgrounds, and `"eigen-universal"` is a single image that works on both. matplotlib is an optional dependency. Install it with `eigenkit[viz]`. Parameters: | Name | Type | Description | Default | | ---------- | ----- | --------------------------------------------------------------------------------------------------------- | ------- | | `**kwargs` | `Any` | Arguments passed to draw_mpl(): decimals, show_params, show_measurements, fontsize, style, legend and ax. | `{}` | Returns: | Name | Type | Description | | -------- | ------- | ------------------------------ | | `fig_ax` | `tuple` | The matplotlib (fig, ax) pair. | #### `dry_run_info()` Describe the prepared circuit in a dictionary, without submitting it. It is available only on backends whose `CompiledCircuit` provides a `dry_run_info` method. Currently, these are Braket (on its local simulator as well as on cloud devices), Qiskit Runtime and IQM. It is useful to check the size of the circuit, the gate counts after transpilation, the number of shots and the number of circuits that each call runs, before you submit to a cloud simulator or a quantum processing unit (QPU). It does not estimate a price. #### `jacobian(weights, inputs=None, *, input_grad=False)` Return the gradient of the expectation values with respect to the weights. It returns `d/d(weights)`, of shape `(n_weights,)` for a single observable or `(n_observables, n_weights)` for a list of observables, like the output of `__call__()`. Pass a single 1-D `inputs` sample. For a batch, call it once for each row. The NumPy interface can compute every gradient without PyTorch. This method uses the same sequence of gradient methods as the variational optimisation tools and the PyTorch interface. It uses the backend's own differentiation when the backend has one (the analytic adjoint method of the NumPy simulator, the differentiation of PennyLane, and so on), and otherwise the parameter-shift rule, which works on any backend. With `input_grad=True`, it returns `(weight_jacobian, input_jacobian)`, where `input_jacobian` is `d/d(inputs)`, with the same leading shape. Parameters: | Name | Type | Description | Default | | ------------ | --------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `weights` | `ndarray` | The weight vector, of shape (n_weights,). | *required* | | `inputs` | \`ndarray | None\` | One 1-D sample, of shape (n_inputs,). This method takes a single sample, so for a batch, call it once for each row. None works only for a circuit that takes no inputs, and it raises a ValueError for any other circuit. Default: None. | | `input_grad` | `bool` | If True, also return the jacobian with respect to the inputs. Default: False. | `False` | Returns: | Name | Type | Description | | ---------- | --------- | ------------------------- | | `jacobian` | \`ndarray | tuple[ndarray, ndarray]\` | Raises: | Type | Description | | -------------- | ----------------------------------------------------------------------------------------------- | | `RuntimeError` | On a hardware backend whose configuration leaves enable_gradients off (gradient_method="none"). | #### `probabilities(weights, inputs=None)` Return the measurement distribution over all `2**n_qubits` basis states. The basis states are ordered with qubit `0` as the most significant bit. The distribution is used to decode samples. For example, the quantum approximate optimisation algorithm (QAOA) reads the most probable bitstrings from the optimised state. Parameters: | Name | Type | Description | Default | | --------- | --------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `weights` | `ndarray` | The weight vector, of shape (n_weights,). | *required* | | `inputs` | \`ndarray | None\` | One 1-D sample. None works only for a circuit that takes no inputs, and it raises a ValueError for any other circuit. Default: None. | Returns: | Type | Description | | --------- | ----------------------------------------------------------------- | | `ndarray` | The probabilities of all basis states, of shape (2\*\*n_qubits,). | Raises: | Type | Description | | --------------------- | ------------------------------------------------------------------------------------------------------------ | | `NotImplementedError` | On a backend that has neither a probabilities() method nor a state vector to compute the probabilities from. | #### `resources(**kwargs)` Report the resources that the circuit uses once transpiled for its target. It reports the native gate counts, the number of two-qubit gates and the two-qubit depth, the physical qubits chosen by the layout, the number of two-qubit gates added by routing (compared with the same circuit on the native gates with all-to-all connectivity), and the success probability predicted by the error rates of the target. Nothing is submitted. Parameters: | Name | Type | Description | Default | | ---------- | ----- | ------------------------------------------------------------------------------------------------------------- | ------- | | `**kwargs` | `Any` | Arguments passed to the backend, for example measured=False on Qiskit Runtime for the bare estimator circuit. | `{}` | Returns: | Type | Description | | ------ | ----------------------------------------------------------------- | | `dict` | A dictionary. See QiskitRuntimeCompiled.resources() for its keys. | Raises: | Type | Description | | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `NotImplementedError` | On backends that do not transpile for a device, which includes every simulator. To inspect a device locally, use Qiskit Runtime or IQM with a fake backend_instance. | #### `sample(weights, inputs=None)` Return the raw measurement histogram, which maps each bitstring to a count. It is what a sampling backend measured, before any averaging. These are the counts behind `probabilities()`, and it is the only form that stays usable when the circuit has too many qubits for a dense distribution. In each bitstring, qubit `0` is the most significant bit, so `"100"` means that qubit `0` was measured in state `1`. Only sampling backends can return a histogram, because it needs a finite number of shots. Exact statevector simulators raise an error instead of making up a histogram. Use `probabilities()` with those. Parameters: | Name | Type | Description | Default | | --------- | --------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `weights` | `ndarray` | The weight vector, of shape (n_weights,). | *required* | | `inputs` | \`ndarray | None\` | One 1-D sample. None works only for a circuit that takes no inputs, and it raises a ValueError for any other circuit. Default: None. | Returns: | Name | Type | Description | | -------- | ---------------- | -------------------------------------------------------------------------------------------------------------- | | `counts` | `dict[str, int]` | The number of shots for each measured bitstring. The counts add up to the number of shots set for the backend. | Raises: | Type | Description | | --------------------- | -------------------------------------------------------------------------------------------------- | | `NotImplementedError` | On backends that do not sample. This includes every exact simulator, which has no shots to report. | #### `statevector(weights, inputs=None, initial_state=None)` Return the full complex statevector after the circuit runs. It is a flat array of length `2**n_qubits`, ordered with qubit `0` as the most significant bit. A 2-D `inputs` batch returns an array of shape `(B, 2**n_qubits)`. With `initial_state`, a normalised flat statevector in the same order, the evolution starts from that state instead of `|0…0⟩`. Reading a state out and passing it back in this way is the basic operation behind the state carried over in quantum reservoirs and behind amplitude encoding. Parameters: | Name | Type | Description | Default | | --------------- | --------- | ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | `weights` | `ndarray` | The weight vector, of shape (n_weights,). | *required* | | `inputs` | \`ndarray | None\` | A 1-D sample, or a 2-D batch. None works only for a circuit that takes no inputs, and it raises a ValueError for any other circuit. Default: None. | | `initial_state` | \`ndarray | None\` | The state to start the evolution from, instead of | Returns: | Type | Description | | --------- | ------------------------------------------------------------------------------ | | `ndarray` | The statevector, of shape (2\*\*n_qubits,), or (B, 2\*\*n_qubits) for a batch. | Raises: | Type | Description | | --------------------- | --------------------------------------------------------------------------------------------------------------------------- | | `NotImplementedError` | On backends without access to the statevector, which is the case for most quantum processing unit (QPU) and cloud backends. | #### `submit(weights, inputs=None)` Submit a job without waiting for it, and return a `JobHandle`. `handle.result()` returns the expectation value of each observable. Parameters: | Name | Type | Description | Default | | --------- | --------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `weights` | `ndarray` | The weight vector, of shape (n_weights,). | *required* | | `inputs` | \`ndarray | None\` | One 1-D sample. None works only for a circuit that takes no inputs, and it raises a ValueError for any other circuit. Default: None. | Returns: | Name | Type | Description | | -------- | ----------- | ------------------------------- | | `handle` | `JobHandle` | A handle for the submitted job. | Raises: | Type | Description | | --------------------- | -------------------------------------------------------------------------------------------------------------------- | | `NotImplementedError` | If the CompiledCircuit of the backend does not provide submit. Currently, Braket, Qiskit Runtime and IQM provide it. | #### `transpiled_circuit(**kwargs)` Return the `qiskit.QuantumCircuit` that a hardware backend submits. The circuit is transpiled for the target of the backend. Use it to draw the circuit (`.draw("mpl")`) or to inspect it. Raises: | Type | Description | | --------------------- | ----------------------------------------------- | | `NotImplementedError` | On backends that do not transpile for a device. | #### `zero_state_probabilities(weights, inputs)` Compute `zero_state_probability()` for every row of a batch of inputs. `inputs` has shape `(B, n_inputs)`, and the result is an array of shape `(B,)` of all-zeros probabilities. Backends that provide a batched `zero_state_probabilities` (such as the NumPy simulator) compute the whole batch at once. The other backends call the single-sample method once for each row. This is what lets QuantumKernel build a fidelity matrix without a Python loop over pairs of samples. Parameters: | Name | Type | Description | Default | | --------- | --------- | ----------------------------------------- | ---------- | | `weights` | `ndarray` | The weight vector, of shape (n_weights,). | *required* | | `inputs` | `ndarray` | A batch, of shape (B, n_inputs). | *required* | Returns: | Type | Description | | --------- | ------------------------------------------------------------------ | | `ndarray` | One all-zeros probability for each row, as an array of shape (B,). | #### `zero_state_probability(weights, inputs=None)` Return the probability of measuring the all-zeros state after the circuit. It is useful for quantities based on fidelity: quantum kernels built from inversion circuits, swap tests and the Bures distance. Each backend computes it in its own way. PennyLane returns `qml.probs(...)[0]`, the Qiskit and NumPy simulators return the squared amplitude of the all-zeros basis state, Braket uses its own probability result, and IQM and Qiskit Runtime estimate it from shots. This method does not go through the `Observable` path, because the value is not the expectation value of a Pauli operator. Parameters: | Name | Type | Description | Default | | --------- | --------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `weights` | `ndarray` | The weight vector, of shape (n_weights,). | *required* | | `inputs` | \`ndarray | None\` | One 1-D sample. None works only for a circuit that takes no inputs, and it raises a ValueError for any other circuit. Default: None. | Returns: | Type | Description | | ------- | ----------------------------------- | | `float` | The probability, a float in [0, 1]. | Raises: | Type | Description | | --------------------- | -------------------------------------------------------------- | | `NotImplementedError` | If the active backend does not provide zero_state_probability. | ## `eigenkit.core.draw` Text drawer for `CircuitSpec`, which does not depend on the provider. It draws the circuit as a string of several lines: ```text q_0: ──RX(x_0)──RZ(w_0)──●──H── │ q_1: ──RY(x_1)───────────X───── ``` A single-qubit gate is drawn as a `NAME(param)` label on its wire. A controlled-NOT (CNOT) gate is drawn as `●` on the control and `X` on the target, with `│` connectors between the wires that it spans. Parameter expressions are drawn as symbols: ```text Constant(0.5) -> "0.5" WeightRef(3) -> "w_3" InputRef(0) -> "x_0" Scale(InputRef(0), 2*pi) -> "6.28*x_0" Negate(WeightRef(0)) -> "-w_0" Sum((w_0, x_0)) -> "w_0+x_0" Multiply(w_0, x_0) -> "w_0*x_0" ``` ### `draw_text(spec, *, decimals=3)` Draw `spec` as text on several lines. The text is made of American Standard Code for Information Interchange (ASCII) characters and a few box-drawing characters. Parameters: | Name | Type | Description | Default | | ---------- | ------------- | ---------------------------------------------------------------- | ---------- | | `spec` | `CircuitSpec` | The CircuitSpec to draw. | *required* | | `decimals` | `int` | The number of significant digits of constant angles. Default: 3. | `3` | Returns: | Type | Description | | ----- | ------------------------------------------------------ | | `str` | The drawing, as a string. Print it to see the circuit. | ## `eigenkit.core.draw_mpl` Matplotlib drawer for `CircuitSpec`, which does not depend on the provider. It draws a circuit diagram: gates as boxes with labels, controls as filled dots, the targets of controlled-NOT (`CNOT`) and multi-controlled `X` (`MCX`) gates as `⊕`, `SWAP` gates as crossed wires and, optionally, measurement meters at the end of the wires that the observables act on. Like the text drawer in `draw.py`, the picture is built from the `CircuitSpec` intermediate representation (IR) and the observables of the circuit, never from the compiled backend object. The drawing is therefore the same whichever provider ran the circuit. It uses `draw._layout` to place the gates in columns, so the matplotlib and the text drawings stay consistent. matplotlib is an optional dependency, and it is imported only when `draw_mpl` is called. A core installation does not install it. Install it with `eigenkit[viz]`. The styles come in light and dark pairs, selected by name: - `"default"`: black on white. - `"eigen"`: the brand palette. Each gate is coloured by its kind: violet for rotations, cyan for `H` and the block gates, and blue for the meters. - `"roles"`: the outline of each gate is coloured by its role (encoding, trainable or fixed), with a legend. Each of them has a `"-dark"` variant (`"default-dark"`, `"eigen-dark"`, `"roles-dark"`) for dark backgrounds. These variants have a transparent canvas and light strokes, so they work on any dark shade. There is also `"eigen-universal"`: a single transparent image of filled gate chips, which can be read on both light and dark pages. The styles are plain dictionaries in `_STYLES`, where a new theme can be added. The wires are drawn only in the gaps between gates, so no wire shows through the label of a gate. ### `draw_mpl(spec, *, observables=None, decimals=3, show_params=True, show_measurements=True, fontsize=_DEFAULT_FONTSIZE, style='default', legend=True, ax=None)` Draw `spec` as a matplotlib circuit diagram. Parameters: | Name | Type | Description | Default | | ------------------- | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | | `spec` | `CircuitSpec` | The circuit to draw. | *required* | | `observables` | `Optional[Sequence[Observable]]` | The observables whose Pauli support sets the wires that end with a measurement meter. Pass None, or show_measurements=False, to draw no meters. Default: None. | `None` | | `decimals` | `int` | The number of significant digits of constant gate angles. Default: 3. | `3` | | `show_params` | `bool` | If True, the angle of each parametrised gate is written in its box (for example RX above w_0). Default: True. | `True` | | `show_measurements` | `bool` | If True and observables is given, a meter is drawn on each measured wire. Default: True. | `True` | | `fontsize` | `float` | The font size of the gate names. The angle labels are smaller. Default: 13.0. | `_DEFAULT_FONTSIZE` | | `style` | `str` | The name of a style: "default" (black on white), "eigen" (the brand palette, where gates are coloured by kind: rotations, H and block gates, and meters), "roles" (gate outlines coloured by role: encoding, trainable or fixed) or "eigen-universal" (one image for light and dark pages). Each of them except the last has a "-dark" variant (for example "eigen-dark") for dark backgrounds. A style dictionary can also be passed. Default: "default". | `'default'` | | `legend` | `bool` | If True, the "roles" styles draw a legend that maps each colour to its role. Styles that do not colour gates by role ignore it. Default: True. | `True` | | `ax` | `'Optional[Axes]'` | An existing matplotlib Axes to draw into. If it is not given, a new figure and axes are created, with a size that fits the circuit. The background of a style is applied only to a figure that draw_mpl creates. When you pass your own ax, set the face colour of its figure yourself: the "-dark" styles draw in light colours and need a dark background to be readable. Default: None. | `None` | Returns: | Type | Description | | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `'tuple[Figure, Axes]'` | The (figure, axes) pair. Save the figure with figure.savefig(...). A figure that draw_mpl creates is not registered with pyplot, so plt.show() does not display it. | ### `measured_qubits(observables)` Return every qubit that any of `observables` acts on (their Pauli support). Parameters: | Name | Type | Description | Default | | ------------- | ---------------------- | --------------------------- | ---------- | | `observables` | `Sequence[Observable]` | The observables to measure. | *required* | Returns: | Type | Description | | ---------- | ----------------------------------------------------- | | `set[int]` | The index of every qubit that the observables act on. | ## `eigenkit.core.interfaces` Differentiation interfaces, which connect the core to automatic differentiation. The core Circuit computes forward values and analytic NumPy jacobians (with the parameter-shift rule, the chain rule or the adjoint method). An *interface* connects this NumPy code to the automatic differentiation of a framework. It wraps the NumPy `run` and jacobians of the circuit into a differentiable operation of that framework (a `torch.autograd.Function` now, a `jax.custom_vjp` later), and it converts between the framework and NumPy. NumPy needs no interface, because the core returns arrays directly. Frameworks register themselves when their package is imported. Adding JAX therefore takes a new adapter module and one register_interface() call. The core never changes and never imports a framework. `get_interface("torch")` imports the PyTorch package (`eigenkit.torch`) the first time it is needed, and raises a clear error if PyTorch is not installed. ### `Interface` Bases: `Protocol` An adapter from the NumPy core to the automatic differentiation of a framework. `run_circuit` receives the core `Circuit` (for `compiled`, `provider.gradient_method`, `observables` and the jacobian helpers), and the `weights` and `inputs` as tensors of the framework. It returns a differentiable output, also as a tensor of the framework. #### `run_circuit(circuit, weights, inputs)` Run `circuit` on tensors of the framework and return a differentiable result. An implementation that builds its own default `inputs` (for a circuit that takes none) must call `circuit._check_parameter_lengths(weights, inputs)` once that default exists. The core cannot run this check before it passes the call here, and without it an input of the wrong size can be cut short without any error. Parameters: | Name | Type | Description | Default | | --------- | --------- | ------------------------------------------ | ---------- | | `circuit` | `Circuit` | The core circuit being run. | *required* | | `weights` | `Any` | The weights, as a tensor of the framework. | *required* | | `inputs` | `Any` | The inputs, as a tensor of the framework. | *required* | Returns: | Name | Type | Description | | -------- | ----- | ------------------------------------------------------ | | `result` | `Any` | A differentiable result, as a tensor of the framework. | ### `get_interface(name)` Return the interface registered under `name`, importing its package if needed. Parameters: | Name | Type | Description | Default | | ------ | ----- | -------------------- | ---------- | | `name` | `str` | The registered name. | *required* | Returns: | Type | Description | | ----------- | ------------------------- | | `Interface` | The registered interface. | Raises: | Type | Description | | ------------- | ---------------------------------------------------------------------------------------------------------------- | | `ImportError` | If the package of the interface is known but its framework is not installed. The message says how to install it. | | `KeyError` | If no interface is registered under name. | ### `register_interface(name, interface)` Register the interface of a framework under `name` (for example `"torch"`). Parameters: | Name | Type | Description | Default | | ----------- | ----------- | ------------------------ | ---------- | | `name` | `str` | The name to register. | *required* | | `interface` | `Interface` | The adapter to register. | *required* | ### `registered_interfaces()` Return the names of the interfaces registered so far. An interface appears only once its package has been imported, so this lists what is loaded now, not everything that is installed. Example ```python from eigenkit.core.interfaces import registered_interfaces print(registered_interfaces()) # [] import eigenkit.torch # registers itself on import print(registered_interfaces()) # ['torch'] ``` Returns: | Type | Description | | ----------- | ------------------------------- | | `list[str]` | The registered interface names. | ## `eigenkit.core.ir` The intermediate representation (IR) of quantum circuits. The IR is plain data and does not depend on any third-party package. Every object is a frozen dataclass, so circuits can be hashed and pickled, and they can be shared safely between threads or processes. The parameter expressions (`Scale`, `Negate`, `Sum` and `Multiply`) let encoding blocks write angles such as `RY(2π * x)` or `RY(λ * x)`, so the user does not have to scale the inputs beforehand. They also let transforms such as `adjoint` work on symbolic references instead of fixed numbers. Example ```python from eigenkit import Encoding, Variational from eigenkit.core import assemble from eigenkit.core.ir import Observable, adjoint spec = assemble([Encoding(in_features=2), Variational(depth=1)]) print(spec.n_qubits, len(spec.gates)) # 2 5 print(spec.gates[0].name, spec.gates[0].qubits) # RY (0,) # adjoint reverses the gate order and negates every angle print(adjoint(spec).gates[0].name) # CNOT # an observable can be parsed from a sum of Pauli terms written as a string print(Observable.from_string("0.5 Z0 X1").terms[0].coeff) # 0.5 ``` ### `CircuitSpec` A whole circuit as data. It holds the list of gates, the number of qubits, and the lengths of the weight and input vectors that the gates refer to. Every provider compiles this object, and `assemble` builds it from a list of blocks. It holds no weights and no observables: the weights are passed at each call, and the observables are set by the `Circuit` that wraps the spec. Attributes: | Name | Type | Description | | ----------- | ------------------ | -------------------------------------------------------------------------- | | `n_qubits` | `int` | The number of qubits of the circuit. | | `n_weights` | `int` | The length of the weight vector that the gates refer to through WeightRef. | | `n_inputs` | `int` | The length of the input vector that the gates refer to through InputRef. | | `gates` | `tuple[Gate, ...]` | The gates, in the order in which they are applied. | ### `Constant` Bases: `_ParamOps` A fixed angle, used where a block needs a rotation by a set amount. Attributes: | Name | Type | Description | | ------- | ------- | ---------------------- | | `value` | `float` | The angle, in radians. | ### `Gate` One gate of a circuit: which gate it is, where it acts, and its angles. Attributes: | Name | Type | Description | | --------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `name` | `GateName` | The name of the gate, from the fixed list in GateName. | | `qubits` | `tuple[int, ...]` | The indices of the qubits that the gate acts on, in the order that the gate expects (the controls before the targets). | | `params` | `tuple[Parameter, ...]` | The angle expressions, one for each rotation parameter. Each one is a Constant, a WeightRef, an InputRef, or an arithmetic combination of them. | | `data` | `tuple` | The constant arrays of the gates that carry data (AmplitudeEncoding, QubitUnitary and ControlledQubitUnitary). They are stored as nested tuples so that the gate can still be hashed. The field is empty for every other gate. \_array_to_data and \_data_to_array convert between arrays and this form. | | `adjoint` | `bool` | If True, the conjugate transpose of the gate is applied instead of the gate itself. It is used only for AmplitudeEncoding gates, inside Grover iterates. The adjoint of a QubitUnitary is computed in the intermediate representation instead, by taking the conjugate transpose of the stored matrix, so this flag is never set for it. | ### `InputRef` Bases: `_ParamOps` A placeholder for the input feature at `index` in the input vector. Attributes: | Name | Type | Description | | ------- | ----- | ---------------------------------------------------------------------------------------------------------------- | | `index` | `int` | The position in the input vector of the circuit. assemble shifts it in the same way as the indices of WeightRef. | ### `Multiply` Bases: `_ParamOps` The product of two parameter expressions. It is used when a gate angle is the product of two symbolic terms. The most common case is a trainable scaling of a feature (Schuld et al. 2021): `RY(λ * x)`, where `λ` is a `WeightRef` and `x` is an `InputRef`. When one of the two factors is a constant number, use `Scale` instead, so that backends can see the number directly. Backends that do not support the product of two symbolic terms raise `NotImplementedError` when this node appears. Currently, this is the case of the CUDA Quantum backend. Attributes: | Name | Type | Description | | ---- | ------------- | ----------------- | | `a` | `'Parameter'` | The left factor. | | `b` | `'Parameter'` | The right factor. | ### `Negate` Bases: `_ParamOps` The negation of another expression: `-expression`. The `adjoint` transform builds this node when it reverses a rotation. When the angle is a `Constant` or a `Scale`, it changes the sign of the number instead, and when the angle is already a `Negate`, it removes the negation. Attributes: | Name | Type | Description | | ------------ | ------------- | ------------------------- | | `expression` | `'Parameter'` | The expression to negate. | ### `Observable` A Hermitian operator to measure, written as a sum of Pauli terms. You can build one with the `z`, `x` and `y` methods for a single Pauli operator on one qubit, with `from_string` for a whole sum, or with the `X`, `Y` and `Z` functions of `eigenkit.core.observables`, which support arithmetic. Attributes: | Name | Type | Description | | ------- | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `terms` | `tuple[PauliTerm, ...]` | The Pauli terms of the sum. It can be empty: jordan_wigner returns an empty observable, together with an offset, when the operator reduces to a multiple of the identity. However, Circuit refuses to measure an empty observable, because there is nothing to measure. | #### `from_string(text)` Parse a sum of Pauli terms written as a string. Examples of such strings are `"0.5 Z0 X1 + 0.3 Y2"` and `"-Z0 + Z1"`. The grammar is as follows. The terms are joined by `+` or `-`. Each term starts with an optional numeric coefficient, in plain or scientific notation (`1e-3` is accepted). The coefficient is followed by an optional `*` and by one or more `[XYZ]` operators on distinct qubits. Any amount of whitespace is accepted, including none. Every part of the string must be a valid term, and every term must contain Pauli operators. A constant term (`"0.5 + Z0"`, `"0.5 I"`) raises an error instead of being dropped from the sum: an `Observable` holds only Pauli terms, and the constant must be kept next to it as an offset. Parameters: | Name | Type | Description | Default | | ------ | ----- | -------------------- | ---------- | | `text` | `str` | The string to parse. | *required* | Returns: | Type | Description | | -------------- | ---------------------- | | `'Observable'` | The parsed observable. | Raises: | Type | Description | | ------------ | ------------------------------------------------------------------------------------------------------------------------------ | | `ValueError` | If text is empty, if it contains a constant term, if a part of it is not a valid term, or if a term uses the same qubit twice. | #### `x(qubit)` Return the observable `X` on the single qubit `qubit`. Parameters: | Name | Type | Description | Default | | ------- | ----- | ----------------------- | ---------- | | `qubit` | `int` | The index of the qubit. | *required* | #### `y(qubit)` Return the observable `Y` on the single qubit `qubit`. Parameters: | Name | Type | Description | Default | | ------- | ----- | ----------------------- | ---------- | | `qubit` | `int` | The index of the qubit. | *required* | #### `z(qubit)` Return the observable `Z` on the single qubit `qubit`. Parameters: | Name | Type | Description | Default | | ------- | ----- | ----------------------- | ---------- | | `qubit` | `int` | The index of the qubit. | *required* | ### `PauliTerm` A coefficient times a product of Pauli operators on distinct qubits. `PauliTerm(0.5, (("Z", 0), ("X", 1)))` is the term `0.5 Z0 X1`. Attributes: | Name | Type | Description | | -------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `coeff` | `float` | The real coefficient. Complex values are rejected, because an Observable represents only Hermitian operators. | | `paulis` | `tuple[tuple[str, int], ...]` | Pairs of a Pauli letter ("X", "Y" or "Z") and a qubit index. It must not be empty, because a term without Pauli operators is a constant, which an Observable does not hold. Each qubit can appear at most once, so reduce products such as Z * Z = I before building the term. | Raises: | Type | Description | | ------------ | --------------------------------------------------------------------------------------------- | | `TypeError` | If coeff is complex. | | `ValueError` | If paulis is empty, if a Pauli letter is not X, Y or Z, or if a qubit appears more than once. | ### `Scale` Bases: `_ParamOps` A constant multiple of another expression: `factor * expression`. Use it instead of `Multiply` when one of the two factors is a plain number. A backend can then include the constant factor in the gate angle, instead of treating it as a second symbolic operand. Attributes: | Name | Type | Description | | ------------ | ------------- | ------------------------ | | `expression` | `'Parameter'` | The expression to scale. | | `factor` | `float` | The constant factor. | ### `Sum` Bases: `_ParamOps` The sum of several expressions. Attributes: | Name | Type | Description | | ------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `expressions` | `tuple['Parameter', ...]` | The terms to add. There must be at least one. An empty Sum is not evaluated as 0: resolve and the backends start from the first term, so they raise an IndexError on it. | ### `WeightRef` Bases: `_ParamOps` A placeholder for the trainable weight at `index` in the weight vector. Attributes: | Name | Type | Description | | ------- | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `index` | `int` | The position in the weight vector of the circuit. Each block numbers the weights of its own fragment from 0, and assemble shifts these indices to their positions in the whole circuit. | ### `adjoint(circuit)` Reverse the order of the gates and invert each gate. The transform does not depend on the provider. It returns another `CircuitSpec` that any backend can compile, so the transform does not have to be written again for each backend. Each type of gate is inverted as follows: - **Rotations with parameters** (`RX`, `RY`, `RZ`, `PhaseShift`, `CRY`, `CPHASE`): the angle parameters are negated. - **Gates without parameters** (`H`, `X`, `Z`, `CNOT`, `SWAP`, `MCZ`, `MCX`): they are copied unchanged, because applying them twice gives the identity. `S` and `Sdg` are not their own inverse, so each one is replaced by the other. - **`QubitUnitary(U)`**: the stored matrix is replaced by its conjugate transpose `U†`. It is computed without NumPy, so that the intermediate representation stays pure Python. - **`ControlledQubitUnitary(U)`**: the target unitary (`data[0]`) is replaced by its conjugate transpose, and the control values (`data[1]`) are kept unchanged. - **`AmplitudeEncoding`**: the transform flips `gate.adjoint` and leaves the inversion to the backend. PennyLane, for example, applies the conjugate transpose of the state-preparation unitary. The Qiskit backend does not support this adjoint: `Circuit` rejects such a circuit with a `ValueError`, and the backend itself raises `NotImplementedError`. Parameters: | Name | Type | Description | Default | | --------- | ------------- | ---------------------- | ---------- | | `circuit` | `CircuitSpec` | The circuit to invert. | *required* | Returns: | Type | Description | | ------------- | ------------------------------------------------------ | | `CircuitSpec` | The inverted circuit, with the gates in reverse order. | ### `chain_factor(parameter, weight_index, weights, inputs)` Compute the partial derivative of `parameter` with respect to one weight. The derivative with respect to `weights[weight_index]` is evaluated numerically at the given `weights` and `inputs`. The parameter-shift Jacobian that applies the chain rule uses it to turn the gradient with respect to a gate angle into its contribution to the gradient with respect to one weight. Parameters: | Name | Type | Description | Default | | -------------- | ----------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | | `parameter` | `Parameter` | The angle expression to differentiate. | *required* | | `weight_index` | `int` | The index of the weight to differentiate with respect to. | *required* | | `weights` | \`ndarray | Sequence[float]\` | The weight vector. It can be any container that supports indexing: a NumPy array, a PyTorch tensor or a list. | | `inputs` | \`ndarray | Sequence[float]\` | The input vector. It can be any container that weights can be. | Returns: | Type | Description | | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `float` | The partial derivative. For a single sample, it is a float. When inputs is a batch, transposed so that inputs[j] holds feature j of every sample, and the expression contains a Multiply, it can be an array with one value per sample. | ### `chain_factor_input(parameter, input_index, weights, inputs)` Compute the partial derivative of `parameter` with respect to one input. The derivative with respect to `inputs[input_index]` is evaluated numerically at the given `weights` and `inputs`. This function is the counterpart of `chain_factor` for the inputs. Parameters: | Name | Type | Description | Default | | ------------- | ----------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | | `parameter` | `Parameter` | The angle expression to differentiate. | *required* | | `input_index` | `int` | The index of the input to differentiate with respect to. | *required* | | `weights` | \`ndarray | Sequence[float]\` | The weight vector. It can be any container that supports indexing: a NumPy array, a PyTorch tensor or a list. | | `inputs` | \`ndarray | Sequence[float]\` | The input vector. It can be any container that weights can be. | Returns: | Type | Description | | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `float` | The partial derivative. For a single sample, it is a float. When inputs is a batch, transposed so that inputs[j] holds feature j of every sample, and the expression contains a Multiply, it can be an array with one value per sample. | ### `collect_input_dependencies(parameter)` Return the set of input indices that a parameter expression depends on. This function is the counterpart of `collect_weight_dependencies` for the inputs. The parameter-shift Jacobian that applies the chain rule uses it to find the gates that contribute to the gradient with respect to a given input. Parameters: | Name | Type | Description | Default | | ----------- | ----------- | -------------------------------- | ---------- | | `parameter` | `Parameter` | The angle expression to inspect. | *required* | Returns: | Type | Description | | ---------- | ---------------------------------------------------- | | `set[int]` | The indices of the inputs that the expression reads. | ### `collect_weight_dependencies(parameter)` Return the set of weight indices that a parameter expression depends on. Parameters: | Name | Type | Description | Default | | ----------- | ----------- | -------------------------------- | ---------- | | `parameter` | `Parameter` | The angle expression to inspect. | *required* | Returns: | Type | Description | | ---------- | ----------------------------------------------------- | | `set[int]` | The indices of the weights that the expression reads. | ### `find_input_shift_unsafe_gates(circuit)` Return the gates whose input angle needs the chain-rule path. A gate is included when its angle wraps an `InputRef` in an arithmetic node, or when it uses an input that another angle also uses. `Reuploading` does this when it encodes each feature once per layer. An arithmetic node is a `Scale`, `Multiply`, `Sum` or `Negate`. This function is the counterpart of `find_parameter_shift_unsafe_gates` for the inputs. When the list is empty, the fast path, which shifts each input directly, can be used. Otherwise, the gradients are correct only with the chain-rule path, which shifts single gate angles with `shift_gate_angle`. Parameters: | Name | Type | Description | Default | | --------- | ------------- | ----------------------- | ---------- | | `circuit` | `CircuitSpec` | The circuit to inspect. | *required* | Returns: | Type | Description | | -------------- | ---------------------------------------- | | `list['Gate']` | The gates that need the chain-rule path. | ### `find_parameter_shift_unsafe_gates(circuit)` Return the gates whose weight angle needs the chain-rule path. A gate is included when its angle wraps a `WeightRef` in an arithmetic node, which is a `Scale`, `Multiply`, `Sum` or `Negate`, or when it uses a weight that another angle also uses. `Circuit` reads this list to choose how to compute gradients. A circuit without such gates takes the fast path, which applies the parameter-shift rule directly to each weight. A circuit with such gates takes the chain-rule path. Two examples are `Reuploading(trainable_scale=True)`, which builds `Multiply(weight, input)`, and a builder circuit that uses one weight in two gates. Parameters: | Name | Type | Description | Default | | --------- | ------------- | ----------------------- | ---------- | | `circuit` | `CircuitSpec` | The circuit to inspect. | *required* | Returns: | Type | Description | | -------------- | ---------------------------------------- | | `list['Gate']` | The gates that need the chain-rule path. | ### `inputs_needing_four_term_shift(circuit)` Return the inputs that the two-term parameter-shift rule cannot differentiate. This function is the counterpart of `weights_needing_four_term_shift` for the inputs. Parameters: | Name | Type | Description | Default | | --------- | ------------- | ----------------------- | ---------- | | `circuit` | `CircuitSpec` | The circuit to analyse. | *required* | Returns: | Type | Description | | ---------------- | ------------------------------------------------------- | | `frozenset[int]` | The indices of the inputs that need the four-term rule. | ### `negate_parameter(parameter)` Negate a parameter expression of any kind, symbolically. Parameters: | Name | Type | Description | Default | | ----------- | ----------- | ------------------------------- | ---------- | | `parameter` | `Parameter` | The angle expression to negate. | *required* | Returns: | Type | Description | | ----------- | ----------------------- | | `Parameter` | The negated expression. | ### `resolve(parameter, weights, inputs)` Compute the numeric value of a parameter expression, recursively. `weights` and `inputs` can be any container that supports indexing (a NumPy array, a PyTorch tensor or a list). The type of the result follows the type of these containers, so the same function serves both the NumPy and the PyTorch evaluation paths. Parameters: | Name | Type | Description | Default | | ----------- | ----------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------- | | `parameter` | `Parameter` | The angle expression to evaluate. | *required* | | `weights` | \`ndarray | Sequence[float]\` | The weight vector. It can be any container that supports indexing: a NumPy array, a PyTorch tensor or a list. | | `inputs` | \`ndarray | Sequence[float]\` | The input vector. It can be any container that weights can be. | Returns: | Type | Description | | ------- | ------------------ | | `float` | The numeric angle. | ### `shift_gate_angle(circuit, gate_index, shift_value)` Return a copy of the circuit in which the angle of one gate is shifted. In the new `CircuitSpec`, the first parameter of the gate at `gate_index` is replaced by `Sum((original, Constant(shift_value)))`. The parameter-shift Jacobian that applies the chain rule uses it to shift the angle of one gate, without changing the weights or the other gates. The shifts are `±π/2`, and also `±3π/2` for the gates in `MULTI_FREQUENCY_GATES`. Parameters: | Name | Type | Description | Default | | ------------- | ------------- | ------------------------------------------------- | ---------- | | `circuit` | `CircuitSpec` | The circuit to shift. | *required* | | `gate_index` | `int` | The position of the gate in circuit.gates. | *required* | | `shift_value` | `float` | The amount added to the first angle of that gate. | *required* | Returns: | Type | Description | | --------------- | ----------------------------------------------------- | | `'CircuitSpec'` | A new CircuitSpec in which only that gate is shifted. | ### `weights_needing_four_term_shift(circuit)` Return the weights that the two-term parameter-shift rule cannot differentiate. A weight is included when one of the gates that it feeds is in `MULTI_FREQUENCY_GATES`. A weight that feeds more than one gate never uses this direct shift: the circuit takes the chain-rule path, which shifts each gate on its own (see `find_parameter_shift_unsafe_gates`). Parameters: | Name | Type | Description | Default | | --------- | ------------- | ----------------------- | ---------- | | `circuit` | `CircuitSpec` | The circuit to analyse. | *required* | Returns: | Type | Description | | ---------------- | -------------------------------------------------------- | | `frozenset[int]` | The indices of the weights that need the four-term rule. | ## `eigenkit.core.observables` Functions and classes that make observables easy to write. There are three ways to choose what a circuit measures: ```python # Pauli algebra observable = Z(0) * X(1) + 0.5 * Y(2) # class methods, or a string observable = Observable.from_string("Z0 X1 + 0.5 Y2") observable = Observable.z(0) # no observable at all: by default, Circuit measures Z on every qubit ``` `PauliExpr` is a simple builder. It collects terms, each made of a coefficient and a tensor product of Pauli operators, and it converts them to an `Observable` when needed. A product of two operators on the same qubit is simplified when the two operators are equal (`P * P = I`). A product of different Pauli operators on the same qubit (`X * Y = iZ` and so on) needs complex arithmetic and is not supported. ### `PauliExpr` A sum of terms, each a coefficient times a product of Pauli operators. In each term, the Pauli operators act on distinct qubits, so the product is a tensor product. #### `constant` The coefficient of the identity term, or `0.0` when there is none. #### `__init__(terms)` Build an expression from raw terms. Parameters: | Name | Type | Description | Default | | ------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------- | | `terms` | `Iterable[_Term]` | Internal records of the terms. Prefer the X, Y and Z functions and ordinary arithmetic, such as 0.5 * Z(0) + X(1). They build the same expression without creating these records directly. | *required* | #### `single(operator, qubit, coeff=1.0)` Return `coeff` times a single Pauli `operator` on `qubit`. Parameters: | Name | Type | Description | Default | | ---------- | ------- | ------------------------------------ | ---------- | | `operator` | `str` | The Pauli operator: "X", "Y" or "Z". | *required* | | `qubit` | `int` | The index of the qubit. | *required* | | `coeff` | `float` | The coefficient. Default: 1.0. | `1.0` | Returns: | Type | Description | | ------------- | ---------------------------------------- | | `'PauliExpr'` | The expression, which has a single term. | #### `to_observable()` Convert the expression to an `Observable`. The terms with the same Pauli string are combined, and the terms whose coefficient is `0` (at most `1e-15` in magnitude) are dropped. An `Observable` holds only Pauli terms, so a non-zero identity term raises an error. Such a term comes from a scalar offset, or from `P * P = I`. Dropping it without an error would shift every expectation value. To measure such an expression, measure `expr - expr.constant` and add `expr.constant` to the result. The chemistry entry points take this constant as `offset`. Returns: | Type | Description | | ------------ | --------------- | | `Observable` | The observable. | Raises: | Type | Description | | ------------ | ------------------------------------------------------------------------------------------------ | | `ValueError` | If the expression has a constant term that is not 0, or if no term that can be measured is left. | ### `X(qubit)` Return Pauli `X` on `qubit`, as an expression that can be added and scaled. Parameters: | Name | Type | Description | Default | | ------- | ----- | ----------------------- | ---------- | | `qubit` | `int` | The index of the qubit. | *required* | Returns: | Type | Description | | ----------- | --------------- | | `PauliExpr` | The expression. | ### `Y(qubit)` Return Pauli `Y` on `qubit`, as an expression that can be added and scaled. Parameters: | Name | Type | Description | Default | | ------- | ----- | ----------------------- | ---------- | | `qubit` | `int` | The index of the qubit. | *required* | Returns: | Type | Description | | ----------- | --------------- | | `PauliExpr` | The expression. | ### `Z(qubit)` Return Pauli `Z` on `qubit`, as an expression that can be added and scaled. Parameters: | Name | Type | Description | Default | | ------- | ----- | ----------------------- | ---------- | | `qubit` | `int` | The index of the qubit. | *required* | Returns: | Type | Description | | ----------- | --------------- | | `PauliExpr` | The expression. | ### `all_z(n_qubits)` Return one `Z` observable for each qubit. This is useful as a default measurement. Parameters: | Name | Type | Description | Default | | ---------- | ----- | ------------------------------------ | ---------- | | `n_qubits` | `int` | The number of qubits of the circuit. | *required* | Returns: | Type | Description | | ------------------ | ------------------------------ | | `list[Observable]` | One Observable for each qubit. | ### `from_string(text)` Parse an observable from a string. This function is a shortcut for `Observable.from_string`. Parameters: | Name | Type | Description | Default | | ------ | ----- | -------------------- | ---------- | | `text` | `str` | The string to parse. | *required* | Returns: | Type | Description | | ------------ | ---------------------- | | `Observable` | The parsed observable. | ## `eigenkit.core.optimizers` Optimisers that need no gradient, for objectives that are noisy and expensive. `spsa` implements Simultaneous Perturbation Stochastic Approximation (SPSA) (Spall 1992). It estimates the gradient from two evaluations of the objective per iteration, whatever the number of parameters. This is why it is the usual choice when every evaluation is a whole kernel matrix or a paid job on hardware. It uses only NumPy. Example ```python import numpy as np from eigenkit.core.optimizers import spsa result = spsa(lambda x: float(np.sum((x - 1.0) ** 2)), np.zeros(3), maxiter=200) print(np.round(result.x, 1)) # [0.9 0.9 0.9] ``` ### `SPSAResult` Outcome of spsa(). Attributes: | Name | Type | Description | | --------------- | ------------------- | ----------------------------------------------------------------------------- | | `x` | `ndarray` | The final parameters. | | `fun` | `float` | The value of the objective at x. | | `n_iterations` | `int` | The number of iterations run. | | `n_evaluations` | `int` | The number of evaluations of the objective, including the one at the final x. | | `history` | `tuple[float, ...]` | The value of the objective after every iteration, in order. | | `success` | `bool` | Whether the final value of the objective is lower than the starting value. | | `initial_fun` | `float` | The value of the objective at x0. | | `best_x` | \`ndarray | None\` | | `best_fun` | `float` | The value of the objective at best_x. | ### `spsa(fun, x0, *, maxiter=100, learning_rate=0.05, perturbation=0.05, alpha=0.602, gamma=0.101, random_state=0, callback=None)` Minimise `fun` with Simultaneous Perturbation Stochastic Approximation (SPSA). Each iteration draws a random direction `delta` with entries `±1`, evaluates `fun` at `x ± c_k * delta`, and takes a step against the resulting estimate of the gradient. The step size and the perturbation size decrease as `a / (k + 1) ** alpha` and `c / (k + 1) ** gamma`, with the standard exponents of Spall. Parameters: | Name | Type | Description | Default | | --------------- | ----------------------------------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `fun` | `Callable[[ndarray], float]` | The objective to minimise. It maps a 1-D parameter vector to a float. | *required* | | `x0` | `ndarray` | The starting parameters. | *required* | | `maxiter` | `int` | The number of iterations to run. | `100` | | `learning_rate` | \`float | None\` | The initial step size a. None calibrates it from a few gradient estimates at x0, so that the first step moves the parameters by about 2π / 10, as the SPSA of qiskit-algorithms does. These evaluations are counted in n_evaluations. | | `perturbation` | `float` | The initial perturbation size c. | `0.05` | | `alpha` | `float` | The exponent with which the step size decreases. | `0.602` | | `gamma` | `float` | The exponent with which the perturbation size decreases. | `0.101` | | `random_state` | \`int | None\` | The seed for the perturbation directions. | | `callback` | \`Callable\[[int, ndarray, float], None\] | None\` | A function called after every iteration as callback(iteration, x, fun(x)). | Returns: | Type | Description | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------ | | `SPSAResult` | An SPSAResult. The evaluations for the history are extra calls to fun, made so that the run can be plotted, and n_evaluations counts them. | ## `eigenkit.core.provider` The provider protocol, its registry, and the parameter-shift fallback. A provider compiles a circuit separately from running it, which matters for speed. Building a `qml.device` and a QNode (PennyLane), or a parametrised `QuantumCircuit` and a `SparsePauliOp` (Qiskit), takes far longer than one execution. Doing that work once per `Circuit` and reusing the compiled handle for every forward pass is what makes repeated evaluations fast. The `run` and `run_torch` methods of each backend accept batched arrays directly. The first axis is the batch, and `weights` and `inputs` can each be 1-D (one sample) or 2-D (a batch). Backends that can run a batch at once override `run` to submit one job. The others call `batch_call` and pay for a loop in Python, but the loop is inside the backend and not in the `Circuit` that calls it. The `Capabilities.supports_batching` flag only gives information, and nothing chooses a path based on it: `Circuit` always passes the batch on unchanged. Example ```python from eigenkit.core.provider import get, registered_names print(registered_names()) # ['numpy', 'pennylane'] when no extra is installed backend = get("numpy") print(backend.gradient_method) # adjoint print(backend.capabilities.supports_batching) # True ``` ### `Capabilities` The description that a provider gives of what it supports. It is used to check a circuit before compilation, and to choose between code paths at run time. Attributes: | Name | Type | Description | | ------------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `supported_gates` | `frozenset[str]` | The names of the gates that the backend can run. A circuit that uses any other gate is rejected before compilation, with a message that names that gate. | | `supports_batching` | `bool` | Whether the backend evaluates a 2-D inputs batch in one call, instead of looping over it internally. It only gives information: Circuit passes the batch on in both cases and never reads this flag. It describes the strategy of the backend, and nothing chooses a path based on it. | | `supports_shots` | `bool` | Whether the backend can sample, instead of returning exact expectation values. | | `max_qubits` | \`int | None\` | | `supported_adjoint_gates` | \`frozenset[str] | None\` | | `supports_oracle_matrix` | `bool` | Whether the dense unitary of a callable oracle can be extracted. Iterative quantum amplitude estimation (IQAE) uses this to accept oracles that are PennyLane callables. | | `supports_provider_shots` | `bool` | Whether the backend can sample an ancilla qubit with its own shots, which is what IQAE needs for shot_mode="provider". | ### `CompiledCircuit` Bases: `Protocol` A handle to a circuit that a backend has prepared, cheap to call many times. `run` accepts `weights` and `inputs` that are each either 1-D (one sample) or 2-D (a batch along the first axis). The output has shape `(n_obs,)` when both are 1-D, and `(B, n_obs)` otherwise. Each backend decides whether to evaluate the batch at once or in a loop (see `batch_call`). Implementations can also provide `run_torch(...)`, for backends that support PyTorch automatic differentiation themselves. When the `gradient_method` of the backend is `"native"`, the PyTorch interface detects this method with `hasattr` and uses it instead of the parameter-shift fallback. The same shape rule applies. #### `run(weights, inputs)` Evaluate the circuit and return one expectation value for each observable. See the class description for the shapes. Parameters: | Name | Type | Description | Default | | --------- | --------- | ----------------- | --------------------------------------------------------------------------------------------------- | | `weights` | \`ndarray | Sequence[float]\` | The weight vector, in any container that can be indexed: a NumPy array, a PyTorch tensor or a list. | | `inputs` | \`ndarray | Sequence[float]\` | The input vector, in the same kinds of container as weights. | ### `CostWarning` Bases: `UserWarning` Warning issued before a submission to a paid quantum backend. A paid backend is a quantum processing unit (QPU) or a cloud simulator that charges for use. The first compilation for such a device issues this warning, so that costs cannot build up without the user noticing. ### `JobHandle` Bases: `Protocol` A handle to a job running on a quantum processing unit (QPU) or in the cloud. `compiled.submit(...)` returns it without waiting for the job. Call `.result()` to wait for the job and combine its results. #### `result()` Wait until the job finishes, then return its expectation values. The shape follows the same rule as CompiledCircuit.run(): `(n_obs,)` for a single sample, and `(B, n_obs)` for a batch. #### `status()` Return the state of the job as the provider reports it, without waiting. The strings are the provider's own. For example, Braket returns `"CREATED"`, `"QUEUED"`, `"RUNNING"`, `"COMPLETED"`, `"FAILED"` or `"CANCELLED"`. Compare them within one provider, not across providers. ### `Provider` Bases: `Protocol` The protocol that every backend follows. A provider turns a `CircuitSpec` into something that can run. Anything with these members can be passed as `provider=`: the built-in backends, the backend built by a configuration object, or your own class. To select it by a string instead, register it under a short name with `register`. Attributes: | Name | Type | Description | | ----------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | `name` | `str` | A short name, used in error messages and in repr. | | `gradient_method` | `GradientMethod` | How the gradient code should differentiate this backend: "adjoint", "parameter_shift", "native", or "none" for hardware that only returns values. | #### `capabilities` Return what this backend supports, as a `Capabilities` object. It covers the supported gates, shots, batching and the other flags. #### `compile(circuit, observables)` Prepare `circuit` so that `observables` can be evaluated many times. A `Circuit` calls it once, and reuses the returned handle for every call, so the expensive work belongs here. Parameters: | Name | Type | Description | Default | | ------------- | ---------------------- | --------------------------- | ---------- | | `circuit` | `CircuitSpec` | The circuit to prepare. | *required* | | `observables` | `Sequence[Observable]` | The observables to measure. | *required* | ### `batch_call(run_one, weights, inputs)` Call `run_one(w_i, x_i)` for each sample along the first (batch) axis. It is meant for backends that cannot run a batch at once. It holds the loop over the samples, and if one sample fails, it raises a `RuntimeError` that gives the index of that sample, so the caller can find which sample failed. Parameters: | Name | Type | Description | Default | | --------- | ---------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `run_one` | `Callable` | A function that evaluates one sample. | *required* | | `weights` | \`ndarray | Sequence[float]\` | The weights, 1-D for one sample or 2-D for a batch. They must be a NumPy array or a PyTorch tensor, because the batch is detected from ndim and shape. A list is not accepted. | | `inputs` | \`ndarray | Sequence[float]\` | The inputs, 1-D or 2-D, in the same kinds of container as weights. | Returns: | Type | Description | | --------- | ------------------------------------ | | `ndarray` | The results of the samples, stacked. | ### `get(name)` Return the provider registered under `name`. A backend registered with `register_lazy` imports its package at the first lookup, and the backend is kept for later lookups. Parameters: | Name | Type | Description | Default | | ------ | ----- | -------------------------------------------------------- | ---------- | | `name` | `str` | A registered short name, such as "numpy" or "pennylane". | *required* | Returns: | Type | Description | | ---------- | ---------------------- | | `Provider` | The provider instance. | Raises: | Type | Description | | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ImportError` | If name is a registered backend whose package is not installed, or is installed but fails to import. For a missing package the message names the extra that installs it. The registration is kept, so a later call works once the package is installed. | | `KeyError` | If nothing is registered under name. The message lists the available names and, for a known backend, the extra that installs it. | ### `load_failure_hint(exc)` Name the fix for a package that is installed but fails to load, if known. Parameters: | Name | Type | Description | Default | | ----- | ------------- | ------------------------------------------------ | ---------- | | `exc` | `ImportError` | The ImportError that loading the package raised. | *required* | Returns: | Type | Description | | ----- | ---------------------------------------------------------- | | `str` | A parenthesised hint to append to an error message, or "". | ### `parameter_shift_input_jacobian(compiled, weights, inputs, four_term_indices=None)` Return the jacobian with respect to the inputs, by shifting each input. Each entry is `d/dx_j`, computed by shifting `inputs[j]` directly by `±π/2` (and also by `±3π/2` for an input listed in `four_term_indices`). The result is correct only when the angle of every gate that depends on an input is exactly `InputRef(j)`, not wrapped in `Scale`, `Multiply`, `Sum` or `Negate`, and when each `InputRef(j)` feeds a single gate. When that is not the case, use the chain-rule path instead. This simple path does not use `shift_gate_angle` (which wraps the angle in a `Sum`), so it works on backends that do not support symbolic `Sum` expressions, such as CUDA Quantum. Parameters: | Name | Type | Description | Default | | ------------------- | ----------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `compiled` | `CompiledCircuit` | The prepared circuit to differentiate. | *required* | | `weights` | \`ndarray | Sequence[float]\` | The weight vector, in any container that compiled.run accepts: a NumPy array, a PyTorch tensor or a list. | | `inputs` | \`ndarray | Sequence[float]\` | The input vector, as a NumPy array or a list. A PyTorch tensor is not accepted, because the vector is shifted on a copy made with .copy(). | | `four_term_indices` | \`frozenset[int] | None\` | The inputs that feed a gate with several frequencies, as returned by ir.inputs_needing_four_term_shift. | Returns: | Type | Description | | --------- | ------------------------------------------------- | | `ndarray` | The jacobian, of shape (n_observables, n_inputs). | ### `parameter_shift_jacobian(compiled, weights, inputs, four_term_indices=None)` Return the jacobian with respect to the weights, by the parameter-shift rule. Each entry is `d/dθ_i`, computed with the parameter-shift rule. For a weight whose gates have a single frequency, the rule takes two evaluations, `(f(θ + π/2) - f(θ - π/2)) / 2`. For a weight listed in `four_term_indices`, it uses the four-term rule. Parameters: | Name | Type | Description | Default | | ------------------- | ----------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `compiled` | `CompiledCircuit` | The prepared circuit to differentiate. | *required* | | `weights` | \`ndarray | Sequence[float]\` | The weight vector, as a NumPy array or a list. A PyTorch tensor is not accepted, because the vector is shifted on a copy made with .copy(). | | `inputs` | \`ndarray | Sequence[float]\` | The input vector, in any container that compiled.run accepts: a NumPy array, a PyTorch tensor or a list. | | `four_term_indices` | \`frozenset[int] | None\` | The weights that feed a gate with several frequencies, as returned by ir.weights_needing_four_term_shift. The two-term rule is not exact for those weights (on a controlled rotation it is wrong by tens of percent), so leaving them out here returns wrong gradients, not an error. | Returns: | Type | Description | | --------- | -------------------------------------------------- | | `ndarray` | The jacobian, of shape (n_observables, n_weights). | ### `register(name, provider_or_config)` Register a backend, or a configuration that can build one, under `name`. Later calls to `Circuit(..., provider=name)` then use that backend. If a configuration is passed, its `make_backend()` method is called to build the backend. Parameters: | Name | Type | Description | Default | | -------------------- | ---------- | --------------------- | -------------------------------------------------------------------- | | `name` | `str` | The name to register. | *required* | | `provider_or_config` | \`Provider | object\` | A backend instance, or a configuration with a make_backend() method. | ### `register_lazy(name, loader)` Register a backend under `name` without importing its package yet. `loader` is called the first time the name is looked up, and its result is kept for later lookups. The optional backends are registered in this way, so that naming them imports nothing. It replaces whatever `name` currently refers to, as `register` does. If the registration were skipped when the name was already taken, a caller could not replace a built-in backend with a lazy one, and would get no sign that the loader had been ignored. Parameters: | Name | Type | Description | Default | | -------- | ------------------------ | --------------------------------------------------------------------------------------------------------- | ---------- | | `name` | `str` | The name to register. | *required* | | `loader` | `Callable[[], Provider]` | A function with no arguments that returns the backend. It is called the first time the name is looked up. | *required* | ### `registered_names()` Return every provider name that `get` accepts, including the lazy ones. ### `resolve_provider(spec)` Turn a provider (a name, a configuration or a backend) into a backend instance. A string is looked up in the registry. Any object with a `make_backend()` method is treated as a configuration. A backend is used as it is. Parameters: | Name | Type | Description | Default | | ------ | ----- | ----------- | ---------- | | `spec` | \`str | object | Provider\` | Returns: | Type | Description | | ---------- | --------------------- | | `Provider` | The backend instance. | ### `validate_circuit(circuit, capabilities)` Raise a `ValueError` if the circuit cannot run on the provider. The message says clearly why: the circuit uses a gate, or the adjoint of a gate, that the provider does not support, or it needs more qubits than the provider has. Parameters: | Name | Type | Description | Default | | -------------- | -------------- | ----------------------------------- | ---------- | | `circuit` | `CircuitSpec` | The circuit to check. | *required* | | `capabilities` | `Capabilities` | What the provider says it supports. | *required* | ### `validate_initial_state(initial_state, n_qubits, *, batch=None)` Check and flatten a statevector that the user gives as a starting state. It returns a contiguous `complex128` array in the flat order of the library, where qubit `0` is the most significant bit. The shape is `(2**n,)` when `batch` is `None`, and `(batch, 2**n)` otherwise. A 1-D state passed with a `batch` is repeated for every row of the batch. It raises a `ValueError` if the length is wrong or the state is not normalised. A unitary evolution keeps the norm, so a state that is not normalised is almost always a mistake of the caller. Each backend reshapes the returned flat array into its own state layout. Doing the checks here, in one place, keeps the error messages the same for the NumPy, PennyLane and Qiskit backends. Parameters: | Name | Type | Description | Default | | --------------- | --------- | ------------------------------------ | ------------------------------------------------------------------------------------ | | `initial_state` | `ndarray` | The statevector to start from. | *required* | | `n_qubits` | `int` | The number of qubits of the circuit. | *required* | | `batch` | \`int | None\` | The batch size that the state must match, or None for a single state. Default: None. | Returns: | Type | Description | | --------- | -------------------------------------------- | | `ndarray` | The flattened statevector, after the checks. | ## `eigenkit.core.secret_fields` Secret configuration fields, which are kept in memory and never saved. A configuration dataclass marks a field that holds a credential with `metadata={"secret": True}`, and with `repr=False`, so that the field stays out of `repr()`, `str()` and the `metadata.json` of a saved model. Two things respect the mark: - `SecretFieldsMixin` makes pickling drop those fields. joblib checkpoints and process pools also use pickle, so they drop them as well. Copies inside one process (`copy.copy`, `copy.deepcopy`, and therefore `sklearn.base.clone`) keep them, so a grid search can still authenticate. - The `.eigenkit` model serialiser writes a configuration with `dataclasses.asdict()`, which ignores both `repr=False` and the pickle hooks, so it explicitly drops every name in `secret_field_names()`. After a load, a secret field holds its default value (usually `None`), and the backend falls back to its own way of finding credentials, usually an environment variable. Example ```python import pickle from dataclasses import dataclass, field from eigenkit.core.secret_fields import ( SecretFieldsMixin, secret_field_names, ) @dataclass(frozen=True) class MyConfig(SecretFieldsMixin): device: str = "sim" token: str | None = field(default=None, repr=False, metadata={"secret": True}) cfg = MyConfig(token="s3cr3t") print(secret_field_names(cfg)) # frozenset({'token'}) print(cfg) # MyConfig(device='sim') print(pickle.loads(pickle.dumps(cfg)).token) # None ``` ### `SecretFieldsMixin` A mixin for frozen configuration dataclasses that hold credentials. Pickling (and therefore joblib) drops every secret field, and unpickling gives each of them its default value. Copying (`copy.copy`, `copy.deepcopy`, `sklearn.base.clone`) keeps every field, including the secret ones. Mark a field secret with `field(default=None, repr=False, metadata={"secret": True})`, and put the mixin first among the bases of a `@dataclass(frozen=True)` class. #### `__copy__()` Return a shallow copy that keeps the secret fields. #### `__deepcopy__(memo)` Return a deep copy that keeps the secret fields. #### `__getstate__()` Return every field that is not secret, for pickle. #### `__setstate__(state)` Restore the pickled fields and set every secret field to its default. ### `secret_field_names(config)` Return the names of the dataclass fields that are marked secret. A field is secret when the `"secret"` entry of its metadata is true. Parameters: | Name | Type | Description | Default | | -------- | ----- | ------------------------------------------------------ | ---------- | | `config` | `Any` | The dataclass instance, or dataclass type, to inspect. | *required* | Returns: | Type | Description | | ---------------- | ----------------------------------------------------------------------------- | | `frozenset[str]` | The names of the secret fields. The set is empty when the dataclass has none. | Raises: | Type | Description | | ----------- | ----------------------------- | | `TypeError` | If config is not a dataclass. | ## `eigenkit.core.solver` The base class every quantum solver inherits. It lives in `core` rather than in `algorithms` because the four solvers that inherit it sit in three different packages, and `optimization` and `chemistry` depend on `core` alone. In `algorithms`, inheriting it would make those two packages depend on `algorithms`, which they otherwise never touch. ### `BaseQuantumSolver` Bases: `ABC` Base class for quantum solvers that compute a result in a single call. Unlike BaseQuantumEstimator, a solver has no training phase and no fitted state, and it does not follow the scikit-learn estimator interface. It receives a mathematical problem directly in `solve()` and returns a structured result object. Four classes inherit from it: HHL, IQAE, QAOA and VQE. PortfolioRiskEstimator does not, by design. It is a scikit-learn-style estimator with `fit()`, `estimate()` and `var_at()`, and it has no `solve()` method. As a result, `isinstance(x, BaseQuantumSolver)` tells you whether an object can be given a problem directly, and it returns `False` for the estimator. Each subclass sets its own `provider` default in its signature and passes it to this class. All of them use `"numpy"`. Two of the modes of `IQAE`, a callable oracle and `shot_mode="provider"`, need `provider="pennylane"`. Parameters: | Name | Type | Description | Default | | ---------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | | `provider` | `ProviderSpec` | The backend to use: a registered name ("numpy", "pennylane", "qiskit" and so on), a config instance, or a backend object. The default is "numpy", the built-in simulator that the core installation includes. | `'numpy'` | Example ```python from eigenkit.core import BaseQuantumSolver from eigenkit.optimization import QAOA isinstance(QAOA(), BaseQuantumSolver) # True ``` #### `solve(*args, **kwargs)` Compute and return the solution. Each subclass declares the signature that its problem needs. `HHL` takes a matrix and a right-hand side, `IQAE` takes an oracle, `QAOA` takes a problem, and `VQE` takes a Hamiltonian and the number of qubits. ## `eigenkit.core.variational` Variational optimisation: minimise `(θ)` over the parameters of a circuit. This is the training engine, independent of any framework, that the Variational Quantum Eigensolver (VQE) in `chemistry`, the quantum approximate optimisation algorithm (QAOA) in `optimization`, and any workflow of the form "prepare a parametrised state, measure ``, optimise `θ`" share. It uses only NumPy and SciPy. It runs the NumPy forward pass of the core `Circuit` (`compiled.run`) and its analytic NumPy gradients (`_gradient_jacobians`: the backend's own gradient when it has one, such as the adjoint method of the NumPy simulator, and the parameter-shift rule otherwise). So it runs on **any** backend (NumPy, PennyLane, Qiskit, Braket, CUDA Quantum) without PyTorch. Even on PennyLane it uses the NumPy path, not the PyTorch QNode. SciPy is imported only at the first call. Backends that cannot differentiate (`gradient_method="none"`, for example a quantum processing unit (QPU) whose configuration leaves `enable_gradients` off) must use an optimiser that needs no gradient, such as `"COBYLA"`, `"Nelder-Mead"`, `"Powell"` or `"spsa"`. These only need the forward evaluation. ### `VariationalResult` Bases: `ValueEquality` Outcome of minimize_expectation(). The result is read-only. Assigning to a field raises `FrozenInstanceError`, and writing into an array field raises `ValueError`. This prevents a mistyped `=` (instead of `==`) or an in-place edit from leaving a result whose scalar fields describe a different run from the array it holds. To get a writable copy of the array, use `np.array(result.field)`. Results compare by value, including their arrays. They are not hashable, because a result is a collection of numbers and not a key, and a correct hash of a float array is expensive to compute. Attributes: | Name | Type | Description | | -------------------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `optimal_value` | `float` | The lowest expectation value reached. When the observable is a Hamiltonian, it is the estimated ground-state energy. | | `optimal_parameters` | `ndarray` | The weights that produced this value, of shape (n_weights,). | | `n_iterations` | `int` | The number of iterations reported by the SciPy optimiser. When there is no such count (a custom callable, "spsa", or a SciPy method that does not report one), it is the number of cost evaluations, as in n_evaluations. | | `n_evaluations` | `int` | The number of circuit evaluations that the optimiser made. With a SciPy method it is usually higher than n_iterations, because a line search costs several evaluations per step. Evaluations made only for bookkeeping are not counted, so a custom optimiser that never called the cost function reports 0. | | `success` | `bool` | Whether the run converged. A SciPy optimiser reports it. A custom callable reports it only if it returns (parameters, converged), and otherwise it is inferred from whether the final cost is lower than the starting cost. A False with a plausible energy usually means that the maximum number of iterations was reached. | | `message` | `str` | The termination message of the optimiser. For a custom callable that reported nothing, it gives the cost before and after the run. | | `history` | `tuple[float, ...]` | The expectation value at every evaluation, in order. Plot it to see whether the run converged or stalled. It is a tuple, so nothing can be appended to it after the run. | #### `__post_init__()` Make the array field read-only. Freezing the dataclass prevents a field from being reassigned, but it does not prevent writing into the array that the field holds, and a result that has been written into still reports the scalar values of the original run. The result therefore keeps its own copy, so the array it was built from can still be changed by the caller. To get a writable copy, use `np.array(result.optimal_parameters)`. ### `minimize_expectation(circuit, *, initial_parameters=None, optimizer='L-BFGS-B', max_iterations=None, tol=None, random_state=42, callback=None)` Minimise `(θ)` for a circuit whose parameters are all variational. Parameters: | Name | Type | Description | Default | | -------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | `circuit` | `Circuit` | A core NumPy Circuit whose single observable is the Hamiltonian H and whose spec.n_inputs == 0 (every parameter is a variational weight). It works with any provider. | *required* | | `initial_parameters` | \`ndarray | None\` | The starting θ, of shape (n_weights,). When it is not given, it is drawn at random with the seed random_state. | | `optimizer` | `Optimizer` | The name of a scipy.optimize.minimize method (the default is "L-BFGS-B"), "spsa" for spsa(), or a callable optimizer(cost, theta0, gradient) -> parameters for a custom or framework optimiser. SPSA needs no gradient: each iteration evaluates the circuit twice for the gradient estimate and once more to record the history. A callable can instead return (parameters, converged) to report convergence itself. Otherwise, success is inferred from whether the final cost is lower than the starting cost, because a vector alone says nothing about convergence. | `'L-BFGS-B'` | | `max_iterations` | \`int | None\` | The maximum number of iterations. It is passed to SciPy as options={"maxiter": ...}, and to spsa() as maxiter, which is 100 when max_iterations is None. | | `tol` | \`float | None\` | The tolerance, passed to scipy.optimize.minimize. | | `random_state` | \`int | None\` | The seed for the random starting point, which also seeds the perturbations of "spsa". None draws an unseeded starting point. | | `callback` | \`Callable\[[ndarray], None\] | None\` | An optional function called after every iteration with the current parameters. It is passed to SciPy, and "spsa" also calls it. | Returns: | Type | Description | | ------------------- | ------------------------------------------------------------------------------------------------------------ | | `VariationalResult` | A VariationalResult with the optimal value, the parameters, and the history of the cost at every evaluation. | Example ```python from eigenkit import Circuit, Variational from eigenkit.core.ir import Observable from eigenkit.core.variational import minimize_expectation # One qubit, one trainable rotation, H = Z. The minimum is -1. circuit = Circuit( blocks=[Variational(depth=1)], observables=Observable.from_string("Z0"), provider="numpy", ) result = minimize_expectation(circuit, random_state=42) print(round(result.optimal_value, 6)) # -1.0 print(result.success, result.n_iterations) # True 4 ``` ## `eigenkit.core.wire` A stable JSON form for circuits and observables. The functions of this module convert a `CircuitSpec` and its observables to plain JSON types (`dict`, `list`, `str`, `int`, `float`, `bool` and `None`), and back. `.eigenkit` bundles use the same encoding, so a change to it would break the saved models. Example ```python import json from eigenkit.core import wire from eigenkit.core.blocks import AngleEncoding, BasicVariational, assemble from eigenkit.core.observables import all_z spec = assemble([AngleEncoding(in_features=2), BasicVariational(depth=1)]) payload = json.dumps( { "circuit": wire.circuit_spec_to_json(spec), "observables": wire.observables_to_json(all_z(2)), } ) data = json.loads(payload) assert wire.circuit_spec_from_json(data["circuit"]) == spec assert wire.observables_from_json(data["observables"]) == all_z(2) ``` ### `circuit_spec_from_json(data)` Decode a circuit encoded by `circuit_spec_to_json`. Parameters: | Name | Type | Description | Default | | ------ | ------ | ----------------------------------------------------------- | ---------- | | `data` | `dict` | The encoded circuit, for example as returned by json.loads. | *required* | Returns: | Type | Description | | ------------- | ----------------------------------------------- | | `CircuitSpec` | The circuit, equal to the one that was encoded. | ### `circuit_spec_to_json(spec)` Encode a circuit as a dictionary that can be saved as JSON. Parameters: | Name | Type | Description | Default | | ------ | ------------- | ---------------------- | ---------- | | `spec` | `CircuitSpec` | The assembled circuit. | *required* | Returns: | Type | Description | | ------ | -------------------------------------------------------------------------------------------------- | | `dict` | A dictionary with the keys n_qubits, n_weights, n_inputs and gates, made of plain JSON types only. | ### `observables_from_json(data)` Decode observables encoded by `observables_to_json`. Parameters: | Name | Type | Description | Default | | ------ | ------ | ----------- | --------------------------------------------------------------- | | `data` | \`list | dict\` | The encoded observables, for example as returned by json.loads. | Returns: | Type | Description | | ------------ | ------------------ | | \`Observable | list[Observable]\` | ### `observables_to_json(observables)` Encode one observable, or a list of them, as data that can be saved as JSON. Each observable becomes `{"terms": [{"coeff": c, "paulis": [[op, q], ...]}]}`. A coefficient that is a Python `int` or `float` is written unchanged. A NumPy integer becomes an `int`, and any other real number (a NumPy float or a `Fraction`, for example) becomes a `float`, so the output is plain JSON for any encoder. Parameters: | Name | Type | Description | Default | | ------------- | ------------ | ---------------------- | ------------------------------------------------------- | | `observables` | \`Observable | Sequence[Observable]\` | A single observable, or a list or tuple of observables. | Returns: | Type | Description | | ------ | ----------- | | \`list | dict\` | Raises: | Type | Description | | ----------- | --------------------------------------------------- | | `TypeError` | If a coefficient is a bool or is not a real number. | ## `eigenkit.core.backends.braket_backend` The Amazon Braket backend. The `amazon-braket-sdk` package is an optional dependency (`pip install eigenkit[braket]`). It is imported only when it is needed, so the rest of the library works without it. If it is missing, `compile()` raises an `ImportError` with a clear message. `BraketConfig` selects one of three modes: - The local simulator, the default, with `device_arn=None`. The circuit runs on `LocalSimulator`, in the same process. Exact values (`shots=0`) are supported. Gradients use the general parameter-shift method. - A cloud simulator, when `device_arn` names SV1, DM1 or TN1. Amazon Web Services bills each task, and a single `CostWarning` is issued when the circuit is compiled. - A quantum processing unit (QPU), when `device_arn` contains `/qpu/` (Rigetti, IonQ, IQM, AQT and others). `shots > 0` is required. Gradients are turned off (`gradient_method="none"`), and calling `.backward()` raises an error whose message recommends training on a simulator and then using the weights on the device. `enable_gradients=True` turns on parameter-shift gradients on the QPU, and compiling then issues a `CostWarning` that gives the number of tasks of one backward pass. Both cloud modes can submit without waiting, through `submit()`, which returns a `BraketJobHandle`. Its `result()` waits for the task, and its `status()` checks the task without waiting. `submit()` can run `run`, `sample`, `probabilities` or `zero_state_probability` (see `BraketCompiled.submit_methods`), and it can add Amazon Web Services (AWS) tags to the task. `attach()` rebuilds a handle from the Amazon Resource Name (ARN) of a task submitted earlier, for example after the process that submitted it restarted. `sample` reads the measurement counts of the task, so it needs `shots > 0`. `probabilities` and `zero_state_probability` read the first result type that the device publishes for the configured shots, in the order `Probability`, `StateVector`, `Amplitude`, `DensityMatrix` (`zero_state_probability` skips `StateVector`). With `shots > 0`, that is always `Probability`. At `shots=0`, the managed simulators publish no `Probability`, so SV1 gives the exact answer through `Amplitude`, and DM1 through `DensityMatrix`. The local simulator uses `Probability` at any number of shots. Three of these paths are limited to the number of qubits that Braket accepts, and wider circuits are refused before any task is created. On a cloud device, `Probability` covers at most 15 qubits ([List of result types](https://docs.aws.amazon.com/braket/latest/developerguide/braket-result-types.html)), so both methods stop there with `shots > 0`. On the local simulator, only the memory limits `Probability`. At `shots=0`, `probabilities` on SV1 lists every basis state in the program, so it stops at 17 qubits (because of the program limit of 5 megabytes), and DM1 returns a density matrix of at most 8 qubits. `sample` has no limit. `compile()` builds a parametric `braket.circuits.Circuit`, whose gates use `FreeParameter` symbols, and adds one expectation result type for each distinct Pauli term of all the observables. With `shots=0`, one circuit measures every term. With `shots > 0`, one task can measure only terms that commute qubit-wise, so the terms are split into such groups, with one circuit, and one task per call, for each group. Each `run` or `submit` sets the parameter values through the `inputs={...}` dictionary of the Braket package. Example ```python from eigenkit import Circuit, Encoding, Variational, all_z blocks = [Encoding(in_features=4), Variational(depth=2)] # By registered name. circuit = Circuit(blocks=blocks, observables=all_z(4), provider="braket") # Or with the configuration, when a device setting matters. from eigenkit.configs import BraketConfig circuit = Circuit( blocks=blocks, observables=all_z(4), provider=BraketConfig( device_arn="arn:aws:braket:::device/quantum-simulator/amazon/sv1" ), ) ``` Without a `device_arn`, this is the local simulator. With one, the circuit is submitted to AWS. The ARN of a QPU turns gradients off (unless `enable_gradients=True`) and requires shots. ### `BraketBackend` Provider for Amazon Braket, registered as `"braket"`. It runs on the local simulator by default, or on a cloud simulator or a quantum processing unit (QPU) once `device_arn` is set in the configuration. This class is internal: you select the backend with `provider="braket"` or by passing a BraketConfig, not by creating this class. #### `capabilities` What this backend supports. It supports every gate except `AmplitudeEncoding`, `CPHASE` and `ControlledQubitUnitary`, which this backend does not translate into Braket circuits. It can sample with a finite number of shots. It has no native batching, so `run()` loops over the samples itself. It has no limit on the number of qubits. See Capabilities. #### `gradient_method` How this backend computes gradients. It is `"parameter_shift"` on a simulator. On a quantum processing unit (QPU), it is `"none"`, unless the configuration sets `enable_gradients`. A parameter-shift backward pass costs at least two paid tasks per weight on real hardware, so gradients are refused there unless you turn them on. #### `__init__(config=None, name='braket')` Build the backend from a device configuration. Parameters: | Name | Type | Description | Default | | -------- | -------------- | -------------------------------------------- | ---------- | | `config` | `BraketConfig` | The device settings. | `None` | | `name` | `str` | The registry name, which error messages use. | `'braket'` | #### `compile(circuit, observables)` Prepare `circuit` for repeated evaluation of `observables`. It is called once for each `Circuit`, and the handle is reused for every call, so the expensive work happens here. Parameters: | Name | Type | Description | Default | | ------------- | ---------------------- | ----------------------------------------- | ---------- | | `circuit` | `CircuitSpec` | The circuit to prepare. | *required* | | `observables` | `Sequence[Observable]` | The observables that the handle measures. | *required* | Returns: | Type | Description | | ---------------- | ------------------------ | | `BraketCompiled` | A BraketCompiled handle. | ### `BraketCompiled` Bases: `CompiledCircuit` A circuit prepared for Amazon Braket. This class is internal: you get it from BraketBackend.compile(), and you never create it directly. `run()` evaluates the observables and waits for the result. `submit()` returns a BraketJobHandle for a cloud or quantum processing unit (QPU) task, without waiting. `attach()` rebuilds that handle from the Amazon Resource Name (ARN) of a task. `dry_run_info()` reports how many circuits a call runs, without submitting anything. #### `device` The local simulator or the `AwsDevice`, created when first read. Authentication with Amazon Web Services (AWS) and network calls happen only when this property is first read. #### `submit_methods = _SUBMIT_METHODS` The methods `submit()` and `attach()` accept: `"run"`, `"sample"`, `"probabilities"` and `"zero_state_probability"`. Any method added to this adapter's async path must also be added here, or callers that check this set will refuse it. #### `__init__(circuit, observables, config, *, warn=True)` Prepare the circuit for repeated evaluation. `BraketBackend.compile()` builds it, and there is no reason to create one directly. Parameters: | Name | Type | Description | Default | | ------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ---------- | | `circuit` | `CircuitSpec` | The circuit to prepare. | *required* | | `observables` | `Sequence[Observable]` | The observables that this handle measures. | *required* | | `config` | `BraketConfig` | The device settings. | *required* | | `warn` | `bool` | Whether to issue the cost warnings for a cloud device. It is False only for the shifted copies that compile_shifted builds. Default: True. | `True` | #### `attach(task_id, *, method='run')` Rebuild a job handle for a task submitted earlier from this circuit. Use it to pick up a task after the process that submitted it has stopped, for example after a crash. The handle decodes the results exactly as the handle returned by `submit()` would have, provided that the task was submitted from the same circuit, observables and `method`. Parameters: | Name | Type | Description | Default | | --------- | ----- | ---------------------------------------------------------------------------------------------------- | ---------- | | `task_id` | `str` | The Amazon Resource Name (ARN) of the Braket quantum task, as returned by BraketJobHandle.task_id(). | *required* | | `method` | `str` | The method that the task was submitted with, one of submit_methods. | `'run'` | Returns: | Name | Type | Description | | -------- | ----------- | ------------------------------- | | `handle` | `JobHandle` | A handle for the existing task. | Raises: | Type | Description | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `ValueError` | If the configuration is not a cloud configuration, if task_id is not the ARN of a Braket quantum task, if method is not in submit_methods, or if method is "run" and the observables need more than one task (see submit()). The Braket package is not called in any of these cases. | #### `compile_shifted(circuit)` Compile a copy of this circuit with one gate angle shifted. The chain-rule gradient compiles one copy for each shifted gate. The compilation of this handle already issued the cost warnings, so the copy issues none. Parameters: | Name | Type | Description | Default | | --------- | ------------- | -------------------- | ---------- | | `circuit` | `CircuitSpec` | The shifted circuit. | *required* | Returns: | Type | Description | | ------------------ | ------------------------------------ | | `'BraketCompiled'` | A BraketCompiled handle for circuit. | #### `dry_run_info()` Report the size of the circuit without submitting anything. Use it to check a cloud submission before you submit it. It reports how many circuits a call runs. It does not estimate a price. #### `probabilities(weights, inputs)` Return the measurement distribution over all `2**n_qubits` basis states. With `shots > 0`, it is estimated from the shot frequencies, through the `Probability` result type. With `shots=0`, it is the exact distribution: `Probability` on the local simulator, the squared amplitudes of every basis state on SV1 (`Amplitude`), and the diagonal of the density matrix on DM1 (`DensityMatrix`). It is limited to `_sampling.MAX_DENSE_QUBITS` qubits, and every cloud path has a lower limit. The limit is 15 qubits with `shots > 0`, the widest `Probability` that the managed simulators and the quantum processing units (QPUs) of Braket return ([List of result types](https://docs.aws.amazon.com/braket/latest/developerguide/braket-result-types.html)). It is 17 qubits on SV1 at `shots=0`, because the program lists all `2**n_qubits` states and must stay under the Braket limit of 5 megabytes. It is 8 qubits on DM1 at `shots=0`, the widest density matrix that DM1 returns. `sample()` has no limit on the number of qubits. Parameters: | Name | Type | Description | Default | | --------- | --------- | ------------------------------- | ---------- | | `weights` | `ndarray` | A 1-D parameter vector. | *required* | | `inputs` | `ndarray` | A 1-D vector of input features. | *required* | Returns: | Name | Type | Description | | --------------- | --------- | ----------------------------------------------------------------------------------------------------------------------- | | `probabilities` | `ndarray` | A float64 array of shape (2\*\*n_qubits,), ordered with qubit 0 as the most significant bit, whose entries add up to 1. | Raises: | Type | Description | | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ValueError` | If the device publishes none of the result types above for the configured shots, if the circuit has more qubits than the limit of the path taken, or if weights or inputs is not 1-D. Nothing runs then. | #### `run(weights, inputs)` Evaluate the circuit and return one expectation value per observable. It follows the `CompiledCircuit` contract: the result has shape `(n_obs,)` when `weights` and `inputs` are both 1-D, and `(B, n_obs)` otherwise. With `shots > 0`, each sample runs one task for each group of terms that commute qubit-wise (see `dry_run_info()["n_circuit_runs_per_call"]`). Parameters: | Name | Type | Description | Default | | --------- | --------- | --------------------------------------------------------------------------- | ---------- | | `weights` | `ndarray` | The parameter vector: 1-D for one sample, or 2-D with a leading batch axis. | *required* | | `inputs` | `ndarray` | The input features, with the same 1-D or 2-D shape rule as weights. | *required* | Returns: | Type | Description | | --------- | ------------------------------------------------------------------------ | | `ndarray` | One expectation value for each observable (for each sample, in a batch). | #### `sample(weights, inputs)` Measure one sample in the computational basis and return the histogram. It runs one task with the configured shots and no result type, and reads the measurement counts of the task. A qubit that no gate acts on is reported as `0`. Parameters: | Name | Type | Description | Default | | --------- | --------- | ------------------------------- | ---------- | | `weights` | `ndarray` | A 1-D parameter vector. | *required* | | `inputs` | `ndarray` | A 1-D vector of input features. | *required* | Returns: | Name | Type | Description | | -------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | `counts` | `dict[str, int]` | The number of shots for each measured bitstring, with qubit 0 as the first character of each key. The counts add up to the configured shots. | Raises: | Type | Description | | ------------ | ------------------------------------------------------------------------------------------------------------------ | | `ValueError` | If the configuration has shots=0 (a histogram needs shots), or if weights or inputs is not 1-D. Nothing runs then. | #### `submit(weights, inputs, *, method='run', tags=None)` Submit the circuit without waiting, and return a `BraketJobHandle`. The handle holds the Braket `QuantumTask`. Parameters: | Name | Type | Description | Default | | --------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `weights` | `ndarray` | The parameter vector of one sample, 1-D. | *required* | | `inputs` | `ndarray` | The input features of one sample, 1-D. | *required* | | `method` | `str` | What the task computes, one of submit_methods. "run" (the default) measures the observables, like run(). "sample" returns the measurement histogram, like sample(). "probabilities" returns the distribution over every basis state, like probabilities(). "zero_state_probability" measures the all-zeros probability, like zero_state_probability(). | `'run'` | | `tags` | \`Mapping[str, str] | None\` | Amazon Web Services (AWS) tags to put on the quantum task, to find it again later. There can be at most 50 tags. Keys have 1 to 128 characters and values at most 256 characters, made of letters, digits, spaces and _ . : / = + - @. A key cannot start with aws:. Only cloud tasks are tagged, and the local simulator ignores the tags. | Returns: | Name | Type | Description | | -------- | ----------- | ------------------------------- | | `handle` | `JobHandle` | A handle for the submitted job. | Raises: | Type | Description | | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ValueError` | If method is not in submit_methods, if weights or inputs is not 1-D, if tags breaks the AWS tag rules, if method is "sample" with shots=0, if the device publishes no result type that method can read at the configured shots, or if the result type it reads is limited to fewer qubits than the circuit has. The limits are 15 qubits for Probability on a cloud device (List of result types), which "probabilities" and "zero_state_probability" read with shots > 0, and the exact limits listed under probabilities(). It is also raised if method is "run" and the observables need more than one task, because a handle tracks one task. This happens with shots > 0 when the terms do not all commute qubit-wise. Use run(), or split the observables into sets that commute. Nothing is submitted in any of these cases. | #### `zero_state_probability(weights, inputs)` Return the probability of measuring all zeros at the end of the circuit. It uses the `Probability` result type of Braket, which every Braket simulator and quantum processing unit (QPU) supports with `shots > 0`, and which the local simulator supports at any number of shots. The managed simulators and the QPUs return it for at most 15 qubits ([List of result types](https://docs.aws.amazon.com/braket/latest/developerguide/braket-result-types.html)), so a wider circuit with `shots > 0` on a cloud device is refused. In that case, use `sample()`, which has no limit on the number of qubits, and divide the all-zeros count by the shots. The local simulator has no limit. With `shots=0` on a cloud simulator, which publishes no `Probability` at `shots=0`, the value is exact instead: the squared all-zeros amplitude on SV1 (`Amplitude`), for any number of qubits, and the `[0, 0]` entry of the density matrix on DM1 (`DensityMatrix`), which DM1 returns for at most 8 qubits. Parameters: | Name | Type | Description | Default | | --------- | --------- | ------------------------------- | ---------- | | `weights` | `ndarray` | A 1-D parameter vector. | *required* | | `inputs` | `ndarray` | A 1-D vector of input features. | *required* | Returns: | Type | Description | | ------- | ----------------------------------- | | `float` | The probability, a float in [0, 1]. | Raises: | Type | Description | | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ValueError` | If the device publishes none of those result types for the configured shots, or if the circuit has more than 15 qubits on the cloud Probability path or more than 8 qubits on the DM1 path. Nothing runs then. | ### `BraketConfig` Device configuration for the Braket backend. Parameters: | Name | Type | Description | Default | | ---------------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `device_arn` | \`str | None\` | None (the default) for the local simulator, or the Amazon Resource Name (ARN) of an Amazon Web Services (AWS) device. An example for a cloud simulator is arn:aws:braket:::device/quantum-simulator/amazon/sv1. Quantum processing units (QPUs) have ARNs of the form arn:aws:braket:::device/qpu//. | | `local_backend` | `str` | The LocalSimulator backend to use when device_arn is None. Common values are "braket_sv" (state vector) and "braket_dm" (density matrix). | `'braket_sv'` | | `shots` | `int` | 0 means exact values (simulators only), and a positive integer turns on sampling with that number of shots. AWS QPUs require shots > 0. | `0` | | `s3_bucket` | \`str | None\` | The Amazon Simple Storage Service (S3) bucket for the results of cloud tasks. With None, the Braket package picks the default of the account. | | `s3_prefix` | `str` | The key prefix inside s3_bucket. | `'braket-results'` | | `poll_timeout_seconds` | `int` | How many seconds result() waits for a submitted task. | `3600` | | `enable_gradients` | `bool` | Whether to allow parameter-shift gradients on a QPU, for PyTorch .backward(), jacobian() and gradient-based optimisers. It is off by default, because one backward pass costs at least 2 * n_weights paid tasks. When it is set, compiling for a QPU issues a CostWarning that gives this count. Simulators compute gradients in either case. | `False` | #### `is_cloud` Whether `device_arn` is set. If it is, the circuit runs on Amazon Web Services (AWS), on a cloud simulator or a quantum processing unit (QPU), instead of on the local simulator. #### `is_qpu` Whether `device_arn` names real hardware (it contains `/qpu/`). #### `make_backend()` Build the backend that this configuration describes. The provider layer calls it when a configuration is passed as `provider=`. There is no reason to call it directly. Returns: | Type | Description | | ----------------- | ----------------------------------------- | | `'BraketBackend'` | The BraketBackend for this configuration. | ### `BraketJobHandle` A handle to a submitted Braket task. It holds the Braket `QuantumTask` and the plan that turns the raw expectation values of the task into one value for each `Observable`. `compiled.submit(weights, inputs)` and `compiled.attach(task_id)` return one of these handles. Call `result()` to wait until the task finishes, or `status()` to check the task without waiting. #### `method` The method of the compiled circuit that this task runs. It is `"run"`, `"sample"`, `"probabilities"` or `"zero_state_probability"`. #### `__init__(task, aggregation_plan, poll_timeout_seconds, method='run', *, n_qubits=None)` Wrap a submitted Braket task in a job handle. `BraketCompiled.submit()` or `BraketCompiled.attach()` builds it. It is not meant to be created directly. Parameters: | Name | Type | Description | Default | | ---------------------- | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `task` | `QuantumTask` | The Braket quantum task returned by the Braket package. | *required* | | `aggregation_plan` | `list[list[tuple[float, int]]]` | How the raw result values map to the requested observables. For each observable, it lists the (coefficient, result index) pairs to add up. | *required* | | `poll_timeout_seconds` | `int` | How many seconds result() waits before giving up. | *required* | | `method` | `str` | The method of the compiled circuit that the task runs, one of BraketCompiled.submit_methods. It decides how result() decodes the values of the task. | `'run'` | | `n_qubits` | \`int | None\` | The number of qubits of the circuit. When it is given, result() checks that a "probabilities" or "zero_state_probability" task returned a distribution over 2\*\*n_qubits states, and it writes the keys of a "sample" histogram as bitstrings of n_qubits characters. | Raises: | Type | Description | | ------------ | -------------------------------------------------- | | `ValueError` | If method is not in BraketCompiled.submit_methods. | #### `cancel()` Ask Braket to cancel the quantum task. It returns without waiting. #### `result()` Wait until the task finishes, then decode its values. It waits for at most `poll_timeout_seconds`. The decoding follows the `method` of this handle and the result type that the result of the task actually holds, so a handle from `attach()` decodes a task exactly as the handle returned by `submit()` would have. Returns: | Type | Description | | --------- | ---------------- | | \`ndarray | dict[str, int]\` | Raises: | Type | Description | | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `RuntimeError` | If the task has no result, because it ended FAILED or CANCELLED, or because the wait timed out. The message names the task and its state. | | `ValueError` | If the values of the task do not have the shape that the method and the circuit of this handle produce, for example because the handle was attached with the wrong method. | #### `status()` Return the state of the task, without waiting. The state is a string such as `"CREATED"`, `"QUEUED"`, `"RUNNING"`, `"COMPLETED"`, `"FAILED"` or `"CANCELLED"`. #### `task_id()` Return the identifier of the Braket quantum task, to look it up in the Amazon Web Services (AWS) console. ## `eigenkit.core.backends.cudaquantum_backend` The CUDA Quantum backend (the `cuda-quantum` package). `cudaq` is an optional dependency (`pip install eigenkit[cudaquantum]`). The package is imported only when it is needed, so the rest of the library works without it, and `compile()` raises a clear `ImportError` if it is missing. It declares `gradient_method = "parameter_shift"`. `cudaq` has its own parameter-shift tools, but this backend uses the fallback of the gradient code instead, for consistency across backends. This keeps Qiskit, Braket and `cudaq` on a single gradient code path. `compile()` builds a parametric kernel with `cudaq.make_kernel` once. Each `run()` calls `cudaq.observe(kernel, hamiltonian, *params)` for each observable. Limitation: only the parameter expressions `Constant`, `WeightRef`, `InputRef`, `Scale` and `Negate` are supported. `Sum` and `Multiply` expressions raise `NotImplementedError`, because the arithmetic of the `cudaq` `QuakeValue` is more restricted than that of the Qiskit `ParameterExpression`. Example ```python from eigenkit import Circuit, Encoding, Variational, all_z from eigenkit import CudaQuantumBackend, register blocks = [Encoding(in_features=4), Variational(depth=2)] # by registered name, after registering it once register("cudaq", CudaQuantumBackend()) circuit = Circuit(blocks=blocks, observables=all_z(4), provider="cudaq") # or with the config, when a device setting matters from eigenkit.configs import CudaQuantumConfig circuit = Circuit( blocks=blocks, observables=all_z(4), provider=CudaQuantumConfig(target="nvidia"), ) ``` This backend is optional: register it once with `register("cudaq", CudaQuantumBackend())` before the name can be used. ### `CudaQuantumBackend` The provider for NVIDIA CUDA Quantum, under the name `"cudaq"`. It is not registered automatically. Register it with `register("cudaq", CudaQuantumBackend())` to select it with `provider="cudaq"`, or pass a CudaQuantumConfig as `provider=`. #### `capabilities` What this backend supports. It runs every gate except the state preparation gates, the arbitrary unitaries, the generic multi-controlled gates and `CPHASE`. A CUDA Quantum kernel compiles to a hardware intermediate representation, so it cannot express the first three, and this backend does not translate `CPHASE`. It can sample a finite number of shots, evaluates a batch in one call through `cudaq.observe`, and has no limit on the number of qubits. See Capabilities. #### `__init__(config=None, name='cudaq')` Build the backend from a device configuration. Parameters: | Name | Type | Description | Default | | -------- | ------------------- | ------------------------------------------------------------------- | --------- | | `config` | `CudaQuantumConfig` | The device settings. Default: None, which uses CudaQuantumConfig(). | `None` | | `name` | `str` | The registry name, used in error messages. Default: "cudaq". | `'cudaq'` | #### `compile(circuit, observables)` Prepare `circuit` for repeated evaluation of `observables`. A `Circuit` calls it once and reuses the result for every call, so the expensive work belongs here. Parameters: | Name | Type | Description | Default | | ------------- | ---------------------- | --------------------------- | ---------- | | `circuit` | `CircuitSpec` | The circuit to prepare. | *required* | | `observables` | `Sequence[Observable]` | The observables to measure. | *required* | Returns: | Type | Description | | --------------------- | ------------------------------------------------------ | | `CudaQuantumCompiled` | The prepared circuit, as a CudaQuantumCompiled object. | ### `CudaQuantumCompiled` Bases: `CompiledCircuit` A circuit prepared for CUDA Quantum. It is internal: CudaQuantumBackend.compile() builds it and returns it, and it is never built directly. #### `__init__(circuit, observables, config)` Prepare the circuit for repeated evaluation. `CudaQuantumBackend.compile()` builds this object, so there is no reason to build one directly. Parameters: | Name | Type | Description | Default | | ------------- | ---------------------- | --------------------------- | ---------- | | `circuit` | `CircuitSpec` | The circuit to prepare. | *required* | | `observables` | `Sequence[Observable]` | The observables to measure. | *required* | | `config` | `CudaQuantumConfig` | The device settings. | *required* | #### `run(weights, inputs)` Evaluate the circuit and return one expectation value per observable. It follows the `CompiledCircuit` contract: the result has shape `(n_obs,)` when `weights` and `inputs` are both 1-D, and `(B, n_obs)` otherwise. Parameters: | Name | Type | Description | Default | | --------- | --------- | --------------------------------------------------------------------------- | ---------- | | `weights` | `ndarray` | The parameter vector: 1-D for one sample, or 2-D with a leading batch axis. | *required* | | `inputs` | `ndarray` | The input features, with the same 1-D or 2-D shape convention as weights. | *required* | Returns: | Type | Description | | --------- | ------------------------------------------------ | | `ndarray` | The expectation values, one for each observable. | #### `zero_state_probability(weights, inputs)` Return the probability of `|0…0⟩`, read from the statevector of `cudaq`. Parameters: | Name | Type | Description | Default | | --------- | --------- | --------------------------------------- | ---------- | | `weights` | `ndarray` | The parameter vector (1-D). | *required* | | `inputs` | `ndarray` | The input features of one sample (1-D). | *required* | Returns: | Type | Description | | ------- | ----------------------------------- | | `float` | The probability, a float in [0, 1]. | ### `CudaQuantumConfig` Device configuration for the CUDA Quantum backend. Parameters: | Name | Type | Description | Default | | -------- | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | | `target` | `str` | The cudaq target. Common values are "qpp-cpu", a statevector simulator on the central processing unit (CPU), which is the default; "nvidia", on one graphics processing unit (GPU); "nvidia-mgpu", on several GPUs; "nvidia-mqpu", with several quantum processing units (QPUs); and "tensornet". Default: "qpp-cpu". | `'qpp-cpu'` | | `shots` | \`int | None\` | None for exact expectation values, or a positive integer for a finite number of shots. Default: None. | #### `make_backend()` Build the backend that this configuration describes. The provider layer calls it when a configuration is passed as `provider=`. There is no reason to call it directly. Returns: | Type | Description | | ---------------------- | ---------------------------------------------- | | `'CudaQuantumBackend'` | The CudaQuantumBackend for this configuration. | ## `eigenkit.core.backends.iqm_backend` The IQM backend, which runs circuits on IQM quantum computers through iqm-client. The `iqm-client` package, with its `qiskit` extra, is an optional dependency (`pip install eigenkit[iqm]`). It is imported only when it is needed, so the rest of the library works without it. If it is missing, `compile()` raises an `ImportError` with a clear message. This backend connects directly to IQM Resonance, or to any IQM Server, instead of using the same hardware through Amazon Braket. You then pay through IQM's own plans instead of an Amazon Web Services (AWS) bill. It is also the only way to reach IQM's fake backends, its calibration sets and its architectures with `MOVE` gates. IQM has **no estimator primitive**: the IQM service returns histograms of measured bitstrings and nothing else. So this backend works by sampling everywhere. The observables are sorted into groups of qubit-wise commuting terms, one circuit runs for each group, and the expectation values are computed locally from the counts (see the `_sampling` module). `probabilities()` and `sample()` use the same code, and this is what lets the quantum approximate optimisation algorithm (QAOA) read a solution from real hardware. There are two kinds of target: - **Resonance or any IQM Server**: pass `url` and a `token` (or set the `IQM_TOKEN` environment variable). You can also pass `quantum_computer` to choose one machine when the server hosts several. - **Local fake backends**: pass `IQMFakeAdonis()`, `IQMFakeApollo()`, `IQMFakeDeneb()` or a similar fake backend as `backend_instance`. These run on Qiskit Aer with a realistic IQM noise model, with no token and no network access. The tests of this backend run on them. The default is `gradient_method = "none"`, because hardware is too expensive for parameter-shift gradients: one backward pass takes at least `2 * n_weights` jobs, each with the full number of shots. Train on a simulator, then use the trained weights on the device. `enable_gradients=True` in the configuration turns on parameter-shift gradients on the device. Compiling for a real device then issues a `CostWarning` that gives the number of circuits of one backward pass. The circuits are transpiled for the device target. The native gates are `r` and `cz`, and also `move` on the Star architectures, which the Qiskit transpiler routes through the plugin that iqm-client provides. `transpile_mode` chooses when this happens: - `"parametric"`, the default, transpiles once for each measurement group and sets the parameter values at each call. This is cheap, but the transpiler cannot merge rotations whose angles it does not know. For example, an encoding block and a variational block on 2 qubits, measured in one group, gave 6 `r` gates in this mode against 3 with the values set before transpiling. - `"per_call"` sets the values first and transpiles at every call. The circuits are shallower, so they run with higher fidelity on hardware. The cost is one transpilation per evaluation, which is small next to the time spent in the queue, but not next to a local simulator. Example ```python from eigenkit import Circuit, Encoding, Variational, all_z from eigenkit.configs import IQMConfig blocks = [Encoding(in_features=4), Variational(depth=2)] # Offline, against IQM's noise-modelled fake backend. from iqm.qiskit_iqm import IQMFakeAdonis circuit = Circuit( blocks=blocks, observables=all_z(4), provider=IQMConfig(backend_instance=IQMFakeAdonis(), shots=2048), ) # On real hardware, by name. circuit = Circuit( blocks=blocks, observables=all_z(4), provider=IQMConfig( url="https://resonance.iqm.tech/", quantum_computer="garnet", shots=2048, ), ) ``` The backend is registered as `"iqm"` when the `iqm` extra is installed, and its module is imported only when it is first used. A real device uses your IQM quota: the first compilation for a real device issues a `CostWarning`, and `dry_run_info()` reports the size of the circuit and the number of circuits that each call submits, before you submit anything. ### `IQMBackend` Provider for IQM quantum computers, through iqm-client. It is registered as `"iqm"` when iqm-client is installed, and its module is imported only when it is first used. A real device uses your IQM quota: the first compilation for a real machine issues a CostWarning, and `dry_run_info()` reports the size of a run and the number of circuits that each call submits, beforehand. Compiling for one of IQM's fake backends is free and gives no warning. This class is internal: you select the backend with `provider="iqm"` or by passing an IQMConfig, not by creating this class. #### `capabilities` What this backend supports. It supports every gate except state preparation, arbitrary unitaries and `CPHASE`. It samples with a finite number of shots, which is the only thing IQM offers. It has no native batching, because a batch takes one job for each sample. It has no qubit limit of its own, and the number of qubits of the device applies. See Capabilities. #### `gradient_method` How this backend computes gradients. It is `"none"`, unless the configuration sets `enable_gradients`, and then it is `"parameter_shift"`. A parameter-shift backward pass costs at least `2 * n_weights` jobs, each with the full number of shots, so gradients are refused unless you turn them on. #### `__init__(config=None, name='iqm')` Build the backend from a device configuration. Parameters: | Name | Type | Description | Default | | -------- | ----------- | --------------------------------------------------------------------------------------------------------------- | ------- | | `config` | `IQMConfig` | The device settings. None builds the default configuration, which reads the server address from IQM_SERVER_URL. | `None` | | `name` | `str` | The registry name, which error messages use. | `'iqm'` | #### `compile(circuit, observables)` Prepare `circuit` for repeated evaluation of `observables`. It is called once for each `Circuit`, and the handle is reused for every call. So the expensive work happens here: planning the measurement groups and, in `"parametric"` mode, transpiling one circuit for each group. Parameters: | Name | Type | Description | Default | | ------------- | ---------------------- | ----------------------------------------- | ---------- | | `circuit` | `CircuitSpec` | The circuit to prepare. | *required* | | `observables` | `Sequence[Observable]` | The observables that the handle measures. | *required* | Returns: | Type | Description | | ------------- | ---------------------- | | `IQMCompiled` | An IQMCompiled handle. | ### `IQMCompiled` Bases: `CompiledCircuit` A circuit prepared for an IQM quantum computer. This class is internal: you get it from IQMBackend.compile(), and you never create it directly. The measurement plan and the parametric circuit are built once. `run()` and `submit()` set the parameter values, and in `"per_call"` mode they also transpile. #### `__init__(circuit, observables, config, *, warn=True)` Prepare the circuit for repeated evaluation. `IQMBackend.compile()` builds it, and there is no reason to create one directly. Parameters: | Name | Type | Description | Default | | ------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `circuit` | `CircuitSpec` | The circuit to prepare. | *required* | | `observables` | `Sequence[Observable]` | The observables that this handle measures. | *required* | | `config` | `IQMConfig` | The device settings. | *required* | | `warn` | `bool` | Whether to issue the cost warnings for a real device. It is False only for the shifted copies that compile_shifted builds. Default: True. | `True` | #### `compile_shifted(circuit)` Compile a copy of this circuit with one gate angle shifted. The chain-rule gradient compiles one copy for each shifted gate. Each copy reuses the backend of this handle, without asking the server for it again, and is transpiled with the seed of this handle, so every term of the gradient comes from the same qubits. The compilation of this handle already issued the cost warnings, so the copy issues none. Parameters: | Name | Type | Description | Default | | --------- | ------------- | -------------------- | ---------- | | `circuit` | `CircuitSpec` | The shifted circuit. | *required* | Returns: | Type | Description | | --------------- | ---------------------------------- | | `'IQMCompiled'` | An IQMCompiled handle for circuit. | #### `counts(weights, inputs)` Measure one sample in the computational basis and return the histogram. It runs one job of `shots` shots, measured in the `Z` basis with no change of basis. Parameters: | Name | Type | Description | Default | | --------- | --------- | ------------------------------- | ---------- | | `weights` | `ndarray` | A 1-D parameter vector. | *required* | | `inputs` | `ndarray` | A 1-D vector of input features. | *required* | Returns: | Name | Type | Description | | -------- | ---------------- | ----------------------------------------------------------------------------------------- | | `counts` | `dict[str, int]` | The number of shots for each measured bitstring. The leftmost bit of each key is qubit 0. | #### `dry_run_info()` Report the size of a run, before anything is submitted. The report gives the target backend and whether it is one of IQM's local fake backends, the number of shots, the transpile mode, the optimisation level, the `seed_transpiler` setting, the numbers of logical and physical qubits, the number of observables, and the number of measurement groups that the observables need, which is the number of circuits that one call submits. In `"parametric"` mode, it also gives the gate counts and the depth of the transpiled circuit for each measurement basis. `"per_call"` mode transpiles at call time, so there is no stored circuit to report. Returns: | Name | Type | Description | | ------ | ------ | -------------------------------- | | `info` | `dict` | The values above, keyed by name. | #### `probabilities(weights, inputs)` Return the measurement distribution over all `2**n_qubits` basis states. The distribution is estimated from the shot frequencies, and it is not the exact one: this is hardware, and the basis states that no shot measured get a probability of exactly `0`. It is limited to `_sampling.MAX_DENSE_QUBITS` qubits. For wider circuits, read `sample()` directly. Parameters: | Name | Type | Description | Default | | --------- | --------- | ------------------------------- | ---------- | | `weights` | `ndarray` | A 1-D parameter vector. | *required* | | `inputs` | `ndarray` | A 1-D vector of input features. | *required* | Returns: | Name | Type | Description | | --------------- | --------- | ------------------------------------------------------------- | | `probabilities` | `ndarray` | An array of shape (2\*\*n_qubits,) whose entries add up to 1. | #### `resources(measured=True)` Report the resources that the transpiled circuit uses on this device. The two-qubit gate counts include `move` on Star devices, because every `CZ` there goes through the resonator. In `"per_call"` mode, the report describes the parametric circuit, and the circuits with the values set can merge a few more rotations. Parameters: | Name | Type | Description | Default | | ---------- | ------ | ------------------------------------------------------------------------------------- | ------- | | `measured` | `bool` | It is accepted for consistency with the Qiskit Runtime backend, and it has no effect. | `True` | Returns: | Type | Description | | ------ | ----------------------------------------------------------------------------- | | `dict` | A dictionary with the same keys as resources() of the Qiskit Runtime backend. | #### `retrieve_zero_state_probabilities(job_id)` Reconnect to a job that `submit_zero_state_probabilities()` submitted. Parameters: | Name | Type | Description | Default | | -------- | ----- | ------------------------------ | ---------- | | `job_id` | `str` | The identifier of the IQM job. | *required* | Returns: | Type | Description | | ---------------------- | ------------------------------------------------------ | | `ProbabilityJobHandle` | A handle whose result() is the array of probabilities. | Raises: | Type | Description | | --------------------- | --------------------------------------------------------------------------------------------------------- | | `NotImplementedError` | For a local fake backend, which keeps no history of its jobs, or for a backend that cannot retrieve jobs. | #### `run(weights, inputs)` Evaluate the circuit and return one expectation value per observable. It follows the `CompiledCircuit` contract: the result has shape `(n_obs,)` when `weights` and `inputs` are both 1-D, and `(B, n_obs)` otherwise. Each sample costs one job of `n_measurement_groups` circuits. The values are estimated from shots, so they carry the shot noise of `shots`. Parameters: | Name | Type | Description | Default | | --------- | --------- | --------------------------------------------------------------------------- | ---------- | | `weights` | `ndarray` | The parameter vector: 1-D for one sample, or 2-D with a leading batch axis. | *required* | | `inputs` | `ndarray` | The input features, with the same 1-D or 2-D shape rule as weights. | *required* | Returns: | Type | Description | | --------- | ------------------------------------------------------------------------ | | `ndarray` | One expectation value for each observable (for each sample, in a batch). | #### `sample(weights, inputs)` Return the same histogram as `counts()`. This is an alias of counts, under the name that `Circuit.sample()` looks for. Parameters: | Name | Type | Description | Default | | --------- | --------- | ------------------------------- | ---------- | | `weights` | `ndarray` | A 1-D parameter vector. | *required* | | `inputs` | `ndarray` | A 1-D vector of input features. | *required* | Returns: | Name | Type | Description | | -------- | ---------------- | ------------------------------------------------ | | `counts` | `dict[str, int]` | The number of shots for each measured bitstring. | #### `submit(weights, inputs)` Submit the circuit without waiting, and return a job handle. All the measurement groups go into one job, so `handle.result()` waits once and returns the whole vector of expectation values. Parameters: | Name | Type | Description | Default | | --------- | --------- | ------------------------------- | ---------- | | `weights` | `ndarray` | A 1-D parameter vector. | *required* | | `inputs` | `ndarray` | A 1-D vector of input features. | *required* | Returns: | Name | Type | Description | | -------- | ----------- | ------------------------------- | | `handle` | `JobHandle` | A handle for the submitted job. | #### `submit_zero_state_probabilities(weights, inputs)` Submit the all-zeros probabilities of the rows of `inputs`, without waiting. The rows go into one IQM job of `B` circuits. In `"parametric"` mode, the circuit measured in the `Z` basis is transpiled once, and each row only sets the values. In `"per_call"` mode, every row is transpiled. Parameters: | Name | Type | Description | Default | | --------- | --------- | ------------------------------------------ | ---------- | | `weights` | `ndarray` | A 1-D parameter vector. | *required* | | `inputs` | `ndarray` | A batch of inputs, of shape (B, n_inputs). | *required* | Returns: | Type | Description | | ---------------------- | --------------------------------------------------------------------- | | `ProbabilityJobHandle` | A handle whose result() is the array of probabilities, of shape (B,). | #### `transpiled_circuit(measured=True)` Return the circuit measured in `Z` and transpiled for this device. The parameters are not set. Use it to draw or inspect the circuit. Parameters: | Name | Type | Description | Default | | ---------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------- | ------- | | `measured` | `bool` | It is accepted for consistency with the Qiskit Runtime backend, and it has no effect, because IQM always submits measured circuits. | `True` | Returns: | Type | Description | | ------------------ | ----------------------------------- | | `'QuantumCircuit'` | A transpiled qiskit.QuantumCircuit. | #### `zero_state_probabilities(weights, inputs)` Return the all-zeros probability of every row of `inputs`, from one job. Parameters: | Name | Type | Description | Default | | --------- | --------- | ------------------------------------------ | ---------- | | `weights` | `ndarray` | A 1-D parameter vector. | *required* | | `inputs` | `ndarray` | A batch of inputs, of shape (B, n_inputs). | *required* | Returns: | Type | Description | | --------- | ---------------------------------------------------------- | | `ndarray` | The probabilities estimated from the shots, of shape (B,). | #### `zero_state_probability(weights, inputs)` Return the all-zeros probability of one sample, estimated from the shots. Parameters: | Name | Type | Description | Default | | --------- | --------- | ------------------------------- | ---------- | | `weights` | `ndarray` | A 1-D parameter vector. | *required* | | `inputs` | `ndarray` | A 1-D vector of input features. | *required* | Returns: | Type | Description | | ------- | ---------------- | | `float` | The probability. | ### `IQMConfig` Bases: `SecretFieldsMixin` Device configuration for the IQM backend. There are two ways to choose a target: - `url`, and optionally `quantum_computer`, opens an `IQMProvider` for an IQM Server. This needs a token, from `token` or from the `IQM_TOKEN` environment variable. - `backend_instance` uses any IQM `BackendV2` directly: a fake backend for offline work, or a backend that you configured yourself. It takes precedence over `url`, and it needs no token and no network access. Parameters: | Name | Type | Description | Default | | ---------------------- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `url` | \`str | None\` | The address of the IQM Server, for example "https://resonance.iqm.tech/". It is ignored when backend_instance is set. With None, the backend reads the address from the IQM_SERVER_URL environment variable when the circuit is compiled. | | `quantum_computer` | \`str | None\` | The machine to use when the server hosts more than one, for example "garnet". With None, the server's default is used. | | `backend_instance` | `Any` | An IQM backend that is already configured. It takes precedence over url. | `None` | | `shots` | `int` | The number of shots for each circuit run. It must be positive, because hardware cannot compute exact values. | `1024` | | `optimization_level` | `int` | The optimisation level passed to qiskit.transpile, from 0 to 3. | `1` | | `seed_transpiler` | \`int | None\` | The seed of the random steps of the transpiler, mainly the SabreLayout and SabreSwap passes, which choose the layout and the routing. With None, the default, a new seed is drawn for every compilation, so a circuit that needs routing compiles to a different number of gates on every run. Within one compilation, all the transpilations share the seed, including those of the shifted copies that a hardware gradient compiles. Set it whenever two compilations must be comparable. Trying a few seeds and keeping the cheapest result is a cheap and fair way to choose a layout, because compiling submits nothing. | | `transpile_mode` | `TranspileMode` | "parametric" transpiles once and sets the parameter values at each call. "per_call" sets the values first and transpiles at every call, which gives shallower circuits but costs one transpilation per evaluation. | `'parametric'` | | `group_commuting` | `bool` | Whether to put Pauli terms that commute qubit by qubit into shared measurement circuits. With False, one circuit runs for each term: there are more jobs, but each term gets all the shots. | `True` | | `token` | \`str | None\` | The access token for the IQM Server. It is passed on only when it is set. With None, iqm-client handles authentication on its own, from IQM_TOKEN and from its own tokens file. Do not set both token and IQM_TOKEN, because iqm-client rejects a token given in two ways at once. This field is secret: repr() never shows it, and it is never saved in .eigenkit bundles, pickles or joblib checkpoints. After a load it is None, so set IQM_TOKEN. | | `calibration_set_id` | `Any` | The calibration set to use, as a UUID. With None, the server's default is used. | `None` | | `use_metrics` | `bool` | Whether to ask the server for the error rates of each qubit and each coupler, and load them into the InstructionProperties of the target. With False (the default of iqm-client itself), the target has no error rates, so the Qiskit layout pass does not see the noise. It then cannot prefer the better qubits, and a comparison with a backend that does have calibration data is not like for like. It costs one extra request at compile time, and it is ignored when backend_instance is set. | `False` | | `provider_kwargs` | `dict` | Extra keyword arguments passed to IQMProvider, for example tokens_file. | `dict()` | | `poll_timeout_seconds` | `int` | How many seconds result() waits before giving up. | `3600` | | `enable_gradients` | `bool` | Whether to allow parameter-shift gradients on the device, for PyTorch .backward(), jacobian() and gradient-based optimisers. It is off by default, because one backward pass runs the circuits of a forward call at least 2 * n_weights times. When it is set, compiling for a real device issues a CostWarning that gives this count. | `False` | #### `__post_init__()` Reject the settings that can never work, when the configuration is created. If they were left to the first submission, the error would appear after the transpilation and the cost warning, which on a paid device is the wrong time to find out. Only the settings that are wrong on their own are checked here. Whether a *target* can be found depends on the environment at compile time, so `_resolve_backend` checks that. #### `make_backend()` Build the backend that this configuration describes. The provider layer calls it when a configuration is passed as `provider=`. There is no reason to call it directly. Returns: | Type | Description | | -------------- | -------------------------------------- | | `'IQMBackend'` | The IQMBackend for this configuration. | ### `IQMJobHandle` A handle to a submitted IQM job. `compiled.submit(weights, inputs)` returns one of these handles. Call `result()` to wait until the job finishes and get the array of expectation values, or `status()` to check the job without waiting. #### `__init__(job, groups, n_observables, n_qubits, poll_timeout_seconds)` Wrap a submitted IQM job in a job handle. `IQMCompiled.submit()` builds it. It is not meant to be created directly. Parameters: | Name | Type | Description | Default | | ---------------------- | ------------------------------ | ------------------------------------------------------------------------------------ | ---------- | | `job` | `JobV1` | The job returned by the IQM backend. | *required* | | `groups` | `tuple[MeasurementGroup, ...]` | The measurement plan that the submitted circuits follow, in the order of submission. | *required* | | `n_observables` | `int` | The number of expectation values to compute. | *required* | | `n_qubits` | `int` | The number of qubits of the circuit, used to read the keys of the histograms. | *required* | | `poll_timeout_seconds` | `int` | How many seconds result() waits before giving up. | *required* | #### `cancel()` Ask IQM to cancel the job. It returns without waiting. #### `counts()` Wait until the job finishes, then return the raw histograms. There is one histogram for each measurement group, and qubit `0` is the leftmost bit of each key. These are the counts that the device returned. result computes the expectation values of the observables from them. #### `job_id()` Return the identifier of the IQM job, to look it up in Resonance or through iqm-client. #### `result()` Wait until the job finishes, then return one expectation value for each observable, in the order of the observables. #### `status()` Return the state of the job, as the job object reports it, without waiting. The state is a string such as `"QUEUED"`, `"RUNNING"`, `"DONE"`, `"CANCELLED"` or `"ERROR"`. ## `eigenkit.core.backends.numpy_backend` The reference NumPy statevector simulator, which is the default backend. This backend uses only NumPy. It needs neither PyTorch nor an external quantum software development kit. It exists for two reasons: 1. **It works out of the box.** The library runs (`Circuit(provider="numpy")`) without installing a large simulator or one that needs a licence. This backend is always available. 1. **It is the reference for correctness.** Every other backend is tested against it (and against PennyLane) for equal results, so it also serves as the reference that the others are checked against. It is written to be easy to read, not tuned for speed. For speed, install one of the optional backends. It does use two simple NumPy optimisations. A batch of inputs evolves in one vectorised pass, with a leading batch axis on the state tensor, instead of a Python loop over the samples. A controlled-NOT (CNOT) gate is applied by swapping amplitudes (`np.flip` on the subspace where the control is `1`) instead of by contracting a `4x4` matrix. #### State representation The state is a `complex128` tensor of shape `(2,) * n_qubits`, and qubit `q` is on axis `q`. In C order, this makes qubit `0` the most significant bit, as in PennyLane, where wire `0` is the most significant bit. As a result, `state.reshape(-1)[0]` is the amplitude of `|0…0⟩`, and the computed values agree with PennyLane to machine precision. #### Gradients The backend declares `gradient_method = "adjoint"` and implements NumpyCompiled.jacobian(). This method computes analytic gradients in reverse mode, with one forward pass and one backward pass, so the number of passes is `O(1)` in the number of parameters. The gradient code of the core package uses it in preference to the parameter-shift rule, which is the fallback, and it differentiates a 2-D `inputs` batch in one batched pass. The gradients agree with the parameter-shift rule and with PennyLane to machine precision. Example ```python from eigenkit import Circuit, Encoding, Variational, all_z blocks = [Encoding(in_features=4), Variational(depth=2)] # by registered name circuit = Circuit(blocks=blocks, observables=all_z(4), provider="numpy") # or with the config, when a device setting matters from eigenkit.configs import NumpyConfig circuit = Circuit( blocks=blocks, observables=all_z(4), provider=NumpyConfig(), ) ``` This backend is always available. It is the default, and it needs no extra. ### `NumpyCompiled` Bases: `CompiledCircuit` A circuit prepared for the reference NumPy simulator. NumpySimulator.compile() builds it, and it is cheap to call many times. `run()` evaluates the observables, and `jacobian()` computes their analytic gradient. `statevector()`, `probabilities()` and `zero_state_probability()` read the state directly, which the Harrow-Hassidim-Lloyd (HHL) linear solver needs. #### `__init__(circuit, observables, config)` Prepare the circuit for repeated evaluation. `NumpySimulator.compile()` builds this object, so there is no reason to build one directly. Parameters: | Name | Type | Description | Default | | ------------- | ---------------------- | --------------------------- | ---------- | | `circuit` | `CircuitSpec` | The circuit to prepare. | *required* | | `observables` | `Sequence[Observable]` | The observables to measure. | *required* | | `config` | `NumpyConfig` | The device settings. | *required* | #### `jacobian(weights, inputs, *, need_input_grad=False)` Compute the gradient with this backend's own method, the analytic adjoint. The gradient code of the core package looks for a `jacobian` method. When a backend has one, it is used in preference to the parameter-shift rule, which works on any backend and is the fallback. Here it is the adjoint method in reverse mode: one forward pass and one backward pass, so the number of passes is `O(1)` in the number of parameters. For a single sample (1-D `inputs`), the two jacobians have shapes `(n_obs, n_weights)` and `(n_obs, n_inputs)`. For a 2-D `inputs` batch, they have a leading batch axis (`(B, n_obs, n_weights)` and `(B, n_obs, n_inputs)`), and the whole batch is differentiated in one pass. This is what makes the training of a hybrid quantum neural network with PyTorch fast on this backend. The gradients with respect to the gate angles are mapped onto the weights and inputs with the same symbolic chain-rule factors as the parameter-shift code, so angles built with `Scale`, `Multiply`, `Sum` or `Negate` are also differentiated correctly. Parameters: | Name | Type | Description | Default | | ----------------- | --------- | ------------------------------------------------------------------------------ | ---------- | | `weights` | `ndarray` | The parameter vector (1-D). | *required* | | `inputs` | `ndarray` | The input features: 1-D for one sample, or 2-D with a leading batch axis. | *required* | | `need_input_grad` | `bool` | If True, also compute the jacobian with respect to the inputs. Default: False. | `False` | Returns: | Name | Type | Description | | ----------------- | --------- | ----------------------------------------- | | `weight_jacobian` | `ndarray` | The jacobian with respect to the weights. | | `input_jacobian` | \`ndarray | None\` | #### `probabilities(weights, inputs)` Return the measurement probabilities of all `2**n_qubits` basis states. Qubit `0` is the most significant bit, as in PennyLane's `qml.probs`. The probabilities are used to decode samples from the prepared state, for example the bitstrings of the Quantum Approximate Optimization Algorithm (QAOA). Parameters: | Name | Type | Description | Default | | --------- | --------- | --------------------------------------- | ---------- | | `weights` | `ndarray` | The parameter vector (1-D). | *required* | | `inputs` | `ndarray` | The input features of one sample (1-D). | *required* | Returns: | Type | Description | | --------- | --------------------------------------------- | | `ndarray` | The probabilities, of shape (2\*\*n_qubits,). | #### `run(weights, inputs)` Evaluate the circuit and return one expectation value per observable. It follows the `CompiledCircuit` contract: the result has shape `(n_obs,)` when `weights` and `inputs` are both 1-D, and `(B, n_obs)` otherwise. Parameters: | Name | Type | Description | Default | | --------- | --------- | --------------------------------------------------------------------------- | ---------- | | `weights` | `ndarray` | The parameter vector: 1-D for one sample, or 2-D with a leading batch axis. | *required* | | `inputs` | `ndarray` | The input features, with the same 1-D or 2-D shape convention as weights. | *required* | Returns: | Type | Description | | --------- | ------------------------------------------------ | | `ndarray` | The expectation values, one for each observable. | #### `statevector(weights, inputs, initial_state=None)` Return the full complex statevector after the circuit has run. The result is a flat array of `2**n_qubits` amplitudes, with qubit `0` as the most significant bit (the same order as `probabilities()`). `initial_state`, a normalised flat vector in the same order, makes the evolution start from that state instead of `|0…0⟩`. Reading a state out and passing it back in this way is what quantum reservoir carry-over and amplitude encoding are built on. A 2-D `inputs` batch evolves in one pass and returns an array of shape `(B, 2**n_qubits)`. `initial_state` can then be one state of shape `(2**n,)`, shared by all samples, or a stack of shape `(B, 2**n)` with one state for each sample. Parameters: | Name | Type | Description | Default | | --------------- | --------- | ------------------------------------------------------------------------- | ----------------------------------- | | `weights` | `ndarray` | The parameter vector (1-D). | *required* | | `inputs` | `ndarray` | The input features: 1-D for one sample, or 2-D with a leading batch axis. | *required* | | `initial_state` | \`ndarray | None\` | The state to start from, instead of | Returns: | Type | Description | | --------- | ------------------------------------------------------------------------------ | | `ndarray` | The statevector, of shape (2\*\*n_qubits,), or (B, 2\*\*n_qubits) for a batch. | #### `zero_state_probabilities(weights, inputs)` Return the probability of `|0…0⟩` for every input of a batch, vectorised. `inputs` has shape `(B, n_inputs)`, and the result has shape `(B,)`. The whole batch evolves in one pass. The quantum kernel uses this method to build a fidelity matrix without a Python loop over pairs. Parameters: | Name | Type | Description | Default | | --------- | --------- | -------------------------------------------- | ---------- | | `weights` | `ndarray` | The parameter vector (1-D). | *required* | | `inputs` | `ndarray` | The batch of inputs, of shape (B, n_inputs). | *required* | Returns: | Type | Description | | --------- | --------------------------------------------------- | | `ndarray` | The probabilities, one for each row, of shape (B,). | #### `zero_state_probability(weights, inputs)` Return the probability of measuring the state `|0…0⟩`. Fidelity kernels and swap tests are built on it. It does not go through `Observable`, because it is not the expectation value of a Pauli operator. Parameters: | Name | Type | Description | Default | | --------- | --------- | --------------------------------------- | ---------- | | `weights` | `ndarray` | The parameter vector (1-D). | *required* | | `inputs` | `ndarray` | The input features of one sample (1-D). | *required* | Returns: | Type | Description | | ------- | ----------------------------------- | | `float` | The probability, a float in [0, 1]. | ### `NumpyConfig` Configuration for the reference NumPy simulator. It has no settings yet. It exists so that `Circuit(provider=NumpyConfig())` works in the same way as with the configurations of the other backends. #### `make_backend()` Build the backend that this configuration describes. The provider layer calls it when a configuration is passed as `provider=`. There is no reason to call it directly. Returns: | Type | Description | | ------------------ | ------------------------------------------ | | `'NumpySimulator'` | The NumpySimulator for this configuration. | ### `NumpySimulator` The provider for the reference NumPy statevector simulator. It is registered automatically as `"numpy"`. It uses only NumPy and no optional dependency, so it is always available. It declares `gradient_method = "adjoint"`, so the gradient code of the core package uses NumpyCompiled.jacobian() (analytic, in reverse mode, with one forward pass and one backward pass whatever the number of parameters) instead of the parameter-shift rule, whose cost is `O(n_params)`. #### `capabilities` What this backend supports. It runs every gate of the library, evaluates a batch of inputs in one call, returns exact expectation values only (it does not sample shots), and has no limit on the number of qubits. See Capabilities. #### `__init__(config=None, name='numpy')` Build the backend from a device configuration. Parameters: | Name | Type | Description | Default | | -------- | ------------- | ------------------------------------------------------------- | --------- | | `config` | `NumpyConfig` | The device settings. Default: None, which uses NumpyConfig(). | `None` | | `name` | `str` | The registry name, used in error messages. Default: "numpy". | `'numpy'` | #### `compile(circuit, observables)` Prepare `circuit` for repeated evaluation of `observables`. A `Circuit` calls it once and reuses the result for every call, so the expensive work belongs here. Parameters: | Name | Type | Description | Default | | ------------- | ---------------------- | --------------------------- | ---------- | | `circuit` | `CircuitSpec` | The circuit to prepare. | *required* | | `observables` | `Sequence[Observable]` | The observables to measure. | *required* | Returns: | Type | Description | | --------------- | ------------------------------------------------ | | `NumpyCompiled` | The prepared circuit, as a NumpyCompiled object. | ## `eigenkit.core.backends.pennylane_backend` The PennyLane backend. It declares `gradient_method = "native"`: the PyTorch interface of PennyLane carries the gradients through the tape, so the parameter-shift rule is not needed here. `compile()` builds the device, both qnodes (one for NumPy and one for PyTorch) and the Pauli operators once. Each `run()` only binds the parameters into the cached qnodes. A finite number of shots is set on each qnode with `qml.set_shots`, because passing `shots=` to `qml.device` was deprecated in PennyLane 0.43. Example ```python from eigenkit import Circuit, Encoding, Variational, all_z blocks = [Encoding(in_features=4), Variational(depth=2)] # by registered name circuit = Circuit(blocks=blocks, observables=all_z(4), provider="pennylane") # or with the config, when a device setting matters from eigenkit.configs import PennyLaneConfig circuit = Circuit( blocks=blocks, observables=all_z(4), provider=PennyLaneConfig(device_name="lightning.qubit"), ) ``` This backend needs the `pennylane` extra. ### `PennyLaneBackend` The provider for PennyLane devices. It is registered as `"pennylane"`. It is internal: you select this backend with `provider="pennylane"` or by passing a PennyLaneConfig, not by building this class. #### `capabilities` What this backend supports. It runs every gate, evaluates a batch of inputs in one call, can sample a finite number of shots, and has no limit on the number of qubits. It also provides the two features that Iterative Quantum Amplitude Estimation (IQAE) needs: the dense unitary of a callable oracle, through `qml.matrix`, and sampling of the ancilla qubit by the provider, for `shot_mode="provider"`. See Capabilities. #### `__init__(config=None, name='pennylane')` Build the backend from a device configuration. Parameters: | Name | Type | Description | Default | | -------- | ----------------- | ----------------------------------------------------------------- | ------------- | | `config` | `PennyLaneConfig` | The device settings. Default: None, which uses PennyLaneConfig(). | `None` | | `name` | `str` | The registry name, used in error messages. Default: "pennylane". | `'pennylane'` | #### `compile(circuit, observables)` Prepare `circuit` for repeated evaluation of `observables`. A `Circuit` calls it once and reuses the result for every call, so the expensive work belongs here. Parameters: | Name | Type | Description | Default | | ------------- | ---------------------- | --------------------------- | ---------- | | `circuit` | `CircuitSpec` | The circuit to prepare. | *required* | | `observables` | `Sequence[Observable]` | The observables to measure. | *required* | Returns: | Type | Description | | ------------------- | ---------------------------------------------------- | | `PennyLaneCompiled` | The prepared circuit, as a PennyLaneCompiled object. | ### `PennyLaneCompiled` Bases: `CompiledCircuit` A circuit prepared for PennyLane. It is internal: PennyLaneBackend.compile() builds it and returns it, and it is never built directly. It has a `run_torch()` method, so the PyTorch interface differentiates through PennyLane itself instead of falling back to the parameter-shift rule. #### `__init__(circuit, observables, config)` Prepare the circuit for repeated evaluation. `PennyLaneBackend.compile()` builds this object, so there is no reason to build one directly. Parameters: | Name | Type | Description | Default | | ------------- | ---------------------- | --------------------------- | ---------- | | `circuit` | `CircuitSpec` | The circuit to prepare. | *required* | | `observables` | `Sequence[Observable]` | The observables to measure. | *required* | | `config` | `PennyLaneConfig` | The device settings. | *required* | #### `jacobian(weights, inputs, *, need_input_grad=False)` Compute the gradient with PennyLane, using the configured `diff_method`. The gradient code of the core package looks for a `jacobian` method, and prefers it to its own parameter-shift rule, which works on any backend. Here PennyLane differentiates the gate angles with its own engine (adjoint, backpropagation or parameter shift, as `diff_method` selects). These gate-angle gradients are then mapped onto the weights and inputs with the shared symbolic chain-rule factors, so angles built with `Scale`, `Multiply`, `Sum` or `Negate` are handled. It takes a single sample (1-D) only, and the caller loops over a batch. Parameters: | Name | Type | Description | Default | | ----------------- | --------- | ------------------------------------------------------------------------------ | ---------- | | `weights` | `ndarray` | The parameter vector (1-D). | *required* | | `inputs` | `ndarray` | The input features of one sample (1-D). | *required* | | `need_input_grad` | `bool` | If True, also compute the jacobian with respect to the inputs. Default: False. | `False` | Returns: | Name | Type | Description | | ----------------- | --------- | ----------------------------------------- | | `weight_jacobian` | `ndarray` | The jacobian with respect to the weights. | | `input_jacobian` | \`ndarray | None\` | #### `probabilities(weights, inputs)` Return the full measurement distribution over the `2**n_qubits` basis states. Qubit `0` is the most significant bit. The distribution comes from `qml.probs`, through a qnode that is built on first use and then cached. Parameters: | Name | Type | Description | Default | | --------- | --------- | --------------------------------------- | ---------- | | `weights` | `ndarray` | The parameter vector (1-D). | *required* | | `inputs` | `ndarray` | The input features of one sample (1-D). | *required* | Returns: | Type | Description | | --------- | --------------------------------------------- | | `ndarray` | The probabilities, of shape (2\*\*n_qubits,). | #### `run(weights, inputs)` Evaluate the circuit and return one expectation value per observable. It follows the `CompiledCircuit` contract: the result has shape `(n_obs,)` when `weights` and `inputs` are both 1-D, and `(B, n_obs)` otherwise. Parameters: | Name | Type | Description | Default | | --------- | --------- | --------------------------------------------------------------------------- | ---------- | | `weights` | `ndarray` | The parameter vector: 1-D for one sample, or 2-D with a leading batch axis. | *required* | | `inputs` | `ndarray` | The input features, with the same 1-D or 2-D shape convention as weights. | *required* | Returns: | Type | Description | | --------- | ------------------------------------------------ | | `ndarray` | The expectation values, one for each observable. | #### `run_torch(weights, inputs)` Evaluate the circuit on PyTorch tensors, keeping the result on the tape. This is the path of native automatic differentiation: the gradients flow through the simulator itself instead of through the parameter-shift fallback. Parameters: | Name | Type | Description | Default | | --------- | -------- | ------------------------------------- | ---------- | | `weights` | `Tensor` | The parameter vector, 1-D or batched. | *required* | | `inputs` | `Tensor` | The input features, 1-D or batched. | *required* | Returns: | Type | Description | | -------- | -------------------------------------------------------------------------- | | `Tensor` | A PyTorch tensor of expectation values, which can still be differentiated. | #### `statevector(weights, inputs, initial_state=None)` Return the full complex statevector, computed with `qml.state()`. Qubit `0` is the most significant bit. `initial_state` is added at the start of the circuit as a `qml.StatePrep`, so the evolution starts from that state instead of `|0…0⟩`. A 2-D `inputs` batch returns an array of shape `(B, 2**n_qubits)`. The batch is evaluated in a loop, because reading the state does not use the fast path of parameter broadcasting. This method needs exact mode, because `qml.state` is not defined with a finite number of shots. Parameters: | Name | Type | Description | Default | | --------------- | --------- | ------------------------------------------------------------------------- | ----------------------------------- | | `weights` | `ndarray` | The parameter vector (1-D). | *required* | | `inputs` | `ndarray` | The input features: 1-D for one sample, or 2-D with a leading batch axis. | *required* | | `initial_state` | \`ndarray | None\` | The state to start from, instead of | Returns: | Type | Description | | --------- | ------------------------------------------------------------------------------ | | `ndarray` | The statevector, of shape (2\*\*n_qubits,), or (B, 2\*\*n_qubits) for a batch. | Raises: | Type | Description | | --------------------- | --------------------------------------------------- | | `NotImplementedError` | If the configuration sets a finite number of shots. | #### `zero_state_probability(weights, inputs)` Return the probability of measuring `|0…0⟩` after the circuit has run. On first use, it builds a separate qnode that returns `qml.probs(...)`, and the method returns entry `0` of that distribution. The qnode uses the same device as the expectation-value qnodes. Parameters: | Name | Type | Description | Default | | --------- | --------- | --------------------------------------- | ---------- | | `weights` | `ndarray` | The parameter vector (1-D). | *required* | | `inputs` | `ndarray` | The input features of one sample (1-D). | *required* | Returns: | Type | Description | | ------- | ----------------------------------- | | `float` | The probability, a float in [0, 1]. | ### `PennyLaneConfig` Device settings, used for every `qml.device()` call. Pass it directly to `Circuit(provider=PennyLaneConfig(...))`, or pass it to `register()` under a name to reuse it. Parameters: | Name | Type | Description | Default | | ------------- | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | | `device_name` | `str` | The identifier of the PennyLane simulator plugin: "default.qubit", "lightning.qubit", "lightning.gpu", or any other installed device. Default: "default.qubit". | `'default.qubit'` | | `shots` | \`int | None\` | None computes exact expectation values, and a positive integer samples that many shots for each expectation value. Default: None. | | `diff_method` | `str` | The PennyLane differentiation method. With "best", PennyLane chooses for each device, usually backpropagation on a simulator and the parameter-shift rule on hardware. Default: "best". | `'best'` | #### `make_backend()` Build the backend that this configuration describes. The provider layer calls it when a configuration is passed as `provider=`. There is no reason to call it directly. Returns: | Type | Description | | -------------------- | -------------------------------------------- | | `'PennyLaneBackend'` | The PennyLaneBackend for this configuration. | ## `eigenkit.core.backends.qiskit_aer_backend` The Qiskit Aer backend, with the statevector and matrix product state (MPS) methods. Aer comes with the `qiskit` extra (`pip install eigenkit[qiskit]`), so this backend needs no new dependency. It reuses, unchanged, the translation of the plain Qiskit backend: the gate translation, the binding of the symbolic `Parameter` values and the `SparsePauliOp` observables. Only the execution is different. Instead of `Statevector.from_instruction`, it runs the bound circuit on an `AerSimulator` and reads the expectation values with the Aer `save_expectation_value` instruction. As a result, the **same** code measures with both the exact statevector simulator and the MPS simulator, which `method` selects. `method="statevector"` is the dense Aer simulator, which is exact. `method="matrix_product_state"` is the Aer MPS simulator. It is exact when the bond dimension is not bounded, which is the Aer default. Otherwise, it is a fast approximation for circuits with little entanglement. The provider names are `qiskit-aer` for the statevector method and `qiskit-aer-mps` for the MPS method. Both methods get the circuit translated first to gates that they can run (see `_to_native_basis`). The MPS method rejects `cry` with "contains invalid instructions" instead of decomposing it, and the statevector method cannot run the controlled unitary that `ControlledQubitUnitary` becomes. The backend has `gradient_method = "parameter_shift"`. Aer has no cheap analytic reverse-mode Jacobian for these expectation values, so the dispatch layer computes the gradients with the parameter-shift rule, as for plain Qiskit. Expectation values do not depend on the qubit ordering convention, as long as the circuit and the operator use the same one, and here both use the Qiskit convention. So `run` needs no reordering. Only the statevectors that are read out are reordered to the convention of the library, where qubit 0 is the most significant bit. Example ```python from eigenkit import Circuit, Encoding, Variational, all_z blocks = [Encoding(in_features=4), Variational(depth=2)] # by its registered name circuit = Circuit(blocks=blocks, observables=all_z(4), provider="qiskit-aer") # or with the configuration, when a device setting matters from eigenkit.configs import QiskitAerConfig circuit = Circuit( blocks=blocks, observables=all_z(4), provider=QiskitAerConfig(method="matrix_product_state"), ) ``` The name `"qiskit-aer-mps"` selects the same backend with the MPS method already set. The backend comes with the `qiskit` extra. ### `QiskitAerBackend` The provider for Qiskit Aer. It is registered as `"qiskit-aer"` for the statevector method and as `"qiskit-aer-mps"` for the matrix product state method. This class is internal. Select the backend with `provider="qiskit-aer"` or by passing a QiskitAerConfig, instead of constructing this class. #### `capabilities` Return what this backend supports. It supports every gate, but only exact expectation values. It does not batch, and it has no limit on the number of qubits. The adjoint of `AmplitudeEncoding` is excluded, because the Qiskit gate translation does not implement it. See Capabilities. #### `__init__(config=None, name=None)` Build the backend from a device configuration. Parameters: | Name | Type | Description | Default | | -------- | ----------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------- | | `config` | \`QiskitAerConfig | None\` | The device settings. None uses QiskitAerConfig(). | | `name` | \`str | None\` | The registry name, used in error messages. None gives "qiskit-aer-mps" for the matrix product state method and "qiskit-aer" otherwise. | #### `compile(circuit, observables)` Prepare `circuit` for repeated evaluation of `observables`. It is called once per `Circuit`, and the handle is reused for every call, so the expensive work is done here. Parameters: | Name | Type | Description | Default | | ------------- | ---------------------- | ----------------------------------------- | ---------- | | `circuit` | `CircuitSpec` | The circuit to prepare. | *required* | | `observables` | `Sequence[Observable]` | The observables that the handle measures. | *required* | Returns: | Type | Description | | ------------------- | --------------------------- | | `QiskitAerCompiled` | A QiskitAerCompiled handle. | ### `QiskitAerCompiled` Bases: `CompiledCircuit` A circuit prepared for Qiskit Aer. This class is internal. It is built by QiskitAerBackend.compile() and obtained from it, and it is never constructed directly. #### `__init__(circuit, observables, config)` Prepare the circuit for repeated evaluation. `QiskitAerBackend.compile()` builds it, and there is no reason to construct one directly. Parameters: | Name | Type | Description | Default | | ------------- | ---------------------- | ------------------------------------------ | ---------- | | `circuit` | `CircuitSpec` | The circuit to prepare. | *required* | | `observables` | `Sequence[Observable]` | The observables that this handle measures. | *required* | | `config` | `QiskitAerConfig` | The device settings. | *required* | #### `run(weights, inputs)` Evaluate the circuit and return one expectation value per observable. It follows the `CompiledCircuit` contract: the result has shape `(n_obs,)` when `weights` and `inputs` are both 1-D, and `(B, n_obs)` otherwise. Parameters: | Name | Type | Description | Default | | --------- | --------- | ------------------------------------------------------------------------------------------- | ---------- | | `weights` | `ndarray` | The parameter vector: a 1-D array for one sample, or a 2-D array with a leading batch axis. | *required* | | `inputs` | `ndarray` | The input features, with the same 1-D or 2-D shapes as weights. | *required* | Returns: | Type | Description | | --------- | ------------------------------------- | | `ndarray` | One expectation value per observable. | #### `statevector(weights, inputs, initial_state=None)` Return the full complex statevector after the circuit. In the result, qubit 0 is the most significant bit. When `inputs` is 2-D, the method loops over its rows and returns an array of shape `(B, 2**n_qubits)`. `initial_state`, in the same order as the result, is loaded with the Aer `set_statevector` instruction. Parameters: | Name | Type | Description | Default | | --------------- | --------- | ----------------------------------------------------------------------------------------- | ----------------------------------- | | `weights` | `ndarray` | The parameter vector, which must be 1-D. | *required* | | `inputs` | `ndarray` | The input features: a 1-D array for one sample, or a 2-D array with a leading batch axis. | *required* | | `initial_state` | \`ndarray | None\` | The state to evolve from instead of | Returns: | Type | Description | | --------- | ------------------------------------------------------------------------------ | | `ndarray` | The statevector, of shape (2\*\*n_qubits,), or (B, 2\*\*n_qubits) for a batch. | #### `zero_state_probability(weights, inputs)` Return the probability of the all-zeros state for one sample. It is the squared magnitude of the all-zeros amplitude of the simulated statevector, and it is the basic operation of fidelity kernels. Parameters: | Name | Type | Description | Default | | --------- | --------- | ---------------------------------------------------- | ---------- | | `weights` | `ndarray` | The parameter vector, which must be 1-D. | *required* | | `inputs` | `ndarray` | The input features of one sample, which must be 1-D. | *required* | Returns: | Type | Description | | ------- | ----------------------------------- | | `float` | The probability, a float in [0, 1]. | ### `QiskitAerConfig` Device configuration for the Qiskit Aer backend. Parameters: | Name | Type | Description | Default | | -------- | ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `method` | `str` | The Aer simulation method. "statevector" selects the exact dense simulator. "matrix_product_state" selects the matrix product state (MPS) simulator, which is exact when the bond dimension is not bounded, and an approximation for circuits with little entanglement otherwise. The provider name "qiskit-aer-mps" is this configuration with the MPS method already set. Any other value raises ValueError. Default: "statevector". | `'statevector'` | | `shots` | \`int | None\` | None returns exact expectation values. Finite-shot sampling is not supported yet, and any other value makes the compilation raise NotImplementedError. Default: None. | #### `make_backend()` Build the backend that this configuration describes. The provider layer calls it when a configuration is passed as `provider=`. There is no reason to call it directly. Returns: | Type | Description | | -------------------- | -------------------------------------------- | | `'QiskitAerBackend'` | The QiskitAerBackend for this configuration. | ## `eigenkit.core.backends.qiskit_backend` The Qiskit backend. Qiskit is an optional dependency (`pip install eigenkit[qiskit]`). It is imported only when it is needed, so the rest of the library works without it, and `compile()` raises a clear `ImportError` if it is missing. The backend has `gradient_method = "parameter_shift"`. Qiskit has no automatic differentiation for statevector expectation values, so the dispatch layer computes the gradients with the parameter-shift rule. `compile()` builds a single parametrised `QuantumCircuit` whose gates use `qiskit.circuit.Parameter` symbols. Each `run()` calls `assign_parameters` on this circuit, so the circuit is not rebuilt at each call. The `SparsePauliOp` operators are also built only once. Example ```python from eigenkit import Circuit, Encoding, Variational, all_z blocks = [Encoding(in_features=4), Variational(depth=2)] # by its registered name circuit = Circuit(blocks=blocks, observables=all_z(4), provider="qiskit") # or with the configuration, when a device setting matters from eigenkit.configs import QiskitConfig circuit = Circuit( blocks=blocks, observables=all_z(4), provider=QiskitConfig(), ) ``` The name `"qiskit"` is registered when the `qiskit` extra is installed. ### `QiskitBackend` The provider for Qiskit, registered as `"qiskit"`. This class is internal. Select the backend with `provider="qiskit"` or by passing a QiskitConfig, instead of constructing this class. #### `capabilities` Return what this backend supports. It supports every gate, but only exact expectation values, because finite-shot sampling is not supported yet. It does not batch, and it has no limit on the number of qubits. The adjoint of `AmplitudeEncoding` is excluded, because the Qiskit gate translation does not implement it. See Capabilities. #### `__init__(config=None, name='qiskit')` Build the backend from a device configuration. Parameters: | Name | Type | Description | Default | | -------- | -------------- | ------------------------------------------ | ---------------------------------------------- | | `config` | \`QiskitConfig | None\` | The device settings. None uses QiskitConfig(). | | `name` | `str` | The registry name, used in error messages. | `'qiskit'` | #### `compile(circuit, observables)` Prepare `circuit` for repeated evaluation of `observables`. It is called once per `Circuit`, and the handle is reused for every call, so the expensive work is done here. Parameters: | Name | Type | Description | Default | | ------------- | ---------------------- | ----------------------------------------- | ---------- | | `circuit` | `CircuitSpec` | The circuit to prepare. | *required* | | `observables` | `Sequence[Observable]` | The observables that the handle measures. | *required* | Returns: | Type | Description | | ---------------- | ------------------------ | | `QiskitCompiled` | A QiskitCompiled handle. | ### `QiskitCompiled` Bases: `CompiledCircuit` A circuit prepared for Qiskit. This class is internal. It is built by QiskitBackend.compile() and obtained from it, and it is never constructed directly. #### `__init__(circuit, observables, config)` Prepare the circuit for repeated evaluation. `QiskitBackend.compile()` builds it, and there is no reason to construct one directly. Parameters: | Name | Type | Description | Default | | ------------- | ---------------------- | ------------------------------------------ | ---------- | | `circuit` | `CircuitSpec` | The circuit to prepare. | *required* | | `observables` | `Sequence[Observable]` | The observables that this handle measures. | *required* | | `config` | `QiskitConfig` | The device settings. | *required* | #### `run(weights, inputs)` Evaluate the circuit and return one expectation value per observable. It follows the `CompiledCircuit` contract: the result has shape `(n_obs,)` when `weights` and `inputs` are both 1-D, and `(B, n_obs)` otherwise. Parameters: | Name | Type | Description | Default | | --------- | --------- | ------------------------------------------------------------------------------------------- | ---------- | | `weights` | `ndarray` | The parameter vector: a 1-D array for one sample, or a 2-D array with a leading batch axis. | *required* | | `inputs` | `ndarray` | The input features, with the same 1-D or 2-D shapes as weights. | *required* | Returns: | Type | Description | | --------- | ------------------------------------- | | `ndarray` | One expectation value per observable. | #### `statevector(weights, inputs, initial_state=None)` Return the full complex statevector after the circuit. In the result, qubit 0 is the most significant bit. The amplitudes are reordered from the Qiskit convention, where qubit 0 is the least significant bit, so that they match the NumPy and PennyLane backends. `initial_state`, in the same order as the result, is evolved through the bound circuit, so the evolution starts from it instead of `|0…0⟩`. When `inputs` is 2-D, the method loops over its rows and returns an array of shape `(B, 2**n_qubits)`. Parameters: | Name | Type | Description | Default | | --------------- | --------- | ----------------------------------------------------------------------------------------- | ----------------------------------- | | `weights` | `ndarray` | The parameter vector, which must be 1-D. | *required* | | `inputs` | `ndarray` | The input features: a 1-D array for one sample, or a 2-D array with a leading batch axis. | *required* | | `initial_state` | \`ndarray | None\` | The state to evolve from instead of | Returns: | Type | Description | | --------- | ------------------------------------------------------------------------------ | | `ndarray` | The statevector, of shape (2\*\*n_qubits,), or (B, 2\*\*n_qubits) for a batch. | #### `zero_state_probability(weights, inputs)` Return the probability of the all-zeros state for one sample. It is the squared magnitude of the all-zeros amplitude of the simulated statevector. Parameters: | Name | Type | Description | Default | | --------- | --------- | ---------------------------------------------------- | ---------- | | `weights` | `ndarray` | The parameter vector, which must be 1-D. | *required* | | `inputs` | `ndarray` | The input features of one sample, which must be 1-D. | *required* | Returns: | Type | Description | | ------- | ----------------------------------- | | `float` | The probability, a float in [0, 1]. | ### `QiskitConfig` Device configuration for the Qiskit backend. Pass it to `Circuit(provider=QiskitConfig(...))`, or register it under a name with `register()` to use it again. Parameters: | Name | Type | Description | Default | | ------- | ----- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `shots` | \`int | None\` | None computes exact expectation values with Statevector. Finite-shot sampling is not supported on this backend yet, and any other value makes the compilation raise NotImplementedError. For sampling, use the Qiskit Runtime backend, which can also run locally on a fake device. Default: None. | #### `make_backend()` Build the backend that this configuration describes. The provider layer calls it when a configuration is passed as `provider=`. There is no reason to call it directly. Returns: | Type | Description | | ----------------- | ----------------------------------------- | | `'QiskitBackend'` | The QiskitBackend for this configuration. | ## `eigenkit.core.backends.qiskit_runtime_backend` The Qiskit Runtime backend, for IBM Quantum hardware. Qiskit and qiskit-ibm-runtime are optional dependencies (`pip install eigenkit[qiskit]`). They are imported only when they are needed, so the rest of the library works without them. If Qiskit is missing, `compile()` raises a clear `ImportError`. If qiskit-ibm-runtime is missing, the same kind of error comes when it is first needed: when `compile()` looks up a backend by name, or when the first job is submitted. The backend runs on real IBM backends (Heron devices such as `ibm_fez`, `ibm_marrakesh` and `ibm_torino`), on the device that `service.least_busy()` chooses (`backend_name="least_busy"`), and, for local testing, on the fake backends with noise models from `qiskit_ibm_runtime.fake_provider` and on `qiskit_aer.AerSimulator`. `QiskitRuntimeConfig(primitive=...)` chooses one of two primitives: - `"estimator"`, the default, computes the expectation values of Pauli sums with `EstimatorV2`, which does the basis rotation and the averaging on the server. Each observable is one Primitive Unified Bloc (PUB) of a single job, and only the numbers come back. - `"sampler"` computes the same expectation values from raw histograms of bitstrings with `SamplerV2`. The Pauli terms of the observables are sorted into qubit-wise commuting measurement groups, one circuit is run for each group, and the counts are reduced locally (see the `_sampling` module). There is one PUB per *group* instead of one per observable, and the shot budget is explicit. This is what you want when the shot cost of a result is itself what you measure. `sample()` and `probabilities()` always use `SamplerV2`, whatever the configured primitive, because they are sampling operations and `EstimatorV2` returns no bitstrings. `primitive` only chooses how the expectation values are computed. `submit()` runs `run`, `sample` or `probabilities` without blocking (see `QiskitRuntimeCompiled.submit_methods`), and it can tag the IBM job so that you can find it again. `attach()` rebuilds a handle from the id of a job submitted earlier, for example after the process that submitted it restarted. The `result()` of either handle returns what the blocking method of the same name returns. `zero_state_probability()` and its batched form `zero_state_probabilities()` are used by quantum kernels. They run one `SamplerV2` job, measured in the `Z` basis, for the whole batch. They are not `submit()` methods: `submit_zero_state_probabilities()` submits the batch without blocking, and `retrieve_zero_state_probabilities()` attaches to it again by its job id. The credentials are looked up in this order, and the most explicit one wins: 1. `QiskitRuntimeConfig(token=..., channel=..., instance=...)`. 1. The environment variable `QISKIT_IBM_TOKEN`. 1. The saved account (`~/.qiskit/qiskit-ibm.json`). The channel defaults to `"ibm_quantum_platform"`. The legacy `"ibm_quantum"` channel was retired with the old IBM Quantum Platform, and `QiskitRuntimeService` no longer accepts it at all. For testing, pass a `FakeBackendV2` instance directly as `backend_instance`. No token or service is needed. By default, `gradient_method = "none"`, because runs on a quantum processing unit (QPU) are too expensive for parameter-shift gradients: one backward step costs at least `2 * n_weights` jobs, each with the full shot count. The PyTorch wrapper raises a clear error on `.backward()`. Train on a simulator first, then use the trained weights on the QPU. `enable_gradients=True` in the configuration turns on parameter-shift gradients on the device, and compiling for a paid device then gives a `CostWarning` that states the cost of one backward pass. `compile()` runs `qiskit.transpile(circuit, backend=target, optimization_level=N)` once and caches the transpiled circuit. For the estimator, it also caches the `SparsePauliOp` operators, mapped to the layout of the transpiled circuit. The sampler transpiles each measured circuit on its first use and caches it too. Each `run()` or `submit()` then only binds the parameter values in the PUBs that it passes to the primitive. Example ```python from eigenkit import Circuit, Encoding, Variational, all_z, register from eigenkit.configs import QiskitRuntimeConfig blocks = [Encoding(in_features=4), Variational(depth=2)] # by a registered name (this backend is opt-in, so register it first) register("qiskit-runtime", QiskitRuntimeConfig(backend_name="ibm_fez")) circuit = Circuit(blocks=blocks, observables=all_z(4), provider="qiskit-runtime") # or with the configuration, when a device setting matters circuit = Circuit( blocks=blocks, observables=all_z(4), provider=QiskitRuntimeConfig(backend_name="ibm_fez"), ) ``` It is opt-in, and it costs money. The first compile for a paid device gives a `CostWarning`, and `dry_run_info()` reports the shape of a run, including how many circuits a call runs, before you submit it. ### `QiskitRuntimeBackend` The provider for IBM Quantum hardware through Qiskit Runtime. It is opt-in: the name `"qiskit-runtime"` exists only after the caller registers it, for example with `register("qiskit-runtime", QiskitRuntimeConfig(...))`. It costs money: the first compile for a paid device gives a CostWarning, and `dry_run_info()` reports the shape of a run beforehand. It does not estimate a price. This class is internal. Select the backend with `provider="qiskit-runtime"` or by passing a QiskitRuntimeConfig, instead of constructing this class. #### `capabilities` Return what this backend supports. It supports every gate except `AmplitudeEncoding` (state preparation), `QubitUnitary` and `ControlledQubitUnitary` (arbitrary unitaries) and `CPHASE`. It supports finite-shot sampling, which a quantum processing unit (QPU) always uses. It supports batching, because the primitives evaluate a batch of parameter values in one job when `weights` is 1-D and `inputs` is 2-D. It has no limit of its own on the number of qubits: the limit of the device applies. See Capabilities. #### `gradient_method` Return the gradient method, which depends on `enable_gradients`. It is `"parameter_shift"` when the configuration sets `enable_gradients`, and `"none"` otherwise. A parameter-shift backward pass on a quantum processing unit (QPU) costs at least `2 * n_weights` jobs, each with the full shot count, so gradients are refused unless the caller opts in. #### `__init__(config=None, name='qiskit-runtime')` Build the backend from a device configuration. Parameters: | Name | Type | Description | Default | | -------- | --------------------- | ------------------------------------------ | ----------------------------------------------------- | | `config` | \`QiskitRuntimeConfig | None\` | The device settings. None uses QiskitRuntimeConfig(). | | `name` | `str` | The registry name, used in error messages. | `'qiskit-runtime'` | #### `compile(circuit, observables)` Prepare `circuit` for repeated evaluation of `observables`. It is called once per `Circuit`, and the handle is reused for every call, so the expensive work is done here. Parameters: | Name | Type | Description | Default | | ------------- | ---------------------- | ----------------------------------------- | ---------- | | `circuit` | `CircuitSpec` | The circuit to prepare. | *required* | | `observables` | `Sequence[Observable]` | The observables that the handle measures. | *required* | Returns: | Type | Description | | ----------------------- | ------------------------------- | | `QiskitRuntimeCompiled` | A QiskitRuntimeCompiled handle. | ### `QiskitRuntimeCompiled` Bases: `CompiledCircuit` A circuit transpiled for an IBM Quantum backend. This class is internal. It is built by QiskitRuntimeBackend.compile() and obtained from it, and it is never constructed directly. The circuit is transpiled once and cached (each measured circuit of the sampler on its first use), so `run()` and `submit()` only bind the parameter values. `submit()` returns a QiskitRuntimeJobHandle without blocking, and `attach()` rebuilds that handle from a job id. #### `submit_methods = _SUBMIT_METHODS` The methods `submit()` and `attach()` accept: `"run"`, `"sample"` and `"probabilities"`. `zero_state_probability` is not one of them: its batched async form is `submit_zero_state_probabilities()`. Any method added to this adapter's async path must also be added here, or callers that check this set will refuse it. #### `__init__(circuit, observables, config, *, warn=True)` Prepare the circuit for repeated evaluation. `QiskitRuntimeBackend.compile()` builds it, and there is no reason to construct one directly. Parameters: | Name | Type | Description | Default | | ------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `circuit` | `CircuitSpec` | The circuit to prepare. | *required* | | `observables` | `Sequence[Observable]` | The observables that this handle measures. | *required* | | `config` | `QiskitRuntimeConfig` | The device settings. | *required* | | `warn` | `bool` | Whether to give the cost warnings for a paid device. It is False only for the shifted copies that compile_shifted builds. Default: True. | `True` | #### `attach(job_id, *, method='run')` Rebuild the handle of a job submitted earlier from this circuit. Use it to pick up a job from a process other than the one that submitted it, for example after a crash. The handle decodes results exactly as the handle that `submit()` returned would have, provided that the job was submitted from the same circuit, observables, primitive and `method`. Parameters: | Name | Type | Description | Default | | -------- | ----- | ------------------------------------------------------------------ | ---------- | | `job_id` | `str` | The IBM job id, as returned by QiskitRuntimeJobHandle.job_id(). | *required* | | `method` | `str` | The method that the job was submitted with, one of submit_methods. | `'run'` | Returns: | Name | Type | Description | | -------- | ----------- | ------------------------------ | | `handle` | `JobHandle` | A handle for the existing job. | Raises: | Type | Description | | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ValueError` | If job_id is not made of 1 to 64 letters, digits, _ or -, if method is not in submit_methods, or if the backend is not an IBM Runtime backend with a service to look the job up in (a fake backend has none). In all these cases, the service is not contacted. | #### `compile_shifted(circuit)` Compile a copy of this circuit with one gate angle shifted. The chain-rule gradient compiles one copy per shifted gate. Each copy runs on the device of this handle, never on a new `"least_busy"` choice, and is transpiled with the seed of this handle, so every term of the gradient comes from the same qubits. The compile of this handle already gave the cost warnings, so the copy gives none. Parameters: | Name | Type | Description | Default | | --------- | ------------- | -------------------- | ---------- | | `circuit` | `CircuitSpec` | The shifted circuit. | *required* | Returns: | Type | Description | | ------------------------- | ------------------------------------------- | | `'QiskitRuntimeCompiled'` | A QiskitRuntimeCompiled handle for circuit. | #### `counts(weights, inputs)` Return the histogram in the computational basis for one sample. It is always a `SamplerV2` job, whatever `primitive` the configuration names, because `EstimatorV2` has no bitstrings to return. It is one job of `shots` shots. Parameters: | Name | Type | Description | Default | | --------- | --------- | ---------------------------------------- | ---------- | | `weights` | `ndarray` | The parameter vector, which must be 1-D. | *required* | | `inputs` | `ndarray` | The input features, which must be 1-D. | *required* | Returns: | Name | Type | Description | | -------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------- | | `counts` | `dict[str, int]` | The number of shots for each measured bitstring. The first character of each key is qubit 0, which is the most significant bit. | #### `dry_run_info()` Report the size of a run, before anything is submitted. It reports the target backend and whether it is a fake one, the primitive in use, the number of shots, the optimisation level, the transpiler seed set in the configuration, the numbers of logical and physical qubits, the gate counts and the depth of the transpiled circuit, the numbers of observables and of measurement groups, and how many circuits one call runs: one per observable with the estimator, and one per measurement group with the sampler. `transpiled_gate_counts` and `transpiled_depth` describe the circuit that is actually submitted. With the estimator, this is the circuit without measurements, which carries its observables separately, so both are single values. With the sampler, there is one circuit per measurement group, with the basis rotation and the measurements added *before* transpiling, so that the layout can place them on the qubits that the circuit ran on. Both are then dictionaries keyed by basis, as in the IQM backend. Reporting the circuit without measurements there would understate a routed circuit, because the transpiler sees a different input and chooses a different layout. Returns: | Name | Type | Description | | ------ | ------ | -------------------------------- | | `info` | `dict` | The fields above, keyed by name. | #### `probabilities(weights, inputs)` Return the measurement distribution over all `2**n_qubits` basis states. It is the estimate from the shot frequencies, not the exact distribution: on hardware, the basis states that were never sampled come back as exact zeros. It is limited to circuits of at most `_sampling.MAX_DENSE_QUBITS` (`24`) qubits. For a wider circuit, read `sample()` directly. Parameters: | Name | Type | Description | Default | | --------- | --------- | ---------------------------------------- | ---------- | | `weights` | `ndarray` | The parameter vector, which must be 1-D. | *required* | | `inputs` | `ndarray` | The input features, which must be 1-D. | *required* | Returns: | Name | Type | Description | | --------------- | --------- | ------------------------------------------------------------- | | `probabilities` | `ndarray` | The distribution, of shape (2\*\*n_qubits,), which sums to 1. | #### `resources(measured=True)` Report the resources that the transpiled circuit uses on this device. Parameters: | Name | Type | Description | Default | | ---------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | | `measured` | `bool` | If True, the default, report the circuit measured in the Z basis, which is what kernels and sampling submit. If False, report the circuit without measurements that the estimator runs. | `True` | Returns: | Type | Description | | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `dict` | A dictionary with backend_name, is_fake, n_logical_qubits, optimization_level, seed_transpiler, gate_counts, n_2q_gates, two_qubit_gate_counts, depth, depth_2q, n_2q_gates_unrouted (the same circuit on the native gates with all-to-all connectivity), routing_overhead_2q, physical_qubits, n_physical_qubits_device, and estimated_success_probability, computed from the error rates of the target. | #### `retrieve_zero_state_probabilities(job_id)` Attach again to a job that `submit_zero_state_probabilities()` submitted. The job may have been submitted from another session. Parameters: | Name | Type | Description | Default | | -------- | ----- | ----------------------- | ---------- | | `job_id` | `str` | The IBM Quantum job id. | *required* | Returns: | Type | Description | | ---------------------- | ------------------------------------------------- | | `ProbabilityJobHandle` | A handle whose result() is the probability array. | Raises: | Type | Description | | --------------------- | ---------------------------------------------------------------------------------------------------------------- | | `ValueError` | If job_id is not made of 1 to 64 letters, digits, _ or -. | | `NotImplementedError` | For a backend without a service to look the job up in, such as a fake backend or Aer, which keep no job history. | #### `run(weights, inputs)` Evaluate the circuit and return one expectation value per observable. It follows the `CompiledCircuit` contract: the result has shape `(n_obs,)` when `weights` and `inputs` are both 1-D, and `(B, n_obs)` otherwise. Parameters: | Name | Type | Description | Default | | --------- | --------- | ------------------------------------------------------------------------------------------- | ---------- | | `weights` | `ndarray` | The parameter vector: a 1-D array for one sample, or a 2-D array with a leading batch axis. | *required* | | `inputs` | `ndarray` | The input features, with the same 1-D or 2-D shapes as weights. | *required* | Returns: | Type | Description | | --------- | ------------------------------------- | | `ndarray` | One expectation value per observable. | #### `sample(weights, inputs)` Return the same histogram as `counts()`. This is an alias of counts, under the name that the `Circuit.sample()` dispatch looks for. Parameters: | Name | Type | Description | Default | | --------- | --------- | ---------------------------------------- | ---------- | | `weights` | `ndarray` | The parameter vector, which must be 1-D. | *required* | | `inputs` | `ndarray` | The input features, which must be 1-D. | *required* | Returns: | Name | Type | Description | | -------- | ---------------- | ------------------------------------------------ | | `counts` | `dict[str, int]` | The number of shots for each measured bitstring. | #### `submit(weights, inputs, *, method='run', tags=None)` Submit the circuit without blocking, and return a job handle. Nothing blocks: `handle.result()` waits for the job and decodes it in the same way as the blocking method of the same name. For `"run"`, the handle returns expectation values with either primitive: the counts of the sampler are reduced when the result is collected, so the two paths look the same to the caller. `"sample"` and `"probabilities"` are always one `SamplerV2` job measured in the `Z` basis, whatever the configured primitive. Parameters: | Name | Type | Description | Default | | --------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `weights` | `ndarray` | The parameter vector for one sample (1-D). | *required* | | `inputs` | `ndarray` | The input features for one sample (1-D). | *required* | | `method` | `str` | What the job computes, one of submit_methods. "run", the default, measures the observables, like run(). "sample" returns the measurement histogram, like sample(). "probabilities" returns the distribution over every basis state, like probabilities(). | `'run'` | | `tags` | \`Mapping[str, str] | None\` | Tags to put on the IBM job, so that you can find it again later with QiskitRuntimeService.jobs(job_tags=[...]). Each one becomes the job tag "key=value", sorted by key. IBM allows at most 8 tags of at most 86 characters each. Keys and values must not be empty, and a key must not contain =. | Returns: | Name | Type | Description | | -------- | ----------- | ------------------------------- | | `handle` | `JobHandle` | A handle for the submitted job. | Raises: | Type | Description | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ValueError` | If method is not in submit_methods, if weights or inputs is not 1-D, or if tags breaks the IBM job tag limits. In all these cases, no primitive is built and nothing is submitted. | #### `submit_zero_state_probabilities(weights, inputs)` Submit the all-zeros probability of every row of `inputs` as one job. It submits one `SamplerV2` job and returns without waiting. The circuit is transpiled once and measured in the `Z` basis, and every row binds one set of parameter values of the same Primitive Unified Bloc (PUB), so `B` kernel entries cost one job of `B * shots` shots. It works whatever `primitive` the configuration names. Parameters: | Name | Type | Description | Default | | --------- | --------- | ---------------------------------------- | ---------- | | `weights` | `ndarray` | The parameter vector, which must be 1-D. | *required* | | `inputs` | `ndarray` | A batch, of shape (B, n_inputs). | *required* | Returns: | Type | Description | | ---------------------- | ---------------------------------------------------------------- | | `ProbabilityJobHandle` | A handle whose result() is the probability array, of shape (B,). | #### `transpiled_circuit(measured=True)` Return the Qiskit circuit that this backend submits, to draw or inspect it. Parameters: | Name | Type | Description | Default | | ---------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | | `measured` | `bool` | If True, the default, return the circuit measured in the Z basis that sample(), probabilities() and the kernel path run. If False, return the transpiled circuit without measurements that the estimator runs. | `True` | Returns: | Type | Description | | ------------------ | ----------------------------------- | | `'QuantumCircuit'` | A transpiled qiskit.QuantumCircuit. | #### `zero_state_probabilities(weights, inputs)` Return the all-zeros probability of every row of `inputs`, from one job. Parameters: | Name | Type | Description | Default | | --------- | --------- | ---------------------------------------- | ---------- | | `weights` | `ndarray` | The parameter vector, which must be 1-D. | *required* | | `inputs` | `ndarray` | A batch, of shape (B, n_inputs). | *required* | Returns: | Type | Description | | --------- | -------------------------------------------- | | `ndarray` | The estimates from the shots, of shape (B,). | #### `zero_state_probability(weights, inputs)` Return the all-zeros probability for one sample, estimated from shots. Parameters: | Name | Type | Description | Default | | --------- | --------- | ---------------------------------------- | ---------- | | `weights` | `ndarray` | The parameter vector, which must be 1-D. | *required* | | `inputs` | `ndarray` | The input features, which must be 1-D. | *required* | Returns: | Type | Description | | ------- | ---------------- | | `float` | The probability. | ### `QiskitRuntimeConfig` Bases: `SecretFieldsMixin` Device configuration for the Qiskit Runtime backend. There are two ways to choose the target backend: - `backend_name` is looked up with `QiskitRuntimeService`. It needs credentials: a token, an environment variable or a saved account. - `backend_instance` is a `BackendV2` that is used directly. It is useful for tests with a fake backend, or to pass any backend that is already configured. Parameters: | Name | Type | Description | Default | | ---------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `backend_name` | \`str | None\` | The IBM backend name, for example "ibm_fez", or "least_busy" for the least busy operational device that the account can use, chosen once per compiled circuit. It is required unless backend_instance is set, and it is ignored when it is. Default: None. | | `backend_instance` | `Any` | A backend that is already configured. It takes precedence over backend_name. Default: None. | `None` | | `shots` | `int` | The number of shots per circuit run. Default: 1024. | `1024` | | `optimization_level` | `int` | The optimisation level passed to qiskit.transpile, from 0 to 3. Default: 1. | `1` | | `seed_transpiler` | \`int | None\` | The seed of the random passes of the transpiler, mainly the SabreLayout and SabreSwap passes, which choose the layout and the routing. None, the default, draws a new seed for every compile, so a circuit that needs routing compiles to a different number of gates on every run. Within one compile, all the transpilations share the seed, including the shifted copies that a gradient on hardware compiles. Set it whenever two compilations must be comparable. Since compiling submits nothing, trying a few seeds and keeping the cheapest circuit is a cheap and fair way to choose a layout. | | `primitive` | `Primitive` | The Runtime primitive that computes the expectation values. "estimator", the default, uses EstimatorV2, with one Primitive Unified Bloc (PUB) per observable in each job. "sampler" uses SamplerV2, with one PUB per qubit-wise commuting measurement group, and reduces the raw counts locally. sample() and probabilities() use SamplerV2 with either value. | `'estimator'` | | `group_commuting` | `bool` | Whether the sampler puts qubit-wise commuting Pauli terms into shared measurement circuits. False runs one circuit per term, which runs more circuits but keeps the shot budget of each term independent. The estimator ignores it. Default: True. | `True` | | `token` | \`str | None\` | The token passed to QiskitRuntimeService when backend_instance is None. With None, the backend uses QISKIT_IBM_TOKEN, then the saved account file. It is secret: it is never shown by repr(), and it is never saved in .eigenkit bundles, pickles or joblib checkpoints. After a load it is None, so set QISKIT_IBM_TOKEN. | | `channel` | `str` | The service channel: "ibm_quantum_platform" (the default), "ibm_cloud" or "local". The legacy "ibm_quantum" channel was retired, and QiskitRuntimeService no longer accepts it. | `'ibm_quantum_platform'` | | `instance` | \`str | None\` | The service instance, as a Cloud Resource Name (CRN) or a name, when one applies. Default: None. | | `service_kwargs` | `dict` | Extra keyword arguments passed to QiskitRuntimeService, such as region, url or proxies. | `dict()` | | `poll_timeout_seconds` | `int` | How long result() waits before it gives up, in seconds. Default: 3600. | `3600` | | `enable_gradients` | `bool` | Whether to allow parameter-shift gradients on the device: PyTorch .backward(), jacobian() and gradient-based optimisers. It is off by default, because one backward pass costs at least 2 * n_weights times the jobs of a forward call. When it is on, compiling for a paid device gives a CostWarning that states this cost. | `False` | #### `__post_init__()` Reject an unknown primitive when the configuration is built. If the check waited for the first submission, the error would come as a confusing failure on a paid backend, after the transpilation and the cost warning. #### `make_backend()` Build the backend that this configuration describes. The provider layer calls it when a configuration is passed as `provider=`. There is no reason to call it directly. Returns: | Type | Description | | ------------------------ | ------------------------------------------------ | | `'QiskitRuntimeBackend'` | The QiskitRuntimeBackend for this configuration. | ### `QiskitRuntimeJobHandle` A handle to a Qiskit Runtime job, whose `result()` blocks and decodes it. `compiled.submit(weights, inputs)` and `compiled.attach(job_id)` return one of these. What `result()` returns depends on the `method` of the handle. #### `method` Return the method of the compiled circuit that this job runs. It is `"run"`, `"sample"` or `"probabilities"`. #### `__init__(job, n_observables, poll_timeout_seconds, reduce=None, counts=None, method='run', *, n_qubits=None)` Wrap a submitted Runtime job as a job handle. `QiskitRuntimeCompiled.submit()` and `QiskitRuntimeCompiled.attach()` build it, and it is not constructed directly. Parameters: | Name | Type | Description | Default | | ---------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `job` | `RuntimeJob` | The IBM Runtime job returned by the primitive. | *required* | | `n_observables` | `int` | The number of expectation values to expect back, which sets the shape of the result array. | *required* | | `poll_timeout_seconds` | `int` | How long result() waits before it gives up, in seconds. | *required* | | `reduce` | \`Callable | None\` | The function that turns the result of the primitive into the vector of expectation values. None reads the EstimatorV2 result of each Primitive Unified Bloc (PUB) directly, which is what the estimator submits. The sampler passes its reduction of the counts instead. | | `counts` | \`Callable | None\` | The function that takes the histograms out of the result of the primitive, one per submitted circuit. Only sampler jobs have histograms, because the estimator returns expectation values and never sees a bitstring. With None, counts() raises NotImplementedError. | | `method` | `str` | The method of the compiled circuit that the job runs, one of QiskitRuntimeCompiled.submit_methods. It decides how result() decodes the job. | `'run'` | | `n_qubits` | \`int | None\` | The number of qubits of the circuit, which sets the size of the "probabilities" distribution. | Raises: | Type | Description | | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ValueError` | If method is not in QiskitRuntimeCompiled.submit_methods, if method is "sample" or "probabilities" without counts, or if method is "probabilities" without n_qubits. | #### `cancel()` Ask IBM Quantum to cancel the job, without blocking. #### `counts()` Block until the job finishes, then return the raw histograms. There is one histogram per measurement group. In each key, qubit 0 is the leftmost character, which is the most significant bit. A `"sample"` or `"probabilities"` job measured one circuit, so it has one histogram. These are the numbers that the device returned, and result is these numbers reduced against the observables. Because both come from a single job, a run can be scored again offline, against different observables or with a different metric, without paying for it twice. Raises: | Type | Description | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `NotImplementedError` | For a "run" job on the estimator, which returns expectation values and never sees a bitstring. Set primitive="sampler" to get counts. | #### `job_id()` Return the IBM Quantum job id. Use it to look the job up in the console or in `qiskit-ibm-runtime`. #### `result()` Block until the job finishes, then decode it by the `method` of the handle. A handle from `attach()` decodes a job exactly as the handle that `submit()` returned would have. Returns: | Type | Description | | --------- | ---------------- | | \`ndarray | dict[str, int]\` | Raises: | Type | Description | | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ValueError` | If the results of the job do not have the shape that the method and the circuit of this handle produce, for example because the handle was attached with the wrong method. Also, for "probabilities", if the circuit has more than \_sampling.MAX_DENSE_QUBITS (24) qubits. | #### `status()` Return the state of the job as IBM Quantum reports it, without blocking. Examples of states are `"QUEUED"`, `"RUNNING"`, `"DONE"`, `"CANCELLED"` and `"ERROR"`. ## `eigenkit.core.backends.qsim_backend` The qsim backend, through Cirq. qsim is a fast Schrödinger statevector simulator from Google, and `qsimcirq` connects it to Cirq. Both are in the optional `[qsim]` extra (`pip install eigenkit[qsim]`) and are imported only when needed, so the rest of the library works without them. The circuit is translated once into a **parametrised** `cirq.Circuit`, whose angles are SymPy symbols `w{i}` and `x{i}`. Each call sets the values of the symbols and runs the circuit on a `qsimcirq.QSimSimulator`. Expectation values use the native qsim method `simulate_expectation_values` (each `Observable` of the library becomes a `cirq.PauliSum`), and statevectors come from `simulate(...).final_state_vector`. It declares `gradient_method = "parameter_shift"`: qsim has no cheap analytic reverse-mode jacobian here, so the gradient code of the library differentiates with the parameter-shift rule. **Precision.** qsim is a **single-precision** simulator (32-bit floating point), and the pip wheel has no double-precision (64-bit) build. The outputs are returned as `complex128`, but they carry an accumulated error of about `1e-6`, so they agree with a double-precision backend to about `1e-4`, not to machine precision. This difference comes from the precision and is not a bug, as with the single-precision path of CUDA Quantum. It does mean that qsim is the wrong choice when you need exact agreement rather than speed. Bit order: Cirq orders the statevector with the **first** qubit of `qubit_order` as the most significant bit. This backend passes `[LineQubit(0), …, LineQubit(n-1)]`, so index `0` is qubit `0`, the most significant bit. This is the same convention as in NumPy and PennyLane. No reordering is needed, and the backend comparison tests check this against the NumPy reference. The number of threads follows `OMP_NUM_THREADS`, unless a configuration sets it. On macOS, importing this module sets `KMP_DUPLICATE_LIB_OK=TRUE` (unless it is already set). qsimcirq and PyTorch each include their own OpenMP runtime, and without this setting the second one to start aborts the process. Example ```python from eigenkit import Circuit, Encoding, Variational, all_z blocks = [Encoding(in_features=4), Variational(depth=2)] # by registered name circuit = Circuit(blocks=blocks, observables=all_z(4), provider="qsim") # or with the config, when a device setting matters from eigenkit.configs import QsimConfig circuit = Circuit( blocks=blocks, observables=all_z(4), provider=QsimConfig(threads=8), ) ``` qsim is fast and single-precision, with an error of about `1e-6`. This backend needs the `qsim` extra. ### `QsimBackend` The provider for qsim, the statevector simulator from Google, through Cirq. It is registered as `"qsim"`. It is internal: you select this backend with `provider="qsim"` or by passing a QsimConfig, not by building this class. #### `capabilities` What this backend supports. It runs the subset of gates that can be translated to Cirq, returns exact expectation values only, does not evaluate a batch in one call, and has no limit on the number of qubits. A circuit that uses a gate outside this subset is rejected before compilation, with a message that names the gate. See Capabilities. #### `__init__(config=None, name='qsim')` Build the backend from a device configuration. Parameters: | Name | Type | Description | Default | | -------- | ------------ | ------------------------------------------------------------ | -------- | | `config` | `QsimConfig` | The device settings. Default: None, which uses QsimConfig(). | `None` | | `name` | `str` | The registry name, used in error messages. Default: "qsim". | `'qsim'` | #### `compile(circuit, observables)` Prepare `circuit` for repeated evaluation of `observables`. A `Circuit` calls it once and reuses the result for every call, so the expensive work belongs here. Parameters: | Name | Type | Description | Default | | ------------- | ---------------------- | --------------------------- | ---------- | | `circuit` | `CircuitSpec` | The circuit to prepare. | *required* | | `observables` | `Sequence[Observable]` | The observables to measure. | *required* | Returns: | Type | Description | | -------------- | ----------------------------------------------- | | `QsimCompiled` | The prepared circuit, as a QsimCompiled object. | ### `QsimCompiled` Bases: `CompiledCircuit` A circuit prepared for qsim, as a Cirq circuit. It is internal: QsimBackend.compile() builds it and returns it, and it is never built directly. #### `__init__(circuit, observables, config)` Prepare the circuit for repeated evaluation. `QsimBackend.compile()` builds this object, so there is no reason to build one directly. Parameters: | Name | Type | Description | Default | | ------------- | ---------------------- | --------------------------- | ---------- | | `circuit` | `CircuitSpec` | The circuit to prepare. | *required* | | `observables` | `Sequence[Observable]` | The observables to measure. | *required* | | `config` | `QsimConfig` | The device settings. | *required* | #### `run(weights, inputs)` Evaluate the circuit and return one expectation value per observable. It follows the `CompiledCircuit` contract: the result has shape `(n_obs,)` when `weights` and `inputs` are both 1-D, and `(B, n_obs)` otherwise. Parameters: | Name | Type | Description | Default | | --------- | --------- | --------------------------------------------------------------------------- | ---------- | | `weights` | `ndarray` | The parameter vector: 1-D for one sample, or 2-D with a leading batch axis. | *required* | | `inputs` | `ndarray` | The input features, with the same 1-D or 2-D shape convention as weights. | *required* | Returns: | Type | Description | | --------- | ------------------------------------------------ | | `ndarray` | The expectation values, one for each observable. | #### `statevector(weights, inputs, initial_state=None)` Return the full complex statevector after the circuit has run. The result is a flat array of `2**n_qubits` amplitudes, with qubit `0` as the most significant bit. Parameters: | Name | Type | Description | Default | | --------------- | --------- | ------------------------------------------------------------------------- | ---------------------------------------------- | | `weights` | `ndarray` | The parameter vector (1-D). | *required* | | `inputs` | `ndarray` | The input features: 1-D for one sample, or 2-D with a leading batch axis. | *required* | | `initial_state` | \`ndarray | None\` | Must be None. This backend can only start from | Returns: | Type | Description | | --------- | ------------------------------------------------------------------------------ | | `ndarray` | The statevector, of shape (2\*\*n_qubits,), or (B, 2\*\*n_qubits) for a batch. | Raises: | Type | Description | | --------------------- | ----------------------------- | | `NotImplementedError` | If initial_state is not None. | #### `zero_state_probability(weights, inputs)` Return the probability of measuring the state `|0…0⟩`. Fidelity kernels and swap tests are built on it. It does not go through `Observable`, because it is not the expectation value of a Pauli operator. Parameters: | Name | Type | Description | Default | | --------- | --------- | --------------------------------------- | ---------- | | `weights` | `ndarray` | The parameter vector (1-D). | *required* | | `inputs` | `ndarray` | The input features of one sample (1-D). | *required* | Returns: | Type | Description | | ------- | ----------------------------------- | | `float` | The probability, a float in [0, 1]. | ### `QsimConfig` Device configuration for the qsim backend. Parameters: | Name | Type | Description | Default | | --------- | ----- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `threads` | \`int | None\` | The size of the internal thread pool of qsim. None reads OMP_NUM_THREADS instead, which defaults to 1, so qsim uses one core unless you set it here or in the environment. Set it explicitly before you report any timing. Default: None. | #### `make_backend()` Build the backend that this configuration describes. The provider layer calls it when a configuration is passed as `provider=`. There is no reason to call it directly. Returns: | Type | Description | | --------------- | --------------------------------------- | | `'QsimBackend'` | The QsimBackend for this configuration. | ## `eigenkit.core.backends.qulacs_backend` The Qulacs backend. Qulacs is an optional dependency (`pip install eigenkit[qulacs]`). The package is imported only when it is needed, so the rest of the library works without it, and `compile()` raises a clear `ImportError` if it is missing. It declares `gradient_method = "adjoint"`. Qulacs provides an analytic reverse-mode gradient (`ParametricQuantumCircuit.backprop`), so, as with the NumPy reference simulator, the compiled circuit has a `jacobian` method, and the gradient code of the core package prefers it to the parameter-shift rule, whose cost is `O(n_params)`. #### Compile and run `compile()` builds one `ParametricQuantumCircuit` and the Qulacs `Observable` objects once. Every rotation whose angle depends on the weights or inputs becomes a *parametric slot*. `run()` sets the slots with `set_parameter`, without rebuilding the circuit at each call, and `jacobian()` reads the gradients of all slots from one `backprop` pass for each observable. #### Conventions matched with the NumPy and PennyLane reference - **Rotation sign.** Qulacs rotations are `e^{+iθP/2}`, while the library and PennyLane use `e^{-iθP/2}`. Every rotation angle is negated when it is passed to Qulacs: the Qulacs parameter of a slot is `φ = -(coeff · angle)`. As a result, `backprop` returns `d⟨O⟩/dφ`, and the chain rule through `φ` gives `d⟨O⟩/d(weights)` with the sign handled automatically (see `jacobian`). - **Bit order.** Qulacs stores the amplitudes with qubit `0` as the least significant bit (as Qiskit does), while the library uses qubit `0` as the most significant bit. The statevector input and output, and the matrices of multi-qubit gates, are bit-reversed to match the two. The Pauli *observables* need no reindexing, because the qubit labels match: only the storage order of the amplitudes differs. #### Gate coverage `add_parametric_R{X,Y,Z}` covers the trainable rotations directly. The gates that have no parametric primitive in Qulacs are decomposed into these rotations, so that they stay differentiable: - `PhaseShift(θ)` becomes a parametric `RZ`. The two are equal up to a global phase that cannot be observed. - `CRY(θ)` becomes `RY(θ/2) CX RY(-θ/2) CX`, where `CX` is a controlled-NOT gate. This decomposition is exact. - `CPHASE(θ)` becomes `P(θ/2)_c CX P(-θ/2)_t CX P(θ/2)_t`, with each `P` as an `RZ` slot. This decomposition is exact up to a global phase that cannot be observed. `PhaseShift`, `CPHASE` and `CRY` with a constant angle, and the non-parametric gates (`H`, `X`, `Z`, `S`, `Sdg`, `CNOT`, `SWAP`, `MCX`, `MCZ`, `QubitUnitary`, `ControlledQubitUnitary`, `AmplitudeEncoding`), are added once as exact Qulacs gates. `backprop` accepts these fixed gates, so native adjoint gradients are available for every circuit. Example ```python from eigenkit import Circuit, Encoding, Variational, all_z blocks = [Encoding(in_features=4), Variational(depth=2)] # by registered name circuit = Circuit(blocks=blocks, observables=all_z(4), provider="qulacs") # or with the config, when a device setting matters from eigenkit.configs import QulacsConfig circuit = Circuit( blocks=blocks, observables=all_z(4), provider=QulacsConfig(), ) ``` Gradients are computed natively in reverse mode, with `backprop`. This backend needs the `qulacs` extra. ### `QulacsBackend` The provider for the Qulacs statevector simulator. It is optional: it is registered as `"qulacs"` when the package is installed, and the package is imported only on first use. It declares `gradient_method = "adjoint"`, so the gradient code of the core package uses QulacsCompiled.jacobian() (analytic, in reverse mode, through Qulacs `backprop`) instead of the parameter-shift rule, whose cost is `O(n_params)`. #### `capabilities` What this backend supports. It runs every gate, returns exact expectation values only, does not evaluate a batch in one call, and has no limit on the number of qubits. The adjoint of `AmplitudeEncoding` is excluded, because a state preparation cannot be inverted as a Qulacs gate. See Capabilities. #### `__init__(config=None, name='qulacs')` Build the backend from a device configuration. Parameters: | Name | Type | Description | Default | | -------- | -------------- | -------------------------------------------------------------- | ---------- | | `config` | `QulacsConfig` | The device settings. Default: None, which uses QulacsConfig(). | `None` | | `name` | `str` | The registry name, used in error messages. Default: "qulacs". | `'qulacs'` | #### `compile(circuit, observables)` Prepare `circuit` for repeated evaluation of `observables`. A `Circuit` calls it once and reuses the result for every call, so the expensive work belongs here. Parameters: | Name | Type | Description | Default | | ------------- | ---------------------- | --------------------------- | ---------- | | `circuit` | `CircuitSpec` | The circuit to prepare. | *required* | | `observables` | `Sequence[Observable]` | The observables to measure. | *required* | Returns: | Type | Description | | ---------------- | ------------------------------------------------- | | `QulacsCompiled` | The prepared circuit, as a QulacsCompiled object. | ### `QulacsCompiled` Bases: `CompiledCircuit` A compiled circuit: one `ParametricQuantumCircuit`, reused across calls. `_slots[k] = (expr, coeff)` records that the parametric Qulacs gate `k` implements the library angle `coeff · expr`. Its Qulacs parameter is set to `-(coeff · resolve(expr))`, because Qulacs uses the opposite rotation sign. #### `__init__(circuit, observables, config)` Prepare the circuit for repeated evaluation. `QulacsBackend.compile()` builds this object, so there is no reason to build one directly. Parameters: | Name | Type | Description | Default | | ------------- | ---------------------- | --------------------------- | ---------- | | `circuit` | `CircuitSpec` | The circuit to prepare. | *required* | | `observables` | `Sequence[Observable]` | The observables to measure. | *required* | | `config` | `QulacsConfig` | The device settings. | *required* | #### `jacobian(weights, inputs, *, need_input_grad=False)` Compute the gradient with the native Qulacs method, the analytic adjoint. The gradient code of the core package looks for a `jacobian` method. When a backend has one, it is used in preference to the parameter-shift rule, which works on any backend and is the fallback. Here the gradient comes from `backprop`: one pass for each observable gives `d⟨O⟩/dφ` for every parametric slot. These gradients are mapped onto the weights (and the inputs) with the same symbolic chain-rule factors as the parameter-shift code, so angles built with `Scale`, `Multiply`, `Sum` or `Negate` (for example in `Reuploading(trainable_scale=True)`) are also handled. For a single sample, the two jacobians have shapes `(n_obs, n_weights)` and `(n_obs, n_inputs)`. For a 2-D `inputs` batch, they have a leading batch axis, and the batch is evaluated in a loop. Parameters: | Name | Type | Description | Default | | ----------------- | --------- | ------------------------------------------------------------------------------ | ---------- | | `weights` | `ndarray` | The parameter vector (1-D). | *required* | | `inputs` | `ndarray` | The input features: 1-D for one sample, or 2-D with a leading batch axis. | *required* | | `need_input_grad` | `bool` | If True, also compute the jacobian with respect to the inputs. Default: False. | `False` | Returns: | Name | Type | Description | | ----------------- | --------- | ----------------------------------------- | | `weight_jacobian` | `ndarray` | The jacobian with respect to the weights. | | `input_jacobian` | \`ndarray | None\` | #### `run(weights, inputs)` Evaluate the circuit and return one expectation value per observable. It follows the `CompiledCircuit` contract: the result has shape `(n_obs,)` when `weights` and `inputs` are both 1-D, and `(B, n_obs)` otherwise. Parameters: | Name | Type | Description | Default | | --------- | --------- | --------------------------------------------------------------------------- | ---------- | | `weights` | `ndarray` | The parameter vector: 1-D for one sample, or 2-D with a leading batch axis. | *required* | | `inputs` | `ndarray` | The input features, with the same 1-D or 2-D shape convention as weights. | *required* | Returns: | Type | Description | | --------- | ------------------------------------------------ | | `ndarray` | The expectation values, one for each observable. | #### `statevector(weights, inputs, initial_state=None)` Return the full complex statevector after the circuit has run. Qubit `0` is the most significant bit. `initial_state`, in the same order, is the starting state instead of `|0…0⟩`. A 2-D `inputs` batch is evaluated in a loop and returns an array of shape `(B, 2**n)`. `initial_state` can then be one state of shape `(2**n,)`, shared by all samples, or a stack of shape `(B, 2**n)` with one state for each sample. Parameters: | Name | Type | Description | Default | | --------------- | --------- | ------------------------------------------------------------------------- | ----------------------------------- | | `weights` | `ndarray` | The parameter vector (1-D). | *required* | | `inputs` | `ndarray` | The input features: 1-D for one sample, or 2-D with a leading batch axis. | *required* | | `initial_state` | \`ndarray | None\` | The state to start from, instead of | Returns: | Type | Description | | --------- | ------------------------------------------------------------------------------ | | `ndarray` | The statevector, of shape (2\*\*n_qubits,), or (B, 2\*\*n_qubits) for a batch. | #### `zero_state_probability(weights, inputs)` Return `|⟨0…0|ψ⟩|²`, the squared amplitude of `|0…0⟩`. The basis state `0` is `|0…0⟩` whether qubit `0` is the least or the most significant bit, so no reindexing is needed. The fidelity quantum kernel is built on this method. Parameters: | Name | Type | Description | Default | | --------- | --------- | --------------------------------------- | ---------- | | `weights` | `ndarray` | The parameter vector (1-D). | *required* | | `inputs` | `ndarray` | The input features of one sample (1-D). | *required* | Returns: | Type | Description | | ------- | ----------------------------------- | | `float` | The probability, a float in [0, 1]. | ### `QulacsConfig` Device configuration for the Qulacs backend. The Qulacs statevector simulator for the central processing unit (CPU) needs no settings, so this configuration is currently only a marker. It makes `Circuit(provider=QulacsConfig())` select this backend, and it leaves room for future fields, for example a state on a graphics processing unit (GPU). Pass it to `Circuit(provider=QulacsConfig())`, or pass it to `register()` under a name to reuse it. #### `make_backend()` Build the backend that this configuration describes. The provider layer calls it when a configuration is passed as `provider=`. There is no reason to call it directly. Returns: | Type | Description | | ----------------- | ----------------------------------------- | | `'QulacsBackend'` | The QulacsBackend for this configuration. | ## `eigenkit.core.backends.tensorcircuit_backend` The tensorcircuit-ng backend, for tensor-network simulation in JAX. It uses the native automatic differentiation of JAX. tensorcircuit-ng is an optional extra (`pip install eigenkit[tensorcircuit]`). Its packages (tensorcircuit and JAX) are imported only when needed, so `import eigenkit` does not import them. `compile()` raises `ImportError` if they are missing. It declares `gradient_method = "adjoint"`: the compiled circuit has a `jacobian` method, computed by reverse-mode automatic differentiation in JAX (`jax.jacrev`), so the gradient code of `Circuit` uses it directly. Both the NumPy variational optimisation (used by the Variational Quantum Eigensolver and the Quantum Approximate Optimization Algorithm) and the parameter-shift fallback of the PyTorch interface get their gradients through `_gradient_jacobians`, which calls this `jacobian`. The simulation contracts the tensor network exactly, through the `Circuit.expectation` method of tensorcircuit. The batch axis is vectorised with `jax.vmap`, and the whole forward and gradient computation is compiled with `jax.jit`. This suits **repeated** workloads (training loops, sweeps, restarts with an ansatz of fixed shape), where the one-time compilation is spread over many evaluations. For a single solve, the NumPy reference simulator is faster, because the compilation takes most of the time. The simulation is still an exact contraction, so the gain is a constant factor: it does not make the cost sub-exponential. The supported gates are `RX`, `RY`, `RZ`, `H`, `X`, `Z`, `CNOT` (controlled-NOT) and `SWAP`. To add more, extend `_apply` and `SUPPORTED`. The observables are sums of Pauli operators. The 64-bit mode of JAX is turned on at first use, for the `complex128` statevector. Example ```python from eigenkit import Circuit, Encoding, Variational, all_z blocks = [Encoding(in_features=4), Variational(depth=2)] # by registered name circuit = Circuit(blocks=blocks, observables=all_z(4), provider="tensorcircuit") # or with the config, when a device setting matters from eigenkit.configs import TensorCircuitConfig circuit = Circuit( blocks=blocks, observables=all_z(4), provider=TensorCircuitConfig(), ) ``` The simulation uses tensor networks on JAX, with batching through `jax.vmap`. This backend needs the `tensorcircuit` extra. ### `TensorCircuitBackend` The provider for tensorcircuit-ng, tensor-network simulation on JAX. It is registered as `"tensorcircuit"` once the extra is installed. It is internal: you select this backend with `provider="tensorcircuit"` or by passing a TensorCircuitConfig, not by building this class. #### `capabilities` What this backend supports. It runs the subset of gates that this backend translates to tensorcircuit (`RX`, `RY`, `RZ`, `H`, `X`, `Z`, `CNOT` (controlled-NOT) and `SWAP`), evaluates a batch in one call through `jax.vmap`, returns exact expectation values only, and has no limit on the number of qubits. See Capabilities. #### `__init__(config=None, name='tensorcircuit')` Build the backend from a device configuration. Parameters: | Name | Type | Description | Default | | -------- | --------------------- | --------------------------------------------------------------------- | ----------------- | | `config` | `TensorCircuitConfig` | The device settings. Default: None, which uses TensorCircuitConfig(). | `None` | | `name` | `str` | The registry name, used in error messages. Default: "tensorcircuit". | `'tensorcircuit'` | #### `compile(circuit, observables)` Prepare `circuit` for repeated evaluation of `observables`. A `Circuit` calls it once and reuses the result for every call, so the expensive work belongs here. Parameters: | Name | Type | Description | Default | | ------------- | ---------------------- | --------------------------- | ---------- | | `circuit` | `CircuitSpec` | The circuit to prepare. | *required* | | `observables` | `Sequence[Observable]` | The observables to measure. | *required* | Returns: | Type | Description | | ----------------------- | -------------------------------------------------------- | | `TensorCircuitCompiled` | The prepared circuit, as a TensorCircuitCompiled object. | ### `TensorCircuitCompiled` A circuit prepared for tensorcircuit-ng, compiled with `jax.jit` on first use. It is internal: TensorCircuitBackend.compile() builds it and returns it, and it is never built directly. `jacobian()` uses reverse-mode automatic differentiation in JAX, and the batch axis is vectorised with `jax.vmap`, so the one-time compilation cost is spread over a training loop or a sweep. #### `__init__(circuit, observables)` Prepare the circuit for repeated evaluation. `TensorCircuitBackend.compile()` builds this object, so there is no reason to build one directly. Parameters: | Name | Type | Description | Default | | ------------- | ---------------------- | --------------------------- | ---------- | | `circuit` | `CircuitSpec` | The circuit to prepare. | *required* | | `observables` | `Sequence[Observable]` | The observables to measure. | *required* | #### `jacobian(weights, inputs, *, need_input_grad=False)` Compute the jacobians with reverse-mode automatic differentiation in JAX. The shapes are the same as for the adjoint method of the NumPy simulator: `(n_obs, n_weights)` and `(n_obs, n_inputs)`, with a leading batch axis for 2-D inputs. Parameters: | Name | Type | Description | Default | | ----------------- | --------- | ------------------------------------------------------------------------------ | ---------- | | `weights` | `ndarray` | The parameter vector (1-D). | *required* | | `inputs` | `ndarray` | The input features: 1-D for one sample, or 2-D with a leading batch axis. | *required* | | `need_input_grad` | `bool` | If True, also compute the jacobian with respect to the inputs. Default: False. | `False` | Returns: | Name | Type | Description | | ----------------- | --------- | ----------------------------------------- | | `weight_jacobian` | `ndarray` | The jacobian with respect to the weights. | | `input_jacobian` | \`ndarray | None\` | #### `probabilities(weights, inputs)` Return the full measurement distribution over the `2**n` basis states. Qubit `0` is the most significant bit. The Quantum Approximate Optimization Algorithm (QAOA) uses it to decode its result. It builds the dense state, so it is only practical for a small number of qubits. Parameters: | Name | Type | Description | Default | | --------- | --------- | --------------------------------------- | ---------- | | `weights` | `ndarray` | The parameter vector (1-D). | *required* | | `inputs` | `ndarray` | The input features of one sample (1-D). | *required* | Returns: | Type | Description | | --------- | --------------------------------------------- | | `ndarray` | The probabilities, of shape (2\*\*n_qubits,). | #### `run(weights, inputs)` Evaluate the circuit and return one expectation value per observable. `weights` must be 1-D. The result has shape `(n_obs,)` when `inputs` is 1-D, and `(B, n_obs)` when `inputs` is 2-D. Parameters: | Name | Type | Description | Default | | --------- | --------- | ------------------------------------------------------------------------- | ---------- | | `weights` | `ndarray` | The parameter vector (1-D). | *required* | | `inputs` | `ndarray` | The input features: 1-D for one sample, or 2-D with a leading batch axis. | *required* | Returns: | Type | Description | | --------- | ------------------------------------------------ | | `ndarray` | The expectation values, one for each observable. | ### `TensorCircuitConfig` Configuration for the tensorcircuit backend. It has no settings yet: the contraction path is the default of tensorcircuit, and the batch axis is vectorised with `jax.vmap`. It exists for consistency with the configurations of the other backends, and so that `provider=TensorCircuitConfig()` works without a `register` call. #### `make_backend()` Build the backend that this configuration describes. The provider layer calls it when a configuration is passed as `provider=`. There is no reason to call it directly. Returns: | Type | Description | | ------------------------ | ------------------------------------------------ | | `'TensorCircuitBackend'` | The TensorCircuitBackend for this configuration. | ## `eigenkit.datasets.eda` Helpers for the visual analysis of a dataset, and for reproducible summaries. This module has one main entry point, DatasetReport. It converts built-in and custom datasets to the same format, and then: - it draws quick overview plots of the distributions, the correlations, and the relation between the features and the target; - it draws diagnostics that are specific to time series; - it writes a reproducible summary, as text and as a dictionary, without using a language model. ### `FONTSIZE_AXES = 20` The font size, in points, that `rc_params()` gives the axis labels and titles. ### `FONTSIZE_LEGEND = 18` The font size, in points, that `rc_params()` gives the legend entries. ### `FONTSIZE_TICKS = 16` The font size, in points, of the tick labels, which is also the base `font.size`. ### `STYLE_COLORS = ('#CE2173', '#AA5EFF', '#7D4ECF', '#43CDFF', '#3333FB', '#11E57A', '#E6C541', '#94A3B8')` The plot palette of the library, in the order of the colour cycle. `rc_params()` sets it as `axes.prop_cycle`, so a plot drawn inside that context uses these colours without naming them. Index it when a series needs a fixed colour, and not the next one in the cycle. For example, use `STYLE_COLORS[0]` for a training curve and `STYLE_COLORS[1]` for validation, so that the two curves keep their colours across figures. It is a tuple, so that an `append` by mistake cannot change how every later plot in the process looks. ### `DatasetReport` An exploratory data analysis (EDA) report for a built-in or custom dataset. Parameters: | Name | Type | Description | Default | | --------------- | ----------- | ---------------------------------------------------------------------------------------------- | ---------- | | `X` | `ndarray` | The feature matrix, of shape (n_samples, n_features). | *required* | | `y` | `ndarray` | The target vector, of shape (n_samples,). | *required* | | `feature_names` | `list[str]` | Readable names of the features, in the order of the columns of X. | *required* | | `target_name` | `str` | The name of the target in the plots and the summary. | *required* | | `task` | `str` | The type of task, usually "classification", "regression" or "timeseries". | *required* | | `metadata` | `Any` | The metadata of the dataset, for example a DatasetInfo. | `None` | | `time_index` | `Any` | The time axis for the time-series plots. It can be numeric (array-like) or a pd.DatetimeIndex. | `None` | | `theme` | `str` | The theme of the plots, "light" or "dark". | `'light'` | Notes The recommended way to build a report is `from_data()`, which accepts the dictionaries returned by `load_dataset(...)` as well as your own `X` and `y`. Example ```python from eigenkit.datasets import DatasetReport, load_dataset data = load_dataset("iris", split=False) report = DatasetReport.from_data(data) figs = report.plot_overview() summary = report.summarize() ``` #### `from_data(data=None, *, X=None, y=None, feature_names=None, target_name='target', task=None, time_index=None, theme='light')` Build a report from a dataset dictionary, or from your own arrays or tensors. Parameters: | Name | Type | Description | Default | | --------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `data` | `dict[str, Any]` | A dataset dictionary in the format of eigenkit, either {"X", "y"} or {"X_train", "y_train", "X_test", "y_test"}. The training and test sets are combined. | `None` | | `X` | `Any` | Your own feature matrix, as a NumPy array, a pandas object, a PyTorch tensor or another array-like. | `None` | | `y` | `Any` | Your own target vector, in the same formats as X. | `None` | | `feature_names` | `list[str]` | The names of the features. If they are not given, the names come from the metadata, or they are feature_0, feature_1 and so on. | `None` | | `target_name` | `str` | The name of the target in the plots and the summary. | `'target'` | | `task` | `str` | The type of task. If it is not given, it is taken from the metadata, or guessed from y. | `None` | | `time_index` | `Any` | The time axis for the time-series analysis. | `None` | | `theme` | `str` | The theme of the plots, "light" or "dark". | `'light'` | Returns: | Type | Description | | ----------------- | --------------------------------------------------- | | `'DatasetReport'` | The report, ready for plotting and for the summary. | Raises: | Type | Description | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------ | | `ValueError` | If the inputs are missing or do not agree, for example if the shapes do not match, or if feature_names does not have one name per feature. | #### `plot(kind, **kwargs)` Build one plot, chosen by its kind. Parameters: | Name | Type | Description | Default | | ---------- | ----- | ------------------------------------------------------------------------------------------------------------------- | ---------- | | `kind` | `str` | The kind of plot: "data_health", "target", "distributions", "correlation", "feature_target" or "timeseries_target". | *required* | | `**kwargs` | `Any` | Options of the plot, such as bins, max_features and max_points. | `{}` | Returns: | Type | Description | | ----- | ---------------------- | | `Any` | The matplotlib figure. | Raises: | Type | Description | | ------------ | -------------------------------------- | | `ValueError` | If kind is not one of the kinds above. | #### `plot_overview(*, max_features=None, bins=30, max_points=2000, path=None)` Build the default set of plots of the exploratory data analysis. Parameters: | Name | Type | Description | Default | | -------------- | ----- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | | `max_features` | `int` | The largest number of features shown in the panels about features. None shows every feature. | `None` | | `bins` | `int` | The number of histogram bins in the distribution plots. | `30` | | `max_points` | `int` | The largest number of points in the scatter plots. Larger datasets are subsampled. | `2000` | | `path` | \`str | Path\` | A directory where every overview plot is saved as a PNG file. With None, the plots are not saved. | Returns: | Type | Description | | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `dict[str, Any]` | A dictionary from each kind of plot to its matplotlib figure. The "timeseries_target" plot is included only for a time-series task. | #### `summarize()` Build a reproducible summary of the dataset, with recommendations. Returns: | Type | Description | | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `dict[str, Any]` | A dictionary with statistics about the shape and the quality of the data, signals specific to the task when there are any, and a readable summary under the "text" key. | ### `rc_params(theme='light')` Return the matplotlib settings of the library for one plot, as rcParams. Every figure that the library draws uses these settings: `DatasetReport.plot_overview()`, the benchmark curves and the plots in the worked examples. This is what makes them look like one set. Pass the result to `matplotlib.rc_context()` and draw as usual. The settings apply only inside that block, because the returned dictionary is a new copy and the global `matplotlib.rcParams` is never changed. The two themes differ only in what has to change for a dark page. `"dark"` makes the backgrounds of the figure, the axes and the legend transparent, and makes the text, the ticks and the spines white. The sizes, the spines, the grid and STYLE_COLORS are the same in both themes, so a figure saved with each theme differs only in its background and in the colour of its text and lines. Parameters: | Name | Type | Description | Default | | ------- | ----- | ------------------------------------------------------------------- | --------- | | `theme` | `str` | "light" or "dark". The case and any surrounding spaces are ignored. | `'light'` | Returns: | Type | Description | | ------ | ------------------------------------------------------------- | | `dict` | The rcParams to draw with, ready for matplotlib.rc_context(). | Raises: | Type | Description | | ------------ | --------------------------------------- | | `ValueError` | If theme is neither "light" nor "dark". | Example ```python import matplotlib import matplotlib.pyplot as plt from eigenkit.datasets import STYLE_COLORS, rc_params with matplotlib.rc_context(rc_params(theme="light")): fig, ax = plt.subplots(figsize=(7, 4)) ax.plot(history, color=STYLE_COLORS[0], label="train loss") ax.legend() fig.savefig("loss.png", bbox_inches="tight") ``` ## `eigenkit.optimization.problems` Combinatorial problems as Ising and QUBO models, and their qubit Hamiltonians. QUBO means quadratic unconstrained binary optimisation. The mapping convention is that the spin `s_i = +1` is the qubit state `|0⟩` and `s_i = -1` is `|1⟩`, which are the eigenvalues of `Z`. So a bitstring `b`, with qubit values `b_i ∈ {0, 1}`, corresponds to `s_i = 1 - 2 b_i`. `to_hamiltonian()` returns the pair `(Observable, offset)`. The Pauli sum holds only the `Z` and `ZZ` terms, and the constant is kept apart, because the Pauli backends reject terms made only of the identity. The solver adds `offset` back to `⟨H⟩`. ### `Ising` An Ising problem, which minimises `Σ h_i s_i + Σ_{i/d weights, d/d inputs)` for an observable. The observable is diagonal in the Fock basis. Only one sample is accepted (1-D weights and inputs). Parameters: | Name | Type | Description | Default | | ------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | | `weights` | `ndarray` | The 1-D weight vector. | *required* | | `inputs` | `ndarray` | The 1-D input vector. | *required* | | `observable` | `PhotonicObservable` | A PhotonNumber or a FockProbability. | *required* | | `method` | `str` | "parameter_shift" for the exact photonic parameter-shift rule (in gradients.py), or "finite_difference" for central finite differences, the reference method. Default: "parameter_shift". | `'parameter_shift'` | | `need_weight_grad` | `bool` | If True, compute the gradient with respect to the weights. If False, it is skipped, and None is returned in its place. | `True` | | `need_input_grad` | `bool` | If True, compute the gradient with respect to the inputs. If False, it is skipped, and None is returned in its place. | `True` | Raises: | Type | Description | | ------------ | ----------------------------------------- | | `ValueError` | If method is neither of these two values. | #### `run(weights, inputs)` Return the output distribution of photon numbers. The distribution is a dictionary `{photon_counts_tuple: probability}`. Fock states with probability zero may be left out, because Perceval's `prob_distribution` is sparse. Parameters: | Name | Type | Description | Default | | --------- | --------- | ------------------------------------------------ | ---------- | | `weights` | `ndarray` | The 1-D weight vector, of length spec.n_weights. | *required* | | `inputs` | `ndarray` | The 1-D input vector, of length spec.n_inputs. | *required* | Raises: | Type | Description | | ------------ | ----------------------------------------- | | `ValueError` | If weights or inputs has the wrong shape. | ## `eigenkit.photonics.executor` The executor protocol for photonic circuits, the linear-optics counterpart of `core.provider`. A photonic **executor** compiles a PhotonicCircuitSpec into a `Compiled` handle that can be reused. This is a *different contract* from the qubit `core.provider.Provider`: there is no `CircuitSpec`, no Pauli `Observable` and no array of expectation values. This is why photonics is a separate package, and not a qubit backend. The executors themselves are in `photonics/backends/` (for example, the Perceval backend, which uses Perceval's simulation of linear optical systems (SLOS)). This module does not depend on any framework (neither Perceval nor PyTorch). It defines only the contract, and a small registry of names whose executors are built on first use, like the one in `core.provider`. ### `Compiled` Bases: `Protocol` A handle to a compiled photonic circuit, cheap to call many times. Its contents are internal to the executor that built it. #### `expectation(weights, inputs, observable)` Evaluate one observable that is diagonal in the Fock basis, for one sample. Parameters: | Name | Type | Description | Default | | ------------ | -------------------- | ------------------------------------ | ---------- | | `weights` | `ndarray` | The 1-D weight vector. | *required* | | `inputs` | `ndarray` | The 1-D input vector. | *required* | | `observable` | `PhotonicObservable` | A PhotonNumber or a FockProbability. | *required* | Returns: | Type | Description | | ------- | ------------------------------------ | | `float` | The expectation value of observable. | #### `jacobian(weights, inputs, observable, *, method='parameter_shift', need_weight_grad=True, need_input_grad=True)` Compute the gradient of one observable with respect to weights and inputs. Parameters: | Name | Type | Description | Default | | ------------------ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------- | | `weights` | `ndarray` | The 1-D weight vector. | *required* | | `inputs` | `ndarray` | The 1-D input vector. | *required* | | `observable` | `PhotonicObservable` | A PhotonNumber or a FockProbability. | *required* | | `method` | `str` | How to differentiate, for example "parameter_shift". | `'parameter_shift'` | | `need_weight_grad` | `bool` | If True, compute the gradient with respect to the weights. If False, the implementation may skip it and return None in its place. The PyTorch bridge takes this value from ctx.needs_input_grad. | `True` | | `need_input_grad` | `bool` | The same as need_weight_grad, for the gradient with respect to the inputs. | `True` | Returns: | Name | Type | Description | | ------------ | --------- | ----------- | | `weight_jac` | \`ndarray | None\` | | `input_jac` | \`ndarray | None\` | #### `run(weights, inputs)` Evaluate the circuit for one sample. Parameters: | Name | Type | Description | Default | | --------- | --------- | ------------------------------------------------ | ---------- | | `weights` | `ndarray` | The 1-D weight vector, of length spec.n_weights. | *required* | | `inputs` | `ndarray` | The 1-D input vector, of length spec.n_inputs. | *required* | Returns: | Type | Description | | ------ | --------------------------------------------------------------------------------- | | `dict` | The output Fock distribution, as a dictionary {photon_counts_tuple: probability}. | ### `Executor` Bases: `Protocol` An object that compiles a `PhotonicCircuitSpec` into a `Compiled` handle. The handle can be reused for many evaluations. #### `compile(spec)` Prepare `spec` so that it can be evaluated many times. Parameters: | Name | Type | Description | Default | | ------ | --------------------- | ----------------------- | ---------- | | `spec` | `PhotonicCircuitSpec` | The circuit to compile. | *required* | Returns: | Type | Description | | ---------- | --------------------------- | | `Compiled` | A Compiled handle for spec. | ### `get(name)` Return the executor registered under `name`, building it on first use. Parameters: | Name | Type | Description | Default | | ------ | ----- | ------------------------------- | ---------- | | `name` | `str` | A name passed to register_lazy. | *required* | Returns: | Name | Type | Description | | ---------- | ---------- | ---------------------------------------------------------------------------------------------- | | `executor` | `Executor` | The executor. It is kept after the first call, and the same object is returned by later calls. | Raises: | Type | Description | | ---------- | ------------------------------------ | | `KeyError` | If nothing is registered under name. | ### `register_lazy(name, loader)` Register a function that builds an executor, without building it yet. The first registration of a name is kept, and later ones are ignored. Parameters: | Name | Type | Description | Default | | -------- | ------------------------ | --------------------------------------------------------------------------------------------------------- | ---------- | | `name` | `str` | The name that get looks up. | *required* | | `loader` | `Callable[[], Executor]` | A function with no arguments that builds the executor. It is called the first time the name is looked up. | *required* | ### `registered_names()` Return the names of all registered executors, in sorted order. ## `eigenkit.photonics.gradients` Gradients of photonic circuits: the photonic parameter-shift rule, and finite differences as a simple reference method. The qubit rule shifts the angle by `±π/2`. It holds for a Pauli rotation because a Pauli generator has the eigenvalues `±½`, which give a single frequency. The generator of a linear-optical component instead has *equally spaced* eigenvalues. For a phase shifter (and for the `phi` of a beam splitter), the generator is the number operator `n̂`, whose eigenvalues are integers. The expectation value of any observable that is diagonal in the Fock basis is then a trigonometric polynomial in the angle, of degree at most `N`, where `N` is the total number of photons. Its derivative is given *exactly* by the general shift rule for equally spaced frequencies (Wierichs et al., 2022), which uses `2N` terms. At `N = 1`, this rule is the usual `±π/2` rule. The `theta` of a beam splitter needs more care. Its generator is a generator of the special unitary group SU(2), on the two modes that the beam splitter acts on. The eigenvalues of this generator are integers or half-integers, depending on whether the number of photons `n_ij` in those two modes is even or odd: `U(theta + 2π) = (-1)^n_ij`. This is a sign that depends on the photon-number sector, not a global phase. In a circuit with two modes, `n_ij = N` always, so the frequencies are integers at most `N`, and the rule above applies. With more modes, the components around the beam splitter create coherent combinations of sectors with even and odd photon numbers, and half-integer frequencies appear. The rule is then applied in the angle `theta / 2`, with order `2N` (`4N` evaluations). This module is the linear-optics counterpart of `core.provider.parameter_shift_jacobian`. It reuses the `chain_factor` and `chain_factor_input` functions of the qubit intermediate representation to turn the derivative with respect to a component angle into derivatives with respect to the weights and inputs that the angle depends on. The executor reuses the same `Parameter` expression tree. Reference: the photonic parameter-shift rule, arXiv:2410.02726 (Phys. Rev. A 111, 032429). Scope: one sample at a time (1-D weights and inputs), and observables that are diagonal in the Fock basis. ### `finite_difference_jacobian(executor, spec, weights, inputs, observable, *, eps=1e-06, need_weight_grad=True, need_input_grad=True)` Compute the gradients with central finite differences, as a simple reference. It shifts the weights and inputs themselves, so the chain rule is included automatically. It is the reference that checks the exact rule, in the same way as the NumPy simulator is the reference for qubit circuits. Parameters: | Name | Type | Description | Default | | ------------------ | --------------------- | ---------------------------------------------------------------------------------------------------------------------- | ---------- | | `executor` | `Executor` | The executor that evaluates the circuit, for example PercevalBackend(). | *required* | | `spec` | `PhotonicCircuitSpec` | The circuit to differentiate. | *required* | | `weights` | `ndarray` | The 1-D weight vector. | *required* | | `inputs` | `ndarray` | The 1-D input vector. | *required* | | `observable` | `PhotonicObservable` | An observable that is diagonal in the Fock basis. | *required* | | `eps` | `float` | The step of the central difference. Default: 1e-6. | `1e-06` | | `need_weight_grad` | `bool` | If True, compute the gradient with respect to the weights. If False, it is skipped, and None is returned in its place. | `True` | | `need_input_grad` | `bool` | If True, compute the gradient with respect to the inputs. If False, it is skipped, and None is returned in its place. | `True` | Returns: | Name | Type | Description | | ------------ | --------- | ----------- | | `weight_jac` | \`ndarray | None\` | | `input_jac` | \`ndarray | None\` | Raises: | Type | Description | | ------------ | -------------------------------------------------------- | | `ValueError` | If observable can never match the output of the circuit. | ### `photonic_parameter_shift_jacobian(executor, spec, weights, inputs, observable, *, order=None, need_weight_grad=True, need_input_grad=True)` Compute the exact `d/d(weights)` and `d/d(inputs)` with the photonic parameter-shift rule. Parameters: | Name | Type | Description | Default | | ------------------ | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `executor` | `Executor` | The executor that evaluates each shifted circuit, for example PercevalBackend(). | *required* | | `spec` | `PhotonicCircuitSpec` | The circuit to differentiate. | *required* | | `weights` | `ndarray` | The 1-D weight vector. | *required* | | `inputs` | `ndarray` | The 1-D input vector. | *required* | | `observable` | `PhotonicObservable` | An observable that is diagonal in the Fock basis. | *required* | | `order` | \`int | None\` | The degree of the trigonometric polynomial to reconstruct. The default is the total number of photons, which is a safe upper bound: the rule is exact whenever the true degree is at most order. In a circuit with more than two modes, the theta of a beam splitter is reconstructed in theta / 2 with order 2 * order (see the module docstring). order=1 gives the simple ±π/2 estimate, which is in general wrong with more than one photon. The tests use it to show that the rule with more terms is necessary. | | `need_weight_grad` | `bool` | If True, compute the gradient with respect to the weights. If False, its shifted evaluations are skipped, and None is returned in its place. | `True` | | `need_input_grad` | `bool` | If True, compute the gradient with respect to the inputs. If False, its shifted evaluations are skipped, and None is returned in its place. | `True` | Returns: | Name | Type | Description | | ------------ | --------- | ----------- | | `weight_jac` | \`ndarray | None\` | | `input_jac` | \`ndarray | None\` | Raises: | Type | Description | | ------------ | -------------------------------------------------------- | | `ValueError` | If observable can never match the output of the circuit. | ## `eigenkit.photonics.ir` The intermediate representation (IR) of photonic circuits, which use linear optics. This module holds only data, and uses neither Perceval nor NumPy. It is the photonic counterpart of `core.ir`. A photonic circuit is a sequence of linear-optical **components** that act on `n_modes` optical modes. They transform an input **Fock state**, which gives the number of photons in each mode. This model of computation is very different from the qubit-gate `CircuitSpec`. The states live in Fock space, the "gates" are beam splitters and phase shifters that act on modes, and the readout is a distribution of photon numbers, not the expectation value of a Pauli operator. This is why the photonic circuits have their own IR, instead of an adapted `CircuitSpec`. It deliberately **reuses** the `Parameter` expression tree of the qubit IR (`Constant`, `WeightRef`, `InputRef`, `Scale`, `Sum` and `Multiply`). The angles and phases of the components are symbolic numbers, so a component can be trained (with a `WeightRef`) or can encode data (with an `InputRef`) with no extra code. The same `resolve` function evaluates the angles of both kinds of circuit. The simulation in Fock space and the translation to Perceval are in `backends/perceval_backend`, never in this module. ### `BeamSplitter` A beam splitter on two modes. `modes` are two adjacent modes `(m, m + 1)`, because Perceval can place a beam splitter only on adjacent modes. `theta` sets the reflectivity, with the Perceval convention `R = cos²(theta/2)`, so `theta = π/2` gives a balanced 50:50 beam splitter. `phi` is a relative phase. Each angle is a `Parameter` expression or a plain number, and a plain number is stored as a `Constant`. The exact unitary is set by the executor, which maps the component to Perceval's `pcvl.BS`, so this class only describes the component. ### `FockProbability` The probability of detecting exactly the output Fock state `target`. In formula form, ` = P(target)`. ### `Permutation` A reordering of the modes, which is a network of mode swaps given as one component. `perm[i]` is the output mode to which input mode `i` is sent. The component acts on the modes `0` to `len(perm) - 1`, so `perm` must reorder exactly these modes. ### `PhaseShifter` A phase shift `exp(i·phi·n̂)` on a single mode. `n̂` is the photon-number operator of the mode. `phi` is a `Parameter` expression or a plain number, and a plain number is stored as a `Constant`. ### `PhotonNumber` The expected number of photons in a mode: ` = `. ### `PhotonicCircuitSpec` A description of a linear-optical circuit that does not depend on the backend. It is the photonic counterpart of `core.ir.CircuitSpec`. It cannot be changed after it is built, and it can be hashed and pickled. `input_state` is the Fock state sent into the circuit, as the number of photons in each mode. For example, `(1, 0, 1, 0)` means one photon in mode `0` and one in mode `2`. What is measured (the full distribution of photon numbers, the expected photon number of each mode, or a function of the detection pattern) is left to the executor for now. A `PhotonicMeasurement` type is planned. Every component must act on the modes `0` to `n_modes - 1`, and `input_state` must hold non-negative integers. Both are checked when the object is built, so a wrong circuit fails at that point, and not inside Perceval. #### `n_photons` The total number of photons sent into the circuit. This number sets the cost of the simulation in Fock space. ## `eigenkit.photonics.reservoir` Photonic reservoir computing, as a quantum extreme learning machine. The reservoir is a *fixed* linear-optical interferometer, which is random and never trained. At each timestep, the input is encoded into its phases, the photons pass through it, and photon-number statistics are read out. This gives a nonlinear map to many features. Only a classical linear readout is trained, so the executor only has to evaluate the circuit, and no gradients go through the optics. The parts that do not depend on the kind of hardware come from the qubit reservoir package, `eigenkit.reservoir`. This module uses its `Stateless` policy, which gives temporal context with a classical window of past inputs, and the readout can be that package's `LinearReadout`, a ridge regression solved in closed form. The part specific to photonics turns the input of each timestep into a vector of features from a Fock distribution, with a photonic `Executor`. Scope: no memory, or a classical window of inputs (`Stateless`). Carrying the photonic *state* from one timestep to the next (a quantum memory, like the qubit `Carry` policy) needs the executor to accept a Fock state as input, and is not supported yet. Policies that carry state raise an error here. ### `fock_basis(n_modes, n_photons)` Return the sorted list of output Fock states. The list holds every way to place `n_photons` photons into `n_modes` modes. Passive linear optics conserves the number of photons, so these are exactly the outputs that the circuit can reach. They are the fixed columns of the feature vector. Parameters: | Name | Type | Description | Default | | ----------- | ----- | ---------------------------- | ---------- | | `n_modes` | `int` | The number of optical modes. | *required* | | `n_photons` | `int` | The total number of photons. | *required* | ### `fock_probability_features(dist, basis)` Return the probability of every state in `basis`, as a dense vector. These are the detailed features of the reservoir. Parameters: | Name | Type | Description | Default | | ------- | ----------------------- | ----------------------------------------------------------------------------- | ---------- | | `dist` | `dict` | A Fock distribution, as {photon_counts_tuple: probability}. | *required* | | `basis` | `list[tuple[int, ...]]` | The output states, in the order of the features, for example from fock_basis. | *required* | ### `photon_number_features(dist, n_modes)` Return the expected number of photons `` in each mode `m`. This readout is compact, and easy to measure with detectors. Parameters: | Name | Type | Description | Default | | --------- | ------ | ----------------------------------------------------------- | ---------- | | `dist` | `dict` | A Fock distribution, as {photon_counts_tuple: probability}. | *required* | | `n_modes` | `int` | The number of optical modes. | *required* | ### `random_reservoir_spec(n_modes, n_inputs, *, depth=2, input_state=None, input_scale=1.0, rng=None)` Build a fixed, random photonic reservoir and its frozen weights. A mixing layer comes first, so that the photons are spread over the modes. Then the `n_inputs` features are encoded as phases on the modes `0` to `n_inputs - 1` (each multiplied by `input_scale`), and `depth` more mixing layers follow. A mixing layer is a beam splitter on every pair of neighbouring modes, followed by a phase shifter on every mode. All their angles are random weights, drawn uniformly from `[0, 2π)`. Parameters: | Name | Type | Description | Default | | ------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | `n_modes` | `int` | The number of optical modes. It must be at least 2. | *required* | | `n_inputs` | `int` | The number of features encoded at each step, with one phase each. It must be at least 1 and at most n_modes - 1. | *required* | | `depth` | `int` | The number of mixing layers after the encoding. It must be at least 1. Default: 2. | `2` | | `input_state` | \`tuple[int, ...] | None\` | The Fock state sent into the circuit. The default is a single photon in mode 0. More photons give a larger space of Fock features. | | `input_scale` | `float` | The factor applied to each input before it is used as a phase. Default: 1.0. | `1.0` | | `rng` | \`Generator | None\` | The source of the random weights of the reservoir. The default is a new, unseeded generator. | Returns: | Name | Type | Description | | --------- | --------------------- | ------------------------------------------------------------- | | `spec` | `PhotonicCircuitSpec` | The circuit of the reservoir. | | `weights` | `ndarray` | The frozen weights of the reservoir, which are never trained. | Raises: | Type | Description | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ValueError` | If n_modes < 2, depth < 1, n_inputs is outside [1, n_modes - 1], or input_state holds no photon. In each of these cases, the features of the reservoir would ignore the input, or, when every mode encodes an input, would see only the differences between inputs. | ### `run_photonic_reservoir(spec, weights, sequences, *, executor=None, policy=None, features='fock')` Run the fixed reservoir over input sequences, and return the features. The reservoir `spec`, with its frozen `weights`, is evaluated at every timestep of `sequences`, of shape `(B, T, F)`. The result is the sequence of features, of shape `(B, T, n_features)`. Parameters: | Name | Type | Description | Default | | ----------- | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `spec` | `PhotonicCircuitSpec` | The reservoir, for example from random_reservoir_spec. | *required* | | `weights` | `ndarray` | The frozen weights of the reservoir. | *required* | | `sequences` | `ndarray` | The input sequences, of shape (B, T, F). | *required* | | `executor` | \`Executor | None\` | The executor that runs each step. The default is PercevalBackend(). | | `policy` | \`Stateless | None\` | The policy whose expand_inputs turns each timestep into the circuit's n_inputs values (window * F). The default is Stateless(window=1). Policies that carry the state from one step to the next are not supported for photonics yet, because they need a Fock state as the input of the executor. | | `features` | `str` | "fock" gives the probabilities of all the states of the Fock basis with the input number of photons. This is the default, and gives the most features. "photon_number" gives the expected number of photons \ in each mode. | `'fock'` | Returns: | Type | Description | | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ndarray` | The features, of shape (B, T, n_features). n_features is the number of Fock states with n_modes modes and spec.n_photons photons for "fock", and n_modes for "photon_number". | Raises: | Type | Description | | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | `ValueError` | If features is unknown, sequences is not 3-D, or the number of inputs per step that the policy gives does not match spec.n_inputs. | | `NotImplementedError` | If policy carries state from one step to the next. | ## `eigenkit.photonics.torch` The PyTorch interface for photonic circuits (experimental). `PhotonicCircuit` is a PyTorch `nn.Module` that stores the weights of the circuit as an `nn.Parameter`. With it, `loss.backward()` computes the exact photonic parameter-shift gradient. It uses the same `torch.autograd.Function` bridge as the qubit backends that use the parameter-shift rule (see `eigenkit/torch/interface.py`). This is what makes photonic circuits *trainable* for quantum machine learning with the usual PyTorch tools, such as the optimisers and the combination with classical layers. PyTorch is imported at the top of this module. The module is imported only when the PyTorch interface is requested, so the rest of the photonics package (and `import eigenkit`) imports neither PyTorch nor Perceval. Scope: one observable that is diagonal in the Fock basis, so the output is a single number, and one sample per forward pass. Outputs with several values or several observables, batches, and gradients computed by PyTorch's own automatic differentiation are not supported yet. ### `PhotonicCircuit` Bases: `Module` A trainable linear-optical circuit, as a PyTorch `nn.Module`. It stores the circuit's `weights` as an `nn.Parameter`. `forward(inputs)` returns the expectation value of `observable`, connected to PyTorch's automatic differentiation. Parameters: | Name | Type | Description | Default | | -------------- | --------------------- | --------------------------------------------------------------------------------------------------- | ---------- | | `spec` | `PhotonicCircuitSpec` | The circuit to train. | *required* | | `observable` | `PhotonicObservable` | The readout, a single observable that is diagonal in the Fock basis. | *required* | | `backend_name` | `str` | The Perceval backend, as for PercevalBackend. Default: "SLOS". | `'SLOS'` | | `init_weights` | \`Sequence[float] | Tensor | None\` | | `dtype` | `dtype` | The data type of the weights. The default is torch.float64, to match the precision of the executor. | `float64` | Raises: | Type | Description | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ValueError` | If init_weights does not have the shape (spec.n_weights,), if observable can never match the output of the circuit (a mode outside the circuit, or a target with the wrong length or number of photons), or if backend_name is unknown or only samples. | #### `forward(inputs=None)` Return the expectation value of `observable` for one sample. The value is connected to PyTorch's automatic differentiation. Parameters: | Name | Type | Description | Default | | -------- | ----------------- | ----------- | ------- | | `inputs` | \`Sequence[float] | Tensor | None\` | ## `eigenkit.photonics.backends.perceval_backend` The Perceval executor for photonic circuits (experimental). It translates a PhotonicCircuitSpec into a Perceval `Circuit`, and computes the output distribution of photon numbers with an exact Perceval simulator, by default simulation of linear optical systems (SLOS). It is the linear-optics counterpart of a qubit `Provider`, but it does **not** implement the qubit `Provider` protocol: there is no `CircuitSpec`, no Pauli `Observable` and no array of expectation values. This is why photonics is a separate package. `perceval` is imported only *inside* the methods, when they run, so importing this module (and therefore `import eigenkit`) never imports the Perceval software development kit (SDK). The data module `photonics.ir` never uses Perceval at all. The work is split between compilation and evaluation, as in the qubit providers: `compile()` is called once, and `run(weights, inputs)` at every call. For now, the Perceval circuit is built again at every call, after the `Parameter` expressions are evaluated to numbers. Building it once with symbolic `pcvl.P` parameters, and setting their values at each call, is not done yet. (This is the saving that the qubit providers get from the split between compilation and evaluation.) ### `PercevalBackend` The photonic executor that runs on a local Perceval simulator. `compile(spec)` returns a `PercevalCompiled`. Parameters: | Name | Type | Description | Default | | -------------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | | `backend_name` | `str` | A Perceval backend that computes the full output distribution. "SLOS" (the default) and "Naive" are exact, while "MPS" and "NaiveApprox" approximate the distribution, and so the gradients too. Backends that only sample, such as "CliffordClifford2017", are rejected, because run returns probabilities, not samples. | `'SLOS'` | Raises: | Type | Description | | ------------ | ------------------------------------------- | | `ValueError` | If backend_name is unknown or only samples. | #### `compile(spec)` Wrap `spec` in a reusable handle. Perceval runs at each call of the handle. Parameters: | Name | Type | Description | Default | | ------ | --------------------- | ----------------------- | ---------- | | `spec` | `PhotonicCircuitSpec` | The circuit to compile. | *required* | Returns: | Type | Description | | ------------------ | ------------------------------------- | | `PercevalCompiled` | The PercevalCompiled handle for spec. | ### `PercevalCompiled` A reusable handle to a compiled photonic circuit, cheap to call many times. It is the linear-optics counterpart of `CompiledCircuit`. `PercevalBackend.compile` builds it. Parameters: | Name | Type | Description | Default | | -------------- | --------------------- | -------------------------------------------------------------- | ---------- | | `spec` | `PhotonicCircuitSpec` | The circuit that this handle evaluates. | *required* | | `backend_name` | `str` | The Perceval backend, as for PercevalBackend. Default: "SLOS". | `'SLOS'` | Raises: | Type | Description | | ------------ | ------------------------------------------- | | `ValueError` | If backend_name is unknown or only samples. | #### `expectation(weights, inputs, observable)` Return the expectation value of a `PhotonicObservable`, as a single number. The observable is diagonal in the Fock basis. Parameters: | Name | Type | Description | Default | | ------------ | -------------------- | ------------------------------------ | ---------- | | `weights` | `ndarray` | The 1-D weight vector. | *required* | | `inputs` | `ndarray` | The 1-D input vector. | *required* | | `observable` | `PhotonicObservable` | A PhotonNumber or a FockProbability. | *required* | Raises: | Type | Description | | ------------ | -------------------------------------------------------- | | `ValueError` | If observable can never match the output of the circuit. | #### `jacobian(weights, inputs, observable, *, method='parameter_shift', need_weight_grad=True, need_input_grad=True)` Return `(d/d weights, d/d inputs)` for an observable. The observable is diagonal in the Fock basis. Only one sample is accepted (1-D weights and inputs). Parameters: | Name | Type | Description | Default | | ------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | | `weights` | `ndarray` | The 1-D weight vector. | *required* | | `inputs` | `ndarray` | The 1-D input vector. | *required* | | `observable` | `PhotonicObservable` | A PhotonNumber or a FockProbability. | *required* | | `method` | `str` | "parameter_shift" for the exact photonic parameter-shift rule (in gradients.py), or "finite_difference" for central finite differences, the reference method. Default: "parameter_shift". | `'parameter_shift'` | | `need_weight_grad` | `bool` | If True, compute the gradient with respect to the weights. If False, it is skipped, and None is returned in its place. | `True` | | `need_input_grad` | `bool` | If True, compute the gradient with respect to the inputs. If False, it is skipped, and None is returned in its place. | `True` | Raises: | Type | Description | | ------------ | ----------------------------------------- | | `ValueError` | If method is neither of these two values. | #### `run(weights, inputs)` Return the output distribution of photon numbers. The distribution is a dictionary `{photon_counts_tuple: probability}`. Fock states with probability zero may be left out, because Perceval's `prob_distribution` is sparse. Parameters: | Name | Type | Description | Default | | --------- | --------- | ------------------------------------------------ | ---------- | | `weights` | `ndarray` | The 1-D weight vector, of length spec.n_weights. | *required* | | `inputs` | `ndarray` | The 1-D input vector, of length spec.n_inputs. | *required* | Raises: | Type | Description | | ------------ | ----------------------------------------- | | `ValueError` | If weights or inputs has the wrong shape. | ### `build_perceval_circuit(spec, weights, inputs)` Translate a `PhotonicCircuitSpec` into a Perceval `Circuit`. The `Parameter` angles are evaluated to numbers first. The local and the remote executors both use this function, so they build exactly the same circuit. (The live test of the remote executor compares sampled and exact results on the *same* circuit.) Parameters: | Name | Type | Description | Default | | --------- | --------------------- | -------------------------------------------------- | ---------- | | `spec` | `PhotonicCircuitSpec` | The circuit to translate. | *required* | | `weights` | `ndarray` | The 1-D weight vector used to evaluate the angles. | *required* | | `inputs` | `ndarray` | The 1-D input vector used to evaluate the angles. | *required* | Returns: | Name | Type | Description | | --------- | --------- | ----------------------------------- | | `circuit` | `Circuit` | The circuit, on spec.n_modes modes. | Raises: | Type | Description | | ------------ | ----------------------------------------------------------------------- | | `ValueError` | If weights or inputs is not 1-D with the length that the spec declares. | ### `fock_tuple(state, n_modes)` Convert a Perceval `BasicState`, or any indexable object, to a tuple of photon counts. Parameters: | Name | Type | Description | Default | | --------- | ------------ | ---------------------------- | ---------------------------------------------------- | | `state` | \`BasicState | Sequence[int]\` | The number of photons in each mode, indexed by mode. | | `n_modes` | `int` | The number of modes to read. | *required* | Returns: | Type | Description | | ----------------- | ----------------------------------------------------------- | | `tuple[int, ...]` | The number of photons in each mode, as a tuple of integers. | ## `eigenkit.photonics.backends.perceval_remote` The Quandela Cloud executor, which runs photonic circuits on remote hardware or cloud simulators. It sends circuits to a Quandela Cloud platform through Perceval's `RemoteProcessor`. The platform is a real photonic quantum processing unit (QPU), such as `"qpu:ascella"`, or a cloud simulator (`"sim:..."`). The results are *sampled* (counts of shots), so `run` returns an **estimated** Fock distribution, and gradients are not available, as with the qubit QPU backends. Perceval 1.3 keeps `RemoteProcessor` and `RemoteJob` in `perceval.runtime.legacy` (they are still available as `pcvl.*`). Moving to Perceval's newer interface is planned. The executor follows the conventions of the library for QPUs: - It issues a `CostWarning` at the first submission, because the backend may charge for use. - `submit(...)` returns a `RemoteJobHandle` without waiting for the job, and the handle has `.result()` and `.status()`. - `dry_run_info()` describes the prepared job **offline**, before anything is paid for. The access token comes from the `token` argument, then from `$QUANDELA_TOKEN` or `$PERCEVAL_TOKEN`, then from the token that Perceval itself would use (`pcvl.RemoteConfig`, which reads its environment variable, then its saved configuration). It is never written in the code and never logged. The `RemoteProcessor`, which connects to the cloud, is built at the first `run` or `submit` and reused after that, so `compile()` and `dry_run_info()` need neither a network connection nor a token. ### `QuandelaCloudBackend` An executor that runs on a Quandela Cloud platform. The platform is a real quantum processing unit (QPU) or a cloud simulator. Parameters: | Name | Type | Description | Default | | ---------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | `platform` | `str` | The name of a Quandela Cloud platform, for example "qpu:ascella" for hardware or "sim:slos" for a cloud simulator. Default: "sim:slos". | `'sim:slos'` | | `token` | \`str | None\` | The access token. Without it, the token comes from $QUANDELA_TOKEN or $PERCEVAL_TOKEN, then from the token that Perceval has saved. Default: None. | | `shots` | `int` | The number of shots sampled in each run. Default: 10000. | `10000` | | `url` | \`str | None\` | The web address of the Quandela Cloud service, if it is not Perceval's default. Default: None. | | `min_detected_photons` | \`int | None\` | Keep only the shots that detect at least this many photons (post-selection). The default is the number of photons in the input state. | | `poll_timeout_seconds` | `float` | How long, in seconds, run and result() wait for a job before they raise TimeoutError. Default: 3600. | `3600` | #### `compile(spec)` Tie `spec` to this platform, offline, with no network connection or token. Parameters: | Name | Type | Description | Default | | ------ | --------------------- | ----------------------- | ---------- | | `spec` | `PhotonicCircuitSpec` | The circuit to compile. | *required* | Returns: | Type | Description | | ---------------- | ----------------------------------- | | `RemoteCompiled` | The RemoteCompiled handle for spec. | ### `RemoteCompiled` A compiled handle tied to a Quandela Cloud platform. It implements the `Compiled` protocol (`run`, `expectation` and `jacobian`), and adds `submit` and `dry_run_info`. `QuandelaCloudBackend.compile` builds it. Nothing connects to the cloud until the first `run` or `submit`. Parameters: | Name | Type | Description | Default | | --------- | ---------------------- | ----------------------------------------------------------------------- | ---------- | | `spec` | `PhotonicCircuitSpec` | The circuit that this handle submits. | *required* | | `backend` | `QuandelaCloudBackend` | The platform, the credentials and the sampling settings to submit with. | *required* | #### `dry_run_info()` Describe the job that *would* be submitted, offline and without cost. Returns: | Type | Description | | ------ | --------------------------------------------------------------------------------------------------------- | | `dict` | A dictionary with the keys "platform", "n_modes", "n_photons", "input_state", "n_components" and "shots". | #### `expectation(weights, inputs, observable)` Estimate an observable from one sampled run, and wait for the result. The observable is diagonal in the Fock basis. Parameters: | Name | Type | Description | Default | | ------------ | -------------------- | ------------------------------------ | ---------- | | `weights` | `ndarray` | The 1-D weight vector. | *required* | | `inputs` | `ndarray` | The 1-D input vector. | *required* | | `observable` | `PhotonicObservable` | A PhotonNumber or a FockProbability. | *required* | Raises: | Type | Description | | ------------ | --------------------------------------------------------------------------------------------------------------------- | | `ValueError` | If observable can never match an output that is kept by post-selection. This is checked before anything is submitted. | #### `jacobian(weights, inputs, observable, *, method='parameter_shift', need_weight_grad=True, need_input_grad=True)` Always raise, because gradients are not available on sampled hardware. Parameters: | Name | Type | Description | Default | | ------------------ | -------------------- | ------------------------------------ | ------------------- | | `weights` | `ndarray` | The 1-D weight vector. | *required* | | `inputs` | `ndarray` | The 1-D input vector. | *required* | | `observable` | `PhotonicObservable` | A PhotonNumber or a FockProbability. | *required* | | `method` | `str` | This argument is ignored. | `'parameter_shift'` | | `need_weight_grad` | `bool` | This argument is ignored. | `True` | | `need_input_grad` | `bool` | This argument is ignored. | `True` | Raises: | Type | Description | | --------------------- | ----------------------------------------------------------- | | `NotImplementedError` | Always. Use the local PercevalBackend to compute gradients. | #### `run(weights, inputs)` Sample the circuit on the cloud, and return the estimated Fock distribution. The call waits for the job to finish. The distribution is a dictionary `{photon_counts: probability}`. Parameters: | Name | Type | Description | Default | | --------- | --------- | ---------------------- | ---------- | | `weights` | `ndarray` | The 1-D weight vector. | *required* | | `inputs` | `ndarray` | The 1-D input vector. | *required* | Raises: | Type | Description | | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `TimeoutError` | If the job has not finished within poll_timeout_seconds. The job is cancelled first, because the caller never gets its handle. The same happens on Ctrl-C. Use submit to keep a job running after the wait. | | `RuntimeError` | If the job fails, or if post-selection discards every shot. | #### `submit(weights, inputs)` Submit the job and return a handle at once, without waiting for the job. Parameters: | Name | Type | Description | Default | | --------- | --------- | ---------------------- | ---------- | | `weights` | `ndarray` | The 1-D weight vector. | *required* | | `inputs` | `ndarray` | The 1-D input vector. | *required* | Returns: | Type | Description | | ----------------- | ------------------------------------------------------------- | | `RemoteJobHandle` | The RemoteJobHandle of the job, with .result() and .status(). | ### `RemoteJobHandle` A handle to a Quandela Cloud job that is running. It is the linear-optics counterpart of the qubit `JobHandle`. `RemoteCompiled.submit` returns it, and it is not meant to be built directly. Parameters: | Name | Type | Description | Default | | ---------------------- | ----------- | --------------------------------------------------------------------------------- | ---------- | | `job` | `RemoteJob` | The Perceval job that was submitted. | *required* | | `n_modes` | `int` | The number of modes of the circuit, used to read the sampled Fock states. | *required* | | `poll_interval` | `float` | The time, in seconds, between two checks of the status in result(). Default: 2.0. | `2.0` | | `poll_timeout_seconds` | `float` | How long, in seconds, result() waits before it stops. Default: 3600. | `3600` | #### `cancel()` Ask Quandela Cloud to cancel the job, without waiting. #### `result()` Wait until the job finishes, then return the estimated Fock distribution. Raises: | Type | Description | | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `TimeoutError` | If the job has not finished after poll_timeout_seconds. The job keeps running. Call result() again or cancel(), or find the job in the Quandela console with the id given in the message. | | `RuntimeError` | If the job ended in error or was cancelled, or if post-selection discarded every shot. | #### `status()` Return the state of the job as Perceval reports it, without waiting. For example, `"RUNNING"` or `"SUCCESS"`. ## `eigenkit.qml.qrc` Quantum reservoir computing (QRC) estimators, compatible with scikit-learn. A reservoir is a circuit with fixed random weights, which are never trained. These estimators sample the weights once (with the seed `random_state`), run the circuit over the input sequences with reservoir.run_reservoir(), extract classical features, and train only a linear readout. This split into a fixed reservoir and a linear readout is the standard form of QRC. So these estimators belong with the scikit-learn estimators (next to `QSVC` and `QSVR`), and not in the PyTorch package. They are thin wrappers around the functions of the `reservoir` package. For full control (custom processing of the features, custom readouts), call `run_reservoir` and the feature extractors directly. Data shapes: `X` has shape `(n_sequences, n_timesteps, n_features)`, and `y` has shape `(n_sequences, n_timesteps)`, with one target per timestep (sequence to sequence). `washout` drops the first timesteps from the fit of the readout, so that the reservoir can forget its initial state. Predictions are still made for every timestep. ### `QuantumReservoirClassifier` Bases: `_ReservoirMixin`, `BaseQuantumClassifier` Quantum reservoir classifier: a fixed random reservoir and a logistic readout. It uses the same reservoir as QuantumReservoirRegressor. The readout is a scikit-learn `LogisticRegression` on the features of each timestep. `y` holds integer or label targets, of shape `(n_sequences, n_timesteps)`. Parameters: | Name | Type | Description | Default | | -------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | `blocks` | `list` | The architecture of the reservoir circuit, a list of blocks that is assembled once. Its n_inputs must match the policy: F for the policies that carry the state, and window * F for state_policy="stateless", where F is the number of features per timestep. | *required* | | `state_policy` | `str` | "carry" (the unitary of the circuit carries the state from one step to the next), "stateless" (each step starts from | 0…0⟩ and encodes a sliding window of inputs), or "leaky" (the carried state is mixed with a reset state and renormalised). | | `window` | `int` | The length of the sliding window, for state_policy="stateless". | `1` | | `leak` | `float` | The mixing coefficient alpha, for state_policy="leaky". | `1.0` | | `features` | `FeaturesSpec` | The features read from the reservoir. It is "z" (the value of ⟨Z⟩ on each qubit, the default), "probabilities", "amplitudes", a Pauli or Hamiltonian string (for example "0.5 Z0 X1 - 0.3 Y0"), or a list of such strings, to read out any observables. | `'z'` | | `C` | `float` | The inverse regularisation strength of the logistic readout, passed to scikit-learn. Smaller values regularise more. This is the only argument that the regressor does not have: there, the readout is a ridge regression controlled by ridge. | `1.0` | | `washout` | `int` | The number of timesteps dropped from the fit of the readout. They are not dropped from the predictions. | `0` | | `weight_scale` | `float` | The standard deviation of the sampled reservoir weights. | `1.0` | | `provider` | `ProviderSpec` | The backend, which must give access to the statevector. Default: "numpy". | `'numpy'` | | `random_state` | \`int | None\` | The seed of the fixed reservoir weights. | #### `__sklearn_is_fitted__()` Return True when the readout has been fitted. #### `fit(X, y)` Fit the estimator on `X` and `y`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------------------------------------------------- | ---------- | | `X` | `ndarray` | The sequences, of shape (n_sequences, n_timesteps, n_features). A 2-D array is rejected. | *required* | | `y` | `ndarray` | One target for each timestep, of shape (n_sequences, n_timesteps). | *required* | Returns: | Type | Description | | ------------------------------ | --------------------------------------------- | | `'QuantumReservoirClassifier'` | This estimator, so that calls can be chained. | #### `predict(X)` Predict a label for every timestep of every sequence in `X`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------------------------------------------------- | ---------- | | `X` | `ndarray` | The sequences, of shape (n_sequences, n_timesteps, n_features). A 2-D array is rejected. | *required* | Returns: | Type | Description | | --------- | ---------------------------------------------------------- | | `ndarray` | The predicted labels, of shape (n_sequences, n_timesteps). | #### `predict_proba(X)` Predict the class probabilities for every timestep of every sequence in `X`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------------------------------------------------- | ---------- | | `X` | `ndarray` | The sequences, of shape (n_sequences, n_timesteps, n_features). A 2-D array is rejected. | *required* | Returns: | Type | Description | | --------- | ---------------------------------------------------------------------------------------------------------------- | | `ndarray` | The probabilities, of shape (n_sequences, n_timesteps, n_classes), with the last axis in the order of classes\_. | #### `score(X, y)` Score the estimator on `X` against `y`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------------------------------------------------- | ---------- | | `X` | `ndarray` | The sequences, of shape (n_sequences, n_timesteps, n_features). A 2-D array is rejected. | *required* | | `y` | `ndarray` | One target for each timestep, of shape (n_sequences, n_timesteps). | *required* | Returns: | Type | Description | | ------- | ----------------------------------------------------------------------- | | `float` | The accuracy of the predictions, over every timestep of every sequence. | ### `QuantumReservoirRegressor` Bases: `_ReservoirMixin`, `BaseQuantumRegressor` Quantum reservoir regressor: a fixed random reservoir and a ridge readout. Parameters: | Name | Type | Description | Default | | -------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | `blocks` | `list` | The architecture of the reservoir circuit, a list of blocks that is assembled once. Its n_inputs must match the policy: F for the policies that carry the state, and window * F for state_policy="stateless", where F is the number of features per timestep. | *required* | | `state_policy` | `str` | "carry" (the unitary of the circuit carries the state from one step to the next), "stateless" (each step starts from | 0…0⟩ and encodes a sliding window of inputs), or "leaky" (the carried state is mixed with a reset state and renormalised). | | `window` | `int` | The length of the sliding window, for state_policy="stateless". | `1` | | `leak` | `float` | The mixing coefficient alpha, for state_policy="leaky". | `1.0` | | `features` | `FeaturesSpec` | The features read from the reservoir. It is "z" (the value of ⟨Z⟩ on each qubit, the default), "probabilities", "amplitudes", a Pauli or Hamiltonian string (for example "0.5 Z0 X1 - 0.3 Y0"), or a list of such strings, to read out any observables. | `'z'` | | `ridge` | `float` | The strength of the L2 penalty of the linear readout, which is solved in closed form. | `1e-06` | | `washout` | `int` | The number of timesteps dropped from the fit of the readout. They are not dropped from the predictions. | `0` | | `weight_scale` | `float` | The standard deviation of the sampled reservoir weights. | `1.0` | | `provider` | `ProviderSpec` | The backend, which must give access to the statevector. Default: "numpy". | `'numpy'` | | `random_state` | \`int | None\` | The seed of the fixed reservoir weights. | #### `__sklearn_is_fitted__()` Return True when the readout has been fitted. #### `fit(X, y)` Fit the estimator on `X` and `y`. Parameters: | Name | Type | Description | Default | | ---- | --------- | -------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `X` | `ndarray` | The sequences, of shape (n_sequences, n_timesteps, n_features). A 2-D array is rejected. | *required* | | `y` | `ndarray` | One target for each timestep, of shape (n_sequences, n_timesteps), or (n_sequences, n_timesteps, n_outputs) for several outputs. | *required* | Returns: | Type | Description | | ----------------------------- | --------------------------------------------- | | `'QuantumReservoirRegressor'` | This estimator, so that calls can be chained. | #### `predict(X)` Predict a value for every timestep of every sequence in `X`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------------------------------------------------- | ---------- | | `X` | `ndarray` | The sequences, of shape (n_sequences, n_timesteps, n_features). A 2-D array is rejected. | *required* | Returns: | Type | Description | | --------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `ndarray` | The predictions, of shape (n_sequences, n_timesteps), or (n_sequences, n_timesteps, n_outputs) when the targets had several outputs. | #### `score(X, y)` Score the estimator on `X` against `y`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------------------------------------------------- | ---------- | | `X` | `ndarray` | The sequences, of shape (n_sequences, n_timesteps, n_features). A 2-D array is rejected. | *required* | | `y` | `ndarray` | One target for each timestep, of shape (n_sequences, n_timesteps). | *required* | Returns: | Type | Description | | ------- | ---------------------------------------------------------------------------------------------- | | `float` | The coefficient of determination R² of the predictions, over every timestep of every sequence. | ## `eigenkit.qml.qsvc` ## `eigenkit.qml.qsvc.QSVC` Bases: `BaseQuantumClassifier` Quantum support vector classifier (QSVC). It computes a quantum kernel matrix from a fidelity circuit, and then uses `SVC(kernel="precomputed")` from scikit-learn. Parameters: | Name | Type | Description | Default | | -------------- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `feature_map` | `str` | The feature map. It is "Angle" (angle encoding into a product state) or "AngleCNOT" (angle encoding with ladders of controlled-NOT (CNOT) gates). Default: "Angle". It is ignored when kernel is given. | `'Angle'` | | `reps` | `int` | The number of repetitions of the feature map. Default: 1. It is ignored when kernel is given. | `1` | | `C` | `float` | The regularisation parameter of the support vector machine. Default: 1.0. | `1.0` | | `probability` | `bool` | If True, the model gives Platt-scaled probabilities. If False (the default), predict_proba is unavailable. The calibration is a sigmoid fitted by cross-validation on the training kernel, as in CalibratedClassifierCV(SVC(), ensemble=False). The predictions still come from the plain SVC. | `False` | | `provider` | `ProviderSpec` | The backend. It takes the same options as Circuit(provider=...). Any backend with a statevector or a zero_state_probability method works, including Qiskit Runtime and IQM hardware. Default: "numpy". It is ignored when kernel is given. | `'numpy'` | | `random_state` | \`int | None\` | The seed. | | `kernel` | \`QuantumKernel | None\` | A QuantumKernel to use instead of building one from feature_map, reps and provider. It can be a custom kernel, a trained kernel or a hardware kernel. Default: None. | | `scale` | `float` | The bandwidth. The inputs are multiplied by it before they reach the kernel, so it can be tuned with GridSearchCV. Default: 1.0. | `1.0` | Example ```python import numpy as np from eigenkit import QSVC, Encoding, QuantumKernel, Variational rng = np.random.default_rng(0) X_train, X_test = rng.uniform(0, np.pi, (8, 4)), rng.uniform(0, np.pi, (3, 4)) y_train = np.array([0, 1] * 4) model = QSVC(feature_map="AngleCNOT", reps=2) model.fit(X_train, y_train) predictions = model.predict(X_test) # Any kernel works, including custom and trained ones. my_map = [Encoding(in_features=4), Variational(depth=1)] model = QSVC(kernel=QuantumKernel(n_features=4, feature_map=my_map)) ``` ### `decision_function(X)` Return the signed distance of each sample to the separating hyperplane. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | Returns: | Type | Description | | --------- | ------------------------------------------------------------------------------------- | | `ndarray` | An array of shape (n_samples,) for two classes, and (n_samples, n_classes) otherwise. | ### `fit(X, y)` Fit the classifier on training data. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | | `y` | `ndarray` | The class labels, of shape (n_samples,). | *required* | Returns: | Type | Description | | ------ | --------------------------------------------- | | `QSVC` | This estimator, so that calls can be chained. | ### `load(path, *, provider=None)` Load a fitted model from a `.eigenkit` bundle. Load only bundles that you trust. A bundle can contain joblib pickles, and unpickling runs code, as with `torch.load` or `joblib.load`. Parameters: | Name | Type | Description | Default | | ---------- | -------------- | ----------- | --------------------------------------- | | `path` | \`str | Path\` | The path of the bundle. | | `provider` | \`ProviderSpec | None\` | A provider that replaces the saved one. | Returns: | Type | Description | | ------------------------ | --------------------- | | `'BaseQuantumEstimator'` | The loaded estimator. | Raises: | Type | Description | | ----------- | ---------------------------------------------------------------------------------------------------------------------------- | | `TypeError` | If the method is called on a subclass and the bundle holds a different class. BaseQuantumEstimator.load() accepts any class. | ### `predict(X)` Predict the class labels of `X`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | Returns: | Type | Description | | --------- | ------------------------------- | | `ndarray` | One prediction for each sample. | ### `predict_proba(X)` Predict the probability of each class. It requires `probability=True`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | Returns: | Type | Description | | --------- | ----------------------------------------------------------------------------------------------- | | `ndarray` | The probabilities, of shape (n_samples, n_classes), with the columns in the order of classes\_. | Raises: | Type | Description | | ------------ | ------------------------------------------------ | | `ValueError` | If the model was created with probability=False. | ### `save(path)` Save a fitted model to a `.eigenkit` bundle. The bundles of models that hold scikit-learn objects (`QSVC`, `QSVR`, the reservoir estimators and the time-series estimators) contain joblib pickles, so loading one can run code. Share them only with people who trust you. Parameters: | Name | Type | Description | Default | | ------ | ----- | ----------- | ------------------------------------------------------------------------- | | `path` | \`str | Path\` | The path of the bundle. The suffix .eigenkit is added when it is missing. | ## `eigenkit.qml.qsvr` ## `eigenkit.qml.qsvr.QSVR` Bases: `BaseQuantumRegressor` Quantum support vector regressor (QSVR). It is the regression counterpart of QSVC. It uses `SVR(kernel="precomputed")` from scikit-learn on the quantum kernel matrix. Parameters: | Name | Type | Description | Default | | -------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | | `feature_map` | `str` | The feature map. It is "Angle" (angle encoding into a product state) or "AngleCNOT" (angle encoding with ladders of controlled-NOT (CNOT) gates). Default: "Angle". It is ignored when kernel is given. | `'Angle'` | | `reps` | `int` | The number of repetitions of the feature map. Default: 1. It is ignored when kernel is given. | `1` | | `C` | `float` | The regularisation parameter of the support vector regressor. Default: 1.0. | `1.0` | | `epsilon` | `float` | The width of the epsilon-insensitive tube. Default: 0.1. | `0.1` | | `provider` | `ProviderSpec` | The backend. It takes the same options as Circuit(provider=...). Default: "numpy". It is ignored when kernel is given. | `'numpy'` | | `random_state` | \`int | None\` | The seed. | | `kernel` | \`QuantumKernel | None\` | A QuantumKernel to use instead of building one from feature_map, reps and provider. Default: None. | | `scale` | `float` | The bandwidth. The inputs are multiplied by it before they reach the kernel. Default: 1.0. | `1.0` | Example ```python import numpy as np from eigenkit import QSVR rng = np.random.default_rng(0) X_train, X_test = rng.uniform(0, np.pi, (8, 2)), rng.uniform(0, np.pi, (3, 2)) y_train = np.sin(X_train).sum(axis=1) model = QSVR(feature_map="Angle", reps=1) model.fit(X_train, y_train) predictions = model.predict(X_test) ``` ### `fit(X, y)` Fit the regressor on training data. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | | `y` | `ndarray` | The float targets, of shape (n_samples,). | *required* | Returns: | Type | Description | | ------ | --------------------------------------------- | | `QSVR` | This estimator, so that calls can be chained. | ### `load(path, *, provider=None)` Load a fitted model from a `.eigenkit` bundle. Load only bundles that you trust. A bundle can contain joblib pickles, and unpickling runs code, as with `torch.load` or `joblib.load`. Parameters: | Name | Type | Description | Default | | ---------- | -------------- | ----------- | --------------------------------------- | | `path` | \`str | Path\` | The path of the bundle. | | `provider` | \`ProviderSpec | None\` | A provider that replaces the saved one. | Returns: | Type | Description | | ------------------------ | --------------------- | | `'BaseQuantumEstimator'` | The loaded estimator. | Raises: | Type | Description | | ----------- | ---------------------------------------------------------------------------------------------------------------------------- | | `TypeError` | If the method is called on a subclass and the bundle holds a different class. BaseQuantumEstimator.load() accepts any class. | ### `predict(X)` Predict continuous targets for `X`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | Returns: | Type | Description | | --------- | ------------------------------- | | `ndarray` | One prediction for each sample. | ### `save(path)` Save a fitted model to a `.eigenkit` bundle. The bundles of models that hold scikit-learn objects (`QSVC`, `QSVR`, the reservoir estimators and the time-series estimators) contain joblib pickles, so loading one can run code. Share them only with people who trust you. Parameters: | Name | Type | Description | Default | | ------ | ----- | ----------- | ------------------------------------------------------------------------- | | `path` | \`str | Path\` | The path of the bundle. The suffix .eigenkit is added when it is missing. | ## `eigenkit.reservoir.driver` The reservoir driver: run a frozen circuit over input sequences, carrying quantum state across timesteps per a policy, and return the statevector trajectory. The reservoir (circuit + fixed `weights`) is never trained here — this is the forward pass that produces the features a classical readout learns on. Two dispatch paths, chosen by the policy: - **Stateless / parallel** (policy exposes `expand_inputs`): all `$B \cdot T$` timesteps are independent, so they evolve in a single batched `statevector` sweep. No Python timestep loop. - **Sequential** (policy implements `initial` / `inject` / `advance`): the quantum state is carried step to step, so timesteps run in order. The `B` sequences still evolve in parallel within each step via per-sample injection. ### `run_reservoir(circuit, policy, sequences, weights)` Evolve `circuit` over `sequences` under `policy`, with frozen `weights`. The result is the trajectory of statevectors, a complex array of shape `(B, T, 2**n)`. `sequences` has shape `(B, T, F)`: `B` independent series, `T` timesteps and `F` features per step. `weights` is the fixed 1-D parameter vector of the reservoir, shared by the whole batch. Features are extracted separately from the returned trajectory (see the feature extractors), so the same run can produce expectation, probability or amplitude features without evolving the circuit again. Parameters: | Name | Type | Description | Default | | ----------- | --------- | --------------------------------------------------- | ---------- | | `circuit` | `Circuit` | The reservoir circuit, run with frozen weights. | *required* | | `policy` | `Policy` | How the state passes from one timestep to the next. | *required* | | `sequences` | `ndarray` | The inputs, of shape (B, T, F). | *required* | | `weights` | `ndarray` | The frozen weights of the reservoir. | *required* | Returns: | Type | Description | | --------- | -------------------------------------------------------- | | `ndarray` | The trajectory of statevectors, of shape (B, T, 2\*\*n). | ## `eigenkit.reservoir.features` Feature extractors: map a `(B, T, 2**n)` statevector trajectory to real features for a classical readout. The headline extractor is expectation_features(), which measures arbitrary Pauli observables / Hamiltonians given as the library's `Observable` / `PauliExpr` types or a Hamiltonian string — so you get `$\langle\psi|H|\psi\rangle$` features without hand-rolling Pauli algebra. probability_features() and amplitude_features() read out the same trajectory differently. All extractors are pure numpy and operate on the whole `(B, T)` grid at once. ### `amplitude_features(states)` The raw statevector as real features: its real and imaginary parts, concatenated. `states` is a complex array of shape `(B, T, 2**n)`, and the result is a real array of shape `(B, T, 2 * 2**n)`. It exposes the full state, which only a simulator can provide; it cannot be measured on hardware. Parameters: | Name | Type | Description | Default | | -------- | --------- | -------------------------------------------------------- | ---------- | | `states` | `ndarray` | The trajectory of statevectors, of shape (B, T, 2\*\*n). | *required* | Returns: | Type | Description | | --------- | -------------------------------------------------------------- | | `ndarray` | The real and imaginary parts, concatenated, for each timestep. | ### `expectation_features(states, observables)` Expectation values `⟨ψ|H_k|ψ⟩` of each observable, over the whole trajectory. `states` is a complex array of shape `(B, T, 2**n)`, and the result is a real array of shape `(B, T, n_obs)`. `observables` can be an `Observable`, a `PauliExpr` or a Hamiltonian string, or a sequence of these. These are the same types that the rest of the library uses, so measuring an arbitrary Hamiltonian, as QRC experiments often require, needs no manual handling of Pauli terms. A single Hamiltonian string is one observable, and gives one feature. Parameters: | Name | Type | Description | Default | | ------------- | ---------------------- | -------------------------------------------------------- | ---------- | | `states` | `ndarray` | The trajectory of statevectors, of shape (B, T, 2\*\*n). | *required* | | `observables` | `Sequence[Observable]` | The observables to measure. | *required* | Returns: | Type | Description | | --------- | -------------------------------------------- | | `ndarray` | One feature per observable and per timestep. | ### `pauli_z_features(states)` The expectation value `⟨Z_q⟩` on each qubit, the usual default readout in QRC. `states` has shape `(B, T, 2**n)`, and the result has shape `(B, T, n)`. The values are computed with expectation_features(), so both functions share the same tested measurement code. Parameters: | Name | Type | Description | Default | | -------- | --------- | -------------------------------------------------------- | ---------- | | `states` | `ndarray` | The trajectory of statevectors, of shape (B, T, 2\*\*n). | *required* | Returns: | Type | Description | | --------- | --------------------------------------- | | `ndarray` | One feature per qubit and per timestep. | ### `probability_features(states)` The measurement distribution over all `2**n` basis states, `|ψ|²`. `states` is a complex array of shape `(B, T, 2**n)`, and the result is a real array of the same shape. Parameters: | Name | Type | Description | Default | | -------- | --------- | -------------------------------------------------------- | ---------- | | `states` | `ndarray` | The trajectory of statevectors, of shape (B, T, 2\*\*n). | *required* | Returns: | Type | Description | | --------- | ---------------------------------------------------------- | | `ndarray` | The distribution over the basis states, for each timestep. | ## `eigenkit.reservoir.policies` Reservoir policies — how each timestep maps the carried state and the current input to the circuit's `(initial_state, gate_inputs)`. Two execution shapes, because they are genuinely different computations: - **Sequential (a fold over time).** The quantum state *is* the memory: `$\psi_t$` depends on `$\psi_{t-1}$`, so timesteps must run in order. These policies implement `initial` / `inject` / `advance` (Carry, LeakyCarry). - **Parallel (a map over time).** The quantum state carries nothing between steps — each step's input is a pure function of the raw input sequence (e.g. a sliding window). These policies expose `expand_inputs(sequences)` and the driver runs all `B·T` evaluations in **one batched sweep** instead of a Python timestep loop (Stateless). The driver prefers `expand_inputs` when a policy has it (idiomatic optional method, like the backends' `statevector` / `probabilities`). ### `Carry` Policy that applies the circuit to the carried state: `ψ_t = U(x_t) ψ_{t-1}`. The quantum state is the memory of the reservoir, and the first step starts from `|0…0⟩`. This policy is equivalent to `LeakyCarry(alpha=1.0)`. #### `advance(x_t, evolved, state)` Build the state for the next timestep from the result of this step. Parameters: | Name | Type | Description | Default | | --------- | ---------------- | -------------------------------------------- | ---------- | | `x_t` | `ndarray` | The input of this timestep, of shape (B, F). | *required* | | `evolved` | `ndarray` | The statevector that the circuit produced. | *required* | | `state` | `ReservoirState` | The state that went into this step. | *required* | Returns: | Type | Description | | ---------------- | -------------------------------- | | `ReservoirState` | The state for the next timestep. | #### `initial(batch)` Return the state carried into the first timestep. Parameters: | Name | Type | Description | Default | | ------- | ----- | --------------------------------------------- | ---------- | | `batch` | `int` | The number of sequences that run in parallel. | *required* | Returns: | Type | Description | | ---------------- | --------------------------- | | `ReservoirState` | The initial ReservoirState. | #### `inject(x_t, state)` Turn the input of this timestep into the arguments of the circuit call. Parameters: | Name | Type | Description | Default | | ------- | ---------------- | ---------------------------------------------- | ---------- | | `x_t` | `ndarray` | The input of this timestep, of shape (B, F). | *required* | | `state` | `ReservoirState` | The state carried over from the previous step. | *required* | Returns: | Name | Type | Description | | --------------- | --------- | ----------------------------------------------- | | `initial_state` | \`ndarray | None\` | | `gate_inputs` | `ndarray` | The values to pass to the gates of the circuit. | ### `LeakyCarry` Policy that mixes the carried state with a reset state and renormalises it. It approximates a leaky, dissipative reservoir with pure states: `ψ_t = normalise(α U(x_t) ψ_{t-1} + (1 − α) reset)`. `reset` defaults to `|0…0⟩` and should be a normalised statevector; this is not checked. With `alpha=1`, the policy is exactly Carry. This is not a true dissipative channel, which would need density matrices that the pure-state backends do not provide. It is an approximation that uses pure states only. Attributes: | Name | Type | Description | | ------- | --------- | ------------------------------------------------------------------------------------------------------------------------------ | | `alpha` | `float` | The mixing weight of the carried state, in [0, 1]. Lower values forget faster, and 1 keeps everything, which is exactly Carry. | | `reset` | \`ndarray | None\` | #### `advance(x_t, evolved, state)` Build the state for the next timestep from the result of this step. Parameters: | Name | Type | Description | Default | | --------- | ---------------- | -------------------------------------------- | ---------- | | `x_t` | `ndarray` | The input of this timestep, of shape (B, F). | *required* | | `evolved` | `ndarray` | The statevector that the circuit produced. | *required* | | `state` | `ReservoirState` | The state that went into this step. | *required* | Returns: | Type | Description | | ---------------- | -------------------------------- | | `ReservoirState` | The state for the next timestep. | #### `initial(batch)` Return the state carried into the first timestep. Parameters: | Name | Type | Description | Default | | ------- | ----- | --------------------------------------------- | ---------- | | `batch` | `int` | The number of sequences that run in parallel. | *required* | Returns: | Type | Description | | ---------------- | --------------------------- | | `ReservoirState` | The initial ReservoirState. | #### `inject(x_t, state)` Turn the input of this timestep into the arguments of the circuit call. Parameters: | Name | Type | Description | Default | | ------- | ---------------- | ---------------------------------------------- | ---------- | | `x_t` | `ndarray` | The input of this timestep, of shape (B, F). | *required* | | `state` | `ReservoirState` | The state carried over from the previous step. | *required* | Returns: | Name | Type | Description | | --------------- | --------- | ----------------------------------------------- | | `initial_state` | \`ndarray | None\` | | `gate_inputs` | `ndarray` | The values to pass to the gates of the circuit. | ### `ReservoirPolicy` Bases: `Protocol` A reservoir policy that carries state from one timestep to the next. Parallel policies, which carry no state, do not implement this protocol. They provide `expand_inputs(sequences)`, which returns the inputs of every timestep with shape `(B, T, n_inputs)`, and the driver evaluates all timesteps at once. See Stateless. #### `advance(x_t, evolved, state)` Build the state for the next timestep from the result of this step. Parameters: | Name | Type | Description | Default | | --------- | ---------------- | -------------------------------------------- | ---------- | | `x_t` | `ndarray` | The input of this timestep, of shape (B, F). | *required* | | `evolved` | `ndarray` | The statevector that the circuit produced. | *required* | | `state` | `ReservoirState` | The state that went into this step. | *required* | Returns: | Type | Description | | ---------------- | -------------------------------- | | `ReservoirState` | The state for the next timestep. | #### `initial(batch)` Return the state carried into the first timestep. Parameters: | Name | Type | Description | Default | | ------- | ----- | --------------------------------------------- | ---------- | | `batch` | `int` | The number of sequences that run in parallel. | *required* | Returns: | Type | Description | | ---------------- | --------------------------- | | `ReservoirState` | The initial ReservoirState. | #### `inject(x_t, state)` Turn the input of this timestep into the arguments of the circuit call. Parameters: | Name | Type | Description | Default | | ------- | ---------------- | ---------------------------------------------- | ---------- | | `x_t` | `ndarray` | The input of this timestep, of shape (B, F). | *required* | | `state` | `ReservoirState` | The state carried over from the previous step. | *required* | Returns: | Name | Type | Description | | --------------- | --------- | ----------------------------------------------- | | `initial_state` | \`ndarray | None\` | | `gate_inputs` | `ndarray` | The values to pass to the gates of the circuit. | ### `ReservoirState` The state that sequential policies carry from one timestep to the next. `vector` is the `(B, 2**n)` statevector to inject at the next step, and `None` means that the step starts from `|0…0⟩`. `history` holds the recent raw inputs, of shape `(B, k, F)`, for a sequential policy that encodes a window of inputs. Attributes: | Name | Type | Description | | --------- | --------- | ----------- | | `vector` | \`ndarray | None\` | | `history` | \`ndarray | None\` | ### `Stateless` Parallel policy that carries no state. Every step starts from `|0…0⟩`, and the last `window` inputs are encoded as rotation angles of the circuit's gates. The memory is purely classical (the window), and the quantum state carries nothing from one step to the next. Every timestep is therefore independent, and the driver evaluates all of them in one batched run. With `window=1`, the policy has no memory: it encodes only the current input. The circuit's `n_inputs` must equal `window * F`, where `F` is the number of features per step, because each step passes the whole flattened window to the encoding gates. Attributes: | Name | Type | Description | | -------- | ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `window` | `int` | The number of consecutive inputs to encode at each step. 1 means no memory: only the current input is encoded. Whatever the value, the circuit's n_inputs must equal window * F. The default is 1. | #### `expand_inputs(sequences)` Return the gate inputs of every timestep, with shape `(B, T, window * F)`. Step `t` encodes the inputs `t - window + 1` to `t`. The first `window - 1` steps are padded with zeros at the front. No quantum state is involved, so this is only a reshaping of the input sequence. Parameters: | Name | Type | Description | Default | | ----------- | --------- | ------------------------------------------ | ---------- | | `sequences` | `ndarray` | The whole input batch, of shape (B, T, F). | *required* | Returns: | Type | Description | | --------- | ---------------------------------------------------------------- | | `ndarray` | The gate inputs for every timestep, of shape (B, T, window * F). | ## `eigenkit.reservoir.readout` A pure-numpy ridge (Tikhonov) linear readout — the classic QRC head. Frozen-reservoir QRC trains *only* the readout, and the canonical readout is a linear layer fit by ridge regression. That has a closed-form solution, so no iterative optimizer (and no torch/sklearn) is needed — this stays framework-free and lives in the numpy domain ring. For a trainable non-linear readout, use the estimator layer (an MLP head), not this. ### `LinearReadout` Ridge regression in closed form, with a bias term. It solves `W = (XᵀX + λI)⁻¹ XᵀY`, and the bias column is not regularised. `Y` can have one output or several. `ridge` is the strength `λ` of the L2 penalty, which adds `λ‖W‖²` to the squared error of the fit. A small positive value also keeps the solution numerically stable when the reservoir features are collinear. Parameters: | Name | Type | Description | Default | | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | | `ridge` | `float` | The strength λ of the L2 penalty. Raise it when the readout overfits or the feature matrix is nearly singular, and lower it when the readout underfits. The default is 1e-06. | `1e-06` | #### `fit(X, y)` Fit the readout to `X` and `y`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ----------------------------------------------------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | | `y` | `ndarray` | The targets, of shape (n_samples,) for one output, or (n_samples, n_outputs) for several. | *required* | Returns: | Type | Description | | ----------------- | ------------------------------------------- | | `'LinearReadout'` | This readout, so that calls can be chained. | #### `predict(X)` Predict the target for every sample in `X`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | Returns: | Type | Description | | --------- | ----------------------------------------------------------------------------------------------------- | | `ndarray` | One prediction per sample, with one column per output when the readout was fitted on several outputs. | ## `eigenkit.torch.circuit` The PyTorch `nn.Module` circuit, which is the trainable quantum layer for users. `TorchCircuit` stores its trainable weights as an `nn.Parameter` and passes the execution to the core `Circuit` with the PyTorch interface. It can therefore be used inside any PyTorch model (`nn.Sequential` or your own `nn.Module`), and it trains with a standard PyTorch optimiser. The gradients are computed with the method that the active backend declares: the backend's own automatic differentiation, the parameter-shift rule or the adjoint method. The top-level `Circuit(..., interface="torch")` function returns this class. It accepts a list of blocks, a `CircuitSpec` that is already assembled, or a `CircuitBuilder`. ### `TorchCircuit` Bases: `Module` A quantum layer that stores trainable weights and runs on any registered backend. It runs through the PyTorch interface, so that PyTorch can compute the gradients of its output. Parameters: | Name | Type | Description | Default | | ------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | | `blocks` | `_BlocksOrSpec` | The circuit. It can be a list of blocks to assemble, a CircuitSpec that is already assembled, or a CircuitBuilder. A builder is built when the layer is created, so gates added to it afterwards are not part of this layer. | *required* | | `observables` | `ObservableLike` | What to measure. None measures Z on every qubit and returns one value per qubit. A single Observable or PauliExpr returns a single value, and a list of them returns one value for each. Default: None. | `None` | | `provider` | `ProviderSpec` | The backend that runs the circuit. It can be a registered name, a configuration instance, or a provider object. Any backend works. A backend that computes gradients itself avoids the fallback to the parameter-shift rule. Default: "numpy". | `'numpy'` | | `init` | \`Tensor | None\` | The initial weights, of shape (n_weights,). They are random when init is not given. Default: None. | #### `n_qubits` The number of qubits of the circuit in `spec`. #### `out_features` The number of observables, or `1` for a single observable. #### `provider` The backend that this circuit was compiled for. It is the same as Circuit.provider, which turns the `provider=` argument (a registered name, a configuration or a backend object) into one `Provider`. The registered name is `provider.name`. To run the same circuit on another backend, use `with_provider()`, which returns a new instance and leaves this one unchanged. #### `spec` The circuit that this layer runs. It is the CircuitSpec assembled from `blocks`, or the one that was passed in already assembled. #### `draw(*, decimals=3)` Return a text drawing of the circuit, over several lines. The drawing does not depend on the provider: it is built from the `CircuitSpec`, so it is the same whatever the active backend is. Print the returned string directly. Example ```python from eigenkit import Circuit, Encoding, Variational blocks = [Encoding(in_features=2), Variational(depth=1)] circuit = Circuit(blocks=blocks, interface="torch") print(circuit.draw()) # q_0: ──RY(x_0)──RY(w_0)──●── # │ # q_1: ──RY(x_1)──RY(w_1)──X── ``` Parameters: | Name | Type | Description | Default | | ---------- | ----- | ---------------------------------------------------------------------------- | ------- | | `decimals` | `int` | The number of significant digits shown for a numeric gate angle. Default: 3. | `3` | Returns: | Type | Description | | ----- | ------------------------------------------------------- | | `str` | The drawing, as a single string that contains newlines. | #### `draw_mpl(**kwargs)` Draw the circuit as a matplotlib figure, and return `(fig, ax)`. The picture does not depend on the provider, because it is built from the `CircuitSpec` and the observables. matplotlib is an optional dependency. Install it with `eigenkit[viz]`. The keyword arguments are passed to draw_mpl(). #### `dry_run_info()` Describe the prepared circuit in a dictionary, without submitting it. It is useful to check the size of the circuit, the gate counts after transpilation, the number of shots and the number of circuits that each call runs, before you submit to a cloud simulator or a quantum processing unit (QPU). It does not estimate a price. It is available only on the backends that provide it. Currently, these are Braket (on its local simulator as well as on cloud devices), Qiskit Runtime and IQM. Other backends raise a `NotImplementedError`. #### `forward(x=None)` Run the layer. Parameters: | Name | Type | Description | Default | | ---- | -------- | ----------------------------------------------- | ------- | | `x` | `Tensor` | The input batch, of shape (batch, in_features). | `None` | Returns: | Type | Description | | -------- | --------------------------------------- | | `Tensor` | The output of the layer for that batch. | #### `reset_parameters()` Draw new circuit weights from the current PyTorch random number generator. Every `nn.Module` that PyTorch provides defines this method. The estimators call `model.apply(reset_parameters)` after setting the seed, so that `random_state` controls the whole initialisation. Without this method, the estimators would skip the layer: two fits with different values of `random_state` would give different classical weights but exactly the same quantum weights, and a user who restarts with new seeds to escape a barren plateau would change only the part of the model that is not the circuit. #### `sample(x=None)` Return the raw measurement histogram for the current weights and input. The histogram maps each bitstring to its number of shots. In each bitstring, qubit `0` is the most significant bit. It is the second step of the workflow in which you train on a simulator and then run on hardware: you train the layer, and then you read what the device measured. It takes one sample, and its result is not differentiable, because the counts are integers that PyTorch does not record for automatic differentiation. Parameters: | Name | Type | Description | Default | | ---- | -------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `x` | \`Tensor | None\` | The input features of one sample, as a 1-D tensor. None works only for a circuit that takes no inputs, and it raises a ValueError for any other circuit. Default: None. | Returns: | Name | Type | Description | | -------- | ---------------- | ------------------------------------------------ | | `counts` | `dict[str, int]` | The number of shots for each measured bitstring. | Raises: | Type | Description | | --------------------- | ------------------------------- | | `NotImplementedError` | On backends that do not sample. | | `ValueError` | If x is 2-D. | #### `submit(x=None)` Submit a job without waiting for it, and return a `JobHandle`. Only the backends that provide submission support it (Braket, Qiskit Runtime and IQM). Parameters: | Name | Type | Description | Default | | ---- | -------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `x` | \`Tensor | None\` | One 1-D input sample. None works only for a circuit that takes no inputs, and it raises a ValueError for any other circuit. Default: None. | Returns: | Name | Type | Description | | -------- | ----------- | ------------------------------- | | `handle` | `JobHandle` | A handle for the submitted job. | #### `with_provider(provider, *, copy_weights=True, init=None)` Return a new `TorchCircuit` for another `provider`. The new circuit has the same `spec` and the same `observables` argument as this instance, and it is compiled for the given `provider`. `provider` accepts the same forms as in `__init__()`: a registered name (a `str`), a configuration object with a `make_backend()` method (for example `PennyLaneConfig`), or a backend instance. The gradients follow the `gradient_method` of the new backend. The forward values should match across simulators up to numerical tolerance, but not exactly. Parameters: | Name | Type | Description | Default | | -------------- | -------------- | ------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | `provider` | `ProviderSpec` | The backend for the new circuit. | *required* | | `copy_weights` | `bool` | If True, the new circuit starts from a copy of self.weights. It is ignored when init is given. Default: True. | `True` | | `init` | \`Tensor | None\` | An optional weight tensor of shape (spec.n_weights,). When it is given, it takes precedence over copy_weights. Default: None. | Returns: | Type | Description | | ---------------- | --------------------- | | `'TorchCircuit'` | The new TorchCircuit. | #### `zero_state_probability(x=None)` Return the probability of measuring the all-zeros state. It uses the current weights and the given input. It is useful for fidelity kernels and swap tests. It takes one sample, and its result is not differentiable. The core reads the amplitude through the backend's access to the state and returns a plain `float`, so PyTorch does not record it for automatic differentiation. A batch raises an error, because a single number cannot hold a batch axis. For a batch, call this method once for each row, or use `Circuit.zero_state_probabilities`. Parameters: | Name | Type | Description | Default | | ---- | -------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `x` | \`Tensor | None\` | The input features of one sample, as a 1-D tensor. None works only for a circuit that takes no inputs, and it raises a ValueError for any other circuit. Default: None. | Returns: | Type | Description | | ------- | --------------------------------------------- | | `float` | The all-zeros probability, a float in [0, 1]. | Raises: | Type | Description | | ------------ | ------------ | | `ValueError` | If x is 2-D. | ## `eigenkit.torch.estimators` The PyTorch estimators: differentiable hybrid quantum-classical models. The estimator base classes compatible with scikit-learn, the quantum kernels, the support vector machines and the saving and loading of models are in a separate package, `eigenkit.qml`, which does not need PyTorch. The base classes and `load_model` are also exported here for convenience, because the hybrid estimators build on them. ### `BaseQuantumClassifier` Bases: `ClassifierMixin`, `BaseQuantumEstimator` Base class for quantum classifiers. Its `score()` returns the accuracy. #### `predict_proba(X)` Predict the class probabilities. Subclasses should return the probabilities in the order of `classes_`, with shape `(n_samples, n_classes)`. The default implementation raises a `NotImplementedError`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | Returns: | Type | Description | | --------- | ----------------------------------------------------------------------------------------------- | | `ndarray` | The probabilities, of shape (n_samples, n_classes), with the columns in the order of classes\_. | ### `BaseQuantumEstimator` Bases: `BaseEstimator`, `ABC` Base class, compatible with scikit-learn, for all quantum algorithms. Parameters: | Name | Type | Description | Default | | -------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `provider` | `ProviderSpec` | The backend. It can be a registered name ("pennylane", "qiskit", "braket", or any name passed to register()), a configuration instance (for example PennyLaneConfig(device_name=...)), or a backend instance. | `'numpy'` | | `random_state` | \`int | None\` | The seed for reproducibility, applied inside fit() and load(). What it seeds depends on the estimator. The PyTorch estimators isolate the random number generator of PyTorch, so seeding one of them does not reseed the random stream of the caller. The kernel estimators are deterministic, and they take it only so that all the estimators have the same interface. | #### `fit(X, y)` Fit the estimator on `X` and `y`. Parameters: | Name | Type | Description | Default | | ---- | --------- | -------------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | | `y` | `ndarray` | One target for each sample, of shape (n_samples,). | *required* | Returns: | Type | Description | | ------------------------ | --------------------------------------------- | | `'BaseQuantumEstimator'` | This estimator, so that calls can be chained. | #### `load(path, *, provider=None)` Load a fitted model from a `.eigenkit` bundle. Load only bundles that you trust. A bundle can contain joblib pickles, and unpickling runs code, as with `torch.load` or `joblib.load`. Parameters: | Name | Type | Description | Default | | ---------- | -------------- | ----------- | --------------------------------------- | | `path` | \`str | Path\` | The path of the bundle. | | `provider` | \`ProviderSpec | None\` | A provider that replaces the saved one. | Returns: | Type | Description | | ------------------------ | --------------------- | | `'BaseQuantumEstimator'` | The loaded estimator. | Raises: | Type | Description | | ----------- | ---------------------------------------------------------------------------------------------------------------------------- | | `TypeError` | If the method is called on a subclass and the bundle holds a different class. BaseQuantumEstimator.load() accepts any class. | #### `predict(X)` Predict a value for every sample in `X`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | Returns: | Type | Description | | --------- | ------------------------------- | | `ndarray` | One prediction for each sample. | #### `save(path)` Save a fitted model to a `.eigenkit` bundle. The bundles of models that hold scikit-learn objects (`QSVC`, `QSVR`, the reservoir estimators and the time-series estimators) contain joblib pickles, so loading one can run code. Share them only with people who trust you. Parameters: | Name | Type | Description | Default | | ------ | ----- | ----------- | ------------------------------------------------------------------------- | | `path` | \`str | Path\` | The path of the bundle. The suffix .eigenkit is added when it is missing. | ### `BaseQuantumRegressor` Bases: `RegressorMixin`, `BaseQuantumEstimator` Base class for quantum regressors. Its `score()` returns `R²`. ### `CustomQuantumClassifier` Bases: `ModelAccessMixin`, `BaseQuantumClassifier` Classifier compatible with scikit-learn, built from your own list of blocks. Give any sequence of blocks (`Encoding`, `Variational`, `StrongVariational`, `Reuploading`, or a custom block from `CircuitBuilder.to_block()`) and an `observables` specification, as for `Circuit`. The estimator connects them into a quantum layer followed by a classical `nn.Linear` head, and trains the whole model with the cross-entropy loss. When the number of features of the data differs from the number of inputs that the circuit expects, an `nn.Linear` adapter is added in front automatically. Parameters: | Name | Type | Description | Default | | ------------------ | ---------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | | `blocks` | `list[Block]` | The list of blocks, in order. | *required* | | `observables` | `ObservableLike` | The observables. It takes the same options as Circuit(observables=...). Default: None (Z on every qubit). | `None` | | `provider` | `ProviderSpec` | The backend. Default: "numpy". | `'numpy'` | | `in_features` | \`int | None\` | The number of features of the data. If None, it is inferred from the first Encoding or Reuploading block. | | `n_classes` | \`int | None\` | The number of output classes. If None, it is inferred from the training labels. | | `lr` | `float` | The learning rate of the optimiser. | `0.01` | | `optimizer` | `str` | The name of the optimiser. The supported names are "adam", "adamw", "rmsprop" and "sgd". | `'adam'` | | `optimizer_kwargs` | \`dict | None\` | Extra keyword arguments passed to the optimiser. | | `epochs` | `int` | The number of training epochs. | `50` | | `batch_size` | `int` | The size of each mini-batch. | `32` | | `show_progress` | `bool` | If True, a tqdm progress bar is shown. | `True` | | `random_state` | \`int | None\` | The seed. | Attributes: | Name | Type | Description | | -------------------- | ---- | -------------------------------------------------------------------------------------------- | | `train_losses_` | | The cross-entropy loss of each epoch, set by fit(). | | `validation_losses_` | | The validation cross-entropy loss of each epoch, set by fit() when validation data is given. | Example ```python import numpy as np from eigenkit import ( CustomQuantumClassifier, Encoding, Variational, all_z, ) rng = np.random.default_rng(0) X_train, X_test = rng.uniform(0, np.pi, (20, 4)), rng.uniform(0, np.pi, (5, 4)) y_train = np.array([0, 1] * 10) blocks = [ Encoding(in_features=4, rotation="RY"), Variational(depth=2, rotation="RY"), ] model = CustomQuantumClassifier( blocks=blocks, observables=all_z(4), epochs=10, ) model.fit(X_train, y_train) predictions = model.predict(X_test) ``` #### `fit(X, y, X_val=None, y_val=None)` Fit the classifier on training data. Parameters: | Name | Type | Description | Default | | ------- | --------- | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | | `X` | `ndarray` | The samples, of shape (n_samples, in_features). | *required* | | `y` | `ndarray` | The class labels, of shape (n_samples,). They can be of any type that scikit-learn's LabelEncoder accepts. | *required* | | `X_val` | \`ndarray | None\` | Optional validation samples, used to compute the validation loss of each epoch. | | `y_val` | \`ndarray | None\` | Optional validation labels, used to compute the validation loss of each epoch. | Returns: | Type | Description | | --------------------------- | --------------------------------------------- | | `'CustomQuantumClassifier'` | This estimator, so that calls can be chained. | Raises: | Type | Description | | ------------ | -------------------------------------------------------------- | | `ValueError` | If in_features is set and X does not have in_features columns. | #### `predict(X)` Predict the class labels of `X`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | Returns: | Type | Description | | --------- | ------------------------------- | | `ndarray` | One prediction for each sample. | #### `predict_proba(X)` Predict the probability of each class, with a softmax over the logits. It returns a float32 array of shape `(n_samples, n_classes)`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | Returns: | Type | Description | | --------- | ----------------------------------------------------------------------------------------------- | | `ndarray` | The probabilities, of shape (n_samples, n_classes), with the columns in the order of classes\_. | ### `CustomQuantumRegressor` Bases: `ModelAccessMixin`, `BaseQuantumRegressor` Regressor compatible with scikit-learn, built from your own list of blocks. It is the regression counterpart of CustomQuantumClassifier. It wraps your quantum `Circuit` with an `nn.Linear` head of one neuron, trained with the chosen regression loss. An `nn.Linear` input adapter is added when the number of features of the data differs from the number of inputs of the circuit. Parameters: | Name | Type | Description | Default | | ------------------ | ---------------- | ----------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | | `blocks` | `list[Block]` | The list of blocks, in order. | *required* | | `observables` | `ObservableLike` | The observables. It takes the same options as Circuit(observables=...). | `None` | | `provider` | `ProviderSpec` | The backend. Default: "numpy". | `'numpy'` | | `in_features` | \`int | None\` | The number of features of the data. If None, it is inferred from the first Encoding or Reuploading block. | | `lr` | `float` | The learning rate of the optimiser. | `0.01` | | `optimizer` | `str` | The name of the optimiser: "adam", "adamw", "rmsprop" or "sgd". | `'adam'` | | `optimizer_kwargs` | \`dict | None\` | Extra keyword arguments passed to the optimiser. | | `epochs` | `int` | The number of training epochs. | `50` | | `batch_size` | `int` | The size of each mini-batch. | `32` | | `loss_func` | `str` | The regression loss: "mse", "mae" or "huber". | `'mse'` | | `huber_delta` | `float` | The threshold of the Huber loss. | `1.0` | | `huber_reduction` | `str` | The reduction of the Huber loss: "mean" or "sum". | `'mean'` | | `show_progress` | `bool` | If True, a tqdm progress bar is shown. | `True` | | `random_state` | \`int | None\` | The seed. | Attributes: | Name | Type | Description | | -------------------- | ---- | ------------------------------------------------------------------------------ | | `train_losses_` | | The training loss of each epoch, set by fit(). | | `validation_losses_` | | The validation loss of each epoch, set by fit() when validation data is given. | #### `fit(X, y, X_val=None, y_val=None)` Fit the regressor on training data. Parameters: | Name | Type | Description | Default | | ------- | --------- | ----------------------------------------------- | ------------------------------------------------------------------------------- | | `X` | `ndarray` | The samples, of shape (n_samples, in_features). | *required* | | `y` | `ndarray` | The real-valued targets, of shape (n_samples,). | *required* | | `X_val` | \`ndarray | None\` | Optional validation samples, used to compute the validation loss of each epoch. | | `y_val` | \`ndarray | None\` | Optional validation targets, used to compute the validation loss of each epoch. | Returns: | Type | Description | | -------------------------- | --------------------------------------------- | | `'CustomQuantumRegressor'` | This estimator, so that calls can be chained. | #### `predict(X)` Predict the continuous targets of `X`. It returns a float32 array. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | Returns: | Type | Description | | --------- | ------------------------------- | | `ndarray` | One prediction for each sample. | ### `GenericClassifier` Bases: `ModelAccessMixin`, `BaseQuantumClassifier` Classifier compatible with scikit-learn that wraps any `torch.nn.Module`. You supply a complete model whose `forward(X)` accepts a float32 tensor of shape `(batch, in_features)` and returns logits of shape `(batch, n_classes)`. For sequence models (for example a long short-term memory network), set `sequence_input=True`. The wrapper then passes a 3-D tensor of shape `(batch, seq_len, in_features)` as it is, instead of flattening it. The wrapper trains with `CrossEntropyLoss` and encodes the labels automatically. Parameters: | Name | Type | Description | Default | | ------------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `model` | `Module` | Any nn.Module. It must include its own classification head, which gives n_classes logits. | *required* | | `in_features` | \`int | None\` | The expected number of features at each time step (the last dimension of the input). When it is given, fit raises an error if X.shape[-1] does not match. Pass None to skip the check. | | `sequence_input` | `bool` | If True, the estimator tells QuantumTimeSeriesClassifier that it expects a 3-D tensor of shape (batch, seq_len, in_features), so the windowed data must not be flattened. Default: False. | `False` | | `n_classes` | \`int | None\` | The number of output classes. If None, it is inferred from the training labels. If it is given, it must equal the number of distinct labels in y. | | `lr` | `float` | The learning rate of the optimiser. | `0.01` | | `optimizer` | `str` | The name of the optimiser: "adam", "adamw", "rmsprop" or "sgd". | `'adam'` | | `optimizer_kwargs` | \`dict | None\` | Extra keyword arguments passed to the constructor of the optimiser. | | `epochs` | `int` | The number of full passes over the training data. | `50` | | `batch_size` | `int` | The size of each mini-batch. | `32` | | `show_progress` | `bool` | If True, a tqdm progress bar is shown during training. | `True` | | `random_state` | \`int | None\` | The seed, applied before the first training step. | Attributes: | Name | Type | Description | | -------------------- | ---- | -------------------------------------------------------------------------------------------- | | `train_losses_` | | The average cross-entropy loss of each epoch, set by fit(). | | `validation_losses_` | | The validation cross-entropy loss of each epoch, set by fit() when validation data is given. | Example ```python import numpy as np from torch import nn from eigenkit.torch.estimators import GenericClassifier from eigenkit.torch.layers import Basic rng = np.random.default_rng(0) X_train, X_test = rng.normal(size=(30, 4)), rng.normal(size=(5, 4)) y_train = np.arange(30) % 3 # A plain PyTorch network, with a head for 3 classes. net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 3)) clf = GenericClassifier(model=net, in_features=4, epochs=20) clf.fit(X_train, y_train) labels = clf.predict(X_test) probs = clf.predict_proba(X_test) # A quantum layer with a classification head. q = Basic(n_qubits=4, depth=2) hybrid = nn.Sequential(q, nn.Linear(4, 3)) clf2 = GenericClassifier(model=hybrid, in_features=4) ``` #### `accepts_sequence_input` Whether the model expects a 3-D input `(batch, seq_len, features)`. #### `fit(X, y, X_val=None, y_val=None)` Fit the model on training data. Parameters: | Name | Type | Description | Default | | ------- | --------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------- | | `X` | `ndarray` | The samples, of shape (n_samples, in_features) for flat models, or (n_samples, seq_len, in_features) when sequence_input=True. | *required* | | `y` | `ndarray` | The class labels, of shape (n_samples,). | *required* | | `X_val` | \`ndarray | None\` | Optional validation samples, used to compute the validation loss of each epoch. | | `y_val` | \`ndarray | None\` | Optional validation labels, used to compute the validation loss of each epoch. | Returns: | Type | Description | | --------------------- | --------------------------------------------- | | `'GenericClassifier'` | This estimator, so that calls can be chained. | Raises: | Type | Description | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ValueError` | If X.shape[-1] or X_val.shape[-1] differs from in_features, if n_classes differs from the number of distinct labels, or if the model does not give one logit for each class. | #### `load(path, *, model=None, provider=None)` Load a fitted `GenericClassifier` from a `.eigenkit` bundle. The architecture of the model cannot be rebuilt automatically, so you must give it. The saved weights and the label encoder are restored automatically. Parameters: | Name | Type | Description | Default | | ---------- | -------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `path` | \`str | Path\` | The path of the .eigenkit bundle. | | `model` | \`Module | None\` | An nn.Module with the same architecture as the one used in training. Its weights are not changed: the saved weights go into a deep copy, which is model\_ of the loaded estimator. | | `provider` | \`ProviderSpec | None\` | It is ignored. It is kept so that the signature matches the other estimators. | Returns: | Type | Description | | --------------------- | --------------------- | | `'GenericClassifier'` | The loaded estimator. | Raises: | Type | Description | | -------------------- | -------------------------------------- | | `SerializationError` | If model is not given. | | `TypeError` | If the bundle holds a different class. | Example ```python from torch import nn from eigenkit.torch.estimators import GenericClassifier net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 3)) clf = GenericClassifier.load("checkpoint.eigenkit", model=net) ``` #### `predict(X)` Predict the class labels of `X`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | Returns: | Type | Description | | --------- | ------------------------------- | | `ndarray` | One prediction for each sample. | #### `predict_proba(X)` Predict the probability of each class, with a softmax over the logits. It returns a float32 array of shape `(n_samples, n_classes)`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | Returns: | Type | Description | | --------- | ----------------------------------------------------------------------------------------------- | | `ndarray` | The probabilities, of shape (n_samples, n_classes), with the columns in the order of classes\_. | ### `GenericRegressor` Bases: `ModelAccessMixin`, `BaseQuantumRegressor` Regressor compatible with scikit-learn that wraps any `torch.nn.Module`. You supply a complete model whose `forward(X)` accepts a float32 tensor of shape `(batch, in_features)` and returns a tensor of shape `(batch, 1)` or `(batch,)`. For sequence models (for example a long short-term memory network), set `sequence_input=True`. The wrapper then passes a 3-D tensor of shape `(batch, seq_len, in_features)` as it is, instead of flattening it. The wrapper trains the model with the standard `train_loop()` of the library, and returns the predictions as a float32 NumPy array. Parameters: | Name | Type | Description | Default | | ------------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `model` | `Module` | Any nn.Module. It must include its own output head. | *required* | | `in_features` | \`int | None\` | The expected number of features at each time step (the last dimension of the input). When it is given, fit raises an error if X.shape[-1] does not match. Pass None to skip the check. | | `sequence_input` | `bool` | If True, the estimator tells QuantumTimeSeriesRegressor that it expects a 3-D tensor of shape (batch, seq_len, in_features), so the windowed data must not be flattened. Default: False. | `False` | | `loss_func` | \`str | Callable\` | The loss: "mse", "mae", "huber", or any callable (output, target) -> scalar_tensor. | | `huber_delta` | `float` | The threshold of the Huber loss. It is ignored for the other losses. | `1.0` | | `huber_reduction` | `str` | The reduction of the Huber loss: "mean" or "sum". | `'mean'` | | `lr` | `float` | The learning rate of the optimiser. | `0.01` | | `optimizer` | `str` | The name of the optimiser: "adam", "adamw", "rmsprop" or "sgd". | `'adam'` | | `optimizer_kwargs` | \`dict | None\` | Extra keyword arguments passed to the constructor of the optimiser. | | `epochs` | `int` | The number of full passes over the training data. | `50` | | `batch_size` | `int` | The size of each mini-batch. | `32` | | `show_progress` | `bool` | If True, a tqdm progress bar is shown during training. | `True` | | `random_state` | \`int | None\` | The seed, applied before the first training step. | Attributes: | Name | Type | Description | | -------------------- | ---- | ------------------------------------------------------------------------------------------ | | `train_losses_` | | The list of the average loss of each epoch, set by fit(). | | `validation_losses_` | | The list of the validation loss of each epoch, set by fit() when validation data is given. | Example ```python import numpy as np from torch import nn from eigenkit.torch.estimators import GenericRegressor from eigenkit.torch.layers import Basic rng = np.random.default_rng(0) X_train, X_test = rng.normal(size=(30, 8)), rng.normal(size=(5, 8)) y_train = X_train.sum(axis=1) # A plain PyTorch network. net = nn.Sequential(nn.Linear(8, 16), nn.ReLU(), nn.Linear(16, 1)) reg = GenericRegressor(model=net, in_features=8, epochs=30) reg.fit(X_train, y_train) preds = reg.predict(X_test) # A quantum layer with a classical head. q = Basic(n_qubits=4, depth=2) hybrid = nn.Sequential(q, nn.Linear(4, 1)) reg2 = GenericRegressor(model=hybrid, in_features=4) ``` #### `accepts_sequence_input` Whether the model expects a 3-D input `(batch, seq_len, features)`. #### `fit(X, y, X_val=None, y_val=None)` Fit the model on training data. Parameters: | Name | Type | Description | Default | | ------- | --------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------- | | `X` | `ndarray` | The samples, of shape (n_samples, in_features) for flat models, or (n_samples, seq_len, in_features) when sequence_input=True. | *required* | | `y` | `ndarray` | The real-valued targets, of shape (n_samples,). | *required* | | `X_val` | \`ndarray | None\` | Optional validation samples, used to compute the validation loss of each epoch. | | `y_val` | \`ndarray | None\` | Optional validation targets, used to compute the validation loss of each epoch. | Returns: | Type | Description | | -------------------- | --------------------------------------------- | | `'GenericRegressor'` | This estimator, so that calls can be chained. | Raises: | Type | Description | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------- | | `ValueError` | If X.shape[-1] or X_val.shape[-1] differs from in_features, or if the output of the model does not have shape (batch,) or (batch, 1). | #### `load(path, *, model=None, provider=None)` Load a fitted `GenericRegressor` from a `.eigenkit` bundle. The architecture of the model cannot be rebuilt automatically, so you must give it. The saved weights are loaded into a copy of it. Parameters: | Name | Type | Description | Default | | ---------- | -------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `path` | \`str | Path\` | The path of the .eigenkit bundle. | | `model` | \`Module | None\` | An nn.Module with the same architecture as the one used in training. Its weights are not changed: the saved weights go into a deep copy, which is model\_ of the loaded estimator. | | `provider` | \`ProviderSpec | None\` | It is ignored. It is kept so that the signature matches the other estimators. | Returns: | Type | Description | | -------------------- | --------------------- | | `'GenericRegressor'` | The loaded estimator. | Raises: | Type | Description | | -------------------- | -------------------------------------- | | `SerializationError` | If model is not given. | | `TypeError` | If the bundle holds a different class. | Example ```python from torch import nn from eigenkit.torch.estimators import GenericRegressor net = nn.Sequential(nn.Linear(8, 16), nn.ReLU(), nn.Linear(16, 1)) reg = GenericRegressor.load("checkpoint.eigenkit", model=net) ``` #### `predict(X)` Predict the continuous targets of `X`. It returns a float32 array. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | Returns: | Type | Description | | --------- | ------------------------------- | | `ndarray` | One prediction for each sample. | ### `HQNC` Bases: `ModelAccessMixin`, `BaseQuantumClassifier` Hybrid quantum neural network classifier (HQNC). The model is `MinMaxNorm -> quantum_layer -> nn.Linear(q_out, n_classes)`. If the number of features of the data differs from the resolved input size of the quantum layer (derived from `in_quantum`, `n_qubits` and `layer_type`), an `nn.Linear` adapter is added in front automatically. Parameters: | Name | Type | Description | Default | | ------------------ | ---------------------------- | ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `in_features` | `int` | The number of input features. | *required* | | `in_quantum` | \`int | None\` | The number of inputs that the quantum layer expects. It defaults to in_features. | | `n_qubits` | \`int | None\` | The number of qubits in each circuit. It defaults to in_quantum. | | `depth` | `int` | The number of repetitions of the variational layer. | `2` | | `n_classes` | \`int | None\` | The number of output classes. If None, it is inferred from the training labels. | | `layer_type` | `str` | The quantum layer. It is one of "Basic", "Reupload" and "Ensemble". | `'Basic'` | | `enc_rotation` | \`Literal['RX', 'RY', 'RZ'] | None\` | The encoding rotation: "RX", "RY", "RZ" or None. | | `var_rotation` | \`Literal['RX', 'RY', 'RZ'] | None\` | The variational rotation of the "basic" ansatz: "RX", "RY", "RZ" or None. | | `variational_type` | \`Literal['basic', 'strong'] | None\` | The type of variational block: "basic", "strong" or None. | | `basis` | \`Literal['X', 'Y', 'Z'] | None\` | The measurement basis: "X", "Y", "Z" or None. For these four arguments, None means "use the default of the layer named by layer_type", which is where each of these values is decided. | | `provider` | `ProviderSpec` | The backend. It takes the same options as Circuit(provider=...). | `'numpy'` | | `lr` | `float` | The learning rate of the optimiser. | `0.01` | | `optimizer` | `str` | The name of the optimiser. The supported names are "adam", "adamw", "rmsprop" and "sgd". | `'adam'` | | `optimizer_kwargs` | \`dict | None\` | Extra keyword arguments passed to the optimiser. | | `epochs` | `int` | The number of training epochs. | `50` | | `batch_size` | `int` | The size of each mini-batch. | `32` | | `show_progress` | `bool` | If True, a tqdm progress bar is shown. | `True` | | `random_state` | \`int | None\` | The seed for the initialisation of the weights. | Attributes: | Name | Type | Description | | -------------------- | ---- | -------------------------------------------------------------------------------------------- | | `train_losses_` | | The average cross-entropy loss of each epoch, set by fit(). | | `validation_losses_` | | The validation cross-entropy loss of each epoch, set by fit() when validation data is given. | Example ```python import numpy as np from eigenkit.torch.estimators import HQNC rng = np.random.default_rng(0) X_train, X_test = rng.normal(size=(30, 4)), rng.normal(size=(5, 4)) y_train = np.arange(30) % 3 model = HQNC(in_features=4, depth=2, n_classes=3) model.fit(X_train, y_train) predictions = model.predict(X_test) ``` #### `__init__(in_features, in_quantum=None, n_qubits=None, depth=2, n_classes=None, layer_type='Basic', enc_rotation=None, var_rotation=None, variational_type=None, basis=None, provider='numpy', lr=0.01, optimizer='adam', optimizer_kwargs=None, epochs=50, batch_size=32, show_progress=True, random_state=42)` Initialise a hybrid quantum neural network classifier (HQNC). Parameters: | Name | Type | Description | Default | | ------------------ | ---------------------------- | ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `in_features` | `int` | The number of input features that fit() expects. | *required* | | `in_quantum` | \`int | None\` | The number of inputs that the quantum layer expects. It defaults to in_features. | | `n_qubits` | \`int | None\` | The number of qubits in each circuit. It defaults to in_quantum. | | `depth` | `int` | The number of repetitions of the variational layer. | `2` | | `n_classes` | \`int | None\` | The number of output classes. If None, it is inferred from the labels passed to fit(). | | `layer_type` | `str` | The family of the quantum layer. It is one of "Basic", "Reupload" and "Ensemble". | `'Basic'` | | `enc_rotation` | \`Literal['RX', 'RY', 'RZ'] | None\` | The encoding rotation: "RX", "RY", "RZ" or None. | | `var_rotation` | \`Literal['RX', 'RY', 'RZ'] | None\` | The variational rotation of the basic ansatz: "RX", "RY", "RZ" or None. | | `variational_type` | \`Literal['basic', 'strong'] | None\` | The type of variational block: "basic", "strong" or None. | | `basis` | \`Literal['X', 'Y', 'Z'] | None\` | The measurement basis: "X", "Y", "Z" or None. For these four arguments, None means "use the default of the layer named by layer_type", which is where each of these values is decided. | | `provider` | `ProviderSpec` | The backend. It takes the same options as Circuit(provider=...). | `'numpy'` | | `lr` | `float` | The learning rate of the optimiser. | `0.01` | | `optimizer` | `str` | The name of the optimiser. The supported names are "adam", "adamw", "rmsprop" and "sgd". | `'adam'` | | `optimizer_kwargs` | \`dict | None\` | Extra keyword arguments passed to the optimiser. | | `epochs` | `int` | The number of training epochs. | `50` | | `batch_size` | `int` | The size of each mini-batch. | `32` | | `show_progress` | `bool` | If True, a tqdm progress bar is shown during training. | `True` | | `random_state` | \`int | None\` | The seed for the initialisation of the weights. | #### `fit(X, y, X_val=None, y_val=None)` Fit the classifier on training data. Parameters: | Name | Type | Description | Default | | ------- | --------- | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | | `X` | `ndarray` | The samples, of shape (n_samples, in_features). | *required* | | `y` | `ndarray` | The class labels, of shape (n_samples,). They can be of any type that scikit-learn's LabelEncoder accepts. | *required* | | `X_val` | \`ndarray | None\` | Optional validation samples, used to compute the validation loss of each epoch. | | `y_val` | \`ndarray | None\` | Optional validation labels, used to compute the validation loss of each epoch. | Returns: | Type | Description | | ------ | --------------------------------------------- | | `HQNC` | This estimator, so that calls can be chained. | Raises: | Type | Description | | ------------ | --------------------------------------- | | `ValueError` | If X does not have in_features columns. | #### `predict(X)` Predict the class labels of `X`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | Returns: | Type | Description | | --------- | ------------------------------- | | `ndarray` | One prediction for each sample. | #### `predict_proba(X)` Predict the probability of each class, with a softmax over the logits. It returns a float32 array of shape `(n_samples, n_classes)`. Each row sums to `1`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | Returns: | Type | Description | | --------- | ----------------------------------------------------------------------------------------------- | | `ndarray` | The probabilities, of shape (n_samples, n_classes), with the columns in the order of classes\_. | ### `HQNR` Bases: `ModelAccessMixin`, `BaseQuantumRegressor` Hybrid quantum neural network regressor (HQNR). It is the regression counterpart of `HQNC`. The model is `MinMaxNorm -> quantum_layer -> nn.Linear(q_out, 1)`. If the number of features of the data differs from the resolved input size of the quantum layer, an `nn.Linear` adapter is added in front automatically. Parameters: | Name | Type | Description | Default | | ------------------ | ---------------------------- | ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `in_features` | `int` | The number of input features. | *required* | | `in_quantum` | \`int | None\` | The number of inputs that the quantum layer expects. It defaults to in_features. | | `n_qubits` | \`int | None\` | The number of qubits in each circuit. It defaults to in_quantum. | | `depth` | `int` | The number of repetitions of the variational layer. | `2` | | `layer_type` | `str` | The quantum layer. It is one of "Basic", "Reupload" and "Ensemble". | `'Basic'` | | `enc_rotation` | \`Literal['RX', 'RY', 'RZ'] | None\` | The encoding rotation: "RX", "RY", "RZ" or None. | | `var_rotation` | \`Literal['RX', 'RY', 'RZ'] | None\` | The variational rotation of the "basic" ansatz: "RX", "RY", "RZ" or None. | | `variational_type` | \`Literal['basic', 'strong'] | None\` | The type of variational block: "basic", "strong" or None. | | `basis` | \`Literal['X', 'Y', 'Z'] | None\` | The measurement basis: "X", "Y", "Z" or None. For these four arguments, None means "use the default of the layer named by layer_type", which is where each of these values is decided. | | `provider` | `ProviderSpec` | The backend. It takes the same options as Circuit(provider=...). | `'numpy'` | | `lr` | `float` | The learning rate of the optimiser. | `0.01` | | `optimizer` | `str` | The name of the optimiser. The supported names are "adam", "adamw", "rmsprop" and "sgd". | `'adam'` | | `optimizer_kwargs` | \`dict | None\` | Extra keyword arguments passed to the optimiser. | | `epochs` | `int` | The number of training epochs. | `50` | | `batch_size` | `int` | The size of each mini-batch. | `32` | | `show_progress` | `bool` | If True, a tqdm progress bar is shown. | `True` | | `loss_func` | `str` | The regression loss: "mse", "mae" or "huber". | `'mse'` | | `huber_delta` | `float` | The threshold of the Huber loss. | `1.0` | | `huber_reduction` | `str` | The reduction of the Huber loss: "mean" or "sum". | `'mean'` | | `random_state` | \`int | None\` | The seed. | Attributes: | Name | Type | Description | | -------------------- | ---- | ------------------------------------------------------------------------------ | | `train_losses_` | | The training loss of each epoch, set by fit(). | | `validation_losses_` | | The validation loss of each epoch, set by fit() when validation data is given. | Example ```python import numpy as np from eigenkit.torch.estimators import HQNR rng = np.random.default_rng(0) X_train, X_test = rng.normal(size=(30, 8)), rng.normal(size=(5, 8)) y_train = X_train.sum(axis=1) model = HQNR(in_features=8, depth=2) model.fit(X_train, y_train) predictions = model.predict(X_test) ``` #### `fit(X, y, X_val=None, y_val=None)` Fit the regressor on training data. Parameters: | Name | Type | Description | Default | | ------- | --------- | ----------------------------------------------- | ------------------------------------------------------------------------------- | | `X` | `ndarray` | The samples, of shape (n_samples, in_features). | *required* | | `y` | `ndarray` | The real-valued targets, of shape (n_samples,). | *required* | | `X_val` | \`ndarray | None\` | Optional validation samples, used to compute the validation loss of each epoch. | | `y_val` | \`ndarray | None\` | Optional validation targets, used to compute the validation loss of each epoch. | Returns: | Type | Description | | ------ | --------------------------------------------- | | `HQNR` | This estimator, so that calls can be chained. | Raises: | Type | Description | | ------------ | --------------------------------------- | | `ValueError` | If X does not have in_features columns. | #### `predict(X)` Predict the continuous targets of `X`. It returns a float32 array. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | Returns: | Type | Description | | --------- | ------------------------------- | | `ndarray` | One prediction for each sample. | ### `QuantumTimeSeriesClassifier` Bases: `BaseQuantumClassifier` Supervised wrapper for time-series classification. It builds sliding windows over the rows of `X` (one row for each time step). Each window has shape `(in_size, n_features)`. Stacked along the batch axis, they form an array of shape `(n_windows, in_size, n_features)`, ready for sequence models such as `RecurrentHQNC`. When `autoregressive=True`, an extra last channel holds the target value of each timestep, so each step has `n_features + 1` values. For a window that starts at row `i`, the classification target is `y[i + in_size - 1 + shift]`. Parameters: | Name | Type | Description | Default | | ---------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | | `estimator` | `Any` | The inner estimator. It must have fit(X, y) and predict(X). | *required* | | `in_size` | \`int | None\` | The length of the window, in time steps. If None, it is treated as 1. | | `shift` | `int` | How many steps after the last timestep of the window the target is. It must be zero or more. shift=0 predicts the target at the last timestep. | `1` | | `autoregressive` | `bool` | If True, a channel with y[i + k] for k = 0, ..., in_size - 1 is added to each window. With shift=0, this leaks the target into the inputs, and a UserWarning is raised. When autoregressive=True, predict() and predict_proba() require a y_history argument. | `False` | Attributes: | Name | Type | Description | | --------------- | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `classes_` | | The distinct class labels seen by fit(). | | `train_losses_` | | The training loss of each epoch, copied from the inner estimator if it has train_losses\_ (HQNC, RecurrentHQNC), and an empty list otherwise. It is set by fit(). | Example ```python import numpy as np from eigenkit.torch.estimators import ( QuantumTimeSeriesClassifier, RecurrentHQNC, ) rng = np.random.default_rng(0) X_train, X_test = rng.normal(size=(40, 4)), rng.normal(size=(15, 4)) y_train = np.arange(40) % 2 inner = RecurrentHQNC(seq_feature_dim=4, rnn_hidden_dim=4, depth=2) model = QuantumTimeSeriesClassifier(estimator=inner, in_size=10, shift=1) model.fit(X_train, y_train) predictions = model.predict(X_test) ``` #### `output` The number of classes. After `fit()`, it is the number of classes seen. Before, it is the `n_classes` of the inner estimator, or None if it has none. #### `fit(X, y)` Fit the inner classifier on the windows and their labels. Parameters: | Name | Type | Description | Default | | ---- | --------- | --------------------------------------------------------------- | ---------- | | `X` | `ndarray` | The features in time order, of shape (n_timesteps, n_features). | *required* | | `y` | `ndarray` | The class labels, of shape (n_timesteps,) or (n_timesteps, 1). | *required* | Returns: | Type | Description | | ------------------------------- | --------------------------------------------- | | `'QuantumTimeSeriesClassifier'` | This estimator, so that calls can be chained. | #### `load(path, *, provider=None, model=None)` Load a fitted time-series classifier wrapper from a `.eigenkit` bundle. Parameters: | Name | Type | Description | Default | | ---------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | | `path` | \`str | Path\` | The path of the .eigenkit bundle. | | `provider` | \`ProviderSpec | None\` | An optional provider that replaces the saved one. It is passed to the inner estimator. | | `model` | `Any` | The nn.Module architecture, required when the inner estimator is GenericClassifier. Pass the same architecture as the one used in training. | `None` | Returns: | Type | Description | | ------------------------------- | ------------------- | | `'QuantumTimeSeriesClassifier'` | The loaded wrapper. | Raises: | Type | Description | | -------------------- | ------------------------------------------------------- | | `TypeError` | If the bundle holds a different class. | | `SerializationError` | If the inner estimator needs model and it is not given. | #### `predict(X, y_history=None)` Predict the class labels of the windows of `X`. Parameters: | Name | Type | Description | Default | | ----------- | --------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `X` | `ndarray` | The features in time order, of shape (n_timesteps, n_features). | *required* | | `y_history` | \`ndarray | None\` | The past class labels, aligned with the rows of X. They fill the autoregressive channel of each window. It is required when autoregressive=True, and it must have at least n_timesteps - shift elements (that is, n_timesteps - in_size - shift + 1 + in_size - 1). | Returns: | Type | Description | | --------- | -------------------------------------------------------------------- | | `ndarray` | The predicted labels, of shape (n_timesteps - in_size - shift + 1,). | #### `predict_proba(X, y_history=None)` Return the estimated class probabilities of the windows of `X`. Parameters: | Name | Type | Description | Default | | ----------- | --------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------ | | `X` | `ndarray` | The features in time order, of shape (n_timesteps, n_features). | *required* | | `y_history` | \`ndarray | None\` | The past class labels. It is required when autoregressive=True. See predict(). | Returns: | Type | Description | | --------- | ------------------------------------------------ | | `ndarray` | A float32 array of shape (n_windows, n_classes). | Raises: | Type | Description | | --------------------- | -------------------------------------------- | | `NotImplementedError` | If the inner estimator has no predict_proba. | #### `score(X, y, *, return_dict=False)` ```python score( X: np.ndarray, y: np.ndarray, *, return_dict: Literal[False] = False, ) -> float ``` ```python score( X: np.ndarray, y: np.ndarray, *, return_dict: Literal[True], ) -> dict[str, float] ``` Score the estimator on the window targets, aligned as in `predict`. By default, it returns the accuracy, for compatibility with scikit-learn (`cross_val_score`, `GridSearchCV`). Pass `return_dict=True` to get a dict with `accuracy`, `f1` (weighted), `precision` (weighted) and `recall` (weighted). Note When `autoregressive=True`, the true labels `y` are used as the autoregressive history channel (teacher forcing). This is the correct behaviour for held-out evaluation, but it means that `score` cannot be called without the true labels of all the timesteps in `X`. Parameters: | Name | Type | Description | Default | | ------------- | --------- | ---------------------------------------------------------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). The windows are built inside. | *required* | | `y` | `ndarray` | One target for each sample, of shape (n_samples,). | *required* | | `return_dict` | `bool` | If True, every metric is returned in a dict, instead of the single main score. Default: False. | `False` | Returns: | Type | Description | | ------- | ------------------ | | \`float | dict[str, float]\` | ### `QuantumTimeSeriesRegressor` Bases: `BaseQuantumRegressor` Supervised wrapper with a single output, for time-series regression. It builds sliding windows over the rows of `X` (one row for each time step). Each window has shape `(in_size, n_features)`. Stacked along the batch axis, they form an array of shape `(n_windows, in_size, n_features)`, ready for sequence models such as `torch.nn.LSTM`. When `autoregressive=True`, an extra last channel holds the target value of each timestep, so each step has `n_features + 1` values. For a window that starts at row `i`, the regression target is `y[i + in_size - 1 + shift]`. Parameters: | Name | Type | Description | Default | | ---------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | | `estimator` | `Any` | The inner estimator. It must have fit(X, y) and predict(X). | *required* | | `in_size` | \`int | None\` | The length of the window, in time steps. If None, it is treated as 1. | | `shift` | `int` | How many steps after the last timestep of the window the target is. It must be zero or more. shift=0 predicts the target at the last timestep. | `1` | | `autoregressive` | `bool` | If True, a channel with y[i + k] for k = 0, ..., in_size - 1 is added to each window. With shift=0, this leaks the target into the inputs, and a UserWarning is raised. | `False` | Attributes: | Name | Type | Description | | --------------- | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `train_losses_` | | The training loss of each epoch, copied from the inner estimator if it has train_losses\_ (HQNR, RecurrentHQNR), and an empty list otherwise. It is set by fit(). | Example ```python import numpy as np from eigenkit.torch.estimators import QuantumTimeSeriesRegressor, RecurrentHQNR rng = np.random.default_rng(0) X_train, X_test = rng.normal(size=(40, 3)), rng.normal(size=(15, 3)) y_train = np.sin(np.arange(40) / 4) # The inner estimator should accept X of shape (batch, in_size, n_features). inner = RecurrentHQNR(seq_feature_dim=3, rnn_hidden_dim=4, epochs=5) model = QuantumTimeSeriesRegressor(estimator=inner, in_size=10, shift=1) model.fit(X_train, y_train) predictions = model.predict(X_test) ``` #### `output` The number of outputs, which is always `1`. #### `fit(X, y)` Fit the inner estimator on the windows and their targets. Parameters: | Name | Type | Description | Default | | ---- | --------- | --------------------------------------------------------------- | ---------- | | `X` | `ndarray` | The features in time order, of shape (n_timesteps, n_features). | *required* | | `y` | `ndarray` | The targets, of shape (n_timesteps,) or (n_timesteps, 1). | *required* | Returns: | Type | Description | | ------------------------------ | --------------------------------------------- | | `'QuantumTimeSeriesRegressor'` | This estimator, so that calls can be chained. | #### `load(path, *, provider=None, model=None)` Load a fitted time-series wrapper from a `.eigenkit` bundle. Parameters: | Name | Type | Description | Default | | ---------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------- | | `path` | \`str | Path\` | The path of the .eigenkit bundle. | | `provider` | \`ProviderSpec | None\` | An optional provider that replaces the saved one. It is passed to the inner estimator. | | `model` | `Any` | The nn.Module architecture, required when the inner estimator is GenericRegressor. Pass the same architecture as the one used in training. | `None` | Returns: | Type | Description | | ------------------------------ | ------------------- | | `'QuantumTimeSeriesRegressor'` | The loaded wrapper. | Raises: | Type | Description | | -------------------- | ------------------------------------------------------- | | `TypeError` | If the bundle holds a different class. | | `SerializationError` | If the inner estimator needs model and it is not given. | #### `predict(X, y_history=None)` Predict the regression targets of the windows of `X`. Parameters: | Name | Type | Description | Default | | ----------- | --------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | `X` | `ndarray` | The features in time order, of shape (n_timesteps, n_features). | *required* | | `y_history` | \`ndarray | None\` | The past targets, aligned with the rows of X. They fill the autoregressive channel of each window. It is required when autoregressive=True. | Returns: | Type | Description | | --------- | --------------------------------------------------------------- | | `ndarray` | The predictions, of shape (n_timesteps - in_size - shift + 1,). | #### `score(X, y, *, return_dict=False)` ```python score( X: np.ndarray, y: np.ndarray, *, return_dict: Literal[False] = False, ) -> float ``` ```python score( X: np.ndarray, y: np.ndarray, *, return_dict: Literal[True], ) -> dict[str, float] ``` Score the estimator on the window targets, aligned as in `predict`. By default, it returns `R²`, for compatibility with scikit-learn (`cross_val_score`, `GridSearchCV`). Pass `return_dict=True` to get a dict with `r2`, `mse`, `mae`, `rmse` and `mape`. Parameters: | Name | Type | Description | Default | | ------------- | --------- | ---------------------------------------------------------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). The windows are built inside. | *required* | | `y` | `ndarray` | One target for each sample, of shape (n_samples,). | *required* | | `return_dict` | `bool` | If True, every metric is returned in a dict, instead of the single main score. Default: False. | `False` | Returns: | Type | Description | | ------- | ------------------ | | \`float | dict[str, float]\` | ### `RecurrentHQNC` Bases: `ModelAccessMixin`, `BaseQuantumClassifier` Hybrid classifier: a recurrent encoder, a quantum layer and a linear class head. The recurrent encoder is a recurrent neural network (RNN). The estimator expects 3-D inputs of shape `(n_samples, seq_len, seq_feature_dim)`. By default, only the last timestep of the RNN output goes to the quantum layer. Set `rnn_return_last_timestep=False`, together with `sequence_length`, to use the full RNN output (flattened) instead. The quantum part is the same as in HQNC: `MinMaxNorm`, the quantum layer and an `nn.Linear` head. Parameters: | Name | Type | Description | Default | | -------------------------- | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `seq_feature_dim` | `int` | The number of features at each time step (the last axis of X). | *required* | | `rnn_type` | `Literal['rnn', 'gru', 'lstm']` | The type of recurrent layer: "rnn", "gru" (gated recurrent unit) or "lstm" (long short-term memory). | `'gru'` | | `rnn_hidden_dim` | `int` | The hidden size in each direction. | `32` | | `rnn_num_layers` | `int` | The number of stacked RNN layers. | `1` | | `rnn_dropout` | `float` | The dropout between the RNN layers. It applies only if rnn_num_layers > 1. | `0.0` | | `rnn_bidirectional` | `bool` | If True, the RNN is bidirectional, and its output size doubles. | `False` | | `rnn_return_last_timestep` | `bool` | If True, only the last output step is kept. Otherwise, the full sequence is flattened, which requires sequence_length. | `True` | | `sequence_length` | \`int | None\` | The number of time steps in each sample. It is required when rnn_return_last_timestep=False, and it must equal X.shape[1] in fit. | | `in_quantum` | \`int | None\` | The number of inputs of the quantum layer. It defaults to the size of the RNN output that reaches the quantum layer: rnn_hidden_dim, doubled when the RNN is bidirectional, and multiplied by sequence_length when rnn_return_last_timestep=False. | | `n_qubits` | \`int | None\` | The number of qubits in each circuit. It defaults to in_quantum. | | `depth` | `int` | The number of repetitions of the variational layer. | `2` | | `n_classes` | \`int | None\` | The number of output classes. If None, it is inferred from the labels passed to fit(). If it is given, it must equal the number of distinct labels in y. | | `layer_type` | `str` | The quantum layer. It is one of "Basic", "Reupload" and "Ensemble". | `'Basic'` | | `enc_rotation` | \`Literal['RX', 'RY', 'RZ'] | None\` | The encoding rotation: "RX", "RY", "RZ" or None. | | `var_rotation` | \`Literal['RX', 'RY', 'RZ'] | None\` | The variational rotation of the basic ansatz: "RX", "RY", "RZ" or None. | | `variational_type` | \`Literal['basic', 'strong'] | None\` | The type of variational block: "basic", "strong" or None. | | `basis` | \`Literal['X', 'Y', 'Z'] | None\` | The measurement basis: "X", "Y", "Z" or None. For these four arguments, None means "use the default of the layer named by layer_type", which is where each of these values is decided. | | `provider` | `ProviderSpec` | The backend. It takes the same options as Circuit(provider=...). | `'numpy'` | | `lr` | `float` | The learning rate of the optimiser. | `0.01` | | `optimizer` | `str` | The name of the optimiser. The supported names are "adam", "adamw", "rmsprop" and "sgd". | `'adam'` | | `optimizer_kwargs` | \`dict | None\` | Extra keyword arguments passed to the optimiser. | | `epochs` | `int` | The number of training epochs. | `50` | | `batch_size` | `int` | The size of each mini-batch. | `32` | | `show_progress` | `bool` | If True, a tqdm progress bar is shown during training. | `True` | | `random_state` | \`int | None\` | The seed for the initialisation of the weights. | Attributes: | Name | Type | Description | | -------------------- | ---- | -------------------------------------------------------------------------------------------- | | `train_losses_` | | The cross-entropy loss of each epoch, set by fit(). | | `validation_losses_` | | The validation cross-entropy loss of each epoch, set by fit() when validation data is given. | #### `fit(X, y, X_val=None, y_val=None)` Fit the estimator on `X` and `y`. Parameters: | Name | Type | Description | Default | | ------- | --------- | -------------------------------------------------------------- | --------------------------------------------------------------------- | | `X` | `ndarray` | The sequences, of shape (n_samples, seq_len, seq_feature_dim). | *required* | | `y` | `ndarray` | One label for each sequence, of shape (n_samples,). | *required* | | `X_val` | \`ndarray | None\` | Optional validation sequences, of the same shape as X. Default: None. | | `y_val` | \`ndarray | None\` | The labels of X_val. Default: None. | Returns: | Type | Description | | --------------- | --------------------------------------------- | | `RecurrentHQNC` | This estimator, so that calls can be chained. | #### `predict(X)` Predict a label for every sample in `X`. Parameters: | Name | Type | Description | Default | | ---- | --------- | -------------------------------------------------------------- | ---------- | | `X` | `ndarray` | The sequences, of shape (n_samples, seq_len, seq_feature_dim). | *required* | Returns: | Type | Description | | --------- | -------------------------------------------------------- | | `ndarray` | One prediction for each sequence, of shape (n_samples,). | #### `predict_proba(X)` Predict the class probabilities of every sample in `X`. Parameters: | Name | Type | Description | Default | | ---- | --------- | -------------------------------------------------------------- | ---------- | | `X` | `ndarray` | The sequences, of shape (n_samples, seq_len, seq_feature_dim). | *required* | Returns: | Type | Description | | --------- | ----------------------------------------------------------------------------------------------- | | `ndarray` | The probabilities, of shape (n_samples, n_classes), with the columns in the order of classes\_. | ### `RecurrentHQNR` Bases: `ModelAccessMixin`, `BaseQuantumRegressor` Hybrid regressor: a recurrent encoder, a quantum layer and a linear head. The recurrent encoder is a recurrent neural network (RNN). The estimator expects 3-D inputs of shape `(n_samples, seq_len, seq_feature_dim)`. By default, only the last timestep of the RNN output goes to the quantum layer. Set `rnn_return_last_timestep=False`, together with `sequence_length`, to use the full RNN output (flattened) instead. The quantum part is the same as in HQNR: `MinMaxNorm`, the quantum layer and an `nn.Linear` head. Parameters: | Name | Type | Description | Default | | -------------------------- | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `seq_feature_dim` | `int` | The number of features at each time step (the last axis of X). | *required* | | `rnn_type` | `Literal['rnn', 'gru', 'lstm']` | The type of recurrent layer: "rnn", "gru" (gated recurrent unit) or "lstm" (long short-term memory). | `'gru'` | | `rnn_hidden_dim` | `int` | The hidden size in each direction. | `32` | | `rnn_num_layers` | `int` | The number of stacked RNN layers. | `1` | | `rnn_dropout` | `float` | The dropout between the RNN layers. It applies only if rnn_num_layers > 1. | `0.0` | | `rnn_bidirectional` | `bool` | If True, the RNN is bidirectional, and its output size doubles. | `False` | | `rnn_return_last_timestep` | `bool` | If True, only the last output step is kept. Otherwise, the full sequence is flattened, which requires sequence_length. | `True` | | `sequence_length` | \`int | None\` | The number of time steps in each sample. It is required when rnn_return_last_timestep=False, and it must equal X.shape[1] in fit. | | `in_quantum` | \`int | None\` | The number of inputs of the quantum layer. It defaults to the size of the RNN output that reaches the quantum layer: rnn_hidden_dim, doubled when the RNN is bidirectional, and multiplied by sequence_length when rnn_return_last_timestep=False. | | `n_qubits` | \`int | None\` | The number of qubits in each circuit. It defaults to in_quantum. | | `depth` | `int` | The number of repetitions of the variational layer. | `2` | | `layer_type` | `str` | The quantum layer. It is one of "Basic", "Reupload" and "Ensemble". | `'Basic'` | | `enc_rotation` | \`Literal['RX', 'RY', 'RZ'] | None\` | The encoding rotation: "RX", "RY", "RZ" or None. | | `var_rotation` | \`Literal['RX', 'RY', 'RZ'] | None\` | The variational rotation of the "basic" ansatz: "RX", "RY", "RZ" or None. | | `variational_type` | \`Literal['basic', 'strong'] | None\` | The type of variational block: "basic", "strong" or None. | | `basis` | \`Literal['X', 'Y', 'Z'] | None\` | The measurement basis: "X", "Y", "Z" or None. For these four arguments, None means "use the default of the layer named by layer_type", which is where each of these values is decided. | | `provider` | `ProviderSpec` | The backend. It takes the same options as Circuit(provider=...). | `'numpy'` | | `lr` | `float` | The learning rate of the optimiser. | `0.01` | | `optimizer` | `str` | The name of the optimiser. | `'adam'` | | `optimizer_kwargs` | \`dict | None\` | Extra keyword arguments passed to the optimiser. | | `epochs` | `int` | The number of training epochs. | `50` | | `batch_size` | `int` | The size of each mini-batch. | `32` | | `show_progress` | `bool` | If True, a tqdm progress bar is shown. | `True` | | `loss_func` | `str` | The regression loss: "mse", "mae" or "huber". | `'mse'` | | `huber_delta` | `float` | The threshold of the Huber loss. | `1.0` | | `huber_reduction` | `str` | The reduction of the Huber loss: "mean" or "sum". | `'mean'` | | `random_state` | \`int | None\` | The seed. | Attributes: | Name | Type | Description | | -------------------- | ---- | ------------------------------------------------------------------------------ | | `train_losses_` | | The training loss of each epoch, set by fit(). | | `validation_losses_` | | The validation loss of each epoch, set by fit() when validation data is given. | #### `fit(X, y, X_val=None, y_val=None)` Fit the estimator on `X` and `y`. Parameters: | Name | Type | Description | Default | | ------- | --------- | ---------------------------------------------------------------------- | --------------------------------------------------------------------- | | `X` | `ndarray` | The sequences, of shape (n_samples, seq_len, seq_feature_dim). | *required* | | `y` | `ndarray` | One target for each sequence, of shape (n_samples,) or (n_samples, 1). | *required* | | `X_val` | \`ndarray | None\` | Optional validation sequences, of the same shape as X. Default: None. | | `y_val` | \`ndarray | None\` | The targets of X_val. Default: None. | Returns: | Type | Description | | --------------- | --------------------------------------------- | | `RecurrentHQNR` | This estimator, so that calls can be chained. | #### `predict(X)` Predict a value for every sample in `X`. Parameters: | Name | Type | Description | Default | | ---- | --------- | -------------------------------------------------------------- | ---------- | | `X` | `ndarray` | The sequences, of shape (n_samples, seq_len, seq_feature_dim). | *required* | Returns: | Type | Description | | --------- | --------------------------------------------------------------------------- | | `ndarray` | One prediction for each sequence, as a float32 array of shape (n_samples,). | ### `load_model(path, *, provider=None, model=None)` Load any saved model. The class of the model is read from `metadata.json`. Load only bundles that you trust. A bundle can contain joblib pickles, and unpickling runs code, as with `torch.load` or `joblib.load`. Parameters: | Name | Type | Description | Default | | ---------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------- | | `path` | \`str | Path\` | The path to the .eigenkit bundle. | | `provider` | \`ProviderSpec | None\` | A provider that replaces the saved one. It is passed to the load() method of the estimator. | | `model` | `Any` | The nn.Module architecture, for checkpoints of GenericRegressor and GenericClassifier. It is ignored for the other estimators. | `None` | Returns: | Type | Description | | ---------------------- | ------------------------------------------------------------------- | | `BaseQuantumEstimator` | The loaded estimator, an instance of the class named in the bundle. | ## `eigenkit.torch.interface` The PyTorch interface, which makes the NumPy core differentiable in PyTorch. This module is the PyTorch *adapter* of the interface model (see `eigenkit.core.interfaces`). The core `Circuit` computes the forward values and the jacobians with NumPy. This module wraps them into `torch.autograd.Function` objects, so that the automatic differentiation of PyTorch works through a quantum circuit. When it is imported, the module registers itself as the `"torch"` interface. Nothing here is part of the core, and importing this module needs PyTorch. This is intended: PyTorch is an optional layer on top of the NumPy core. ### `TorchInterface` The `"torch"` differentiation interface. It chooses how to run a call of a core `Circuit`: - When the backend's `gradient_method` is `"native"` and it provides `run_torch`, PyTorch differentiates through the simulator itself. - When the backend computes no gradients (`gradient_method="none"`), the forward values are wrapped in an operation that has no backward pass. - Otherwise, the call goes through a `torch.autograd.Function` whose backward pass asks the core circuit for the jacobians. The core uses the backend's own jacobian when there is one (for example the adjoint method of the NumPy simulator), and the parameter-shift rule otherwise. #### `run_circuit(circuit, weights, inputs)` Run the circuit with PyTorch, so that PyTorch can differentiate the result. This is the PyTorch side of the `Interface` protocol. It uses the backend's own differentiation when there is one, and otherwise it wraps the NumPy path in a `torch.autograd.Function`. Parameters: | Name | Type | Description | Default | | --------- | --------- | -------------------------------------- | ---------- | | `circuit` | `Circuit` | The core circuit to run. | *required* | | `weights` | `Tensor` | The trainable parameters of the layer. | *required* | | `inputs` | `Tensor` | The input features, 1-D or batched. | *required* | Returns: | Name | Type | Description | | -------- | -------- | ------------------------------------------------- | | `values` | `Tensor` | The expectation values, which are differentiable. | ### `batch_call_torch(run_one, weights, inputs)` Loop over the first axis of a batch and keep the PyTorch autograd graph. It is the PyTorch version of `core.provider.batch_call`. It lives in the PyTorch package and not in the core, so that the core never imports PyTorch. The backends that differentiate with PyTorch themselves (PennyLane) use it. Parameters: | Name | Type | Description | Default | | --------- | ---------- | ------------------------------------------ | ---------- | | `run_one` | `Callable` | A function that evaluates a single sample. | *required* | | `weights` | `Tensor` | The parameter vector, 1-D or batched. | *required* | | `inputs` | `Tensor` | The input features, 1-D or batched. | *required* | Returns: | Name | Type | Description | | --------- | -------- | -------------------------------------------------------------------- | | `stacked` | `Tensor` | The results for each sample, stacked. They are still differentiable. | ## `eigenkit.torch.layers` Quantum layers for the standard patterns. - `Basic`: one `Encoding`, one `Variational` block and a measurement. - `Reupload`: standard data reuploading, optionally with a trainable scale. - `Ensemble`: `N` independent circuits, one for each chunk of the input. - `MinMaxNorm`: min-max normalisation of each feature, in the style of `BatchNorm`. ### `Basic` Bases: `Module` One `Encoding`, one `Variational` block and a measurement on each qubit. Parameters: | Name | Type | Description | Default | | ------------------ | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `n_qubits` | `int` | The number of qubits, which is also the number of input features. | *required* | | `depth` | `int` | The number of variational layers. | *required* | | `enc_rotation` | `Literal['RX', 'RY', 'RZ']` | The encoding rotation: "RX", "RY" or "RZ". Default: "RX". | `'RX'` | | `var_rotation` | `Literal['RX', 'RY', 'RZ']` | The variational rotation. It is used only when variational_type="basic". The default, "RY", is off both the "RX" encoding axis and the "Z" measurement axis, and this is what lets the weights change the output. Default: "RY". | `'RY'` | | `variational_type` | `Literal['basic', 'strong']` | "basic", for one rotation and a controlled-NOT (CNOT) ladder in each layer, or "strong", for RZ, RY and RZ rotations and a CNOT ladder in each layer. Default: "basic". | `'basic'` | | `basis` | `Literal['X', 'Y', 'Z']` | The Pauli measurement basis: "X", "Y" or "Z". Default: "Z". | `'Z'` | | `provider` | `ProviderSpec` | The backend that runs the circuit. Default: "numpy". | `'numpy'` | Example ```python import torch from eigenkit import Basic layer = Basic(n_qubits=4, depth=2) out = layer(torch.randn(8, 4)) # shape (8, 4) ``` #### `provider` The backend that the circuit of this layer was compiled for. The constructor takes `provider=` in the same forms as `Circuit` (a registered name, a configuration or a backend object) and turns it into one `Provider`. This property returns it. The registered name is `provider.name`. Returns: | Name | Type | Description | | ---------- | ---------- | ------------------------------------- | | `provider` | `Provider` | The backend that the circuit runs on. | #### `forward(x)` Run the layer. Parameters: | Name | Type | Description | Default | | ---- | -------- | ----------------------------------------------- | ---------- | | `x` | `Tensor` | The input batch, of shape (batch, in_features). | *required* | Returns: | Name | Type | Description | | ----- | -------- | --------------------------------------- | | `out` | `Tensor` | The output of the layer for that batch. | ### `Ensemble` Bases: `Module` An ensemble of independent quantum circuits, one for each chunk of the input. Parameters: | Name | Type | Description | Default | | ------------------ | ---------------------------- | --------------------------------------------------------------------------------------- | ---------- | | `in_features` | `int` | The total number of input features. It must be a multiple of n_qubits. | *required* | | `n_qubits` | `int` | The number of qubits of each circuit. | *required* | | `depth` | `int` | The number of variational layers of each circuit. | *required* | | `enc_rotation` | `Literal['RX', 'RY', 'RZ']` | The encoding rotation: "RX", "RY" or "RZ". Default: "RX". | `'RX'` | | `var_rotation` | `Literal['RX', 'RY', 'RZ']` | The variational rotation. It is used only when variational_type="basic". Default: "RY". | `'RY'` | | `variational_type` | `Literal['basic', 'strong']` | "basic" or "strong". Default: "strong". | `'strong'` | | `basis` | `Literal['X', 'Y', 'Z']` | The Pauli measurement basis: "X", "Y" or "Z". Default: "Z". | `'Z'` | | `provider` | `ProviderSpec` | The backend that runs the circuits. Default: "numpy". | `'numpy'` | Example ```python import torch from eigenkit import Ensemble layer = Ensemble(in_features=20, n_qubits=4, depth=5) out = layer(torch.randn(128, 20)) # shape (128, 20) ``` #### `provider` The backend that the circuits of this layer were compiled for. The constructor takes `provider=` in the same forms as `Circuit` (a registered name, a configuration or a backend object) and turns it into one `Provider`. This property returns it. The registered name is `provider.name`. Returns: | Name | Type | Description | | ---------- | ---------- | ------------------------------------- | | `provider` | `Provider` | The backend that the circuits run on. | #### `forward(x)` Run the layer. Parameters: | Name | Type | Description | Default | | ---- | -------- | ----------------------------------------------- | ---------- | | `x` | `Tensor` | The input batch, of shape (batch, in_features). | *required* | Returns: | Name | Type | Description | | ----- | -------- | --------------------------------------- | | `out` | `Tensor` | The output of the layer for that batch. | ### `MinMaxNorm` Bases: `Module` Min-max normalisation of each feature, with running statistics. It rescales each feature to `[min_val, max_val]`. In training mode, it uses the minimum and maximum of each batch, and it updates an exponential moving average of these statistics. In evaluation mode, it uses the running statistics. A training batch of a single row has no range, so it uses the running statistics too, and if no batch with a range has been seen yet, it returns the row unchanged with a warning. Training and evaluation modes work as in the `BatchNorm` and `LayerNorm` layers of PyTorch, but the output is rescaled to a fixed interval instead of standardised. It is useful to prepare the inputs of quantum encoding rotations, which expect angles in a bounded range such as `[0, π]`. Parameters: | Name | Type | Description | Default | | ------------- | ------- | ----------------------------------------------------------------------------------------------------------- | ---------- | | `feature_dim` | `int` | The number of features in the last dimension of the input. The inputs have shape (batch_size, feature_dim). | *required* | | `min_val` | `float` | The lower bound of the target interval. Default: 0.0. | `0.0` | | `max_val` | `float` | The upper bound of the target interval. Default: π. | `pi` | | `momentum` | `float` | The momentum factor of the exponential moving average of the running statistics. Default: 0.1. | `0.1` | Example ```python import torch from eigenkit import Ensemble, MinMaxNorm norm = MinMaxNorm(feature_dim=16, min_val=0.0, max_val=torch.pi) layer = Ensemble(in_features=16, n_qubits=4, depth=5) out = layer(norm(torch.randn(32, 16))) ``` #### `forward(x)` Run the layer. Parameters: | Name | Type | Description | Default | | ---- | -------- | ----------------------------------------------- | ---------- | | `x` | `Tensor` | The input batch, of shape (batch, feature_dim). | *required* | Returns: | Name | Type | Description | | ----- | -------- | --------------------------------------- | | `out` | `Tensor` | The output of the layer for that batch. | ### `Reupload` Bases: `Module` The data-reuploading layer (Perez-Salinas 2020; Schuld 2021). It supports two patterns: - Standard reuploading (the default). The same input is encoded `depth` times, with new variational weights between the encodings. - Trainable feature scaling (`trainable_scale=True`). Each encoding rotation becomes `R(lambda * x)`, with a trainable `lambda` for each pair of layer and feature. Parameters: | Name | Type | Description | Default | | ------------------ | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------- | | `n_qubits` | `int` | The number of qubits, which is also the number of input features. | *required* | | `depth` | `int` | The number of times the input is encoded. | *required* | | `enc_rotation` | `Literal['RX', 'RY', 'RZ']` | The encoding rotation: "RX", "RY" or "RZ". Default: "RY". | `'RY'` | | `var_rotation` | `Literal['RX', 'RY', 'RZ']` | The variational rotation. It is used only when variational_type="basic". Default: "RY". | `'RY'` | | `variational_type` | `Literal['basic', 'strong']` | "basic" or "strong". Default: "basic". | `'basic'` | | `basis` | `Literal['X', 'Y', 'Z']` | The Pauli measurement basis: "X", "Y" or "Z". Default: "Z". | `'Z'` | | `trainable_scale` | `bool` | If True, each encoding rotation gets a trainable scale for each pair of layer and feature. The CUDA Quantum backend does not support it, because its QuakeValue arithmetic is limited. Default: False. | `False` | | `provider` | `ProviderSpec` | The backend that runs the circuit. Default: "numpy". | `'numpy'` | Example ```python import torch from eigenkit import Reupload layer = Reupload(n_qubits=4, depth=3, trainable_scale=True) out = layer(torch.randn(8, 4)) # shape (8, 4) ``` #### `provider` The backend that the circuit of this layer was compiled for. The constructor takes `provider=` in the same forms as `Circuit` (a registered name, a configuration or a backend object) and turns it into one `Provider`. This property returns it. The registered name is `provider.name`. Returns: | Name | Type | Description | | ---------- | ---------- | ------------------------------------- | | `provider` | `Provider` | The backend that the circuit runs on. | #### `forward(x)` Run the layer. Parameters: | Name | Type | Description | Default | | ---- | -------- | ----------------------------------------------- | ---------- | | `x` | `Tensor` | The input batch, of shape (batch, in_features). | *required* | Returns: | Name | Type | Description | | ----- | -------- | --------------------------------------- | | `out` | `Tensor` | The output of the layer for that batch. | ## `eigenkit.torch.estimators.custom` Estimators built from blocks and observables that you supply. They wrap a list of `Block` objects and an `observables` specification into a `Circuit`, and then add a small classical head (an `nn.Linear` layer that gives the classification logits or a single regression output). When the number of features of the data differs from the number of inputs that the circuit expects, an `nn.Linear` adapter is added in front. ## `eigenkit.torch.estimators.custom.CustomQuantumClassifier` Bases: `ModelAccessMixin`, `BaseQuantumClassifier` Classifier compatible with scikit-learn, built from your own list of blocks. Give any sequence of blocks (`Encoding`, `Variational`, `StrongVariational`, `Reuploading`, or a custom block from `CircuitBuilder.to_block()`) and an `observables` specification, as for `Circuit`. The estimator connects them into a quantum layer followed by a classical `nn.Linear` head, and trains the whole model with the cross-entropy loss. When the number of features of the data differs from the number of inputs that the circuit expects, an `nn.Linear` adapter is added in front automatically. Parameters: | Name | Type | Description | Default | | ------------------ | ---------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | | `blocks` | `list[Block]` | The list of blocks, in order. | *required* | | `observables` | `ObservableLike` | The observables. It takes the same options as Circuit(observables=...). Default: None (Z on every qubit). | `None` | | `provider` | `ProviderSpec` | The backend. Default: "numpy". | `'numpy'` | | `in_features` | \`int | None\` | The number of features of the data. If None, it is inferred from the first Encoding or Reuploading block. | | `n_classes` | \`int | None\` | The number of output classes. If None, it is inferred from the training labels. | | `lr` | `float` | The learning rate of the optimiser. | `0.01` | | `optimizer` | `str` | The name of the optimiser. The supported names are "adam", "adamw", "rmsprop" and "sgd". | `'adam'` | | `optimizer_kwargs` | \`dict | None\` | Extra keyword arguments passed to the optimiser. | | `epochs` | `int` | The number of training epochs. | `50` | | `batch_size` | `int` | The size of each mini-batch. | `32` | | `show_progress` | `bool` | If True, a tqdm progress bar is shown. | `True` | | `random_state` | \`int | None\` | The seed. | Attributes: | Name | Type | Description | | -------------------- | ---- | -------------------------------------------------------------------------------------------- | | `train_losses_` | | The cross-entropy loss of each epoch, set by fit(). | | `validation_losses_` | | The validation cross-entropy loss of each epoch, set by fit() when validation data is given. | Example ```python import numpy as np from eigenkit import ( CustomQuantumClassifier, Encoding, Variational, all_z, ) rng = np.random.default_rng(0) X_train, X_test = rng.uniform(0, np.pi, (20, 4)), rng.uniform(0, np.pi, (5, 4)) y_train = np.array([0, 1] * 10) blocks = [ Encoding(in_features=4, rotation="RY"), Variational(depth=2, rotation="RY"), ] model = CustomQuantumClassifier( blocks=blocks, observables=all_z(4), epochs=10, ) model.fit(X_train, y_train) predictions = model.predict(X_test) ``` ### `fit(X, y, X_val=None, y_val=None)` Fit the classifier on training data. Parameters: | Name | Type | Description | Default | | ------- | --------- | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | | `X` | `ndarray` | The samples, of shape (n_samples, in_features). | *required* | | `y` | `ndarray` | The class labels, of shape (n_samples,). They can be of any type that scikit-learn's LabelEncoder accepts. | *required* | | `X_val` | \`ndarray | None\` | Optional validation samples, used to compute the validation loss of each epoch. | | `y_val` | \`ndarray | None\` | Optional validation labels, used to compute the validation loss of each epoch. | Returns: | Type | Description | | --------------------------- | --------------------------------------------- | | `'CustomQuantumClassifier'` | This estimator, so that calls can be chained. | Raises: | Type | Description | | ------------ | -------------------------------------------------------------- | | `ValueError` | If in_features is set and X does not have in_features columns. | ### `load(path, *, provider=None)` Load a fitted model from a `.eigenkit` bundle. Load only bundles that you trust. A bundle can contain joblib pickles, and unpickling runs code, as with `torch.load` or `joblib.load`. Parameters: | Name | Type | Description | Default | | ---------- | -------------- | ----------- | --------------------------------------- | | `path` | \`str | Path\` | The path of the bundle. | | `provider` | \`ProviderSpec | None\` | A provider that replaces the saved one. | Returns: | Type | Description | | ------------------------ | --------------------- | | `'BaseQuantumEstimator'` | The loaded estimator. | Raises: | Type | Description | | ----------- | ---------------------------------------------------------------------------------------------------------------------------- | | `TypeError` | If the method is called on a subclass and the bundle holds a different class. BaseQuantumEstimator.load() accepts any class. | ### `predict(X)` Predict the class labels of `X`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | Returns: | Type | Description | | --------- | ------------------------------- | | `ndarray` | One prediction for each sample. | ### `predict_proba(X)` Predict the probability of each class, with a softmax over the logits. It returns a float32 array of shape `(n_samples, n_classes)`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | Returns: | Type | Description | | --------- | ----------------------------------------------------------------------------------------------- | | `ndarray` | The probabilities, of shape (n_samples, n_classes), with the columns in the order of classes\_. | ### `save(path)` Save a fitted model to a `.eigenkit` bundle. The bundles of models that hold scikit-learn objects (`QSVC`, `QSVR`, the reservoir estimators and the time-series estimators) contain joblib pickles, so loading one can run code. Share them only with people who trust you. Parameters: | Name | Type | Description | Default | | ------ | ----- | ----------- | ------------------------------------------------------------------------- | | `path` | \`str | Path\` | The path of the bundle. The suffix .eigenkit is added when it is missing. | ## `eigenkit.torch.estimators.custom.CustomQuantumRegressor` Bases: `ModelAccessMixin`, `BaseQuantumRegressor` Regressor compatible with scikit-learn, built from your own list of blocks. It is the regression counterpart of CustomQuantumClassifier. It wraps your quantum `Circuit` with an `nn.Linear` head of one neuron, trained with the chosen regression loss. An `nn.Linear` input adapter is added when the number of features of the data differs from the number of inputs of the circuit. Parameters: | Name | Type | Description | Default | | ------------------ | ---------------- | ----------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | | `blocks` | `list[Block]` | The list of blocks, in order. | *required* | | `observables` | `ObservableLike` | The observables. It takes the same options as Circuit(observables=...). | `None` | | `provider` | `ProviderSpec` | The backend. Default: "numpy". | `'numpy'` | | `in_features` | \`int | None\` | The number of features of the data. If None, it is inferred from the first Encoding or Reuploading block. | | `lr` | `float` | The learning rate of the optimiser. | `0.01` | | `optimizer` | `str` | The name of the optimiser: "adam", "adamw", "rmsprop" or "sgd". | `'adam'` | | `optimizer_kwargs` | \`dict | None\` | Extra keyword arguments passed to the optimiser. | | `epochs` | `int` | The number of training epochs. | `50` | | `batch_size` | `int` | The size of each mini-batch. | `32` | | `loss_func` | `str` | The regression loss: "mse", "mae" or "huber". | `'mse'` | | `huber_delta` | `float` | The threshold of the Huber loss. | `1.0` | | `huber_reduction` | `str` | The reduction of the Huber loss: "mean" or "sum". | `'mean'` | | `show_progress` | `bool` | If True, a tqdm progress bar is shown. | `True` | | `random_state` | \`int | None\` | The seed. | Attributes: | Name | Type | Description | | -------------------- | ---- | ------------------------------------------------------------------------------ | | `train_losses_` | | The training loss of each epoch, set by fit(). | | `validation_losses_` | | The validation loss of each epoch, set by fit() when validation data is given. | ### `fit(X, y, X_val=None, y_val=None)` Fit the regressor on training data. Parameters: | Name | Type | Description | Default | | ------- | --------- | ----------------------------------------------- | ------------------------------------------------------------------------------- | | `X` | `ndarray` | The samples, of shape (n_samples, in_features). | *required* | | `y` | `ndarray` | The real-valued targets, of shape (n_samples,). | *required* | | `X_val` | \`ndarray | None\` | Optional validation samples, used to compute the validation loss of each epoch. | | `y_val` | \`ndarray | None\` | Optional validation targets, used to compute the validation loss of each epoch. | Returns: | Type | Description | | -------------------------- | --------------------------------------------- | | `'CustomQuantumRegressor'` | This estimator, so that calls can be chained. | ### `load(path, *, provider=None)` Load a fitted model from a `.eigenkit` bundle. Load only bundles that you trust. A bundle can contain joblib pickles, and unpickling runs code, as with `torch.load` or `joblib.load`. Parameters: | Name | Type | Description | Default | | ---------- | -------------- | ----------- | --------------------------------------- | | `path` | \`str | Path\` | The path of the bundle. | | `provider` | \`ProviderSpec | None\` | A provider that replaces the saved one. | Returns: | Type | Description | | ------------------------ | --------------------- | | `'BaseQuantumEstimator'` | The loaded estimator. | Raises: | Type | Description | | ----------- | ---------------------------------------------------------------------------------------------------------------------------- | | `TypeError` | If the method is called on a subclass and the bundle holds a different class. BaseQuantumEstimator.load() accepts any class. | ### `predict(X)` Predict the continuous targets of `X`. It returns a float32 array. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | Returns: | Type | Description | | --------- | ------------------------------- | | `ndarray` | One prediction for each sample. | ### `save(path)` Save a fitted model to a `.eigenkit` bundle. The bundles of models that hold scikit-learn objects (`QSVC`, `QSVR`, the reservoir estimators and the time-series estimators) contain joblib pickles, so loading one can run code. Share them only with people who trust you. Parameters: | Name | Type | Description | Default | | ------ | ----- | ----------- | ------------------------------------------------------------------------- | | `path` | \`str | Path\` | The path of the bundle. The suffix .eigenkit is added when it is missing. | ## `eigenkit.torch.estimators.generic` Estimators that wrap any `nn.Module`. They wrap any `torch.nn.Module` that you supply in a `fit` and `predict` interface compatible with scikit-learn, with the standard training and inference loops of the library. You must build the complete model, including any output head. ## `eigenkit.torch.estimators.generic.GenericRegressor` Bases: `ModelAccessMixin`, `BaseQuantumRegressor` Regressor compatible with scikit-learn that wraps any `torch.nn.Module`. You supply a complete model whose `forward(X)` accepts a float32 tensor of shape `(batch, in_features)` and returns a tensor of shape `(batch, 1)` or `(batch,)`. For sequence models (for example a long short-term memory network), set `sequence_input=True`. The wrapper then passes a 3-D tensor of shape `(batch, seq_len, in_features)` as it is, instead of flattening it. The wrapper trains the model with the standard `train_loop()` of the library, and returns the predictions as a float32 NumPy array. Parameters: | Name | Type | Description | Default | | ------------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `model` | `Module` | Any nn.Module. It must include its own output head. | *required* | | `in_features` | \`int | None\` | The expected number of features at each time step (the last dimension of the input). When it is given, fit raises an error if X.shape[-1] does not match. Pass None to skip the check. | | `sequence_input` | `bool` | If True, the estimator tells QuantumTimeSeriesRegressor that it expects a 3-D tensor of shape (batch, seq_len, in_features), so the windowed data must not be flattened. Default: False. | `False` | | `loss_func` | \`str | Callable\` | The loss: "mse", "mae", "huber", or any callable (output, target) -> scalar_tensor. | | `huber_delta` | `float` | The threshold of the Huber loss. It is ignored for the other losses. | `1.0` | | `huber_reduction` | `str` | The reduction of the Huber loss: "mean" or "sum". | `'mean'` | | `lr` | `float` | The learning rate of the optimiser. | `0.01` | | `optimizer` | `str` | The name of the optimiser: "adam", "adamw", "rmsprop" or "sgd". | `'adam'` | | `optimizer_kwargs` | \`dict | None\` | Extra keyword arguments passed to the constructor of the optimiser. | | `epochs` | `int` | The number of full passes over the training data. | `50` | | `batch_size` | `int` | The size of each mini-batch. | `32` | | `show_progress` | `bool` | If True, a tqdm progress bar is shown during training. | `True` | | `random_state` | \`int | None\` | The seed, applied before the first training step. | Attributes: | Name | Type | Description | | -------------------- | ---- | ------------------------------------------------------------------------------------------ | | `train_losses_` | | The list of the average loss of each epoch, set by fit(). | | `validation_losses_` | | The list of the validation loss of each epoch, set by fit() when validation data is given. | Example ```python import numpy as np from torch import nn from eigenkit.torch.estimators import GenericRegressor from eigenkit.torch.layers import Basic rng = np.random.default_rng(0) X_train, X_test = rng.normal(size=(30, 8)), rng.normal(size=(5, 8)) y_train = X_train.sum(axis=1) # A plain PyTorch network. net = nn.Sequential(nn.Linear(8, 16), nn.ReLU(), nn.Linear(16, 1)) reg = GenericRegressor(model=net, in_features=8, epochs=30) reg.fit(X_train, y_train) preds = reg.predict(X_test) # A quantum layer with a classical head. q = Basic(n_qubits=4, depth=2) hybrid = nn.Sequential(q, nn.Linear(4, 1)) reg2 = GenericRegressor(model=hybrid, in_features=4) ``` ### `accepts_sequence_input` Whether the model expects a 3-D input `(batch, seq_len, features)`. ### `fit(X, y, X_val=None, y_val=None)` Fit the model on training data. Parameters: | Name | Type | Description | Default | | ------- | --------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------- | | `X` | `ndarray` | The samples, of shape (n_samples, in_features) for flat models, or (n_samples, seq_len, in_features) when sequence_input=True. | *required* | | `y` | `ndarray` | The real-valued targets, of shape (n_samples,). | *required* | | `X_val` | \`ndarray | None\` | Optional validation samples, used to compute the validation loss of each epoch. | | `y_val` | \`ndarray | None\` | Optional validation targets, used to compute the validation loss of each epoch. | Returns: | Type | Description | | -------------------- | --------------------------------------------- | | `'GenericRegressor'` | This estimator, so that calls can be chained. | Raises: | Type | Description | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------- | | `ValueError` | If X.shape[-1] or X_val.shape[-1] differs from in_features, or if the output of the model does not have shape (batch,) or (batch, 1). | ### `load(path, *, model=None, provider=None)` Load a fitted `GenericRegressor` from a `.eigenkit` bundle. The architecture of the model cannot be rebuilt automatically, so you must give it. The saved weights are loaded into a copy of it. Parameters: | Name | Type | Description | Default | | ---------- | -------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `path` | \`str | Path\` | The path of the .eigenkit bundle. | | `model` | \`Module | None\` | An nn.Module with the same architecture as the one used in training. Its weights are not changed: the saved weights go into a deep copy, which is model\_ of the loaded estimator. | | `provider` | \`ProviderSpec | None\` | It is ignored. It is kept so that the signature matches the other estimators. | Returns: | Type | Description | | -------------------- | --------------------- | | `'GenericRegressor'` | The loaded estimator. | Raises: | Type | Description | | -------------------- | -------------------------------------- | | `SerializationError` | If model is not given. | | `TypeError` | If the bundle holds a different class. | Example ```python from torch import nn from eigenkit.torch.estimators import GenericRegressor net = nn.Sequential(nn.Linear(8, 16), nn.ReLU(), nn.Linear(16, 1)) reg = GenericRegressor.load("checkpoint.eigenkit", model=net) ``` ### `predict(X)` Predict the continuous targets of `X`. It returns a float32 array. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | Returns: | Type | Description | | --------- | ------------------------------- | | `ndarray` | One prediction for each sample. | ### `save(path)` Save a fitted model to a `.eigenkit` bundle. The bundles of models that hold scikit-learn objects (`QSVC`, `QSVR`, the reservoir estimators and the time-series estimators) contain joblib pickles, so loading one can run code. Share them only with people who trust you. Parameters: | Name | Type | Description | Default | | ------ | ----- | ----------- | ------------------------------------------------------------------------- | | `path` | \`str | Path\` | The path of the bundle. The suffix .eigenkit is added when it is missing. | ## `eigenkit.torch.estimators.generic.GenericClassifier` Bases: `ModelAccessMixin`, `BaseQuantumClassifier` Classifier compatible with scikit-learn that wraps any `torch.nn.Module`. You supply a complete model whose `forward(X)` accepts a float32 tensor of shape `(batch, in_features)` and returns logits of shape `(batch, n_classes)`. For sequence models (for example a long short-term memory network), set `sequence_input=True`. The wrapper then passes a 3-D tensor of shape `(batch, seq_len, in_features)` as it is, instead of flattening it. The wrapper trains with `CrossEntropyLoss` and encodes the labels automatically. Parameters: | Name | Type | Description | Default | | ------------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `model` | `Module` | Any nn.Module. It must include its own classification head, which gives n_classes logits. | *required* | | `in_features` | \`int | None\` | The expected number of features at each time step (the last dimension of the input). When it is given, fit raises an error if X.shape[-1] does not match. Pass None to skip the check. | | `sequence_input` | `bool` | If True, the estimator tells QuantumTimeSeriesClassifier that it expects a 3-D tensor of shape (batch, seq_len, in_features), so the windowed data must not be flattened. Default: False. | `False` | | `n_classes` | \`int | None\` | The number of output classes. If None, it is inferred from the training labels. If it is given, it must equal the number of distinct labels in y. | | `lr` | `float` | The learning rate of the optimiser. | `0.01` | | `optimizer` | `str` | The name of the optimiser: "adam", "adamw", "rmsprop" or "sgd". | `'adam'` | | `optimizer_kwargs` | \`dict | None\` | Extra keyword arguments passed to the constructor of the optimiser. | | `epochs` | `int` | The number of full passes over the training data. | `50` | | `batch_size` | `int` | The size of each mini-batch. | `32` | | `show_progress` | `bool` | If True, a tqdm progress bar is shown during training. | `True` | | `random_state` | \`int | None\` | The seed, applied before the first training step. | Attributes: | Name | Type | Description | | -------------------- | ---- | -------------------------------------------------------------------------------------------- | | `train_losses_` | | The average cross-entropy loss of each epoch, set by fit(). | | `validation_losses_` | | The validation cross-entropy loss of each epoch, set by fit() when validation data is given. | Example ```python import numpy as np from torch import nn from eigenkit.torch.estimators import GenericClassifier from eigenkit.torch.layers import Basic rng = np.random.default_rng(0) X_train, X_test = rng.normal(size=(30, 4)), rng.normal(size=(5, 4)) y_train = np.arange(30) % 3 # A plain PyTorch network, with a head for 3 classes. net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 3)) clf = GenericClassifier(model=net, in_features=4, epochs=20) clf.fit(X_train, y_train) labels = clf.predict(X_test) probs = clf.predict_proba(X_test) # A quantum layer with a classification head. q = Basic(n_qubits=4, depth=2) hybrid = nn.Sequential(q, nn.Linear(4, 3)) clf2 = GenericClassifier(model=hybrid, in_features=4) ``` ### `accepts_sequence_input` Whether the model expects a 3-D input `(batch, seq_len, features)`. ### `fit(X, y, X_val=None, y_val=None)` Fit the model on training data. Parameters: | Name | Type | Description | Default | | ------- | --------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------- | | `X` | `ndarray` | The samples, of shape (n_samples, in_features) for flat models, or (n_samples, seq_len, in_features) when sequence_input=True. | *required* | | `y` | `ndarray` | The class labels, of shape (n_samples,). | *required* | | `X_val` | \`ndarray | None\` | Optional validation samples, used to compute the validation loss of each epoch. | | `y_val` | \`ndarray | None\` | Optional validation labels, used to compute the validation loss of each epoch. | Returns: | Type | Description | | --------------------- | --------------------------------------------- | | `'GenericClassifier'` | This estimator, so that calls can be chained. | Raises: | Type | Description | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ValueError` | If X.shape[-1] or X_val.shape[-1] differs from in_features, if n_classes differs from the number of distinct labels, or if the model does not give one logit for each class. | ### `load(path, *, model=None, provider=None)` Load a fitted `GenericClassifier` from a `.eigenkit` bundle. The architecture of the model cannot be rebuilt automatically, so you must give it. The saved weights and the label encoder are restored automatically. Parameters: | Name | Type | Description | Default | | ---------- | -------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `path` | \`str | Path\` | The path of the .eigenkit bundle. | | `model` | \`Module | None\` | An nn.Module with the same architecture as the one used in training. Its weights are not changed: the saved weights go into a deep copy, which is model\_ of the loaded estimator. | | `provider` | \`ProviderSpec | None\` | It is ignored. It is kept so that the signature matches the other estimators. | Returns: | Type | Description | | --------------------- | --------------------- | | `'GenericClassifier'` | The loaded estimator. | Raises: | Type | Description | | -------------------- | -------------------------------------- | | `SerializationError` | If model is not given. | | `TypeError` | If the bundle holds a different class. | Example ```python from torch import nn from eigenkit.torch.estimators import GenericClassifier net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 3)) clf = GenericClassifier.load("checkpoint.eigenkit", model=net) ``` ### `predict(X)` Predict the class labels of `X`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | Returns: | Type | Description | | --------- | ------------------------------- | | `ndarray` | One prediction for each sample. | ### `predict_proba(X)` Predict the probability of each class, with a softmax over the logits. It returns a float32 array of shape `(n_samples, n_classes)`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | Returns: | Type | Description | | --------- | ----------------------------------------------------------------------------------------------- | | `ndarray` | The probabilities, of shape (n_samples, n_classes), with the columns in the order of classes\_. | ### `save(path)` Save a fitted model to a `.eigenkit` bundle. The bundles of models that hold scikit-learn objects (`QSVC`, `QSVR`, the reservoir estimators and the time-series estimators) contain joblib pickles, so loading one can run code. Share them only with people who trust you. Parameters: | Name | Type | Description | Default | | ------ | ----- | ----------- | ------------------------------------------------------------------------- | | `path` | \`str | Path\` | The path of the bundle. The suffix .eigenkit is added when it is missing. | ## `eigenkit.torch.estimators.hqnc` ## `eigenkit.torch.estimators.hqnc.HQNC` Bases: `ModelAccessMixin`, `BaseQuantumClassifier` Hybrid quantum neural network classifier (HQNC). The model is `MinMaxNorm -> quantum_layer -> nn.Linear(q_out, n_classes)`. If the number of features of the data differs from the resolved input size of the quantum layer (derived from `in_quantum`, `n_qubits` and `layer_type`), an `nn.Linear` adapter is added in front automatically. Parameters: | Name | Type | Description | Default | | ------------------ | ---------------------------- | ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `in_features` | `int` | The number of input features. | *required* | | `in_quantum` | \`int | None\` | The number of inputs that the quantum layer expects. It defaults to in_features. | | `n_qubits` | \`int | None\` | The number of qubits in each circuit. It defaults to in_quantum. | | `depth` | `int` | The number of repetitions of the variational layer. | `2` | | `n_classes` | \`int | None\` | The number of output classes. If None, it is inferred from the training labels. | | `layer_type` | `str` | The quantum layer. It is one of "Basic", "Reupload" and "Ensemble". | `'Basic'` | | `enc_rotation` | \`Literal['RX', 'RY', 'RZ'] | None\` | The encoding rotation: "RX", "RY", "RZ" or None. | | `var_rotation` | \`Literal['RX', 'RY', 'RZ'] | None\` | The variational rotation of the "basic" ansatz: "RX", "RY", "RZ" or None. | | `variational_type` | \`Literal['basic', 'strong'] | None\` | The type of variational block: "basic", "strong" or None. | | `basis` | \`Literal['X', 'Y', 'Z'] | None\` | The measurement basis: "X", "Y", "Z" or None. For these four arguments, None means "use the default of the layer named by layer_type", which is where each of these values is decided. | | `provider` | `ProviderSpec` | The backend. It takes the same options as Circuit(provider=...). | `'numpy'` | | `lr` | `float` | The learning rate of the optimiser. | `0.01` | | `optimizer` | `str` | The name of the optimiser. The supported names are "adam", "adamw", "rmsprop" and "sgd". | `'adam'` | | `optimizer_kwargs` | \`dict | None\` | Extra keyword arguments passed to the optimiser. | | `epochs` | `int` | The number of training epochs. | `50` | | `batch_size` | `int` | The size of each mini-batch. | `32` | | `show_progress` | `bool` | If True, a tqdm progress bar is shown. | `True` | | `random_state` | \`int | None\` | The seed for the initialisation of the weights. | Attributes: | Name | Type | Description | | -------------------- | ---- | -------------------------------------------------------------------------------------------- | | `train_losses_` | | The average cross-entropy loss of each epoch, set by fit(). | | `validation_losses_` | | The validation cross-entropy loss of each epoch, set by fit() when validation data is given. | Example ```python import numpy as np from eigenkit.torch.estimators import HQNC rng = np.random.default_rng(0) X_train, X_test = rng.normal(size=(30, 4)), rng.normal(size=(5, 4)) y_train = np.arange(30) % 3 model = HQNC(in_features=4, depth=2, n_classes=3) model.fit(X_train, y_train) predictions = model.predict(X_test) ``` ### `fit(X, y, X_val=None, y_val=None)` Fit the classifier on training data. Parameters: | Name | Type | Description | Default | | ------- | --------- | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | | `X` | `ndarray` | The samples, of shape (n_samples, in_features). | *required* | | `y` | `ndarray` | The class labels, of shape (n_samples,). They can be of any type that scikit-learn's LabelEncoder accepts. | *required* | | `X_val` | \`ndarray | None\` | Optional validation samples, used to compute the validation loss of each epoch. | | `y_val` | \`ndarray | None\` | Optional validation labels, used to compute the validation loss of each epoch. | Returns: | Type | Description | | ------ | --------------------------------------------- | | `HQNC` | This estimator, so that calls can be chained. | Raises: | Type | Description | | ------------ | --------------------------------------- | | `ValueError` | If X does not have in_features columns. | ### `load(path, *, provider=None)` Load a fitted model from a `.eigenkit` bundle. Load only bundles that you trust. A bundle can contain joblib pickles, and unpickling runs code, as with `torch.load` or `joblib.load`. Parameters: | Name | Type | Description | Default | | ---------- | -------------- | ----------- | --------------------------------------- | | `path` | \`str | Path\` | The path of the bundle. | | `provider` | \`ProviderSpec | None\` | A provider that replaces the saved one. | Returns: | Type | Description | | ------------------------ | --------------------- | | `'BaseQuantumEstimator'` | The loaded estimator. | Raises: | Type | Description | | ----------- | ---------------------------------------------------------------------------------------------------------------------------- | | `TypeError` | If the method is called on a subclass and the bundle holds a different class. BaseQuantumEstimator.load() accepts any class. | ### `predict(X)` Predict the class labels of `X`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | Returns: | Type | Description | | --------- | ------------------------------- | | `ndarray` | One prediction for each sample. | ### `predict_proba(X)` Predict the probability of each class, with a softmax over the logits. It returns a float32 array of shape `(n_samples, n_classes)`. Each row sums to `1`. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | Returns: | Type | Description | | --------- | ----------------------------------------------------------------------------------------------- | | `ndarray` | The probabilities, of shape (n_samples, n_classes), with the columns in the order of classes\_. | ### `save(path)` Save a fitted model to a `.eigenkit` bundle. The bundles of models that hold scikit-learn objects (`QSVC`, `QSVR`, the reservoir estimators and the time-series estimators) contain joblib pickles, so loading one can run code. Share them only with people who trust you. Parameters: | Name | Type | Description | Default | | ------ | ----- | ----------- | ------------------------------------------------------------------------- | | `path` | \`str | Path\` | The path of the bundle. The suffix .eigenkit is added when it is missing. | ## `eigenkit.torch.estimators.hqnr` ## `eigenkit.torch.estimators.hqnr.HQNR` Bases: `ModelAccessMixin`, `BaseQuantumRegressor` Hybrid quantum neural network regressor (HQNR). It is the regression counterpart of `HQNC`. The model is `MinMaxNorm -> quantum_layer -> nn.Linear(q_out, 1)`. If the number of features of the data differs from the resolved input size of the quantum layer, an `nn.Linear` adapter is added in front automatically. Parameters: | Name | Type | Description | Default | | ------------------ | ---------------------------- | ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `in_features` | `int` | The number of input features. | *required* | | `in_quantum` | \`int | None\` | The number of inputs that the quantum layer expects. It defaults to in_features. | | `n_qubits` | \`int | None\` | The number of qubits in each circuit. It defaults to in_quantum. | | `depth` | `int` | The number of repetitions of the variational layer. | `2` | | `layer_type` | `str` | The quantum layer. It is one of "Basic", "Reupload" and "Ensemble". | `'Basic'` | | `enc_rotation` | \`Literal['RX', 'RY', 'RZ'] | None\` | The encoding rotation: "RX", "RY", "RZ" or None. | | `var_rotation` | \`Literal['RX', 'RY', 'RZ'] | None\` | The variational rotation of the "basic" ansatz: "RX", "RY", "RZ" or None. | | `variational_type` | \`Literal['basic', 'strong'] | None\` | The type of variational block: "basic", "strong" or None. | | `basis` | \`Literal['X', 'Y', 'Z'] | None\` | The measurement basis: "X", "Y", "Z" or None. For these four arguments, None means "use the default of the layer named by layer_type", which is where each of these values is decided. | | `provider` | `ProviderSpec` | The backend. It takes the same options as Circuit(provider=...). | `'numpy'` | | `lr` | `float` | The learning rate of the optimiser. | `0.01` | | `optimizer` | `str` | The name of the optimiser. The supported names are "adam", "adamw", "rmsprop" and "sgd". | `'adam'` | | `optimizer_kwargs` | \`dict | None\` | Extra keyword arguments passed to the optimiser. | | `epochs` | `int` | The number of training epochs. | `50` | | `batch_size` | `int` | The size of each mini-batch. | `32` | | `show_progress` | `bool` | If True, a tqdm progress bar is shown. | `True` | | `loss_func` | `str` | The regression loss: "mse", "mae" or "huber". | `'mse'` | | `huber_delta` | `float` | The threshold of the Huber loss. | `1.0` | | `huber_reduction` | `str` | The reduction of the Huber loss: "mean" or "sum". | `'mean'` | | `random_state` | \`int | None\` | The seed. | Attributes: | Name | Type | Description | | -------------------- | ---- | ------------------------------------------------------------------------------ | | `train_losses_` | | The training loss of each epoch, set by fit(). | | `validation_losses_` | | The validation loss of each epoch, set by fit() when validation data is given. | Example ```python import numpy as np from eigenkit.torch.estimators import HQNR rng = np.random.default_rng(0) X_train, X_test = rng.normal(size=(30, 8)), rng.normal(size=(5, 8)) y_train = X_train.sum(axis=1) model = HQNR(in_features=8, depth=2) model.fit(X_train, y_train) predictions = model.predict(X_test) ``` ### `fit(X, y, X_val=None, y_val=None)` Fit the regressor on training data. Parameters: | Name | Type | Description | Default | | ------- | --------- | ----------------------------------------------- | ------------------------------------------------------------------------------- | | `X` | `ndarray` | The samples, of shape (n_samples, in_features). | *required* | | `y` | `ndarray` | The real-valued targets, of shape (n_samples,). | *required* | | `X_val` | \`ndarray | None\` | Optional validation samples, used to compute the validation loss of each epoch. | | `y_val` | \`ndarray | None\` | Optional validation targets, used to compute the validation loss of each epoch. | Returns: | Type | Description | | ------ | --------------------------------------------- | | `HQNR` | This estimator, so that calls can be chained. | Raises: | Type | Description | | ------------ | --------------------------------------- | | `ValueError` | If X does not have in_features columns. | ### `load(path, *, provider=None)` Load a fitted model from a `.eigenkit` bundle. Load only bundles that you trust. A bundle can contain joblib pickles, and unpickling runs code, as with `torch.load` or `joblib.load`. Parameters: | Name | Type | Description | Default | | ---------- | -------------- | ----------- | --------------------------------------- | | `path` | \`str | Path\` | The path of the bundle. | | `provider` | \`ProviderSpec | None\` | A provider that replaces the saved one. | Returns: | Type | Description | | ------------------------ | --------------------- | | `'BaseQuantumEstimator'` | The loaded estimator. | Raises: | Type | Description | | ----------- | ---------------------------------------------------------------------------------------------------------------------------- | | `TypeError` | If the method is called on a subclass and the bundle holds a different class. BaseQuantumEstimator.load() accepts any class. | ### `predict(X)` Predict the continuous targets of `X`. It returns a float32 array. Parameters: | Name | Type | Description | Default | | ---- | --------- | ---------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). | *required* | Returns: | Type | Description | | --------- | ------------------------------- | | `ndarray` | One prediction for each sample. | ### `save(path)` Save a fitted model to a `.eigenkit` bundle. The bundles of models that hold scikit-learn objects (`QSVC`, `QSVR`, the reservoir estimators and the time-series estimators) contain joblib pickles, so loading one can run code. Share them only with people who trust you. Parameters: | Name | Type | Description | Default | | ------ | ----- | ----------- | ------------------------------------------------------------------------- | | `path` | \`str | Path\` | The path of the bundle. The suffix .eigenkit is added when it is missing. | ## `eigenkit.torch.estimators.recurrent_hqnc` ## `eigenkit.torch.estimators.recurrent_hqnc.RecurrentHQNC` Bases: `ModelAccessMixin`, `BaseQuantumClassifier` Hybrid classifier: a recurrent encoder, a quantum layer and a linear class head. The recurrent encoder is a recurrent neural network (RNN). The estimator expects 3-D inputs of shape `(n_samples, seq_len, seq_feature_dim)`. By default, only the last timestep of the RNN output goes to the quantum layer. Set `rnn_return_last_timestep=False`, together with `sequence_length`, to use the full RNN output (flattened) instead. The quantum part is the same as in HQNC: `MinMaxNorm`, the quantum layer and an `nn.Linear` head. Parameters: | Name | Type | Description | Default | | -------------------------- | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `seq_feature_dim` | `int` | The number of features at each time step (the last axis of X). | *required* | | `rnn_type` | `Literal['rnn', 'gru', 'lstm']` | The type of recurrent layer: "rnn", "gru" (gated recurrent unit) or "lstm" (long short-term memory). | `'gru'` | | `rnn_hidden_dim` | `int` | The hidden size in each direction. | `32` | | `rnn_num_layers` | `int` | The number of stacked RNN layers. | `1` | | `rnn_dropout` | `float` | The dropout between the RNN layers. It applies only if rnn_num_layers > 1. | `0.0` | | `rnn_bidirectional` | `bool` | If True, the RNN is bidirectional, and its output size doubles. | `False` | | `rnn_return_last_timestep` | `bool` | If True, only the last output step is kept. Otherwise, the full sequence is flattened, which requires sequence_length. | `True` | | `sequence_length` | \`int | None\` | The number of time steps in each sample. It is required when rnn_return_last_timestep=False, and it must equal X.shape[1] in fit. | | `in_quantum` | \`int | None\` | The number of inputs of the quantum layer. It defaults to the size of the RNN output that reaches the quantum layer: rnn_hidden_dim, doubled when the RNN is bidirectional, and multiplied by sequence_length when rnn_return_last_timestep=False. | | `n_qubits` | \`int | None\` | The number of qubits in each circuit. It defaults to in_quantum. | | `depth` | `int` | The number of repetitions of the variational layer. | `2` | | `n_classes` | \`int | None\` | The number of output classes. If None, it is inferred from the labels passed to fit(). If it is given, it must equal the number of distinct labels in y. | | `layer_type` | `str` | The quantum layer. It is one of "Basic", "Reupload" and "Ensemble". | `'Basic'` | | `enc_rotation` | \`Literal['RX', 'RY', 'RZ'] | None\` | The encoding rotation: "RX", "RY", "RZ" or None. | | `var_rotation` | \`Literal['RX', 'RY', 'RZ'] | None\` | The variational rotation of the basic ansatz: "RX", "RY", "RZ" or None. | | `variational_type` | \`Literal['basic', 'strong'] | None\` | The type of variational block: "basic", "strong" or None. | | `basis` | \`Literal['X', 'Y', 'Z'] | None\` | The measurement basis: "X", "Y", "Z" or None. For these four arguments, None means "use the default of the layer named by layer_type", which is where each of these values is decided. | | `provider` | `ProviderSpec` | The backend. It takes the same options as Circuit(provider=...). | `'numpy'` | | `lr` | `float` | The learning rate of the optimiser. | `0.01` | | `optimizer` | `str` | The name of the optimiser. The supported names are "adam", "adamw", "rmsprop" and "sgd". | `'adam'` | | `optimizer_kwargs` | \`dict | None\` | Extra keyword arguments passed to the optimiser. | | `epochs` | `int` | The number of training epochs. | `50` | | `batch_size` | `int` | The size of each mini-batch. | `32` | | `show_progress` | `bool` | If True, a tqdm progress bar is shown during training. | `True` | | `random_state` | \`int | None\` | The seed for the initialisation of the weights. | Attributes: | Name | Type | Description | | -------------------- | ---- | -------------------------------------------------------------------------------------------- | | `train_losses_` | | The cross-entropy loss of each epoch, set by fit(). | | `validation_losses_` | | The validation cross-entropy loss of each epoch, set by fit() when validation data is given. | ### `fit(X, y, X_val=None, y_val=None)` Fit the estimator on `X` and `y`. Parameters: | Name | Type | Description | Default | | ------- | --------- | -------------------------------------------------------------- | --------------------------------------------------------------------- | | `X` | `ndarray` | The sequences, of shape (n_samples, seq_len, seq_feature_dim). | *required* | | `y` | `ndarray` | One label for each sequence, of shape (n_samples,). | *required* | | `X_val` | \`ndarray | None\` | Optional validation sequences, of the same shape as X. Default: None. | | `y_val` | \`ndarray | None\` | The labels of X_val. Default: None. | Returns: | Type | Description | | --------------- | --------------------------------------------- | | `RecurrentHQNC` | This estimator, so that calls can be chained. | ### `load(path, *, provider=None)` Load a fitted model from a `.eigenkit` bundle. Load only bundles that you trust. A bundle can contain joblib pickles, and unpickling runs code, as with `torch.load` or `joblib.load`. Parameters: | Name | Type | Description | Default | | ---------- | -------------- | ----------- | --------------------------------------- | | `path` | \`str | Path\` | The path of the bundle. | | `provider` | \`ProviderSpec | None\` | A provider that replaces the saved one. | Returns: | Type | Description | | ------------------------ | --------------------- | | `'BaseQuantumEstimator'` | The loaded estimator. | Raises: | Type | Description | | ----------- | ---------------------------------------------------------------------------------------------------------------------------- | | `TypeError` | If the method is called on a subclass and the bundle holds a different class. BaseQuantumEstimator.load() accepts any class. | ### `predict(X)` Predict a label for every sample in `X`. Parameters: | Name | Type | Description | Default | | ---- | --------- | -------------------------------------------------------------- | ---------- | | `X` | `ndarray` | The sequences, of shape (n_samples, seq_len, seq_feature_dim). | *required* | Returns: | Type | Description | | --------- | -------------------------------------------------------- | | `ndarray` | One prediction for each sequence, of shape (n_samples,). | ### `predict_proba(X)` Predict the class probabilities of every sample in `X`. Parameters: | Name | Type | Description | Default | | ---- | --------- | -------------------------------------------------------------- | ---------- | | `X` | `ndarray` | The sequences, of shape (n_samples, seq_len, seq_feature_dim). | *required* | Returns: | Type | Description | | --------- | ----------------------------------------------------------------------------------------------- | | `ndarray` | The probabilities, of shape (n_samples, n_classes), with the columns in the order of classes\_. | ### `save(path)` Save a fitted model to a `.eigenkit` bundle. The bundles of models that hold scikit-learn objects (`QSVC`, `QSVR`, the reservoir estimators and the time-series estimators) contain joblib pickles, so loading one can run code. Share them only with people who trust you. Parameters: | Name | Type | Description | Default | | ------ | ----- | ----------- | ------------------------------------------------------------------------- | | `path` | \`str | Path\` | The path of the bundle. The suffix .eigenkit is added when it is missing. | ## `eigenkit.torch.estimators.recurrent_hqnr` ## `eigenkit.torch.estimators.recurrent_hqnr.RecurrentHQNR` Bases: `ModelAccessMixin`, `BaseQuantumRegressor` Hybrid regressor: a recurrent encoder, a quantum layer and a linear head. The recurrent encoder is a recurrent neural network (RNN). The estimator expects 3-D inputs of shape `(n_samples, seq_len, seq_feature_dim)`. By default, only the last timestep of the RNN output goes to the quantum layer. Set `rnn_return_last_timestep=False`, together with `sequence_length`, to use the full RNN output (flattened) instead. The quantum part is the same as in HQNR: `MinMaxNorm`, the quantum layer and an `nn.Linear` head. Parameters: | Name | Type | Description | Default | | -------------------------- | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `seq_feature_dim` | `int` | The number of features at each time step (the last axis of X). | *required* | | `rnn_type` | `Literal['rnn', 'gru', 'lstm']` | The type of recurrent layer: "rnn", "gru" (gated recurrent unit) or "lstm" (long short-term memory). | `'gru'` | | `rnn_hidden_dim` | `int` | The hidden size in each direction. | `32` | | `rnn_num_layers` | `int` | The number of stacked RNN layers. | `1` | | `rnn_dropout` | `float` | The dropout between the RNN layers. It applies only if rnn_num_layers > 1. | `0.0` | | `rnn_bidirectional` | `bool` | If True, the RNN is bidirectional, and its output size doubles. | `False` | | `rnn_return_last_timestep` | `bool` | If True, only the last output step is kept. Otherwise, the full sequence is flattened, which requires sequence_length. | `True` | | `sequence_length` | \`int | None\` | The number of time steps in each sample. It is required when rnn_return_last_timestep=False, and it must equal X.shape[1] in fit. | | `in_quantum` | \`int | None\` | The number of inputs of the quantum layer. It defaults to the size of the RNN output that reaches the quantum layer: rnn_hidden_dim, doubled when the RNN is bidirectional, and multiplied by sequence_length when rnn_return_last_timestep=False. | | `n_qubits` | \`int | None\` | The number of qubits in each circuit. It defaults to in_quantum. | | `depth` | `int` | The number of repetitions of the variational layer. | `2` | | `layer_type` | `str` | The quantum layer. It is one of "Basic", "Reupload" and "Ensemble". | `'Basic'` | | `enc_rotation` | \`Literal['RX', 'RY', 'RZ'] | None\` | The encoding rotation: "RX", "RY", "RZ" or None. | | `var_rotation` | \`Literal['RX', 'RY', 'RZ'] | None\` | The variational rotation of the "basic" ansatz: "RX", "RY", "RZ" or None. | | `variational_type` | \`Literal['basic', 'strong'] | None\` | The type of variational block: "basic", "strong" or None. | | `basis` | \`Literal['X', 'Y', 'Z'] | None\` | The measurement basis: "X", "Y", "Z" or None. For these four arguments, None means "use the default of the layer named by layer_type", which is where each of these values is decided. | | `provider` | `ProviderSpec` | The backend. It takes the same options as Circuit(provider=...). | `'numpy'` | | `lr` | `float` | The learning rate of the optimiser. | `0.01` | | `optimizer` | `str` | The name of the optimiser. | `'adam'` | | `optimizer_kwargs` | \`dict | None\` | Extra keyword arguments passed to the optimiser. | | `epochs` | `int` | The number of training epochs. | `50` | | `batch_size` | `int` | The size of each mini-batch. | `32` | | `show_progress` | `bool` | If True, a tqdm progress bar is shown. | `True` | | `loss_func` | `str` | The regression loss: "mse", "mae" or "huber". | `'mse'` | | `huber_delta` | `float` | The threshold of the Huber loss. | `1.0` | | `huber_reduction` | `str` | The reduction of the Huber loss: "mean" or "sum". | `'mean'` | | `random_state` | \`int | None\` | The seed. | Attributes: | Name | Type | Description | | -------------------- | ---- | ------------------------------------------------------------------------------ | | `train_losses_` | | The training loss of each epoch, set by fit(). | | `validation_losses_` | | The validation loss of each epoch, set by fit() when validation data is given. | ### `fit(X, y, X_val=None, y_val=None)` Fit the estimator on `X` and `y`. Parameters: | Name | Type | Description | Default | | ------- | --------- | ---------------------------------------------------------------------- | --------------------------------------------------------------------- | | `X` | `ndarray` | The sequences, of shape (n_samples, seq_len, seq_feature_dim). | *required* | | `y` | `ndarray` | One target for each sequence, of shape (n_samples,) or (n_samples, 1). | *required* | | `X_val` | \`ndarray | None\` | Optional validation sequences, of the same shape as X. Default: None. | | `y_val` | \`ndarray | None\` | The targets of X_val. Default: None. | Returns: | Type | Description | | --------------- | --------------------------------------------- | | `RecurrentHQNR` | This estimator, so that calls can be chained. | ### `load(path, *, provider=None)` Load a fitted model from a `.eigenkit` bundle. Load only bundles that you trust. A bundle can contain joblib pickles, and unpickling runs code, as with `torch.load` or `joblib.load`. Parameters: | Name | Type | Description | Default | | ---------- | -------------- | ----------- | --------------------------------------- | | `path` | \`str | Path\` | The path of the bundle. | | `provider` | \`ProviderSpec | None\` | A provider that replaces the saved one. | Returns: | Type | Description | | ------------------------ | --------------------- | | `'BaseQuantumEstimator'` | The loaded estimator. | Raises: | Type | Description | | ----------- | ---------------------------------------------------------------------------------------------------------------------------- | | `TypeError` | If the method is called on a subclass and the bundle holds a different class. BaseQuantumEstimator.load() accepts any class. | ### `predict(X)` Predict a value for every sample in `X`. Parameters: | Name | Type | Description | Default | | ---- | --------- | -------------------------------------------------------------- | ---------- | | `X` | `ndarray` | The sequences, of shape (n_samples, seq_len, seq_feature_dim). | *required* | Returns: | Type | Description | | --------- | --------------------------------------------------------------------------- | | `ndarray` | One prediction for each sequence, as a float32 array of shape (n_samples,). | ### `save(path)` Save a fitted model to a `.eigenkit` bundle. The bundles of models that hold scikit-learn objects (`QSVC`, `QSVR`, the reservoir estimators and the time-series estimators) contain joblib pickles, so loading one can run code. Share them only with people who trust you. Parameters: | Name | Type | Description | Default | | ------ | ----- | ----------- | ------------------------------------------------------------------------- | | `path` | \`str | Path\` | The path of the bundle. The suffix .eigenkit is added when it is missing. | ## `eigenkit.torch.estimators.timeseries` ## `eigenkit.torch.estimators.timeseries.QuantumTimeSeriesRegressor` Bases: `BaseQuantumRegressor` Supervised wrapper with a single output, for time-series regression. It builds sliding windows over the rows of `X` (one row for each time step). Each window has shape `(in_size, n_features)`. Stacked along the batch axis, they form an array of shape `(n_windows, in_size, n_features)`, ready for sequence models such as `torch.nn.LSTM`. When `autoregressive=True`, an extra last channel holds the target value of each timestep, so each step has `n_features + 1` values. For a window that starts at row `i`, the regression target is `y[i + in_size - 1 + shift]`. Parameters: | Name | Type | Description | Default | | ---------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | | `estimator` | `Any` | The inner estimator. It must have fit(X, y) and predict(X). | *required* | | `in_size` | \`int | None\` | The length of the window, in time steps. If None, it is treated as 1. | | `shift` | `int` | How many steps after the last timestep of the window the target is. It must be zero or more. shift=0 predicts the target at the last timestep. | `1` | | `autoregressive` | `bool` | If True, a channel with y[i + k] for k = 0, ..., in_size - 1 is added to each window. With shift=0, this leaks the target into the inputs, and a UserWarning is raised. | `False` | Attributes: | Name | Type | Description | | --------------- | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `train_losses_` | | The training loss of each epoch, copied from the inner estimator if it has train_losses\_ (HQNR, RecurrentHQNR), and an empty list otherwise. It is set by fit(). | Example ```python import numpy as np from eigenkit.torch.estimators import QuantumTimeSeriesRegressor, RecurrentHQNR rng = np.random.default_rng(0) X_train, X_test = rng.normal(size=(40, 3)), rng.normal(size=(15, 3)) y_train = np.sin(np.arange(40) / 4) # The inner estimator should accept X of shape (batch, in_size, n_features). inner = RecurrentHQNR(seq_feature_dim=3, rnn_hidden_dim=4, epochs=5) model = QuantumTimeSeriesRegressor(estimator=inner, in_size=10, shift=1) model.fit(X_train, y_train) predictions = model.predict(X_test) ``` ### `output` The number of outputs, which is always `1`. ### `fit(X, y)` Fit the inner estimator on the windows and their targets. Parameters: | Name | Type | Description | Default | | ---- | --------- | --------------------------------------------------------------- | ---------- | | `X` | `ndarray` | The features in time order, of shape (n_timesteps, n_features). | *required* | | `y` | `ndarray` | The targets, of shape (n_timesteps,) or (n_timesteps, 1). | *required* | Returns: | Type | Description | | ------------------------------ | --------------------------------------------- | | `'QuantumTimeSeriesRegressor'` | This estimator, so that calls can be chained. | ### `load(path, *, provider=None, model=None)` Load a fitted time-series wrapper from a `.eigenkit` bundle. Parameters: | Name | Type | Description | Default | | ---------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------- | | `path` | \`str | Path\` | The path of the .eigenkit bundle. | | `provider` | \`ProviderSpec | None\` | An optional provider that replaces the saved one. It is passed to the inner estimator. | | `model` | `Any` | The nn.Module architecture, required when the inner estimator is GenericRegressor. Pass the same architecture as the one used in training. | `None` | Returns: | Type | Description | | ------------------------------ | ------------------- | | `'QuantumTimeSeriesRegressor'` | The loaded wrapper. | Raises: | Type | Description | | -------------------- | ------------------------------------------------------- | | `TypeError` | If the bundle holds a different class. | | `SerializationError` | If the inner estimator needs model and it is not given. | ### `predict(X, y_history=None)` Predict the regression targets of the windows of `X`. Parameters: | Name | Type | Description | Default | | ----------- | --------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | `X` | `ndarray` | The features in time order, of shape (n_timesteps, n_features). | *required* | | `y_history` | \`ndarray | None\` | The past targets, aligned with the rows of X. They fill the autoregressive channel of each window. It is required when autoregressive=True. | Returns: | Type | Description | | --------- | --------------------------------------------------------------- | | `ndarray` | The predictions, of shape (n_timesteps - in_size - shift + 1,). | ### `score(X, y, *, return_dict=False)` ```python score( X: np.ndarray, y: np.ndarray, *, return_dict: Literal[False] = False, ) -> float ``` ```python score( X: np.ndarray, y: np.ndarray, *, return_dict: Literal[True], ) -> dict[str, float] ``` Score the estimator on the window targets, aligned as in `predict`. By default, it returns `R²`, for compatibility with scikit-learn (`cross_val_score`, `GridSearchCV`). Pass `return_dict=True` to get a dict with `r2`, `mse`, `mae`, `rmse` and `mape`. Parameters: | Name | Type | Description | Default | | ------------- | --------- | ---------------------------------------------------------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). The windows are built inside. | *required* | | `y` | `ndarray` | One target for each sample, of shape (n_samples,). | *required* | | `return_dict` | `bool` | If True, every metric is returned in a dict, instead of the single main score. Default: False. | `False` | Returns: | Type | Description | | ------- | ------------------ | | \`float | dict[str, float]\` | ### `save(path)` Save a fitted model to a `.eigenkit` bundle. The bundles of models that hold scikit-learn objects (`QSVC`, `QSVR`, the reservoir estimators and the time-series estimators) contain joblib pickles, so loading one can run code. Share them only with people who trust you. Parameters: | Name | Type | Description | Default | | ------ | ----- | ----------- | ------------------------------------------------------------------------- | | `path` | \`str | Path\` | The path of the bundle. The suffix .eigenkit is added when it is missing. | ## `eigenkit.torch.estimators.timeseries.QuantumTimeSeriesClassifier` Bases: `BaseQuantumClassifier` Supervised wrapper for time-series classification. It builds sliding windows over the rows of `X` (one row for each time step). Each window has shape `(in_size, n_features)`. Stacked along the batch axis, they form an array of shape `(n_windows, in_size, n_features)`, ready for sequence models such as `RecurrentHQNC`. When `autoregressive=True`, an extra last channel holds the target value of each timestep, so each step has `n_features + 1` values. For a window that starts at row `i`, the classification target is `y[i + in_size - 1 + shift]`. Parameters: | Name | Type | Description | Default | | ---------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | | `estimator` | `Any` | The inner estimator. It must have fit(X, y) and predict(X). | *required* | | `in_size` | \`int | None\` | The length of the window, in time steps. If None, it is treated as 1. | | `shift` | `int` | How many steps after the last timestep of the window the target is. It must be zero or more. shift=0 predicts the target at the last timestep. | `1` | | `autoregressive` | `bool` | If True, a channel with y[i + k] for k = 0, ..., in_size - 1 is added to each window. With shift=0, this leaks the target into the inputs, and a UserWarning is raised. When autoregressive=True, predict() and predict_proba() require a y_history argument. | `False` | Attributes: | Name | Type | Description | | --------------- | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `classes_` | | The distinct class labels seen by fit(). | | `train_losses_` | | The training loss of each epoch, copied from the inner estimator if it has train_losses\_ (HQNC, RecurrentHQNC), and an empty list otherwise. It is set by fit(). | Example ```python import numpy as np from eigenkit.torch.estimators import ( QuantumTimeSeriesClassifier, RecurrentHQNC, ) rng = np.random.default_rng(0) X_train, X_test = rng.normal(size=(40, 4)), rng.normal(size=(15, 4)) y_train = np.arange(40) % 2 inner = RecurrentHQNC(seq_feature_dim=4, rnn_hidden_dim=4, depth=2) model = QuantumTimeSeriesClassifier(estimator=inner, in_size=10, shift=1) model.fit(X_train, y_train) predictions = model.predict(X_test) ``` ### `output` The number of classes. After `fit()`, it is the number of classes seen. Before, it is the `n_classes` of the inner estimator, or None if it has none. ### `fit(X, y)` Fit the inner classifier on the windows and their labels. Parameters: | Name | Type | Description | Default | | ---- | --------- | --------------------------------------------------------------- | ---------- | | `X` | `ndarray` | The features in time order, of shape (n_timesteps, n_features). | *required* | | `y` | `ndarray` | The class labels, of shape (n_timesteps,) or (n_timesteps, 1). | *required* | Returns: | Type | Description | | ------------------------------- | --------------------------------------------- | | `'QuantumTimeSeriesClassifier'` | This estimator, so that calls can be chained. | ### `load(path, *, provider=None, model=None)` Load a fitted time-series classifier wrapper from a `.eigenkit` bundle. Parameters: | Name | Type | Description | Default | | ---------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | | `path` | \`str | Path\` | The path of the .eigenkit bundle. | | `provider` | \`ProviderSpec | None\` | An optional provider that replaces the saved one. It is passed to the inner estimator. | | `model` | `Any` | The nn.Module architecture, required when the inner estimator is GenericClassifier. Pass the same architecture as the one used in training. | `None` | Returns: | Type | Description | | ------------------------------- | ------------------- | | `'QuantumTimeSeriesClassifier'` | The loaded wrapper. | Raises: | Type | Description | | -------------------- | ------------------------------------------------------- | | `TypeError` | If the bundle holds a different class. | | `SerializationError` | If the inner estimator needs model and it is not given. | ### `predict(X, y_history=None)` Predict the class labels of the windows of `X`. Parameters: | Name | Type | Description | Default | | ----------- | --------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `X` | `ndarray` | The features in time order, of shape (n_timesteps, n_features). | *required* | | `y_history` | \`ndarray | None\` | The past class labels, aligned with the rows of X. They fill the autoregressive channel of each window. It is required when autoregressive=True, and it must have at least n_timesteps - shift elements (that is, n_timesteps - in_size - shift + 1 + in_size - 1). | Returns: | Type | Description | | --------- | -------------------------------------------------------------------- | | `ndarray` | The predicted labels, of shape (n_timesteps - in_size - shift + 1,). | ### `predict_proba(X, y_history=None)` Return the estimated class probabilities of the windows of `X`. Parameters: | Name | Type | Description | Default | | ----------- | --------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------ | | `X` | `ndarray` | The features in time order, of shape (n_timesteps, n_features). | *required* | | `y_history` | \`ndarray | None\` | The past class labels. It is required when autoregressive=True. See predict(). | Returns: | Type | Description | | --------- | ------------------------------------------------ | | `ndarray` | A float32 array of shape (n_windows, n_classes). | Raises: | Type | Description | | --------------------- | -------------------------------------------- | | `NotImplementedError` | If the inner estimator has no predict_proba. | ### `score(X, y, *, return_dict=False)` ```python score( X: np.ndarray, y: np.ndarray, *, return_dict: Literal[False] = False, ) -> float ``` ```python score( X: np.ndarray, y: np.ndarray, *, return_dict: Literal[True], ) -> dict[str, float] ``` Score the estimator on the window targets, aligned as in `predict`. By default, it returns the accuracy, for compatibility with scikit-learn (`cross_val_score`, `GridSearchCV`). Pass `return_dict=True` to get a dict with `accuracy`, `f1` (weighted), `precision` (weighted) and `recall` (weighted). Note When `autoregressive=True`, the true labels `y` are used as the autoregressive history channel (teacher forcing). This is the correct behaviour for held-out evaluation, but it means that `score` cannot be called without the true labels of all the timesteps in `X`. Parameters: | Name | Type | Description | Default | | ------------- | --------- | ---------------------------------------------------------------------------------------------- | ---------- | | `X` | `ndarray` | The samples, of shape (n_samples, n_features). The windows are built inside. | *required* | | `y` | `ndarray` | One target for each sample, of shape (n_samples,). | *required* | | `return_dict` | `bool` | If True, every metric is returned in a dict, instead of the single main score. Default: False. | `False` | Returns: | Type | Description | | ------- | ------------------ | | \`float | dict[str, float]\` | ### `save(path)` Save a fitted model to a `.eigenkit` bundle. The bundles of models that hold scikit-learn objects (`QSVC`, `QSVR`, the reservoir estimators and the time-series estimators) contain joblib pickles, so loading one can run code. Share them only with people who trust you. Parameters: | Name | Type | Description | Default | | ------ | ----- | ----------- | ------------------------------------------------------------------------- | | `path` | \`str | Path\` | The path of the bundle. The suffix .eigenkit is added when it is missing. | ## `eigenkit.torch.layers.basic` The basic quantum layer: one `Encoding`, one `Variational` block and a measurement. It maps `n_qubits` input features to `n_qubits` expectation values, one for each qubit, in the chosen Pauli basis. For data reuploading or ensembles, see `Reupload` and `Ensemble`. ### `Basic` Bases: `Module` One `Encoding`, one `Variational` block and a measurement on each qubit. Parameters: | Name | Type | Description | Default | | ------------------ | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `n_qubits` | `int` | The number of qubits, which is also the number of input features. | *required* | | `depth` | `int` | The number of variational layers. | *required* | | `enc_rotation` | `Literal['RX', 'RY', 'RZ']` | The encoding rotation: "RX", "RY" or "RZ". Default: "RX". | `'RX'` | | `var_rotation` | `Literal['RX', 'RY', 'RZ']` | The variational rotation. It is used only when variational_type="basic". The default, "RY", is off both the "RX" encoding axis and the "Z" measurement axis, and this is what lets the weights change the output. Default: "RY". | `'RY'` | | `variational_type` | `Literal['basic', 'strong']` | "basic", for one rotation and a controlled-NOT (CNOT) ladder in each layer, or "strong", for RZ, RY and RZ rotations and a CNOT ladder in each layer. Default: "basic". | `'basic'` | | `basis` | `Literal['X', 'Y', 'Z']` | The Pauli measurement basis: "X", "Y" or "Z". Default: "Z". | `'Z'` | | `provider` | `ProviderSpec` | The backend that runs the circuit. Default: "numpy". | `'numpy'` | Example ```python import torch from eigenkit import Basic layer = Basic(n_qubits=4, depth=2) out = layer(torch.randn(8, 4)) # shape (8, 4) ``` #### `provider` The backend that the circuit of this layer was compiled for. The constructor takes `provider=` in the same forms as `Circuit` (a registered name, a configuration or a backend object) and turns it into one `Provider`. This property returns it. The registered name is `provider.name`. Returns: | Name | Type | Description | | ---------- | ---------- | ------------------------------------- | | `provider` | `Provider` | The backend that the circuit runs on. | #### `forward(x)` Run the layer. Parameters: | Name | Type | Description | Default | | ---- | -------- | ----------------------------------------------- | ---------- | | `x` | `Tensor` | The input batch, of shape (batch, in_features). | *required* | Returns: | Name | Type | Description | | ----- | -------- | --------------------------------------- | | `out` | `Tensor` | The output of the layer for that batch. | ## `eigenkit.torch.layers.ensemble` An ensemble of independent quantum circuits, one for each chunk of the input. It splits the input into chunks of `n_qubits` features and runs one independent circuit on each chunk, each with its own weights. The outputs are concatenated along the feature dimension. ### `Ensemble` Bases: `Module` An ensemble of independent quantum circuits, one for each chunk of the input. Parameters: | Name | Type | Description | Default | | ------------------ | ---------------------------- | --------------------------------------------------------------------------------------- | ---------- | | `in_features` | `int` | The total number of input features. It must be a multiple of n_qubits. | *required* | | `n_qubits` | `int` | The number of qubits of each circuit. | *required* | | `depth` | `int` | The number of variational layers of each circuit. | *required* | | `enc_rotation` | `Literal['RX', 'RY', 'RZ']` | The encoding rotation: "RX", "RY" or "RZ". Default: "RX". | `'RX'` | | `var_rotation` | `Literal['RX', 'RY', 'RZ']` | The variational rotation. It is used only when variational_type="basic". Default: "RY". | `'RY'` | | `variational_type` | `Literal['basic', 'strong']` | "basic" or "strong". Default: "strong". | `'strong'` | | `basis` | `Literal['X', 'Y', 'Z']` | The Pauli measurement basis: "X", "Y" or "Z". Default: "Z". | `'Z'` | | `provider` | `ProviderSpec` | The backend that runs the circuits. Default: "numpy". | `'numpy'` | Example ```python import torch from eigenkit import Ensemble layer = Ensemble(in_features=20, n_qubits=4, depth=5) out = layer(torch.randn(128, 20)) # shape (128, 20) ``` #### `provider` The backend that the circuits of this layer were compiled for. The constructor takes `provider=` in the same forms as `Circuit` (a registered name, a configuration or a backend object) and turns it into one `Provider`. This property returns it. The registered name is `provider.name`. Returns: | Name | Type | Description | | ---------- | ---------- | ------------------------------------- | | `provider` | `Provider` | The backend that the circuits run on. | #### `forward(x)` Run the layer. Parameters: | Name | Type | Description | Default | | ---- | -------- | ----------------------------------------------- | ---------- | | `x` | `Tensor` | The input batch, of shape (batch, in_features). | *required* | Returns: | Name | Type | Description | | ----- | -------- | --------------------------------------- | | `out` | `Tensor` | The output of the layer for that batch. | ## `eigenkit.torch.layers.minmax` ### `MinMaxNorm` Bases: `Module` Min-max normalisation of each feature, with running statistics. It rescales each feature to `[min_val, max_val]`. In training mode, it uses the minimum and maximum of each batch, and it updates an exponential moving average of these statistics. In evaluation mode, it uses the running statistics. A training batch of a single row has no range, so it uses the running statistics too, and if no batch with a range has been seen yet, it returns the row unchanged with a warning. Training and evaluation modes work as in the `BatchNorm` and `LayerNorm` layers of PyTorch, but the output is rescaled to a fixed interval instead of standardised. It is useful to prepare the inputs of quantum encoding rotations, which expect angles in a bounded range such as `[0, π]`. Parameters: | Name | Type | Description | Default | | ------------- | ------- | ----------------------------------------------------------------------------------------------------------- | ---------- | | `feature_dim` | `int` | The number of features in the last dimension of the input. The inputs have shape (batch_size, feature_dim). | *required* | | `min_val` | `float` | The lower bound of the target interval. Default: 0.0. | `0.0` | | `max_val` | `float` | The upper bound of the target interval. Default: π. | `pi` | | `momentum` | `float` | The momentum factor of the exponential moving average of the running statistics. Default: 0.1. | `0.1` | Example ```python import torch from eigenkit import Ensemble, MinMaxNorm norm = MinMaxNorm(feature_dim=16, min_val=0.0, max_val=torch.pi) layer = Ensemble(in_features=16, n_qubits=4, depth=5) out = layer(norm(torch.randn(32, 16))) ``` #### `forward(x)` Run the layer. Parameters: | Name | Type | Description | Default | | ---- | -------- | ----------------------------------------------- | ---------- | | `x` | `Tensor` | The input batch, of shape (batch, feature_dim). | *required* | Returns: | Name | Type | Description | | ----- | -------- | --------------------------------------- | | `out` | `Tensor` | The output of the layer for that batch. | ## `eigenkit.torch.layers.reupload` The data-reuploading layer. It encodes the input `depth` times, with new variational weights between the encodings. Optionally, it adds to each encoding rotation a trainable scale for each pair of layer and feature (Schuld et al. 2021). In this layer, `in_features == n_qubits`. ### `Reupload` Bases: `Module` The data-reuploading layer (Perez-Salinas 2020; Schuld 2021). It supports two patterns: - Standard reuploading (the default). The same input is encoded `depth` times, with new variational weights between the encodings. - Trainable feature scaling (`trainable_scale=True`). Each encoding rotation becomes `R(lambda * x)`, with a trainable `lambda` for each pair of layer and feature. Parameters: | Name | Type | Description | Default | | ------------------ | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------- | | `n_qubits` | `int` | The number of qubits, which is also the number of input features. | *required* | | `depth` | `int` | The number of times the input is encoded. | *required* | | `enc_rotation` | `Literal['RX', 'RY', 'RZ']` | The encoding rotation: "RX", "RY" or "RZ". Default: "RY". | `'RY'` | | `var_rotation` | `Literal['RX', 'RY', 'RZ']` | The variational rotation. It is used only when variational_type="basic". Default: "RY". | `'RY'` | | `variational_type` | `Literal['basic', 'strong']` | "basic" or "strong". Default: "basic". | `'basic'` | | `basis` | `Literal['X', 'Y', 'Z']` | The Pauli measurement basis: "X", "Y" or "Z". Default: "Z". | `'Z'` | | `trainable_scale` | `bool` | If True, each encoding rotation gets a trainable scale for each pair of layer and feature. The CUDA Quantum backend does not support it, because its QuakeValue arithmetic is limited. Default: False. | `False` | | `provider` | `ProviderSpec` | The backend that runs the circuit. Default: "numpy". | `'numpy'` | Example ```python import torch from eigenkit import Reupload layer = Reupload(n_qubits=4, depth=3, trainable_scale=True) out = layer(torch.randn(8, 4)) # shape (8, 4) ``` #### `provider` The backend that the circuit of this layer was compiled for. The constructor takes `provider=` in the same forms as `Circuit` (a registered name, a configuration or a backend object) and turns it into one `Provider`. This property returns it. The registered name is `provider.name`. Returns: | Name | Type | Description | | ---------- | ---------- | ------------------------------------- | | `provider` | `Provider` | The backend that the circuit runs on. | #### `forward(x)` Run the layer. Parameters: | Name | Type | Description | Default | | ---- | -------- | ----------------------------------------------- | ---------- | | `x` | `Tensor` | The input batch, of shape (batch, in_features). | *required* | Returns: | Name | Type | Description | | ----- | -------- | --------------------------------------- | | `out` | `Tensor` | The output of the layer for that batch. |