Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),

### Added

- `examples/` with a Hamiltonian-energy Quantum Function workload and client-side parameter optimization.
- `QctrlQaoaJobCreationPayload` and `QctrlQaoaJobInput` for submitting Q-CTRL QAOA maxcut combinatorial-optimization jobs via `create_job`. The `create_job` body union now also accepts `QctrlQaoaJobCreationPayload`.
- `cost_model` optional field on `BaseJob`, `GetCircuitJobResponse`, and `GetJobResponse`, typed as `CostModel` (`"quantum_compute_time"` or `"execution_time"`).

Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ Each generated endpoint module exposes four callables: `sync`, `sync_detailed`,

For options (`api_key`, `base_url`, `max_retries`, `timeout`, `extension`), error classes, retry behavior, pagination, polling, sessions, and downstream-SDK extension hooks, see the [API reference](https://ionq.github.io/ionq-core-python/).

## Examples

See [examples/README.md](examples/README.md) for runnable examples, including a Hamiltonian-energy Quantum Function workload with client-side parameter optimization.

## Versioning

This package follows [SemVer 2.0](https://semver.org/spec/v2.0.0.html), independent of the upstream REST API version - pass an explicit `base_url` to `IonQClient` to pin against a different API. Print the installed version with:
Expand Down
20 changes: 20 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# ionq-core Examples

## Hamiltonian Energy Optimization

`hamiltonian_energy_optimization.py` demonstrates a Hamiltonian-energy Quantum Function using only the public `ionq-core` API:

- `IonQClient`
- `create_job.sync`
- `wait_for_job`
- typed models under `ionq_core.models`

It minimizes the one-qubit Hamiltonian `H = -Z` with a parameterized OpenQASM 3 `RY(theta)` ansatz and a small dependency-free coordinate search optimizer.

```sh
pip install ionq-core
export IONQ_API_KEY=...
python examples/hamiltonian_energy_optimization.py --iterations 4
```

The script targets the free `simulator` backend by default and prints each optimization iteration plus the final energy and parameters.
4 changes: 4 additions & 0 deletions examples/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# SPDX-FileCopyrightText: 2026 IonQ, Inc.
# SPDX-License-Identifier: Apache-2.0

"""Runnable ionq-core examples."""
189 changes: 189 additions & 0 deletions examples/hamiltonian_energy_optimization.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
# SPDX-FileCopyrightText: 2026 IonQ, Inc.
# SPDX-License-Identifier: Apache-2.0

"""Hamiltonian-energy Quantum Function example with client-side optimization.

This example minimizes the one-qubit Hamiltonian ``H = -Z`` with a
parameterized OpenQASM 3 ansatz, ``RY(theta) |0>``. The optimizer is a small
dependency-free coordinate search that repeatedly submits Hamiltonian-energy
Quantum Function jobs through `ionq-core`.
"""

from __future__ import annotations

import argparse
from collections.abc import Callable, Sequence
from dataclasses import dataclass
from typing import Any

from ionq_core import AuthenticatedClient, IonQClient, wait_for_job
from ionq_core.api.default import create_job
from ionq_core.models.ansatz import Ansatz
from ionq_core.models.hamiltonian_energy_data import HamiltonianEnergyData
from ionq_core.models.hamiltonian_energy_input import HamiltonianEnergyInput
from ionq_core.models.hamiltonian_energy_input_data import HamiltonianEnergyInputData
from ionq_core.models.hamiltonian_pauli_term import HamiltonianPauliTerm
from ionq_core.models.quantum_function_job_creation_payload import QuantumFunctionJobCreationPayload

ANSATZ_OPENQASM = """OPENQASM 3.0;
input float theta;
qubit[1] q;
ry(theta) q[0];
"""


@dataclass(frozen=True)
class OptimizationResult:
"""Result from the client-side coordinate-search loop."""

parameters: list[float]
energy: float
history: list[tuple[int, list[float], float]]


def build_hamiltonian_energy_payload(
params: Sequence[float],
*,
backend: str = "simulator",
shots: int = 100,
name: str = "ionq-core hamiltonian energy example",
) -> QuantumFunctionJobCreationPayload:
"""Build a typed Hamiltonian-energy Quantum Function payload."""
return QuantumFunctionJobCreationPayload(
backend=backend,
type_="quantum-function",
input_=HamiltonianEnergyInput(
data=HamiltonianEnergyInputData(
type_="hamiltonian-energy",
data=HamiltonianEnergyData(
hamiltonian=[HamiltonianPauliTerm(pauli_string="Z", coefficient=-1.0)],
ansatz=Ansatz(data=ANSATZ_OPENQASM),
),
),
params=list(params),
),
name=name,
shots=shots,
)


def evaluate_energy(
client: AuthenticatedClient,
params: Sequence[float],
*,
backend: str = "simulator",
shots: int = 100,
poll_interval: float = 1.0,
timeout: float = 300.0,
create_job_sync: Callable[..., Any] = create_job.sync,
wait_for_job_fn: Callable[..., Any] = wait_for_job,
) -> float:
"""Submit a Hamiltonian-energy job and return its completed energy."""
payload = build_hamiltonian_energy_payload(params, backend=backend, shots=shots)
created = create_job_sync(client=client, body=payload)
if created is None:
raise RuntimeError("IonQ API did not return a job creation response")

completed = wait_for_job_fn(client, created.id, poll_interval=poll_interval, timeout=timeout)
return extract_energy(completed)


def extract_energy(completed_job: Any) -> float:
"""Extract an energy value from a completed Quantum Function job response."""
output = completed_job.output.to_dict()
candidates = [output.get("energy"), output.get("value")]

result = output.get("result")
if isinstance(result, dict):
candidates.append(result.get("energy"))

solution = output.get("solution")
if isinstance(solution, dict):
candidates.append(solution.get("minimum_value"))

for candidate in candidates:
if isinstance(candidate, (int, float)):
return float(candidate)

raise KeyError("completed job output did not contain an energy value")


def coordinate_search(
energy_fn: Callable[[list[float]], float],
initial_params: Sequence[float],
*,
step_size: float = 0.4,
min_step: float = 0.025,
max_iterations: int = 8,
log: Callable[[str], None] = print,
) -> OptimizationResult:
"""Minimize ``energy_fn`` with a small deterministic coordinate search."""
params = list(initial_params)
energy = energy_fn(params)
history = [(0, params.copy(), energy)]
log(f"iteration 0: energy={energy:.12g}, params={params}")

step = step_size
for iteration in range(1, max_iterations + 1):
improved = False

for index in range(len(params)):
for direction in (1.0, -1.0):
candidate = params.copy()
candidate[index] += direction * step
candidate_energy = energy_fn(candidate)
if candidate_energy < energy:
params = candidate
energy = candidate_energy
improved = True

if improved:
history.append((iteration, params.copy(), energy))
else:
step *= 0.5

log(f"iteration {iteration}: energy={energy:.12g}, params={params}, step={step:.12g}")

if not improved and step < min_step:
break

return OptimizationResult(parameters=params, energy=energy, history=history)


def main(argv: Sequence[str] | None = None) -> int:
"""Run the Hamiltonian-energy example from the command line."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--backend", default="simulator", help="IonQ backend to target")
parser.add_argument("--shots", type=int, default=100, help="Shots per energy evaluation")
parser.add_argument("--initial-theta", type=float, default=1.2, help="Initial RY angle")
parser.add_argument("--iterations", type=int, default=4, help="Maximum coordinate-search iterations")
parser.add_argument("--step", type=float, default=0.4, help="Initial coordinate-search step")
parser.add_argument("--poll-interval", type=float, default=1.0, help="Initial polling interval in seconds")
parser.add_argument("--timeout", type=float, default=300.0, help="Polling timeout per submitted job")
args = parser.parse_args(argv)

client = IonQClient()

def energy_fn(params: list[float]) -> float:
return evaluate_energy(
client,
params,
backend=args.backend,
shots=args.shots,
poll_interval=args.poll_interval,
timeout=args.timeout,
)

result = coordinate_search(
energy_fn,
[args.initial_theta],
step_size=args.step,
max_iterations=args.iterations,
)
print(f"final_energy={result.energy:.12g}")
print(f"optimal_parameters={result.parameters}")
return 0


if __name__ == "__main__":
raise SystemExit(main())
86 changes: 86 additions & 0 deletions tests/test_examples.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# SPDX-FileCopyrightText: 2026 IonQ, Inc.
# SPDX-License-Identifier: Apache-2.0

import pytest

from examples import hamiltonian_energy_optimization as example


def test_hamiltonian_energy_payload_uses_typed_models():
payload = example.build_hamiltonian_energy_payload([0.25], shots=200)

assert payload.to_dict() == {
"backend": "simulator",
"type": "quantum-function",
"input": {
"data": {
"type": "hamiltonian-energy",
"data": {
"hamiltonian": [{"pauli_string": "Z", "coefficient": -1.0}],
"ansatz": {"data": example.ANSATZ_OPENQASM},
"penalty": 0.0,
},
},
"params": [0.25],
},
"name": "ionq-core hamiltonian energy example",
"shots": 200,
}


def test_evaluate_energy_submits_payload_and_polls_job(auth_client):
calls = []

class CreatedJob:
id = "job-123"

class CompletedOutput:
def to_dict(self):
return {"energy": -0.75}

class CompletedJob:
output = CompletedOutput()

def fake_create_job(*, client, body):
calls.append(("create", client, body.to_dict()))
return CreatedJob()

def fake_wait_for_job(client, job_id, *, poll_interval, timeout):
calls.append(("wait", client, job_id, poll_interval, timeout))
return CompletedJob()

energy = example.evaluate_energy(
auth_client,
[0.5],
create_job_sync=fake_create_job,
wait_for_job_fn=fake_wait_for_job,
poll_interval=0.25,
timeout=12.0,
)

assert energy == -0.75
assert calls[0][0] == "create"
assert calls[0][1] is auth_client
assert calls[0][2]["input"]["params"] == [0.5]
assert calls[1] == ("wait", auth_client, "job-123", 0.25, 12.0)


def test_coordinate_search_reports_and_lowers_energy():
messages = []

def bowl(params):
return (params[0] - 0.25) ** 2

result = example.coordinate_search(
bowl,
[1.25],
step_size=0.5,
min_step=0.125,
max_iterations=4,
log=messages.append,
)

assert result.energy == pytest.approx(0.0)
assert result.parameters == pytest.approx([0.25])
assert result.history[-1] == (2, [0.25], 0.0)
assert any(message.startswith("iteration 2:") for message in messages)