Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 13 additions & 11 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,20 +147,22 @@ reference every other backend is checked against, and it advertises
`gradient_method="adjoint"`. The remaining backends translate IR to native SDK
objects; they all implement the same Provider protocol, with these differences:

| | PennyLane | Qiskit | Braket (local/cloud) | Braket (QPU) | Qiskit Runtime | IQM | CudaQuantum | Qulacs |
|---|---|---|---|---|---|---|---|---|
| `gradient_method` | `"native"` | `"parameter_shift"` | `"parameter_shift"` | `"none"`¹ | `"none"`¹ | `"none"`¹ | `"parameter_shift"` | `"adjoint"` |
| Symbolic params | resolved per-call | `qiskit.circuit.Parameter` | `FreeParameter` | `FreeParameter` | `qiskit.circuit.Parameter` (transpiled) | `qiskit.circuit.Parameter` (transpiled) | `make_kernel(list)` | `set_parameter` per-call on a `ParametricQuantumCircuit` |
| Compile output | device + qnodes + Pauli ops | parametric QuantumCircuit + SparsePauliOp | parametric Circuit + deduplicated result_types | same + lazy `AwsDevice` | transpiled circuit + layout-mapped SparsePauliOp (estimator) or a qubit-wise-commuting measurement plan (sampler) | parametric QuantumCircuit + one transpiled measured circuit per qubit-wise-commuting group (default `transpile_mode="parametric"`) | `make_kernel` + cudaq spin-op hamiltonians | `ParametricQuantumCircuit` + qulacs `Observable`s |
| Sync `run()` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Async `submit()` | – | – | ✓ (returns `BraketJobHandle`) | ✓ | ✓ (returns `QiskitRuntimeJobHandle`) | ✓ (returns `IQMJobHandle`) | – | – |
| `dry_run_info()` | – | – | ✓ | ✓ | ✓ | ✓ | – | – |
| Cost guardrails | – | – | `CostWarning` on cloud compile | + `shots > 0` enforced + `gradient_method="none"`¹ | `CostWarning` on real-hardware compile + `shots > 0` enforced | `CostWarning` on real-hardware compile + `shots > 0` enforced | – | – |
| Auto-registered | ✓ "pennylane" | ✓ "qiskit" | ✓ "braket" | ✓ "braket" (config flips behaviour) | ✗ opt-in | ✓ "iqm" (when extra installed) | ✗ opt-in (extra dep) | ✓ "qulacs" (when extra installed) |
| | PennyLane | Qiskit | Braket (local/cloud) | Braket (QPU) | Qiskit Runtime | IQM | CudaQuantum | Qulacs | EigenOrbit Router² |
|---|---|---|---|---|---|---|---|---|---|
| `gradient_method` | `"native"` | `"parameter_shift"` | `"parameter_shift"` | `"none"`¹ | `"none"`¹ | `"none"`¹ | `"parameter_shift"` | `"adjoint"` | `"none"` on a QPU, `"parameter_shift"` on a simulator |
| Symbolic params | resolved per-call | `qiskit.circuit.Parameter` | `FreeParameter` | `FreeParameter` | `qiskit.circuit.Parameter` (transpiled) | `qiskit.circuit.Parameter` (transpiled) | `make_kernel(list)` | `set_parameter` per-call on a `ParametricQuantumCircuit` | – (the circuit goes out as wire JSON; weights per call) |
| Compile output | device + qnodes + Pauli ops | parametric QuantumCircuit + SparsePauliOp | parametric Circuit + deduplicated result_types | same + lazy `AwsDevice` | transpiled circuit + layout-mapped SparsePauliOp (estimator) or a qubit-wise-commuting measurement plan (sampler) | parametric QuantumCircuit + one transpiled measured circuit per qubit-wise-commuting group (default `transpile_mode="parametric"`) | `make_kernel` + cudaq spin-op hamiltonians | `ParametricQuantumCircuit` + qulacs `Observable`s | wire JSON of the circuit and observables |
| Sync `run()` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Async `submit()` | – | – | ✓ (returns `BraketJobHandle`) | ✓ | ✓ (returns `QiskitRuntimeJobHandle`) | ✓ (returns `IQMJobHandle`) | – | – | ✓ (returns `RouterJobHandle`) |
| `dry_run_info()` | – | – | ✓ | ✓ | ✓ | ✓ | – | – | ✓ |
| Cost guardrails | – | – | `CostWarning` on cloud compile | + `shots > 0` enforced + `gradient_method="none"`¹ | `CostWarning` on real-hardware compile + `shots > 0` enforced | `CostWarning` on real-hardware compile + `shots > 0` enforced | – | – | `CostWarning` with the router's price estimate on compile |
| Auto-registered | ✓ "pennylane" | ✓ "qiskit" | ✓ "braket" | ✓ "braket" (config flips behaviour) | ✗ opt-in | ✓ "iqm" (when extra installed) | ✗ opt-in (extra dep) | ✓ "qulacs" (when extra installed) | ✓ "router" (when extra installed) |

¹ `"parameter_shift"` when the device config sets `enable_gradients=True`; compiling against a paid device then raises a second `CostWarning` quoting the job count of one backward pass. Every gradient path (torch backward, `jacobian()`, the variational spine) goes through `Circuit._gradient_jacobians`, which refuses a `"none"` backend; torch's `_NoGradFn` and `variational()` refuse earlier, with messages specific to their path.

Async `submit()` covers Braket, Qiskit Runtime and IQM.
² The `router` extra installs eigenorbit-router-client, which ships this backend and `RouterConfig`; eigenkit registers `"router"` lazily when that package is importable. It runs circuits on the EigenOrbit Router, a hosted service that reaches real QPUs with one EigenOrbit API key.

Async `submit()` covers Braket, Qiskit Runtime, IQM and EigenOrbit Router.

Read PennyLane and Qiskit first (the simpler pair); Braket and CudaQuantum
follow the same shape with backend-specific quirks.
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ pip install "eigenkit[all]"
| `qulacs` | Qulacs | `provider="qulacs"` |
| `qsim` | qsimcirq + Cirq | `provider="qsim"` (single-precision, ~1e-6 residual) |
| `perceval` | Perceval (Quandela's photonic SDK) | `eigenkit.photonics` (local simulation, Quandela Cloud) |
| `router` | eigenorbit-router-client | `provider="router"`, `RouterConfig` (EigenOrbit Router: real QPUs with one EigenOrbit API key) |
| `qml` | scikit-learn (torch-free) | `QuantumKernel`, `QSVC`/`QSVR` |
| `datasets` | scikit-learn | `eigenkit.datasets` |
| `benchmark` | matplotlib + pandas + scikit-learn | the model comparison harness |
Expand Down
3 changes: 2 additions & 1 deletion docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,13 @@ pip install "eigenkit[all]"
| `qulacs` | Qulacs | `provider="qulacs"` |
| `qsim` | qsimcirq + Cirq | `provider="qsim"` (single-precision) |
| `perceval` | Perceval (Quandela's photonic SDK) | `eigenkit.photonics` (local simulation, Quandela Cloud) |
| `router` | eigenorbit-router-client | `provider="router"`, `RouterConfig` (EigenOrbit Router: real QPUs with one EigenOrbit API key) |
| `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` | — |
| `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
Expand Down
1 change: 1 addition & 0 deletions docs/user-guide/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ choose a backend for its speed.
| 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. |
| EigenOrbit Router | Lazy (`"router"`). | None on a QPU, parameter shift on a simulator. | No, one job per sample, submitted in parallel. | Real QPUs and cloud simulators through the hosted EigenOrbit Router, with one EigenOrbit API key and a prepaid balance. Pass `RouterConfig(device=...)` from eigenorbit-router-client: the device has no default, so a bare `provider="router"` raises an error that says so. It needs the `router` extra. |

## NumPy (built in)

Expand Down
6 changes: 5 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ qsim = [
perceval = [
"perceval-quandela>=1.3,<2",
]
# EigenOrbit Router client: real QPUs with one EigenOrbit API key.
router = [
"eigenorbit-router-client>=0.1,<1",
]
# Synthetic + real datasets and the sklearn baselines used by them.
datasets = [
"scikit-learn>=1.3,<2",
Expand Down Expand Up @@ -159,7 +163,7 @@ qml = [
"scikit-learn>=1.3,<2",
]
all = [
"eigenkit[torch,pennylane,qiskit,braket,iqm,tensorcircuit,cudaquantum,qulacs,qsim,benchmark,datasets,chemistry,qml,viz]",
"eigenkit[torch,pennylane,qiskit,braket,iqm,tensorcircuit,cudaquantum,qulacs,qsim,router,benchmark,datasets,chemistry,qml,viz]",
]

[project.urls]
Expand Down
23 changes: 23 additions & 0 deletions src/eigenkit/configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@
- [`TensorCircuitConfig`][eigenkit.core.backends.tensorcircuit_backend.TensorCircuitConfig]
- [`QulacsConfig`][eigenkit.core.backends.qulacs_backend.QulacsConfig]
- [`QsimConfig`][eigenkit.core.backends.qsim_backend.QsimConfig]
- `RouterConfig`: the EigenOrbit Router, which runs circuits on real quantum
hardware with one EigenOrbit API key. It is the class of
eigenorbit-router-client and needs the `router` extra
(`pip install eigenkit[router]`).

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
Expand Down Expand Up @@ -63,6 +67,7 @@
from .core.blocks import AngleEncoding as Encoding
from .core.blocks import BasicVariational as Variational
from .core.blocks import Reuploading, StrongVariational
from .core.provider import _EXTRA_HINT


# config name -> defining backend module (under eigenkit.core.backends)
Expand All @@ -80,7 +85,25 @@
}


# Configs that live in another distribution: config name -> (module, extra).
# They stay out of `_LAZY_CONFIGS` and `__all__`, so a star import and the
# lazy-config roster never need that distribution.
_EXTERNAL_CONFIGS = {"RouterConfig": ("eigenorbit_router_client", "router")}


def __getattr__(name: str):
external = _EXTERNAL_CONFIGS.get(name)
if external is not None:
module_name, extra = external
try:
module = importlib.import_module(module_name)
except ModuleNotFoundError as exc:
if exc.name != module_name:
raise
raise ImportError(
f"{name} needs {module_name} ({_EXTRA_HINT[extra]})"
) from exc
return getattr(module, name)
module_name = _LAZY_CONFIGS.get(name)
if module_name is not None:
module = importlib.import_module(f"eigenkit.core.backends.{module_name}")
Expand Down
20 changes: 17 additions & 3 deletions src/eigenkit/core/backends/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
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"`, `"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:
`"pennylane"`, `"qiskit"`, `"braket"`, `"router"` 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
Expand Down Expand Up @@ -88,6 +88,20 @@ def _has_iqm_qiskit() -> bool:
)
if importlib.util.find_spec("qulacs") is not None:
register_lazy("qulacs", _loader("qulacs_backend", "QulacsBackend"))


# EigenOrbit Router through eigenorbit-router-client (the `router` extra). The
# backend lives in that package, not under this one, so it gets its own loader.
# `find_spec` on a top-level name locates the package without importing it, so
# `import eigenkit` pulls neither the client nor httpx.
def _router_backend():
return importlib.import_module("eigenorbit_router_client").RouterBackend()


if importlib.util.find_spec("eigenorbit_router_client") is not None:
register_lazy("router", _router_backend)


# Lazy class/config re-exports: name -> defining module (under this package).
_LAZY_EXPORTS = {
"BraketBackend": "braket_backend",
Expand Down
1 change: 1 addition & 0 deletions src/eigenkit/core/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,7 @@ def register_lazy(name: str, loader: Callable[[], Provider]) -> None:
"iqm": "install with `pip install eigenkit[iqm]`",
"qiskit-aer": "install with `pip install eigenkit[qiskit]`",
"qiskit-aer-mps": "install with `pip install eigenkit[qiskit]`",
"router": "install with `pip install eigenkit[router]`",
"cudaq": (
"opt-in and Linux/arm64-macOS only: after installing it, register it "
"yourself with `register('cudaq', CudaQuantumBackend())`"
Expand Down
37 changes: 29 additions & 8 deletions src/eigenkit/qml/_serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,15 +223,23 @@ def serialize_provider_param(provider) -> object:
# Fields that decide where a provider connects. They are never written to a
# bundle and never read from one: a bundle is data from whoever shared it, and
# loading it builds the backend with the loader's credentials (IQM_TOKEN,
# QISKIT_IBM_TOKEN), which must not go to a host the bundle names.
_ENDPOINT_FIELDS = ("url", "service_kwargs", "provider_kwargs", "backend_instance")
# QISKIT_IBM_TOKEN, EIGENORBIT_ROUTER_API_KEY), which must not go to a host the
# bundle names.
_ENDPOINT_FIELDS = (
"url",
"service_kwargs",
"provider_kwargs",
"backend_instance",
"base_url",
)


def deserialize_provider_param(value) -> object:
"""Restore a provider value from its JSON form.

The endpoint fields (`_ENDPOINT_FIELDS`) in the bundle are ignored, with a
warning. To connect elsewhere, pass `provider=` to `load()`.
The endpoint fields (`_ENDPOINT_FIELDS`) and the secret fields in the bundle
are ignored, with a warning. To connect elsewhere or with other credentials,
pass `provider=` to `load()`.

Raises:
SerializationError: If the configuration class is unknown.
Expand All @@ -251,15 +259,28 @@ def deserialize_provider_param(value) -> object:
)
from eigenkit import configs

# Every config `eigenkit.configs` exports, so a new one cannot save fine and
# Every config `eigenkit.configs` resolves, so a new one cannot save fine and
# then fail to load. Only the class named is imported: the others may need
# SDKs this install lacks.
known = sorted({"NumpyConfig", *configs._LAZY_CONFIGS})
# SDKs or distributions this install lacks.
known = sorted({"NumpyConfig", *configs._LAZY_CONFIGS, *configs._EXTERNAL_CONFIGS})
if cls_name not in known:
raise SerializationError(
f"Unknown provider config class {cls_name!r}. Known: {known}"
)
return getattr(configs, cls_name)(**kwargs)
cls = getattr(configs, cls_name)
# A credential in the bundle would run the loader's jobs, and send its
# inputs, under the bundle author's account; the loader's own is read instead.
secrets = sorted(
name for name in secret_field_names(cls) if kwargs.pop(name, None) is not None
)
if secrets:
warnings.warn(
f"ignored {', '.join(secrets)} in the bundle's {cls_name}: a bundle "
"cannot carry credentials. Pass provider= to load() to set them.",
UserWarning,
stacklevel=2,
)
return cls(**kwargs)


def _observables_to_dict(observables) -> list | dict:
Expand Down
Loading
Loading