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/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
- """Pulse level access library for IQM's circuit-to-pulse compiler and Station Control API."""
16
+ from __future__ import annotations
16
17
 
17
- from collections.abc import Callable
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
- CalibrationSet,
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.client.utils import get_progress_bar_callback, init_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 library for IQM's circuit-to-pulse compiler and Station Control API.
64
- Conceptually, represents a connection to a remote quantum computer, and a provider of calibration data.
65
- Can create a compiler instance ready to be used with the connected quantum computer.
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
- station_control_url: URL to a Station Control instance.
69
- get_token_callback: An optional function that returns an authentication token for the Station Control API.
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
- station_control_url: str,
86
+ iqm_server_url: str,
76
87
  *,
77
- get_token_callback: Callable[[], str] | None = None,
78
- **kwargs,
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
- self._signature = f"{platform.platform(terse=True)}"
81
- self._signature += f", python {platform.python_version()}"
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._station_control = init_station_control(
90
- station_control_url, get_token_callback=self.get_token_callback, **kwargs
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 Station Control Client: %s", e)
95
- raise ValueError("Failed to initialize Station Control Client") from e
96
- # Separate wrapper on top of SC Client to simplify calibration data fetching.
97
- self._calibration_data_provider = CalibrationDataProvider(self._station_control)
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
- calibration_set: CalibrationSet | None = None,
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
- calibration_set: Calibration set to use. If None, the default calibration set will be used.
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
- calibration_set=calibration_set or self.fetch_latest_calibration_set()[0],
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 fetch_latest_calibration_set(self) -> tuple[CalibrationSet, CalibrationSetId]:
142
- """Fetches the latest default calibration set from the server, and returns its decoded representation and id.""" # noqa: E501
143
- latest_calibration_set, latest_calibration_set_id = self._calibration_data_provider.get_latest_calibration_set(
144
- self.get_chip_label()
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 latest_calibration_set, latest_calibration_set_id
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
- def fetch_calibration_set_by_id(self, calibration_set_id: CalibrationSetId) -> CalibrationSet:
149
- """Fetches a specific calibration set from the server, and returns its decoded representation.
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: id of the calibration set to fetch.
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.get_calibration_set(calibration_set_id)
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
- """Returns the chip label of the current quantum computer.
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._station_control.get_duts()
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
- """Returns chip topology that was fetched from the IQM server during Pulla initialization."""
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
- record = self._station_control.get_chip_design_record(self.get_chip_label())
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(record)
197
+ return ChipTopology.from_chip_design_record(chip_design_record)
181
198
 
182
199
  def _get_station_control_settings(self) -> SettingNode:
183
- """Returns the Station Control settings node that was fetched from the IQM server during Pulla initialization.""" # noqa: E501
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._station_control.get_settings()
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 execute(
224
+ def submit_playlist(
208
225
  self,
209
226
  playlist: Playlist,
210
- context: dict[str, Any],
211
227
  settings: SettingNode,
212
- verbose: bool = True,
213
- wait_completion: bool = True,
214
- ) -> StationControlResult:
215
- """Executes a quantum circuit on the remote quantum computer.
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: Final schedule to be executed.
219
- context: Context object of the successful compiler run, containing the readout mappings.
220
- settings: Station settings.
221
- verbose: Whether to print results.
222
- wait_completion: If True, returns immediately with job ID. If False, waits for completion.
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
- results of the execution
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
- sweep_response = self._station_control.sweep(
235
- SweepDefinition(
236
- sweep_id=uuid.uuid4(),
237
- playlist=playlist,
238
- return_parameters=list(extract_readout_controller_result_names(context["readout_mappings"])),
239
- settings=settings,
240
- dut_label=self.get_chip_label(),
241
- sweeps=[],
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
- sc_result = StationControlResult(sweep_id=job_id, task_id=job_id, status=TaskStatus.PENDING)
271
+ @dataclass
272
+ class SweepJob(IQMServerClientJob):
273
+ """Status and results of a Pulla sweep job.
275
274
 
276
- try:
277
- if wait_completion:
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
- if verbose and wait_completion:
345
- # TODO: Consider using just 'logger.debug' here and remove 'verbose'
346
- logger.info(sc_result.result)
278
+ _pulla: Pulla
279
+ """Client instance used to create the job."""
347
280
 
348
- return sc_result
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
- if job_data.job_status == JobExecutorStatus.FAILED:
351
- sc_result.status = TaskStatus.FAILED
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
- if job_data.job_status == JobExecutorStatus.ABORTED:
360
- sc_result.status = TaskStatus.FAILED
361
- sc_result.start_time = sweep_data.begin_timestamp.isoformat() if sweep_data.begin_timestamp else None
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
- return None
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
- init_loggers({"iqm": "INFO"})
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, CalibrationSet, CircuitMeasurementResultsBatch
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.observation import ObservationBase
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: dict[str, str]) -> type[CompositeGate]:
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(calibration_set: CalibrationSet) -> OpCalibrationDataTree:
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 calibration_set.items():
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
- calibration_set: CalibrationSet,
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
- calibration_set: calibration data for the station the circuits are executed on
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(channel_properties, component_channels, calibration_set)
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(calibration_set),
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
- calset: CalibrationSet,
540
+ calibration_set_values: CalibrationSetValues,
539
541
  ):
540
- """Copy probe line center frequencies from a calset to their readout channel properties.
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
- calset: calibration data
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 = calset.get(f"controllers.{component}.readout.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 = calset.get(f"controllers.{component}.readout.local_oscillator.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
- calibration_set: CalibrationSet,
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
- calibration_set: calibration data for the station the circuits are executed on
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
- calibration_set=calibration_set,
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: dict[str, str] | None = None
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) -> dict[str, str]:
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.