-
Notifications
You must be signed in to change notification settings - Fork 14
/
Copy pathquantinuum.py
1764 lines (1544 loc) · 66.6 KB
/
quantinuum.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Copyright Quantinuum
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Pytket Backend for Quantinuum devices."""
import datetime
import json
import re
import warnings
from ast import literal_eval
from base64 import b64encode
from collections import Counter
from collections.abc import Sequence
from copy import copy
from dataclasses import dataclass
from enum import Enum
from functools import cache, cached_property
from http import HTTPStatus
from typing import TYPE_CHECKING, Any, Optional, Union, cast
from uuid import uuid1
import numpy as np
import requests
from pytket.architecture import FullyConnected
from pytket.backends import Backend, CircuitStatus, ResultHandle, StatusEnum
from pytket.backends.backend import KwargTypes
from pytket.backends.backend_exceptions import CircuitNotRunError
from pytket.backends.backendinfo import BackendInfo
from pytket.backends.backendresult import BackendResult
from pytket.backends.resulthandle import _ResultIdTuple
from pytket.circuit import Bit, Circuit, OpType
from pytket.extensions.quantinuum._metadata import __extension_version__
from pytket.extensions.quantinuum.backends.credential_storage import (
MemoryCredentialStorage,
)
from pytket.extensions.quantinuum.backends.leakage_gadget import get_detection_circuit
from pytket.passes import (
AutoRebase,
AutoSquash,
BasePass,
DecomposeBoxes,
DecomposeTK2,
FlattenRelabelRegistersPass,
FullPeepholeOptimise,
GreedyPauliSimp,
NormaliseTK2,
RemoveBarriers,
RemovePhaseOps,
RemoveRedundancies,
SequencePass,
SimplifyInitial,
SynthesiseTK,
ZZPhaseToRz,
scratch_reg_resize_pass,
)
from pytket.predicates import (
GateSetPredicate,
MaxNClRegPredicate,
MaxNQubitsPredicate,
NoSymbolsPredicate,
Predicate,
)
from pytket.qasm import circuit_to_qasm_str
from pytket.qir import QIRFormat, QIRProfile, pytket_to_qir
from pytket.unit_id import _TEMP_BIT_NAME
from pytket.utils import prepare_circuit
from pytket.utils.outcomearray import OutcomeArray
from pytket.wasm import WasmFileHandler
from .api_wrappers import QuantinuumAPI, QuantinuumAPIError
if TYPE_CHECKING:
import matplotlib
try:
from pytket.extensions.quantinuum.backends.calendar_visualisation import (
QuantinuumCalendar,
)
MATPLOTLIB_IMPORT = True
except ImportError:
MATPLOTLIB_IMPORT = False
_DEBUG_HANDLE_PREFIX = "_MACHINE_DEBUG_"
MAX_C_REG_WIDTH = 32
_STATUS_MAP = {
"queued": StatusEnum.QUEUED,
"running": StatusEnum.RUNNING,
"completed": StatusEnum.COMPLETED,
"failed": StatusEnum.ERROR,
"canceling": StatusEnum.CANCELLED,
"canceled": StatusEnum.CANCELLED,
}
_ADDITIONAL_GATES = {
OpType.Reset,
OpType.Measure,
OpType.Barrier,
OpType.RangePredicate,
OpType.MultiBit,
OpType.ExplicitPredicate,
OpType.ExplicitModifier,
OpType.SetBits,
OpType.CopyBits,
OpType.ClassicalExpBox,
OpType.ClExpr,
OpType.WASM,
}
_GATE_MAP = {
"Rxxyyzz": OpType.TK2,
"Rz": OpType.Rz,
"RZZ": OpType.ZZPhase,
"TK2": OpType.TK2,
"U1q": OpType.PhasedX,
"ZZ": OpType.ZZMax,
}
_ALL_GATES = _ADDITIONAL_GATES.copy()
_ALL_GATES.update(_GATE_MAP.values())
def _default_2q_gate(device_name: str) -> OpType:
# If we change this, we should update the main documentation page and highlight it
# in the changelog.
return OpType.ZZPhase
def _get_gateset(gates: list[str]) -> set[OpType]:
gs = _ADDITIONAL_GATES.copy()
for gate in gates:
if gate not in _GATE_MAP:
warnings.warn(f"Gate {gate} not recognized.")
else:
gs.add(_GATE_MAP[gate])
return gs
def _is_scratch(bit: Bit) -> bool:
reg_name = bit.reg_name
return bool(reg_name == _TEMP_BIT_NAME) or reg_name.startswith(f"{_TEMP_BIT_NAME}_")
def _used_scratch_registers(qasm: str) -> set[str]:
# See https://github.com/CQCL/tket/blob/e846e8a7bdcc4fa29967d211b7fbf452ec970dfb/
# pytket/pytket/qasm/qasm.py#L966
def_matcher = re.compile(rf"creg ({_TEMP_BIT_NAME}\_*\d*)\[\d+\]")
regs = set()
for line in qasm.split("\n"):
if reg := def_matcher.match(line):
regs.add(reg.group(1))
return regs
class GetResultFailed(Exception):
pass
class NoSyntaxChecker(Exception):
pass
class MaxShotsExceeded(Exception):
pass
class WasmUnsupported(Exception):
pass
class BatchingUnsupported(Exception):
"""Batching not supported for this backend."""
class LanguageUnsupported(Exception):
"""Submission language not supported for this backend."""
@dataclass
class DeviceNotAvailable(Exception):
device_name: str
class Language(Enum):
"""Language used for submission of circuits."""
QASM = 0 # "OPENQASM 2.0"
QIR = 1 # pytket qir with classical functions: "QIR 1.0"
PQIR = 2 # profile QIR: "QIR 1.0"
def _language2str(language: Language) -> str:
"""returns matching string for Language enum"""
if language == Language.QASM:
return "OPENQASM 2.0"
else:
return "QIR 1.0"
# DEFAULT_CREDENTIALS_STORAGE for use with the DEFAULT_API_HANDLER.
DEFAULT_CREDENTIALS_STORAGE = MemoryCredentialStorage()
# DEFAULT_API_HANDLER provides a global re-usable API handler
# that will persist after this module is imported.
#
# This allows users to create multiple QuantinuumBackend instances
# without requiring them to acquire new tokens.
DEFAULT_API_HANDLER = QuantinuumAPI(DEFAULT_CREDENTIALS_STORAGE)
QuumKwargTypes = Union[KwargTypes, WasmFileHandler, dict[str, Any], OpType, bool]
@dataclass
class QuantinuumBackendCompilationConfig:
"""
Options to configure default compilation and rebase passes.
* ``allow_implicit_swaps``: Whether to allow use of implicit swaps when rebasing.
The default is to allow implicit swaps.
* ``target_2qb_gate``: Choice of two-qubit gate. The default is to use the device's
default.
"""
allow_implicit_swaps: bool = True
target_2qb_gate: Optional[OpType] = None
@cache
def have_pecos() -> bool:
try:
import pytket_pecos # type: ignore # noqa # pylint: disable=unused-import
return True
except ImportError:
return False
@dataclass
class _LocalEmulatorConfiguration:
"""Options stored internally when running circuits on the local emulator."""
circuit: Circuit
wasm_fh: Optional[WasmFileHandler]
n_shots: int
seed: Optional[int]
multithreading: bool
noisy_simulation: bool
class QuantinuumBackend(Backend):
"""
Interface to a Quantinuum device.
More information about the QuantinuumBackend can be found on this page
https://docs.quantinuum.com/tket/extensions/pytket-quantinuum/index.html
"""
_supports_shots = True
_supports_counts = True
_supports_contextual_optimisation = True
_persistent_handles = True
def __init__(
self,
device_name: str,
label: Optional[str] = "job",
simulator: str = "state-vector",
group: Optional[str] = None,
provider: Optional[str] = None,
machine_debug: bool = False,
api_handler: QuantinuumAPI = DEFAULT_API_HANDLER,
compilation_config: Optional[QuantinuumBackendCompilationConfig] = None,
**kwargs: QuumKwargTypes,
):
"""Construct a new Quantinuum backend.
:param device_name: Name of device, e.g. "H1-1"
:param label: Job labels used if Circuits have no name, defaults to "job"
:param simulator: Only applies to simulator devices, options are
"state-vector" or "stabilizer", defaults to "state-vector"
:param group: string identifier of a collection of jobs, can be used for usage
tracking.
:param provider: select a provider for federated authentication. We currently
only support 'microsoft', which enables the microsoft Device Flow.
:param api_handler: Instance of API handler, defaults to DEFAULT_API_HANDLER
:param compilation_config: Optional compilation configuration
Supported kwargs:
* `options`: items to add to the "options" dictionary of the request body, as a
json-style dictionary (see :py:meth:`QuantinuumBackend.process_circuits`)
"""
super().__init__()
self._device_name = device_name
self._label = label
self._group = group
self._backend_info: Optional[BackendInfo] = None
self._MACHINE_DEBUG = machine_debug
self.simulator_type = simulator
self.api_handler = api_handler
self.api_handler.provider = provider
self._process_circuits_options = cast(dict[str, Any], kwargs.get("options", {}))
self._local_emulator_handles: dict[
ResultHandle,
_LocalEmulatorConfiguration,
] = dict()
if compilation_config is None:
self._compilation_config = QuantinuumBackendCompilationConfig()
else:
self._compilation_config = compilation_config
@property
def compilation_config(self) -> QuantinuumBackendCompilationConfig:
"""The current compilation configuration for the Backend.
Accessing this property will set the target_2qb_gate if it
has not already been set.
"""
if self._compilation_config.target_2qb_gate is None:
self._compilation_config.target_2qb_gate = self.default_two_qubit_gate
return self._compilation_config
def get_compilation_config(self) -> QuantinuumBackendCompilationConfig:
"""Get the current compilation configuration."""
return self.compilation_config
def set_compilation_config_allow_implicit_swaps(
self, allow_implicit_swaps: bool
) -> None:
"""Set the option to allow or disallow implicit swaps during compilation."""
self.compilation_config.allow_implicit_swaps = allow_implicit_swaps
def set_compilation_config_target_2qb_gate(self, target_2qb_gate: OpType) -> None:
"""Set the target two-qubit gate for compilation."""
if target_2qb_gate not in self.two_qubit_gate_set:
raise QuantinuumAPIError(
"Requested target_2qb_gate is not supported by the given Device. "
"The supported gateset is: " + str(self.two_qubit_gate_set)
)
self.compilation_config.target_2qb_gate = target_2qb_gate
@classmethod
@cache
def _available_devices(
cls,
api_handler: QuantinuumAPI,
) -> list[dict[str, Any]]:
"""List devices available from Quantinuum.
>>> QuantinuumBackend._available_devices()
e.g. [{'name': 'H1', 'n_qubits': 6}]
:param api_handler: Instance of API handler
:return: Dictionaries of machine name and number of qubits.
"""
return api_handler.get_machine_list()
@classmethod
def _dict_to_backendinfo(
cls, dct: dict[str, Any], local_emulator: bool = False
) -> BackendInfo:
dct1 = copy(dct)
name: str = dct1.pop("name")
n_qubits: int = dct1.pop("n_qubits")
n_cl_reg: Optional[int] = None
if "n_classical_registers" in dct:
n_cl_reg = dct1.pop("n_classical_registers")
gate_set: list[str] = dct1.pop("gateset", [])
if local_emulator:
dct1["system_type"] = "local_emulator"
dct1.pop("emulator", None)
dct1["batching"] = False
return BackendInfo(
name=cls.__name__,
device_name=name + "LE" if local_emulator else name,
version=__extension_version__,
architecture=FullyConnected(n_qubits, "q"),
gate_set=_get_gateset(gate_set),
n_cl_reg=n_cl_reg,
supports_fast_feedforward=True,
supports_midcircuit_measurement=True,
supports_reset=True,
misc=dct1,
)
@classmethod
def available_devices(
cls,
**kwargs: Any,
) -> list[BackendInfo]:
"""
See :py:meth:`pytket.backends.Backend.available_devices`.
:param api_handler: Instance of API handler, defaults to DEFAULT_API_HANDLER
:return: A list of BackendInfo objects for each available Backend.
"""
api_handler = kwargs.get("api_handler", DEFAULT_API_HANDLER)
jr = cls._available_devices(api_handler)
devices = []
for d in jr:
devices.append(cls._dict_to_backendinfo(copy(d)))
if have_pecos() and (d.get("system_type") == "hardware"):
# Add a local-emulator variant
devices.append(cls._dict_to_backendinfo(d, local_emulator=True))
return devices
def _retrieve_backendinfo(self, machine: str) -> BackendInfo:
infos = self.available_devices(api_handler=self.api_handler)
try:
info = next(entry for entry in infos if entry.device_name == machine)
except StopIteration:
raise DeviceNotAvailable(machine)
info.misc["options"] = self._process_circuits_options
return info
@classmethod
def device_state(
cls,
device_name: str,
api_handler: QuantinuumAPI = DEFAULT_API_HANDLER,
) -> str:
"""Check the status of a device.
>>> QuantinuumBackend.device_state('H1') # e.g. "online"
:param device_name: Name of the device.
:param api_handler: Instance of API handler, defaults to DEFAULT_API_HANDLER
:return: String of state, e.g. "online"
"""
infos = cls.available_devices(api_handler=api_handler)
try:
info = next(entry for entry in infos if entry.device_name == device_name)
except StopIteration:
raise DeviceNotAvailable(device_name)
if info.get_misc("system_type") == "local_emulator":
return "online"
res = requests.get(
f"{api_handler.url}machine/{device_name}",
headers={"Authorization": api_handler.login()},
)
api_handler._response_check(res, "get machine status")
return str(res.json()["state"])
def get_calendar(
self,
start_date: datetime.datetime,
end_date: datetime.datetime,
localise: bool = True,
) -> list[dict[str, Any]]:
"""Retrieves the Quantinuum H-Series operations calendar
for the period specified by start_date and end_date.
The calendar data returned is for the local timezone of the
end-user.
The output is a sorted list of dictionaries. Each dictionary is an
event on the operations calendar for the period specified by the
end-user. The output from this function can be readily used
to instantiate a pandas.DataFrame.
The dictionary has the following properties.
* 'start-date': The start date and start time as a datetime.datetime object.
* 'end-date': The end date and end time as a datetime.datetime object.
* 'machine': A string specifying the H-Series device attached to the event.
* 'event-type': The type of event as a string. The value `online` denotes queued
access to the device, and the value `reservation` denotes priority access
for a particular organisation.
* 'organization': If the 'event-type' is assigned the value 'reservation', the
organization with reservation access is specified. Only users within an
organization have visibility on organization reservations. Otherwise,
organization is listed as 'Fair-Share Queue', which means all users from all
organizations are able to submit jobs to the Fairshare queue during this
period.
:param start_date: The start date as datetime.date object
for the period to return the operations calendar.
:param end_date: The end date as datetime.date object
for the period to return the operations calendar.
:param localise: Apply localization to the datetime based
on the end-users time zone. Default is True. Disable by
setting False.
:return: A list of events from the H-Series operations calendar,
sorted by the `start-date` of each event. Each event is a python
dictionary.
:return_type: List[Dict[str, str]]
:raises: RuntimeError if an emulator or syntax-checker is specified
:raises: ValueError if the argument `start_date` or `end_date` are not
datetime.datetime objects.
"""
if not isinstance(start_date, datetime.datetime) or not isinstance(
end_date, datetime.datetime
):
raise ValueError(
"start_date and end_date must be datetime.datetime objects."
)
if self._device_name.endswith("E") | self._device_name.endswith("SC"):
raise RuntimeError(
f"Error requesting data for {self._device_name}. Emulators (E) \
and Syntax Checkers (SC) are online 24/7. Calendar \
information not available."
)
l4_calendar_data = self.api_handler.get_calendar(
start_date.date().isoformat(), end_date.date().isoformat()
)
calendar_data = []
for l4_event in l4_calendar_data:
device_name = l4_event["machine"]
if device_name != self._device_name:
continue
dt_start = _convert_datetime_string(
l4_event["start-date"]
) # datetime in UTC tz
dt_end = _convert_datetime_string(
l4_event["end-date"]
) # datetime in UTC tz
if localise: # Apply timezone localisation on UTC datetime
dt_start = dt_start.astimezone()
dt_end = dt_end.astimezone()
event = {
"start-date": dt_start,
"end-date": dt_end,
"machine": device_name,
"event-type": l4_event["event-type"],
"organization": l4_event.get("organization", "Fair-Share Queue"),
}
calendar_data.append(event)
calendar_data.sort(key=lambda item: item["start-date"]) # type: ignore
return calendar_data
def view_calendar(
self,
month: int,
year: int,
figsize: tuple[float, float] = (40, 20),
fontsize: float = 15,
titlesize: float = 40,
) -> "matplotlib.figure.Figure":
"""Visualise the H-Series operations calendar for a user-specified
month and year. The operations hours are shown for the machine name
used to construct the QuantinuumBackend object, i.e. 'H1-1'. Operations
days are coloured. In addition, a description of the event is also
displayed (`start-time`, `duration` and `event-type`, see the
`get_calendar` method for more information).
:param month: An integer specifying the calendar month to visualise.
1 is January and 12 is December.
:param year: An integer specifying the calendar year to visualise.
:param figsize: A tuple specifying width and height of the output
matplotlib.figure.Figure.
:param fontsize: The fontsize of the event description within the
calendar.
:return: A matplotlib.figure.Figure visualising the H-Series
calendar for a user-specified calendar month.
:return_type: matplotlib.figure.Figure
"""
if not MATPLOTLIB_IMPORT:
raise ImportError(
"Matplotlib is not installed. Please run \
'pip install pytket-quantinuum[calendar]'"
)
qntm_calendar = QuantinuumCalendar(
year=year, month=month, title_prefix=self._device_name
)
end_day = max(qntm_calendar._cal[-1])
dt_start = datetime.datetime(year=year, month=month, day=1)
dt_end = datetime.datetime(year=year, month=month, day=end_day)
data = self.get_calendar(dt_start, dt_end, localise=True)
qntm_calendar.add_events(data)
calendar_figure = qntm_calendar.build_calendar(
figsize=figsize, fontsize=fontsize, titlesize=titlesize
)
return calendar_figure
@property
def backend_info(self) -> Optional[BackendInfo]:
if self._backend_info is None and not self._MACHINE_DEBUG:
self._backend_info = self._retrieve_backendinfo(self._device_name)
return self._backend_info
@cached_property
def _gate_set(self) -> set[OpType]:
return (
_ALL_GATES
if self._MACHINE_DEBUG
else cast(BackendInfo, self.backend_info).gate_set
)
@property
def required_predicates(self) -> list[Predicate]:
preds = [
NoSymbolsPredicate(),
GateSetPredicate(self._gate_set),
]
if not self._MACHINE_DEBUG:
assert self.backend_info is not None
preds.append(MaxNQubitsPredicate(self.backend_info.n_nodes))
preds.append(MaxNClRegPredicate(cast(int, self.backend_info.n_cl_reg)))
return preds
@cached_property
def default_two_qubit_gate(self) -> OpType:
"""Returns the default two-qubit gate for the device."""
default_2q_gate = _default_2q_gate(self._device_name)
if default_2q_gate in self.two_qubit_gate_set:
pass
elif len(self.two_qubit_gate_set) > 0:
default_2q_gate = list(self.two_qubit_gate_set)[0]
else:
raise ValueError("The device is not supporting any two qubit gates")
return default_2q_gate
@cached_property
def two_qubit_gate_set(self) -> set[OpType]:
"""Returns the set of supported two-qubit gates.
Submitted circuits must contain only one of these.
"""
return self._gate_set & set([OpType.ZZPhase, OpType.ZZMax, OpType.TK2])
@property
def is_local_emulator(self) -> bool:
"""True if the backend is a local emulator, otherwise False"""
if self._MACHINE_DEBUG:
return False
info = self.backend_info
assert info is not None
return bool(info.get_misc("system_type") == "local_emulator")
def rebase_pass(self) -> BasePass:
assert self.compilation_config.target_2qb_gate in self.two_qubit_gate_set
return AutoRebase(
(self._gate_set - self.two_qubit_gate_set)
| {self.compilation_config.target_2qb_gate},
allow_swaps=self.compilation_config.allow_implicit_swaps,
)
def default_compilation_pass(
self, optimisation_level: int = 2, timeout: int = 300
) -> BasePass:
"""
:param optimisation_level: Allows values of 0, 1, 2 or 3, with higher values
prompting more computationally heavy optimising compilation that
can lead to reduced gate count in circuits.
:param timeout: Only valid for optimisation level 3, gives a maximimum time
for running a single thread of the pass `GreedyPauliSimp`. Increase for
optimising larger circuits.
:return: Compilation pass for compiling circuits to Quantinuum devices
"""
assert optimisation_level in range(4)
passlist = [
DecomposeBoxes(),
scratch_reg_resize_pass(),
]
squash = AutoSquash({OpType.PhasedX, OpType.Rz})
target_2qb_gate = self.compilation_config.target_2qb_gate
assert target_2qb_gate is not None
if target_2qb_gate == OpType.TK2:
decomposition_passes = []
elif target_2qb_gate == OpType.ZZPhase:
decomposition_passes = [
NormaliseTK2(),
DecomposeTK2(
allow_swaps=self.compilation_config.allow_implicit_swaps,
ZZPhase_fidelity=1.0,
),
]
elif target_2qb_gate == OpType.ZZMax:
decomposition_passes = [
NormaliseTK2(),
DecomposeTK2(
allow_swaps=self.compilation_config.allow_implicit_swaps,
ZZMax_fidelity=1.0,
),
]
else:
raise ValueError(
f"Unrecognized target 2-qubit gate: {target_2qb_gate.name}"
)
# If you make changes to the default_compilation_pass,
# then please update this page accordingly
# https://docs.quantinuum.com/tket/extensions/pytket-quantinuum/index.html#default-compilation
# Edit this docs source file -> pytket-quantinuum/docs/intro.txt
if optimisation_level == 0:
passlist.append(self.rebase_pass())
elif optimisation_level == 1:
passlist.append(SynthesiseTK())
passlist.extend(decomposition_passes)
passlist.extend(
[
self.rebase_pass(),
ZZPhaseToRz(),
RemoveRedundancies(),
squash,
RemoveRedundancies(),
]
)
elif optimisation_level == 2:
passlist.append(
FullPeepholeOptimise(
allow_swaps=self.compilation_config.allow_implicit_swaps,
target_2qb_gate=OpType.TK2,
)
)
passlist.extend(decomposition_passes)
passlist.extend(
[
self.rebase_pass(),
RemoveRedundancies(),
squash,
RemoveRedundancies(),
]
)
else:
passlist.extend(
[
RemoveBarriers(),
AutoRebase(
{
OpType.Z,
OpType.X,
OpType.Y,
OpType.S,
OpType.Sdg,
OpType.V,
OpType.Vdg,
OpType.H,
OpType.CX,
OpType.CY,
OpType.CZ,
OpType.SWAP,
OpType.Rz,
OpType.Rx,
OpType.Ry,
OpType.T,
OpType.Tdg,
OpType.ZZMax,
OpType.ZZPhase,
OpType.XXPhase,
OpType.YYPhase,
OpType.PhasedX,
}
),
GreedyPauliSimp(
allow_zzphase=True,
only_reduce=True,
thread_timeout=timeout,
trials=10,
),
]
)
passlist.extend(decomposition_passes)
passlist.extend(
[
self.rebase_pass(),
RemoveRedundancies(),
squash,
RemoveRedundancies(),
]
)
passlist.append(RemovePhaseOps())
# In TKET, a qubit register with N qubits can have qubits
# indexed with a a value greater than N, i.e. a single
# qubit register can exist with index "7" or similar.
# Similarly, a qubit register with N qubits could be defined
# in a Circuit, but fewer than N qubits in the register
# have operations.
# Both of these cases can causes issues when converting to QASM,
# as the size of the defined "qreg" can be larger than the
# number of Qubits actually used, or at times larger than the
# number of device Qubits, even if fewer are really used.
# By flattening the Circuit qubit registers, we make sure
# that the produced QASM has one "qreg", with the exact number
# of qubits actually used in the Circuit.
# The Circuit qubits attribute is iterated through, with the ith
# qubit being assigned to the ith qubit of a new "q" register
passlist.append(FlattenRelabelRegistersPass("q"))
return SequencePass(passlist)
def get_compiled_circuit(
self, circuit: Circuit, optimisation_level: int = 2, timeout: int = 300
) -> Circuit:
"""
Return a single circuit compiled with :py:meth:`default_compilation_pass`.
:param optimisation_level: Allows values of 0, 1, 2 or 3, with higher values
prompting more computationally heavy optimising compilation that
can lead to reduced gate count in circuits.
:type optimisation_level: int, optional
:param timeout: Only valid for optimisation level 3, gives a maximimum time
for running a single thread of the pass `GreedyPauliSimp`. Increase for
optimising larger circuits.
:type timeout: int, optional
:return: An optimised quantum circuit
:rtype: Circuit
"""
return_circuit = circuit.copy()
if optimisation_level == 3 and circuit.n_gates_of_type(OpType.Barrier) > 0:
warnings.warn(
"Barrier operations in this circuit will be removed when using "
"optimisation level 3."
)
self.default_compilation_pass(optimisation_level, timeout).apply(return_circuit)
return return_circuit
def get_compiled_circuits(
self,
circuits: Sequence[Circuit],
optimisation_level: int = 2,
timeout: int = 300,
) -> list[Circuit]:
"""Compile a sequence of circuits with :py:meth:`default_compilation_pass`
and return the list of compiled circuits (does not act in place).
As well as applying a degree of optimisation (controlled by the
`optimisation_level` parameter), this method tries to ensure that the circuits
can be run on the backend (i.e. successfully passed to
:py:meth:`process_circuits`), for example by rebasing to the supported gate set,
or routing to match the connectivity of the device. However, this is not always
possible, for example if the circuit contains classical operations that are not
supported by the backend. You may use :py:meth:`valid_circuit` to check whether
the circuit meets the backend's requirements after compilation. This validity
check is included in :py:meth:`process_circuits` by default, before any circuits
are submitted to the backend.
If the validity check fails, you can obtain more information about the failure
by iterating through the predicates in the `required_predicates` property of the
backend, and running the :py:meth:`verify` method on each in turn with your
circuit.
:param circuits: The circuits to compile.
:type circuit: Sequence[Circuit]
:param optimisation_level: The level of optimisation to perform during
compilation. See :py:meth:`default_compilation_pass` for a description of
the different levels (0, 1, 2 or 3). Defaults to 2.
:type optimisation_level: int, optional
:param timeout: Only valid for optimisation level 3, gives a maximimum time
for running a single thread of the pass `GreedyPauliSimp`. Increase for
optimising larger circuits.
:type timeout: int, optional
:return: Compiled circuits.
:rtype: List[Circuit]
"""
return [
self.get_compiled_circuit(c, optimisation_level, timeout) for c in circuits
]
@property
def _result_id_type(self) -> _ResultIdTuple:
return tuple((str, str, int, str))
@staticmethod
def _update_result_handle(handle: ResultHandle) -> ResultHandle:
"""Update a legacy handle to be compatible with current format."""
if len(handle) == 2:
return ResultHandle(handle[0], handle[1], -1, "")
elif len(handle) == 3:
return ResultHandle(handle[0], handle[1], handle[2], "")
else:
return handle
@staticmethod
def get_jobid(handle: ResultHandle) -> str:
"""Return the corresponding Quantinuum Job ID from a ResultHandle.
:param handle: result handle.
:return: Quantinuum API Job ID string.
"""
return cast(str, handle[0])
@staticmethod
def get_ppcirc_rep(handle: ResultHandle) -> Any:
"""Return the JSON serialization of the classiocal postprocessing circuit
attached to a handle, if any.
:param handle: result handle
:return: serialized post-processing circuit, if any
"""
return json.loads(cast(str, handle[1]))
@staticmethod
def get_results_width(handle: ResultHandle) -> Optional[int]:
"""Return the truncation width of the results, if any.
:param handle: result handle
:return: truncation width of results, if any
"""
n = cast(int, handle[2])
if n == -1:
return None
else:
assert n >= 0
return n
@staticmethod
def get_results_selection(handle: ResultHandle) -> Any:
"""Return a list of pairs (register name, register index) representing the order
of the expected results in the response. If None, then all results in the
response are used, in lexicographic order.
"""
s = cast(str, handle[3])
if s == "":
return None
bits = json.loads(s)
if bits is None:
return None
assert all(isinstance(name, str) and isinstance(idx, int) for name, idx in bits)
return bits
def submit_program(
self,
language: Language,
program: str,
n_shots: int,
name: Optional[str] = None,
noisy_simulation: bool = True,
group: Optional[str] = None,
wasm_file_handler: Optional[WasmFileHandler] = None,
pytket_pass: Optional[BasePass] = None,
options: Optional[dict[str, Any]] = None,
request_options: Optional[dict[str, Any]] = None,
results_selection: Optional[list[tuple[str, int]]] = None,
) -> ResultHandle:
"""Submit a program directly to the backend.
:param program: program (encoded as string)
:param language: language
:param n_shots: Number of shots
:param name: Job name, defaults to None
:param noisy_simulation: Boolean flag to specify whether the simulator should
perform noisy simulation with an error model defaults to True
:param group: String identifier of a collection of jobs, can be used for usage
tracking. Overrides the instance variable `group`, defaults to None
:param wasm_file_handler: ``WasmFileHandler`` object for linked WASM
module, defaults to None
:param pytket_pass: ``pytket.passes.BasePass`` intended to be applied
by the backend (beta feature, may be ignored), defaults to None
:param options: Items to add to the "options" dictionary of the request body
:param request_options: Extra options to add to the request body as a
json-style dictionary, defaults to None
:param results_selection: Ordered list of register names and indices used to
construct final :py:class:`BackendResult`. If None, all all results are used
in lexicographic order.
:raises WasmUnsupported: WASM submitted to backend that does not support it.
:raises QuantinuumAPIError: API error.
:raises ConnectionError: Connection to remote API failed
:return: ResultHandle for submitted job.
"""
if self.is_local_emulator:
raise NotImplementedError(
"submit_program() not supported with local emulator"
)
lang_str = _language2str(language)
if self.backend_info is not None:
supported_languages = self.backend_info.misc.get("supported_languages")
if supported_languages is not None and lang_str not in supported_languages:
raise LanguageUnsupported(
f"Language {lang_str} unsupported for submissions to this backend."
)
body: dict[str, Any] = {
"name": name or f"{self._label}",
"count": n_shots,
"machine": self._device_name,
"language": lang_str,
"program": program,
"priority": "normal",
"options": {