Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
e8f7911
fix(schemes): honour `execute_function` in `MSQPAM.decode`
dlyongemallo Apr 16, 2026
0d6fb1b
refactor(schemes): type `execute_function` with `ExecuteFunction` pro…
dlyongemallo Apr 16, 2026
e749f59
fix(utils): saturate quantize output to avoid sign-flip wraparound
dlyongemallo Apr 28, 2026
d3a4b90
feat(backends): add core types and abstractions
dlyongemallo May 2, 2026
f891d40
feat(backends): add Qiskit backend provider
dlyongemallo Apr 19, 2026
afca30d
feat(backends): add Cirq backend provider
dlyongemallo Apr 19, 2026
bb6abb5
feat(quantumaudio): export backends subpackage
dlyongemallo May 6, 2026
db837bf
refactor(utils): support `CircuitSpec` and `UnifiedResult` alongside …
dlyongemallo Apr 19, 2026
8c7941e
test(backends): add tests for `CircuitSpec`, providers, and cross-bac…
dlyongemallo Apr 26, 2026
5747181
test(schemes): update existing tests for `CircuitSpec` types
dlyongemallo Apr 19, 2026
03f45b9
refactor(schemes)!: emit `CircuitSpec`, retain `execute_function` for…
dlyongemallo Apr 26, 2026
6e7379c
refactor(schemes)!: remove `execute_function` parameter from `decode`
dlyongemallo Apr 26, 2026
882e814
feat(backends): add PennyLane backend provider
dlyongemallo Apr 28, 2026
d8a46e3
feat(backends): add NVIDIA CUDA-Q backend provider
dlyongemallo Apr 28, 2026
9202cfa
feat(backends): give CUDA-Q kernels meaningful job names
dlyongemallo May 2, 2026
66ad505
feat(backends): give PennyLane-submitted IonQ jobs meaningful names
dlyongemallo May 2, 2026
6a04e50
docs: rewrite Native and External Backends sections for backend layer
dlyongemallo May 2, 2026
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
55 changes: 45 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,11 +187,49 @@ It wraps the functions provided in the module `quantumaudio.tools.stream` that h

### Running on Native Backends

A Scheme's ```decode()``` method uses local [_AerSimulator_](https://github.com/Qiskit/qiskit-aer) as the default backend. Internally, the function calls `utils.execute()` method that performs ```backend.run()```. Any such backend object compatible with Qiskit can be passed to the ```backend=``` parameter of the `decode()` function. To configure this further or to use primitives, please refer to custom [execute functions](#custom_functions).
A Scheme's `decode()` method runs the circuit through one of the framework-agnostic backends registered in `quantumaudio.backends`. Each is auto-registered if its underlying library is installed, all default to a local simulator, and the choice is selected by name on `decode(spec, backend="...")`:

| `backend=` | Library / extras | Local default | Install |
|---|---|---|---|
| `"qiskit"` (default) | `qiskit`, `qiskit_aer` | `AerSimulator()` | included |
| `"cirq"` | `cirq-core` | `cirq.Simulator()` | `pip install "quantumaudio[cirq]"` |
| `"pennylane"` | `pennylane` | `default.qubit` | `pip install "quantumaudio[pennylane]"` |
| `"cudaq"` | `cudaq` | `qpp-cpu` (CPU statevector) | `pip install "quantumaudio[cudaq]"` |

```python
spec = scheme.encode(audio)
out = scheme.decode(spec, backend="cudaq", shots=8000)
```

`scheme.decode(..., backend="<name>")` always runs **locally**. Reaching the cloud requires constructing a backend instance directly with a vendor device/target name, see below.

### Running on External Quantum Backends

The package allows flexible use of Quantum Hardware from different Providers as the execution of circuits can be done independently. Depending on the results, there are two ways to decode quantum audio:
To submit to vendor hardware (or a vendor-hosted cloud simulator), construct the relevant backend yourself with the device/target name as a constructor argument, then drive it through `build_circuit` / `run` / `decode_counts` (`decode(backend=...)` only accepts registered names, not instances):

```python
import os
os.environ["IONQ_API_KEY"] = "..."

# PennyLane plugin route -- IonQ cloud simulator (free):
from quantumaudio.backends.providers.pennylane_backend import PennyLaneBackend
be = PennyLaneBackend(device="ionq.simulator") # or "ionq.qpu"

# CUDA-Q route -- IonQ via NVIDIA's runtime:
from quantumaudio.backends.providers.cudaq_backend import CudaQBackend
be = CudaQBackend(target="ionq", qpu="simulator") # or qpu="qpu.aria-1"

native = be.build_circuit(spec)
result = be.run(native, shots=1024)
out = scheme.decode_counts(result.counts, metadata=spec.metadata)
```

A single `PennyLaneBackend` reaches every vendor PennyLane has a plugin for (IonQ, Braket, Rigetti, Quantinuum, IQM, Pasqal, ...); install the corresponding plugin (`pennylane-ionq`, `amazon-braket-pennylane-plugin`, ...) for your target. `CudaQBackend` reaches IonQ, Quantinuum, IQM, Anyon, ORCA, Pasqal, Rigetti, QuEra, Braket, etc. via NVIDIA's runtime.

> [!Caution]
> For vendor targets that default to **real (paid) hardware** when no machine is specified -- IonQ, Anyon, Braket, Infleqtion, OQC, Quantinuum, Scaleway -- `CudaQBackend` refuses to instantiate without an explicit selector kwarg. So `CudaQBackend(target="ionq")` raises; you must pass `qpu="simulator"` (free cloud sim) or `qpu="qpu.aria-1"` (specific QPU). Validate on the cloud simulator first; the audio schemes generate one circuit per `stream()` chunk and per-shot fees add up fast.

You can also skip the high-level backend entirely and submit through the vendor's own SDK, then feed the result back into the scheme:

- **Results Object:** If the result obtained follow the format of [qiskit.result.Result](https://docs.quantum.ibm.com/api/qiskit/qiskit.result.Result) or [qiskit.primitives.PrimitiveResult](https://docs.quantum.ibm.com/api/qiskit/qiskit.primitives.PrimitiveResult),
- The audio can be decoded with ```scheme.decode_result(result_object)``` method.
Expand Down Expand Up @@ -220,19 +258,16 @@ The _QPAM_ scheme's encoding only preserves **num_samples** (_int_) and the norm
> The essential keys required for decoding with any scheme can be checked from the scheme's `.keys` attribute.

### Using Custom Functions <a id="custom_functions"></a>
The `decode` and `stream` operations can be configured with the following custom functions. They require few mandatory arguments followed by custom preceding keyword arguments (denoted as `**kwargs`).
The `stream` operation can be configured with a custom process function:
- **Process Function**:
The default process function of `stream()` simply encodes and decodes a chunk of data with default parameters. It can be overriden by passing a custom function to the `process_function=` parameter. The mandatory arguments for the custom process function are `data=` and `scheme=`.
```python
processed_data = process_function(data, scheme, **kwargs)
```
- **Execute Function** :
The default execute function for `decode()` can be overriden by passing a custom function to the `execute_function=` parameter. The mandatory argument for the custom execute function is `circuit=`. (QPAM also expects `shots=` since it's a metadata)
```python
result = execute_function(circuit, **kwargs)
```
**Example**: An optional execute function is included in the package which uses [Sampler Primitive](https://docs.quantum.ibm.com/api/qiskit-ibm-runtime/qiskit_ibm_runtime.SamplerV2): `quantumaudio.utils.execute_with_sampler` that can be passed to the `decode()` method. It requires the dependency `pip install qiskit-ibm-runtime`. <br>


To use an alternative circuit-execution path (a different framework, a different vendor, or a custom orchestration layer), register a `Backend` subclass via `quantumaudio.backends.registry` and select it by name from `decode(backend=...)`, or follow the explicit `build_circuit` / `run` / `decode_counts` pattern shown in the [External Quantum Backends](#running-on-external-quantum-backends) section above.


## 📘 Additional Resources <a id="materials"></a>
### Notebook Examples
For examples of circuit preparation, signal reconstruction, and interactive demonstrations, please check the [Demo Notebook](https://github.com/moth-quantum/quantum-audio/blob/main/demos/1_Basics_Walkthrough.ipynb). It combines the core package with additional functions from the `demos/tools` folder to go through Visual and Digital Audio examples.
Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ Repository = "https://github.com/moth-quantum/quantum-audio"
Documentation = "https://quantumaudio.readthedocs.io/"

[project.optional-dependencies]
cirq = ["cirq-core"]
pennylane = ["pennylane"]
cudaq = ["cudaq"]
demos = [
"soundfile==0.12.1",
"librosa==0.10.2.post1",
Expand Down
1 change: 1 addition & 0 deletions quantumaudio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ def __dir__():
"schemes",
"utils",
"tools",
"backends",
"load_scheme",
"encode",
"decode",
Expand Down
58 changes: 58 additions & 0 deletions quantumaudio/backends/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Copyright 2024 Moth Quantum
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==========================================================================

"""Framework-agnostic backend layer for quantumaudio.

This subpackage provides a backend abstraction that allows quantum
circuits built by quantumaudio schemes to be executed on different
quantum frameworks (Qiskit, Cirq, and others).
"""

from quantumaudio.backends._optional import is_available, require
from quantumaudio.backends.core import (
CircuitSpec,
GateOp,
GateType,
UnifiedResult,
Backend,
registry,
)

# Trigger auto-registration of installed backends.
import quantumaudio.backends.providers # noqa: F401


def available_backends() -> list[str]:
"""Return names of all backends whose dependencies are installed."""
return registry.available()


def get_backend(name: str = "qiskit") -> Backend:
"""Instantiate and return a backend by name."""
return registry.get(name)


__all__ = [
"Backend",
"CircuitSpec",
"GateOp",
"GateType",
"UnifiedResult",
"available_backends",
"get_backend",
"is_available",
"registry",
"require",
]
56 changes: 56 additions & 0 deletions quantumaudio/backends/_optional.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Copyright 2024 Moth Quantum
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==========================================================================

"""Helpers for guarding optional backend dependencies."""

from __future__ import annotations

import importlib
import importlib.util
from types import ModuleType


def is_available(package: str) -> bool:
"""Check whether *package* can be imported without importing it."""
return importlib.util.find_spec(package) is not None


def require(
package: str, extras_name: str | None = None
) -> ModuleType:
"""Import and return *package*, raising a helpful error if missing.

Only a missing top-level package is converted into the "install
quantumaudio[<extra>]" hint. Import errors raised inside an
installed dependency (e.g. due to a broken transitive dep) are
re-raised unchanged so the original traceback is preserved.

Args:
package: Dotted module name, e.g. ``"cirq"`` or ``"qiskit"``.
extras_name: Optional pip extras hint used in the error message.
"""
top_level = package.split(".", 1)[0]
try:
return importlib.import_module(package)
except ModuleNotFoundError as e:
# Only convert "the requested package itself is missing" into
# the install hint; let nested missing modules propagate.
if e.name in (package, top_level):
hint = extras_name or top_level
raise ImportError(
f"{package!r} is required but not installed. "
f"Install it with: pip install quantumaudio[{hint}]"
) from None
raise
33 changes: 33 additions & 0 deletions quantumaudio/backends/core/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Copyright 2024 Moth Quantum
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==========================================================================

from quantumaudio.backends.core.types import GateType
from quantumaudio.backends.core.circuit import CircuitSpec, GateOp
from quantumaudio.backends.core.result import UnifiedResult
from quantumaudio.backends.core.backend import (
Backend,
BackendRegistry,
registry,
)

__all__ = [
"Backend",
"BackendRegistry",
"CircuitSpec",
"GateOp",
"GateType",
"UnifiedResult",
"registry",
]
86 changes: 86 additions & 0 deletions quantumaudio/backends/core/backend.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Copyright 2024 Moth Quantum
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==========================================================================

"""Backend abstract base class and registry."""

from __future__ import annotations

from abc import ABC, abstractmethod
from typing import Any

import numpy as np

from quantumaudio.backends.core.circuit import CircuitSpec
from quantumaudio.backends.core.result import UnifiedResult


class Backend(ABC):
"""Interface that every quantum backend must implement."""

name: str

@abstractmethod
def build_circuit(self, spec: CircuitSpec) -> Any:
"""Translate a CircuitSpec into the backend's native circuit."""

@abstractmethod
def run(
self, native_circuit: Any, shots: int = 1024
) -> UnifiedResult:
"""Execute a native circuit and return a UnifiedResult."""

@abstractmethod
def statevector(self, native_circuit: Any) -> np.ndarray:
"""Return the exact statevector (simulator only)."""

def run_spec(
self, spec: CircuitSpec, shots: int = 1024
) -> UnifiedResult:
"""Convenience: build and run in one call."""
native = self.build_circuit(spec)
return self.run(native, shots)


class BackendRegistry:
"""Singleton that tracks available backends."""

_instance: BackendRegistry | None = None
_backends: dict[str, type[Backend]]

def __new__(cls) -> BackendRegistry:
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._backends = {}
return cls._instance

def register(self, name: str, cls_: type[Backend]) -> None:
self._backends[name] = cls_

def get(self, name: str) -> Backend:
"""Instantiate and return a backend by name."""
if name not in self._backends:
available = ", ".join(self._backends) or "(none)"
raise KeyError(
f"Backend {name!r} not available. "
f"Installed: {available}"
)
return self._backends[name]()

def available(self) -> list[str]:
"""Return names of all registered backends."""
return list(self._backends)


registry = BackendRegistry()
Loading