-
Notifications
You must be signed in to change notification settings - Fork 54
/
Copy pathrandomcircuit.py
2463 lines (2000 loc) · 117 KB
/
randomcircuit.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
"""
Random circuit sampling functions.
"""
#***************************************************************************************************
# Copyright 2015, 2019, 2025 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 copy as _copy
import itertools as _itertools
import numpy as _np
from pygsti.algorithms import compilers as _cmpl
from pygsti.circuits import circuit as _cir
from pygsti.baseobjs import label as _lbl
from pygsti.tools import group as _rbobjs
from pygsti.tools import symplectic as _symp
from pygsti.tools import compilationtools as _comp
from pygsti.tools import internalgates as _gates
################################
#### BEGIN CODE FROM JORDAN ######
################################
def sample_haar_random_one_qubit_unitary_parameters():
"""
TODO: docstring
"""
psi, chi = 2 * _np.pi * _np.random.rand(2)
psi = psi - _np.pi
chi = chi - _np.pi
phi = _np.arcsin(_np.sqrt(_np.random.rand(1)))[0]
#U = _np.exp(1j*alpha)*_np.array([[_np.exp(1j*psi)*_np.cos(phi), _np.exp(1j*chi)*_np.sin(phi)],[-1*_np.exp(-1j*chi)
# *_np.sin(phi), _np.exp(-1j*psi)*_np.cos(phi)]])
#this needs to be decomposed in the form Zrot(theta3) Xpi/2 Zrot(theta2) Xpi/2 Zrot(theta1)
theta1 = _comp.mod_2pi(psi - chi + _np.pi)
theta2 = _comp.mod_2pi(_np.pi - 2 * phi)
theta3 = _comp.mod_2pi(psi + chi)
return (theta1, theta2, theta3)
def sample_random_clifford_one_qubit_unitary_parameters():
"""
TODO: docstring
"""
theta1 = _comp.mod_2pi(_np.random.randint(4) * _np.pi / 2)
theta2 = _comp.mod_2pi(_np.random.randint(4) * _np.pi / 2)
theta3 = _comp.mod_2pi(_np.random.randint(4) * _np.pi / 2)
return (theta1, theta2, theta3)
def sample_compiled_haar_random_one_qubit_gates_zxzxz_circuit(pspec, zname='Gzr', xname='Gxpi2', qubit_labels=None):
"""
TODO: docstring #generate layer of random unitaries and make a series of circuit layers with the compiled versions
of these
"""
if qubit_labels is not None:
n = len(qubit_labels)
qubits = qubit_labels[:] # copy this list
else:
n = pspec.num_qubits
qubits = pspec.qubit_labels[:] # copy this list
Xpi2layer = _cir.Circuit(layer_labels=[[(xname, qubits[t]) for t in range(n)], ])
# samples random rotation angles.
rot_angles = [sample_haar_random_one_qubit_unitary_parameters() for q in qubits]
circ = _cir.Circuit(layer_labels=[[_lbl.Label(zname, qubits[t], args=(str(rot_angles[t][0]),))
for t in range(n)], ], editable=True)
circ.append_circuit_inplace(Xpi2layer)
circ.append_circuit_inplace(_cir.Circuit(layer_labels=[[_lbl.Label(zname, qubits[t], args=(str(rot_angles[t][1]),))
for t in range(n)], ]))
circ.append_circuit_inplace(Xpi2layer)
circ.append_circuit_inplace(_cir.Circuit(layer_labels=[[_lbl.Label(zname, qubits[t], args=(str(rot_angles[t][2]),))
for t in range(n)], ]))
circ.done_editing()
return circ
def sample_compiled_random_clifford_one_qubit_gates_zxzxz_circuit(pspec, zname='Gzr', xname='Gxpi2', qubit_labels=None):
"""
TODO: docstring #generate layer of random unitaries and make a series of circuit layers with the compiled versions
of these
"""
if qubit_labels is not None:
n = len(qubit_labels)
qubits = qubit_labels[:] # copy this list
else:
n = pspec.num_qubits
qubits = pspec.qubit_labels[:] # copy this list
Xpi2layer = _cir.Circuit(layer_labels=[[(xname, qubits[t]) for t in range(n)], ])
# samples random rotation angles.
rot_angles = [sample_random_clifford_one_qubit_unitary_parameters() for q in qubits]
circ = _cir.Circuit(layer_labels=[[_lbl.Label(zname, qubits[t], args=(str(rot_angles[t][0]),))
for t in range(n)], ], editable=True)
circ.append_circuit_inplace(Xpi2layer)
circ.append_circuit_inplace(_cir.Circuit(layer_labels=[[_lbl.Label(zname, qubits[t], args=(str(rot_angles[t][1]),))
for t in range(n)], ]))
circ.append_circuit_inplace(Xpi2layer)
circ.append_circuit_inplace(_cir.Circuit(layer_labels=[[_lbl.Label(zname, qubits[t], args=(str(rot_angles[t][2]),))
for t in range(n)], ]))
circ.done_editing()
return circ
def sample_random_cz_zxzxz_circuit(pspec, length, qubit_labels=None, two_q_gate_density=0.25,
one_q_gate_type='haar',
two_q_gate_args_lists=None):
'''
TODO: docstring
Generates a forward circuits with benchmark depth d for non-clifford mirror randomized benchmarking.
The circuits alternate Haar-random 1q unitaries and layers of Gczr gates.
If two_q_gate_args_lists is None, then we set it to {'Gczr': [(str(_np.pi / 2),), (str(-_np.pi / 2),)]}.
'''
if two_q_gate_args_lists is None:
two_q_gate_args_lists = {'Gczr': [(str(_np.pi / 2),), (str(-_np.pi / 2),)]}
#choose length to be the number of (2Q layer, 1Q layer) blocks
circuit = _cir.Circuit(layer_labels=[], line_labels=qubit_labels, editable=True)
for a in range(length):
#generate random 1q unitary layer
if one_q_gate_type == 'haar':
new_layer = sample_compiled_haar_random_one_qubit_gates_zxzxz_circuit(pspec, qubit_labels=qubit_labels)
elif one_q_gate_type == 'clifford':
new_layer = sample_compiled_random_clifford_one_qubit_gates_zxzxz_circuit(pspec, qubit_labels=qubit_labels)
else:
raise ValueError("Unknown value {} for `one_q_gate_type`!".format(one_q_gate_type))
#append new layer to circuit
circuit.append_circuit_inplace(new_layer)
#generate 2q gate layer
sampled_layer = sample_circuit_layer_by_edgegrab(pspec, qubit_labels=qubit_labels,
two_q_gate_density=two_q_gate_density,
one_q_gate_names=[], gate_args_lists=two_q_gate_args_lists)
if sampled_layer == []: new_layer = _cir.Circuit(layer_labels=[[]], line_labels=qubit_labels)
else: new_layer = _cir.Circuit([sampled_layer])
#append new layer to circuit
circuit.append_circuit_inplace(new_layer)
#add one more layer of Haar-random 1Q unitaries
if one_q_gate_type == 'haar':
new_layer = sample_compiled_haar_random_one_qubit_gates_zxzxz_circuit(pspec, qubit_labels=qubit_labels)
elif one_q_gate_type == 'clifford':
new_layer = sample_compiled_random_clifford_one_qubit_gates_zxzxz_circuit(pspec, qubit_labels=qubit_labels)
else:
raise ValueError("Unknown value {} for `one_q_gate_type`!".format(one_q_gate_type))
circuit.append_circuit_inplace(new_layer)
circuit.done_editing()
return circuit
def find_all_sets_of_compatible_two_q_gates(edgelist, n, gatename='Gcnot', aslabel=False):
"""
TODO: docstring
<TODO summary>
Parameters
----------
edgelist : <TODO typ>
<TODO description>
n : int
The number of two-qubit gates to have in the set.
gatename : <TODO typ>, optional
<TODO description>
aslabel : <TODO typ>, optional
<TODO description>
Returns
-------
<TODO typ>
"""
co2Qgates = []
# Go for all combinations of n two-qubit gates from the edgelist.
for npairs in _itertools.combinations(edgelist, n):
# Make a list of the qubits involved in the gates
flat_list = [item for sublist in npairs for item in sublist]
# If no qubit is involved in more than one gate we accept the combination
if len(flat_list) == len(set(flat_list)):
if aslabel:
co2Qgates.append([_lbl.Label(gatename, pair) for pair in npairs])
else:
co2Qgates.append([gatename + ':' + pair[0] + ':' + pair[1] for pair in npairs])
return co2Qgates
def sample_circuit_layer_by_edgegrab(pspec, qubit_labels=None, two_q_gate_density=0.25, one_q_gate_names=None,
gate_args_lists=None, rand_state=None):
"""
TODO: docstring
<TODO summary>
Parameters
----------
pspec : <TODO typ>
<TODO description>
qubit_labels : <TODO typ>, optional
<TODO description>
mean_two_q_gates : <TODO typ>, optional
<TODO description>
modelname : <TODO typ>, optional
<TODO description>
rand_state: RandomState, optional
A np.random.RandomState object for seeding RNG
Returns
-------
<TODO typ>
"""
if gate_args_lists is None: gate_args_lists = {}
if qubit_labels is None:
qubits = list(pspec.qubit_labels[:]) # copy this list
else:
assert(isinstance(qubit_labels, (list, tuple))), "SubsetQs must be a list or a tuple!"
qubits = list(qubit_labels[:]) # copy this list
if rand_state is None:
rand_state = _np.random.RandomState()
# Prep the sampling variables.
sampled_layer = []
edgelist = pspec.compute_2Q_connectivity().edges()
edgelist = [e for e in edgelist if all([q in qubits for q in e])]
selectededges = []
# Go through until all qubits have been assigned a gate.
while len(edgelist) > 0:
edge = edgelist[rand_state.randint(0, len(edgelist))]
selectededges.append(edge)
# Delete all edges containing these qubits.
edgelist = [e for e in edgelist if not any([q in e for q in edge])]
num2Qgates = len(selectededges)
if len(qubits) > 1:
mean_two_q_gates = len(qubits) * two_q_gate_density / 2
else:
mean_two_q_gates = 0
assert(num2Qgates >= mean_two_q_gates), "Device has insufficient connectivity!"
if mean_two_q_gates > 0:
twoQprob = mean_two_q_gates / num2Qgates
else:
twoQprob = 0
unusedqubits = _copy.copy(qubits)
ops_on_qubits = pspec.compute_ops_on_qubits()
for edge in selectededges:
if bool(rand_state.binomial(1, twoQprob)):
# The two-qubit gates on that edge.
possibleops = ops_on_qubits[edge]
argless_gate_label = possibleops[rand_state.randint(0, len(possibleops))]
if argless_gate_label.name not in gate_args_lists.keys():
sampled_layer.append(argless_gate_label)
else:
possibleargs = gate_args_lists[argless_gate_label.name]
args = possibleargs[rand_state.randint(0, len(possibleargs))]
sampled_layer.append(_lbl.Label(argless_gate_label.name, edge, args=args))
for q in edge:
del unusedqubits[unusedqubits.index(q)]
if one_q_gate_names is None or len(one_q_gate_names) > 0:
for q in unusedqubits:
if one_q_gate_names is None:
possibleops = ops_on_qubits[(q,)]
else:
possibleops = [gate_lbl for gate_lbl in ops_on_qubits[(q,)] if gate_lbl.name in one_q_gate_names]
gate_label = possibleops[rand_state.randint(0, len(possibleops))]
sampled_layer.append(gate_label)
return sampled_layer
def sample_circuit_layer_by_q_elimination(pspec, qubit_labels=None, two_q_prob=0.5, rand_state=None):
"""
Samples a random circuit layer by eliminating qubits one by one.
This sampler works with any connectivity, but the expected number of 2-qubit gates
in a layer depends on both the specified 2-qubit gate probability and the exact
connectivity graph.
This sampler is the following algorithm: List all the qubits, and repeat the
following steps until all qubits are deleted from this list. 1) Uniformly at random
pick a qubit from the list, and delete it from the list 2) Flip a coin with bias
`two_q_prob` to be "Heads". 3) If "Heads" then -- if there is one or more 2-qubit gates
from this qubit to other qubits still in the list -- pick one of these at random.
4) If we haven't chosen a 2-qubit gate for this qubit ("Tails" or "Heads" but there
are no possible 2-qubit gates) then pick a uniformly random 1-qubit gate to apply to
this qubit.
Parameters
----------
pspec : QubitProcessorSpec
The QubitProcessorSpec for the device that the circuit layer is being sampled for. Unless
`qubit_labels` is not None, a circuit layer is sampled over all the qubits in `pspec`.
qubit_labels : list, optional
If not None, a list of the qubits to sample the circuit layer for. This is a subset of
`pspec.qubit_labels`. If None, the circuit layer is sampled to acton all the qubits
in `pspec`.
two_q_prob : float, optional
If a 2-qubit can is still possible on a qubit at that stage of the sampling, this is
the probability a 2-qubit gate is chosen for that qubit. The expected number of
2-qubit gates per layer depend on this quantity and the connectivity graph of
the device.
rand_state: RandomState, optional
A np.random.RandomState object for seeding RNG
Returns
-------
list of gates
A list of gate Labels that defines a "complete" circuit layer (there is one and
only one gate acting on each qubit in `pspec` or `qubit_labels`).
"""
if qubit_labels is None:
n = pspec.num_qubits
qubits = list(pspec.qubit_labels[:]) # copy this list
else:
assert(isinstance(qubit_labels, (list, tuple))), "SubsetQs must be a list or a tuple!"
n = len(qubit_labels)
qubits = list(qubit_labels[:]) # copy this list
if rand_state is None:
rand_state = _np.random.RandomState()
possible_ops = pspec.compute_ops_on_qubits()
# Prep the sampling variables.
sampled_layer = []
remaining_qubits = _copy.deepcopy(qubits)
num_qubits_used = 0
# Go through until all qubits have been assigned a gate.
while num_qubits_used < n:
# Pick a random qubit
r = rand_state.randint(0, n - num_qubits_used)
q = remaining_qubits[r]
del remaining_qubits[r]
oneq_ops_on_q = possible_ops[(q,)]
twoq_ops_on_q = []
for q2 in remaining_qubits:
twoq_ops_on_q += possible_ops[(q, q2)]
twoq_ops_on_q += possible_ops[(q2, q)]
# Decide whether to to implement a 2-qubit gate or a 1-qubit gate.
if len(twoq_ops_on_q) == 0:
do_twoq_gate = False
else:
do_twoq_gate = rand_state.choice([False, True], p=[1 - two_q_prob, two_q_prob])
# Implement a random 1-qubit gate on qubit q.
if not do_twoq_gate:
sampled_layer.append(oneq_ops_on_q[rand_state.randint(0, len(oneq_ops_on_q))])
num_qubits_used += 1. # We have assigned gates to 1 of the remaining qubits.
# Implement a 2-qubit gate on qubit q.
else:
lbl = twoq_ops_on_q[rand_state.randint(0, len(twoq_ops_on_q))]
sampled_layer.append(lbl)
# Find the label of the other qubit in the sampled gate.
other_qubit = lbl.qubits[0]
if other_qubit == q:
other_qubit = lbl.qubits[1]
del remaining_qubits[remaining_qubits.index(other_qubit)]
num_qubits_used += 2
return sampled_layer
def sample_circuit_layer_by_co2_q_gates(pspec, qubit_labels, co2_q_gates, co2_q_gates_prob='uniform', two_q_prob=1.0,
one_q_gate_names='all', rand_state=None):
"""
Samples a random circuit layer using the specified list of "compatible two-qubit gates" (co2_q_gates).
That is, the user inputs a list (`co2_q_gates`) specifying 2-qubit gates that are
"compatible" -- meaning that they can be implemented simulatenously -- and a distribution
over the different compatible sets, and a layer is sampled from this via:
1. Pick a set of compatible two-qubit gates from the list `co2_q_gates`, according to the
distribution specified by `co2_q_gates_prob`.
2. For each 2-qubit gate in the chosen set of compatible gates, with probability `two_q_prob`
add this gate to the layer.
3. Uniformly sample 1-qubit gates for any qubits that don't yet have a gate on them,
from those 1-qubit gates specified by `one_q_gate_names`.
For example, consider 4 qubits with linear connectivity. a valid `co2_q_gates` list is
`co2_q_gates = [[,],[Label(Gcphase,(0,1)),Label(Gcphase,(2,3))]]` which consists of an
element containing zero 2-qubit gates and an element containing two 2-qubit gates
that can be applied in parallel. In this example there are 5 possible sets of compatible
2-qubit gates:
1. [,] (zero 2-qubit gates)
2. [Label(Gcphase,(0,1)),] (one of the three 2-qubit gate)
3. [Label(Gcphase,(1,2)),] (one of the three 2-qubit gate)
4. [Label(Gcphase,(2,3)),] (one of the three 2-qubit gate)
5. [Label(Gcphase,(0,1)), Label(Gcphase,(2,3)),] (the only compatible pair of 2-qubit gates).
The list of compatible two-qubit gates `co2_q_gates` can be any list containing anywhere
from 1 to all 5 of these lists.
In order to allow for convenient sampling of some commonly useful distributions,
`co2_q_gates` can be a list of lists of lists of compatible 2-qubit gates ("nested" sampling).
In this case, a list of lists of compatible 2-qubit gates is picked according to the distribution
`co2_q_gates_prob`, and then one of the sublists of compatible 2-qubit gates in the selected list is
then chosen uniformly at random. For example, this is useful for sampling a layer containing one
uniformly random 2-qubit gate with probability p and a layer of 1-qubit gates with probability
1-p. Here, we can specify `co2_q_gates` as `[[],[[the 1st 2Q-gate,],[the 2nd 2Q-gate,], ...]]` and
set `two_q_prob=1` and `co2_q_gates_prob = [1-p,p]`.
Parameters
----------
pspec : QubitProcessorSpec
The QubitProcessorSpec for the device that the circuit layer is being sampled for. Unless
`qubit_labels` is not None, a circuit layer is sampled over all the qubits in `pspec`.
qubit_labels : list
If not None, a list of the qubits to sample the circuit layer for. This is a subset of
`pspec.qubit_labels`. If None, the circuit layer is sampled to act on all the qubits
in `pspec`.
co2_q_gates : list
This is either:
1. A list of lists of 2-qubit gate Labels that can be applied in parallel.
2. A list of lists of lists of 2-qubit gate Labels that can be applied in parallel.
In case (1) each list in `co2_q_gates` should contain 2-qubit gates, in the form of Labels,
that can be applied in parallel and act only on the qubits in `pspec` if `qubit_labels` is None,
or act only on the qubits in `qubit_labels` if `qubit_labels` is not None. The sampler then picks
one of these compatible sets of gates (with probability specified by `co2_q_gates_prob`, and converts
this into a circuit layer by applying the 2-qubit gates it contains with the user-specified
probability `two_q_prob`, and augmenting these 2-qubit gates with 1-qubit gates on all other qubits.
In case (2) a sublist of lists is sampled from `co2_q_gates` according to `co2_q_gates_prob` and then we
proceed as in case (1) but as though `co2_q_gates_prob` is the uniform distribution.
co2_q_gates_prob : str or list of floats
If a list, they are unnormalized probabilities to sample each of the elements of `co2_q_gates`. So it
is a list of non-negative floats of the same length as `co2_q_gates`. If 'uniform', then the uniform
distribution is used.
two_q_prob : float, optional
The probability for each two-qubit gate to be applied to a pair of qubits, after a
set of compatible 2-qubit gates has been chosen. The expected number of 2-qubit
gates in a layer is `two_q_prob` times the expected number of 2-qubit gates in a
set of compatible 2-qubit gates sampled according to `co2_q_gates_prob`.
one_q_gate_names : 'all' or list of strs, optional
If not 'all', a list of the names of the 1-qubit gates to be sampled from when applying
a 1-qubit gate to a qubit. If this is 'all', the full set of 1-qubit gate names is
extracted from the QubitProcessorSpec.
rand_state: RandomState, optional
A np.random.RandomState object for seeding RNG
Returns
-------
list of gates
A list of gate Labels that defines a "complete" circuit layer (there is one and
only one gate acting on each qubit).
"""
if rand_state is None:
rand_state = _np.random.RandomState()
# Pick the sector.
if isinstance(co2_q_gates_prob, str):
assert(co2_q_gates_prob == 'uniform'), "If `co2_q_gates_prob` is a string it must be 'uniform!'"
twoqubitgates_or_nestedco2Qgates = co2_q_gates[rand_state.randint(0, len(co2_q_gates))]
else:
co2_q_gates_prob = _np.array(co2_q_gates_prob) / _np.sum(co2_q_gates_prob)
x = list(rand_state.multinomial(1, co2_q_gates_prob))
twoqubitgates_or_nestedco2Qgates = co2_q_gates[x.index(1)]
# The special case where the selected co2_q_gates contains no gates or co2_q_gates.
if len(twoqubitgates_or_nestedco2Qgates) == 0:
twoqubitgates = twoqubitgates_or_nestedco2Qgates
# If it's a nested sector, sample uniformly from the nested co2_q_gates.
elif type(twoqubitgates_or_nestedco2Qgates[0]) == list:
twoqubitgates = twoqubitgates_or_nestedco2Qgates[rand_state.randint(0, len(twoqubitgates_or_nestedco2Qgates))]
# If it's not a list of "co2_q_gates" (lists) then this is the list of gates to use.
else:
twoqubitgates = twoqubitgates_or_nestedco2Qgates
# Prep the sampling variables
sampled_layer = []
if qubit_labels is not None:
assert(isinstance(qubit_labels, list) or isinstance(qubit_labels, tuple)), "SubsetQs must be a list or a tuple!"
remaining_qubits = list(qubit_labels[:]) # copy this list
else:
remaining_qubits = list(pspec.qubit_labels[:]) # copy this list
# Go through the 2-qubit gates in the sector, and apply each one with probability two_q_prob
for i in range(0, len(twoqubitgates)):
if rand_state.binomial(1, two_q_prob) == 1:
gate = twoqubitgates[i]
# If it's a nested co2_q_gates:
sampled_layer.append(gate)
# Delete the qubits that have been assigned a gate.
del remaining_qubits[remaining_qubits.index(gate.qubits[0])]
del remaining_qubits[remaining_qubits.index(gate.qubits[1])]
# Go through the qubits which don't have a 2-qubit gate assigned to them, and pick a 1-qubit gate
clifford_ops_on_qubits = pspec.compute_clifford_ops_on_qubits()
for i in range(0, len(remaining_qubits)):
qubit = remaining_qubits[i]
# If the 1-qubit gate names are specified, use these.
if one_q_gate_names != 'all':
possibleops = [_lbl.Label(name, (qubit,)) for name in one_q_gate_names]
# If the 1-qubit gate names are not specified, find the available 1-qubit gates
else:
#if modelname == 'clifford':
possibleops = clifford_ops_on_qubits[(qubit,)]
#else:
# possibleops = pspec.models[modelname].primitive_op_labels
# l = len(possibleops)
# for j in range(0, l):
# if possibleops[l - j].num_qubits != 1:
# del possibleops[l - j]
# else:
# if possibleops[l - j].qubits[0] != qubit:
# del possibleops[l - j]
gate = possibleops[rand_state.randint(0, len(possibleops))]
sampled_layer.append(gate)
return sampled_layer
def sample_circuit_layer_of_one_q_gates(pspec, qubit_labels=None, one_q_gate_names='all', pdist='uniform',
modelname='clifford', rand_state=None):
"""
Samples a random circuit layer containing only 1-qubit gates.
The allowed 1-qubit gates are specified by `one_q_gate_names`, and the 1-qubit gates are
sampled independently and uniformly.
Parameters
----------
pspec : QubitProcessorSpec
The QubitProcessorSpec for the device that the circuit layer is being sampled for. Unless
`qubit_labels` is not None, a circuit layer is sampled over all the qubits in `pspec`.
qubit_labels : list, optional
If not None, a list of the qubits to sample the circuit layer for. This is a subset of
`pspec.qubit_labels`. If None, the circuit layer is sampled to acton all the qubits
in `pspec`.
one_q_gate_names : 'all' or list of strs, optional
If not 'all', a list of the names of the 1-qubit gates to be sampled from when applying
a 1-qubit gate to a qubit. If this is 'all', the full set of 1-qubit gate names is
extracted from the QubitProcessorSpec.
pdist : 'uniform' or list of floats, optional
If a list, they are unnormalized probabilities to sample each of the 1-qubit gates
in the list `one_q_gate_names`. If this is not 'uniform', then oneQgatename` must not
be 'all' (it must be a list so that it is unambigious which probability correpsonds
to which gate). So if not 'uniform', `pdist` is a list of non-negative floats of the
same length as `one_q_gate_names`. If 'uniform', then the uniform distribution over
the gates is used.
modelname : str, optional
Only used if one_q_gate_names is 'all'. Specifies which of the `pspec.models` to use to
extract the model. The `clifford` default is suitable for Clifford or direct RB,
but will not use any non-Clifford gates in the model.
rand_state: RandomState, optional
A np.random.RandomState object for seeding RNG
Returns
-------
list of gates
A list of gate Labels that defines a "complete" circuit layer (there is one and
only one gate acting on each qubit).
"""
if qubit_labels is not None:
assert(isinstance(qubit_labels, list) or isinstance(qubit_labels, tuple)), "SubsetQs must be a list or a tuple!"
qubits = list(qubit_labels[:]) # copy this list
else:
qubits = list(pspec.qubit_labels[:]) # copy this list
if rand_state is None:
rand_state = _np.random.RandomState()
sampled_layer = []
if isinstance(pdist, str): assert(pdist == 'uniform'), "If pdist is not a list or numpy.array it must be 'uniform'"
if one_q_gate_names == 'all':
assert(pdist == 'uniform'), "If `one_q_gate_names` = 'all', pdist must be 'uniform'"
clifford_ops_on_qubits = pspec.compute_clifford_ops_on_qubits()
if modelname == 'clifford':
for i in qubits:
try:
gate = clifford_ops_on_qubits[(i,)][rand_state.randint(
0, len(clifford_ops_on_qubits[(i,)]))]
sampled_layer.append(gate)
except:
raise ValueError("There are no 1Q Clifford gates on qubit {}!".format(i))
else: raise ValueError("Currently, 'modelname' must be 'clifford'")
else:
# A basic check for the validity of pdist.
if not isinstance(pdist, str):
assert(len(pdist) == len(one_q_gate_names)), "The pdist probability distribution is invalid!"
# Find out how many 1-qubit gate names there are
num_oneQgatenames = len(one_q_gate_names)
# Sample a gate for each qubit.
for i in qubits:
# If 'uniform', then sample according to the uniform dist.
if isinstance(pdist, str): sampled_gatename = one_q_gate_names[rand_state.randint(0, num_oneQgatenames)]
# If not 'uniform', then sample according to the user-specified dist.
else:
pdist = _np.array(pdist) / sum(pdist)
x = list(rand_state.multinomial(1, pdist))
sampled_gatename = one_q_gate_names[x.index(1)]
# Add sampled gate to the layer.
sampled_layer.append(_lbl.Label(sampled_gatename, i))
return sampled_layer
def create_random_circuit(pspec, length, qubit_labels=None, sampler='Qelimination', samplerargs=None,
addlocal=False, lsargs=None, rand_state=None):
"""
Samples a random circuit of the specified length (or ~ twice this length).
The created circuit's layers are independently sampled according to the specified
sampling distribution.
Parameters
----------
pspec : QubitProcessorSpec
The QubitProcessorSpec for the device that the circuit is being sampled for. This is always
handed to the sampler, as the first argument of the sampler function. Unless
`qubit_labels` is not None, the circuit is sampled over all the qubits in `pspec`.
length : int
If `addlocal` is False, this is the length of the sampled circuit. If `addlocal is
True the length of the circuits is 2*length+1 with odd-indexed layers sampled according
to the sampler specified by `sampler`, and the the zeroth layer + the even-indexed
layers consisting of random 1-qubit gates (with the sampling specified by `lsargs`)
qubit_labels : list, optional
If not None, a list of the qubits to sample the circuit for. This is a subset of
`pspec.qubit_labels`. If None, the circuit is sampled to act on all the qubits
in `pspec`.
sampler : str or function, optional
If a string, this should be one of: {'edgegrab'', 'Qelimination', 'co2Qgates', 'local'}.
Except for 'local', this corresponds to sampling layers according to the sampling function
in rb.sampler named circuit_layer_by* (with * replaced by 'sampler'). For 'local', this
corresponds to sampling according to rb.sampler.circuit_layer_of_oneQgates. If this is a
function, it should be a function that takes as the first argument a QubitProcessorSpec, and
returns a random circuit layer as a list of gate Label objects. Note that the default
'Qelimination' is not necessarily the most useful in-built sampler, but it is the only
sampler that requires no parameters beyond the QubitProcessorSpec *and* works for arbitrary
connectivity devices. See the docstrings for each of these samplers for more information.
samplerargs : list, optional
A list of arguments that are handed to the sampler function, specified by `sampler`.
The first argument handed to the sampler is `pspec` and `samplerargs` lists the
remaining arguments handed to the sampler. For some in-built samplers this is not
optional.
addlocal : bool, optional
If False, the circuit sampled is of length `length` and each layer is independently
sampled according to the sampler specified by `sampler`. If True, the circuit sampled
is of length 2*`length`+1 where: the zeroth + all even layers are consisting of
independently random 1-qubit gates (with the sampling specified by `lsargs`); the
odd-indexed layers are independently sampled according to `sampler`. So `length`+1
layers consist only of 1-qubit gates, and `length` layers are sampled according to
`sampler`.
lsargs : list, optional
A list of arguments that are handed to the 1-qubit gate layers sampler
rb.sampler.circuit_layer_of_oneQgates for the alternating 1-qubit-only layers that are
included in the circuit if `addlocal` is True. This argument is not used if `addlocal`
is false. Note that `pspec` is used as the first, and only required, argument of
rb.sampler.circuit_layer_of_oneQgates. If `lsargs` = [] then all available 1-qubit gates
are uniformly sampled from. To uniformly sample from only a subset of the available
1-qubit gates (e.g., the Paulis to Pauli-frame-randomize) then `lsargs` should be a
1-element list consisting of a list of the relevant gate names (e.g., `lsargs` = ['Gi,
'Gxpi, 'Gypi', 'Gzpi']).
rand_state: RandomState or int, optional (default None)
A np.random.RandomState object for seeding RNG. If an integer is passed in
this is used to set the seed for a newly constructed RNG.
Returns
-------
Circuit
A random circuit of length `length` (if not addlocal) or length 2*`length`+1 (if addlocal)
with layers independently sampled using the specified sampling distribution.
"""
if samplerargs is None:
samplerargs = []
if lsargs is None:
lsargs = []
if rand_state is None:
rand_state = _np.random.RandomState()
if isinstance(rand_state, int):
rand_state = _np.random.RandomState(rand_state)
if isinstance(sampler, str):
# Removed redundant sampler
#if sampler == 'pairingQs': sampler = sample_circuit_layer_by_pairing_qubits
if sampler == 'Qelimination': sampler = sample_circuit_layer_by_q_elimination
elif sampler == 'co2Qgates':
sampler = sample_circuit_layer_by_co2_q_gates
assert(len(samplerargs) >= 1), \
("The samplerargs must at least a 1-element list with the first element "
"the 'co2Qgates' argument of the co2Qgates sampler.")
elif sampler == 'edgegrab':
sampler = sample_circuit_layer_by_edgegrab
assert(len(samplerargs) >= 1), \
("The samplerargs must at least a 1-element list")
elif sampler == 'local': sampler = sample_circuit_layer_of_one_q_gates
else: raise ValueError("Sampler type not understood!")
if qubit_labels is not None:
assert(isinstance(qubit_labels, list) or isinstance(qubit_labels, tuple)), "SubsetQs must be a list or a tuple!"
qubits = list(qubit_labels[:]) # copy this list
else:
qubits = list(pspec.qubit_labels[:]) # copy this list
# Initialize an empty circuit, to populate with sampled layers.
circuit = _cir.Circuit(layer_labels=[], line_labels=qubits, editable=True)
# If we are not add layers of random local gates between the layers, sample 'length' layers
# according to the sampler `sampler`.
if not addlocal:
for i in range(0, length):
layer = sampler(pspec, qubit_labels, *samplerargs, rand_state=rand_state)
circuit.insert_layer_inplace(layer, 0)
# If we are adding layers of random local gates between the layers.
if addlocal:
for i in range(0, 2 * length + 1):
local = not bool(i % 2)
# For odd layers, we uniformly sample the specified type of local gates.
if local:
layer = sample_circuit_layer_of_one_q_gates(pspec, qubit_labels, *lsargs, rand_state=rand_state)
# For even layers, we sample according to the given distribution
else:
layer = sampler(pspec, qubit_labels, *samplerargs, rand_state=rand_state)
circuit.insert_layer_inplace(layer, 0)
circuit.done_editing()
return circuit
def create_direct_rb_circuit(pspec, clifford_compilations, length, qubit_labels=None, sampler='Qelimination',
samplerargs=None, addlocal=False, lsargs=None, randomizeout=True, cliffordtwirl=True,
conditionaltwirl=True, citerations=20, compilerargs=None, partitioned=False, seed=None):
"""
Generates a "direct randomized benchmarking" (DRB) circuit.
DRB is the protocol introduced in arXiv:1807.07975 (2018). The length of the "core" circuit is
given by `length` and may be any integer >= 0. An n-qubit DRB circuit consists of (1) a circuit
the prepares a uniformly random stabilizer state; (2) a length-l circuit (specified by `length`)
consisting of circuit layers sampled according to some user-specified distribution (specified by
`sampler`), (3) a circuit that maps the output of the preceeding circuit to a computational
basis state. See arXiv:1807.07975 (2018) for further details.
Parameters
----------
pspec : QubitProcessorSpec
The QubitProcessorSpec for the device that the circuit is being sampled for, which defines the
"native" gate-set and the connectivity of the device. The returned DRB circuit will be over
the gates in `pspec`, and will respect the connectivity encoded by `pspec`. Note that `pspec`
is always handed to the sampler, as the first argument of the sampler function (this is only
of importance when not using an in-built sampler for the "core" of the DRB circuit). Unless
`qubit_labels` is not None, the circuit is sampled over all the qubits in `pspec`.
clifford_compilation : CompilationRules
Rules for compiling the "native" gates of `pspec` into Clifford gates.
length : int
The "direct RB length" of the circuit, which is closely related to the circuit depth. It
must be an integer >= 0. Unless `addlocal` is True, it is the depth of the "core" random
circuit, sampled according to `sampler`, specified in step (2) above. If `addlocal` is True,
each layer in the "core" circuit sampled according to "sampler` is followed by a layer of
1-qubit gates, with sampling specified by `lsargs` (and the first layer is proceeded by a
layer of 1-qubit gates), and so the circuit of step (2) is length 2*`length` + 1.
qubit_labels : list, optional
If not None, a list of the qubits to sample the circuit for. This is a subset of
`pspec.qubit_labels`. If None, the circuit is sampled to act on all the qubits
in `pspec`.
sampler : str or function, optional
If a string, this should be one of: {'pairingQs', 'Qelimination', 'co2Qgates', 'local'}.
Except for 'local', this corresponds to sampling layers according to the sampling function
in rb.sampler named circuit_layer_by* (with * replaced by 'sampler'). For 'local', this
corresponds to sampling according to rb.sampler.circuit_layer_of_oneQgates [which is not
a valid form of sampling for n-qubit DRB, but is not explicitly forbidden in this function].
If `sampler` is a function, it should be a function that takes as the first argument a
QubitProcessorSpec, and returns a random circuit layer as a list of gate Label objects. Note that
the default 'Qelimination' is not necessarily the most useful in-built sampler, but it is
the only sampler that requires no parameters beyond the QubitProcessorSpec *and* works for arbitrary
connectivity devices. See the docstrings for each of these samplers for more information.
samplerargs : list, optional
A list of arguments that are handed to the sampler function, specified by `sampler`.
The first argument handed to the sampler is `pspec`, the second argument is `qubit_labels`,
and `samplerargs` lists the remaining arguments handed to the sampler. This is not
optional for some choices of `sampler`.
addlocal : bool, optional
Whether to follow each layer in the "core" circuit, sampled according to `sampler` with
a layer of 1-qubit gates.
lsargs : list, optional
Only used if addlocal is True. A list of optional arguments handed to the 1Q gate
layer sampler circuit_layer_by_oneQgate(). Specifies how to sample 1Q-gate layers.
randomizeout : bool, optional
If False, the ideal output of the circuit (the "success" or "survival" outcome) is the all-zeros
bit string. If True, the ideal output of the circuit is randomized to a uniformly random bit-string.
This setting is useful for, e.g., detecting leakage/loss/measurement-bias etc.
cliffordtwirl : bool, optional
Wether to begin the circuit with a sequence that generates a random stabilizer state. For
standard DRB this should be set to True. There are a variety of reasons why it is better
to have this set to True.
conditionaltwirl : bool, optional
DRB only requires that the initial/final sequences of step (1) and (3) create/measure
a uniformly random / particular stabilizer state, rather than implement a particular unitary.
step (1) and (3) can be achieved by implementing a uniformly random Clifford gate and the
unique inversion Clifford, respectively. This is implemented if `conditionaltwirl` is False.
However, steps (1) and (3) can be implemented much more efficiently than this: the sequences
of (1) and (3) only need to map a particular input state to a particular output state,
if `conditionaltwirl` is True this more efficient option is chosen -- this is option corresponds
to "standard" DRB. (the term "conditional" refers to the fact that in this case we essentially
implementing a particular Clifford conditional on a known input).
citerations : int, optional
Some of the stabilizer state / Clifford compilation algorithms in pyGSTi (including the default
algorithms) are randomized, and the lowest-cost circuit is chosen from all the circuit generated
in the iterations of the algorithm. This is the number of iterations used. The time required to
generate a DRB circuit is linear in `citerations`. Lower-depth / lower 2-qubit gate count
compilations of steps (1) and (3) are important in order to successfully implement DRB on as many
qubits as possible.
compilerargs : list, optional
A list of arguments that are handed to the compile_stabilier_state/measurement()functions (or the
compile_clifford() function if `conditionaltwirl `is False). This includes all the optional
arguments of these functions *after* the `iterations` option (set by `citerations`). For most
purposes the default options will be suitable (or at least near-optimal from the compilation methods
in-built into pyGSTi). See the docstrings of these functions for more information.
partitioned : bool, optional
If False, a single circuit is returned consisting of the full circuit. If True, three circuits
are returned in a list consisting of: (1) the stabilizer-prep circuit, (2) the core random circuit,
(3) the pre-measurement circuit. In that case the full circuit is obtained by appended (2) to (1)
and then (3) to (1).
seed : int, optional
A seed to initialize the random number generator used for creating random clifford
circuits.
Returns
-------
Circuit or list of Circuits
If partioned is False, a random DRB circuit sampled as specified. If partioned is True, a list of
three circuits consisting of (1) the stabilizer-prep circuit, (2) the core random circuit,
(3) the pre-measurement circuit. In that case the full circuit is obtained by appended (2) to (1)
and then (3) to (1) [except in the case of cliffordtwirl=False, when it is a list of two circuits].
Tuple
A length-n tuple of integers in [0,1], corresponding to the error-free outcome of the
circuit. Always all zeros if `randomizeout` is False. The ith element of the tuple
corresponds to the error-free outcome for the qubit labelled by: the ith element of
`qubit_labels`, if `qubit_labels` is not None; the ith element of `pspec.qubit_labels`, otherwise.
In both cases, the ith element of the tuple corresponds to the error-free outcome for the
qubit on the ith wire of the output circuit.
"""
if samplerargs is None:
samplerargs = []
if compilerargs is None:
compilerargs = []
if lsargs is None:
lsargs = []
if qubit_labels is not None: n = len(qubit_labels)
else: n = pspec.num_qubits
rand_state = _np.random.RandomState(seed) # Ok if seed is None
# Sample a random circuit of "native gates".
circuit = create_random_circuit(pspec=pspec, length=length, qubit_labels=qubit_labels, sampler=sampler,
samplerargs=samplerargs, addlocal=addlocal, lsargs=lsargs, rand_state=rand_state)
# find the symplectic matrix / phase vector this "native gates" circuit implements.
s_rc, p_rc = _symp.symplectic_rep_of_clifford_circuit(circuit, pspec=pspec)
# If we are clifford twirling, we do an initial random circuit that is either a uniformly random
# cliffor or creates a uniformly random stabilizer state from the standard input.
if cliffordtwirl:
# Sample a uniformly random Clifford.
s_initial, p_initial = _symp.random_clifford(n, rand_state=rand_state)
# Find the composite action of this uniformly random clifford and the random circuit.
s_composite, p_composite = _symp.compose_cliffords(s_initial, p_initial, s_rc, p_rc)
# If conditionaltwirl we do a stabilizer prep (a conditional Clifford).
if conditionaltwirl:
initial_circuit = _cmpl.compile_stabilizer_state(s_initial, p_initial, pspec,
clifford_compilations.get('absolute', None),
clifford_compilations.get('paulieq', None),
qubit_labels, citerations,
*compilerargs, rand_state=rand_state)
# If not conditionaltwirl, we do a full random Clifford.
else:
initial_circuit = _cmpl.compile_clifford(s_initial, p_initial, pspec,
clifford_compilations.get('absolute', None),
clifford_compilations.get('paulieq', None),
qubit_labels, citerations,
*compilerargs, rand_state=rand_state)
# If we are not Clifford twirling, we just copy the effect of the random circuit as the effect
# of the "composite" prep + random circuit (as here the prep circuit is the null circuit).
else:
s_composite = _copy.deepcopy(s_rc)
p_composite = _copy.deepcopy(p_rc)
if conditionaltwirl:
# If we want to randomize the expected output then randomize the p vector, otherwise
# it is left as p. Note that, unlike with compile_clifford, we don't invert (s,p)
# before handing it to the stabilizer measurement function.
if randomizeout: p_for_measurement = _symp.random_phase_vector(s_composite, n, rand_state=rand_state)
else: p_for_measurement = p_composite
inversion_circuit = _cmpl.compile_stabilizer_measurement(s_composite, p_for_measurement, pspec,
clifford_compilations.get('absolute', None),
clifford_compilations.get('paulieq', None),
qubit_labels,
citerations, *compilerargs, rand_state=rand_state)
else:
# Find the Clifford that inverts the circuit so far. We
s_inverse, p_inverse = _symp.inverse_clifford(s_composite, p_composite)
# If we want to randomize the expected output then randomize the p_inverse vector, otherwise
# do not.
if randomizeout: p_for_inversion = _symp.random_phase_vector(s_inverse, n, rand_state=rand_state)
else: p_for_inversion = p_inverse
# Compile the Clifford.
inversion_circuit = _cmpl.compile_clifford(s_inverse, p_for_inversion, pspec,
clifford_compilations.get('absolute', None),
clifford_compilations.get('paulieq', None),
qubit_labels, citerations, *compilerargs, rand_state=rand_state)
if cliffordtwirl:
full_circuit = initial_circuit.copy(editable=True)
full_circuit.append_circuit_inplace(circuit)
full_circuit.append_circuit_inplace(inversion_circuit)
else:
full_circuit = circuit.copy(editable=True)
full_circuit.append_circuit_inplace(inversion_circuit)
full_circuit.done_editing()
# Find the expected outcome of the circuit.
s_out, p_out = _symp.symplectic_rep_of_clifford_circuit(full_circuit, pspec=pspec)
if conditionaltwirl: # s_out is not always the identity with a conditional twirl, only conditional on prep/measure.
assert(_np.array_equal(s_out[:n, n:], _np.zeros((n, n), _np.int64))), "Compiler has failed!"
else: assert(_np.array_equal(s_out, _np.identity(2 * n, _np.int64))), "Compiler has failed!"
# Find the ideal output of the circuit.
s_inputstate, p_inputstate = _symp.prep_stabilizer_state(n, zvals=None)
s_outstate, p_outstate = _symp.apply_clifford_to_stabilizer_state(s_out, p_out, s_inputstate, p_inputstate)
idealout = []
for q in range(0, n):
measurement_out = _symp.pauli_z_measurement(s_outstate, p_outstate, q)
bit = measurement_out[1]
assert(bit == 0 or bit == 1), "Ideal output is not a computational basis state!"
if not randomizeout:
assert(bit == 0), "Ideal output is not the all 0s computational basis state!"