Metadata-Version: 2.4
Name: openquantum-sdk-pennylane
Version: 0.1.7
Summary: PennyLane Python SDK for Open Quantum
License: Apache-2.0
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: openquantum-sdk>=0.3.7
Requires-Dist: pennylane>=0.38
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-cov; extra == "dev"
Requires-Dist: pytest-mock>=3.10; extra == "dev"
Requires-Dist: tomli; python_version < "3.11" and extra == "dev"

# PennyLane-Open Quantum Plugin

A [PennyLane](https://pennylane.ai/) plugin for the [Open Quantum](https://openquantum.com) quantum computing platform. Submit quantum circuits to real quantum hardware through the Open Quantum SDK.

## Installation

```bash
pip install openquantum-sdk-pennylane
```

**That's it.** Installing the package automatically registers the `openquantum.device` with PennyLane via Python entry points — no extra imports needed. Just `import pennylane as qml` and use `qml.device("openquantum.device", ...)`.

For development:

```bash
pip install openquantum-sdk-pennylane[dev]
```

## Quick Start

```python
import pennylane as qml

# No need to import openquantum — the device registers automatically on install.
dev = qml.device(
    "openquantum.device",
    wires=2,
    shots=1024,
    backend="ionq:aria-1",
    client_id="your_client_id",
    client_secret="your_client_secret",
)

@qml.qnode(dev)
def bell_state():
    qml.Hadamard(wires=0)
    qml.CNOT(wires=[0, 1])
    return qml.counts()

result = bell_state()
# The device shows backend status, credit cost, and prompts for confirmation:
#   [Open Quantum] ● IonQ Aria-1 (ionq:aria-1): Online | queue: 0 job(s)
#   [Open Quantum] Batch: 1 circuit(s) × 2 credit(s) each = 2 credit(s) total
#   [Open Quantum] Proceed? [y/N] y
print(result)  # {'00': 512, '11': 512}
```

## Configuration

### Required Parameters

| Parameter | Description |
|-----------|-------------|
| `wires` | Number of qubits |
| `shots` | Number of measurement shots (required, no analytic mode) |
| `backend` | Backend class ID or short_code (e.g., `"ionq:aria-1"`) |

### Authentication

The plugin supports three authentication methods, checked in order:

1. **Pre-built SchedulerClient** -- pass `scheduler=` for full control.
2. **Bearer token** -- pass `token=` or set `OPENQUANTUM_TOKEN`.
3. **Client credentials** -- pass `client_id=` + `client_secret=` or set `OPENQUANTUM_CLIENT_ID` + `OPENQUANTUM_CLIENT_SECRET`.

| Parameter | Environment Variable | Description |
|-----------|---------------------|-------------|
| `token` | `OPENQUANTUM_TOKEN` | Bearer token |
| `client_id` | `OPENQUANTUM_CLIENT_ID` | OAuth2 client ID |
| `client_secret` | `OPENQUANTUM_CLIENT_SECRET` | OAuth2 client secret |
| `organization_id` | `OPENQUANTUM_ORGANIZATION_ID` | Organization UUID (optional if single org) |

### Cost Control and Execution Flow

Before executing circuits, the device:

1. **Checks backend status** — reports whether the target QPU is online, its queue depth, and lists online alternatives if it is offline or not accepting jobs.
2. **Shows credit cost** — prepares the first circuit to get a real quote, then displays the per-circuit and total cost.
3. **Prompts for confirmation** (terminal only) — asks `Proceed? [y/N]`. In Jupyter notebooks, execution proceeds immediately (use `auto_confirm=True` or interrupt the kernel to cancel).
4. **Live status updates** — shows a spinner with job status during execution. While the job is pending, you can interrupt (Ctrl+C or Jupyter stop button) to cancel it on the platform. Once the job is queued, it can no longer be cancelled.

| Parameter | Default | Description |
|-----------|---------|-------------|
| `auto_confirm` | `False` | Skip the interactive cost confirmation prompt (terminal) and backend-unavailable warnings. Credits used are still printed to stdout. |

```python
# Default: shows cost, prompts in terminal
dev = qml.device("openquantum.device", wires=2, shots=1024, backend="ionq:aria-1", ...)

# Skip prompts (still prints cost and status)
dev = qml.device("openquantum.device", wires=2, shots=1024, backend="ionq:aria-1", auto_confirm=True, ...)

# Check cumulative credits spent at any time
print(dev.total_credits_used)
```

Example output during execution:
```
[Open Quantum] ● IonQ Aria-1 (ionq:aria-1): Online | queue: 2 job(s)
[Open Quantum] Batch: 1 circuit(s) × 2 credit(s) each = 2 credit(s) total
[Open Quantum] Proceed? [y/N] y
  [1/1] ⠹ Pending... (interrupt to cancel) (3s)
  [1/1] Queued — can no longer be cancelled.
  [1/1] ⠸ Running... (12s)
[Open Quantum] Batch complete: 2 credits used in 18s (session total: 2)
```

Multi-circuit batch:
```
[Open Quantum] Batch: 40 circuit(s) × 2 credit(s) each = 80 credit(s) total
[Open Quantum] Proceed? [y/N] y
  [1/40] done (2/80 credits)
  [2/40] done (4/80 credits)
  ...
[Open Quantum] Batch complete: 80 credits used in 245s (session total: 80)
```

### Execution Plans & Queue Priority

Control cost and scheduling with `execution_plan` and `queue_priority`:

```python
dev = qml.device(
    "openquantum.device",
    wires=2,
    shots=1024,
    backend="ionq:aria-1",
    execution_plan="private",    # "auto", "public", or "private"
    queue_priority="priority",   # "auto", "standard", "priority", or "instant"
    ...
)
```

| Parameter | Values | Default | Description |
|-----------|--------|---------|-------------|
| `execution_plan` | `"auto"`, `"public"`, `"private"` | `"auto"` | `"auto"` selects the cheapest plan (public). `"private"` uses dedicated resources at higher cost. |
| `queue_priority` | `"auto"`, `"standard"`, `"priority"`, `"instant"` | `"auto"` | `"auto"` selects the cheapest priority (standard). Higher priorities cost more but reduce queue wait time. |

### Job Subcategory

The `job_subcategory_id` parameter classifies the type of workload you're running. It defaults to `"oth:oth"` (Other) but **you should set it** to the most specific subcategory that matches your use case. See the [core SDK README](../../README.md) for the full list of subcategory short codes.

```python
dev = qml.device(
    "openquantum.device",
    wires=2,
    shots=1024,
    backend="ionq:aria-1",
    job_subcategory_id="ml:qcl",  # Quantum Classification
    ...
)
```

### Other Optional Parameters

| Parameter | Default | Description |
|-----------|---------|-------------|
| `scheduler_url` | `https://scheduler.openquantum.com` | Scheduler API base URL |
| `management_url` | `https://management.openquantum.com` | Management API base URL |
| `keycloak_base` | `https://id.openquantum.com` | Keycloak auth base URL |
| `realm` | `"platform"` | Keycloak realm |
| `job_timeout_seconds` | `86400` | Max wait for job completion |

## Supported Operations

### Gates

PauliX, PauliY, PauliZ, Hadamard, CNOT, CZ, SWAP, RX, RY, RZ, PhaseShift, S, T, SX, Toffoli, CSWAP, CRX, CRY, CRZ, Identity

### Measurements

- `qml.counts()` - Measurement counts
- `qml.expval()` - Expectation values
- `qml.var()` - Variance
- `qml.probs()` - Probabilities
- `qml.sample()` - Samples

All measurements require finite shots.

## Examples

### Expectation Values

```python
@qml.qnode(dev)
def circuit(x):
    qml.RX(x, wires=0)
    qml.Hadamard(wires=1)
    qml.CNOT(wires=[0, 1])
    return qml.expval(qml.PauliZ(0))

result = circuit(0.543)
```

### VQE Workflow

```python
# Use auto_confirm=True for iterative algorithms to avoid
# being prompted on every optimization step.
dev = qml.device("openquantum.device", wires=2, shots=4096,
                  backend="ionq:aria-1", auto_confirm=True, ...)

@qml.qnode(dev)
def cost_fn(params):
    qml.RY(params[0], wires=0)
    qml.RY(params[1], wires=1)
    qml.CNOT(wires=[0, 1])
    return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1))

opt = qml.GradientDescentOptimizer(stepsize=0.4)
params = np.array([0.5, 0.1])
for _ in range(100):
    params = opt.step(cost_fn, params)

# Check total credits consumed
print(f"Total credits used: {dev.total_credits_used}")
```

## Device Features

The PennyLane device loads the backend's full `constraint_data` (including the `features` array). This enables:

- Early, client-side rejection of constructs the device does not advertise (e.g. `reset`, `barrier`, mid-circuit measurement patterns) during `qml.QNode` execution / `device.preprocess()`.
- The `decompose` step in preprocess is driven by the device's supported gate set (from `native_ops` / `supported_ops`).
- You can query features at runtime:

```python
dev = qml.device("openquantum.device", wires=4, shots=1000, backend="iqm:emerald", ...)
print(dev.features)
print(dev.allows_reset())                 # False for most real QPUs today
print(dev.allows_mid_circuit_measure())
print(dev.supports_feature("dynamic_circuits"))
print(dev.allows_barriers())
```

The platform precompilers (in the worker) perform the definitive validation using the same feature list and will produce clear errors with source locations if something slips through.

See `sdk/docs/device-features.md` for the canonical list and `sdk/docs/capabilities.md` for the schema.

## Comparison with Qiskit Plugin

| Feature | PennyLane | Qiskit |
|---------|-----------|--------|
| Execution plan | `execution_plan=` on device | `execution_plan=` on `create_sampler()` / `create_estimator()` / config dict |
| Queue priority | `queue_priority=` on device | `queue_priority=` on `create_sampler()` / `create_estimator()` / config dict |
| Job subcategory | `job_subcategory_id=` (default `"oth:oth"`) | `job_subcategory_id=` (default `"oth:oth"`) |
| Shots | Required, no default | Default `100` (sampler) / `1024` (backend) |
| Cost confirmation | Interactive prompt (`auto_confirm=False`) | No prompt (submits immediately) |
| Backend status check | Automatic pre-flight check | Not built-in |
| Interrupt handling | Ctrl+C cancels queued jobs | Not built-in |
| Auth | Token or client credentials (env vars or constructor) | Token, client credentials, or saved accounts with keyring |
| Credit tracking | `dev.total_credits_used` | Not built-in |
| QASM format | QASM 3.0 (automatic) | `"qasm2"` or `"qasm3"` (configurable) |

## License

Apache License 2.0
