Skip to content

Commit 1e03dff

Browse files
authored
feat: ability to show incomplete job items (#271)
1 parent 0c593b7 commit 1e03dff

File tree

13 files changed

+257
-92
lines changed

13 files changed

+257
-92
lines changed

.cz.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
name = "cz_customize"
33
tag_format = "v$version"
44
version_scheme = "semver"
5-
version = "0.33.0"
5+
version = "0.34.0"
66
version_files = ["pyproject.toml"]
77
update_changelog_on_bump = true
88
major_version_zero = true

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ __pycache__/
1414

1515
dist/
1616
docs/_build
17+
.coverage
1718

1819
# IPython
1920
.ipynb_checkpoints

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22

33
# `qnexus` Release Notes
44

5+
## v0.34.0 (2025-09-30)
6+
7+
8+
### Added
9+
10+
- Return `IncompleteJobItemRef` for job items that haven't completed when fetching job results.
11+
12+
513
## v0.33.0 (2025-09-29)
614

715
### Removed

examples/basics/jobs_results.ipynb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,9 @@
194194
"\n",
195195
"By default, you can only retrieve results for jobs with the COMPLETED status (meaning that all items in the Job have successfully completed). \n",
196196
"\n",
197-
"In some contexts you may want to retrieve results for the completed items in an otherwise pending or errored job. For example, maybe you have submitted 10 circuits to be executed on quantum hardware, but only 6 of them have completed before you ran out of credit quota for that device. In this case you can still get the results from the completed items."
197+
"In some contexts you may want to retrieve results for the completed items in an otherwise pending or errored job. For example, maybe you have submitted 10 circuits to be executed on quantum hardware, but only 6 of them have completed before you ran out of credit quota for that device. In this case you can still get the results from the completed items.\n",
198+
"\n",
199+
"Any items that have not completed will be returned as an `IncompleteJobItemRef` with an individual status."
198200
]
199201
},
200202
{

integration/test_backend_configs.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@
77

88
import qnexus as qnx
99
from qnexus.models.job_status import JobStatusEnum
10-
from qnexus.models.references import ProjectRef
10+
from qnexus.models.references import (
11+
CompilationResultRef,
12+
ExecutionResultRef,
13+
ProjectRef,
14+
)
1115

1216
CONFIGS_REQUIRE_NO_MEASURE = [qnx.AerUnitaryConfig]
1317
CONFIGS_NOT_TO_EXECUTE = [
@@ -53,7 +57,11 @@ def test_basic_backend_config_usage(
5357
).backend_config.model_dump(exclude={"noisy_simulation"})
5458

5559
execute_job_ref = qnx.start_execute_job(
56-
programs=[item.get_output() for item in qnx.jobs.results(compile_job_ref)],
60+
programs=[
61+
item.get_output()
62+
for item in qnx.jobs.results(compile_job_ref)
63+
if isinstance(item, CompilationResultRef)
64+
],
5765
name=f"execute job for {test_case_name}",
5866
n_shots=[100],
5967
backend_config=backend_config,
@@ -71,4 +79,5 @@ def test_basic_backend_config_usage(
7179
execute_job_result_refs = qnx.jobs.results(execute_job_ref)
7280

7381
for result_ref in execute_job_result_refs:
82+
assert isinstance(result_ref, ExecutionResultRef)
7483
assert isinstance(result_ref.download_result(), BackendResult)

integration/test_jobs.py

Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from datetime import datetime
55
from time import sleep
66
from typing import Any, Callable, ContextManager
7+
from uuid import UUID
78

89
import pandas as pd
910
import pytest
@@ -22,6 +23,8 @@
2223
CompilationResultRef,
2324
CompileJobRef,
2425
ExecuteJobRef,
26+
ExecutionResultRef,
27+
IncompleteJobItemRef,
2528
JobRef,
2629
Ref,
2730
)
@@ -205,13 +208,14 @@ def test_submit_compile(
205208
compile_results = qnx.jobs.results(compile_job_ref)
206209

207210
assert len(compile_results) == 1
208-
assert isinstance(compile_results[0], CompilationResultRef)
209-
test_ref_serialisation("compile_result", compile_results[0])
211+
compilation_result = compile_results[0]
212+
assert isinstance(compilation_result, CompilationResultRef)
213+
test_ref_serialisation("compile_result", compilation_result)
210214

211-
assert isinstance(compile_results[0].get_input(), CircuitRef)
212-
assert isinstance(compile_results[0].get_output(), CircuitRef)
215+
assert isinstance(compilation_result.get_input(), CircuitRef)
216+
assert isinstance(compilation_result.get_output(), CircuitRef)
213217

214-
first_pass_data = compile_results[0].get_passes()[0]
218+
first_pass_data = compilation_result.get_passes()[0]
215219

216220
assert isinstance(first_pass_data, CompilationPassRef)
217221
assert isinstance(first_pass_data.get_input(), CircuitRef)
@@ -287,11 +291,25 @@ def test_get_results_for_incomplete_compile(
287291
project=proj_ref,
288292
backend_config=qnx.AerConfig(),
289293
)
290-
294+
# check the job while not complete
291295
assert isinstance(compile_job_ref, CompileJobRef)
292296
assert qnx.jobs.status(compile_job_ref).status != JobStatusEnum.COMPLETED
297+
298+
with pytest.raises(qnx_exc.ResourceFetchFailed):
299+
qnx.jobs.results(compile_job_ref)
300+
293301
compile_results = qnx.jobs.results(compile_job_ref, allow_incomplete=True)
294-
assert len(compile_results) == 0
302+
assert len(compile_results) == 1
303+
compile_item = compile_results[0]
304+
assert isinstance(compile_item, IncompleteJobItemRef)
305+
assert isinstance(compile_item.job_item_integer_id, int)
306+
assert compile_item.last_status != JobStatusEnum.COMPLETED
307+
308+
# check the job after completion
309+
qnx.jobs.wait_for(compile_job_ref)
310+
complete_results = qnx.jobs.results(compile_job_ref, allow_incomplete=True)
311+
assert len(complete_results) == 1
312+
assert isinstance(complete_results[0], CompilationResultRef)
295313

296314

297315
def test_compile_hypertket(
@@ -348,6 +366,7 @@ def test_submit_execute(
348366
assert len(execute_results) == 1
349367

350368
first_result = execute_results[0]
369+
assert isinstance(first_result, ExecutionResultRef)
351370
assert isinstance(first_result.get_input(), CircuitRef)
352371
assert isinstance(first_result.download_result(), BackendResult)
353372
assert isinstance(first_result.download_backend_info(), BackendInfo)
@@ -432,23 +451,32 @@ def test_get_results_for_incomplete_execute(
432451
with pytest.raises(qnx_exc.JobError):
433452
qnx.jobs.wait_for(execute_job_ref)
434453

454+
with pytest.raises(qnx_exc.ResourceFetchFailed):
455+
qnx.jobs.results(execute_job_ref)
456+
435457
incomplete_results = qnx.jobs.results(execute_job_ref, allow_incomplete=True)
436458

437459
# wait for the ZZPhase circuit execution to complete
438460
for _ in range(10):
439461
incomplete_results = qnx.jobs.results(
440462
execute_job_ref, allow_incomplete=True
441463
)
442-
if len(incomplete_results) > 0:
464+
if any(isinstance(r, ExecutionResultRef) for r in incomplete_results):
443465
break
444466
sleep(10)
445467

446468
# we expect the CX circuit to fail on H1-1LE, but the ZZPhase circuit should succeed
447-
assert len(incomplete_results) == 1
469+
assert len(incomplete_results) == 2
470+
first_item, second_item = incomplete_results[0], incomplete_results[1]
471+
assert isinstance(first_item, IncompleteJobItemRef)
472+
assert first_item.id == UUID(int=0)
473+
assert isinstance(first_item.job_item_integer_id, int)
474+
assert first_item.last_status == JobStatusEnum.ERROR
448475

449-
assert isinstance(incomplete_results[0].get_input(), CircuitRef)
450-
assert isinstance(incomplete_results[0].download_result(), BackendResult)
451-
assert isinstance(incomplete_results[0].download_backend_info(), BackendInfo)
476+
assert isinstance(second_item, ExecutionResultRef)
477+
assert isinstance(second_item.get_input(), CircuitRef)
478+
assert isinstance(second_item.download_result(), BackendResult)
479+
assert isinstance(second_item.download_backend_info(), BackendInfo)
452480

453481

454482
def test_wait_for_raises_on_job_error(
@@ -523,12 +551,13 @@ def test_results_not_available_error(
523551
execute_results = qnx.jobs.results(execute_job_ref)
524552

525553
assert len(execute_results) == 1
554+
execution_result = execute_results[0]
555+
assert isinstance(execution_result, ExecutionResultRef)
556+
assert isinstance(execution_result.get_input(), CircuitRef)
526557

527-
assert isinstance(execute_results[0].get_input(), CircuitRef)
528-
529-
assert isinstance(execute_results[0].download_result(), BackendResult)
558+
assert isinstance(execution_result.download_result(), BackendResult)
530559

531-
assert isinstance(execute_results[0].download_backend_info(), BackendInfo)
560+
assert isinstance(execution_result.download_backend_info(), BackendInfo)
532561

533562

534563
def test_submit_under_user_group(

integration/test_qir.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,14 @@
1313

1414
import qnexus as qnx
1515
from qnexus.models.annotations import PropertiesDict
16-
from qnexus.models.references import ProjectRef, QIRRef, QIRResult, Ref, ResultVersions
16+
from qnexus.models.references import (
17+
ExecutionResultRef,
18+
ProjectRef,
19+
QIRRef,
20+
QIRResult,
21+
Ref,
22+
ResultVersions,
23+
)
1724

1825

1926
def test_qir_create_and_update(
@@ -156,12 +163,16 @@ def test_execution(
156163
assert len(results) == 1
157164
result_ref = results[0]
158165

166+
assert isinstance(result_ref, ExecutionResultRef)
159167
assert isinstance(result_ref.download_backend_info(), BackendInfo)
160168
assert isinstance(result_ref.get_input(), QIRRef)
161169

162170
assert result_ref.get_input().id == qir_program_ref.id
163171

164-
qir_result = qnx.jobs.results(job_ref)[0].download_result()
172+
qir_result_ref = qnx.jobs.results(job_ref)[0]
173+
174+
assert isinstance(qir_result_ref, ExecutionResultRef)
175+
qir_result = qir_result_ref.download_result()
165176
assert isinstance(qir_result, BackendResult)
166177
assert qir_result.get_counts() == Counter({(0, 0, 0): 10})
167178
assert qir_result.get_bitlist() == [Bit("c", 2), Bit("c", 1), Bit("c", 0)]
@@ -193,17 +204,17 @@ def test_execution_on_NG_devices(
193204

194205
qnx.jobs.wait_for(job_ref)
195206

196-
results = qnx.jobs.results(job_ref)[0].download_result()
207+
result_ref = qnx.jobs.results(job_ref)[0]
208+
assert isinstance(result_ref, ExecutionResultRef)
209+
results = result_ref.download_result()
197210
# Assert this is a QIR compliant result
198211
assert isinstance(results, QIRResult)
199212
escaped_results = results.results.encode("unicode_escape").decode()
200213
assert "HEADER\\tschema_id\\tlabeled" in escaped_results
201214
# Can't assert the value is the same, so just check the output is there
202215
assert "OUTPUT\\tTUPLE\\t2\\tt0" in escaped_results
203216

204-
v4_results = qnx.jobs.results(job_ref)[0].download_result(
205-
version=ResultVersions.RAW
206-
)
217+
v4_results = result_ref.download_result(version=ResultVersions.RAW)
207218
# Assert this is in v4 format
208219
assert isinstance(v4_results, QsysResult)
209220
assert v4_results.results[0].entries[0][0] == "USER:QIRTUPLE:t0"

integration/test_qsys_jobs.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@
1111
from quantinuum_schemas.models.backend_config import BasicEmulatorConfig
1212

1313
import qnexus as qnx
14-
from qnexus.models.references import HUGRRef, ProjectRef, ResultVersions
14+
from qnexus.models.references import (
15+
ExecutionResultRef,
16+
HUGRRef,
17+
ProjectRef,
18+
ResultVersions,
19+
)
1520

1621

1722
def prepare_teleportation() -> Any:
@@ -77,6 +82,7 @@ def test_guppy_execution(
7782
assert len(results) == 1
7883
result_ref = results[0]
7984

85+
assert isinstance(result_ref, ExecutionResultRef)
8086
assert isinstance(result_ref.download_backend_info(), BackendInfo)
8187
assert isinstance(result_ref.get_input(), HUGRRef)
8288

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "qnexus"
3-
version = "0.33.0"
3+
version = "0.34.0"
44
description = "Quantinuum Nexus python client."
55
authors = [
66
{name = "Vanya Eccles", email = "vanya.eccles@quantinuum.com"},

qnexus/client/jobs/__init__.py

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
ExecutionResult,
5454
ExecutionResultRef,
5555
GpuDecoderConfigRef,
56+
IncompleteJobItemRef,
5657
JobRef,
5758
JobType,
5859
ProjectRef,
@@ -443,19 +444,22 @@ def _process_exception(exc: Exception) -> Exception | None:
443444
@overload
444445
def results(
445446
job: CompileJobRef, allow_incomplete: bool = False
446-
) -> DataframableList[CompilationResultRef]: ...
447+
) -> DataframableList[CompilationResultRef | IncompleteJobItemRef]: ...
447448

448449

449450
@overload
450451
def results(
451452
job: ExecuteJobRef, allow_incomplete: bool = False
452-
) -> DataframableList[ExecutionResultRef]: ...
453+
) -> DataframableList[ExecutionResultRef | IncompleteJobItemRef]: ...
453454

454455

455456
def results(
456457
job: CompileJobRef | ExecuteJobRef,
457458
allow_incomplete: bool = False,
458-
) -> DataframableList[CompilationResultRef] | DataframableList[ExecutionResultRef]:
459+
) -> (
460+
DataframableList[CompilationResultRef | IncompleteJobItemRef]
461+
| DataframableList[ExecutionResultRef | IncompleteJobItemRef]
462+
):
459463
"""Get the ResultRefs from a JobRef, if the job is complete.
460464
To enable fetching results from Jobs with incomplete items, set allow_incomplete=True.
461465
"""
@@ -557,9 +561,18 @@ def compile(
557561

558562
compile_results = results(compile_job_ref)
559563

560-
return DataframableList(
561-
[compile_result.get_output() for compile_result in compile_results]
562-
)
564+
compiled_circuits: list[CircuitRef] = []
565+
for compile_result in compile_results:
566+
if isinstance(compile_result, CompilationResultRef):
567+
compiled_circuits.append(compile_result.get_output())
568+
elif isinstance(compile_result, IncompleteJobItemRef):
569+
raise qnx_exc.ResourceFetchFailed(
570+
f"Compile job item {compile_result.job_item_integer_id} is in status {compile_result.last_status}"
571+
)
572+
else:
573+
assert_never(compile_result)
574+
575+
return DataframableList(compiled_circuits)
563576

564577

565578
@accept_circuits_for_programs
@@ -607,7 +620,18 @@ def execute(
607620

608621
execute_results = results(execute_job_ref)
609622

610-
return [result.download_result() for result in execute_results]
623+
ex_results: list[ExecutionResult] = []
624+
for result in execute_results:
625+
if isinstance(result, ExecutionResultRef):
626+
ex_results.append(result.download_result())
627+
elif isinstance(result, IncompleteJobItemRef):
628+
raise qnx_exc.ResourceFetchFailed(
629+
f"Compile job item {result.job_item_integer_id} is in status {result.last_status}"
630+
)
631+
else:
632+
assert_never(result)
633+
634+
return ex_results
611635

612636

613637
def cost(job: CompileJobRef | ExecuteJobRef) -> float:

0 commit comments

Comments
 (0)