iqm-client 29.13.0__py3-none-any.whl → 30.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.
@@ -42,6 +42,7 @@ class GrantType(str, Enum):
42
42
 
43
43
  class AuthRequest(BaseModel):
44
44
  """Request sent to authentication server for access token and refresh token, or for terminating the session.
45
+
45
46
  * Token request with grant type ``'password'`` starts a new session in the authentication server.
46
47
  It uses fields ``client_id``, ``grant_type``, ``username`` and ``password``.
47
48
  * Token request with grant type ``'refresh_token'`` is used for maintaining an existing session.
@@ -51,6 +51,7 @@ def start_token_manager(cycle: int, config: ConfigFile, single_run: bool = False
51
51
  """Refresh tokens periodically.
52
52
 
53
53
  For each refresh cycle new tokens are requested from auth server.
54
+
54
55
  - If refresh is successful next refresh is attempted in the next cycle.
55
56
  - If auth server does not respond refresh is attempted repeatedly until it succeeds or
56
57
  the existing refresh token expires.
@@ -104,11 +105,10 @@ def refresh_tokens(config: ConfigFile, current_tokens: dict, cycle: int) -> tupl
104
105
  cycle: refresh cycle length in seconds
105
106
 
106
107
  Returns:
107
- Tuple[Optional[dict], bool, int] = (tokens, status, sleep_time)
108
- tokens: dict containing new tokens or current tokens if auth server could not be connected or
109
- None if auth server refused to provide new tokens.
110
- status: bool, True if tokens were refreshed successfully, False otherwise
111
- sleep_time: time to sleep before next refresh attempt
108
+ * dict containing new tokens or current tokens if auth server could not be connected or
109
+ None if auth server refused to provide new tokens
110
+ * bool, True if tokens were refreshed successfully, False otherwise
111
+ * time to sleep before next refresh attempt
112
112
 
113
113
  """
114
114
  access_token = current_tokens.get("access_token", "")
@@ -20,6 +20,7 @@ from functools import lru_cache
20
20
  from http import HTTPStatus
21
21
  from importlib.metadata import version
22
22
  import json
23
+ import logging
23
24
  import os
24
25
  import platform
25
26
  import time
@@ -77,6 +78,9 @@ DEFAULT_TIMEOUT_SECONDS = 900
77
78
  SECONDS_BETWEEN_CALLS = float(os.environ.get("IQM_CLIENT_SECONDS_BETWEEN_CALLS", 1.0))
78
79
 
79
80
 
81
+ logger = logging.getLogger(__name__)
82
+
83
+
80
84
  class IQMClient:
81
85
  """Provides access to IQM quantum computers.
82
86
 
@@ -907,11 +911,18 @@ class IQMClient:
907
911
  # model = model_class.model_validate_json(response.text)
908
912
  except json.decoder.JSONDecodeError as e:
909
913
  raise EndpointRequestError(f"Invalid response: {response.text}, {e!r}") from e
914
+
910
915
  return model
911
916
 
912
- def get_structured_metrics(self, calibration_set_id: UUID | None = None) -> ObservationFinder:
917
+ def get_calibration_quality_metrics(self, calibration_set_id: UUID | None = None) -> ObservationFinder:
913
918
  """Retrieve the given calibration set and related quality metrics from the server.
914
919
 
920
+ .. warning::
921
+
922
+ This method is an experimental interface to the quality metrics and calibration data.
923
+ The API may change considerably in the next versions *with no backwards compatibility*,
924
+ including the API of the ObservationFinder class.
925
+
915
926
  Args:
916
927
  calibration_set_id: ID of the calibration set to retrieve.
917
928
  If ``None``, the current default calibration set is retrieved.
@@ -925,8 +936,16 @@ class IQMClient:
925
936
  HTTPException: HTTP exceptions
926
937
 
927
938
  """
939
+ logger.warning(
940
+ "IQMClient.get_calibration_quality_metrics is an experimental method, and the API will likely change "
941
+ "in the future with no backwards compatibility."
942
+ )
943
+ return self._get_calibration_quality_metrics(calibration_set_id)
944
+
945
+ def _get_calibration_quality_metrics(self, calibration_set_id: UUID | None = None) -> ObservationFinder:
946
+ """See :meth:`get_calibration_quality_metrics`."""
928
947
  if isinstance(self._station_control, IqmServerClient):
929
- raise ValueError("The get_structured_metrics method is not supported by IqmServerClient.")
948
+ raise ValueError("The _get_calibration_quality_metrics method is not supported by IqmServerClient.")
930
949
 
931
950
  if not calibration_set_id:
932
951
  # find out the default calset id
@@ -936,5 +955,4 @@ class IQMClient:
936
955
  calset_obs = self._station_control.get_observation_set_observations(calibration_set_id)
937
956
  quality_metrics = self._station_control.get_calibration_set_quality_metrics(calibration_set_id)
938
957
  qm_obs = quality_metrics.observations
939
-
940
- return ObservationFinder(list(calset_obs) + qm_obs)
958
+ return ObservationFinder(calset_obs + qm_obs)
iqm/iqm_client/util.py CHANGED
@@ -14,12 +14,10 @@
14
14
  """Helpful utilities that can be used together with IQMClient."""
15
15
 
16
16
  from json import JSONEncoder, dumps, loads
17
- from typing import Any, TypeVar
17
+ from typing import Any
18
18
 
19
19
  import numpy as np
20
20
 
21
- T = TypeVar("T")
22
-
23
21
 
24
22
  class IQMJSONEncoder(JSONEncoder):
25
23
  """JSONEncoder that that adds support for some non-JSON datatypes"""
@@ -36,8 +36,8 @@ def resonance_example(server_url: str, api_token: str | None) -> dict[str, int]:
36
36
  """
37
37
  SHOTS = 1000
38
38
 
39
- # Initialize a backend
40
- backend = IQMProvider(server_url, token=api_token).get_backend()
39
+ # Initialize a backend without metrics as IQMClient._get_calibration_quality_metrics is not supported by resonance
40
+ backend = IQMProvider(server_url, token=api_token).get_backend(use_metrics=False)
41
41
 
42
42
  # Just to make sure that "get_static_quantum_architecture" method works
43
43
  static_quantum_architecture = backend.client.get_static_quantum_architecture()
@@ -44,5 +44,4 @@ def IQMFakeAdonis() -> IQMFakeBackend:
44
44
  },
45
45
  name="sample-chip",
46
46
  )
47
-
48
47
  return IQMFakeBackend(architecture, error_profile, name="IQMFakeAdonisBackend")
@@ -16,69 +16,89 @@
16
16
  from __future__ import annotations
17
17
 
18
18
  from abc import ABC
19
- import itertools
19
+ import logging
20
20
  from typing import Final
21
21
 
22
22
  from iqm.iqm_client import (
23
23
  DynamicQuantumArchitecture,
24
- GateImplementationInfo,
25
- GateInfo,
24
+ ObservationFinder,
26
25
  )
27
- from iqm.qiskit_iqm.move_gate import MoveGate
28
- from qiskit.circuit import Delay, Parameter, Reset
29
- from qiskit.circuit.library import CZGate, IGate, Measure, RGate
26
+ from iqm.qiskit_iqm.iqm_target import IQMTarget
30
27
  from qiskit.providers import BackendV2
31
28
  from qiskit.transpiler import Target
32
29
 
33
30
  IQM_TO_QISKIT_GATE_NAME: Final[dict[str, str]] = {"prx": "r", "cz": "cz"}
34
-
35
- Locus = tuple[str, ...]
36
- LocusIdx = tuple[int, ...]
31
+ logger = logging.getLogger(__name__)
37
32
 
38
33
 
39
34
  class IQMBackendBase(BackendV2, ABC):
40
35
  """Abstract base class for various IQM-specific backends.
41
36
 
42
37
  Args:
43
- architecture: Description of the quantum architecture associated with the backend instance.
38
+ architecture: Dynamic quantum architecture associated with the backend instance.
39
+ metrics: Optional calibration data and related quality metrics for the transpilation target.
40
+ name: Optional name for the backend instance.
44
41
 
45
42
  """
46
43
 
47
44
  def __init__(
48
45
  self,
49
46
  architecture: DynamicQuantumArchitecture,
47
+ *,
48
+ metrics: ObservationFinder | None = None,
49
+ name: str = "IQMBackend",
50
50
  **kwargs,
51
51
  ):
52
- super().__init__(**kwargs)
52
+ super().__init__(name=name, **kwargs)
53
53
  self.architecture = architecture
54
- """Dynamic quantum architecture of the backend instance."""
55
-
54
+ self.metrics = metrics
56
55
  # Qiskit uses integer indices to refer to qubits, so we need to map component names to indices.
57
56
  # Because of the way the Target and the transpiler interact, the resonators need to have higher indices than
58
57
  # qubits, or else transpiling with optimization_level=0 will fail because of lacking resonator indices.
59
58
  qb_to_idx = {qb: idx for idx, qb in enumerate(architecture.qubits + architecture.computational_resonators)}
60
59
 
61
- self._target = IQMTarget(architecture, component_to_idx=qb_to_idx, include_resonators=False)
62
- self._fake_target_with_moves = (
63
- IQMTarget(architecture, component_to_idx=qb_to_idx, include_resonators=True)
60
+ # NOTE: both targets include fictional CZs
61
+ self._target = IQMTarget(
62
+ architecture=architecture,
63
+ component_to_idx=qb_to_idx,
64
+ include_resonators=False,
65
+ include_fictional_czs=True,
66
+ metrics=metrics,
67
+ )
68
+
69
+ self._target_with_resonators = (
70
+ IQMTarget(
71
+ architecture=architecture,
72
+ component_to_idx=qb_to_idx,
73
+ include_resonators=True,
74
+ include_fictional_czs=True,
75
+ metrics=metrics,
76
+ )
64
77
  if "move" in architecture.gates
65
78
  else None
66
79
  )
80
+
67
81
  self._qb_to_idx = qb_to_idx
68
82
  self._idx_to_qb = {v: k for k, v in qb_to_idx.items()}
69
- self.name = "IQMBackend"
70
83
  self._coupling_map = self.target.build_coupling_map()
71
84
 
72
85
  @property
73
86
  def target(self) -> Target:
87
+ """Return the target without computational resonators."""
74
88
  return self._target
75
89
 
76
90
  @property
77
91
  def target_with_resonators(self) -> Target:
78
- """Return the target with MOVE gates and resonators included."""
79
- if self._fake_target_with_moves is None:
80
- return self.target
81
- return self._fake_target_with_moves
92
+ """Return the target with MOVE gates and resonators included.
93
+
94
+ Raises:
95
+ ValueError: The backend does not have resonators.
96
+
97
+ """
98
+ target = self._target_with_resonators
99
+ if target is None:
100
+ raise ValueError("The backend does not have computational resonators.")
101
+ return target
82
102
 
83
103
  @property
84
104
  def physical_qubits(self) -> list[str]:
@@ -90,12 +110,13 @@ class IQMBackendBase(BackendV2, ABC):
90
110
  return bool(self.architecture.computational_resonators)
91
111
 
92
112
  def get_real_target(self) -> Target:
93
- """Return the real physical target of the backend without virtual CZ gates."""
113
+ """Return the real physical target of the backend without fictional CZ gates."""
94
114
  return IQMTarget(
95
115
  architecture=self.architecture,
96
116
  component_to_idx=self._qb_to_idx,
97
117
  include_resonators=True,
98
- include_fake_czs=False,
118
+ include_fictional_czs=False,
119
+ metrics=self.metrics,
99
120
  )
100
121
 
101
122
  def qubit_name_to_index(self, name: str) -> int:
@@ -135,211 +156,3 @@ class IQMBackendBase(BackendV2, ABC):
135
156
  def get_scheduling_stage_plugin(self) -> str:
136
157
  """Return the plugin that should be used for scheduling the circuits on this backend."""
137
158
  return "iqm_default_scheduling"
138
-
139
- def restrict_to_qubits(
140
- self,
141
- qubits: list[int] | list[str],
142
- include_resonators: bool = False,
143
- include_fake_czs: bool = True,
144
- ) -> IQMTarget:
145
- """Generated a restricted transpilation target from this backend that only contains the given qubits.
146
-
147
- Args:
148
- qubits: Qubits to restrict the target to. Can be either a list of qubit indices or qubit names.
149
- include_resonators: Whether to restrict `self.target` or `self.target_with_resonators`.
150
- include_fake_czs: Whether to include virtual CZs that are unsupported, but could be routed via MOVE.
151
-
152
- Returns:
153
- restricted target
154
-
155
- """
156
- qubits_str = [self._idx_to_qb[q] if isinstance(q, int) else str(q) for q in qubits]
157
- return _restrict_dqa_to_qubits(self.architecture, qubits_str, include_resonators, include_fake_czs)
158
-
159
-
160
- def _restrict_dqa_to_qubits(
161
- architecture: DynamicQuantumArchitecture,
162
- qubits: list[str],
163
- include_resonators: bool,
164
- include_fake_czs: bool = True,
165
- ) -> IQMTarget:
166
- """Generated a restricted transpilation target from this backend that only contains the given qubits.
167
-
168
- Args:
169
- architecture: The dynamic quantum architecture to restrict.
170
- qubits: Qubits to restrict the target to. Can be either a list of qubit indices or qubit names.
171
- include_resonators: Whether to include MOVE gates in the target.
172
- include_fake_czs: Whether to include virtual CZs that are not natively supported, but could be routed via MOVE.
173
-
174
- Returns:
175
- restricted target
176
-
177
- """
178
- new_gates = {}
179
- for gate_name, gate_info in architecture.gates.items():
180
- new_implementations = {}
181
- for (
182
- implementation_name,
183
- implementation_info,
184
- ) in gate_info.implementations.items():
185
- new_loci = tuple(locus for locus in implementation_info.loci if all(q in qubits for q in locus))
186
- if new_loci:
187
- new_implementations[implementation_name] = GateImplementationInfo(loci=new_loci)
188
- if new_implementations:
189
- new_gates[gate_name] = GateInfo(
190
- implementations=new_implementations,
191
- default_implementation=gate_info.default_implementation,
192
- override_default_implementation=gate_info.override_default_implementation,
193
- )
194
- new_arch = DynamicQuantumArchitecture(
195
- calibration_set_id=architecture.calibration_set_id,
196
- qubits=[q for q in qubits if q in architecture.qubits],
197
- computational_resonators=[q for q in qubits if q in architecture.computational_resonators],
198
- gates=new_gates,
199
- )
200
- return IQMTarget(
201
- new_arch,
202
- component_to_idx={name: idx for idx, name in enumerate(qubits)},
203
- include_resonators=include_resonators,
204
- include_fake_czs=include_fake_czs,
205
- )
206
-
207
-
208
- class IQMTarget(Target):
209
- """Transpilation target for an IQM architecture.
210
-
211
- Contains the mapping of physical qubit name on the device to qubit index in the Target.
212
-
213
- Args:
214
- architecture: Quantum architecture that defines the target.
215
- component_to_idx: Mapping from QPU component names to integer indices used by Qiskit to refer to them.
216
- include_resonators: Whether to include MOVE gates in the target.
217
- include_fake_czs: Whether to include virtual CZs that are not natively supported, but could be routed via MOVE.
218
-
219
- """
220
-
221
- def __init__(
222
- self,
223
- architecture: DynamicQuantumArchitecture,
224
- component_to_idx: dict[str, int],
225
- include_resonators: bool,
226
- include_fake_czs: bool = True,
227
- ):
228
- super().__init__()
229
- # Using iqm as a prefix to avoid name clashes with the base class.
230
- self.iqm_dqa = architecture
231
- self.iqm_component_to_idx = component_to_idx
232
- self.iqm_idx_to_component = {v: k for k, v in component_to_idx.items()}
233
- self.iqm_includes_resonators = include_resonators
234
- self.iqm_includes_fake_czs = include_fake_czs
235
- self._add_connections_from_DQA()
236
-
237
- def _add_connections_from_DQA(self):
238
- """Initializes the Target, making it represent the dynamic quantum architecture :attr:`iqm_dqa`."""
239
- # mapping from op name to all its allowed loci
240
- architecture = self.iqm_dqa
241
- component_to_idx = self.iqm_component_to_idx
242
- op_loci = {gate_name: gate_info.loci for gate_name, gate_info in architecture.gates.items()}
243
-
244
- def locus_to_idx(locus: Locus) -> LocusIdx:
245
- """Map the given locus to use component indices instead of component names."""
246
- return tuple(component_to_idx[component] for component in locus)
247
-
248
- def create_properties(name: str, *, symmetrize: bool = False) -> dict[tuple[int, ...], None]:
249
- """Creates the Qiskit instruction properties dictionary for the given IQM native operation.
250
-
251
- Currently we do not provide any actual properties for the operation, hence the all the
252
- allowed loci map to None.
253
- """
254
- if self.iqm_includes_resonators:
255
- loci = op_loci[name]
256
- else:
257
- # Remove the loci that correspond to resonators.
258
- loci = [
259
- locus for locus in op_loci[name] if all(component in self.iqm_dqa.qubits for component in locus)
260
- ]
261
- if symmetrize:
262
- # symmetrize the loci
263
- loci = tuple(permuted_locus for locus in loci for permuted_locus in itertools.permutations(locus))
264
- return {locus_to_idx(locus): None for locus in loci}
265
-
266
- # like barrier, delay is always available for all single-qubit loci
267
- self.add_instruction(Delay(0), {locus_to_idx((q,)): None for q in architecture.qubits})
268
-
269
- if "measure" in op_loci:
270
- self.add_instruction(Measure(), create_properties("measure"))
271
-
272
- # identity gate does nothing and is removed in serialization, so we may as well allow it everywhere
273
- # Except if it is defined for the resonator, the graph is disconnected and the transpiler will fail.
274
- if self.iqm_includes_resonators:
275
- self.add_instruction(
276
- IGate(),
277
- {locus_to_idx((component,)): None for component in architecture.components},
278
- )
279
- else:
280
- self.add_instruction(
281
- IGate(),
282
- {locus_to_idx((component,)): None for component in architecture.qubits},
283
- )
284
-
285
- if "prx" in op_loci:
286
- self.add_instruction(
287
- RGate(Parameter("theta"), Parameter("phi")),
288
- create_properties("prx"),
289
- )
290
-
291
- # HACK reset gate shares cc_prx loci for now
292
- if "cc_prx" in op_loci:
293
- self.add_instruction(Reset(), create_properties("cc_prx"))
294
-
295
- if self.iqm_includes_resonators and "move" in op_loci:
296
- self.add_instruction(MoveGate(), create_properties("move"))
297
-
298
- if "cz" in op_loci:
299
- if self.iqm_includes_fake_czs and "move" in op_loci:
300
- # CZ and MOVE: star
301
- cz_connections: dict[LocusIdx, None] = {}
302
- cz_loci = op_loci["cz"]
303
- for c1, c2 in cz_loci:
304
- if self.iqm_includes_resonators or all(component in self.iqm_dqa.qubits for component in (c1, c2)):
305
- idx_locus = locus_to_idx((c1, c2))
306
- cz_connections[idx_locus] = None
307
-
308
- for c1, res in op_loci["move"]:
309
- for c2 in architecture.qubits:
310
- if c2 not in [c1, res]:
311
- # loop over c2 that is not c1
312
- if (c2, res) in cz_loci or (res, c2) in cz_loci:
313
- # This is a fake CZ and can be bidirectional.
314
- # cz routable via res between qubits, put into fake_cz_conn both ways
315
- idx_locus = locus_to_idx((c1, c2))
316
- cz_connections[idx_locus] = None
317
- cz_connections[idx_locus[::-1]] = None
318
- self.add_instruction(CZGate(), cz_connections)
319
- else:
320
- # CZ but no MOVE: crystal
321
- self.add_instruction(CZGate(), create_properties("cz"))
322
-
323
- @property
324
- def physical_qubits(self) -> list[str]:
325
- """Return the ordered list of physical qubits in the backend."""
326
- # Overrides the property from the superclass to contain the correct information.
327
- return [self.iqm_idx_to_component[i] for i in range(self.num_qubits)]
328
-
329
- def restrict_to_qubits(self, qubits: list[int] | list[str]) -> IQMTarget:
330
- """Generated a restricted transpilation target from this Target that only contains the given qubits.
331
-
332
- Args:
333
- qubits: Qubits to restrict the target to. Can be either a list of qubit indices or qubit names.
334
-
335
- Returns:
336
- restricted target
337
-
338
- """
339
- qubits_str = [self.iqm_idx_to_component[q] if isinstance(q, int) else str(q) for q in qubits]
340
- return _restrict_dqa_to_qubits(
341
- self.iqm_dqa,
342
- qubits_str,
343
- self.iqm_includes_resonators,
344
- self.iqm_includes_fake_czs,
345
- )
@@ -11,12 +11,10 @@
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
- """Generate an initial layout for a quantum circuit that is
15
- valid on the quantum architecture specification of the given backend.
16
- """
14
+ """Generate an initial layout for a quantum circuit containing MOVE gates."""
17
15
 
18
16
  from iqm.iqm_client import DynamicQuantumArchitecture
19
- from iqm.qiskit_iqm.iqm_backend import IQMBackendBase, IQMTarget
17
+ from iqm.qiskit_iqm.iqm_backend import IQMTarget
20
18
  from qiskit import QuantumCircuit
21
19
  from qiskit.circuit import Qubit
22
20
  from qiskit.dagcircuit import DAGCircuit
@@ -228,26 +226,20 @@ class IQMMoveLayout(TrivialLayout):
228
226
 
229
227
 
230
228
  def generate_initial_layout(
231
- backend: IQMBackendBase,
229
+ target: IQMTarget,
232
230
  circuit: QuantumCircuit,
233
- restrict_to_qubits: list[int] | list[str] | None = None,
234
231
  ) -> Layout:
235
- """Generates an initial layout for the given circuit, when run against the given backend.
232
+ """Generate an initial layout for the given circuit, for running on the given Star architecture target.
236
233
 
237
234
  Args:
238
- backend: IQM backend to run against.
239
- circuit: Star architecture circuit for which a layout is to be generated.
240
- restrict_to_qubits: Optional list of qubits to restrict the layout to.
235
+ target: IQM Star architecture target to run the circuit on, without fictional gates.
236
+ circuit: Quantum circuit for which a layout is to be generated.
241
237
 
242
238
  Returns:
243
- Layout that maps the logical qubits of ``circuit`` to the physical qubits of ``backend`` so that
239
+ Layout that maps the virtual/logical qubits of ``circuit`` to the physical qubits of ``target`` so that
244
240
  all the gates in ``circuit`` are available on those loci.
245
241
 
246
242
  """
247
- target = backend.get_real_target()
248
- if restrict_to_qubits is not None:
249
- target = target.restrict_to_qubits(restrict_to_qubits)
250
-
251
243
  layout_gen = IQMMoveLayout(target)
252
244
  pm = PassManager(layout_gen)
253
245
  pm.run(circuit)
@@ -200,7 +200,7 @@ def _get_scheduling_method(
200
200
  def transpile_to_IQM( # noqa: PLR0913
201
201
  circuit: QuantumCircuit,
202
202
  backend: IQMBackendBase,
203
- target: IQMTarget | None = None,
203
+ *,
204
204
  initial_layout: Layout | dict | list | None = None,
205
205
  perform_move_routing: bool = True,
206
206
  optimize_single_qubits: bool = True,
@@ -214,16 +214,14 @@ def transpile_to_IQM( # noqa: PLR0913
214
214
 
215
215
  Works with both the Crystal and Star architectures.
216
216
 
217
- Note: When transpiling a circuit with MOVE gates, you might need to set the `optimization_level` lower.
218
- If the `optimization_level` is set too high, the transpiler might add single qubit gates onto the resonator,
217
+ Note: When transpiling a circuit with MOVE gates, you might need to set ``optimization_level`` lower.
218
+ If ``optimization_level`` is set too high, the transpiler might add single qubit gates onto the resonator,
219
219
  which is not supported by the IQM Star architectures. If this in undesired, it is best to have the transpiler
220
220
  add the MOVE gates automatically, rather than manually adding them to the circuit.
221
221
 
222
222
  Args:
223
- circuit: The circuit to be transpiled without MOVE gates.
224
- backend: The target backend to compile to. Does not require a resonator.
225
- target: An alternative target to compile to than the backend, using this option requires intimate knowledge
226
- of the transpiler and thus it is not recommended to use.
223
+ circuit: The circuit to transpile.
224
+ backend: The backend to transpile to.
227
225
  initial_layout: The initial layout to use for the transpilation, same as :func:`~qiskit.compiler.transpile`.
228
226
  perform_move_routing: Whether to perform MOVE gate routing.
229
227
  optimize_single_qubits: Whether to optimize single qubit gates away.
@@ -233,30 +231,35 @@ def transpile_to_IQM( # noqa: PLR0913
233
231
  existing_moves_handling: How to handle existing MOVE gates in the circuit, required if the circuit contains
234
232
  MOVE gates.
235
233
  restrict_to_qubits: Restrict the transpilation to only use these specific physical qubits. Note that you will
236
- have to pass this information to the ``backend.run`` method as well as a dictionary.
234
+ also have to pass this information to :meth:`.IQMBackend.run` using the ``qubit_mapping`` parameter.
237
235
  qiskit_transpiler_kwargs: Arguments to be passed to the Qiskit transpiler.
238
236
 
239
237
  Returns:
240
238
  Transpiled circuit ready for running on the backend.
241
239
 
242
240
  """
243
- if restrict_to_qubits is not None:
244
- restrict_to_qubits = [backend.qubit_name_to_index(q) if isinstance(q, str) else q for q in restrict_to_qubits]
245
-
246
- if target is None:
247
- if circuit.count_ops().get("move", 0) > 0 or circuit.num_qubits > backend.num_qubits:
248
- target = backend.target_with_resonators
249
- # Create a sensible initial layout if none is provided
250
- if initial_layout is None:
251
- initial_layout = generate_initial_layout(backend, circuit, restrict_to_qubits)
252
- if perform_move_routing and existing_moves_handling is None:
253
- raise ValueError("The circuit contains MOVE gates but existing_moves_handling is not set.")
254
- else:
255
- target = backend.target
241
+ circuit_has_moves = circuit.count_ops().get("move", 0) > 0
242
+ # get the target from the backend
243
+ if circuit_has_moves:
244
+ target = backend.target_with_resonators
245
+ if perform_move_routing and existing_moves_handling is None:
246
+ raise ValueError("The circuit contains MOVE gates but existing_moves_handling is not set.")
247
+ else:
248
+ target = backend.target
256
249
 
257
250
  if restrict_to_qubits is not None:
251
+ # qubit names to qiskit indices
252
+ restrict_to_qubits = [backend.qubit_name_to_index(q) if isinstance(q, str) else q for q in restrict_to_qubits]
258
253
  target = target.restrict_to_qubits(restrict_to_qubits)
259
254
 
255
+ if circuit_has_moves and initial_layout is None:
256
+ # Create a sensible initial layout if none is provided, since
257
+ # the standard Qiskit transpile function does not do this well with resonators.
258
+ real_target = backend.get_real_target()
259
+ if restrict_to_qubits is not None:
260
+ real_target = real_target.restrict_to_qubits(restrict_to_qubits)
261
+ initial_layout = generate_initial_layout(real_target, circuit)
262
+
260
263
  # Determine which scheduling method to use
261
264
  scheduling_method = qiskit_transpiler_kwargs.pop("scheduling_method", None)
262
265
  if scheduling_method is None:
@@ -30,7 +30,7 @@ from iqm.iqm_client import (
30
30
  RunRequest,
31
31
  )
32
32
  from iqm.iqm_client.util import to_json_dict
33
- from iqm.qiskit_iqm import IQMFakeAphrodite, IQMFakeApollo, IQMFakeDeneb
33
+ from iqm.qiskit_iqm import IQMFakeAphrodite, IQMFakeApollo, IQMFakeBackend, IQMFakeDeneb
34
34
  from iqm.qiskit_iqm.fake_backends import IQMFakeAdonis
35
35
  from iqm.qiskit_iqm.fake_backends.fake_garnet import IQMFakeGarnet
36
36
  from iqm.qiskit_iqm.iqm_backend import IQMBackendBase
@@ -51,20 +51,31 @@ class IQMBackend(IQMBackendBase):
51
51
  """Backend for executing quantum circuits on IQM quantum computers.
52
52
 
53
53
  Args:
54
- client: client instance for communicating with an IQM server
54
+ client: Client instance for communicating with an IQM server.
55
55
  calibration_set_id: ID of the calibration set the backend will use.
56
56
  ``None`` means the IQM server will be queried for the current default
57
57
  calibration set.
58
- kwargs: optional arguments to be passed to the parent Backend initializer
58
+ use_metrics: Iff True, the backend will query the server for calibration data and related
59
+ quality metrics, and pass these to the transpilation target(s).
60
+ kwargs: Optional arguments to be passed to the parent Backend initializer.
59
61
 
60
62
  """
61
63
 
62
- def __init__(self, client: IQMClient, *, calibration_set_id: str | UUID | None = None, **kwargs):
64
+ def __init__(
65
+ self,
66
+ client: IQMClient,
67
+ *,
68
+ calibration_set_id: str | UUID | None = None,
69
+ use_metrics: bool = True,
70
+ **kwargs,
71
+ ):
63
72
  if calibration_set_id is not None and not isinstance(calibration_set_id, UUID):
64
73
  calibration_set_id = UUID(calibration_set_id)
74
+
65
75
  self._use_default_calibration_set = calibration_set_id is None
66
76
  architecture = client.get_dynamic_quantum_architecture(calibration_set_id)
67
- super().__init__(architecture, **kwargs)
77
+ metrics = client._get_calibration_quality_metrics(architecture.calibration_set_id) if use_metrics else None
78
+ super().__init__(architecture, metrics=metrics, **kwargs)
68
79
  self.client: IQMClient = client
69
80
  self._max_circuits: int | None = None
70
81
  self.name = "IQM Backend"
@@ -266,10 +277,10 @@ class IQMBackend(IQMBackendBase):
266
277
  return Circuit(name=circuit.name, instructions=instructions, metadata=metadata)
267
278
 
268
279
 
269
- facade_names = {
280
+ facade_names: dict[str, IQMFakeBackend] = {
270
281
  "facade_adonis": IQMFakeAdonis(),
271
- "facade_aphrodite": IQMFakeAphrodite(),
272
282
  "facade_apollo": IQMFakeApollo(),
283
+ "facade_aphrodite": IQMFakeAphrodite(),
273
284
  "facade_deneb": IQMFakeDeneb(),
274
285
  "facade_garnet": IQMFakeGarnet(),
275
286
  }
@@ -280,17 +291,38 @@ class IQMFacadeBackend(IQMBackend):
280
291
 
281
292
  Can be used to submit a circuit to a mock IQM server that has no real quantum hardware,
282
293
  and if the mock execution is successful, simulates the circuit locally using an error model that
283
- is representative of the mocked QPU. Finally returns the simulated results.
294
+ is broadly representative of the mocked QPU. Finally returns the *simulated results*.
295
+
296
+ .. note::
297
+
298
+ There is usually no point in using a facade backend with a real quantum computer, since the computation
299
+ results will be replaced with a local simulation.
284
300
 
285
301
  Args:
286
302
  client: Client instance for communicating with an IQM server.
287
- **kwargs: Optional arguments to be passed to the parent class.
303
+ name: Name of the fake backend (simulator instance) to use. If None, will be determined automatically based
304
+ on the static quantum architecture of the server.
305
+ kwargs: Optional arguments to be passed to the parent class.
288
306
 
289
307
  """
290
308
 
291
- def __init__(self, client: IQMClient, **kwargs):
309
+ def __init__(self, client: IQMClient, *, name: str | None = None, **kwargs):
292
310
  super().__init__(client, **kwargs)
293
- self.backend = self._determine_facade_backend_from_sqa()
311
+ sqa = self.client.get_static_quantum_architecture()
312
+ if name is None:
313
+ # use a fake backend (local simulator) that matches the server
314
+ for backend in facade_names.values():
315
+ if backend.validate_compatible_architecture(sqa):
316
+ self._fake_backend = backend
317
+ return
318
+ raise ValueError("Quantum architecture of the server does not match any known IQMFakeBackend.")
319
+ else:
320
+ if name not in facade_names:
321
+ raise ValueError(f"Unknown facade backend: {name}")
322
+ backend = facade_names[name]
323
+ if not backend.validate_compatible_architecture(sqa):
324
+ raise ValueError("Quantum architecture of the server does not match the requested IQMFakeBackend.")
325
+ self._fake_backend = backend
294
326
 
295
327
  def _validate_no_empty_cregs(self, circuit: QuantumCircuit) -> bool:
296
328
  """Returns True if given circuit has no empty (unused) classical registers, False otherwise."""
@@ -303,26 +335,20 @@ class IQMFacadeBackend(IQMBackend):
303
335
  return False
304
336
  return True
305
337
 
306
- def _determine_facade_backend_from_sqa(self) -> IQMFacadeBackend:
307
- for backend in facade_names.values():
308
- if backend.validate_compatible_architecture(self.client.get_static_quantum_architecture()):
309
- return backend
310
- raise ValueError("Quantum architecture of the remote quantum computer does not match facade input.")
311
-
312
338
  def run(self, run_input: QuantumCircuit | list[QuantumCircuit], **options) -> JobV1:
313
339
  circuits = [run_input] if isinstance(run_input, QuantumCircuit) else run_input
314
340
  circuits_validated_cregs: list[bool] = [self._validate_no_empty_cregs(circuit) for circuit in circuits]
315
341
  if not all(circuits_validated_cregs):
316
342
  raise ValueError(
317
- "One or more circuits contain unused classical registers. This is not allowed for Facade simulation, "
318
- "see user guide."
343
+ "One or more circuits contain unused classical registers. This is not allowed for facade simulation, "
344
+ "see the user guide."
319
345
  )
320
346
 
321
347
  iqm_backend_job = super().run(run_input, **options)
322
348
  iqm_backend_job.result() # get and discard results
323
349
  if iqm_backend_job.status() == JobStatus.ERROR:
324
350
  raise RuntimeError("Remote execution did not succeed.")
325
- return self.backend.run(run_input, **options)
351
+ return self._fake_backend.run(run_input, **options)
326
352
 
327
353
 
328
354
  class IQMProvider:
@@ -334,7 +360,7 @@ class IQMProvider:
334
360
  through to :class:`~iqm.iqm_client.iqm_client.IQMClient` as is, and are documented there.
335
361
 
336
362
  Args:
337
- url: URL of the IQM server (e.g. https://cocos.resonance.meetiqm.com/garnet)
363
+ url: URL of the IQM server (e.g. "https://cocos.resonance.meetiqm.com/garnet")
338
364
 
339
365
  """
340
366
 
@@ -343,24 +369,28 @@ class IQMProvider:
343
369
  self.user_auth_args = user_auth_args
344
370
 
345
371
  def get_backend(
346
- self, name: str | None = None, calibration_set_id: UUID | None = None
347
- ) -> IQMBackend | IQMFacadeBackend:
348
- """An IQMBackend instance associated with this provider.
372
+ self,
373
+ name: str | None = None,
374
+ calibration_set_id: UUID | None = None,
375
+ *,
376
+ use_metrics: bool = True,
377
+ ) -> IQMBackend:
378
+ """IQMBackend instance associated with this provider.
349
379
 
350
380
  Args:
351
- name: optional name of a custom facade backend
352
- calibration_set_id: ID of the calibration set used to create the transpilation target of the backend.
381
+ name: Optional name of a facade backend to request, see :class:`IQMFacadeBackend`.
382
+ calibration_set_id: ID of the calibration set to be used with the backend.
383
+ Affects both the transpilation target and the circuit execution.
353
384
  If None, the server default calibration set will be used.
385
+ use_metrics: Iff True, the backend will provide calibration data and related quality metrics
386
+ to the transpilation target to improve the transpilation.
387
+
388
+ Returns:
389
+ Backend instance for connecting to a quantum computer.
354
390
 
355
391
  """
356
392
  client = IQMClient(self.url, **self.user_auth_args)
357
393
 
358
394
  if name and name.startswith("facade_"):
359
- if name in facade_names:
360
- if not facade_names[name].validate_compatible_architecture(client.get_static_quantum_architecture()):
361
- raise ValueError("Quantum architecture of the remote quantum computer does not match facade input.")
362
- return IQMFacadeBackend(client)
363
-
364
- warnings.warn(f"Unknown facade backend: {name}. A regular backend associated with {self.url} will be used.")
365
-
366
- return IQMBackend(client, calibration_set_id=calibration_set_id)
395
+ return IQMFacadeBackend(client, name=name, calibration_set_id=calibration_set_id, use_metrics=use_metrics)
396
+ return IQMBackend(client, calibration_set_id=calibration_set_id, use_metrics=use_metrics)
@@ -0,0 +1,372 @@
1
+ # Copyright 2022-2025 Qiskit on IQM developers
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """Transpilation target for IQM quantum computers."""
15
+
16
+ from __future__ import annotations
17
+
18
+ from collections.abc import Iterable
19
+ import logging
20
+ from typing import TypeAlias
21
+
22
+ from iqm.iqm_client import (
23
+ DynamicQuantumArchitecture,
24
+ GateImplementationInfo,
25
+ GateInfo,
26
+ ObservationFinder,
27
+ )
28
+ from iqm.qiskit_iqm.move_gate import MoveGate
29
+ from qiskit.circuit import Delay, Gate, Parameter, Reset
30
+ from qiskit.circuit.library import CZGate, IGate, Measure, RGate
31
+ from qiskit.providers import QubitProperties
32
+ from qiskit.transpiler import InstructionProperties, Target
33
+
34
+ Locus: TypeAlias = tuple[str, ...]
35
+ """Sequence of QPU component names on which a gate acts."""
36
+ LocusIdx: TypeAlias = tuple[int, ...]
37
+ """Sequence of qubit indices on which a gate acts."""
38
+
39
+
40
+ _QISKIT_IQM_GATE_MAP: dict[str, Gate] = {
41
+ "prx": RGate(Parameter("theta"), Parameter("phi")),
42
+ "measure": Measure(),
43
+ "reset": Reset(),
44
+ "cz": CZGate(),
45
+ "move": MoveGate(),
46
+ "id": IGate(),
47
+ }
48
+ """Maps IQM native operation names to corresponding Qiskit gate objects."""
49
+
50
+ logger = logging.getLogger(__name__)
51
+
52
+
53
+ class IQMTarget(Target):
54
+ """Transpilation target for an IQM architecture.
55
+
56
+ Contains the mapping of physical qubit name on the device to qubit index in the Target.
57
+
58
+ Args:
59
+ architecture: Quantum architecture that defines the target.
60
+ component_to_idx: Mapping from QPU component names to integer indices used by Qiskit to refer to them.
61
+ include_resonators: Whether to include computational resonators (and MOVE gates) in the target,
62
+ if present in ``architecture``.
63
+ include_fictional_czs: Whether to include "fictional" CZs that are not natively supported,
64
+ but could be routed using MOVE.
65
+ metrics: Optional calibration data and related quality metrics to improve the transpilation.
66
+
67
+ """
68
+
69
+ def __init__(
70
+ self,
71
+ *,
72
+ architecture: DynamicQuantumArchitecture,
73
+ component_to_idx: dict[str, int],
74
+ include_resonators: bool,
75
+ include_fictional_czs: bool,
76
+ metrics: ObservationFinder | None = None,
77
+ ):
78
+ super().__init__()
79
+ # HACK: As of Qiskit 1.3, Target.__init__ does nothing with its args, instead Target.__new__
80
+ # handles them. IQMTarget.__init__ has different args than Target.__init__, hence we must either
81
+ # (1) define IQMTarget.__new__ with matching args that calls Target.__new__, or
82
+ # (2) make all the IQMTarget.__init__ args keyword-only with non-colliding names, and init
83
+ # the necessary superclass attributes ourselves.
84
+ # (1) seems to break pickling during concurrent transpilation using Qiskit's ``transpile``, so we use (2).
85
+ self.qubit_properties = self._create_qubit_properties(architecture.qubits, metrics)
86
+
87
+ # Using iqm_ as a prefix to avoid name clashes with the base class.
88
+ self.iqm_dqa = architecture
89
+ self.iqm_component_to_idx = component_to_idx
90
+ self.iqm_idx_to_component = {v: k for k, v in component_to_idx.items()}
91
+ self.iqm_include_resonators = include_resonators
92
+ self.iqm_include_fictional_czs = include_fictional_czs
93
+ self.iqm_metrics = metrics
94
+ self._add_instructions_from_DQA()
95
+
96
+ def _add_instructions_from_DQA(self):
97
+ """Initializes the Target with instructions and properties that represent the
98
+ dynamic quantum architecture :attr:`iqm_dqa`.
99
+
100
+ """
101
+ dqa = self.iqm_dqa
102
+ # mapping from op name to all its allowed loci
103
+ op_loci = {gate_name: gate_info.loci for gate_name, gate_info in dqa.gates.items()}
104
+
105
+ def add_gate(gate: str) -> None:
106
+ """Adds ``gate`` instructions to the Target."""
107
+ props = {self.locus_to_idx(locus): self._create_gate_properties(gate, locus) for locus in op_loci[gate]}
108
+ self.add_instruction(_QISKIT_IQM_GATE_MAP[gate], props)
109
+
110
+ # identity gate does nothing and is removed in serialization, so we may as well allow it everywhere
111
+ # Except if it is defined for the resonator, the graph is disconnected and the transpiler will fail.
112
+ self.add_instruction(
113
+ IGate(),
114
+ {
115
+ self.locus_to_idx((component,)): None
116
+ for component in (dqa.components if self.iqm_include_resonators else dqa.qubits)
117
+ },
118
+ )
119
+
120
+ # like barrier, delay is always available for all single-qubit loci
121
+ self.add_instruction(Delay(0), {self.locus_to_idx((q,)): None for q in dqa.qubits})
122
+
123
+ if "measure" in op_loci:
124
+ add_gate("measure")
125
+
126
+ if "prx" in op_loci:
127
+ add_gate("prx")
128
+
129
+ # HACK reset gate shares cc_prx loci for now, until reset is also in the DQA/metrics
130
+ if "cc_prx" in op_loci:
131
+ self.add_instruction(
132
+ _QISKIT_IQM_GATE_MAP["reset"],
133
+ {self.locus_to_idx(locus): None for locus in op_loci["cc_prx"]},
134
+ )
135
+
136
+ if self.iqm_include_resonators and "move" in op_loci:
137
+ add_gate("move")
138
+
139
+ if "cz" in op_loci:
140
+ self._add_instructions_cz_gates(op_loci)
141
+
142
+ def _add_instructions_cz_gates(self, op_loci: dict[str, Iterable[Locus]]) -> None:
143
+ """Adds CZ gate instructions to the Qiskit Target.
144
+
145
+ This method handles both "real" and "fictional" CZ gates based on the
146
+ provided operation loci.
147
+
148
+ 1. **Real CZ Gates**:
149
+ A CZ gate for a locus is considered "real" if the locus
150
+ exists in ``dynamic_quantum_architecture.gates['cz']``.
151
+ A "real" CZ instruction is added if either the target is configured to
152
+ include resonators (:attr:`iqm_include_resonators` is True) or if all
153
+ the locus components are qubits.
154
+
155
+ 2. **Fictional CZ Gates**: For STAR QPU architectures, a "fictional" CZ between
156
+ two qubits that are not physically connected is defined
157
+ iff there is a CZ gate from one qubit to a resonator, and a MOVE gate
158
+ from the other qubit to the resonator.
159
+ See :mod:`iqm.iqm_client.transpile`.
160
+
161
+ Args:
162
+ op_loci: Mapping of operation names (e.g., "cz", "move") to their available loci.
163
+
164
+ """
165
+ cz_props: dict[LocusIdx, InstructionProperties] = {}
166
+ cz_loci = op_loci["cz"]
167
+
168
+ # 1. Add real CZs
169
+ for locus in cz_loci:
170
+ if self.iqm_include_resonators or all(component in self.iqm_dqa.qubits for component in locus):
171
+ locus_idx = self.locus_to_idx(locus) # convert locus from IQM to Qiskit format
172
+ cz_props[locus_idx] = self._create_gate_properties("cz", locus)
173
+
174
+ # 2. Add fictional CZs if applicable
175
+ if self.iqm_include_fictional_czs and "move" in op_loci:
176
+ fictional_cz_loci = self._determine_fictional_cz_loci(op_loci["move"], cz_loci)
177
+ for q1, q2, res in fictional_cz_loci:
178
+ locus_idx = self.locus_to_idx((q1, q2))
179
+ props = self._create_fictional_cz_properties(q1, q2, res)
180
+ cz_props[locus_idx] = props
181
+ cz_props[locus_idx[::-1]] = props # fictional CZs are symmetric
182
+ # TODO: if the fictional CZ can be implemented using both CZ(a,r) and MOVE(b,r), and
183
+ # CZ(b,r) and MOVE(a,r), props will be only included for one implementation at random.
184
+ # This is not necessarily the implementation the transpiler chooses!
185
+
186
+ if cz_props:
187
+ self.add_instruction(_QISKIT_IQM_GATE_MAP["cz"], cz_props)
188
+
189
+ def _create_fictional_cz_properties(self, q1: str, q2: str, res: str) -> InstructionProperties:
190
+ """Create ``InstructionProperties`` for a fictional CZ gate.
191
+
192
+ A fictional CZ gate is comprised of the sequence MOVE(q2, res), CZ(q1, res), MOVE(q2, res).
193
+
194
+ The duration and error for the fictional CZ gate are calculated as follows:
195
+
196
+ - Duration: 2 * duration_move(q2, res) + duration_cz(q1, res)
197
+ - Error: 1 - (1 - error_move(q2, res)))^2 * (1 - error_cz(q1, res))
198
+
199
+ See https://arxiv.org/pdf/2503.10903 for more details.
200
+
201
+ Args:
202
+ q1: CZ qubit.
203
+ q2: MOVE qubit.
204
+ res: Shared computational resonator through which the fictional CZ gate is routed.
205
+
206
+ Returns:
207
+ Instruction properties for the fictional CZ gate.
208
+
209
+ """
210
+ if self.iqm_metrics is None:
211
+ return None
212
+
213
+ props_cz = self._create_gate_properties("cz", (q1, res))
214
+ props_move = self._create_gate_properties("move", (q2, res))
215
+
216
+ if props_cz is None or props_move is None:
217
+ return None
218
+
219
+ duration = None
220
+ if props_move.duration is not None and props_cz.duration is not None:
221
+ duration = 2 * props_move.duration + props_cz.duration
222
+
223
+ error: float | None = None
224
+ if props_move.error is not None and props_cz.error is not None:
225
+ error = 1 - (1 - props_move.error) ** 2 * (1 - props_cz.error)
226
+
227
+ return InstructionProperties(duration=duration, error=error)
228
+
229
+ @staticmethod
230
+ def _determine_fictional_cz_loci(move_loci: Iterable[Locus], cz_loci: Iterable[Locus]) -> list[Locus]:
231
+ """Determine fictional CZ loci, i.e. pairs of qubits between which we can implement a CZ using MOVE gates and
232
+ an intermediate computational resonator.
233
+
234
+ Only applicable for the Star architecture.
235
+
236
+ Args:
237
+ move_loci: The loci for which a MOVE operation is defined.
238
+ cz_loci: The loci for which a CZ operation is defined.
239
+
240
+ Returns:
241
+ Loci for fictional CZ gates, *including the resonator*. Each locus is given as ``(q1, q2, res)``.
242
+
243
+ """
244
+ fictional_cz_loci: list[Locus] = []
245
+ for q1, res1 in cz_loci:
246
+ for q2, res2 in move_loci:
247
+ if res1 == res2 and q1 != q2:
248
+ # shared resonator, different qubits
249
+ fictional_cz_loci.append((q1, q2, res1))
250
+
251
+ return fictional_cz_loci
252
+
253
+ def locus_to_idx(self, locus: Locus) -> LocusIdx:
254
+ """Map the given locus to use component indices instead of component names."""
255
+ return tuple(self.iqm_component_to_idx[component] for component in locus)
256
+
257
+ @staticmethod
258
+ def _create_qubit_properties(
259
+ qubits: Iterable[str], metrics: ObservationFinder | None
260
+ ) -> list[QubitProperties] | None:
261
+ """Creates qubit properties from the quality metrics."""
262
+ if metrics is None:
263
+ return None
264
+
265
+ qubit_props = []
266
+ t1_times, t2_times = metrics.get_coherence_times(qubits)
267
+ for q in qubits:
268
+ frequency = metrics.get_qubit_frequency(q)
269
+ qubit_props.append(
270
+ QubitProperties(
271
+ t1=t1_times.get(q),
272
+ t2=t2_times.get(q),
273
+ frequency=frequency,
274
+ )
275
+ )
276
+ return qubit_props
277
+
278
+ def _create_gate_properties(self, gate_name: str, locus: Locus) -> InstructionProperties | None:
279
+ """Creates InstructionProperties for a single gate on a specific locus.
280
+
281
+ Args:
282
+ gate_name: Name of the IQM native operation to look up properties for (e.g., 'cz' or 'move').
283
+ locus: Locus of the operation.
284
+
285
+ Returns:
286
+ Properties for the (default implementation of the) given gate at the given locus, or None if not available.
287
+
288
+ """
289
+ if self.iqm_metrics is None:
290
+ return None
291
+
292
+ # the properties are for the default implementation (other implementations may be available also)
293
+ impl_name = self.iqm_dqa.gates[gate_name].get_default_implementation(locus)
294
+ duration = self.iqm_metrics.get_gate_duration(gate_name, impl_name, locus)
295
+ fidelity = self.iqm_metrics.get_gate_fidelity(gate_name, impl_name, locus)
296
+ return InstructionProperties(duration=duration, error=None if fidelity is None else 1 - fidelity)
297
+
298
+ @property
299
+ def physical_qubits(self) -> list[str]:
300
+ """Return the ordered list of physical qubits in the target."""
301
+ # Overrides the property from the superclass to contain the correct information.
302
+ return [self.iqm_idx_to_component[i] for i in range(self.num_qubits)]
303
+
304
+ def restrict_to_qubits(self, qubits: list[int] | list[str]) -> IQMTarget:
305
+ """Generated a restricted transpilation target from this Target that only contains the given qubits.
306
+
307
+ Args:
308
+ qubits: Qubits to restrict the target to. Can be either a list of qubit indices or qubit names.
309
+
310
+ Returns:
311
+ restricted target
312
+
313
+ """
314
+ qubits_str = [self.iqm_idx_to_component[q] if isinstance(q, int) else str(q) for q in qubits]
315
+ return _restrict_dqa_to_qubits(
316
+ self.iqm_dqa,
317
+ qubits_str,
318
+ self.iqm_include_resonators,
319
+ self.iqm_include_fictional_czs,
320
+ metrics=self.iqm_metrics,
321
+ )
322
+
323
+
324
+ def _restrict_dqa_to_qubits(
325
+ architecture: DynamicQuantumArchitecture,
326
+ components: list[str],
327
+ include_resonators: bool,
328
+ include_fictional_czs: bool,
329
+ metrics: ObservationFinder | None = None,
330
+ ) -> IQMTarget:
331
+ """Generated a restricted transpilation target that only contains the given QPU components.
332
+
333
+ Args:
334
+ architecture: The dynamic quantum architecture to restrict.
335
+ components: QPU components to restrict the target to. Qubits first, then resonators.
336
+ include_resonators: Whether to include computational resonators (and MOVE gates) in the target.
337
+ include_fictional_czs: Whether to include fictional CZs that are not natively supported,
338
+ but could be routed via MOVE.
339
+ metrics: Optional calibration data and related quality metrics to improve the transpilation.
340
+
341
+ Returns:
342
+ restricted target
343
+
344
+ """
345
+ # include gate loci that only involve the given components
346
+ c_set = set(components)
347
+ new_gates = {}
348
+ for gate_name, gate_info in architecture.gates.items():
349
+ new_implementations = {}
350
+ for implementation_name, implementation_info in gate_info.implementations.items():
351
+ new_loci = tuple(locus for locus in implementation_info.loci if set(locus) <= c_set)
352
+ if new_loci:
353
+ new_implementations[implementation_name] = GateImplementationInfo(loci=new_loci)
354
+ if new_implementations:
355
+ new_gates[gate_name] = GateInfo(
356
+ implementations=new_implementations,
357
+ default_implementation=gate_info.default_implementation,
358
+ override_default_implementation=gate_info.override_default_implementation,
359
+ )
360
+ new_arch = DynamicQuantumArchitecture(
361
+ calibration_set_id=architecture.calibration_set_id,
362
+ qubits=[q for q in architecture.qubits if q in c_set],
363
+ computational_resonators=[r for r in architecture.computational_resonators if r in c_set],
364
+ gates=new_gates,
365
+ )
366
+ return IQMTarget(
367
+ architecture=new_arch,
368
+ component_to_idx={name: idx for idx, name in enumerate(components)},
369
+ include_resonators=include_resonators,
370
+ include_fictional_czs=include_fictional_czs,
371
+ metrics=metrics,
372
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: iqm-client
3
- Version: 29.13.0
3
+ Version: 30.0.0
4
4
  Summary: Client library for accessing an IQM quantum computer
5
5
  Author-email: IQM Finland Oy <developers@meetiqm.com>
6
6
  License: Apache License
@@ -21,25 +21,26 @@ iqm/iqm_client/__init__.py,sha256=D-8W54EcQIxk_1JZo_86GYlR1YitHhPIiFwwLJ2IfGE,14
21
21
  iqm/iqm_client/api.py,sha256=_c6OVuv2dyzBF7J2XlK_qxisTSPyOiI4gYokZPsuaJY,3083
22
22
  iqm/iqm_client/authentication.py,sha256=kHFqPI6w3OAk9k5ioPxi-FrD2EP-vjn8Z_wZYccJVyE,12259
23
23
  iqm/iqm_client/errors.py,sha256=ty2P-sg80zlAoL3_kC3PlprgDUv4PI-KFhmmxaaapS0,1429
24
- iqm/iqm_client/iqm_client.py,sha256=yrrpJlSW4N9TCp5zeokHk2lU8N5guYpU643HCMX5oOI,40994
24
+ iqm/iqm_client/iqm_client.py,sha256=oEeosKvXd_lVreB0r7TwHnYs6AAOjJs1sUDNk-AtzAk,41793
25
25
  iqm/iqm_client/models.py,sha256=Sdx_J7wBCM7E_arusU3eC6dQfqu5dpADywr-9JmFvsY,51597
26
26
  iqm/iqm_client/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
27
  iqm/iqm_client/transpile.py,sha256=eEv9eY5QG94Lke7Xp6BkQapl1rvlmlVQ_IkQFopPNQ8,36981
28
- iqm/iqm_client/util.py,sha256=FLRUhhi0YDxomVtilCVPJLixyijFtU10PVefIx-eelw,1516
28
+ iqm/iqm_client/util.py,sha256=hGhl-r9afN3kNOjV9FoheSAPcQiKu7DdwX0FTFXxFl4,1489
29
29
  iqm/iqm_client/validation.py,sha256=IKXBxnhLLnEuUCU2J0q6VEYn9k-O9z_MI4ys7mpFZdE,11912
30
30
  iqm/iqm_client/cli/__init__.py,sha256=zzLDDz5rc3lJke3OKU8zxR5zQyQoM9oI2bLJ2YKk_zQ,692
31
- iqm/iqm_client/cli/auth.py,sha256=SCOZ6QHw9l8oE-A1QSYfAKYuqZxkZkYrwaNWzGFY1AU,6363
31
+ iqm/iqm_client/cli/auth.py,sha256=kESEK9-vpEhrjba3Lb6Wqx24yGfbjxUASeCArnVRYrw,6364
32
32
  iqm/iqm_client/cli/cli.py,sha256=YhTgOGrvNXPwCdCKHcwLHeE_6C3n91MxFhvgQYT78C4,28633
33
33
  iqm/iqm_client/cli/models.py,sha256=Hu-t6c_07Cth3AuQBo0CDTcWVQg1xbJCpy_94V0o64U,1199
34
- iqm/iqm_client/cli/token_manager.py,sha256=e8M3UihnawxGlajAw5AP6x4TEH5PaCRQ4ADNnRw4wUI,6967
34
+ iqm/iqm_client/cli/token_manager.py,sha256=125uRj8kBzKlWAhQWNf-8n-aDG6fQridVd95qCktzD4,6867
35
35
  iqm/qiskit_iqm/__init__.py,sha256=Mv9V_r8ZcmGC8Ke5S8-7yLOx02vjZ1qiVx8mtbOpwnY,1420
36
- iqm/qiskit_iqm/iqm_backend.py,sha256=WMGBg1jbtTvrmJ7ZrXVixgfb6Hm5qKa-2xi5R8cl0IM,14237
36
+ iqm/qiskit_iqm/iqm_backend.py,sha256=HddizT6yHHq-MOG_U48n6ftE9AqmzaqbXYayEC1ljso,5548
37
37
  iqm/qiskit_iqm/iqm_circuit.py,sha256=fFQW8SRlgZjqZUOLfyuJhhXEDp5I1jopFWa1k4rb7ac,1384
38
38
  iqm/qiskit_iqm/iqm_circuit_validation.py,sha256=vE5CNyJOQ7OMRpQV-xsO1uf_NNFE8v6-TSzboFSrGYM,1721
39
39
  iqm/qiskit_iqm/iqm_job.py,sha256=Q26hk4JuZP48Xw3qVk4b44LrHbgNQp-mq_itF9umkqg,11666
40
- iqm/qiskit_iqm/iqm_move_layout.py,sha256=pHqV1G4bri3rFEsMBN6FrtQ0FXVNQG-Ymm4v7zdnilQ,10787
41
- iqm/qiskit_iqm/iqm_naive_move_pass.py,sha256=OQpKiiw-UoNdcjyqJ1Oay9zMsg5IihU-r4ZboXQptkA,12983
42
- iqm/qiskit_iqm/iqm_provider.py,sha256=5L2-GFbTqMlW7JTagRxl2pV2kw0KKA4hw2YGLYsoUvk,16640
40
+ iqm/qiskit_iqm/iqm_move_layout.py,sha256=vIsuO-RUJF0ZxB7nPhfOJ2vaUt6mZCLZfQGc6PEBwnc,10486
41
+ iqm/qiskit_iqm/iqm_naive_move_pass.py,sha256=jhTfvhrNDKt6NhhJg_3Y-5x6E1HRNzC_n4A27ZQTuvQ,12962
42
+ iqm/qiskit_iqm/iqm_provider.py,sha256=AGXi2Knm035VTNr2lMgSr5oNAuhIHJqEWI6Z5c1L9Y8,17996
43
+ iqm/qiskit_iqm/iqm_target.py,sha256=fj18g2-kvHztCZxcc3tUeoc4XZvhy_EAkn-kD59D8vw,15816
43
44
  iqm/qiskit_iqm/iqm_transpilation.py,sha256=2PyDAOukJpvSIVXmuYB8EWhmzlJHBUpzFPbW5VhfApg,9113
44
45
  iqm/qiskit_iqm/move_gate.py,sha256=QU9RKKVvbGq33qcIi9AKLcvQVQMibkgW4ibjzk-oVy4,2808
45
46
  iqm/qiskit_iqm/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -47,19 +48,19 @@ iqm/qiskit_iqm/qiskit_to_iqm.py,sha256=9JGcR_7K1Y5W6_PBP1bVCZqy7khCOa-BU9m1I9MJB
47
48
  iqm/qiskit_iqm/transpiler_plugins.py,sha256=w0TXrAqtMZsPgGC1YcwiLvlBYVKDtpaCi_lnlLjTD2c,8717
48
49
  iqm/qiskit_iqm/examples/__init__.py,sha256=M4ElQHCo-WxtVXK39bF3QiFT3IGXPtZ1khqexHiTBEc,20
49
50
  iqm/qiskit_iqm/examples/bell_measure.py,sha256=iMZB_MNMf2XP6Eiv2XbhtNs4bXbMGQeMw7ohw2JWKS8,1903
50
- iqm/qiskit_iqm/examples/resonance_example.py,sha256=wzWOP4oob-Ckl-LR3LXznb_-cA-m7XY9FmU2Ua9nB3c,2861
51
+ iqm/qiskit_iqm/examples/resonance_example.py,sha256=3eyHkmr6ZSLelSosjTSw1MLIOo3ioWhqBGRYXPFYRDA,2970
51
52
  iqm/qiskit_iqm/examples/transpile_example.py,sha256=hUxGPD_eLaZFFHAbegcjT9lNq7UAvGCzHfZFnuF6cbE,2099
52
53
  iqm/qiskit_iqm/fake_backends/__init__.py,sha256=fkw2UHT-3aJbAKvR1WYUN7_4N5Gdwpx9bm6vlWj1tm0,874
53
- iqm/qiskit_iqm/fake_backends/fake_adonis.py,sha256=OV66SmtEuA9CV7VpWV1T-U5H5XBtCK2SQJ2UlRwW4CU,2189
54
+ iqm/qiskit_iqm/fake_backends/fake_adonis.py,sha256=g5QQ158vd49_SxzteDip97XMD3d50SJymx_rU7g7lzk,2188
54
55
  iqm/qiskit_iqm/fake_backends/fake_aphrodite.py,sha256=3oWZwPmtcsYkYpqVr36-C5K7KtyQGk84RCvZpaL_kLg,15473
55
56
  iqm/qiskit_iqm/fake_backends/fake_apollo.py,sha256=eT2vd3kQBi1rrvxCpePymBCfFK84dpwNpqGV_BkltwQ,6563
56
57
  iqm/qiskit_iqm/fake_backends/fake_deneb.py,sha256=RzQXmLXmBARDiMKVxk5Aw9fVbc6IYlW0A5jibk9iYD0,3156
57
58
  iqm/qiskit_iqm/fake_backends/fake_garnet.py,sha256=GI0xafTCj1Um09qVuccO6GPOGBm6ygul_O40Wu220Ys,5555
58
59
  iqm/qiskit_iqm/fake_backends/iqm_fake_backend.py,sha256=wJtfsxjPYbDKmzaz5R4AuaXvvPHa21WyPtRgNctL9eY,16785
59
- iqm_client-29.13.0.dist-info/AUTHORS.rst,sha256=qsxeK5A3-B_xK3hNbhFHEIkoHNpo7sdzYyRTs7Bdtm8,795
60
- iqm_client-29.13.0.dist-info/LICENSE.txt,sha256=2DXrmQtVVUV9Fc9RBFJidMiTEaQlG2oAtlC9PMrEwTk,11333
61
- iqm_client-29.13.0.dist-info/METADATA,sha256=C4YQqf3isnS5OXicwWmY_VlKZxSoflJRm8X0eXzuCs0,17693
62
- iqm_client-29.13.0.dist-info/WHEEL,sha256=y4mX-SOX4fYIkonsAGA5N0Oy-8_gI4FXw5HNI1xqvWg,91
63
- iqm_client-29.13.0.dist-info/entry_points.txt,sha256=Kk2qfRwk8vbIJ7qCAvmaUogfRRn6t92_hBFhe6kqAE4,1317
64
- iqm_client-29.13.0.dist-info/top_level.txt,sha256=NB4XRfyDS6_wG9gMsyX-9LTU7kWnTQxNvkbzIxGv3-c,4
65
- iqm_client-29.13.0.dist-info/RECORD,,
60
+ iqm_client-30.0.0.dist-info/AUTHORS.rst,sha256=qsxeK5A3-B_xK3hNbhFHEIkoHNpo7sdzYyRTs7Bdtm8,795
61
+ iqm_client-30.0.0.dist-info/LICENSE.txt,sha256=2DXrmQtVVUV9Fc9RBFJidMiTEaQlG2oAtlC9PMrEwTk,11333
62
+ iqm_client-30.0.0.dist-info/METADATA,sha256=u9RkPSf_wx9QF8i5xPHP6KGq_V7uvP5KWfi2serE9q4,17692
63
+ iqm_client-30.0.0.dist-info/WHEEL,sha256=y4mX-SOX4fYIkonsAGA5N0Oy-8_gI4FXw5HNI1xqvWg,91
64
+ iqm_client-30.0.0.dist-info/entry_points.txt,sha256=Kk2qfRwk8vbIJ7qCAvmaUogfRRn6t92_hBFhe6kqAE4,1317
65
+ iqm_client-30.0.0.dist-info/top_level.txt,sha256=NB4XRfyDS6_wG9gMsyX-9LTU7kWnTQxNvkbzIxGv3-c,4
66
+ iqm_client-30.0.0.dist-info/RECORD,,