iqm-pulla 11.16.2__py3-none-any.whl → 12.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- iqm/cpc/compiler/compiler.py +21 -25
- iqm/cpc/compiler/dd.py +1 -1
- iqm/cpc/compiler/standard_stages.py +19 -5
- iqm/cpc/compiler/station_settings.py +9 -7
- iqm/cpc/interface/compiler.py +7 -98
- iqm/pulla/calibration.py +26 -47
- iqm/pulla/interface.py +3 -58
- iqm/pulla/pulla.py +136 -196
- iqm/pulla/utils.py +21 -17
- iqm/pulla/utils_qir.py +3 -2
- iqm/pulla/utils_qiskit.py +37 -46
- {iqm_pulla-11.16.2.dist-info → iqm_pulla-12.0.0.dist-info}/METADATA +11 -20
- iqm_pulla-12.0.0.dist-info/RECORD +27 -0
- iqm_pulla-11.16.2.dist-info/RECORD +0 -27
- {iqm_pulla-11.16.2.dist-info → iqm_pulla-12.0.0.dist-info}/AUTHORS.rst +0 -0
- {iqm_pulla-11.16.2.dist-info → iqm_pulla-12.0.0.dist-info}/LICENSE.txt +0 -0
- {iqm_pulla-11.16.2.dist-info → iqm_pulla-12.0.0.dist-info}/WHEEL +0 -0
- {iqm_pulla-11.16.2.dist-info → iqm_pulla-12.0.0.dist-info}/top_level.txt +0 -0
iqm/pulla/pulla.py
CHANGED
|
@@ -11,17 +11,20 @@
|
|
|
11
11
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
|
+
"""Pulse level access to IQM quantum computers."""
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
from __future__ import annotations
|
|
16
17
|
|
|
17
|
-
from
|
|
18
|
+
from copy import deepcopy
|
|
19
|
+
from dataclasses import dataclass
|
|
18
20
|
from importlib.metadata import version
|
|
19
21
|
import logging
|
|
20
|
-
import platform
|
|
21
|
-
import time
|
|
22
22
|
from typing import Any
|
|
23
23
|
import uuid
|
|
24
24
|
|
|
25
|
+
from iqm.iqm_client.iqm_client import IQMServerClientJob
|
|
26
|
+
from iqm.iqm_server_client.iqm_server_client import _IQMServerClient
|
|
27
|
+
from iqm.iqm_server_client.models import JobStatus
|
|
25
28
|
import requests
|
|
26
29
|
|
|
27
30
|
from exa.common.data.setting_node import SettingNode
|
|
@@ -36,19 +39,15 @@ from iqm.cpc.compiler.standard_stages import get_standard_stages
|
|
|
36
39
|
from iqm.cpc.interface.compiler import CircuitExecutionOptions
|
|
37
40
|
from iqm.pulla.calibration import CalibrationDataProvider
|
|
38
41
|
from iqm.pulla.interface import (
|
|
39
|
-
|
|
40
|
-
CalibrationSetId,
|
|
42
|
+
CalibrationSetValues,
|
|
41
43
|
CHADRetrievalException,
|
|
42
44
|
ChipLabelRetrievalException,
|
|
43
45
|
SettingsRetrievalException,
|
|
44
|
-
StationControlResult,
|
|
45
|
-
TaskStatus,
|
|
46
46
|
)
|
|
47
47
|
from iqm.pulla.utils import extract_readout_controller_result_names, map_sweep_results_to_logical_qubits
|
|
48
48
|
from iqm.pulse.playlist.channel import ChannelProperties, get_channel_properties_from_station_settings
|
|
49
49
|
from iqm.pulse.playlist.playlist import Playlist
|
|
50
|
-
from iqm.station_control.
|
|
51
|
-
from iqm.station_control.interface.models import JobExecutorStatus, SweepDefinition
|
|
50
|
+
from iqm.station_control.interface.models import CircuitMeasurementResultsBatch, SweepDefinition
|
|
52
51
|
|
|
53
52
|
# ██████ ██ ██ ██ ██ █████
|
|
54
53
|
# ██ ██ ██ ██ ██ ██ ██ ██
|
|
@@ -57,44 +56,55 @@ from iqm.station_control.interface.models import JobExecutorStatus, SweepDefinit
|
|
|
57
56
|
# ██ ██████ ███████ ███████ ██ ██
|
|
58
57
|
|
|
59
58
|
logger = logging.getLogger(__name__)
|
|
59
|
+
init_loggers({"iqm": "INFO"})
|
|
60
60
|
|
|
61
61
|
|
|
62
62
|
class Pulla:
|
|
63
|
-
"""Pulse level access
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
"""Pulse level access to IQM quantum computers.
|
|
64
|
+
|
|
65
|
+
Each instance of this class represents a connection to a remote quantum computer.
|
|
66
|
+
Can create a :class:`~iqm.cpc.compiler.compiler.Compiler` instance ready to be used with
|
|
67
|
+
the connected quantum computer.
|
|
66
68
|
|
|
67
69
|
Args:
|
|
68
|
-
|
|
69
|
-
|
|
70
|
+
iqm_server_url: URL for accessing the server. Has to start with http or https.
|
|
71
|
+
quantum_computer: ID or alias of the quantum computer to connect to, if the IQM Server
|
|
72
|
+
instance controls more than one.
|
|
73
|
+
token: Long-lived authentication token in plain text format.
|
|
74
|
+
If ``token`` is given no other user authentication parameters should be given.
|
|
75
|
+
tokens_file: Path to a tokens file used for authentication.
|
|
76
|
+
If ``tokens_file`` is given no other user authentication parameters should be given.
|
|
77
|
+
client_signature: String that Pulla adds to User-Agent header of requests
|
|
78
|
+
it sends to the server. The signature is appended to IQMServerClient's own version
|
|
79
|
+
information and is intended to carry additional version information,
|
|
80
|
+
for example the version information of the caller.
|
|
70
81
|
|
|
71
82
|
"""
|
|
72
83
|
|
|
73
84
|
def __init__(
|
|
74
85
|
self,
|
|
75
|
-
|
|
86
|
+
iqm_server_url: str,
|
|
76
87
|
*,
|
|
77
|
-
|
|
78
|
-
|
|
88
|
+
quantum_computer: str | None = None,
|
|
89
|
+
token: str | None = None,
|
|
90
|
+
tokens_file: str | None = None,
|
|
91
|
+
client_signature: str | None = None,
|
|
79
92
|
):
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
self._signature += f", iqm-pulla {version('iqm-pulla')}"
|
|
83
|
-
|
|
84
|
-
# The function to be passed to Station Control Client if the server requires authentication
|
|
85
|
-
self.get_token_callback = get_token_callback
|
|
86
|
-
|
|
87
|
-
# SC Client to be used for fetching calibration data, submitting sweeps, and retrieving results.
|
|
93
|
+
if not client_signature:
|
|
94
|
+
client_signature = f"iqm-pulla {version('iqm-pulla')}"
|
|
88
95
|
try:
|
|
89
|
-
self.
|
|
90
|
-
|
|
96
|
+
self._iqm_server_client = _IQMServerClient(
|
|
97
|
+
iqm_server_url=iqm_server_url,
|
|
98
|
+
token=token,
|
|
99
|
+
tokens_file=tokens_file,
|
|
100
|
+
client_signature=client_signature,
|
|
101
|
+
quantum_computer=quantum_computer,
|
|
91
102
|
)
|
|
92
|
-
|
|
93
103
|
except Exception as e:
|
|
94
|
-
logger.error("Failed to initialize
|
|
95
|
-
raise ValueError("Failed to initialize
|
|
96
|
-
# Separate wrapper on top of
|
|
97
|
-
self._calibration_data_provider = CalibrationDataProvider(self.
|
|
104
|
+
logger.error("Failed to initialize IQM Server client: %s", e)
|
|
105
|
+
raise ValueError("Failed to initialize IQM Server client") from e
|
|
106
|
+
# Separate wrapper on top of IQM Server client to simplify calibration data fetching.
|
|
107
|
+
self._calibration_data_provider = CalibrationDataProvider(self._iqm_server_client)
|
|
98
108
|
|
|
99
109
|
# Data needed for the compiler.
|
|
100
110
|
self._station_control_settings: SettingNode | None = None
|
|
@@ -107,13 +117,13 @@ class Pulla:
|
|
|
107
117
|
|
|
108
118
|
def get_standard_compiler(
|
|
109
119
|
self,
|
|
110
|
-
|
|
120
|
+
calibration_set_values: CalibrationSetValues | None = None,
|
|
111
121
|
circuit_execution_options: CircuitExecutionOptions | dict | None = None,
|
|
112
122
|
) -> Compiler:
|
|
113
123
|
"""Returns a new instance of the compiler with the default calibration set and standard stages.
|
|
114
124
|
|
|
115
125
|
Args:
|
|
116
|
-
|
|
126
|
+
calibration_set_values: Calibration set to use. If None, the current calibration set will be used.
|
|
117
127
|
circuit_execution_options: circuit execution options to use for the compiler. If a CircuitExecutionOptions
|
|
118
128
|
object is provided, the compiler use it as is. If a dict is provided, the default values will be
|
|
119
129
|
overridden for the present keys in that dict. If left ``None``, the default options will be used.
|
|
@@ -129,7 +139,7 @@ class Pulla:
|
|
|
129
139
|
**STANDARD_CIRCUIT_EXECUTION_OPTIONS_DICT | circuit_execution_options # type: ignore
|
|
130
140
|
)
|
|
131
141
|
return Compiler(
|
|
132
|
-
|
|
142
|
+
calibration_set_values=calibration_set_values or self.fetch_default_calibration_set()[0],
|
|
133
143
|
chip_topology=self._chip_topology,
|
|
134
144
|
channel_properties=self._channel_properties,
|
|
135
145
|
component_channels=self._component_channels,
|
|
@@ -138,32 +148,38 @@ class Pulla:
|
|
|
138
148
|
options=circuit_execution_options,
|
|
139
149
|
)
|
|
140
150
|
|
|
141
|
-
def
|
|
142
|
-
"""
|
|
143
|
-
|
|
144
|
-
|
|
151
|
+
def fetch_default_calibration_set(self) -> tuple[CalibrationSetValues, uuid.UUID]:
|
|
152
|
+
"""Fetch the default calibration set from the server, in a minimal format.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Calibration set contents, calibration set ID.
|
|
156
|
+
|
|
157
|
+
"""
|
|
158
|
+
default_calibration_set, default_calibration_set_id = (
|
|
159
|
+
self._calibration_data_provider.get_default_calibration_set()
|
|
145
160
|
)
|
|
146
|
-
return
|
|
161
|
+
return default_calibration_set, default_calibration_set_id
|
|
162
|
+
|
|
163
|
+
def fetch_calibration_set_values_by_id(self, calibration_set_id: uuid.UUID) -> CalibrationSetValues:
|
|
164
|
+
"""Fetch a specific calibration set from the server.
|
|
147
165
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
All calibration sets are cached in-memory, so if the calibration set with the given id has already been fetched,
|
|
151
|
-
it will be returned immediately.
|
|
166
|
+
All calibration sets are cached in-memory, so if the calibration set with the given
|
|
167
|
+
id has already been fetched, it will be returned immediately.
|
|
152
168
|
|
|
153
169
|
Args:
|
|
154
|
-
calibration_set_id:
|
|
170
|
+
calibration_set_id: ID of the calibration set to fetch.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Calibration set contents.
|
|
155
174
|
|
|
156
175
|
"""
|
|
157
|
-
calibration_set = self._calibration_data_provider.
|
|
176
|
+
calibration_set = self._calibration_data_provider.get_calibration_set_values(calibration_set_id)
|
|
158
177
|
return calibration_set
|
|
159
178
|
|
|
160
179
|
def get_chip_label(self) -> str:
|
|
161
|
-
"""
|
|
162
|
-
|
|
163
|
-
The chip label is fetched from the Station Control API.
|
|
164
|
-
"""
|
|
180
|
+
"""QPU label of the quantum computer we are connected to."""
|
|
165
181
|
try:
|
|
166
|
-
duts = self.
|
|
182
|
+
duts = self._iqm_server_client.get_duts()
|
|
167
183
|
except requests.RequestException as e:
|
|
168
184
|
raise ChipLabelRetrievalException(f"Failed to retrieve the chip label: {e}") from e
|
|
169
185
|
|
|
@@ -172,19 +188,20 @@ class Pulla:
|
|
|
172
188
|
return duts[0].label
|
|
173
189
|
|
|
174
190
|
def get_chip_topology(self) -> ChipTopology:
|
|
175
|
-
"""
|
|
191
|
+
"""QPU topology of the quantum computer we are connected to."""
|
|
192
|
+
self.get_chip_label() # Called just to make sure that there will be only one DUT available
|
|
176
193
|
try:
|
|
177
|
-
|
|
194
|
+
chip_design_record = self._iqm_server_client.get_chip_design_records()[0]
|
|
178
195
|
except Exception as e:
|
|
179
196
|
raise CHADRetrievalException("Could not fetch chip design record") from e
|
|
180
|
-
return ChipTopology.from_chip_design_record(
|
|
197
|
+
return ChipTopology.from_chip_design_record(chip_design_record)
|
|
181
198
|
|
|
182
199
|
def _get_station_control_settings(self) -> SettingNode:
|
|
183
|
-
"""
|
|
200
|
+
"""Station Control default settings tree."""
|
|
184
201
|
if self._station_control_settings is None:
|
|
185
202
|
# request the station settings, cache the results
|
|
186
203
|
try:
|
|
187
|
-
self._station_control_settings = self.
|
|
204
|
+
self._station_control_settings = self._iqm_server_client.get_settings()
|
|
188
205
|
except Exception as e:
|
|
189
206
|
raise SettingsRetrievalException("Could not fetch station settings") from e
|
|
190
207
|
return self._station_control_settings
|
|
@@ -204,25 +221,26 @@ class Pulla:
|
|
|
204
221
|
self._get_station_control_settings(), self.get_chip_topology()
|
|
205
222
|
)
|
|
206
223
|
|
|
207
|
-
def
|
|
224
|
+
def submit_playlist(
|
|
208
225
|
self,
|
|
209
226
|
playlist: Playlist,
|
|
210
|
-
context: dict[str, Any],
|
|
211
227
|
settings: SettingNode,
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
228
|
+
*,
|
|
229
|
+
context: dict[str, Any],
|
|
230
|
+
use_timeslot: bool = False,
|
|
231
|
+
) -> SweepJob:
|
|
232
|
+
"""Submit a Playlist of instruction schedules for execution on the remote quantum computer.
|
|
216
233
|
|
|
217
234
|
Args:
|
|
218
|
-
playlist:
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
235
|
+
playlist: Schedules to execute.
|
|
236
|
+
settings: Station settings to be used for the execution.
|
|
237
|
+
context: Context object of the compiler run that produced ``playlist``, containing the readout mappings.
|
|
238
|
+
Required for postprocessing the results.
|
|
239
|
+
use_timeslot: Submits the job to the timeslot queue if set to ``True``. If set to ``False``,
|
|
240
|
+
the job is submitted to the normal on-demand queue.
|
|
223
241
|
|
|
224
242
|
Returns:
|
|
225
|
-
|
|
243
|
+
Created job object, used to query the job status and the execution results.
|
|
226
244
|
|
|
227
245
|
"""
|
|
228
246
|
readout_components = []
|
|
@@ -231,141 +249,63 @@ class Pulla:
|
|
|
231
249
|
if k == "readout":
|
|
232
250
|
readout_components.append(v)
|
|
233
251
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
252
|
+
sweep = SweepDefinition(
|
|
253
|
+
sweep_id=uuid.uuid4(),
|
|
254
|
+
playlist=playlist,
|
|
255
|
+
return_parameters=list(extract_readout_controller_result_names(context["readout_mappings"])),
|
|
256
|
+
settings=settings,
|
|
257
|
+
dut_label=self.get_chip_label(),
|
|
258
|
+
sweeps=[],
|
|
259
|
+
)
|
|
260
|
+
job_data = self._iqm_server_client.submit_sweep(sweep, use_timeslot=use_timeslot)
|
|
261
|
+
logger.info("Submitted a job with ID: %s", job_data.id)
|
|
262
|
+
|
|
263
|
+
# Initialize the job object, which can be then used to query
|
|
264
|
+
return SweepJob(
|
|
265
|
+
data=job_data,
|
|
266
|
+
_pulla=self,
|
|
267
|
+
_context=deepcopy(context),
|
|
243
268
|
)
|
|
244
|
-
job_id = uuid.UUID(sweep_response["job_id"])
|
|
245
|
-
|
|
246
|
-
logger.info("Created job in queue with ID: %s", job_id)
|
|
247
|
-
if href := sweep_response.get("job_href"):
|
|
248
|
-
logger.info("Job link: %s", href)
|
|
249
|
-
|
|
250
|
-
if wait_completion:
|
|
251
|
-
return self.get_execution_result(job_id, context, verbose, wait_completion=True)
|
|
252
|
-
else:
|
|
253
|
-
return StationControlResult(sweep_id=job_id, task_id=job_id, status=TaskStatus.PENDING)
|
|
254
|
-
|
|
255
|
-
def get_execution_result(
|
|
256
|
-
self,
|
|
257
|
-
job_id: uuid.UUID,
|
|
258
|
-
context: dict[str, Any],
|
|
259
|
-
verbose: bool = True,
|
|
260
|
-
wait_completion: bool = True,
|
|
261
|
-
) -> StationControlResult:
|
|
262
|
-
"""Get execution results.
|
|
263
|
-
|
|
264
|
-
Args:
|
|
265
|
-
job_id: The ID of the job to process.
|
|
266
|
-
context: Context object of the successful compiler run, containing the readout mappings.
|
|
267
|
-
verbose: Whether to print results.
|
|
268
|
-
wait_completion: If True, waits for job completion. If False, returns current status.
|
|
269
269
|
|
|
270
|
-
Returns:
|
|
271
|
-
The processed station control result.
|
|
272
270
|
|
|
273
|
-
|
|
274
|
-
|
|
271
|
+
@dataclass
|
|
272
|
+
class SweepJob(IQMServerClientJob):
|
|
273
|
+
"""Status and results of a Pulla sweep job.
|
|
275
274
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
logger.info("Waiting for the job to finish...")
|
|
279
|
-
|
|
280
|
-
while True:
|
|
281
|
-
job_data = self._station_control.get_job(job_id)
|
|
282
|
-
sc_result.status = TaskStatus.PENDING
|
|
283
|
-
|
|
284
|
-
if job_data.job_status <= JobExecutorStatus.EXECUTION_STARTED: # type: ignore[operator]
|
|
285
|
-
if wait_completion:
|
|
286
|
-
# Wait in the task queue while showing a progress bar
|
|
287
|
-
interrupted = self._station_control._wait_job_completion( # type: ignore[attr-defined]
|
|
288
|
-
str(job_id), get_progress_bar_callback()
|
|
289
|
-
)
|
|
290
|
-
if interrupted:
|
|
291
|
-
raise KeyboardInterrupt
|
|
292
|
-
else:
|
|
293
|
-
# Non-blocking check - return current status
|
|
294
|
-
sc_result.status = (
|
|
295
|
-
TaskStatus.PROGRESS
|
|
296
|
-
if job_data.job_status == JobExecutorStatus.EXECUTION_STARTED
|
|
297
|
-
else TaskStatus.PENDING
|
|
298
|
-
)
|
|
299
|
-
return sc_result
|
|
300
|
-
else:
|
|
301
|
-
# job is not in queue or executing, so we can query the sweep
|
|
302
|
-
result_or_nothing = self._get_result_of_started_job(
|
|
303
|
-
context, job_data, job_id, sc_result, wait_completion, verbose
|
|
304
|
-
)
|
|
305
|
-
if result_or_nothing is not None:
|
|
306
|
-
return result_or_nothing
|
|
307
|
-
|
|
308
|
-
if wait_completion:
|
|
309
|
-
time.sleep(1)
|
|
310
|
-
else:
|
|
311
|
-
break
|
|
312
|
-
|
|
313
|
-
except KeyboardInterrupt as exc:
|
|
314
|
-
if wait_completion:
|
|
315
|
-
logger.info("Caught KeyboardInterrupt, revoking job %s", job_id)
|
|
316
|
-
self._station_control.abort_job(job_id)
|
|
317
|
-
raise KeyboardInterrupt from exc
|
|
318
|
-
|
|
319
|
-
return sc_result
|
|
320
|
-
|
|
321
|
-
def _get_result_of_started_job(
|
|
322
|
-
self,
|
|
323
|
-
context: dict[str, Any],
|
|
324
|
-
job_data: Any,
|
|
325
|
-
job_id: uuid.UUID,
|
|
326
|
-
sc_result: StationControlResult,
|
|
327
|
-
wait_completion: bool,
|
|
328
|
-
verbose: bool,
|
|
329
|
-
) -> StationControlResult | None:
|
|
330
|
-
sweep_data = self._station_control.get_sweep(job_id)
|
|
331
|
-
if job_data.job_status == JobExecutorStatus.READY:
|
|
332
|
-
if wait_completion:
|
|
333
|
-
logger.info("Sweep status: %s", str(sweep_data.job_status))
|
|
334
|
-
|
|
335
|
-
sc_result.status = TaskStatus.READY
|
|
336
|
-
sc_result.result = map_sweep_results_to_logical_qubits(
|
|
337
|
-
self._station_control.get_sweep_results(job_id),
|
|
338
|
-
context["readout_mappings"],
|
|
339
|
-
context["options"].heralding_mode,
|
|
340
|
-
)
|
|
341
|
-
sc_result.start_time = sweep_data.begin_timestamp.isoformat() if sweep_data.begin_timestamp else None
|
|
342
|
-
sc_result.end_time = sweep_data.end_timestamp.isoformat() if sweep_data.end_timestamp else None
|
|
275
|
+
Created by :meth:`Pulla.submit_playlist`.
|
|
276
|
+
"""
|
|
343
277
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
logger.info(sc_result.result)
|
|
278
|
+
_pulla: Pulla
|
|
279
|
+
"""Client instance used to create the job."""
|
|
347
280
|
|
|
348
|
-
|
|
281
|
+
_context: dict[str, Any]
|
|
282
|
+
"""Final context object of the compiler run used to produce the sweep, contains information needed
|
|
283
|
+
for processing the results."""
|
|
349
284
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
sc_result.start_time = sweep_data.begin_timestamp.isoformat() if sweep_data.begin_timestamp else None
|
|
353
|
-
sc_result.end_time = sweep_data.end_timestamp.isoformat() if sweep_data.end_timestamp else None
|
|
354
|
-
sc_result.message = str(job_data.job_error)
|
|
355
|
-
if wait_completion:
|
|
356
|
-
logger.error("Submission failed! Error: %s", sc_result.message)
|
|
357
|
-
return sc_result
|
|
285
|
+
_result: CircuitMeasurementResultsBatch | None = None
|
|
286
|
+
"""Sweep results converted to the circuit measurement results expected by the client."""
|
|
358
287
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
sc_result.end_time = sweep_data.end_timestamp.isoformat() if sweep_data.end_timestamp else None
|
|
363
|
-
sc_result.message = str(job_data.job_error)
|
|
364
|
-
if wait_completion:
|
|
365
|
-
logger.error("Submission was revoked!")
|
|
366
|
-
return sc_result
|
|
288
|
+
@property
|
|
289
|
+
def _iqm_server_client(self) -> _IQMServerClient:
|
|
290
|
+
return self._pulla._iqm_server_client
|
|
367
291
|
|
|
368
|
-
|
|
292
|
+
def result(self) -> CircuitMeasurementResultsBatch | None:
|
|
293
|
+
"""Get (and cache) the job result, if the job has completed.
|
|
369
294
|
|
|
295
|
+
Returns:
|
|
296
|
+
Circuit measurement results for the job, or None if the results are not available.
|
|
370
297
|
|
|
371
|
-
|
|
298
|
+
"""
|
|
299
|
+
if not self._result:
|
|
300
|
+
self.update()
|
|
301
|
+
# if successful, get the results (TODO what about possible partial data?)
|
|
302
|
+
if self.status != JobStatus.COMPLETED:
|
|
303
|
+
return None
|
|
304
|
+
|
|
305
|
+
sweep_results = self._iqm_server_client.get_job_artifact_sweep_results(self.job_id)
|
|
306
|
+
self._result = map_sweep_results_to_logical_qubits(
|
|
307
|
+
sweep_results,
|
|
308
|
+
self._context["readout_mappings"],
|
|
309
|
+
self._context["options"].heralding_mode,
|
|
310
|
+
)
|
|
311
|
+
return self._result
|
iqm/pulla/utils.py
CHANGED
|
@@ -38,7 +38,7 @@ from iqm.cpc.interface.compiler import (
|
|
|
38
38
|
HeraldingMode,
|
|
39
39
|
ReadoutMappingBatch,
|
|
40
40
|
)
|
|
41
|
-
from iqm.pulla.interface import HERALDING_KEY,
|
|
41
|
+
from iqm.pulla.interface import HERALDING_KEY, CalibrationSetValues
|
|
42
42
|
from iqm.pulse.builder import CircuitOperation, ScheduleBuilder, build_quantum_ops
|
|
43
43
|
from iqm.pulse.gate_implementation import CompositeGate, OpCalibrationDataTree
|
|
44
44
|
from iqm.pulse.playlist.channel import ChannelProperties
|
|
@@ -46,7 +46,7 @@ from iqm.pulse.playlist.instructions import Instruction
|
|
|
46
46
|
from iqm.pulse.playlist.schedule import Schedule, Segment
|
|
47
47
|
from iqm.pulse.timebox import TimeBox
|
|
48
48
|
from iqm.station_control.client.qon import locus_str_to_locus
|
|
49
|
-
from iqm.station_control.interface.models
|
|
49
|
+
from iqm.station_control.interface.models import CircuitMeasurementResultsBatch, ObservationBase, QubitMapping
|
|
50
50
|
|
|
51
51
|
logger = logging.getLogger(__name__)
|
|
52
52
|
|
|
@@ -78,7 +78,7 @@ def circuit_operations_to_cpc(circ_ops: tuple[CircuitOperation], name: str | Non
|
|
|
78
78
|
return CPC_Circuit(name=name, instructions=circ_ops)
|
|
79
79
|
|
|
80
80
|
|
|
81
|
-
def iqm_circuit_to_gate_implementation(circuit: CPC_Circuit, qubit_mapping:
|
|
81
|
+
def iqm_circuit_to_gate_implementation(circuit: CPC_Circuit, qubit_mapping: QubitMapping) -> type[CompositeGate]:
|
|
82
82
|
"""Wrap a circuit to a single GateImplementation that can then be registered as an independent "gate".
|
|
83
83
|
|
|
84
84
|
Returns a composite GateImplementation which, when called, produces a TimeBox with the circuit contents
|
|
@@ -469,7 +469,7 @@ def get_hash_for(circuit: CPC_Circuit) -> int:
|
|
|
469
469
|
return hash(str_repr)
|
|
470
470
|
|
|
471
471
|
|
|
472
|
-
def calset_to_cal_data_tree(
|
|
472
|
+
def calset_to_cal_data_tree(calibration_set_values: CalibrationSetValues) -> OpCalibrationDataTree:
|
|
473
473
|
"""Build an iqm-pulse QuantumOp calibration data tree from a calibration set.
|
|
474
474
|
|
|
475
475
|
Splits the dotted observation names that are prefixed with "gates." into the corresponding
|
|
@@ -488,7 +488,7 @@ def calset_to_cal_data_tree(calibration_set: CalibrationSet) -> OpCalibrationDat
|
|
|
488
488
|
set_path(node.setdefault(path[0], {}), path[1:], value)
|
|
489
489
|
|
|
490
490
|
tree: OpCalibrationDataTree = {}
|
|
491
|
-
for key, value in
|
|
491
|
+
for key, value in calibration_set_values.items():
|
|
492
492
|
path = key.split(".")
|
|
493
493
|
if path[0] == "gates":
|
|
494
494
|
if len(path) < 5:
|
|
@@ -502,7 +502,7 @@ def calset_to_cal_data_tree(calibration_set: CalibrationSet) -> OpCalibrationDat
|
|
|
502
502
|
|
|
503
503
|
|
|
504
504
|
def initialize_schedule_builder(
|
|
505
|
-
|
|
505
|
+
calibration_set_values: CalibrationSetValues,
|
|
506
506
|
chip_topology: ChipTopology,
|
|
507
507
|
channel_properties: dict[str, ChannelProperties],
|
|
508
508
|
component_channels: dict[str, dict[str, str]],
|
|
@@ -510,7 +510,7 @@ def initialize_schedule_builder(
|
|
|
510
510
|
"""Initialize a new schedule builder for the station, validate that it is configured properly.
|
|
511
511
|
|
|
512
512
|
Args:
|
|
513
|
-
|
|
513
|
+
calibration_set_values: calibration data for the station the circuits are executed on
|
|
514
514
|
chip_topology: topology of the QPU the circuits are executed on
|
|
515
515
|
channel_properties: properties of control channels on the station
|
|
516
516
|
component_channels: QPU component to function to channel mapping
|
|
@@ -520,11 +520,13 @@ def initialize_schedule_builder(
|
|
|
520
520
|
"""
|
|
521
521
|
op_table = build_quantum_ops({})
|
|
522
522
|
|
|
523
|
-
channel_properties = _update_channel_props_from_calibration(
|
|
523
|
+
channel_properties = _update_channel_props_from_calibration(
|
|
524
|
+
channel_properties, component_channels, calibration_set_values
|
|
525
|
+
)
|
|
524
526
|
|
|
525
527
|
builder = ScheduleBuilder(
|
|
526
528
|
op_table,
|
|
527
|
-
calset_to_cal_data_tree(
|
|
529
|
+
calset_to_cal_data_tree(calibration_set_values),
|
|
528
530
|
chip_topology,
|
|
529
531
|
channel_properties,
|
|
530
532
|
component_channels,
|
|
@@ -535,14 +537,14 @@ def initialize_schedule_builder(
|
|
|
535
537
|
def _update_channel_props_from_calibration( # noqa: ANN202
|
|
536
538
|
channel_properties: dict[str, ChannelProperties],
|
|
537
539
|
component_channels: dict[str, dict[str, str]],
|
|
538
|
-
|
|
540
|
+
calibration_set_values: CalibrationSetValues,
|
|
539
541
|
):
|
|
540
|
-
"""Copy probe line center frequencies from
|
|
542
|
+
"""Copy probe line center frequencies from calibration set values to their readout channel properties.
|
|
541
543
|
|
|
542
544
|
Args:
|
|
543
545
|
channel_properties: channel properties to update
|
|
544
546
|
component_channels: mapping from QPU component to its functions/channels that perform them
|
|
545
|
-
|
|
547
|
+
calibration_set_values: calibration data
|
|
546
548
|
Returns:
|
|
547
549
|
updated channel properties
|
|
548
550
|
|
|
@@ -551,9 +553,11 @@ def _update_channel_props_from_calibration( # noqa: ANN202
|
|
|
551
553
|
replacements = {}
|
|
552
554
|
for component, channels in component_channels.items():
|
|
553
555
|
if "readout" in channels:
|
|
554
|
-
center_frequency =
|
|
556
|
+
center_frequency = calibration_set_values.get(f"controllers.{component}.readout.center_frequency")
|
|
555
557
|
if center_frequency is None:
|
|
556
|
-
center_frequency =
|
|
558
|
+
center_frequency = calibration_set_values.get(
|
|
559
|
+
f"controllers.{component}.readout.local_oscillator.frequency"
|
|
560
|
+
)
|
|
557
561
|
if center_frequency is None:
|
|
558
562
|
raise CalibrationError(
|
|
559
563
|
f"No calibration value found for the center frequency or local oscillator frequency of {component}."
|
|
@@ -603,7 +607,7 @@ def find_circuit_boundary(
|
|
|
603
607
|
|
|
604
608
|
def build_settings(
|
|
605
609
|
shots: int,
|
|
606
|
-
|
|
610
|
+
calibration_set_values: CalibrationSetValues,
|
|
607
611
|
builder: ScheduleBuilder,
|
|
608
612
|
circuit_metrics: Iterable[CircuitMetrics],
|
|
609
613
|
*,
|
|
@@ -613,7 +617,7 @@ def build_settings(
|
|
|
613
617
|
|
|
614
618
|
Args:
|
|
615
619
|
shots: number of times to execute/sample each circuit
|
|
616
|
-
|
|
620
|
+
calibration_set_values: calibration data for the station the circuits are executed on
|
|
617
621
|
builder: schedule builder object, encapsulating station properties and gate calibration data
|
|
618
622
|
circuit_metrics: statistics about the circuits to be executed
|
|
619
623
|
options: various discrete options for circuit execution that affect compilation
|
|
@@ -654,7 +658,7 @@ def build_settings(
|
|
|
654
658
|
# of circuits is needlessly complicated and usually would yield a small benefit
|
|
655
659
|
measured_probe_lines=device.probe_lines,
|
|
656
660
|
shots=shots,
|
|
657
|
-
|
|
661
|
+
calibration_set_values=calibration_set_values,
|
|
658
662
|
boundary_qubits=boundary_components & device.qubits,
|
|
659
663
|
boundary_couplers=boundary_couplers,
|
|
660
664
|
flux_pulsed_qubits=[
|
iqm/pulla/utils_qir.py
CHANGED
|
@@ -40,6 +40,7 @@ from qiskit.providers import BackendV2
|
|
|
40
40
|
from iqm.cpc.compiler.compiler import Compiler
|
|
41
41
|
from iqm.cpc.interface.compiler import Circuit as CPC_Circuit
|
|
42
42
|
from iqm.pulse import CircuitOperation
|
|
43
|
+
from iqm.station_control.interface.models import QubitMapping
|
|
43
44
|
|
|
44
45
|
qir_logger = logging.getLogger(__name__)
|
|
45
46
|
|
|
@@ -148,7 +149,7 @@ def _parse_double(value: str) -> float:
|
|
|
148
149
|
|
|
149
150
|
|
|
150
151
|
def qir_to_pulla( # noqa: PLR0915, PLR0912
|
|
151
|
-
compiler: Compiler, qir: str | bytes, qubit_mapping:
|
|
152
|
+
compiler: Compiler, qir: str | bytes, qubit_mapping: QubitMapping | None = None
|
|
152
153
|
) -> tuple[list[CPC_Circuit], Compiler]:
|
|
153
154
|
"""Convert a QIR module to a CPC circuit.
|
|
154
155
|
|
|
@@ -257,7 +258,7 @@ def qir_to_pulla( # noqa: PLR0915, PLR0912
|
|
|
257
258
|
return circuits, compiler
|
|
258
259
|
|
|
259
260
|
|
|
260
|
-
def generate_qiskit_qir_qubit_mapping(qiskit_circuit: QuantumCircuit, qiskit_backend: BackendV2) ->
|
|
261
|
+
def generate_qiskit_qir_qubit_mapping(qiskit_circuit: QuantumCircuit, qiskit_backend: BackendV2) -> QubitMapping:
|
|
261
262
|
"""qiskit-qir has a bug, which causes qubit pointers to not be generated correctly
|
|
262
263
|
according to the final_layout. So we replicate this logic here and generate a new mapping.
|
|
263
264
|
Then we assign qiskit-qir index to the qiskit logic qubit idx.
|