feat(executors): add IonQ Direct API executor (closes #1)#37
Open
manasa-manoj-nbr wants to merge 5 commits into
Open
feat(executors): add IonQ Direct API executor (closes #1)#37manasa-manoj-nbr wants to merge 5 commits into
manasa-manoj-nbr wants to merge 5 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds first-class support for running circuits on IonQ via IonQ’s native REST API, integrates it into the executor factory/public API, and updates docs/dependencies accordingly.
Changes:
- Introduce
IonQExecutor+IonQExecutorConfigwith async execute/poll/cancel/status logic. - Extend
ExecutorFactoryrouting and public exports to support the “IonQ Direct” provider. - Add dependency extras (
ionq) and comprehensive unit tests + README updates.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/test_ionq_executor.py | Adds new unit tests for IonQ Direct executor behavior with stubbed HTTP session |
| tests/test_executors.py | Adds factory routing tests for “IonQ Direct” provider |
| pyproject.toml | Adds ionq extra (requests + qiskit) and includes requests in all |
| marqov/executors/ionq.py | Implements IonQ Direct REST API executor + config |
| marqov/executors/factory.py | Adds factory support for creating IonQExecutor and lists provider as supported |
| marqov/executors/init.py | Exports IonQ executor/config from package |
| README.md | Documents IonQ Direct usage and marks it available in supported backends table |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+374
to
+376
| queue_depth = backend.get("queue_depth") or backend.get("jobs_queued") | ||
| avg_queue = backend.get("average_queue_time") | ||
| queue_time_seconds = int(avg_queue) if avg_queue is not None else None |
Comment on lines
+284
to
+288
| histogram = job.get("data", {}).get("histogram") | ||
| if histogram is None: | ||
| histogram = await self._request("GET", f"/jobs/{job_id}/results") | ||
|
|
||
| counts = self._histogram_to_counts(histogram, shots, num_qubits) |
Comment on lines
+176
to
+188
| Args: | ||
| histogram: Mapping of state index strings to probabilities. | ||
| shots: Number of shots, used to scale probabilities to counts. | ||
| num_qubits: Number of qubits, used to zero-pad bitstrings. | ||
|
|
||
| Counts are allocated with the largest-remainder (Hamilton) method so the | ||
| totals sum exactly to ``shots`` — naive per-bin rounding can drift above or | ||
| below ``shots`` and break downstream "total == shots" assumptions. | ||
|
|
||
| Args: | ||
| histogram: Mapping of state index strings to probabilities. | ||
| shots: Number of shots, used to scale probabilities to counts. | ||
| num_qubits: Number of qubits, used to zero-pad bitstrings. |
Comment on lines
+265
to
+272
| config = IonQExecutorConfig( | ||
| target=backend_config.get("target", backend_slug), | ||
| api_key=backend_config.get("api_key"), | ||
| base_url=backend_config.get("base_url", "https://api.ionq.co/v0.3"), | ||
| poll_interval_seconds=backend_config.get("poll_interval_seconds", 1.0), | ||
| timeout_seconds=backend_config.get("timeout_seconds"), | ||
| noise_model=backend_config.get("noise_model"), | ||
| ) |
Comment on lines
+333
to
+346
| loop = asyncio.get_running_loop() | ||
| session = await loop.run_in_executor(None, self._get_session_sync) | ||
| url = f"{self.config.base_url}{path}" | ||
| headers = self._auth_headers() | ||
|
|
||
| def _call() -> dict[str, Any]: | ||
| response = session.request( | ||
| method, url, headers=headers, timeout=_HTTP_TIMEOUT_SECONDS, **kwargs | ||
| ) | ||
| response.raise_for_status() | ||
| data: dict[str, Any] = response.json() | ||
| return data | ||
|
|
||
| return await loop.run_in_executor(None, _call) |
Author
|
@ddri, could you please review this when you have a chance? Thank you! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #1
Adds a native IonQ Direct executor that runs circuits straight against IonQ's REST API, bypassing the AWS Braket intermediary — no AWS account or S3 bucket required. Circuits are converted with
circuit.to_qiskit()then dumped to OpenQASM and submitted using IonQ'sqasminput format.execute()submits, polls for completion, and decodes IonQ's probability histogram into measurement counts.Type of change
Testing
pytest tests/ -vand tests passTest details:
tests/test_ionq_executor.py(23 tests) injects a fake HTTP session — no network or credentials needed. Covers:execute()happy path, payload usesqasminput format, job polling, noise model, job-failure ->RuntimeError, histogram→counts decoding, API-key resolution,cancel()(success/failure), andget_status()mapping (online/offline/maintenance/error).tests/test_executors.py("IonQ Direct"supported + builds anIonQExecutor).ruffandmypy(strict) clean on the new code.@pytest.mark.integrationtemplate is included for running against the live IonQ simulator viaIONQ_API_KEY(not run in CI).Checklist
IONQ_API_KEYenv)CONTRIBUTING.md §1(conversion reuses theSDK's existing
to_qiskit()path, so the canonical set is preserved)For new executors only:
ExecutorFactoryperCONTRIBUTING.md §3(provider string"IonQ Direct",_create_ionq_executor(), listed inget_supported_providers(), exported, new optional[ionq]extra)get_status()returns device-level availability ("online"/"offline"/"maintenance") by queryingGET /backends/{target}— not job-level status (perCONTRIBUTING.md §2)Note on the official
ionqclientThe issue suggested the official
ionqPython client. Its only release (0.0.0a15, pre-release) pinspydantic<2, which is unresolvable against marqov's corepydantic>=2requirement —pip install marqov[ionq]fails with ResolutionImpossible. Transport therefore usesrequests` (the same HTTP library the official client uses internally) to call IonQ's REST API directly. This is documented in the executor's module docstring; happy to migrate to the official client once it supports pydantic 2.AI Disclosure
I used Claude to help implement and debug this PR: studying the existing executor patterns (Braket/IBM/Azure), scaffolding the IonQ executor and mocked tests, and diagnosing the
ionqclient'spydantic<2conflict with marqov'spydantic>=2(which is why transport usesrequestsdirectly). I reviewed all generated changes, ranruff/mypy/pytestlocally, and verified the executor end-to-end against a mocked IonQ API before submitting (no live API key, so the live path is untested).