-
Notifications
You must be signed in to change notification settings - Fork 55
/
compilationrules.py
1422 lines (1170 loc) · 66.7 KB
/
compilationrules.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
"""
Defines CompilationLibrary class and supporting functions
"""
#***************************************************************************************************
# Copyright 2015, 2019 National Technology & Engineering Solutions of Sandia, LLC (NTESS).
# Under the terms of Contract DE-NA0003525 with NTESS, the U.S. Government retains certain rights
# in this software.
# 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 or in the LICENSE file in the root pyGSTi directory.
#***************************************************************************************************
import collections as _collections
import copy as _copy
import itertools as _itertools
import numpy as _np
from pygsti.baseobjs.label import Label as _Label
from pygsti.baseobjs.qubitgraph import QubitGraph as _QubitGraph
from pygsti.circuits.circuit import Circuit as _Circuit
from pygsti.processors.processorspec import QubitProcessorSpec as _QubitProcessorSpec
from pygsti.tools import listtools as _lt
from pygsti.tools import symplectic as _symp
from pygsti.tools import internalgates as _itgs
class CompilationError(Exception):
"""
A compilation error, raised by :class:`CompilationLibrary`
"""
pass
class CompilationRules(object):
"""
A prescription for creating ("compiling") a set of gates based on another set.
A :class:`CompilationRules` object contains a dictionary of gate unitaries,
much like a :class:`ProcessorSpec`, and instructions for creating these gates.
The instructions can be given explicitly as circuits corresponding to a given gate,
or implicitly as functions. Instructions can be given for gate *names* (e.g. `"Gx"`),
regardless of the target state space labels of the gate, as well as for specific
gate locations (e.g. `("Gx",2)`).
Parameters
----------
compilation_rules_dict : dict
A dictionary of initial rules, which can be specified in multiple formats.
Keys can be either gate names as strings or gate labels as a Label object.
Values are 2-tuples of (gate unitary, gate template). The gate unitary
can either be a unitary matrix, function returning a matrix, or None if the
gate name is a standard PyGSTi unitary. The gate template is either a Circuit
with local state space labels (i.e. 0..k-1 for k qubits) or a function that takes
the target gate label and returns the proper Circuit. If the key is a gate label,
the gate template (second entry of the value tuple) MUST be a Circuit with
absolute state space labels.
"""
@classmethod
def cast(cls, obj):
"""
Convert an object into compilation rules, if it isn't already.
Parameters
----------
obj : object
The object to convert.
Returns
-------
CompilationRules
"""
if isinstance(obj, CompilationRules): return obj
return cls(obj)
def __init__(self, compilation_rules_dict=None):
self.gate_unitaries = _collections.OrderedDict() # gate_name => unitary mx, fn, or None
self.local_templates = _collections.OrderedDict() # gate_name => Circuit on gate's #qubits
self.function_templates = _collections.OrderedDict() # gate_name => fn(sslbls, args=None, time=None)
# that returns a Circuit on absolute qubits
self.specific_compilations = _collections.OrderedDict() # gate_label => Circuit on absolute qubits
self._compiled_cache = _collections.OrderedDict() # compiled gate_label => Circuit on absolute qubits
if compilation_rules_dict is not None:
for gate_key, (gate_unitary, gate_template) in compilation_rules_dict.items():
if isinstance(gate_key, str):
self.gate_unitaries[gate_key] = gate_unitary
if callable(gate_template):
self.function_templates[gate_key] = gate_template
else:
assert isinstance(gate_template, _Circuit), \
"Values to gate name template must be functions or Circuits, not %s" % type(gate_template)
self.local_templates[gate_key] = gate_template
else:
assert isinstance(gate_key, _Label), \
"Keys to compilation_rules_dict must be str or Labels, not %s" % type(gate_key)
assert isinstance(gate_template, _Circuit), \
"Values to specific compilations must be Circuits, not %s" % type(gate_template)
self.specific_compilations[gate_key] = gate_template
def add_compilation_rule(self, gate_name, template_circuit_or_fn, unitary=None):
"""
Add a compilation rule for a gate *name*, given as a circuit or function.
Parameters
----------
gate_name : str
The gate name to add a rule for.
template_circuit_or_fn : Circuit or callable
The rule. This can be specified as either a circuit or as a function. If a
circuit is given, it must be on the gate's local state space, assumed to be
a k-qubit space (for a k-qubit gate) with qubit labels 0 to k-1. That is,
the circuit must have line labels equal to `0...k-1`. If a function if given,
the function must take as a single argument a tuple of state space labels that
specify the target labels of the gate.
unitary : numpy.ndarray
The unitary corresponding to the gate. This can be left as `None` if
`gate_name` names a standard or internal gate known to pyGSTi.
Returns
-------
None
"""
std_gate_unitaries = _itgs.standard_gatename_unitaries()
std_gate_unitaries.update(_itgs.internal_gate_unitaries()) # internal gates ok too?
if unitary is None:
if gate_name in std_gate_unitaries: unitary = std_gate_unitaries[gate_name]
else: raise ValueError("Must supply `unitary` for non-standard gate name '%s'" % gate_name)
self.gate_unitaries[gate_name] = unitary
if callable(template_circuit_or_fn):
self.function_templates[gate_name] = template_circuit_or_fn
else:
self.local_templates[gate_name] = template_circuit_or_fn
def add_specific_compilation_rule(self, gate_label, circuit, unitary):
"""
Add a compilation rule for a gate at a specific location (target labels)
Parameters
----------
gate_label : Label
The gate label to add a rule for. Includes the gate's name and its target
state space labels (`gate_label.sslbls`).
circuit : Circuit
The rule, given as a circuit on the gate's local state space, i.e. the circuit's
line labels should be the same as `gate_label.sslbls`.
unitary : numpy.ndarray
The unitary corresponding to the gate. This can be left as `None` if
`gate_label.name` names a standard or internal gate known to pyGSTi.
Returns
-------
None
"""
std_gate_unitaries = _itgs.standard_gatename_unitaries()
std_gate_unitaries.update(_itgs.internal_gate_unitaries()) # internal gates ok too?
if gate_label.name not in self.gate_unitaries:
if unitary is None:
if gate_label.name in std_gate_unitaries: unitary = std_gate_unitaries[gate_label.name]
else: raise ValueError("Must supply `unitary` for non-standard gate name '%s'" % gate_label.name)
self.gate_unitaries[gate_label.name] = unitary
self.specific_compilations[gate_label] = circuit
def create_aux_info(self):
"""
Create auxiliary information that should be stored along with the compilation rules herein.
(Currently unused, but perhaps useful in the future.)
Returns
-------
dict
"""
return {}
def retrieve_compilation_of(self, oplabel, force=False):
"""
Get a compilation of `oplabel`, computing one from local templates if necessary.
Parameters
----------
oplabel : Label
The label of the gate to compile.
force : bool, optional
If True, then an attempt is made to recompute a compilation
even if `oplabel` already exists in this `CompilationLibrary`.
Otherwise compilations are only computed when they are *not* present.
Returns
-------
Circuit or None, if failed to retrieve compilation
"""
# First look up in cache
if not force and oplabel in self._compiled_cache:
return self._compiled_cache[oplabel]
if oplabel in self.specific_compilations: # Second, look up in specific compilations
self._compiled_cache[oplabel] = self.specific_compilations[oplabel]
elif oplabel.name in self.local_templates: # Third, construct from local template
template_to_use = self.local_templates[oplabel.name]
# Template compilations always use integer qubit labels: 0 to N
to_real_label = {i: oplabel.sslbls[i] for i in template_to_use.line_labels}
self._compiled_cache[oplabel] = template_to_use.map_state_space_labels(to_real_label)
elif oplabel.name in self.function_templates: # Fourth, construct from local function template
template_fn_to_use = self.function_templates[oplabel.name]
self._compiled_cache[oplabel] = _Circuit(template_fn_to_use(oplabel.sslbls, oplabel.args, oplabel.time))
else:
# Failed to compile
return None
return self._compiled_cache[oplabel]
def apply_to_processorspec(self, processor_spec, action="replace", gates_to_skip=None):
"""
Use these compilation rules to convert one processor specification into another one.
Parameters
----------
processor_spec : QubitProcessorSpec
The initial processor specification, which should contain the gates present within the
circuits/functions of this compilation rules object.
action : {"replace", "add"}
Whether the existing gates in `processor_spec` are conveyed to the the returned
processor spec. If `"replace"`, then they are not conveyed, if `"add"` they are.
gates_to_skip : list
Gate names or labels to skip during processor specification construction.
Returns
-------
QubitProcessorSpec
"""
gate_names = tuple(self.gate_unitaries.keys())
gate_unitaries = self.gate_unitaries.copy() # can contain `None` entries we deal with below
if gates_to_skip is None:
gates_to_skip = []
availability = {}
for gn in gate_names:
if gn in gates_to_skip:
continue
if gn in self.local_templates:
# merge availabilities from gates in local template
compilation_circuit = self.local_templates[gn]
all_sslbls = compilation_circuit.line_labels
gn_nqubits = len(all_sslbls)
assert (all_sslbls == tuple(range(0, gn_nqubits))), \
"Template circuits *must* have line labels == 0...(gate's #qubits-1), not %s!" % (
str(all_sslbls))
# To construct the availability for a circuit, we take the intersection
# of the availability for each of the layers. Each layer's availability is
# the cartesian-like product of the availabilities for each of the components
circuit_availability = None
for layer in compilation_circuit[:]:
layer_availability_factors = []
layer_availability_sslbls = []
for gate in layer.components:
gate_availability = processor_spec.availability[gate.name]
if gate_availability in ('all-edges', 'all-combinations', 'all-permutations'):
raise NotImplementedError("Cannot merge special availabilities yet")
layer_availability_factors.append(gate_availability)
gate_sslbls = gate.sslbls
if gate_sslbls is None: gate_sslbls = all_sslbls
assert (len(set(layer_availability_sslbls).intersection(gate_sslbls)) == 0), \
"Duplicate state space labels in layer: %s" % str(layer)
layer_availability_sslbls.extend(gate_sslbls) # integers
layer_availability = tuple(_itertools.product(*layer_availability_factors))
if tuple(layer_availability_sslbls) != all_sslbls: # then need to permute availability elements
p = {to: frm for frm, to in enumerate(layer_availability_sslbls)} # use sslbls as *indices*
new_order = [p[i] for i in range(gn_nqubits)]
layer_availability = tuple(map(lambda el: tuple([el[i] for i in new_order]),
layer_availability))
circuit_availability = set(layer_availability) if (circuit_availability is None) else \
circuit_availability.intersection(layer_availability)
assert (circuit_availability is not None), "Local template circuit cannot be empty!"
availability[gn] = tuple(sorted(circuit_availability))
if gate_unitaries[gn] is None:
# TODO: compute unitary via product of embedded unitaries of circuit layers, something like:
# gate_unitaries[gn] = product(
# [kronproduct(
# [embed(self.gate_unitaries[gate.name], gate.sslbls, range(gn_nqubits))
# for gate in layer.components])
# for layer in compilation_circuit)])
raise NotImplementedError("Still need to implement product of unitaries logic!")
elif gn in self.function_templates:
# create boolean oracle function for availability
def _fn(sslbls):
try:
self.function_templates[gn](sslbls, None, None) # (returns a circuit)
return True
except CompilationError:
return False
availability[gn] = _fn # boolean function indicating availability
else:
availability[gn] = () # empty tuple for absent gates - OK b/c may have specific compilations
if gate_unitaries[gn] is None:
raise ValueError("Must specify unitary for gate name '%s'" % str(gn))
# specific compilations add specific availability for their gate names:
for gate_lbl in self.specific_compilations.keys():
if gate_lbl in gates_to_skip:
continue
assert (gate_lbl.name in gate_names), \
"gate name '%s' missing from CompilationRules gate unitaries!" % gate_lbl.name
assert (isinstance(availability[gate_lbl.name], tuple)), \
"Cannot add specific values to non-explicit availabilities (e.g. given by functions)"
availability[gate_lbl.name] += (gate_lbl.sslbls,)
if action == "add":
gate_names = tuple(processor_spec.gate_names) + gate_names
gate_unitaries.update(processor_spec.gate_unitaries)
availability.update(processor_spec.availability)
elif action == "replace":
pass
else:
raise ValueError("Invalid `action` argument: %s" % str(action))
aux_info = processor_spec.aux_info.copy()
aux_info.update(self.create_aux_info())
ret = _QubitProcessorSpec(processor_spec.num_qubits, gate_names, gate_unitaries, availability,
processor_spec.qubit_graph, processor_spec.qubit_labels, aux_info=aux_info)
ret.compiled_from = (processor_spec, self)
return ret
def apply_to_circuits(self, circuits, **kwargs):
"""
Use these compilation rules to convert one list of circuits into another one.
Additional kwargs are passed through to Circuit.change_gate_library during translation.
Common kwargs include `depth_compression=False` or `allow_unchanged_gates=True`.
Parameters
----------
circuits : list of Circuits
The initial circuits, which should contain the gates present within the
circuits/functions of this compilation rules object.
Returns
-------
list of Circuits
"""
compiled_circuits = [c.copy(editable=True) for c in circuits]
for circ in compiled_circuits:
circ.change_gate_library(self, **kwargs)
circ.done_editing()
return compiled_circuits
class CliffordCompilationRules(CompilationRules):
"""
An collection of compilations for clifford gates.
Holds mapping between operation labels (:class:`Label` objects) and circuits
(:class:`Circuit` objects).
A `CliffordCompilationRules` holds a processor specification of the "native" gates
of a processor and uses it to produce compilations of many of/all Clifford operations.
Currently, the native gates should all be Clifford gates, so that the processor spec's
`compute_clifford_symplectic_reps` method gives representations for all of its gates.
Compilations can be either "local" or "non-local". A local compilation
ony uses gates that act on its target qubits. All 1-qubit gates can be
local. A non-local compilation uses qubits outside the set of target
qubits (e.g. a CNOT between two qubits between which there is no native
CNOT). Currently, non-local compilations can only be constructed for
the CNOT gate.
To speed up the creation of local compilations, a `CliffordCompilationRules`
instance stores "template" compilations, which specify how to construct a
compilation for some k-qubit gate on qubits labeled 0 to k-1. When creating
a compilation for a gate, a template is used if a suitable one can be found;
otherwise a new template is created and then used.
Parameters
----------
native_gates_processorspec : QubitProcessorSpec
The processor specification of "native" Clifford gates which all
compilation rules are composed from.
compile_type : {"absolute","paulieq"}
The "compilation type" for this rules set. If `"absolute"`, then
compilations must match the gate operation being compiled exactly.
If `"paulieq"`, then compilations only need to match the desired
gate operation up to a Paui operation (which is useful for compiling
multi-qubit Clifford gates / stabilizer states without unneeded 1-qubit
gate over-heads).
"""
@classmethod
def create_standard(cls, base_processor_spec, compile_type="absolute", what_to_compile=("1Qcliffords",),
verbosity=1):
"""
Create a common set of compilation rules based on a base processor specification.
Parameters
----------
base_processor_spec : QubitProcessorSpec
The processor specification of "native" Clifford gates which all
the compilation rules will be in terms of.
compile_type : {"absolute","paulieq"}
The "compilation type" for this rules set. If `"absolute"`, then
compilations must match the gate operation being compiled exactly.
If `"paulieq"`, then compilations only need to match the desired
gate operation up to a Paui operation (which is useful for compiling
multi-qubit Clifford gates / stabilizer states without unneeded 1-qubit
gate over-heads).
what_to_compile : {"1Qcliffords", "localcnots", "allcnots", "paulis"}
What operations should rules be created for? Allowed values may depend on
the value of `compile_type`.
Returns
-------
CliffordCompilationRules
"""
# A list of the 1-qubit gates to compile, in the std names understood inside the compilation code.
one_q_gates = []
# A list of the 2-qubit gates to compile, in the std names understood inside the compilation code.
two_q_gates = []
add_nonlocal_two_q_gates = False # Defaults to not adding non-local compilations of 2-qubit gates.
number_of_qubits = base_processor_spec.num_qubits
qubit_labels = base_processor_spec.qubit_labels
# We construct the requested Pauli-equivalent compilations.
if compile_type == 'paulieq':
for subctype in what_to_compile:
if subctype == '1Qcliffords':
one_q_gates += ['H', 'P', 'PH', 'HP', 'HPH']
elif subctype == 'localcnots':
# So that the default still makes sense with 1 qubit, we ignore the request to compile CNOTs
# in that case
if number_of_qubits > 1:
two_q_gates += ['CNOT', ]
elif subctype == 'allcnots':
# So that the default still makes sense with 1 qubit, we ignore the request to compile CNOTs
# in that case
if number_of_qubits > 1:
two_q_gates += ['CNOT', ]
add_nonlocal_two_q_gates = True
else:
raise ValueError("{} is invalid for the `{}` compile type!".format(subctype, compile_type))
# We construct the requested `absolute` (i.e., not only up to Paulis) compilations.
elif compile_type == 'absolute':
for subctype in what_to_compile:
if subctype == 'paulis':
one_q_gates += ['I', 'X', 'Y', 'Z']
elif subctype == '1Qcliffords':
one_q_gates += ['C' + str(q) for q in range(24)]
else:
raise ValueError("{} is invalid for the `{}` compile type!".format(subctype, compile_type))
else:
raise ValueError("Invalid `compile_type` argument: %s" % str(compile_type))
descs = {'paulieq': 'up to paulis', 'absolute': ''}
# Lists that are all the hard-coded 1-qubit and 2-qubit gates.
# future: should probably import these from _itgss somehow.
hardcoded_oneQgates = ['I', 'X', 'Y', 'Z', 'H', 'P', 'HP', 'PH', 'HPH'] + ['C' + str(i) for i in range(24)]
# Currently we can only compile CNOT gates, although that should be fixed.
for gate in two_q_gates:
assert (gate == 'CNOT'), ("The only 2-qubit gate auto-generated compilations currently possible "
"are for the CNOT gate (Gcnot)!")
# Creates an empty library to fill
compilation_rules = cls(base_processor_spec, compile_type)
# 1-qubit gate compilations. These must be complied "locally" - i.e., out of native gates which act only
# on the target qubit of the gate being compiled, and they are stored in the compilation rules.
for q in qubit_labels:
for gname in one_q_gates:
# Check that this is a gate that is defined in the code, so that we can try and compile it.
assert (gname in hardcoded_oneQgates), "{} is not an allowed hard-coded 1-qubit gate".format(gname)
if verbosity > 0:
print(
"- Creating a circuit to implement {} {} on qubit {}...".format(gname, descs[compile_type],
q))
# This does a brute-force search to compile the gate, by creating `templates` when necessary, and using
# a template if one has already been constructed.
compilation_rules.add_local_compilation_of(_Label(gname, q), verbosity=verbosity)
if verbosity > 0: print("Complete.")
# Manually add in the "obvious" compilations for CNOT gates as templates, so that we use the normal conversions
# based on the Hadamard gate -- if this is possible. If we don't do this, we resort to random compilations,
# which might not give the "expected" compilations (even if the alternatives might be just as good).
if 'CNOT' in two_q_gates:
# Look to see if we have a CNOT gate in the model (with any name).
cnot_name = cls._find_std_gate(base_processor_spec, 'CNOT')
H_name = cls._find_std_gate(base_processor_spec, 'H')
I_name = cls._find_std_gate(base_processor_spec, 'I')
# If we've failed to find a Hadamard gate but we only need paulieq compilation, we try
# to find a gate that is Pauli-equivalent to Hadamard.
if H_name is None and compile_type == 'paulieq':
for gn, gunitary in base_processor_spec.gate_unitaries.items():
if callable(gunitary): continue # can't pre-process factories
if _symp.unitary_is_clifford(gunitary):
if _itgs.is_gate_pauli_equivalent_to_this_standard_unitary(gunitary, 'H'):
H_name = gn; break
# If CNOT is available, add it as a template for 'CNOT'.
if cnot_name is not None:
compilation_rules._clifford_templates['CNOT'] = [(_Label(cnot_name, (0, 1)),)]
# If Hadamard is also available, add the standard conjugation as template for reversed CNOT.
if H_name is not None:
compilation_rules._clifford_templates['CNOT'].append((_Label(H_name, 0), _Label(H_name, 1), _Label(
cnot_name, (1, 0)), _Label(H_name, 0), _Label(H_name, 1)))
# If CNOT isn't available, look to see if we have CPHASE gate in the model (with any name). If we do *and*
# we have Hadamards, we add the obvious construction of CNOT from CPHASE and Hadamards as a template
else:
cphase_name = cls._find_std_gate(base_processor_spec, 'CPHASE')
# If we find CPHASE, and we have a Hadamard-like gate, we add used them to add a CNOT compilation
# template.
if H_name is not None:
if cphase_name is not None:
if I_name is not None:
# we explicitly put identity gates into template (so noise on them is simluated correctly?)
# Add it with CPHASE in both directions, in case the CPHASES have been specified as being
# available in only one direction
compilation_rules._clifford_templates['CNOT'] = [
(_Label(I_name, 0), _Label(H_name, 1), _Label(cphase_name, (0, 1)), _Label(I_name, 0),
_Label(H_name, 1))]
compilation_rules._clifford_templates['CNOT'].append(
(_Label(I_name, 0), _Label(H_name, 1), _Label(cphase_name, (1, 0)), _Label(I_name, 0),
_Label(H_name, 1)))
else: # similar, but without explicit identity gates
compilation_rules._clifford_templates['CNOT'] = [
(_Label(H_name, 1), _Label(cphase_name, (0, 1)), _Label(H_name, 1))]
compilation_rules._clifford_templates['CNOT'].append(
(_Label(H_name, 1), _Label(cphase_name, (1, 0)), _Label(H_name, 1)))
# After adding default templates, we know generate compilations for CNOTs between all connected pairs. If the
# default templates were not relevant or aren't relevant for some qubits, this will generate new templates by
# brute force.
for gate in two_q_gates:
not_locally_compilable = []
for q1 in base_processor_spec.qubit_labels:
for q2 in base_processor_spec.qubit_labels:
if q1 == q2: continue # 2Q gates must be on different qubits!
for gname in two_q_gates:
if verbosity > 0:
print("Creating a circuit to implement {} {} on qubits {}...".format(
gname, descs[compile_type], (q1, q2)))
try:
compilation_rules.add_local_compilation_of(
_Label(gname, (q1, q2)), verbosity=verbosity)
except CompilationError:
not_locally_compilable.append((gname, q1, q2))
# If requested, try to compile remaining 2Q gates that are `non-local` (not between neighbouring qubits)
# using specific algorithms.
if add_nonlocal_two_q_gates:
for gname, q1, q2 in not_locally_compilable:
compilation_rules.add_nonlocal_compilation_of(_Label(gname, (q1, q2)),
verbosity=verbosity)
return compilation_rules
@classmethod
def _find_std_gate(cls, base_processor_spec, std_gate_name):
""" Check to see of a standard/internal gate exists in a processor spec """
for gn in base_processor_spec.gate_names:
if callable(base_processor_spec.gate_unitaries[gn]): continue # can't pre-process factories
if _itgs.is_gate_this_standard_unitary(base_processor_spec.gate_unitaries[gn], std_gate_name):
return gn
return None
def __init__(self, native_gates_processorspec, compile_type="absolute"):
# processor_spec: holds all native Clifford gates (requested gates compile into circuits of these)
self.processor_spec = native_gates_processorspec
self.compile_type = compile_type # "absolute" or "paulieq"
self._clifford_templates = _collections.defaultdict(list) # keys=gate names (strs); vals=tuples of Labels
self.connectivity = {} # QubitGraphs for gates currently compiled in library (key=gate_name)
super(CliffordCompilationRules, self).__init__()
def _create_local_compilation_of(self, oplabel, unitary=None, srep=None, max_iterations=10, verbosity=1):
"""
Constructs a local compilation of `oplabel`.
An existing template is used if one is available, otherwise a new
template is created using an iterative procedure. Raises
:class:`CompilationError` when no compilation can be found.
Parameters
----------
oplabel : Label
The label of the gate to compile. If `oplabel.name` is a
recognized standard Clifford name (e.g. 'H', 'P', 'X', 'CNOT')
then no further information is needed. Otherwise, you must specify
either (or both) of `unitary` or `srep` *unless* the compilation
for this oplabel has already been previously constructed and force
is `False`. In that case, the previously constructed compilation will
be returned in all cases, and so this method does not need to know
what the gate actually is.
unitary : numpy.ndarray, optional
The unitary action of the gate being compiled. If, as is typical,
you're compiling using Clifford gates, then this unitary should
correspond to a Clifford operation. If you specify `unitary`,
you don't need to specify `srep` - it is computed automatically.
srep : tuple, optional
The `(smatrix, svector)` tuple giving the symplectic representation
of the gate being compiled.
max_iterations : int, optional
The maximum number of iterations for the iterative compilation
algorithm.
verbosity : int, optional
An integer >= 0 specifying how much detail to send to stdout.
Returns
-------
Circuit
"""
# Template compilations always use integer qubit labels: 0 to N
# where N is the number of qubits in the template's overall label
# (i.e. its key in self._clifford_templates)
def to_real_label(template_label):
""" Convert a "template" operation label (which uses integer qubit labels
0 to N) to a "real" label for a potential gate in self.processor_spec. """
qlabels = [oplabel.sslbls[i] for i in template_label.sslbls]
return _Label(template_label.name, qlabels)
def to_template_label(real_label):
""" The reverse (qubits in template == oplabel.qubits) """
qlabels = [oplabel.sslbls.index(lbl) for lbl in real_label.sslbls]
return _Label(real_label.name, qlabels)
def is_local_compilation_feasible(allowed_gatenames):
""" Whether template_labels can possibly be enough
gates to compile a template for op_label with """
if oplabel.num_qubits <= 1:
return len(allowed_gatenames) > 0 # 1Q gates, anything is ok
elif oplabel.num_qubits == 2:
# 2Q gates need a compilation gate that is also 2Q (can't do with just 1Q gates!)
return max([self.processor_spec.gate_num_qubits(gn) for gn in allowed_gatenames]) == 2
else:
# >2Q gates need to make sure there's some connected path
return True # future: update using graphs stuff?
template_to_use = None
for template_compilation in self._clifford_templates.get(oplabel.name, []):
#Check availability of gates in self.model to determine
# whether template_compilation can be applied.
if all([self.processor_spec.is_available(gl) for gl in map(to_real_label, template_compilation)]):
template_to_use = template_compilation
if verbosity > 0: print("Existing template found!")
break # compilation found!
else: # no existing templates can be applied, so make a new one
#construct a list of the available gates on the qubits of `oplabel` (or a subset of them)
available_gatenames = self.processor_spec.available_gatenames(oplabel.sslbls)
available_srep_dict = self.processor_spec.compute_clifford_symplectic_reps(available_gatenames)
if is_local_compilation_feasible(available_gatenames):
available_gatelabels = [to_template_label(gl) for gn in available_gatenames
for gl in self.processor_spec.available_gatelabels(gn, oplabel.sslbls)]
template_to_use = self.add_clifford_compilation_template(
oplabel.name, oplabel.num_qubits, unitary, srep,
available_gatelabels, available_srep_dict,
verbosity=verbosity, max_iterations=max_iterations)
#If a template has been found, use it.
if template_to_use is not None:
opstr = list(map(to_real_label, template_to_use))
return _Circuit(layer_labels=opstr, line_labels=self.processor_spec.qubit_labels)
else:
raise CompilationError("Cannot locally compile %s" % str(oplabel))
def _get_local_compilation_of(self, oplabel, unitary=None, srep=None, max_iterations=10, force=False, verbosity=1):
"""
Gets a new local compilation of `oplabel`.
Parameters
----------
oplabel : Label
The label of the gate to compile. If `oplabel.name` is a
recognized standard Clifford name (e.g. 'H', 'P', 'X', 'CNOT')
then no further information is needed. Otherwise, you must specify
either (or both) of `unitary` or `srep`.
unitary : numpy.ndarray, optional
The unitary action of the gate being compiled. If, as is typical,
you're compiling using Clifford gates, then this unitary should
correspond to a Clifford operation. If you specify `unitary`,
you don't need to specify `srep` - it is computed automatically.
srep : tuple, optional
The `(smatrix, svector)` tuple giving the symplectic representation
of the gate being compiled.
max_iterations : int, optional
The maximum number of iterations for the iterative compilation
algorithm.
force : bool, optional
If True, then a compilation is recomputed even if `oplabel`
already exists in this `CompilationLibrary`. Otherwise
compilations are only computed when they are *not* present.
verbosity : int, optional
An integer >= 0 specifying how much detail to send to stdout.
Returns
-------
None
"""
if not force and oplabel in self.specific_compilations:
return self.specific_compilations[oplabel] # don't re-compute unless we're told to
circuit = self._create_local_compilation_of(oplabel,
unitary=unitary,
srep=srep,
max_iterations=max_iterations,
verbosity=verbosity)
return circuit
def add_local_compilation_of(self, oplabel, unitary=None, srep=None, max_iterations=10, force=False, verbosity=1):
"""
Adds a new local compilation of `oplabel`.
Parameters
----------
oplabel : Label
The label of the gate to compile. If `oplabel.name` is a
recognized standard Clifford name (e.g. 'H', 'P', 'X', 'CNOT')
then no further information is needed. Otherwise, you must specify
either (or both) of `unitary` or `srep`.
unitary : numpy.ndarray, optional
The unitary action of the gate being compiled. If, as is typical,
you're compiling using Clifford gates, then this unitary should
correspond to a Clifford operation. If you specify `unitary`,
you don't need to specify `srep` - it is computed automatically.
srep : tuple, optional
The `(smatrix, svector)` tuple giving the symplectic representation
of the gate being compiled.
max_iterations : int, optional
The maximum number of iterations for the iterative compilation
algorithm.
force : bool, optional
If True, then a compilation is recomputed even if `oplabel`
already exists in this `CompilationLibrary`. Otherwise
compilations are only computed when they are *not* present.
verbosity : int, optional
An integer >= 0 specifying how much detail to send to stdout.
Returns
-------
None
"""
circuit = self._get_local_compilation_of(oplabel, unitary, srep, max_iterations, force, verbosity)
self.add_specific_compilation_rule(oplabel, circuit, unitary)
def add_clifford_compilation_template(self, gate_name, nqubits, unitary, srep,
available_gatelabels, available_sreps,
verbosity=1, max_iterations=10):
"""
Adds a new compilation template for `gate_name`.
Parameters
----------
gate_name : str
The gate name to create a compilation for. If it is
recognized standard Clifford name (e.g. 'H', 'P', 'X', 'CNOT')
then `unitary` and `srep` can be None. Otherwise, you must specify
either (or both) of `unitary` or `srep`.
nqubits : int
The number of qubits this gate acts upon.
unitary : numpy.ndarray
The unitary action of the gate being templated. If, as is typical,
you're compiling using Clifford gates, then this unitary should
correspond to a Clifford operation. If you specify `unitary`,
you don't need to specify `srep` - it is computed automatically.
srep : tuple, optional
The `(smatrix, svector)` tuple giving the symplectic representation
of the gate being templated.
available_glabels : list
A list of the gate labels (:class:`Label` objects) that are available for
use in compilations.
available_sreps : dict
A dictionary of available symplectic representations. Keys are gate
labels and values are numpy arrays.
verbosity : int, optional
An integer >= 0 specifying how much detail to send to stdout.
max_iterations : int, optional
The maximum number of iterations for the iterative
template compilation-finding algorithm.
Returns
-------
tuple
A tuple of the operation labels (essentially a circuit) specifying
the template compilation that was generated.
"""
# The unitary is specifed, this takes priority and we use it to construct the
# symplectic rep of the gate.
if unitary is not None:
srep = _symp.unitary_to_symplectic(unitary, flagnonclifford=True)
# If the unitary has not been provided and smatrix and svector are both None, then
# we find them from the dictionary of standard gates.
if srep is None:
template_lbl = _Label(gate_name, tuple(range(nqubits))) # integer ascending qubit labels
smatrix, svector = _symp.symplectic_rep_of_clifford_layer(template_lbl, nqubits)
else:
smatrix, svector = srep
assert(_symp.check_valid_clifford(smatrix, svector)), "The gate is not a valid Clifford!"
assert(_np.shape(smatrix)[0] // 2 == nqubits), \
"The gate acts on a different number of qubits to stated by `nqubits`"
if verbosity > 0:
if self.compile_type == 'absolute':
print("- Generating a template for a compilation of {}...".format(gate_name), end='\n')
elif self.compile_type == 'paulieq':
print("- Generating a template for a pauli-equivalent compilation of {}...".format(gate_name), end='\n')
obtained_sreps = {}
#Separate the available operation labels by their target qubits
available_glabels_by_qubit = _collections.defaultdict(list)
for gl in available_gatelabels:
available_glabels_by_qubit[tuple(sorted(gl.qubits))].append(gl)
#sort qubit labels b/c order doesn't matter and can't hash sets
# Construst all possible circuit layers acting on the qubits.
all_layers = []
#Loop over all partitions of the nqubits
for p in _lt.partitions(nqubits):
pi = _np.concatenate(([0], _np.cumsum(p)))
to_iter_over = [available_glabels_by_qubit[tuple(range(pi[i], pi[i + 1]))] for i in range(len(p))]
for gls_in_layer in _itertools.product(*to_iter_over):
all_layers.append(gls_in_layer)
# Find the symplectic action of all possible circuits of length 1 on the qubits
for layer in all_layers:
obtained_sreps[layer] = _symp.symplectic_rep_of_clifford_layer(layer, nqubits, srep_dict=available_sreps)
# find the 1Q identity gate name
I_name = self._find_std_gate(self.processor_spec, 'I')
# Main loop. We go through the loop at most max_iterations times
found = False
for counter in range(0, max_iterations):
if verbosity > 0:
print(" - Checking all length {} {}-qubit circuits... ({})".format(counter + 1,
nqubits,
len(obtained_sreps)))
candidates = [] # all valid compilations, if any, of this length.
# Look to see if we have found a compilation
for seq, (s, p) in obtained_sreps.items():
if _np.array_equal(smatrix, s):
if self.compile_type == 'paulieq' or \
(self.compile_type == 'absolute' and _np.array_equal(svector, p)):
candidates.append(seq)
found = True
# If there is more than one way to compile gate at this circuit length, pick the
# one containing the most idle gates.
if len(candidates) > 1:
# Look at each sequence, and see if it has more than or equal to max_number_of_idles.
# If so, set it to the current chosen sequence.
if I_name is not None:
number_of_idles = 0
max_number_of_idles = 0
for seq in candidates:
number_of_idles = len([x for x in seq if x.name == I_name])
if number_of_idles >= max_number_of_idles:
max_number_of_idles = number_of_idles
compilation = seq
else:
# idles are absent from circuits - just take one with smallest depth
min_depth = 1e100
for seq in candidates:
depth = len(seq)
if depth < min_depth:
min_depth = depth
compilation = seq
elif len(candidates) == 1:
compilation = candidates[0]
# If we have found a compilation, leave the loop
if found:
if verbosity > 0: print("Compilation template created!")
break
# If we have reached the maximum number of iterations, quit the loop
# before we construct the symplectic rep for all sequences of a longer length.
if (counter == max_iterations - 1):
print(" - Maximum iterations reached without finding a compilation !")
return None
# Construct the gates obtained from the next length sequences.
new_obtained_sreps = {}
for seq, (s, p) in obtained_sreps.items():
# Add all possible tensor products of single-qubit gates to the end of the sequence
for layer in all_layers:
# Calculate the symp rep of this parallel gate
sadd, padd = _symp.symplectic_rep_of_clifford_layer(layer, nqubits, srep_dict=available_sreps)
key = seq + layer # tuple/Circuit concatenation
# Calculate and record the symplectic rep of this gate sequence.
new_obtained_sreps[key] = _symp.compose_cliffords(s, p, sadd, padd)
# Update list of potential compilations
obtained_sreps = new_obtained_sreps
#Compilation done: remove identity labels, as these are just used to
# explicitly keep track of the number of identity gates in a circuit (really needed?)
compilation = list(filter(lambda gl: gl.name != I_name, compilation))
#Store & return template that was found
self._clifford_templates[gate_name].append(compilation)
return compilation
#PRIVATE
def _compute_connectivity_of(self, gate_name):
"""
Compute the connectivity for `gate_name` using the (compiled) gates available this library.
Connectivity is defined in terms of nearest-neighbor links, and the
resulting :class:`QubitGraph`, is stored in `self.connectivity[gate_name]`.
Parameters
----------
gate_name : str
gate name to compute connectivity for.
Returns
-------
None
"""
nQ = self.processor_spec.num_qubits
qubit_labels = self.processor_spec.qubit_labels
d = {qlbl: i for i, qlbl in enumerate(qubit_labels)}
assert(len(qubit_labels) == nQ), "Number of qubit labels is inconsistent with Model dimension!"
connectivity = _np.zeros((nQ, nQ), dtype=bool)
for compiled_gatelabel in self.specific_compilations.keys():
if compiled_gatelabel.name == gate_name:
for p in _itertools.permutations(compiled_gatelabel.qubits, 2):
connectivity[d[p[0]], d[p[1]]] = True
# Note: d converts from qubit labels to integer indices