Skip to content

OpenQuantum support#1169

Open
kuhnboy wants to merge 2 commits intoqBraid:mainfrom
kuhnboy:openquantum
Open

OpenQuantum support#1169
kuhnboy wants to merge 2 commits intoqBraid:mainfrom
kuhnboy:openquantum

Conversation

@kuhnboy
Copy link
Copy Markdown

@kuhnboy kuhnboy commented Apr 28, 2026

Summary of changes

Added OpenQuantumProvider, OpenQuantumDevice, and OpenQuantumJob classes implementing the qBraid runtime interface for Open Quantum

Summary by CodeRabbit

  • New Features
    • Added OpenQuantum provider integration enabling job submission and management to OpenQuantum backends
    • Integrated OpenQuantum backend devices with status tracking and job submission capabilities
    • Implemented OpenQuantum job lifecycle management with status monitoring and result retrieval
    • Enhanced Result objects to dynamically access job detail attributes

@kuhnboy kuhnboy requested a review from ryanhill1 as a code owner April 28, 2026 18:29
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 28, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8bb3925e-f70a-4630-adc4-d077c6108844

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • ✅ Review completed - (🔄 Check again to review again)
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
qbraid/runtime/openquantum/provider.py (1)

205-212: Don't swallow enrichment failures silently.

If the detail fetch starts failing, this quietly drops constraint_data and leaves partially populated profiles with no breadcrumb. Catch the specific client error and log the backend id instead of except Exception: pass.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@qbraid/runtime/openquantum/provider.py` around lines 205 - 212, Replace the
broad except/pass around the get_backend_class_details call with handling for
the specific client error thrown by the session (e.g., catch the session/client
library's specific exception class instead of Exception) and log a warning that
includes the backend id (device_id) and the caught exception; keep falling back
to the basic device data but ensure the log message references
self.session.get_backend_class_details(device_id) and that you still do not
re-raise for non-fatal enrichment failures.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@qbraid/runtime/openquantum/device.py`:
- Around line 146-163: The selection logic in device.py (symbols:
execution_plan_id, queue_priority_id, quote, plan["queue_priorities"],
plan["price"], q["price_increase"]) currently picks the cheapest plan first
which can pick a plan that doesn't offer the requested priority or not minimize
total cost; change it to select a valid plan+priority pair: if execution_plan_id
is provided, find that plan and then validate/choose the priority in
plan["queue_priorities"]; if only queue_priority_id is provided, first filter
quote for plans that contain that queue_priority_id and select among them; if
neither is provided, choose the plan and queue_priority pair that minimizes
total_cost = plan.get("price", 0) + q.get("price_increase", 0); raise ValueError
if a requested execution_plan_id or queue_priority_id cannot be found in the
filtered set.

In `@qbraid/runtime/openquantum/job.py`:
- Around line 79-88: The result() method in OpenQuantumJob accesses
self.device.id which raises when the job was loaded standalone without a device;
update result() (in class OpenQuantumJob / method result) to safely handle
missing device by checking for the attribute or using getattr(self, "device",
None) and only reading .id when device is not None, otherwise set device_id to
an empty string; ensure no other direct uses of self.device occur in result() so
standalone loaded jobs return properly.
- Around line 90-114: The code currently forwards the entire job_data via
enhanced_job_data into Result(...), which can leak transport fields like
output_data_url into Result.details; instead build a whitelist of allowed public
fields (e.g., explicitly pick/assign execution_plan, queue_priority and any
other safe metadata you intend to expose) and pass only those explicit keys to
Result (or set them as explicit keyword args) rather than **enhanced_job_data;
update the area around enhanced_job_data,
execution_plan_reverse/queue_priority_reverse and the Result(...) call to
construct and use a filtered_payload (or explicit kwargs) so sensitive transport
fields are never included.

---

Nitpick comments:
In `@qbraid/runtime/openquantum/provider.py`:
- Around line 205-212: Replace the broad except/pass around the
get_backend_class_details call with handling for the specific client error
thrown by the session (e.g., catch the session/client library's specific
exception class instead of Exception) and log a warning that includes the
backend id (device_id) and the caught exception; keep falling back to the basic
device data but ensure the log message references
self.session.get_backend_class_details(device_id) and that you still do not
re-raise for non-fatal enrichment failures.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3528f8ec-f40a-4a99-a78d-b921f9b7653b

📥 Commits

Reviewing files that changed from the base of the PR and between 88821ae and dca104a.

📒 Files selected for processing (8)
  • CHANGELOG.md
  • qbraid/runtime/__init__.py
  • qbraid/runtime/loader.py
  • qbraid/runtime/openquantum/__init__.py
  • qbraid/runtime/openquantum/device.py
  • qbraid/runtime/openquantum/job.py
  • qbraid/runtime/openquantum/provider.py
  • qbraid/runtime/result.py

Comment on lines +146 to +163
# Plan & priority selection (your logic, slightly cleaner)
if execution_plan_id:
plan = next((p for p in quote if p["execution_plan_id"] == execution_plan_id), None)
if not plan:
raise ValueError(f"Execution plan '{execution_plan_id}' not found in quote.")
else:
plan = min(quote, key=lambda p: p.get("price", float("inf")))

if queue_priority_id:
prio = next(
(q for q in plan["queue_priorities"] if q["queue_priority_id"] == queue_priority_id),
None,
)
if not prio:
raise ValueError(f"Queue priority '{queue_priority_id}' not found in selected plan.")
else:
prio = min(plan["queue_priorities"], key=lambda q: q.get("price_increase", float("inf")))

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Select the cheapest valid plan/priority combination, not the cheapest plan first.

This fallback can overcharge users and can also reject a valid request. If queue_priority_id is set without execution_plan_id, Line 152 may choose a plan that doesn't offer that priority even though another quoted plan does. Even with no inputs, minimizing plan.price before price_increase is not the same as minimizing total cost.

Proposed fix
-        if execution_plan_id:
-            plan = next((p for p in quote if p["execution_plan_id"] == execution_plan_id), None)
-            if not plan:
-                raise ValueError(f"Execution plan '{execution_plan_id}' not found in quote.")
-        else:
-            plan = min(quote, key=lambda p: p.get("price", float("inf")))
-
-        if queue_priority_id:
-            prio = next(
-                (q for q in plan["queue_priorities"] if q["queue_priority_id"] == queue_priority_id),
-                None,
-            )
-            if not prio:
-                raise ValueError(f"Queue priority '{queue_priority_id}' not found in selected plan.")
-        else:
-            prio = min(plan["queue_priorities"], key=lambda q: q.get("price_increase", float("inf")))
+        candidates = []
+        for candidate_plan in quote:
+            if execution_plan_id and candidate_plan["execution_plan_id"] != execution_plan_id:
+                continue
+            for candidate_prio in candidate_plan.get("queue_priorities", []):
+                if queue_priority_id and candidate_prio["queue_priority_id"] != queue_priority_id:
+                    continue
+                total_price = candidate_plan.get("price", float("inf")) + candidate_prio.get(
+                    "price_increase", float("inf")
+                )
+                candidates.append((total_price, candidate_plan, candidate_prio))
+
+        if not candidates:
+            raise ValueError(
+                "No matching execution plan / queue priority combination was returned in the quote."
+            )
+
+        _, plan, prio = min(candidates, key=lambda item: item[0])
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Plan & priority selection (your logic, slightly cleaner)
if execution_plan_id:
plan = next((p for p in quote if p["execution_plan_id"] == execution_plan_id), None)
if not plan:
raise ValueError(f"Execution plan '{execution_plan_id}' not found in quote.")
else:
plan = min(quote, key=lambda p: p.get("price", float("inf")))
if queue_priority_id:
prio = next(
(q for q in plan["queue_priorities"] if q["queue_priority_id"] == queue_priority_id),
None,
)
if not prio:
raise ValueError(f"Queue priority '{queue_priority_id}' not found in selected plan.")
else:
prio = min(plan["queue_priorities"], key=lambda q: q.get("price_increase", float("inf")))
# Plan & priority selection (your logic, slightly cleaner)
candidates = []
for candidate_plan in quote:
if execution_plan_id and candidate_plan["execution_plan_id"] != execution_plan_id:
continue
for candidate_prio in candidate_plan.get("queue_priorities", []):
if queue_priority_id and candidate_prio["queue_priority_id"] != queue_priority_id:
continue
total_price = candidate_plan.get("price", float("inf")) + candidate_prio.get(
"price_increase", float("inf")
)
candidates.append((total_price, candidate_plan, candidate_prio))
if not candidates:
raise ValueError(
"No matching execution plan / queue priority combination was returned in the quote."
)
_, plan, prio = min(candidates, key=lambda item: item[0])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@qbraid/runtime/openquantum/device.py` around lines 146 - 163, The selection
logic in device.py (symbols: execution_plan_id, queue_priority_id, quote,
plan["queue_priorities"], plan["price"], q["price_increase"]) currently picks
the cheapest plan first which can pick a plan that doesn't offer the requested
priority or not minimize total cost; change it to select a valid plan+priority
pair: if execution_plan_id is provided, find that plan and then validate/choose
the priority in plan["queue_priorities"]; if only queue_priority_id is provided,
first filter quote for plans that contain that queue_priority_id and select
among them; if neither is provided, choose the plan and queue_priority pair that
minimizes total_cost = plan.get("price", 0) + q.get("price_increase", 0); raise
ValueError if a requested execution_plan_id or queue_priority_id cannot be found
in the filtered set.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a valid note can you take a look?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ryanhill1, yes, the concern is valid on two counts:

  1. Greedy plan-then-priority fails when only queue_priority_id is specified. The current code picks min(quote, key=lambda p: p.get("price", ...)) first, locking in a plan before checking whether it even contains the requested priority. If the cheapest plan doesn't offer that priority but another plan does, the user gets an unnecessary ValueError.

  2. Independent minimization ≠ minimum total cost. Even with no user constraints, choosing the cheapest base price plan and then the cheapest price increase within it can produce a higher combined cost than a different (plan, priority) pair.

The proposed fix resolves both by:

  • Iterating over all (candidate_plan, candidate_priority) pairs in one pass.
  • Filtering by execution_plan_id and/or queue_priority_id when provided.
  • Selecting the pair that minimizes plan["price"] + prio["price_increase"].
  • Raising a single, clear ValueError if no matching combination exists.

Here's the recommended replacement (same as the suggestion):

        candidates = []
        for candidate_plan in quote:
            if execution_plan_id and candidate_plan["execution_plan_id"] != execution_plan_id:
                continue
            for candidate_prio in candidate_plan.get("queue_priorities", []):
                if queue_priority_id and candidate_prio["queue_priority_id"] != queue_priority_id:
                    continue
                total_price = candidate_plan.get("price", float("inf")) + candidate_prio.get(
                    "price_increase", float("inf")
                )
                candidates.append((total_price, candidate_plan, candidate_prio))

        if not candidates:
            raise ValueError(
                "No matching execution plan / queue priority combination was returned in the quote."
            )

        _, plan, prio = min(candidates, key=lambda item: item[0])

This correctly handles all four cases: both IDs specified, only one specified, or neither specified.

Comment on lines +79 to +88
def result(self) -> Result:
"""Return the result of the OpenQuantum job."""
self.wait_for_final_state()
job_data = self.session.get_job(self.id)
if job_data.get("status", "") != "Completed":
raise QbraidRuntimeError(f"Job failed: {job_data.get('message')}")

results = self.session.download_job_output(self.id)
data = GateModelResultData(measurement_counts=results)
device_id = self.device.id if self.device else ""
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid self.device here when the job was loaded standalone.

QuantumJob.device raises when no device was attached. load_job(..., provider="openquantum") can create an OpenQuantumJob with only a job id/session, so Line 88 will fail before returning the downloaded result.

Proposed fix
-        device_id = self.device.id if self.device else ""
+        device_id = self._device.id if self._device is not None else ""
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@qbraid/runtime/openquantum/job.py` around lines 79 - 88, The result() method
in OpenQuantumJob accesses self.device.id which raises when the job was loaded
standalone without a device; update result() (in class OpenQuantumJob / method
result) to safely handle missing device by checking for the attribute or using
getattr(self, "device", None) and only reading .id when device is not None,
otherwise set device_id to an empty string; ensure no other direct uses of
self.device occur in result() so standalone loaded jobs return properly.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you test instantiating an OpenQauntumJob with no device and see if you can call job.result() without any issue? Or if this needs to be updated to use self._device. Alternatively does the job metadata contain any information about what device it was run on that would allow setting this field without requiring the device object?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check QuantumJob base class - specifically the device property
ast-grep --pattern 'class QuantumJob {
  $$$
}'

rg -n "def device" qbraid/runtime/job.py -A 10

# Check _device attribute in QuantumJob
rg -n "_device" qbraid/runtime/job.py -A 3 -B 1

# Check get_job method in provider.py to see what fields are returned
rg -n "def get_job" qbraid/runtime/openquantum/provider.py -A 20

# Check the full result() method context
cat qbraid/runtime/openquantum/job.py

Length of output: 5965


🏁 Script executed:

#!/bin/bash
# Check create_job and prepare_job to understand job payload/response fields
rg -n "def create_job\|def prepare_job\|backend_class\|device_id\|backendClass" qbraid/runtime/openquantum/provider.py -A 15

# Check if job_data / get_job response ever references a backend or device id
rg -n "backend_class_id\|device_id\|backendClassId" qbraid/runtime/openquantum/ -r

Length of output: 414


🏁 Script executed:

#!/bin/bash
# Check create_job and prepare_job to understand job payload/response fields
rg -n "def create_job|def prepare_job" qbraid/runtime/openquantum/provider.py -A 20

# Check if job_data / get_job response ever references a backend or device id
rg -n "backend_class_id|device_id|backendClassId" qbraid/runtime/openquantum/

Length of output: 2969


@ryanhill1, I can confirm the bug is real. Here's a breakdown:

Why if self.device else "" doesn't work

QuantumJob.device is a property that raises ResourceNotFoundError (not returns None) when no device is attached:

# qbraid/runtime/job.py
`@property`
def device(self) -> qbraid.runtime.QuantumDevice:
    """Returns the qbraid QuantumDevice object associated with this job."""
    if self._device is None:
        raise ResourceNotFoundError("Job does not have an associated device.")
    return self._device

So if self.device else "" never reaches the falsy branch on a standalone-loaded job — it raises instead.


Two possible fixes

Option 1 — Use self._device directly (quick fix):

device_id = self._device.id if self._device is not None else ""

Option 2 — Derive device_id from the job metadata (better):

In device.py Line 118, the submit() method sends "backend_class_id": self.id in the job creation payload:

"backend_class_id": self.id,

The scheduler API likely echoes this field back in the GET /v1/jobs/{job_id} response. If so, job_data already has the device identifier available without needing the device object at all:

device_id = job_data.get("backend_class_id", "")

This would be the most robust approach since it works regardless of whether a device object was attached to the job, and uses the canonical identifier the API already tracks. It's worth verifying that backend_class_id is indeed present in the get_job response from the OpenQuantum scheduler.

Comment thread qbraid/runtime/openquantum/job.py
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 30, 2026

Codecov Report

❌ Patch coverage is 1.57480% with 250 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
qbraid/runtime/openquantum/provider.py 0.00% 138 Missing ⚠️
qbraid/runtime/openquantum/device.py 0.00% 56 Missing ⚠️
qbraid/runtime/openquantum/job.py 0.00% 53 Missing ⚠️
qbraid/runtime/openquantum/__init__.py 0.00% 3 Missing ⚠️

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown
Member

@ryanhill1 ryanhill1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR looks really good!

A few small housekeeping things in addition to my comments:

  • Add unit tests with mocked API responses that satisfy codecov
  • A few remote e2e tests that hit the actual server also good to have but not required
  • Update docs/api/qbraid.runtime.rst with openquantum provider
  • Run tox -e linters && tox -e format-check and fix any errors. arguments-differ, import-outside-toplevel, broad-exception-caught, and too-many-arguments can be ignored inline. But the rest, including cyclic-import will need to be resolved.

Comment on lines +146 to +163
# Plan & priority selection (your logic, slightly cleaner)
if execution_plan_id:
plan = next((p for p in quote if p["execution_plan_id"] == execution_plan_id), None)
if not plan:
raise ValueError(f"Execution plan '{execution_plan_id}' not found in quote.")
else:
plan = min(quote, key=lambda p: p.get("price", float("inf")))

if queue_priority_id:
prio = next(
(q for q in plan["queue_priorities"] if q["queue_priority_id"] == queue_priority_id),
None,
)
if not prio:
raise ValueError(f"Queue priority '{queue_priority_id}' not found in selected plan.")
else:
prio = min(plan["queue_priorities"], key=lambda q: q.get("price_increase", float("inf")))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a valid note can you take a look?

for i, item in enumerate(run_input)
]

# Auto-select organization (your logic — perfect)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Auto-select organization (your logic — perfect)
# Auto-select organization

if not queue_priority_id:
raise ValueError(f"Unknown queue priority: {queue_priority}")

# Plan & priority selection (your logic, slightly cleaner)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Plan & priority selection (your logic, slightly cleaner)
# Plan & priority selection

Comment on lines +79 to +88
def result(self) -> Result:
"""Return the result of the OpenQuantum job."""
self.wait_for_final_state()
job_data = self.session.get_job(self.id)
if job_data.get("status", "") != "Completed":
raise QbraidRuntimeError(f"Job failed: {job_data.get('message')}")

results = self.session.download_job_output(self.id)
data = GateModelResultData(measurement_counts=results)
device_id = self.device.id if self.device else ""
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you test instantiating an OpenQauntumJob with no device and see if you can call job.result() without any issue? Or if this needs to be updated to use self._device. Alternatively does the job metadata contain any information about what device it was run on that would allow setting this field without requiring the device object?

Comment thread qbraid/runtime/result.py
Comment on lines +70 to +75
def __getattr__(self, name: str) -> Any:
"""Allow access to details as attributes."""
if name in self._details:
return self._details[name]
raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure that we want to support retrieving details as attributes because they are not uniform across providers. Could cause confusion. Would rather leave top level Result method/fields/attrs consistent, and then provider-specific fields within result.details and result.data.

Comment thread CHANGELOG.md
Comment on lines +19 to +20
- Added `OpenQuantumProvider`, `OpenQuantumDevice`, and `OpenQuantumJob` classes implementing the qBraid runtime interface for Open Quantum

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you could add a short working example that would be great.

May need to merge in latest from main and move it up to the new "Unreleased" section bc we release v0.12.0 since this PR was originally opened.

)

@cached_method
def get_devices(self, **kwargs) -> list[OpenQuantumDevice]:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def get_devices(self, **kwargs) -> list[OpenQuantumDevice]:
def get_devices(self) -> list[OpenQuantumDevice]:

kwargs unused. can ignore pylint arguments differ.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants