1- # Copyright 2024 Scaleway
1+ # Copyright 2025 Scaleway
22#
33# Licensed under the Apache License, Version 2.0 (the "License");
44# you may not use this file except in compliance with the License.
1212# See the License for the specific language governing permissions and
1313# limitations under the License.
1414import time
15- import zlib
1615import httpx
17- import json
18- import numpy as np
1916import randomname
2017
2118from typing import List , Union , Optional , Dict
2219
23- from qiskit import qasm3 , QuantumCircuit
20+ from qiskit import QuantumCircuit
2421from qiskit .result import Result
2522from qiskit .providers import JobV1
2623from qiskit .providers import JobError , JobTimeoutError , JobStatus
2724
2825from qiskit_scaleway .versions import USER_AGENT
2926
27+ from qio .core import (
28+ QuantumProgram ,
29+ QuantumProgramResult ,
30+ QuantumComputationModel ,
31+ QuantumComputationParameters ,
32+ QuantumNoiseModel ,
33+ BackendData ,
34+ ClientData ,
35+ )
36+
3037from scaleway_qaas_client .v1alpha1 import (
3138 QaaSClient ,
3239 QaaSJobResult ,
33- QaaSJobData ,
34- QaaSJobClientData ,
35- QaaSCircuitData ,
36- QaaSJobRunData ,
37- QaaSJobBackendData ,
38- QaaSCircuitSerializationFormat ,
39- QaaSNoiseModelData ,
40- QaaSNoiseModelSerializationFormat ,
4140)
4241
4342
@@ -55,6 +54,7 @@ def __init__(
5554 self ._client = client
5655 self ._circuits = circuits
5756 self ._config = config
57+ self ._last_progress_message = ""
5858
5959 @property
6060 def name (self ):
@@ -69,6 +69,9 @@ def status(self) -> JobStatus:
6969 "completed" : JobStatus .DONE ,
7070 }
7171
72+ if job .progress_message is not None :
73+ self ._last_progress_message = job .progress_message
74+
7275 return status_mapping .get (job .status , JobStatus .ERROR )
7376
7477 def submit (self , session_id : str ) -> None :
@@ -78,69 +81,46 @@ def submit(self, session_id: str) -> None:
7881 options = self ._config .copy ()
7982 shots = options .pop ("shots" )
8083
81- run_data = QaaSJobRunData (
82- options = {
83- "shots" : shots ,
84- "memory" : options .pop ("memory" , False ),
85- },
86- circuits = list (
87- map (
88- lambda c : QaaSCircuitData (
89- serialization_format = QaaSCircuitSerializationFormat .QASM_V3 ,
90- circuit_serialization = qasm3 .dumps (c ),
91- ),
92- self ._circuits ,
93- )
94- ),
95- )
84+ programs = map (lambda c : QuantumProgram .from_qiskit_circuit (c ), self ._circuits )
9685
9786 noise_model = options .pop ("noise_model" , None )
9887 if noise_model :
99- noise_model_dict = _encode_numpy_complex (noise_model .to_dict (False ))
100- noise_model = QaaSNoiseModelData (
101- serialization_format = QaaSNoiseModelSerializationFormat .AER_COMPRESSED_JSON ,
102- noise_model_serialization = zlib .compress (
103- json .dumps (noise_model_dict ).encode ()
104- ),
105- )
106- ### Uncomment to use standard JSON serialization, provided there is no more issue with AER deserialization logic
107- # noise_model = QaaSNoiseModelData(
108- # serialization_format = QaaSNoiseModelSerializationFormat.JSON,
109- # noise_model_serialization = json.dumps(noise_model.to_dict(True)).encode()
110- # )
111- ###
112-
113- backend_data = QaaSJobBackendData (
88+ noise_model = QuantumNoiseModel .from_qiskit_aer_noise_model (noise_model )
89+
90+ backend_data = BackendData (
11491 name = self .backend ().name ,
11592 version = self .backend ().version ,
11693 options = options ,
11794 )
11895
119- client_data = QaaSJobClientData (
96+ client_data = ClientData (
12097 user_agent = USER_AGENT ,
12198 )
12299
123- data = QaaSJobData .schema ().dumps (
124- QaaSJobData (
125- backend = backend_data ,
126- run = run_data ,
127- client = client_data ,
128- noise_model = noise_model ,
129- )
130- )
100+ computation_model_json = QuantumComputationModel (
101+ programs = programs ,
102+ backend = backend_data ,
103+ client = client_data ,
104+ noise_model = noise_model ,
105+ ).to_json_str ()
106+
107+ computation_parameters_json = QuantumComputationParameters (
108+ shots = shots ,
109+ ).to_json_str ()
131110
132111 model = self ._client .create_model (
133- payload = data ,
112+ payload = computation_model_json ,
134113 )
135114
136115 if not model :
137116 raise RuntimeError ("Failed to push circuit data" )
138117
118+ self ._last_progress_message = ""
139119 self ._job_id = self ._client .create_job (
140120 name = self ._name ,
141121 session_id = session_id ,
142122 model_id = model .id ,
143- parameters = { "shots" : shots } ,
123+ parameters = computation_parameters_json ,
144124 ).id
145125
146126 def result (
@@ -151,37 +131,26 @@ def result(
151131
152132 job_results = self ._wait_for_result (timeout , fetch_interval )
153133
154- def __make_result_from_payload (payload : str ) -> Result :
155- payload_dict = json .loads (payload )
156-
157- return Result .from_dict (
158- {
159- "results" : payload_dict ["results" ],
160- "backend_name" : self .backend ().name ,
161- "backend_version" : self .backend ().version ,
162- "job_id" : self ._job_id ,
163- "qobj_id" : ", " .join (x .name for x in self ._circuits ),
164- "success" : payload_dict ["success" ],
165- "header" : payload_dict .get ("header" ),
166- "metadata" : payload_dict .get ("metadata" ),
167- }
168- )
169-
170- qiskit_results = list (
134+ program_results = list (
171135 map (
172- lambda r : __make_result_from_payload (
173- self ._extract_payload_from_response (r )
136+ lambda r : self ._extract_payload_from_response (r ).to_qiskit_result (
137+ backend_name = self .backend ().name ,
138+ backend_version = self .backend ().version ,
139+ job_id = self ._job_id ,
140+ qobj_id = ", " .join (x .name for x in self ._circuits ),
174141 ),
175142 job_results ,
176143 )
177144 )
178145
179- if len (qiskit_results ) == 1 :
180- return qiskit_results [0 ]
146+ if len (program_results ) == 1 :
147+ return program_results [0 ]
181148
182- return qiskit_results
149+ return program_results
183150
184- def _extract_payload_from_response (self , job_result : QaaSJobResult ) -> str :
151+ def _extract_payload_from_response (
152+ self , job_result : QaaSJobResult
153+ ) -> QuantumProgramResult :
185154 result = job_result .result
186155
187156 if result is None or result == "" :
@@ -190,12 +159,11 @@ def _extract_payload_from_response(self, job_result: QaaSJobResult) -> str:
190159 if url is not None :
191160 resp = httpx .get (url )
192161 resp .raise_for_status ()
193-
194- return resp .text
162+ result = resp .text
195163 else :
196- raise Exception ("Got result with empty data and url fields" )
197- else :
198- return result
164+ raise RuntimeError ("Got result with empty data and url fields" )
165+
166+ return QuantumProgramResult . from_json_str ( result )
199167
200168 def _wait_for_result (
201169 self , timeout : Optional [int ], fetch_interval : int
@@ -214,28 +182,6 @@ def _wait_for_result(
214182 return self ._client .list_job_results (self ._job_id )
215183
216184 if status == JobStatus .ERROR :
217- raise JobError ("Job error " )
185+ raise JobError (f "Job failed: { self . _last_progress_message } " )
218186
219187 time .sleep (fetch_interval )
220-
221-
222- def _encode_numpy_complex (obj ):
223- """
224- Recursively traverses a structure and converts numpy arrays and
225- complex numbers into a JSON-serializable format.
226- """
227- if isinstance (obj , np .ndarray ):
228- return {
229- "__ndarray__" : True ,
230- "data" : _encode_numpy_complex (obj .tolist ()), # Recursively encode data
231- "dtype" : obj .dtype .name ,
232- "shape" : obj .shape ,
233- }
234- elif isinstance (obj , (complex , np .complex128 )):
235- return {"__complex__" : True , "real" : obj .real , "imag" : obj .imag }
236- elif isinstance (obj , dict ):
237- return {key : _encode_numpy_complex (value ) for key , value in obj .items ()}
238- elif isinstance (obj , (list , tuple )):
239- return [_encode_numpy_complex (item ) for item in obj ]
240- else :
241- return obj
0 commit comments