Skip to content

1. Circuits

graphiq.circuit.circuit_dag.CircuitDAG

Bases: CircuitBase

Directed Acyclic Graph (DAG) based circuit implementation

Each node of the graph contains an Operation (it is an input, output, or general Operation). The Operations in the topological order of the DAG.

Each connecting edge of the graph corresponds to a qubit/qudit or classical bit of the circuit

Source code in graphiq/circuit/circuit_dag.py
  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
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
class CircuitDAG(CircuitBase):
    """
    Directed Acyclic Graph (DAG) based circuit implementation

    Each node of the graph contains an Operation (it is an input, output, or general Operation).
    The Operations in the topological order of the DAG.

    Each connecting edge of the graph corresponds to a qubit/qudit or classical bit of the circuit
    """

    def __init__(
        self,
        n_emitter=0,
        n_photon=0,
        n_classical=0,
        openqasm_imports=None,
        openqasm_defs=None,
    ):
        """
        Construct a DAG circuit with n_emitter one-qubit emitter quantum registers, n_photon one-qubit photon
        quantum registers, and n_classical one-cbit classical registers.

        :param n_emitter: the number of emitter qubits/qudits in the system
        :type n_emitter: int
        :param n_photon: the number of photon qubits/qudits in the system
        :type n_photon: int
        :param n_classical: the number of classical bits in the system
        :type n_classical: int
        :return: nothing
        :rtype: None
        """
        self.dag = nx.MultiDiGraph()
        super().__init__(openqasm_imports=openqasm_imports, openqasm_defs=openqasm_defs)

        self._node_id = 0
        self.edge_dict = {}
        self.node_dict = {}
        self._register_depth = {"e": [], "p": [], "c": []}

        # map the key to the tuple register
        reg_map = {
            "e": n_emitter,
            "p": n_photon,
            "c": n_classical,
        }

        for key in reg_map:
            for i in range(reg_map[key]):
                self._add_reg_if_absent(register=i, reg_type=key)

    def add(self, operation: ops.OperationBase):
        """
        Add an operation to the end of the circuit (i.e. to be applied after the pre-existing circuit operations)

        :param operation: Operation (gate and register) to add to the graph
        :type operation: OperationBase type (or a subclass of it)
        :raises UserWarning: if no openqasm definitions exists for operation
        :return: nothing
        :rtype: None
        """
        self._openqasm_update(operation)

        # update registers (if the new operation is adding registers to the circuit)
        # Check for classical register
        for i in range(len(operation.c_registers)):
            self._add_reg_if_absent(
                register=operation.c_registers[i],
                reg_type="c",
            )

        # Check for qubit register
        register, reg_type = zip(
            *sorted(zip(operation.q_registers, operation.q_registers_type))
        )
        for i in range(len(register)):
            self._add_reg_if_absent(
                register=register[i],
                reg_type=reg_type[i],
            )

        self._add(operation)

    def insert_at(self, operation: ops.OperationBase, edges):
        """
        Insert an operation among specified edges

        :param operation: Operation (gate and register) to add to the graph
        :type operation: OperationBase type (or a subclass of it)
        :param edges: a list of edges relevant for this operation
        :type edges: list[tuple]
        :raises UserWarning: if no openqasm definitions exists for operation
        :raises AssertionError: if the number of edges disagrees with the number of q_registers
        :return: nothing
        :rtype: None
        """
        self._openqasm_update(operation)

        # update registers (if the new operation is adding registers to the circuit)
        # Check for classical register
        for i in range(len(operation.c_registers)):
            self._add_reg_if_absent(
                register=operation.c_registers[i],
                reg_type="c",
            )

        # Check for qubit register
        register, reg_type = zip(
            *sorted(zip(operation.q_registers, operation.q_registers_type))
        )
        for i in range(len(register)):
            self._add_reg_if_absent(
                register=register[i],
                reg_type=reg_type[i],
            )

        assert len(edges) == len(operation.q_registers)
        # note that we implicitly assume there is only one classical register (bit) so that
        # we only count the quantum registers here. (Also only including quantum registers in edges)
        self._openqasm_update(operation)
        self._insert_at(operation, edges)

    def replace_op(self, node, new_operation: ops.OperationBase):
        """
        Replaces an operation by a new one with the same set of registers it acts on.

        :param node: the node where the new operation is placed
        :type node: int
        :param new_operation: the new operation
        :type new_operation: OperationBase or its subclass
        :raises AssertionError: if new_operation acts on different registers from the operation in the node
        :return: nothing
        :rtype: None
        """

        old_operation = self.dag.nodes[node]["op"]
        assert old_operation.q_registers == new_operation.q_registers
        assert old_operation.q_registers_type == new_operation.q_registers_type
        assert old_operation.c_registers == new_operation.c_registers

        # remove entries related to old_operation
        for label in old_operation.labels:
            self._node_dict_remove(label, node)
        self._node_dict_remove(type(old_operation).__name__, node)
        self._node_dict_remove(old_operation.parse_q_reg_types(), node)

        # add entries related to new_operation
        for label in new_operation.labels:
            self._node_dict_append(label, node)
        self._node_dict_append(type(new_operation).__name__, node)
        self._node_dict_append(new_operation.parse_q_reg_types(), node)

        # replace the operation in the node
        self._openqasm_update(new_operation)
        self.dag.nodes[node]["op"] = new_operation

    def find_incompatible_edges(self, first_edge):
        """
        Find all incompatible edges of first_edge for which one cannot add any two-qubit operation

        :param first_edge: the edge under consideration
        :type first_edge: tuple
        :return: a set of incompatible edges
        :rtype: set(tuple)
        """

        # all nodes that have a path to the node first_edge[0]
        ancestors = nx.ancestors(self.dag, first_edge[0])

        # all nodes that are reachable from the node first_edge[1]
        descendants = nx.descendants(self.dag, first_edge[1])

        # all incoming edges of the node first_edge[0]
        ancestor_edges = list(self.dag.in_edges(first_edge[0], keys=True))

        for anc in ancestors:
            ancestor_edges.extend(self.dag.edges(anc, keys=True))

        # all outgoing edges of the node first_edge[1]
        descendant_edges = list(self.dag.out_edges(first_edge[1], keys=True))

        for des in descendants:
            descendant_edges.extend(self.dag.edges(des, keys=True))

        return set.union(set([first_edge]), set(ancestor_edges), set(descendant_edges))

    def _add_node(self, node_id, operation: ops.OperationBase):
        """
        Helper function for adding a node to the DAG representation

        :param node_id: the node to be added
        :type node_id: int
        :param operation: the operation for the node
        :type operation: OperationBase or subclass
        :return: nothing
        :rtype: None
        """
        self.dag.add_node(node_id, op=operation)

        for attribute in operation.labels:
            self._node_dict_append(attribute, node_id)
        self._node_dict_append(type(operation).__name__, node_id)
        self._node_dict_append(operation.parse_q_reg_types(), node_id)

    def _remove_node(self, node):
        """
        Helper function for removing a node in the DAG representation

        :param node: the node to be removed
        :type node: int
        :return: nothing
        :rtype: None
        """
        in_edges = list(self.dag.in_edges(node, keys=True))
        out_edges = list(self.dag.out_edges(node, keys=True))

        for in_edge in in_edges:
            for out_edge in out_edges:
                if in_edge[2] == out_edge[2]:  # i.e. if the keys are the same
                    reg = self.dag.edges[in_edge]["reg"]
                    reg_type = self.dag.edges[in_edge]["reg_type"]
                    label = out_edge[2]
                    self._add_edge(
                        in_edge[0], out_edge[1], label, reg_type=reg_type, reg=reg
                    )

            self._remove_edge(in_edge)

        for out_edge in out_edges:
            self._remove_edge(out_edge)

        # remove all entries relevant for this node in node_dict
        operation = self.dag.nodes[node]["op"]
        for attribute in operation.labels:
            self._node_dict_remove(attribute, node)

        self._node_dict_remove(type(operation).__name__, node)
        self._node_dict_remove(operation.parse_q_reg_types(), node)
        self.dag.remove_node(node)

    def _add_edge(self, in_node, out_node, label, reg_type, reg):
        """
        Helper function for adding an edge in the DAG representation

        :param in_node: the incoming node
        :type in_node: int or str
        :param out_node: the outgoing node
        :type out_node: int or str
        :param label: the key for the edge
        :type label: int or str
        :param reg_type: the register type of the edge
        :type reg_type: str
        :param reg: the register of the edge
        :type reg: int or str
        :return: nothing
        :rtype: None
        """
        self.dag.add_edge(
            in_node,
            out_node,
            key=label,
            reg_type=reg_type,
            reg=reg,
        )
        self._edge_dict_append(reg_type, (in_node, out_node, label))

    def _remove_edge(self, edge_to_remove):
        """
        Helper function for removing an edge in the DAG representation

        :param edge_to_remove: the edge to be removed
        :type edge_to_remove: tuple
        :return: nothing
        :rtype: None
        """

        reg_type = self.dag.edges[edge_to_remove]["reg_type"]
        self._edge_dict_remove(reg_type, edge_to_remove)
        self.dag.remove_edges_from([edge_to_remove])

    def get_node_by_labels(self, labels):
        """
        Get all nodes that satisfy all labels

        :param labels: descriptions of a set of nodes
        :type labels: list[str]
        :return: a list of node ids for nodes that satisfy all labels
        :rtype: list[int]
        """
        remaining_nodes = set(self.dag.nodes)
        for label in labels:
            remaining_nodes = remaining_nodes.intersection(set(self.node_dict[label]))
        return list(remaining_nodes)

    def get_node_exclude_labels(self, labels):
        """
        Get all nodes that do not satisfy any label in labels

        :param labels: descriptions of a set of nodes
        :type labels: list[str]
        :return: a list of node ids for nodes that do not satisfy any label in labels
        :rtype: list[int]
        """
        all_nodes = set(self.dag.nodes)
        exclusion_nodes = set()
        for label in labels:
            exclusion_nodes = exclusion_nodes.union(set(self.node_dict[label]))
        return list(all_nodes - exclusion_nodes)

    def remove_op(self, node):
        """
        Remove an operation from the circuit

        :param node: the node to be removed
        :type node: int
        :return: nothing
        :rtype: None
        """
        self._remove_node(node)

    def validate(self):
        """
        Assert that the circuit is valid (is a DAG, all nodes
        without input edges are input nodes, all nodes without output edges
        are output nodes)

        :raises RuntimeError: if the circuit is not valid
        :return: this function returns nothing
        :rtype: None
        """
        assert nx.is_directed_acyclic_graph(self.dag)  # check DAG is correct

        # check all "source" nodes to the DAG are Input operations
        input_nodes = [
            node for node, in_degree in self.dag.in_degree() if in_degree == 0
        ]
        # assert set(input_nodes)  == set(self.node_dict['Input'])
        for input_node in input_nodes:
            if not isinstance(self.dag.nodes[input_node]["op"], ops.Input):
                raise RuntimeError(
                    f"Source node {input_node} in the DAG is not an Input operation"
                )

        # check all "sink" nodes to the DAG are Output operations
        output_nodes = [
            node for node, out_degree in self.dag.out_degree() if out_degree == 0
        ]
        # assert set(output_nodes) == set(self.node_dict['Output'])
        for output_node in output_nodes:
            if not isinstance(self.dag.nodes[output_node]["op"], ops.Output):
                raise RuntimeError(
                    f"Sink node {output_node} in the DAG is not an Output operation"
                )

    def sequence(self, unwrapped=False):
        """
        Return the sequence of operations composing this circuit

        :param unwrapped: If True, we "unwrap" the operation objects such that the returned sequence has only
                          non-composed gates (i.e. wrapper gates which include multiple non-composed gates are
                          broken down into their constituent parts). If False, operations are returned as defined
                          in the circuit (i.e. wrapper gates are returned as wrappers)
        :type unwrapped: bool
        :return: the operations which compose this circuit, in the order they should be applied
        :rtype: list or iterator (of OperationBase subclass objects)
        """
        op_list = [self.dag.nodes[node]["op"] for node in nx.topological_sort(self.dag)]
        if not unwrapped:
            return op_list

        return functools.reduce(lambda x, y: x + y.unwrap(), op_list, [])

    @property
    def depth(self):
        """
        Returns the circuit depth (NOT including input and output nodes)

        :return: circuit depth
        :rtype: int
        """
        # assert len(list(nx.topological_generations(self.dag)))-2 == nx.dag_longest_path_length(self.dag)-1
        return nx.dag_longest_path_length(self.dag) - 1

    @property
    def register_depth(self):
        """
        Returns the copy of register depth for each register

        :return: register depth
        :rtype: dict
        """
        return self.calculate_all_reg_depth()

    def _node_dict_append(self, key, value):
        """
        Helper function to add an entry to the node_dict

        :param key: key for the node_dict
        :type key: str
        :param value: value to be appended to the list corresponding to the key
        :type value: int or str
        :return: nothing
        """
        if key not in self.node_dict.keys():
            self.node_dict[key] = [value]
        else:
            self.node_dict[key].append(value)

    def _node_dict_remove(self, key, value):
        """
        Helper function to remove an entry in the list corresponding to the key in node_dict

        :param key: key for the node_dict
        :type key: str
        :param value: value to be removed from the list corresponding to the key
        :type value: int or str
        :return: nothing
        """
        if key in self.node_dict.keys():
            try:
                self.node_dict[key].remove(value)
            except ValueError:
                pass

    def _edge_dict_append(self, key, value):
        """
        Helper function to add an entry to the edge_dict

        :param key: key for edge_dict
        :type key: str
        :param value: the edge tuple that contains in_node id, out_node id, and the label for the edge (register)
        :type value: tuple(int, int, str)
        :return: nothing
        :rtype: None
        """
        if key not in self.edge_dict.keys():
            self.edge_dict[key] = [value]
        else:
            self.edge_dict[key].append(value)

    def _edge_dict_remove(self, key, value):
        """
        Helper function to remove an entry in the list corresponding to the key in edge_dict

        :param key: key for edge_dict
        :type key: str
        :param value: the edge tuple that contains in_node id, out_node id, and the label for the edge (register)
        :type value: tuple(int, int, str)
        :return: nothing
        :rtype: None
        """
        if key in self.edge_dict.keys():
            try:
                self.edge_dict[key].remove(value)
            except ValueError:
                pass

    def draw_dag(self, show=True, fig=None, ax=None):
        """
        Draw the circuit as a DAG

        :param show: if True, the DAG is displayed (shown). If False, the DAG is drawn but not displayed
        :type show: bool
        :param fig: fig on which to draw the DAG (optional)
        :type fig: None or matplotlib.pyplot.figure
        :param ax: ax on which to draw the DAG (optional)
        :type ax: None or matplotlib.pyplot.axes
        :return: fig, ax on which the DAG was drawn
        :rtype: matplotlib.pyplot.figure, matplotlib.pyplot.axes
        """
        # TODO: fix this such that we can see double edges properly!
        pos = dag_topology_pos(self.dag, method="topology")

        if ax is None or fig is None:
            fig, ax = plt.subplots()
        nx.draw(self.dag, pos=pos, ax=ax, with_labels=True)
        if show:
            plt.show()
        return fig, ax

    @classmethod
    def from_openqasm(cls, qasm_script):
        """
        Create a circuit based on an (assumed to be valid) openQASM script

        :param qasm_script: the openqasm script from which a circuit should be built
        :type qasm_script: str
        :return: a circuit object
        :rtype: CircuitBase
        """

        for elem in string.whitespace:
            if elem != " ":  # keep spaces, but no other whitespace
                qasm_script = qasm_script.replace(elem, "")

        # script must start with OPENQASM 2.0; or another openQASM number
        script_list = re.split(";", qasm_script, 1)
        header = script_list[0]
        for elem in string.whitespace:
            header = header.replace(elem, "")
        assert header == "OPENQASM2.0"
        qasm_script = script_list[1]  # get rid of header now that we've checked it

        # get rid of any gate declarations--we don't actually need them for the parsing
        search_match = re.search(r"gate[^}]*{[^}]*}", qasm_script)
        while search_match is not None:
            qasm_script = qasm_script.replace(search_match.group(0), "")
            search_match = re.search(r"gate[^}]*{[^}]*}", qasm_script)

        # Next, we can parse each sentence
        qasm_commands = re.split(";", qasm_script)

        n_photon = len(
            [
                command
                for command in qasm_commands
                if "qregp" in command.replace(" ", "")
            ]
        )
        n_emitter = len(
            [
                command
                for command in qasm_commands
                if "qrege" in command.replace(" ", "")
            ]
        )
        n_classical = len([command for command in qasm_commands if "creg" in command])

        circuit = CircuitDAG(
            n_photon=n_photon, n_emitter=n_emitter, n_classical=n_classical
        )
        i = 0
        while i in range(len(qasm_commands)):
            command = qasm_commands[i]
            if (
                ("qreg" in command)
                or ("creg" in command)
                or ("barrier" in command)
                or (command == "")
            ):
                i += 1
                continue

            if "measure" in command and "->" in command:
                q_str = re.search(r"(e|p)(\d)+\[0\]", command).group(0)
                c_str = re.search(r"c(\d)+\[0\]", command).group(0)

                q_type = q_str[0]
                q_reg = int(re.split(r"\[", q_str[1:])[0])
                c_reg = int(re.split(r"\[", c_str[1:])[0])

                def _parse_if(command):
                    c_str = re.search(r"c(\d)+==1", command.replace(" ", "")).group(0)
                    c_reg = int(re.split("==", c_str)[0][1:])
                    gate = re.search(
                        r"\)[a-z](p|e)(\d)+\[", command.replace(" ", "")
                    ).group(0)[1]
                    reg_str = re.search(r"(p|e)(\d)+\[", command).group(0)
                    reg = int(reg_str[1:-1])
                    reg_type = reg_str[0]

                    return gate, reg, reg_type, c_reg

                if i + 3 < len(qasm_commands):  # could be a 4 line operation
                    if "if" in qasm_commands[i + 1] and "reset" in qasm_commands[i + 3]:
                        gate, target_reg, target_type, c_reg = _parse_if(
                            qasm_commands[i + 1]
                        )
                        reset_str = re.split(r"\s", qasm_commands[i + 3].strip())[1]
                        reset_type = reset_str[0]
                        reset_reg = int(reset_str[1:-3])
                        assert reset_type == q_type, (
                            f"Reset should be on control qubit, reset type is:{reset_type}, "
                            f"control qubit type was: {q_type}"
                        )
                        assert reset_reg == q_reg, (
                            f"Reset should be on control qubit. Reset reg is: {reset_reg}, "
                            f"control reg is: {q_reg}"
                        )

                        circuit.add(
                            ops.name_to_class_map(f"classical reset {gate}")(
                                control=q_reg,
                                control_type=q_type,
                                target=target_reg,
                                target_type=target_type,
                                c_register=c_reg,
                            )
                        )
                        i += 4
                        continue

                if i + 1 < len(qasm_commands):
                    if "if" in qasm_commands[i + 1]:
                        gate, target_reg, target_type, c_reg = _parse_if(
                            qasm_commands[i + 1]
                        )
                        circuit.add(
                            ops.name_to_class_map(f"classical {gate}")(
                                control=q_reg,
                                control_type=q_type,
                                target=target_reg,
                                target_type=target_type,
                                c_register=c_reg,
                            )
                        )
                        i += 2
                        continue

                circuit.add(
                    ops.MeasurementZ(register=q_reg, reg_type=q_type, c_register=c_reg)
                )
                i += 1
                continue

            # Parse single-qubit operations
            if (
                command.count("[0]") == 1
            ):  # single qubit operation, from current script generation method
                command_breakdown = command.split()
                name = command_breakdown[0]
                reg_type = command_breakdown[1][0]
                reg = int(command_breakdown[1][1:-3])  # we must parse out [0] so -3
                gate_class = ops.name_to_class_map(name)
                if gate_class is not None:
                    circuit.add(gate_class(register=reg, reg_type=reg_type))
                else:
                    circuit_list = [ops.name_to_class_map(letter) for letter in name]
                    assert None not in circuit_list, (
                        f"Gate not recognized, parsing invalid/"
                        f"{name} parsed to {circuit_list}"
                    )
                    circuit.add(
                        ops.OneQubitGateWrapper(
                            circuit_list, register=reg, reg_type=reg_type
                        )
                    )
            elif command.count("[0]") == 2:  # two-qubit gate
                command_breakdown = command.split()
                name = command_breakdown[0]
                control_type = command_breakdown[1][0]
                control_reg = int(
                    command_breakdown[1][1:-4]
                )  # we must parse out [0], so -4
                target_type = command_breakdown[2][0]
                target_reg = int(
                    command_breakdown[2][1:-3]
                )  # we must parse out [0] so -3
                gate_class = ops.name_to_class_map(name)
                assert (
                    gate_class is not None
                ), "gate name not recognized, parsing failed"
                circuit.add(
                    gate_class(
                        control=control_reg,
                        control_type=control_type,
                        target=target_reg,
                        target_type=target_type,
                    )
                )
            else:
                raise ValueError(f"command not recognized, cannot be parsed")
            i += 1

        return circuit

    def _add_register(self, reg_type: str, size=1):
        """
        Helper function for adding a quantum/classical register of a certain size

        :param reg_type: 'e' to add an emitter qubit register, 'p' to add a photonic qubit register,
                         'c' to add a classical register
        :type reg_type: str
        :return: function returns nothing
        :rtype: None
        """
        if size != 1:
            raise ValueError(
                f"_add_register for this circuit class must only add registers of size 1"
            )
        self._add_reg_if_absent(
            register=len(self._registers[reg_type]), reg_type=reg_type
        )

    def _add_reg_if_absent(self, register, reg_type):
        """
        Adds a register to our list of registers and to our graph, if said registers are absent

        :param register: Index of the new register
        :type register: int
        :param reg_type: str indicates register type. Can be "e", "p", or "c"
        :type reg_type: str
        :return: function returns nothing
        :rtype: None
        """

        if register == len(self._registers[reg_type]):
            self._registers[reg_type].append(1)
            self._register_depth[reg_type].append(0)
        elif register > len(self._registers[reg_type]):
            raise ValueError(
                f"Register numbering must be continuous. {reg_type} register {register} cannot be added. "
                f"Next register that can be added is {len(self._registers[reg_type])}"
            )

        if f"{reg_type}{register}_in" not in self.dag.nodes:
            self.dag.add_node(
                f"{reg_type}{register}_in",
                op=ops.Input(register=register, reg_type=reg_type),
                reg=register,
            )
            self._node_dict_append("Input", f"{reg_type}{register}_in")
            self.dag.add_node(
                f"{reg_type}{register}_out",
                op=ops.Output(register=register, reg_type=reg_type),
                reg=register,
            )
            self._node_dict_append("Output", f"{reg_type}{register}_out")
            self.dag.add_edge(
                f"{reg_type}{register}_in",
                f"{reg_type}{register}_out",
                key=f"{reg_type}{register}",
                reg=register,
                reg_type=reg_type,
            )
            self._edge_dict_append(
                reg_type,
                tuple(self.dag.in_edges(nbunch=f"{reg_type}{register}_out", keys=True))[
                    0
                ],
            )

    def _add(self, operation: ops.OperationBase):
        """
        Add an operation to the circuit
        This function assumes that all registers used by operation are already built

        :param operation: Operation (gate and register) to add to the graph
        :type operation: OperationBase (or a subclass thereof)
        :return: nothing
        :rtype: None
        """
        new_id = self._unique_node_id()

        self._add_node(new_id, operation)

        # get all edges that will need to be removed (i.e. the edges on which the Operation is being added)
        relevant_outputs = [
            f"{operation.q_registers_type[i]}{operation.q_registers[i]}_out"
            for i in range(len(operation.q_registers))
        ] + [f"c{c}_out" for c in operation.c_registers]

        for output in relevant_outputs:
            edges_to_remove = list(
                self.dag.in_edges(nbunch=output, keys=True, data=False)
            )

            for edge in edges_to_remove:
                # Add edge from preceding node to the new operation node
                reg_type = self.dag.edges[edge]["reg_type"]
                reg = self.dag.edges[edge]["reg"]

                self._add_edge(
                    edge[0],
                    new_id,
                    edge[2],
                    reg=reg,
                    reg_type=reg_type,
                )
                self._add_edge(
                    new_id,
                    edge[1],
                    edge[2],
                    reg=reg,
                    reg_type=reg_type,
                )

                self._remove_edge(edge)  # remove the unnecessary edges

    def _insert_at(self, operation: ops.OperationBase, reg_edges):
        """
        Add an operation to the circuit at a specified position
        This function assumes that all registers used by operation are already built

        :param operation: Operation (gate and register) to add to the graph
        :type operation: OperationBase (or a subclass thereof)
        :param reg_edges: a list of edges relevant for the operation
        :type reg_edges: list[tuple]
        :return: nothing
        :rtype: None
        """

        self._openqasm_update(operation)
        new_id = self._unique_node_id()

        self._add_node(new_id, operation)

        for reg_edge in reg_edges:
            reg = self.dag.edges[reg_edge]["reg"]
            reg_type = self.dag.edges[reg_edge]["reg_type"]
            label = reg_edge[2]

            self._add_edge(
                reg_edge[0],
                new_id,
                label,
                reg_type=reg_type,
                reg=reg,
            )
            self._add_edge(
                new_id,
                reg_edge[1],
                label,
                reg_type=reg_type,
                reg=reg,
            )
            self._remove_edge(reg_edge)  # remove the edge

    def _unique_node_id(self):
        """
        Internally used to provide a unique ID to each node. Note that this assumes a single thread assigning IDs

        :return: a new, unique node ID
        :rtype: int
        """
        self._node_id += 1
        return self._node_id

    def compare(self, circuit, method="direct"):
        """
        Comparing two circuits by using GED or direct loop method

        :param circuit: circuit that to be compared
        :type circuit: CircuitDAG
        :param method: Determine which comparison function to use
        :type method: str
        :return: whether two circuits are the same
        :rtype: bool
        """
        return compare_circuits(self, circuit, method=method)

    def unwrap_nodes(self):
        """
        Unwrap the nodes with more than 1 ops in it

        :return: nothing
        :rtype: None
        """

        if "OneQubitGateWrapper" in self.node_dict:
            wrapper_list = self.node_dict["OneQubitGateWrapper"].copy()
            for node in wrapper_list:
                op_list = self.dag.nodes[node]["op"].unwrap()
                for op in op_list:
                    in_edge = list(self.dag.in_edges(node, keys=True))
                    self.insert_at(op, in_edge)
                self.remove_op(node)

    def remove_identity(self):
        """
        Remove all identity gates

        :return: nothing
        :rtype: None
        """

        if "Identity" in self.node_dict:
            identity_list = self.node_dict["Identity"].copy()
            for node in identity_list:
                self.remove_op(node)

    def _max_depth(self, root_node):
        """
        Helper function that calculate max depth of a node in the circuit DAG.
        Using recursion, the function will go to previous nodes connected by in_edges, until reach the Input node.

        :param root_node: root node that is used as starting point
        :type root_node: node
        :return: the max depth of the node
        :rtype: int
        """
        # Check if the node is the Input node
        # If Input node then return -1

        if root_node in self.node_dict["Input"]:
            return -1

        in_edges = self.dag.in_edges(root_node)
        connected_nodes = [edge[0] for edge in in_edges]
        depth = []

        for node in connected_nodes:
            depth.append(self._max_depth(node))
        return max(depth) + 1

    def sorted_reg_depth_index(self, reg_type: str):
        """
        Return the array of register indexes with depth from smallest to largest
        Useful to find register index with nth smallest depth

        :param reg_type: str indicates register type. Can be "e", "p", or "c"
        :type reg_type: str
        :return: the array of register indexes with depth from smallest to largest
        :rtype: numpy.array
        """
        return np.argsort(self.calculate_reg_depth(reg_type=reg_type))

    def calculate_reg_depth(self, reg_type: str):
        """
        Calculate the register depth of the register type
        Then return the register depth array

        :param reg_type: str indicates register type. Can be "e", "p", or "c"
        :type reg_type: str
        :return: the array of register depth of all register in the register type
        :rtype: numpy.array
        """
        if reg_type not in self._register_depth:
            raise ValueError(f"register type {reg_type} is not in this circuit")

        for i in range(len(self._register_depth[reg_type])):
            output_node = f"{reg_type}{i}_out"
            self._register_depth[reg_type][i] = self._max_depth(output_node)
        return self._register_depth[reg_type]

    def min_reg_depth_index(self, reg_type: str):
        """
        Calculate the register depth of the register type
        Then return the index of the register with minimum depth

        :param reg_type: str indicates register type. Can be "e", "p", or "c"
        :type reg_type: str
        :return: the index of register with min depth within register type
        :rtype: int
        """
        return np.argmin(self.calculate_reg_depth(reg_type=reg_type))

    def calculate_all_reg_depth(self):
        """
        Calculate all registers depth in the circuit
        Then return the register depth dict

        :return: register depth dict that has been calculated
        :rtype: dict
        """
        for reg_type in self._register_depth:
            self.calculate_reg_depth(reg_type=reg_type)
        return self._register_depth.copy()

    def reg_gate_history(self, reg, reg_type="e"):
        """
        Finds all the gates that the specified register goes through in the given circuit. A list of operations (gates)
        and a list of nodes in the chronological order is returned.

        :param reg: the register
        :type reg: int
        :param reg_type: the type of the register
        :type reg_type: str
        :return: a tuple of lists, the first one is the list of operation and the second one is the list of nodes in
        the DAG
        :rtype: tuple(list, list)
        """
        next_node = f"{reg_type}{reg}_in"
        ordered_nodes = [next_node]
        while next_node != f"{reg_type}{reg}_out":
            next_node = [
                edge[1]
                for edge in self.dag.out_edges(next_node, data=True)
                if edge[2]["reg"] == reg and edge[2]["reg_type"] == reg_type
            ][0]
            ordered_nodes.append(next_node)
        ops_list = [self.dag.nodes[nod]["op"] for nod in ordered_nodes]
        return ops_list, ordered_nodes

    def to_json(self):
        """
        Function to convert circuit object to json data format.

        :return: circuit json object
        :rtype: dict
        """
        data = {
            "n_photons": self.n_photons,
            "n_emitters": self.n_emitters,
            "n_classical": self.n_classical,
            "ops": [],
        }

        for op in self.sequence():
            if isinstance(op, ops.InputOutputOperationBase):
                continue
            elif type(op) == ops.OneQubitGateWrapper:
                op_list = []
                for g in op.operations:
                    name = ops.class_to_name_mapping(g)
                    if name:
                        op_list.append(name)
                op_data = {
                    "type": "one qubit gate wrapper",
                    "op_list": op_list,
                    "q_registers_type": op.q_registers_type,
                    "q_registers": op.q_registers,
                    "c_registers": op.c_registers,
                }
            else:
                op_data = {
                    "type": ops.class_to_name_mapping(type(op)),
                    "q_registers_type": op.q_registers_type,
                    "q_registers": op.q_registers,
                    "c_registers": op.c_registers,
                    # ...,
                }
            data["ops"].append(op_data)

        return data

    @classmethod
    def from_json(cls, data_dict):
        """
        Function to load from json object to circuit object

        :param data_dict: circuit json dict
        :type data_dict: dict
        :return: nothing
        :rtype: None
        """
        circuit = CircuitDAG(
            n_photon=data_dict["n_photons"],
            n_emitter=data_dict["n_emitters"],
            n_classical=data_dict["n_classical"],
        )

        for op in data_dict["ops"]:
            if op["type"] == "one qubit gate wrapper":
                op_list = [ops.name_to_class_map(g) for g in op["op_list"]]
                gate = ops.OneQubitGateWrapper(
                    op_list,
                    register=op["q_registers"][0],
                    reg_type=op["q_registers_type"][0],
                )
            else:
                gate = ops.name_to_class_map(op["type"])
                gate = gate()
                gate.q_registers = op["q_registers"]
                gate.q_registers_type = op["q_registers_type"]
                gate.c_registers = op["c_registers"]

            circuit.add(gate)

        return circuit

    @staticmethod
    def edge_from_reg(t_edges, t_register):
        """
        Helper function to return correct edge from edges that map to the correct register.

        :param t_edges: input edge
        :type t_edges: edge
        :param t_register: register
        :type t_register: str
        :return: correct edge
        :rtype: edge
        """
        for e in t_edges:
            if e[-1] == t_register:
                return e

    def group_one_qubit_gates(self):
        """
        Put consecutive one-qubit gates into a OneQubitGateWrapper

        :return: nothing
        :rtype: None
        """
        for node in self.node_dict["Output"]:
            # traverse the circuit DAG in the reversed order
            reg_type = self.dag.nodes[node]["op"].reg_type
            register = self.dag.nodes[node]["op"].register
            gate_list = []

            in_edges = self.dag.in_edges(nbunch=node, keys=True)
            next_node = self.edge_from_reg(in_edges, f"{reg_type}{register}")[0]

            while next_node not in self.node_dict["Input"]:
                node = next_node
                in_edges = self.dag.in_edges(nbunch=node, keys=True)
                edge = self.edge_from_reg(in_edges, f"{reg_type}{register}")
                next_node = edge[0]

                if node in self.node_dict["one-qubit"]:
                    node_info = self.dag.nodes[node]
                    op = node_info["op"]

                    if isinstance(op, ops.OneQubitGateWrapper):
                        gate_list += op.operations
                    else:
                        gate_list.append(op.__class__)
                    self.remove_op(node)
                if next_node not in self.node_dict["one-qubit"] and gate_list:
                    # insert new op here
                    out_edges = self.dag.out_edges(nbunch=next_node, keys=True)
                    insert_edge = self.edge_from_reg(out_edges, f"{reg_type}{register}")
                    self.insert_at(
                        ops.OneQubitGateWrapper(gate_list, register, reg_type),
                        [insert_edge],
                    )
                    gate_list = []

    def assign_noise(self, noise_model_map):
        """
        Create a copy of the circuit where each gate is appended its noise model

        :param noise_model_map:
        :type noise_model_map:
        :return: a new circuit
        :rtype: CircuitDAG
        """
        empty_circ = CircuitDAG(
            n_emitter=self.n_emitters,
            n_photon=self.n_photons,
            n_classical=self.n_classical,
        )
        new_gates = self._noisy_gates(noise_model_map)
        for gate in new_gates:
            empty_circ.add(gate)
        return empty_circ

    def _noisy_gates(self, noise_model_map):
        seq = self._slim_seq()
        noisy_ops = []
        for op in seq:
            is_controlled = False
            if isinstance(op, ops.OneQubitGateWrapper):
                op_type_seq = [type(gate) for gate in op.unwrap()]
                noise_list = self._find_wrapped_noise(
                    op_type_seq, noise_model_map[op.reg_type]
                )
                op.noise = noise_list
                noisy_ops.append(op)
            else:
                if isinstance(
                    op,
                    (
                        ops.ControlledPairOperationBase,
                        ops.ClassicalControlledPairOperationBase,
                    ),
                ):
                    control_type = op.control_type
                    target_type = op.target_type
                    mapping = noise_model_map[control_type + target_type]
                    is_controlled = True
                else:
                    mapping = noise_model_map[op.reg_type]
                name = type(op).__name__
                if name in mapping:
                    noise_object = mapping[name]
                else:
                    noise_object = NoNoise()
                if is_controlled:
                    if isinstance(noise_object, list):
                        assert (
                            len(noise_object) == 2
                        ), "controlled gate noise list must be of length 2"
                        op.noise = noise_object
                    else:
                        op.noise = [noise_object, noise_object]
                else:
                    op.noise = noise_object
                noisy_ops.append(op)
        return noisy_ops

    def _find_wrapped_noise(self, op_type_list, mapping):
        noise_list = []
        for op_type in op_type_list:
            op_name = op_type.__name__
            if op_name in mapping:
                noise_list.append(mapping[op_name])
            else:
                noise_list.append(NoNoise())
        return noise_list

    def _slim_seq(self):
        seq = self.sequence()
        length = len(seq) - 1
        for i, op in enumerate(seq[::-1]):
            if isinstance(op, (ops.Input, ops.Output)):
                del seq[length - i]
        return seq

depth property

Returns the circuit depth (NOT including input and output nodes)

Returns:

Type Description
int

circuit depth

register_depth property

Returns the copy of register depth for each register

Returns:

Type Description
dict

register depth

__init__(n_emitter=0, n_photon=0, n_classical=0, openqasm_imports=None, openqasm_defs=None)

Construct a DAG circuit with n_emitter one-qubit emitter quantum registers, n_photon one-qubit photon quantum registers, and n_classical one-cbit classical registers.

Parameters:

Name Type Description Default
n_emitter int

the number of emitter qubits/qudits in the system

0
n_photon int

the number of photon qubits/qudits in the system

0
n_classical int

the number of classical bits in the system

0

Returns:

Type Description
None

nothing

Source code in graphiq/circuit/circuit_dag.py
def __init__(
    self,
    n_emitter=0,
    n_photon=0,
    n_classical=0,
    openqasm_imports=None,
    openqasm_defs=None,
):
    """
    Construct a DAG circuit with n_emitter one-qubit emitter quantum registers, n_photon one-qubit photon
    quantum registers, and n_classical one-cbit classical registers.

    :param n_emitter: the number of emitter qubits/qudits in the system
    :type n_emitter: int
    :param n_photon: the number of photon qubits/qudits in the system
    :type n_photon: int
    :param n_classical: the number of classical bits in the system
    :type n_classical: int
    :return: nothing
    :rtype: None
    """
    self.dag = nx.MultiDiGraph()
    super().__init__(openqasm_imports=openqasm_imports, openqasm_defs=openqasm_defs)

    self._node_id = 0
    self.edge_dict = {}
    self.node_dict = {}
    self._register_depth = {"e": [], "p": [], "c": []}

    # map the key to the tuple register
    reg_map = {
        "e": n_emitter,
        "p": n_photon,
        "c": n_classical,
    }

    for key in reg_map:
        for i in range(reg_map[key]):
            self._add_reg_if_absent(register=i, reg_type=key)

add(operation)

Add an operation to the end of the circuit (i.e. to be applied after the pre-existing circuit operations)

Parameters:

Name Type Description Default
operation OperationBase

Operation (gate and register) to add to the graph

required

Returns:

Type Description
None

nothing

Raises:

Type Description
UserWarning

if no openqasm definitions exists for operation

Source code in graphiq/circuit/circuit_dag.py
def add(self, operation: ops.OperationBase):
    """
    Add an operation to the end of the circuit (i.e. to be applied after the pre-existing circuit operations)

    :param operation: Operation (gate and register) to add to the graph
    :type operation: OperationBase type (or a subclass of it)
    :raises UserWarning: if no openqasm definitions exists for operation
    :return: nothing
    :rtype: None
    """
    self._openqasm_update(operation)

    # update registers (if the new operation is adding registers to the circuit)
    # Check for classical register
    for i in range(len(operation.c_registers)):
        self._add_reg_if_absent(
            register=operation.c_registers[i],
            reg_type="c",
        )

    # Check for qubit register
    register, reg_type = zip(
        *sorted(zip(operation.q_registers, operation.q_registers_type))
    )
    for i in range(len(register)):
        self._add_reg_if_absent(
            register=register[i],
            reg_type=reg_type[i],
        )

    self._add(operation)

assign_noise(noise_model_map)

Create a copy of the circuit where each gate is appended its noise model

Parameters:

Name Type Description Default
noise_model_map
required

Returns:

Type Description
CircuitDAG

a new circuit

Source code in graphiq/circuit/circuit_dag.py
def assign_noise(self, noise_model_map):
    """
    Create a copy of the circuit where each gate is appended its noise model

    :param noise_model_map:
    :type noise_model_map:
    :return: a new circuit
    :rtype: CircuitDAG
    """
    empty_circ = CircuitDAG(
        n_emitter=self.n_emitters,
        n_photon=self.n_photons,
        n_classical=self.n_classical,
    )
    new_gates = self._noisy_gates(noise_model_map)
    for gate in new_gates:
        empty_circ.add(gate)
    return empty_circ

calculate_all_reg_depth()

Calculate all registers depth in the circuit Then return the register depth dict

Returns:

Type Description
dict

register depth dict that has been calculated

Source code in graphiq/circuit/circuit_dag.py
def calculate_all_reg_depth(self):
    """
    Calculate all registers depth in the circuit
    Then return the register depth dict

    :return: register depth dict that has been calculated
    :rtype: dict
    """
    for reg_type in self._register_depth:
        self.calculate_reg_depth(reg_type=reg_type)
    return self._register_depth.copy()

calculate_reg_depth(reg_type)

Calculate the register depth of the register type Then return the register depth array

Parameters:

Name Type Description Default
reg_type str

str indicates register type. Can be "e", "p", or "c"

required

Returns:

Type Description
numpy.array

the array of register depth of all register in the register type

Source code in graphiq/circuit/circuit_dag.py
def calculate_reg_depth(self, reg_type: str):
    """
    Calculate the register depth of the register type
    Then return the register depth array

    :param reg_type: str indicates register type. Can be "e", "p", or "c"
    :type reg_type: str
    :return: the array of register depth of all register in the register type
    :rtype: numpy.array
    """
    if reg_type not in self._register_depth:
        raise ValueError(f"register type {reg_type} is not in this circuit")

    for i in range(len(self._register_depth[reg_type])):
        output_node = f"{reg_type}{i}_out"
        self._register_depth[reg_type][i] = self._max_depth(output_node)
    return self._register_depth[reg_type]

compare(circuit, method='direct')

Comparing two circuits by using GED or direct loop method

Parameters:

Name Type Description Default
circuit CircuitDAG

circuit that to be compared

required
method str

Determine which comparison function to use

'direct'

Returns:

Type Description
bool

whether two circuits are the same

Source code in graphiq/circuit/circuit_dag.py
def compare(self, circuit, method="direct"):
    """
    Comparing two circuits by using GED or direct loop method

    :param circuit: circuit that to be compared
    :type circuit: CircuitDAG
    :param method: Determine which comparison function to use
    :type method: str
    :return: whether two circuits are the same
    :rtype: bool
    """
    return compare_circuits(self, circuit, method=method)

draw_dag(show=True, fig=None, ax=None)

Draw the circuit as a DAG

Parameters:

Name Type Description Default
show bool

if True, the DAG is displayed (shown). If False, the DAG is drawn but not displayed

True
fig None | matplotlib.pyplot.figure

fig on which to draw the DAG (optional)

None
ax None | matplotlib.pyplot.axes

ax on which to draw the DAG (optional)

None

Returns:

Type Description
matplotlib.pyplot.figure, matplotlib.pyplot.axes

fig, ax on which the DAG was drawn

Source code in graphiq/circuit/circuit_dag.py
def draw_dag(self, show=True, fig=None, ax=None):
    """
    Draw the circuit as a DAG

    :param show: if True, the DAG is displayed (shown). If False, the DAG is drawn but not displayed
    :type show: bool
    :param fig: fig on which to draw the DAG (optional)
    :type fig: None or matplotlib.pyplot.figure
    :param ax: ax on which to draw the DAG (optional)
    :type ax: None or matplotlib.pyplot.axes
    :return: fig, ax on which the DAG was drawn
    :rtype: matplotlib.pyplot.figure, matplotlib.pyplot.axes
    """
    # TODO: fix this such that we can see double edges properly!
    pos = dag_topology_pos(self.dag, method="topology")

    if ax is None or fig is None:
        fig, ax = plt.subplots()
    nx.draw(self.dag, pos=pos, ax=ax, with_labels=True)
    if show:
        plt.show()
    return fig, ax

edge_from_reg(t_edges, t_register) staticmethod

Helper function to return correct edge from edges that map to the correct register.

Parameters:

Name Type Description Default
t_edges edge

input edge

required
t_register str

register

required

Returns:

Type Description
edge

correct edge

Source code in graphiq/circuit/circuit_dag.py
@staticmethod
def edge_from_reg(t_edges, t_register):
    """
    Helper function to return correct edge from edges that map to the correct register.

    :param t_edges: input edge
    :type t_edges: edge
    :param t_register: register
    :type t_register: str
    :return: correct edge
    :rtype: edge
    """
    for e in t_edges:
        if e[-1] == t_register:
            return e

find_incompatible_edges(first_edge)

Find all incompatible edges of first_edge for which one cannot add any two-qubit operation

Parameters:

Name Type Description Default
first_edge tuple

the edge under consideration

required

Returns:

Type Description
set(tuple)

a set of incompatible edges

Source code in graphiq/circuit/circuit_dag.py
def find_incompatible_edges(self, first_edge):
    """
    Find all incompatible edges of first_edge for which one cannot add any two-qubit operation

    :param first_edge: the edge under consideration
    :type first_edge: tuple
    :return: a set of incompatible edges
    :rtype: set(tuple)
    """

    # all nodes that have a path to the node first_edge[0]
    ancestors = nx.ancestors(self.dag, first_edge[0])

    # all nodes that are reachable from the node first_edge[1]
    descendants = nx.descendants(self.dag, first_edge[1])

    # all incoming edges of the node first_edge[0]
    ancestor_edges = list(self.dag.in_edges(first_edge[0], keys=True))

    for anc in ancestors:
        ancestor_edges.extend(self.dag.edges(anc, keys=True))

    # all outgoing edges of the node first_edge[1]
    descendant_edges = list(self.dag.out_edges(first_edge[1], keys=True))

    for des in descendants:
        descendant_edges.extend(self.dag.edges(des, keys=True))

    return set.union(set([first_edge]), set(ancestor_edges), set(descendant_edges))

from_json(data_dict) classmethod

Function to load from json object to circuit object

Parameters:

Name Type Description Default
data_dict dict

circuit json dict

required

Returns:

Type Description
None

nothing

Source code in graphiq/circuit/circuit_dag.py
@classmethod
def from_json(cls, data_dict):
    """
    Function to load from json object to circuit object

    :param data_dict: circuit json dict
    :type data_dict: dict
    :return: nothing
    :rtype: None
    """
    circuit = CircuitDAG(
        n_photon=data_dict["n_photons"],
        n_emitter=data_dict["n_emitters"],
        n_classical=data_dict["n_classical"],
    )

    for op in data_dict["ops"]:
        if op["type"] == "one qubit gate wrapper":
            op_list = [ops.name_to_class_map(g) for g in op["op_list"]]
            gate = ops.OneQubitGateWrapper(
                op_list,
                register=op["q_registers"][0],
                reg_type=op["q_registers_type"][0],
            )
        else:
            gate = ops.name_to_class_map(op["type"])
            gate = gate()
            gate.q_registers = op["q_registers"]
            gate.q_registers_type = op["q_registers_type"]
            gate.c_registers = op["c_registers"]

        circuit.add(gate)

    return circuit

from_openqasm(qasm_script) classmethod

Create a circuit based on an (assumed to be valid) openQASM script

Parameters:

Name Type Description Default
qasm_script str

the openqasm script from which a circuit should be built

required

Returns:

Type Description
CircuitBase

a circuit object

Source code in graphiq/circuit/circuit_dag.py
@classmethod
def from_openqasm(cls, qasm_script):
    """
    Create a circuit based on an (assumed to be valid) openQASM script

    :param qasm_script: the openqasm script from which a circuit should be built
    :type qasm_script: str
    :return: a circuit object
    :rtype: CircuitBase
    """

    for elem in string.whitespace:
        if elem != " ":  # keep spaces, but no other whitespace
            qasm_script = qasm_script.replace(elem, "")

    # script must start with OPENQASM 2.0; or another openQASM number
    script_list = re.split(";", qasm_script, 1)
    header = script_list[0]
    for elem in string.whitespace:
        header = header.replace(elem, "")
    assert header == "OPENQASM2.0"
    qasm_script = script_list[1]  # get rid of header now that we've checked it

    # get rid of any gate declarations--we don't actually need them for the parsing
    search_match = re.search(r"gate[^}]*{[^}]*}", qasm_script)
    while search_match is not None:
        qasm_script = qasm_script.replace(search_match.group(0), "")
        search_match = re.search(r"gate[^}]*{[^}]*}", qasm_script)

    # Next, we can parse each sentence
    qasm_commands = re.split(";", qasm_script)

    n_photon = len(
        [
            command
            for command in qasm_commands
            if "qregp" in command.replace(" ", "")
        ]
    )
    n_emitter = len(
        [
            command
            for command in qasm_commands
            if "qrege" in command.replace(" ", "")
        ]
    )
    n_classical = len([command for command in qasm_commands if "creg" in command])

    circuit = CircuitDAG(
        n_photon=n_photon, n_emitter=n_emitter, n_classical=n_classical
    )
    i = 0
    while i in range(len(qasm_commands)):
        command = qasm_commands[i]
        if (
            ("qreg" in command)
            or ("creg" in command)
            or ("barrier" in command)
            or (command == "")
        ):
            i += 1
            continue

        if "measure" in command and "->" in command:
            q_str = re.search(r"(e|p)(\d)+\[0\]", command).group(0)
            c_str = re.search(r"c(\d)+\[0\]", command).group(0)

            q_type = q_str[0]
            q_reg = int(re.split(r"\[", q_str[1:])[0])
            c_reg = int(re.split(r"\[", c_str[1:])[0])

            def _parse_if(command):
                c_str = re.search(r"c(\d)+==1", command.replace(" ", "")).group(0)
                c_reg = int(re.split("==", c_str)[0][1:])
                gate = re.search(
                    r"\)[a-z](p|e)(\d)+\[", command.replace(" ", "")
                ).group(0)[1]
                reg_str = re.search(r"(p|e)(\d)+\[", command).group(0)
                reg = int(reg_str[1:-1])
                reg_type = reg_str[0]

                return gate, reg, reg_type, c_reg

            if i + 3 < len(qasm_commands):  # could be a 4 line operation
                if "if" in qasm_commands[i + 1] and "reset" in qasm_commands[i + 3]:
                    gate, target_reg, target_type, c_reg = _parse_if(
                        qasm_commands[i + 1]
                    )
                    reset_str = re.split(r"\s", qasm_commands[i + 3].strip())[1]
                    reset_type = reset_str[0]
                    reset_reg = int(reset_str[1:-3])
                    assert reset_type == q_type, (
                        f"Reset should be on control qubit, reset type is:{reset_type}, "
                        f"control qubit type was: {q_type}"
                    )
                    assert reset_reg == q_reg, (
                        f"Reset should be on control qubit. Reset reg is: {reset_reg}, "
                        f"control reg is: {q_reg}"
                    )

                    circuit.add(
                        ops.name_to_class_map(f"classical reset {gate}")(
                            control=q_reg,
                            control_type=q_type,
                            target=target_reg,
                            target_type=target_type,
                            c_register=c_reg,
                        )
                    )
                    i += 4
                    continue

            if i + 1 < len(qasm_commands):
                if "if" in qasm_commands[i + 1]:
                    gate, target_reg, target_type, c_reg = _parse_if(
                        qasm_commands[i + 1]
                    )
                    circuit.add(
                        ops.name_to_class_map(f"classical {gate}")(
                            control=q_reg,
                            control_type=q_type,
                            target=target_reg,
                            target_type=target_type,
                            c_register=c_reg,
                        )
                    )
                    i += 2
                    continue

            circuit.add(
                ops.MeasurementZ(register=q_reg, reg_type=q_type, c_register=c_reg)
            )
            i += 1
            continue

        # Parse single-qubit operations
        if (
            command.count("[0]") == 1
        ):  # single qubit operation, from current script generation method
            command_breakdown = command.split()
            name = command_breakdown[0]
            reg_type = command_breakdown[1][0]
            reg = int(command_breakdown[1][1:-3])  # we must parse out [0] so -3
            gate_class = ops.name_to_class_map(name)
            if gate_class is not None:
                circuit.add(gate_class(register=reg, reg_type=reg_type))
            else:
                circuit_list = [ops.name_to_class_map(letter) for letter in name]
                assert None not in circuit_list, (
                    f"Gate not recognized, parsing invalid/"
                    f"{name} parsed to {circuit_list}"
                )
                circuit.add(
                    ops.OneQubitGateWrapper(
                        circuit_list, register=reg, reg_type=reg_type
                    )
                )
        elif command.count("[0]") == 2:  # two-qubit gate
            command_breakdown = command.split()
            name = command_breakdown[0]
            control_type = command_breakdown[1][0]
            control_reg = int(
                command_breakdown[1][1:-4]
            )  # we must parse out [0], so -4
            target_type = command_breakdown[2][0]
            target_reg = int(
                command_breakdown[2][1:-3]
            )  # we must parse out [0] so -3
            gate_class = ops.name_to_class_map(name)
            assert (
                gate_class is not None
            ), "gate name not recognized, parsing failed"
            circuit.add(
                gate_class(
                    control=control_reg,
                    control_type=control_type,
                    target=target_reg,
                    target_type=target_type,
                )
            )
        else:
            raise ValueError(f"command not recognized, cannot be parsed")
        i += 1

    return circuit

get_node_by_labels(labels)

Get all nodes that satisfy all labels

Parameters:

Name Type Description Default
labels list[str]

descriptions of a set of nodes

required

Returns:

Type Description
list[int]

a list of node ids for nodes that satisfy all labels

Source code in graphiq/circuit/circuit_dag.py
def get_node_by_labels(self, labels):
    """
    Get all nodes that satisfy all labels

    :param labels: descriptions of a set of nodes
    :type labels: list[str]
    :return: a list of node ids for nodes that satisfy all labels
    :rtype: list[int]
    """
    remaining_nodes = set(self.dag.nodes)
    for label in labels:
        remaining_nodes = remaining_nodes.intersection(set(self.node_dict[label]))
    return list(remaining_nodes)

get_node_exclude_labels(labels)

Get all nodes that do not satisfy any label in labels

Parameters:

Name Type Description Default
labels list[str]

descriptions of a set of nodes

required

Returns:

Type Description
list[int]

a list of node ids for nodes that do not satisfy any label in labels

Source code in graphiq/circuit/circuit_dag.py
def get_node_exclude_labels(self, labels):
    """
    Get all nodes that do not satisfy any label in labels

    :param labels: descriptions of a set of nodes
    :type labels: list[str]
    :return: a list of node ids for nodes that do not satisfy any label in labels
    :rtype: list[int]
    """
    all_nodes = set(self.dag.nodes)
    exclusion_nodes = set()
    for label in labels:
        exclusion_nodes = exclusion_nodes.union(set(self.node_dict[label]))
    return list(all_nodes - exclusion_nodes)

group_one_qubit_gates()

Put consecutive one-qubit gates into a OneQubitGateWrapper

Returns:

Type Description
None

nothing

Source code in graphiq/circuit/circuit_dag.py
def group_one_qubit_gates(self):
    """
    Put consecutive one-qubit gates into a OneQubitGateWrapper

    :return: nothing
    :rtype: None
    """
    for node in self.node_dict["Output"]:
        # traverse the circuit DAG in the reversed order
        reg_type = self.dag.nodes[node]["op"].reg_type
        register = self.dag.nodes[node]["op"].register
        gate_list = []

        in_edges = self.dag.in_edges(nbunch=node, keys=True)
        next_node = self.edge_from_reg(in_edges, f"{reg_type}{register}")[0]

        while next_node not in self.node_dict["Input"]:
            node = next_node
            in_edges = self.dag.in_edges(nbunch=node, keys=True)
            edge = self.edge_from_reg(in_edges, f"{reg_type}{register}")
            next_node = edge[0]

            if node in self.node_dict["one-qubit"]:
                node_info = self.dag.nodes[node]
                op = node_info["op"]

                if isinstance(op, ops.OneQubitGateWrapper):
                    gate_list += op.operations
                else:
                    gate_list.append(op.__class__)
                self.remove_op(node)
            if next_node not in self.node_dict["one-qubit"] and gate_list:
                # insert new op here
                out_edges = self.dag.out_edges(nbunch=next_node, keys=True)
                insert_edge = self.edge_from_reg(out_edges, f"{reg_type}{register}")
                self.insert_at(
                    ops.OneQubitGateWrapper(gate_list, register, reg_type),
                    [insert_edge],
                )
                gate_list = []

insert_at(operation, edges)

Insert an operation among specified edges

Parameters:

Name Type Description Default
operation OperationBase

Operation (gate and register) to add to the graph

required
edges list[tuple]

a list of edges relevant for this operation

required

Returns:

Type Description
None

nothing

Raises:

Type Description
UserWarning

if no openqasm definitions exists for operation

AssertionError

if the number of edges disagrees with the number of q_registers

Source code in graphiq/circuit/circuit_dag.py
def insert_at(self, operation: ops.OperationBase, edges):
    """
    Insert an operation among specified edges

    :param operation: Operation (gate and register) to add to the graph
    :type operation: OperationBase type (or a subclass of it)
    :param edges: a list of edges relevant for this operation
    :type edges: list[tuple]
    :raises UserWarning: if no openqasm definitions exists for operation
    :raises AssertionError: if the number of edges disagrees with the number of q_registers
    :return: nothing
    :rtype: None
    """
    self._openqasm_update(operation)

    # update registers (if the new operation is adding registers to the circuit)
    # Check for classical register
    for i in range(len(operation.c_registers)):
        self._add_reg_if_absent(
            register=operation.c_registers[i],
            reg_type="c",
        )

    # Check for qubit register
    register, reg_type = zip(
        *sorted(zip(operation.q_registers, operation.q_registers_type))
    )
    for i in range(len(register)):
        self._add_reg_if_absent(
            register=register[i],
            reg_type=reg_type[i],
        )

    assert len(edges) == len(operation.q_registers)
    # note that we implicitly assume there is only one classical register (bit) so that
    # we only count the quantum registers here. (Also only including quantum registers in edges)
    self._openqasm_update(operation)
    self._insert_at(operation, edges)

min_reg_depth_index(reg_type)

Calculate the register depth of the register type Then return the index of the register with minimum depth

Parameters:

Name Type Description Default
reg_type str

str indicates register type. Can be "e", "p", or "c"

required

Returns:

Type Description
int

the index of register with min depth within register type

Source code in graphiq/circuit/circuit_dag.py
def min_reg_depth_index(self, reg_type: str):
    """
    Calculate the register depth of the register type
    Then return the index of the register with minimum depth

    :param reg_type: str indicates register type. Can be "e", "p", or "c"
    :type reg_type: str
    :return: the index of register with min depth within register type
    :rtype: int
    """
    return np.argmin(self.calculate_reg_depth(reg_type=reg_type))

reg_gate_history(reg, reg_type='e')

Finds all the gates that the specified register goes through in the given circuit. A list of operations (gates) and a list of nodes in the chronological order is returned.

Parameters:

Name Type Description Default
reg int

the register

required
reg_type str

the type of the register

'e'

Returns:

Type Description
tuple(list, list)

a tuple of lists, the first one is the list of operation and the second one is the list of nodes in the DAG

Source code in graphiq/circuit/circuit_dag.py
def reg_gate_history(self, reg, reg_type="e"):
    """
    Finds all the gates that the specified register goes through in the given circuit. A list of operations (gates)
    and a list of nodes in the chronological order is returned.

    :param reg: the register
    :type reg: int
    :param reg_type: the type of the register
    :type reg_type: str
    :return: a tuple of lists, the first one is the list of operation and the second one is the list of nodes in
    the DAG
    :rtype: tuple(list, list)
    """
    next_node = f"{reg_type}{reg}_in"
    ordered_nodes = [next_node]
    while next_node != f"{reg_type}{reg}_out":
        next_node = [
            edge[1]
            for edge in self.dag.out_edges(next_node, data=True)
            if edge[2]["reg"] == reg and edge[2]["reg_type"] == reg_type
        ][0]
        ordered_nodes.append(next_node)
    ops_list = [self.dag.nodes[nod]["op"] for nod in ordered_nodes]
    return ops_list, ordered_nodes

remove_identity()

Remove all identity gates

Returns:

Type Description
None

nothing

Source code in graphiq/circuit/circuit_dag.py
def remove_identity(self):
    """
    Remove all identity gates

    :return: nothing
    :rtype: None
    """

    if "Identity" in self.node_dict:
        identity_list = self.node_dict["Identity"].copy()
        for node in identity_list:
            self.remove_op(node)

remove_op(node)

Remove an operation from the circuit

Parameters:

Name Type Description Default
node int

the node to be removed

required

Returns:

Type Description
None

nothing

Source code in graphiq/circuit/circuit_dag.py
def remove_op(self, node):
    """
    Remove an operation from the circuit

    :param node: the node to be removed
    :type node: int
    :return: nothing
    :rtype: None
    """
    self._remove_node(node)

replace_op(node, new_operation)

Replaces an operation by a new one with the same set of registers it acts on.

Parameters:

Name Type Description Default
node int

the node where the new operation is placed

required
new_operation OperationBase

the new operation

required

Returns:

Type Description
None

nothing

Raises:

Type Description
AssertionError

if new_operation acts on different registers from the operation in the node

Source code in graphiq/circuit/circuit_dag.py
def replace_op(self, node, new_operation: ops.OperationBase):
    """
    Replaces an operation by a new one with the same set of registers it acts on.

    :param node: the node where the new operation is placed
    :type node: int
    :param new_operation: the new operation
    :type new_operation: OperationBase or its subclass
    :raises AssertionError: if new_operation acts on different registers from the operation in the node
    :return: nothing
    :rtype: None
    """

    old_operation = self.dag.nodes[node]["op"]
    assert old_operation.q_registers == new_operation.q_registers
    assert old_operation.q_registers_type == new_operation.q_registers_type
    assert old_operation.c_registers == new_operation.c_registers

    # remove entries related to old_operation
    for label in old_operation.labels:
        self._node_dict_remove(label, node)
    self._node_dict_remove(type(old_operation).__name__, node)
    self._node_dict_remove(old_operation.parse_q_reg_types(), node)

    # add entries related to new_operation
    for label in new_operation.labels:
        self._node_dict_append(label, node)
    self._node_dict_append(type(new_operation).__name__, node)
    self._node_dict_append(new_operation.parse_q_reg_types(), node)

    # replace the operation in the node
    self._openqasm_update(new_operation)
    self.dag.nodes[node]["op"] = new_operation

sequence(unwrapped=False)

Return the sequence of operations composing this circuit

Parameters:

Name Type Description Default
unwrapped bool

If True, we "unwrap" the operation objects such that the returned sequence has only non-composed gates (i.e. wrapper gates which include multiple non-composed gates are broken down into their constituent parts). If False, operations are returned as defined in the circuit (i.e. wrapper gates are returned as wrappers)

False

Returns:

Type Description
list | iterator (of OperationBase subclass objects)

the operations which compose this circuit, in the order they should be applied

Source code in graphiq/circuit/circuit_dag.py
def sequence(self, unwrapped=False):
    """
    Return the sequence of operations composing this circuit

    :param unwrapped: If True, we "unwrap" the operation objects such that the returned sequence has only
                      non-composed gates (i.e. wrapper gates which include multiple non-composed gates are
                      broken down into their constituent parts). If False, operations are returned as defined
                      in the circuit (i.e. wrapper gates are returned as wrappers)
    :type unwrapped: bool
    :return: the operations which compose this circuit, in the order they should be applied
    :rtype: list or iterator (of OperationBase subclass objects)
    """
    op_list = [self.dag.nodes[node]["op"] for node in nx.topological_sort(self.dag)]
    if not unwrapped:
        return op_list

    return functools.reduce(lambda x, y: x + y.unwrap(), op_list, [])

sorted_reg_depth_index(reg_type)

Return the array of register indexes with depth from smallest to largest Useful to find register index with nth smallest depth

Parameters:

Name Type Description Default
reg_type str

str indicates register type. Can be "e", "p", or "c"

required

Returns:

Type Description
numpy.array

the array of register indexes with depth from smallest to largest

Source code in graphiq/circuit/circuit_dag.py
def sorted_reg_depth_index(self, reg_type: str):
    """
    Return the array of register indexes with depth from smallest to largest
    Useful to find register index with nth smallest depth

    :param reg_type: str indicates register type. Can be "e", "p", or "c"
    :type reg_type: str
    :return: the array of register indexes with depth from smallest to largest
    :rtype: numpy.array
    """
    return np.argsort(self.calculate_reg_depth(reg_type=reg_type))

to_json()

Function to convert circuit object to json data format.

Returns:

Type Description
dict

circuit json object

Source code in graphiq/circuit/circuit_dag.py
def to_json(self):
    """
    Function to convert circuit object to json data format.

    :return: circuit json object
    :rtype: dict
    """
    data = {
        "n_photons": self.n_photons,
        "n_emitters": self.n_emitters,
        "n_classical": self.n_classical,
        "ops": [],
    }

    for op in self.sequence():
        if isinstance(op, ops.InputOutputOperationBase):
            continue
        elif type(op) == ops.OneQubitGateWrapper:
            op_list = []
            for g in op.operations:
                name = ops.class_to_name_mapping(g)
                if name:
                    op_list.append(name)
            op_data = {
                "type": "one qubit gate wrapper",
                "op_list": op_list,
                "q_registers_type": op.q_registers_type,
                "q_registers": op.q_registers,
                "c_registers": op.c_registers,
            }
        else:
            op_data = {
                "type": ops.class_to_name_mapping(type(op)),
                "q_registers_type": op.q_registers_type,
                "q_registers": op.q_registers,
                "c_registers": op.c_registers,
                # ...,
            }
        data["ops"].append(op_data)

    return data

unwrap_nodes()

Unwrap the nodes with more than 1 ops in it

Returns:

Type Description
None

nothing

Source code in graphiq/circuit/circuit_dag.py
def unwrap_nodes(self):
    """
    Unwrap the nodes with more than 1 ops in it

    :return: nothing
    :rtype: None
    """

    if "OneQubitGateWrapper" in self.node_dict:
        wrapper_list = self.node_dict["OneQubitGateWrapper"].copy()
        for node in wrapper_list:
            op_list = self.dag.nodes[node]["op"].unwrap()
            for op in op_list:
                in_edge = list(self.dag.in_edges(node, keys=True))
                self.insert_at(op, in_edge)
            self.remove_op(node)

validate()

Assert that the circuit is valid (is a DAG, all nodes without input edges are input nodes, all nodes without output edges are output nodes)

Returns:

Type Description
None

this function returns nothing

Raises:

Type Description
RuntimeError

if the circuit is not valid

Source code in graphiq/circuit/circuit_dag.py
def validate(self):
    """
    Assert that the circuit is valid (is a DAG, all nodes
    without input edges are input nodes, all nodes without output edges
    are output nodes)

    :raises RuntimeError: if the circuit is not valid
    :return: this function returns nothing
    :rtype: None
    """
    assert nx.is_directed_acyclic_graph(self.dag)  # check DAG is correct

    # check all "source" nodes to the DAG are Input operations
    input_nodes = [
        node for node, in_degree in self.dag.in_degree() if in_degree == 0
    ]
    # assert set(input_nodes)  == set(self.node_dict['Input'])
    for input_node in input_nodes:
        if not isinstance(self.dag.nodes[input_node]["op"], ops.Input):
            raise RuntimeError(
                f"Source node {input_node} in the DAG is not an Input operation"
            )

    # check all "sink" nodes to the DAG are Output operations
    output_nodes = [
        node for node, out_degree in self.dag.out_degree() if out_degree == 0
    ]
    # assert set(output_nodes) == set(self.node_dict['Output'])
    for output_node in output_nodes:
        if not isinstance(self.dag.nodes[output_node]["op"], ops.Output):
            raise RuntimeError(
                f"Sink node {output_node} in the DAG is not an Output operation"
            )

graphiq.circuit.ops

The Operation objects are objects which tell the compiler what gate to apply on which registers / qubits

They serve two purposes: 1. They are used to build our circuit: circuit.add(OperationObj). The circuit constructs the DAG structure from the connectivity information in OperationObj 2. They are passed as an iterable to the compiler, such that the compiler can perform the correct series of information

REGISTERS VS QUDITS/CBITS: following qiskit/openQASM and other software, we allow a distinction between registers and qudits/qubits (where one register can contain a number of qubits / qudits).

IF an operation is given q_registers=(a, b), the circuit takes it to apply between registers a and b (that is, it applies between all qubits a[j], b[j] for 0 < j < n with a, b having length n)

If an operation is given q_registers=((a, b), (c, d)), the circuit takes it to apply between qubit b of register a, and qubit d of register c.

We can also use a mixture of registers and qubits: q_registers=(a, (b, c)) means that the operation will be applied between EACH QUBIT of register a, and qubit c of register b

CNOT

Bases: ControlledPairOperationBase

CNOT gate Operation

Source code in graphiq/circuit/ops.py
class CNOT(ControlledPairOperationBase):
    """
    CNOT gate Operation
    """

    _openqasm_info = oq_lib.cnot_info()

    def __init__(
        self, control=0, control_type="e", target=0, target_type="e", noise=nm.NoNoise()
    ):
        super().__init__(control, control_type, target, target_type, noise)

CZ

Bases: ControlledPairOperationBase

Controlled-Z gate Operation

Source code in graphiq/circuit/ops.py
class CZ(ControlledPairOperationBase):
    """
    Controlled-Z gate Operation
    """

    _openqasm_info = oq_lib.cz_info()

    def __init__(
        self, control=0, control_type="e", target=0, target_type="e", noise=nm.NoNoise()
    ):
        super().__init__(control, control_type, target, target_type, noise)

ClassicalCNOT

Bases: ClassicalControlledPairOperationBase

Classical CNOT gate Operation

Source code in graphiq/circuit/ops.py
class ClassicalCNOT(ClassicalControlledPairOperationBase):
    """
    Classical CNOT gate Operation
    """

    _openqasm_info = oq_lib.classical_cnot_info()

ClassicalCZ

Bases: ClassicalControlledPairOperationBase

Classical CZ gate Operation

Source code in graphiq/circuit/ops.py
class ClassicalCZ(ClassicalControlledPairOperationBase):
    """
    Classical CZ gate Operation
    """

    _openqasm_info = oq_lib.classical_cz_info()

ClassicalControlledPairOperationBase

Bases: OperationBase

This is used as a base class for our classical controlled gates (e.g. classical CNOT, classical CPHASE). Each ClassicalControlledPairOperationBase should have control and target registers/qubits specified, and a c_register target register/cbit specified.

Source code in graphiq/circuit/ops.py
class ClassicalControlledPairOperationBase(OperationBase):
    """
    This is used as a base class for our classical controlled gates (e.g. classical CNOT, classical CPHASE).
    Each ClassicalControlledPairOperationBase should have control and target registers/qubits specified, and
    a c_register target register/cbit specified.
    """

    def __init__(
        self,
        control,
        control_type,
        target,
        target_type,
        c_register=0,
        noise=nm.NoNoise(),
    ):
        """
        Creates the classically controlled gate

        :param control: the control register/qubit
        :type control: int OR tuple (of ints, length 2)
        :param control_type: 'p' if photonic qubit, 'e' if emitter qubit
        :type control_type: str
        :param target: target register/qubit for the Operation
        :type target: int OR tuple (of ints, length 2)
        :param target_type: 'p' if photonic qubit, 'e' if emitter qubit
        :param c_register: the classical register/cbit
        :type c_register: int OR tuple (of ints, length 2)
        :param noise: Noise model
        :type noise: graphiq.noise.noise_models.NoiseBase
        :return: nothing
        :rtype: None
        """
        if isinstance(noise, list):
            assert len(noise) == 2
        else:
            noise = 2 * [noise]
        super().__init__(
            q_registers=(control, target),
            q_registers_type=(control_type, target_type),
            c_registers=(c_register,),
            noise=noise,
        )

        self.control = control
        self.control_type = control_type
        self.target = target
        self.target_type = target_type
        self.c_register = c_register
        self.add_labels("two-qubit")

    @OperationBase.q_registers.setter
    def q_registers(self, q_reg):
        """
        Handle to modify the register-qubit pairs on which the operation acts. This also automatically updates the
        self.control, self.target fields

        :param q_reg: the new q_register value to set
        :raises ValueError: if the new q_reg object does not have a length of 2
        :return: function returns nothing
        :rtype: None
        """
        self._update_q_reg(q_reg)
        self.control = q_reg[0]
        self.target = q_reg[1]

    @OperationBase.c_registers.setter
    def c_registers(self, c_reg):
        """
        Handle to modify the register-cbit pair on which the operation acts. This also automatically updates the
        self.c_register field

        :param c_reg: the new c_register value to set
        :raises ValueError: if the new c_reg object does not have a length of 1
        :return: function returns nothing
        :rtype: None
        """
        self._update_c_reg(c_reg)
        self.c_register = c_reg[0]

__init__(control, control_type, target, target_type, c_register=0, noise=nm.NoNoise())

Creates the classically controlled gate

Parameters:

Name Type Description Default
control int OR tuple (of ints, length 2)

the control register/qubit

required
control_type str

'p' if photonic qubit, 'e' if emitter qubit

required
target int OR tuple (of ints, length 2)

target register/qubit for the Operation

required
target_type

'p' if photonic qubit, 'e' if emitter qubit

required
c_register int OR tuple (of ints, length 2)

the classical register/cbit

0
noise graphiq.noise.noise_models.NoiseBase

Noise model

NoNoise()

Returns:

Type Description
None

nothing

Source code in graphiq/circuit/ops.py
def __init__(
    self,
    control,
    control_type,
    target,
    target_type,
    c_register=0,
    noise=nm.NoNoise(),
):
    """
    Creates the classically controlled gate

    :param control: the control register/qubit
    :type control: int OR tuple (of ints, length 2)
    :param control_type: 'p' if photonic qubit, 'e' if emitter qubit
    :type control_type: str
    :param target: target register/qubit for the Operation
    :type target: int OR tuple (of ints, length 2)
    :param target_type: 'p' if photonic qubit, 'e' if emitter qubit
    :param c_register: the classical register/cbit
    :type c_register: int OR tuple (of ints, length 2)
    :param noise: Noise model
    :type noise: graphiq.noise.noise_models.NoiseBase
    :return: nothing
    :rtype: None
    """
    if isinstance(noise, list):
        assert len(noise) == 2
    else:
        noise = 2 * [noise]
    super().__init__(
        q_registers=(control, target),
        q_registers_type=(control_type, target_type),
        c_registers=(c_register,),
        noise=noise,
    )

    self.control = control
    self.control_type = control_type
    self.target = target
    self.target_type = target_type
    self.c_register = c_register
    self.add_labels("two-qubit")

c_registers(c_reg)

Handle to modify the register-cbit pair on which the operation acts. This also automatically updates the self.c_register field

Parameters:

Name Type Description Default
c_reg

the new c_register value to set

required

Returns:

Type Description
None

function returns nothing

Raises:

Type Description
ValueError

if the new c_reg object does not have a length of 1

Source code in graphiq/circuit/ops.py
@OperationBase.c_registers.setter
def c_registers(self, c_reg):
    """
    Handle to modify the register-cbit pair on which the operation acts. This also automatically updates the
    self.c_register field

    :param c_reg: the new c_register value to set
    :raises ValueError: if the new c_reg object does not have a length of 1
    :return: function returns nothing
    :rtype: None
    """
    self._update_c_reg(c_reg)
    self.c_register = c_reg[0]

q_registers(q_reg)

Handle to modify the register-qubit pairs on which the operation acts. This also automatically updates the self.control, self.target fields

Parameters:

Name Type Description Default
q_reg

the new q_register value to set

required

Returns:

Type Description
None

function returns nothing

Raises:

Type Description
ValueError

if the new q_reg object does not have a length of 2

Source code in graphiq/circuit/ops.py
@OperationBase.q_registers.setter
def q_registers(self, q_reg):
    """
    Handle to modify the register-qubit pairs on which the operation acts. This also automatically updates the
    self.control, self.target fields

    :param q_reg: the new q_register value to set
    :raises ValueError: if the new q_reg object does not have a length of 2
    :return: function returns nothing
    :rtype: None
    """
    self._update_q_reg(q_reg)
    self.control = q_reg[0]
    self.target = q_reg[1]

ControlledPairOperationBase

Bases: OperationBase

This is used as a base class for our quantum controlled gates (e.g. CNOT, CPHASE). Each ControlledPairOperationBase should have control and target registers/qubits specified

Source code in graphiq/circuit/ops.py
class ControlledPairOperationBase(OperationBase):
    """
    This is used as a base class for our quantum controlled gates (e.g. CNOT, CPHASE). Each ControlledPairOperationBase
    should have control and target registers/qubits specified
    """

    def __init__(self, control, control_type, target, target_type, noise=nm.NoNoise()):
        """
        Creates a control gate object

        :param control: control register/qubit for the Operation
        :type control: int OR tuple (of ints, length 2)
        :param control_type: 'p' if photonic qubit, 'e' if emitter qubit
        :type control_type: str
        :param target: target register/qubit for the Operation
        :type target: int OR tuple (of ints, length 2)
        :param target_type: 'p' if photonic qubit, 'e' if emitter qubit
        :type target_type: str
        :return: function returns nothing
        :rtype: None
        """

        if isinstance(noise, list):
            assert len(noise) == 2
        else:
            noise = 2 * [noise]
        super().__init__(
            q_registers=(control, target),
            q_registers_type=(control_type, target_type),
            noise=noise,
        )

        self.control = control
        self.control_type = control_type
        self.target = target
        self.target_type = target_type
        self.add_labels("two-qubit")

    @OperationBase.q_registers.setter
    def q_registers(self, q_reg):
        """
        Handle to modify the register-qubit pairs on which the operation acts. This also automatically updates the
        self.control, self.target fields

        :param q_reg: the new q_register value to set
        :raises ValueError: if the new q_reg object does not have a length of 2
        :return: function returns nothing
        :rtype: None
        """
        self._update_q_reg(q_reg)

        self.control = q_reg[0]
        self.target = q_reg[1]

__init__(control, control_type, target, target_type, noise=nm.NoNoise())

Creates a control gate object

Parameters:

Name Type Description Default
control int OR tuple (of ints, length 2)

control register/qubit for the Operation

required
control_type str

'p' if photonic qubit, 'e' if emitter qubit

required
target int OR tuple (of ints, length 2)

target register/qubit for the Operation

required
target_type str

'p' if photonic qubit, 'e' if emitter qubit

required

Returns:

Type Description
None

function returns nothing

Source code in graphiq/circuit/ops.py
def __init__(self, control, control_type, target, target_type, noise=nm.NoNoise()):
    """
    Creates a control gate object

    :param control: control register/qubit for the Operation
    :type control: int OR tuple (of ints, length 2)
    :param control_type: 'p' if photonic qubit, 'e' if emitter qubit
    :type control_type: str
    :param target: target register/qubit for the Operation
    :type target: int OR tuple (of ints, length 2)
    :param target_type: 'p' if photonic qubit, 'e' if emitter qubit
    :type target_type: str
    :return: function returns nothing
    :rtype: None
    """

    if isinstance(noise, list):
        assert len(noise) == 2
    else:
        noise = 2 * [noise]
    super().__init__(
        q_registers=(control, target),
        q_registers_type=(control_type, target_type),
        noise=noise,
    )

    self.control = control
    self.control_type = control_type
    self.target = target
    self.target_type = target_type
    self.add_labels("two-qubit")

q_registers(q_reg)

Handle to modify the register-qubit pairs on which the operation acts. This also automatically updates the self.control, self.target fields

Parameters:

Name Type Description Default
q_reg

the new q_register value to set

required

Returns:

Type Description
None

function returns nothing

Raises:

Type Description
ValueError

if the new q_reg object does not have a length of 2

Source code in graphiq/circuit/ops.py
@OperationBase.q_registers.setter
def q_registers(self, q_reg):
    """
    Handle to modify the register-qubit pairs on which the operation acts. This also automatically updates the
    self.control, self.target fields

    :param q_reg: the new q_register value to set
    :raises ValueError: if the new q_reg object does not have a length of 2
    :return: function returns nothing
    :rtype: None
    """
    self._update_q_reg(q_reg)

    self.control = q_reg[0]
    self.target = q_reg[1]

Hadamard

Bases: OneQubitOperationBase

Hadamard gate Operation

Source code in graphiq/circuit/ops.py
class Hadamard(OneQubitOperationBase):
    """
    Hadamard gate Operation
    """

    _openqasm_info = oq_lib.hadamard_info()

    def __init__(self, register=0, reg_type="e", noise=nm.NoNoise()):
        super().__init__(register, reg_type, noise)

Identity

Bases: OneQubitOperationBase

Identity Operation

Source code in graphiq/circuit/ops.py
class Identity(OneQubitOperationBase):
    """
    Identity Operation
    """

    _openqasm_info = oq_lib.empty_info()

    def __init__(self, register=0, reg_type="e", noise=nm.NoNoise()):
        super().__init__(register, reg_type, noise)

Input

Bases: InputOutputOperationBase

Input Operation. Serves as a placeholder in the circuit so that we know that this is where a given qubit/cbit is introduced (i.e. there are no prior operations on it)

Source code in graphiq/circuit/ops.py
class Input(InputOutputOperationBase):
    """
    Input Operation. Serves as a placeholder in the circuit so that we know that this is where a given
    qubit/cbit is introduced (i.e. there are no prior operations on it)
    """

    def __init__(self, register=None, reg_type="e"):
        super().__init__(register, reg_type=reg_type)

InputOutputOperationBase

Bases: OperationBase

This is used as a base class for our Input and Output Operations. These operations largely act as "dummy Operations" signalling the input / output of a state (useful for circuit DAG representation, for example)

Source code in graphiq/circuit/ops.py
class InputOutputOperationBase(OperationBase):
    """
    This is used as a base class for our Input and Output Operations. These operations largely act as "dummy Operations"
    signalling the input / output of a state (useful for circuit DAG representation, for example)
    """

    # IO Operations don't need openqasm representations, since they are dummy gates useful to circuit representation
    # and do not actually modify the circuit output
    _openqasm_info = oq_lib.empty_info()

    def __init__(self, register, reg_type, noise=nm.NoNoise()):
        """
        Creates an IO base class Operation

        :param register: the register/qubit which this I/O operation acts on
        :type register: int OR tuple (of ints, length 2)
        :param reg_type: the input/output is for a quantum photonic register if 'p',
                         for a quantum emitter register if 'e',
                         and for a classical register if 'c'
        :type reg_type: str
        :param noise: Noise model
        :type noise: graphiq.noise.noise_models.NoiseBase
        :return: nothing
        :rtype: None
        """
        if reg_type == "p" or reg_type == "e":
            super().__init__(
                q_registers=(register,), q_registers_type=(reg_type,), noise=noise
            )
        elif reg_type == "c":
            super().__init__(c_registers=(register,), noise=noise)
        else:
            raise ValueError(
                "Register type must be either quantum photonic (reg_type='p'), "
                "quantum emitter (reg_type='e'), or classical (reg_type='c')"
            )
        self.reg_type = reg_type
        self.register = register

    @OperationBase.q_registers.setter
    def q_registers(self, q_reg):
        """
        Handle to modify the register-qubit pairs on which the operation acts. This also automatically updates the
        self.register field, if the I/O is quantum

        :param q_reg: the new q_register value to set (if any)
        :raises ValueError: if the new q_reg object does not match the length of self.q_registers (Operations
                           should not have variable register numbers)
        :return: function returns nothing
        :rtype: None
        """
        self._update_q_reg(q_reg)
        self.register = q_reg[0]

    @OperationBase.c_registers.setter
    def c_registers(self, c_reg):
        """
        Handle to modify the register-cbit pairs on which the operation acts. This also automatically updates the
        self.register field, if the I/O is classical

        :param c_reg: the new c_register value to set (if any)
        :raises ValueError: if the new c_reg object does not match the length of self.c_registers (Operations
                           should not have variable register numbers)
        :return: function returns nothing
        :rtype: None
        """
        self._update_c_reg(c_reg)
        if self.reg_type == "c":
            self.register = c_reg[0]

__init__(register, reg_type, noise=nm.NoNoise())

Creates an IO base class Operation

Parameters:

Name Type Description Default
register int OR tuple (of ints, length 2)

the register/qubit which this I/O operation acts on

required
reg_type str

the input/output is for a quantum photonic register if 'p', for a quantum emitter register if 'e', and for a classical register if 'c'

required
noise graphiq.noise.noise_models.NoiseBase

Noise model

NoNoise()

Returns:

Type Description
None

nothing

Source code in graphiq/circuit/ops.py
def __init__(self, register, reg_type, noise=nm.NoNoise()):
    """
    Creates an IO base class Operation

    :param register: the register/qubit which this I/O operation acts on
    :type register: int OR tuple (of ints, length 2)
    :param reg_type: the input/output is for a quantum photonic register if 'p',
                     for a quantum emitter register if 'e',
                     and for a classical register if 'c'
    :type reg_type: str
    :param noise: Noise model
    :type noise: graphiq.noise.noise_models.NoiseBase
    :return: nothing
    :rtype: None
    """
    if reg_type == "p" or reg_type == "e":
        super().__init__(
            q_registers=(register,), q_registers_type=(reg_type,), noise=noise
        )
    elif reg_type == "c":
        super().__init__(c_registers=(register,), noise=noise)
    else:
        raise ValueError(
            "Register type must be either quantum photonic (reg_type='p'), "
            "quantum emitter (reg_type='e'), or classical (reg_type='c')"
        )
    self.reg_type = reg_type
    self.register = register

c_registers(c_reg)

Handle to modify the register-cbit pairs on which the operation acts. This also automatically updates the self.register field, if the I/O is classical

Parameters:

Name Type Description Default
c_reg

the new c_register value to set (if any)

required

Returns:

Type Description
None

function returns nothing

Raises:

Type Description
ValueError

if the new c_reg object does not match the length of self.c_registers (Operations should not have variable register numbers)

Source code in graphiq/circuit/ops.py
@OperationBase.c_registers.setter
def c_registers(self, c_reg):
    """
    Handle to modify the register-cbit pairs on which the operation acts. This also automatically updates the
    self.register field, if the I/O is classical

    :param c_reg: the new c_register value to set (if any)
    :raises ValueError: if the new c_reg object does not match the length of self.c_registers (Operations
                       should not have variable register numbers)
    :return: function returns nothing
    :rtype: None
    """
    self._update_c_reg(c_reg)
    if self.reg_type == "c":
        self.register = c_reg[0]

q_registers(q_reg)

Handle to modify the register-qubit pairs on which the operation acts. This also automatically updates the self.register field, if the I/O is quantum

Parameters:

Name Type Description Default
q_reg

the new q_register value to set (if any)

required

Returns:

Type Description
None

function returns nothing

Raises:

Type Description
ValueError

if the new q_reg object does not match the length of self.q_registers (Operations should not have variable register numbers)

Source code in graphiq/circuit/ops.py
@OperationBase.q_registers.setter
def q_registers(self, q_reg):
    """
    Handle to modify the register-qubit pairs on which the operation acts. This also automatically updates the
    self.register field, if the I/O is quantum

    :param q_reg: the new q_register value to set (if any)
    :raises ValueError: if the new q_reg object does not match the length of self.q_registers (Operations
                       should not have variable register numbers)
    :return: function returns nothing
    :rtype: None
    """
    self._update_q_reg(q_reg)
    self.register = q_reg[0]

MeasurementCNOTandReset

Bases: ClassicalControlledPairOperationBase

Measurement-controlled X gate Operation with resetting the control qubit after measurement

Source code in graphiq/circuit/ops.py
class MeasurementCNOTandReset(ClassicalControlledPairOperationBase):
    """
    Measurement-controlled X gate Operation with resetting the control qubit after measurement
    """

    _openqasm_info = oq_lib.measurement_cnot_and_reset()

    def __init__(
        self,
        control: object = 0,
        control_type: object = "e",
        target: object = 0,
        target_type: object = "p",
        c_register: object = 0,
        noise: object = nm.NoNoise(),
    ) -> object:
        super().__init__(control, control_type, target, target_type, c_register, noise)

MeasurementZ

Bases: OperationBase

Z Measurement Operation

Source code in graphiq/circuit/ops.py
class MeasurementZ(OperationBase):
    """
    Z Measurement Operation
    """

    # TODO: maybe create a base class for measurements in the future IFF we also want to support other measurements

    _openqasm_info = oq_lib.z_measurement_info()

    def __init__(self, register=0, reg_type="e", c_register=0, noise=nm.NoNoise()):
        """
        Creates a Z measurement Operation

        :param register: the quantum register on which the measurement is performed
        :type register: int OR tuple (of ints, length 2)
        :param reg_type: 'p' if photonic qubit, 'e' if emitter qubit
        :type reg_type: str
        :param c_register: the classical register to which the measurement result is saved
        :type c_register: int OR tuple (of ints, length 2)
        :param noise: Noise model
        :type noise: src.noise.noise_models.NoiseBase
        :return: this function returns nothing
        :rtype: None
        """
        super().__init__(
            q_registers=(register,),
            q_registers_type=(reg_type,),
            c_registers=(c_register,),
            noise=noise,
        )

        self.register = register
        self.reg_type = reg_type
        self.c_register = c_register
        self.add_labels("one-qubit")

    @OperationBase.q_registers.setter
    def q_registers(self, q_reg):
        """
        Handle to modify the register-qubit pair which is measured. This also automatically updates the
        self.register field

        :param q_reg: the new q_register value to set
        :raises ValueError: if the new q_reg object does not have a length of 1
        :return: function returns nothing
        :rtype: None
        """
        self._update_q_reg(q_reg)
        self.register = q_reg[0]

    @OperationBase.c_registers.setter
    def c_registers(self, c_reg):
        """
        Handle to modify the register-cbit pair to which measurements are saved. This also automatically updates the
        self.c_register field

        :param c_reg: the new c_register value to set
        :raises ValueError: if the new c_reg object does not have a length of 1
        :return: function returns nothing
        :rtype: None
        """
        self._update_c_reg(c_reg)
        self.c_register = c_reg[0]

__init__(register=0, reg_type='e', c_register=0, noise=nm.NoNoise())

Creates a Z measurement Operation

Parameters:

Name Type Description Default
register int OR tuple (of ints, length 2)

the quantum register on which the measurement is performed

0
reg_type str

'p' if photonic qubit, 'e' if emitter qubit

'e'
c_register int OR tuple (of ints, length 2)

the classical register to which the measurement result is saved

0
noise src.noise.noise_models.NoiseBase

Noise model

NoNoise()

Returns:

Type Description
None

this function returns nothing

Source code in graphiq/circuit/ops.py
def __init__(self, register=0, reg_type="e", c_register=0, noise=nm.NoNoise()):
    """
    Creates a Z measurement Operation

    :param register: the quantum register on which the measurement is performed
    :type register: int OR tuple (of ints, length 2)
    :param reg_type: 'p' if photonic qubit, 'e' if emitter qubit
    :type reg_type: str
    :param c_register: the classical register to which the measurement result is saved
    :type c_register: int OR tuple (of ints, length 2)
    :param noise: Noise model
    :type noise: src.noise.noise_models.NoiseBase
    :return: this function returns nothing
    :rtype: None
    """
    super().__init__(
        q_registers=(register,),
        q_registers_type=(reg_type,),
        c_registers=(c_register,),
        noise=noise,
    )

    self.register = register
    self.reg_type = reg_type
    self.c_register = c_register
    self.add_labels("one-qubit")

c_registers(c_reg)

Handle to modify the register-cbit pair to which measurements are saved. This also automatically updates the self.c_register field

Parameters:

Name Type Description Default
c_reg

the new c_register value to set

required

Returns:

Type Description
None

function returns nothing

Raises:

Type Description
ValueError

if the new c_reg object does not have a length of 1

Source code in graphiq/circuit/ops.py
@OperationBase.c_registers.setter
def c_registers(self, c_reg):
    """
    Handle to modify the register-cbit pair to which measurements are saved. This also automatically updates the
    self.c_register field

    :param c_reg: the new c_register value to set
    :raises ValueError: if the new c_reg object does not have a length of 1
    :return: function returns nothing
    :rtype: None
    """
    self._update_c_reg(c_reg)
    self.c_register = c_reg[0]

q_registers(q_reg)

Handle to modify the register-qubit pair which is measured. This also automatically updates the self.register field

Parameters:

Name Type Description Default
q_reg

the new q_register value to set

required

Returns:

Type Description
None

function returns nothing

Raises:

Type Description
ValueError

if the new q_reg object does not have a length of 1

Source code in graphiq/circuit/ops.py
@OperationBase.q_registers.setter
def q_registers(self, q_reg):
    """
    Handle to modify the register-qubit pair which is measured. This also automatically updates the
    self.register field

    :param q_reg: the new q_register value to set
    :raises ValueError: if the new q_reg object does not have a length of 1
    :return: function returns nothing
    :rtype: None
    """
    self._update_q_reg(q_reg)
    self.register = q_reg[0]

OneQubitGateWrapper

Bases: OneQubitOperationBase

This wrapper class allows us to compose a list of one-qubit operation and treat them as a single component within the circuit (this allows us, for example, to create every local Clifford gate with other gates, without having to separately implement every combination within the compiler)

Source code in graphiq/circuit/ops.py
class OneQubitGateWrapper(OneQubitOperationBase):
    """
    This wrapper class allows us to compose a list of one-qubit operation and treat them as a single component
    within the circuit (this allows us, for example, to create every local Clifford gate with other gates, without
    having to separately implement every combination within the compiler)
    """

    def __init__(self, operations: list, register=0, reg_type="e", noise=nm.NoNoise()):
        if isinstance(noise, nm.NoNoise):
            noise = len(operations) * [nm.NoNoise()]
        super().__init__(register, reg_type, noise)
        if len(operations) == 0:
            raise ValueError(
                "Operation list for the single qubit gate wrapper must be of length 1 or more"
            )
        for op_class in operations:
            assert issubclass(op_class, OneQubitOperationBase)
            # can only contain base classes
            assert not isinstance(op_class, OneQubitGateWrapper)
        self.operations = operations
        self._openqasm_info = oq_lib.single_qubit_wrapper_info(operations)

    def unwrap(self):
        """
        Unwraps the Operation into a list of sub-operations (this can be useful for any operations which are composed
        of multiple other operations) in the reverse order, which corresponds to the order of applying gates

        :return: a sequence of base operations (i.e. operations which are not compositions of other operations)
        :rtype: list
        """
        if isinstance(self.noise, list):
            assert len(self.noise) == len(self.operations)

            gates = [
                self.operations[i](
                    register=self.register, reg_type=self.reg_type, noise=self.noise[i]
                )
                for i in range(len(self.operations))
            ]
        else:
            gates = [
                self.operations[i](
                    register=self.register, reg_type=self.reg_type, noise=nm.NoNoise()
                )
                for i in range(len(self.operations))
            ]
            noise = Identity(
                register=self.register, reg_type=self.reg_type, noise=self.noise
            )
            if self.noise.noise_parameters["After gate"]:
                gates.insert(0, noise)
            else:
                gates.append(noise)
        return gates[::-1]

    def openqasm_info(self):
        return self._openqasm_info

unwrap()

Unwraps the Operation into a list of sub-operations (this can be useful for any operations which are composed of multiple other operations) in the reverse order, which corresponds to the order of applying gates

Returns:

Type Description
list

a sequence of base operations (i.e. operations which are not compositions of other operations)

Source code in graphiq/circuit/ops.py
def unwrap(self):
    """
    Unwraps the Operation into a list of sub-operations (this can be useful for any operations which are composed
    of multiple other operations) in the reverse order, which corresponds to the order of applying gates

    :return: a sequence of base operations (i.e. operations which are not compositions of other operations)
    :rtype: list
    """
    if isinstance(self.noise, list):
        assert len(self.noise) == len(self.operations)

        gates = [
            self.operations[i](
                register=self.register, reg_type=self.reg_type, noise=self.noise[i]
            )
            for i in range(len(self.operations))
        ]
    else:
        gates = [
            self.operations[i](
                register=self.register, reg_type=self.reg_type, noise=nm.NoNoise()
            )
            for i in range(len(self.operations))
        ]
        noise = Identity(
            register=self.register, reg_type=self.reg_type, noise=self.noise
        )
        if self.noise.noise_parameters["After gate"]:
            gates.insert(0, noise)
        else:
            gates.append(noise)
    return gates[::-1]

OneQubitOperationBase

Bases: OperationBase

This is used as a base class for any one-qubit operation (one-qubit operations should all depend on a single parameter, "register"

Source code in graphiq/circuit/ops.py
class OneQubitOperationBase(OperationBase):
    """
    This is used as a base class for any one-qubit operation (one-qubit operations should
    all depend on a single parameter, "register"
    """

    def __init__(self, register, reg_type, noise=nm.NoNoise()):
        """
        Creates a one-qubit operation base class object

        :param register: the (quantum) register on which the single-qubit operation acts
        :type register: int OR tuple (of ints, length 2)
        :param reg_type: 'e' if emitter qubit, 'p' if a photonic qubit
        :type reg_type: str
        :param noise: Noise model
        :type noise: graphiq.noise.noise_models.NoiseBase
        :return: nothing
        :rtype: None
        """
        super().__init__(
            q_registers=(register,), q_registers_type=(reg_type,), noise=noise
        )
        self.register = register
        self.reg_type = reg_type
        self.add_labels("one-qubit")

    @OperationBase.q_registers.setter
    def q_registers(self, q_reg):
        """
        Handle to modify the register-qubit pairs on which the operation acts. This also automatically updates the
        self.register field

        :param q_reg: the new q_register value to set
        :raises ValueError: if the new q_reg object does not have a length of 1
        :return: nothing
        :rtype: None
        """
        self._update_q_reg(q_reg)
        self.register = q_reg[0]

__init__(register, reg_type, noise=nm.NoNoise())

Creates a one-qubit operation base class object

Parameters:

Name Type Description Default
register int OR tuple (of ints, length 2)

the (quantum) register on which the single-qubit operation acts

required
reg_type str

'e' if emitter qubit, 'p' if a photonic qubit

required
noise graphiq.noise.noise_models.NoiseBase

Noise model

NoNoise()

Returns:

Type Description
None

nothing

Source code in graphiq/circuit/ops.py
def __init__(self, register, reg_type, noise=nm.NoNoise()):
    """
    Creates a one-qubit operation base class object

    :param register: the (quantum) register on which the single-qubit operation acts
    :type register: int OR tuple (of ints, length 2)
    :param reg_type: 'e' if emitter qubit, 'p' if a photonic qubit
    :type reg_type: str
    :param noise: Noise model
    :type noise: graphiq.noise.noise_models.NoiseBase
    :return: nothing
    :rtype: None
    """
    super().__init__(
        q_registers=(register,), q_registers_type=(reg_type,), noise=noise
    )
    self.register = register
    self.reg_type = reg_type
    self.add_labels("one-qubit")

q_registers(q_reg)

Handle to modify the register-qubit pairs on which the operation acts. This also automatically updates the self.register field

Parameters:

Name Type Description Default
q_reg

the new q_register value to set

required

Returns:

Type Description
None

nothing

Raises:

Type Description
ValueError

if the new q_reg object does not have a length of 1

Source code in graphiq/circuit/ops.py
@OperationBase.q_registers.setter
def q_registers(self, q_reg):
    """
    Handle to modify the register-qubit pairs on which the operation acts. This also automatically updates the
    self.register field

    :param q_reg: the new q_register value to set
    :raises ValueError: if the new q_reg object does not have a length of 1
    :return: nothing
    :rtype: None
    """
    self._update_q_reg(q_reg)
    self.register = q_reg[0]

OperationBase

Bases: ABC

Base class from which operations will inherit

Source code in graphiq/circuit/ops.py
class OperationBase(ABC):
    """
    Base class from which operations will inherit
    """

    _openqasm_info = None  # This is the information necessary to add our Operation into an openQASM script

    # If _openqasm_info is None, a given operation cannot be added to openQASM

    def __init__(
        self,
        q_registers=tuple(),
        q_registers_type=tuple(),
        c_registers=tuple(),
        noise=nm.NoNoise(),
        params=tuple(),
        param_info=dict(),
    ):
        """
        Creates an Operation base object (which is largely responsible for holding the registers on which
        an operation act--this provides a consistent API for the circuit class to use when dealing with
        any arbitrary operation).

        :param q_registers: (a, ..., b) indices (indicating registers a, ..., b) OR
                            ((a, b), ...) indicating photonic qubit b of register a OR
                            any combination of (reg, bit) notation and reg notation
                            These tuples can be of length 1 or empty as well, depending on the number
                            of registers the gate requires
        :type q_registers: tuple (tuple of: integers OR tuples of length 2)
        :param q_registers_type: tuple of strings, each is either 'p' (photonic qubit) or 'e' (emitter qubit)
        :type q_registers: tuple (of str)
        :param c_registers: same as photon/emitter_registers, but for the classical registers
        :type c_registers: tuple (tuple of: integers OR tuples of length 2)
        :param noise: Noise model
        :type noise: graphiq.noise.noise_models.NoiseBase
        :param params: an ordered list of parameter values for a parameterized gate
        :type params: tuple
        :param param_info: a dictionary that specifies all the information regarding parameters
        :type param_info: dict
        :raises AssertionError: if photon_register, emitter_register, c_registers are not tuples,
                               OR if the elements of the tuple do not correspond to the notation described above
        :return: nothing
        :rtype: None
        """
        assert isinstance(q_registers, tuple)
        assert isinstance(q_registers_type, tuple)
        assert len(q_registers) == len(q_registers_type)
        assert isinstance(c_registers, tuple)

        for q, reg_type in zip(q_registers, q_registers_type):
            assert isinstance(q, int) or (
                isinstance(q, tuple)
                and len(q) == 2
                and isinstance(q[0], int)
                and isinstance(q[1], int)
            ), f"Invalid photon_registers: photon_registers tuple must only contain tuples of length 2 or integers"
            assert reg_type == "e" or reg_type == "p"

        for c in c_registers:
            assert isinstance(c, int) or (
                isinstance(c, tuple)
                and len(c) == 2
                and isinstance(c[0], int)
                and isinstance(c[1], int)
            ), f"Invalid c_register: c_register tuple must only contain tuples of length 2 or integers"

        self._q_registers = q_registers
        self._q_registers_type = q_registers_type
        self._c_registers = c_registers
        self._labels = []
        self.noise = noise
        self.params = params
        self.param_info = param_info

    @classmethod
    def openqasm_info(cls):
        """
        Returns the information needed to generate an openQASM script using this operation
        (needed imports, how to define a gate, how to use a gate), packaged as a single object.
        This is used by the Circuit classes to generate openQASM scripts when needed

        :return: the operation class's openqasm information (possibly None)
        """
        if cls._openqasm_info is None:
            raise ValueError("Operation does not have an openQASM translation")
        return cls._openqasm_info

    @property
    def q_registers(self):
        """
        Returns the quantum registers tuple of the Operation class

        :return: the registers tuple
        :rtype: tuple
        """
        return self._q_registers

    @property
    def q_registers_type(self):
        """
        Returns the quantum registers types ('e' for emitter, 'p' for photons) tuple of the Operation class

        :return: the registers type
        :rtype: tuple
        """
        return self._q_registers_type

    @property
    def c_registers(self):
        """
        Returns the c_registers tuple of the Operation class

        :return: the c_registers_tuple
        :rtype: tuple
        """
        return self._c_registers

    @q_registers.setter
    def q_registers(self, q_reg):
        """
        Allows us to change the emitter_registers object on which an Operation acts
        This should only be used by the circuit class!
        Subclass-specific implementations of this function also update other
        register-related fields (e.g. register, target, control) automatically
        when photon_registers is updated

        :param q_reg: new emitter_register which the operation should target
        :type q_reg: tuple
        :raises ValueError: if the new q_reg object does not match the length of self.emitter_registers (Operations
                           should not have variable register numbers)
        :return: function returns nothing
        :rtype: None
        """
        self._update_q_reg(q_reg)

    @c_registers.setter
    def c_registers(self, c_reg):
        """
        Allows us to change the c_registers object on which an Operation acts
        This should only be used by the circuit class!
        Subclass-specific implementations of this function also update other
        register-related fields (e.g. c_register) automatically
        when c_registers is updated

        :param c_reg: new c_register which the operation should target
        :type c_reg: tuple
        :raises ValueError: if the new q_reg object does not match the length of self.q_registers (Operations
                           should not have variable register numbers)
        :return: function returns nothing
        :rtype: None
        """
        self._update_c_reg(c_reg)

    @q_registers_type.setter
    def q_registers_type(self, q_regs_type):
        self._q_registers_type = q_regs_type

    @property
    def labels(self):
        """
        Returns the list of labels of this operation

        :return: list of labels
        :rtype: list[str]
        """
        return self._labels

    @labels.setter
    def labels(self, new_labels):
        """
        Sets the labels to be new_labels

        :return: nothing
        :rtype: None
        """
        self._labels = new_labels

    def add_labels(self, new_labels):
        """
        Adds new labels to existing labels

        :return: nothing
        :rtype: None
        """
        if isinstance(new_labels, list):
            self._labels += new_labels
        else:
            self._labels.append(new_labels)

    def unwrap(self):
        """
        Unwraps the Operation into a list of sub-operations (this can be useful for any operations which are composed
        of multiple other operations) in the reverse order, which corresponds to the order of applying operations.

        :return: a sequence of base operations (i.e. operations which are not compositions of other operations)
        :rtype: list
        """
        return [self][::-1]

    def _update_q_reg(self, q_reg):
        """
        Helper function to verify the validity of the new q_registers tuple and to update the fields
        This is broken into a separate function because subclasses will need to use this as well,
        and using the super() keyword gets messy with class properties

        :param q_reg: new emitter_register which the operation should target
        :type q_reg: tuple
        :raises ValueError: if the new q_reg object does not match the length of self.emitter_registers (Operations
                           should not have variable register numbers)
        :return: function returns nothing
        :rtype: None
        """
        if len(q_reg) != len(self._q_registers):
            raise ValueError(
                f"The number of quantum registers on which the operation acts cannot be changed!"
            )
        self._q_registers = q_reg

    def _update_c_reg(self, c_reg):
        """
        Helper function to verify the validity of the new c_registers tuple and to update the fields
        This is broken into a separate function because subclasses will need to use this as well,
        and using the super() keyword gets messy with class properties

        :param c_reg: new c_register which the operation should target
        :type c_reg: tuple
        :raises ValueError: if the new c_reg object does not match the length of self.c_registers (Operations
                           should not have variable register numbers)
        :return: function returns nothing
        :rtype: None
        """
        if len(c_reg) != len(self._c_registers):
            raise ValueError(
                f"The number of classical registers on which the operation acts cannot be changed!"
            )
        self._c_registers = c_reg

    def parse_q_reg_types(self):
        """
        Find a proper string description of the register types relevant for this operation

        :raises ValueError: if the quantum register type is not supported
        :return: a string description
        :rtype: str
        """
        type_description = ""
        for i in range(len(self.q_registers_type)):
            if self.q_registers_type[i] == "e":
                type_description += "Emitter-"
            elif self.q_registers_type[i] == "p":
                type_description += "Photonic-"
            else:
                raise ValueError("Detected a non-supported quantum register type.")

        return type_description[:-1]

    @staticmethod
    def _validate_param_info(param_info, n_params):
        """
        Validate the param_info

        :param param_info: param_info for a gate
        :type param_info: None or dict
        :param n_params: number of parameters
        :type n_params: int
        :return: whether the param_info is valid
        :rtype: bool
        """
        if param_info is None:
            return True
        else:
            if not isinstance(param_info, dict):
                return False
            else:
                if ("bounds" not in param_info.keys()) or (
                    "labels" not in param_info.keys()
                ):
                    return False
                else:
                    return (
                        len(param_info["bounds"]) == n_params
                        and len(param_info["labels"]) == n_params
                    )

c_registers property writable

Returns the c_registers tuple of the Operation class

Returns:

Type Description
tuple

the c_registers_tuple

labels property writable

Returns the list of labels of this operation

Returns:

Type Description
list[str]

list of labels

q_registers property writable

Returns the quantum registers tuple of the Operation class

Returns:

Type Description
tuple

the registers tuple

q_registers_type property writable

Returns the quantum registers types ('e' for emitter, 'p' for photons) tuple of the Operation class

Returns:

Type Description
tuple

the registers type

__init__(q_registers=tuple(), q_registers_type=tuple(), c_registers=tuple(), noise=nm.NoNoise(), params=tuple(), param_info=dict())

Creates an Operation base object (which is largely responsible for holding the registers on which an operation act--this provides a consistent API for the circuit class to use when dealing with any arbitrary operation).

Parameters:

Name Type Description Default
q_registers tuple (tuple of: integers OR tuples of length 2)

(a, ..., b) indices (indicating registers a, ..., b) OR ((a, b), ...) indicating photonic qubit b of register a OR any combination of (reg, bit) notation and reg notation These tuples can be of length 1 or empty as well, depending on the number of registers the gate requires

tuple()
q_registers_type

tuple of strings, each is either 'p' (photonic qubit) or 'e' (emitter qubit)

tuple()
c_registers tuple (tuple of: integers OR tuples of length 2)

same as photon/emitter_registers, but for the classical registers

tuple()
noise graphiq.noise.noise_models.NoiseBase

Noise model

NoNoise()
params tuple

an ordered list of parameter values for a parameterized gate

tuple()
param_info dict

a dictionary that specifies all the information regarding parameters

dict()

Returns:

Type Description
None

nothing

Raises:

Type Description
AssertionError

if photon_register, emitter_register, c_registers are not tuples, OR if the elements of the tuple do not correspond to the notation described above

Source code in graphiq/circuit/ops.py
def __init__(
    self,
    q_registers=tuple(),
    q_registers_type=tuple(),
    c_registers=tuple(),
    noise=nm.NoNoise(),
    params=tuple(),
    param_info=dict(),
):
    """
    Creates an Operation base object (which is largely responsible for holding the registers on which
    an operation act--this provides a consistent API for the circuit class to use when dealing with
    any arbitrary operation).

    :param q_registers: (a, ..., b) indices (indicating registers a, ..., b) OR
                        ((a, b), ...) indicating photonic qubit b of register a OR
                        any combination of (reg, bit) notation and reg notation
                        These tuples can be of length 1 or empty as well, depending on the number
                        of registers the gate requires
    :type q_registers: tuple (tuple of: integers OR tuples of length 2)
    :param q_registers_type: tuple of strings, each is either 'p' (photonic qubit) or 'e' (emitter qubit)
    :type q_registers: tuple (of str)
    :param c_registers: same as photon/emitter_registers, but for the classical registers
    :type c_registers: tuple (tuple of: integers OR tuples of length 2)
    :param noise: Noise model
    :type noise: graphiq.noise.noise_models.NoiseBase
    :param params: an ordered list of parameter values for a parameterized gate
    :type params: tuple
    :param param_info: a dictionary that specifies all the information regarding parameters
    :type param_info: dict
    :raises AssertionError: if photon_register, emitter_register, c_registers are not tuples,
                           OR if the elements of the tuple do not correspond to the notation described above
    :return: nothing
    :rtype: None
    """
    assert isinstance(q_registers, tuple)
    assert isinstance(q_registers_type, tuple)
    assert len(q_registers) == len(q_registers_type)
    assert isinstance(c_registers, tuple)

    for q, reg_type in zip(q_registers, q_registers_type):
        assert isinstance(q, int) or (
            isinstance(q, tuple)
            and len(q) == 2
            and isinstance(q[0], int)
            and isinstance(q[1], int)
        ), f"Invalid photon_registers: photon_registers tuple must only contain tuples of length 2 or integers"
        assert reg_type == "e" or reg_type == "p"

    for c in c_registers:
        assert isinstance(c, int) or (
            isinstance(c, tuple)
            and len(c) == 2
            and isinstance(c[0], int)
            and isinstance(c[1], int)
        ), f"Invalid c_register: c_register tuple must only contain tuples of length 2 or integers"

    self._q_registers = q_registers
    self._q_registers_type = q_registers_type
    self._c_registers = c_registers
    self._labels = []
    self.noise = noise
    self.params = params
    self.param_info = param_info

add_labels(new_labels)

Adds new labels to existing labels

Returns:

Type Description
None

nothing

Source code in graphiq/circuit/ops.py
def add_labels(self, new_labels):
    """
    Adds new labels to existing labels

    :return: nothing
    :rtype: None
    """
    if isinstance(new_labels, list):
        self._labels += new_labels
    else:
        self._labels.append(new_labels)

openqasm_info() classmethod

Returns the information needed to generate an openQASM script using this operation (needed imports, how to define a gate, how to use a gate), packaged as a single object. This is used by the Circuit classes to generate openQASM scripts when needed

Returns:

Type Description

the operation class's openqasm information (possibly None)

Source code in graphiq/circuit/ops.py
@classmethod
def openqasm_info(cls):
    """
    Returns the information needed to generate an openQASM script using this operation
    (needed imports, how to define a gate, how to use a gate), packaged as a single object.
    This is used by the Circuit classes to generate openQASM scripts when needed

    :return: the operation class's openqasm information (possibly None)
    """
    if cls._openqasm_info is None:
        raise ValueError("Operation does not have an openQASM translation")
    return cls._openqasm_info

parse_q_reg_types()

Find a proper string description of the register types relevant for this operation

Returns:

Type Description
str

a string description

Raises:

Type Description
ValueError

if the quantum register type is not supported

Source code in graphiq/circuit/ops.py
def parse_q_reg_types(self):
    """
    Find a proper string description of the register types relevant for this operation

    :raises ValueError: if the quantum register type is not supported
    :return: a string description
    :rtype: str
    """
    type_description = ""
    for i in range(len(self.q_registers_type)):
        if self.q_registers_type[i] == "e":
            type_description += "Emitter-"
        elif self.q_registers_type[i] == "p":
            type_description += "Photonic-"
        else:
            raise ValueError("Detected a non-supported quantum register type.")

    return type_description[:-1]

unwrap()

Unwraps the Operation into a list of sub-operations (this can be useful for any operations which are composed of multiple other operations) in the reverse order, which corresponds to the order of applying operations.

Returns:

Type Description
list

a sequence of base operations (i.e. operations which are not compositions of other operations)

Source code in graphiq/circuit/ops.py
def unwrap(self):
    """
    Unwraps the Operation into a list of sub-operations (this can be useful for any operations which are composed
    of multiple other operations) in the reverse order, which corresponds to the order of applying operations.

    :return: a sequence of base operations (i.e. operations which are not compositions of other operations)
    :rtype: list
    """
    return [self][::-1]

Output

Bases: InputOutputOperationBase

Input Operation. Serves as a placeholder in the circuit so that we know that this is the final operation on a qubit/cbit (i.e. there are no subsequent operations on it)

Source code in graphiq/circuit/ops.py
class Output(InputOutputOperationBase):
    """
    Input Operation. Serves as a placeholder in the circuit so that we know that this is the final operation on a
    qubit/cbit (i.e. there are no subsequent operations on it)
    """

    def __init__(self, register=None, reg_type="e"):
        super().__init__(register, reg_type=reg_type)

ParameterizedControlledRotationQubit

Bases: ControlledPairOperationBase

Parameterized two qubit controlled gate,

Source code in graphiq/circuit/ops.py
class ParameterizedControlledRotationQubit(ControlledPairOperationBase):
    """
    Parameterized two qubit controlled gate,
    """

    _openqasm_info = (
        oq_lib.cparameterized_info()
    )  # todo, change to appropriate openQASM info

    def __init__(
        self,
        control=0,
        control_type="e",
        target=0,
        target_type="e",
        noise=nm.NoNoise(),
        params=None,
        param_info=None,
    ):
        super().__init__(control, control_type, target, target_type, noise)
        if params is None:
            params = (0.0, 0.0, 0.0)
        else:
            if len(params) != 3:
                raise ValueError("Length of params must be 3")

        if param_info is None:
            param_info = {
                "bounds": ((-np.pi, np.pi), (-np.pi, np.pi), (-np.pi, np.pi)),
                "labels": ("theta", "phi", "lambda"),
            }
        else:
            if not self._validate_param_info(param_info, 3):
                raise ValueError("The data format of param_info is invalid.")

        self.params = params
        self.param_info = param_info

ParameterizedOneQubitRotation

Bases: OneQubitOperationBase

Parameterized one qubit rotation.

Source code in graphiq/circuit/ops.py
class ParameterizedOneQubitRotation(OneQubitOperationBase):
    """
    Parameterized one qubit rotation.
    """

    _openqasm_info = (
        oq_lib.parameterized_info()
    )  # todo, change to appropriate openQASM info

    def __init__(
        self, register=0, reg_type="e", noise=nm.NoNoise(), params=None, param_info=None
    ):
        super().__init__(register, reg_type, noise)

        if params is None:
            params = (0.0, 0.0, 0.0)

        else:
            if len(params) != 3:
                raise ValueError("Length of params must be 3")

        if param_info is None:
            param_info = {
                "bounds": ((-np.pi, np.pi), (-np.pi, np.pi), (-np.pi, np.pi)),
                "labels": ("theta", "phi", "lambda"),
            }
        else:
            if not self._validate_param_info(param_info, 3):
                raise ValueError("The data format of param_info is invalid.")

        self.params = params
        self.param_info = param_info

Phase

Bases: OneQubitOperationBase

Phase gate operation, P = diag(1, i)

Source code in graphiq/circuit/ops.py
class Phase(OneQubitOperationBase):
    """
    Phase gate operation, P = diag(1, i)
    """

    _openqasm_info = oq_lib.phase_info()

    def __init__(self, register=0, reg_type="e", noise=nm.NoNoise()):
        super().__init__(register, reg_type, noise)

PhaseDagger

Bases: OneQubitOperationBase

Phase gate operation, P_dag = diag(1, -i)

Source code in graphiq/circuit/ops.py
class PhaseDagger(OneQubitOperationBase):
    """
    Phase gate operation, P_dag = diag(1, -i)
    """

    _openqasm_info = oq_lib.phase_dagger_info()

    def __init__(self, register=0, reg_type="e", noise=nm.NoNoise()):
        super().__init__(register, reg_type, noise)

RX

Bases: ParameterizedOneQubitRotation

Rotation around the X axis

Source code in graphiq/circuit/ops.py
class RX(ParameterizedOneQubitRotation):
    """
    Rotation around the X axis
    """

    _openqasm_info = oq_lib.rx_info()

    def __init__(
        self, register=0, reg_type="e", noise=nm.NoNoise(), params=None, param_info=None
    ):
        super().__init__(register, reg_type, noise)

        if params is None:
            params = (0.0,)

        else:
            if len(params) != 1:
                raise ValueError("Length of params must be 1")

        if param_info is None:
            param_info = {
                "bounds": (-np.pi, np.pi),
                "labels": "theta",
            }
        else:
            if not self._validate_param_info(param_info, 1):
                raise ValueError("The data format of param_info is invalid.")

        self.params = params
        self.param_info = param_info

RY

Bases: ParameterizedOneQubitRotation

Rotation around the Y axis

Source code in graphiq/circuit/ops.py
class RY(ParameterizedOneQubitRotation):
    """
    Rotation around the Y axis
    """

    _openqasm_info = oq_lib.ry_info()

    def __init__(
        self, register=0, reg_type="e", noise=nm.NoNoise(), params=None, param_info=None
    ):
        super().__init__(register, reg_type, noise)

        if params is None:
            params = (0.0,)
        else:
            if len(params) != 1:
                raise ValueError("Length of params must be 1")
        if param_info is None:
            param_info = {
                "bounds": (-np.pi, np.pi),
                "labels": "theta",
            }
        else:
            if not self._validate_param_info(param_info, 1):
                raise ValueError("The data format of param_info is invalid.")

        self.params = params
        self.param_info = param_info

RZ

Bases: ParameterizedOneQubitRotation

Rotation around the Z axis

Source code in graphiq/circuit/ops.py
class RZ(ParameterizedOneQubitRotation):
    """
    Rotation around the Z axis
    """

    _openqasm_info = oq_lib.rz_info()

    def __init__(
        self, register=0, reg_type="e", noise=nm.NoNoise(), params=None, param_info=None
    ):
        super().__init__(register, reg_type, noise)

        if params is None:
            params = (0.0,)

        else:
            if len(params) != 1:
                raise ValueError("Length of params must be 1")
        if param_info is None:
            param_info = {
                "bounds": (-np.pi, np.pi),
                "labels": "phi",
            }
        else:
            if not self._validate_param_info(param_info, 1):
                raise ValueError("The data format of param_info is invalid.")

        self.params = params
        self.param_info = param_info

SigmaX

Bases: OneQubitOperationBase

Pauli X gate Operation

Source code in graphiq/circuit/ops.py
class SigmaX(OneQubitOperationBase):
    """
    Pauli X gate Operation
    """

    _openqasm_info = oq_lib.sigma_x_info()

    def __init__(self, register=0, reg_type="e", noise=nm.NoNoise()):
        super().__init__(register, reg_type, noise)

SigmaY

Bases: OneQubitOperationBase

Pauli Y gate Operation

Source code in graphiq/circuit/ops.py
class SigmaY(OneQubitOperationBase):
    """
    Pauli Y gate Operation
    """

    _openqasm_info = oq_lib.sigma_y_info()

    def __init__(self, register=0, reg_type="e", noise=nm.NoNoise()):
        super().__init__(register, reg_type, noise)

SigmaZ

Bases: OneQubitOperationBase

Pauli Z gate Operation

Source code in graphiq/circuit/ops.py
class SigmaZ(OneQubitOperationBase):
    """
    Pauli Z gate Operation
    """

    _openqasm_info = oq_lib.sigma_z_info()

    def __init__(self, register=0, reg_type="e", noise=nm.NoNoise()):
        super().__init__(register, reg_type, noise)

class_to_name_mapping(class_op)

Function to map operation class to string. It's used to convert circuit object to json.

Parameters:

Name Type Description Default
class_op operation

operation class

required

Returns:

Type Description
Source code in graphiq/circuit/ops.py
def class_to_name_mapping(class_op):
    """
    Function to map operation class to string. It's used to convert circuit object to json.

    :param class_op: operation class
    :type class_op: operation
    :return:
    """
    mapping = {
        CNOT: "CX",
        SigmaX: "x",
        SigmaY: "y",
        SigmaZ: "z",
        Hadamard: "h",
        Phase: "s",
        CZ: "cz",
        ClassicalCNOT: "classical x",
        ClassicalCZ: "classical z",
        MeasurementCNOTandReset: "measurement-controlled x and reset",
    }
    if class_op in mapping:
        return mapping[class_op]
    return None

find_local_clifford_by_matrix(matrix)

Find local Clifford by its matrix representation

Parameters:

Name Type Description Default
matrix numpy.ndarray

a matrix representation of one-qubit Clifford gate

required

Returns:

Type Description
list

the local Clifford gate specified by a list of basic gates it consists of or None if the input matrix is not a valid one-qubit Clifford gate

Raises:

Type Description
ValueError

if the matrix does not correspond to a valid one-qubit Clifford gate.

Source code in graphiq/circuit/ops.py
def find_local_clifford_by_matrix(matrix):
    """
    Find local Clifford by its matrix representation

    :param matrix: a matrix representation of one-qubit Clifford gate
    :type matrix: numpy.ndarray
    :raises ValueError: if the matrix does not correspond to a valid one-qubit Clifford gate.
    :return: the local Clifford gate specified by a list of basic gates it consists of
        or None if the input matrix is not a valid one-qubit Clifford gate
    :rtype: list
    """
    gate_list1, gate_list2 = local_clifford_composition()
    for op1 in gate_list1:
        for op2 in gate_list2:
            matrix1 = local_clifford_to_matrix_map(op1)
            matrix2 = local_clifford_to_matrix_map(op2)
            product_matrix = matrix1 @ matrix2
            if dmf.check_equivalent_unitaries(matrix, product_matrix):
                return op1 + op2

    raise ValueError("Invalid one-qubit Clifford gate.")

local_clifford_to_matrix_map(gate)

Find the 2 X 2 matrix corresponding to the local Clifford gate

Parameters:

Name Type Description Default
gate list | a subclass of OperationBase

the local Clifford gate

required

Returns:

Type Description
numpy.ndarray

the 2 X 2 matrix corresponding to the local Clifford gate

Source code in graphiq/circuit/ops.py
def local_clifford_to_matrix_map(gate):
    """
    Find the 2 X 2 matrix corresponding to the local Clifford gate

    :param gate: the local Clifford gate
    :type gate: list or a subclass of OperationBase
    :return: the 2 X 2 matrix corresponding to the local Clifford gate
    :rtype: numpy.ndarray
    """
    mapping = {
        Identity.__name__: np.eye(2),
        Hadamard.__name__: dmf.hadamard(),
        Phase.__name__: dmf.phase(),
        SigmaX.__name__: dmf.sigmax(),
        SigmaY.__name__: dmf.sigmay(),
        SigmaZ.__name__: dmf.sigmaz(),
    }

    if isinstance(gate, list):
        result = np.eye(2)
        for op in gate:
            if op.__name__ in mapping.keys():
                result = result @ mapping[op.__name__]
            else:
                raise ValueError(f"Cannot support the operator of type {op.__name__}")
        return result
    else:
        if gate.__name__ in mapping.keys():
            return mapping[gate.__name__]
        else:
            raise ValueError(f"Cannot support the operator of type {gate.__name__}")

local_cliffords_name_to_matrix_map()

Find all one-qubit local Clifford gates in the matrix representation

Returns:

Type Description
map

all one-qubit local Clifford gates in the matrix representation

Source code in graphiq/circuit/ops.py
def local_cliffords_name_to_matrix_map():
    """
    Find all one-qubit local Clifford gates in the matrix representation

    :return: all one-qubit local Clifford gates in the matrix representation
    :rtype: map
    """
    a, b = local_clifford_composition()

    def gate_matrix(c):
        # where c is a tuple of lists
        matrix1 = local_clifford_to_matrix_map(c[0])
        matrix2 = local_clifford_to_matrix_map(c[1])

        return matrix1 @ matrix2

    return map(gate_matrix, itertools.product(a, b))

name_to_class_map(name)

Maps our openqasm naming scheme to operation classes. Does not handle multi-gate wrappers Does not handle multi-line openqasm components

Parameters:

Name Type Description Default
name str

gate name in openqasm

required

Returns:

Type Description
OperationBase class

the operation class corresponding to the openqasm name if the name is a valid name

Source code in graphiq/circuit/ops.py
def name_to_class_map(name):
    """
    Maps our openqasm naming scheme to operation classes. Does not handle multi-gate wrappers
    Does not handle multi-line openqasm components

    :param name: gate name in openqasm
    :type name: str
    :return: the operation class corresponding to the openqasm name if the name is a valid name
    :rtype: OperationBase class
    """
    mapping = {
        "CX": CNOT,
        "cx": CNOT,
        "x": SigmaX,
        "y": SigmaY,
        "z": SigmaZ,
        "h": Hadamard,
        "s": Phase,
        "p": Phase,
        "cz": CZ,
        "classical x": ClassicalCNOT,
        "classical z": ClassicalCZ,
        "classical reset x": MeasurementCNOTandReset,
    }
    if name in mapping:
        return mapping[name]
    return None

one_qubit_cliffords()

Returns an iterator of single-qubit clifford gates

Returns:

Type Description
map

iterator covering each single-qubit clifford gate

Source code in graphiq/circuit/ops.py
def one_qubit_cliffords():
    """
    Returns an iterator of single-qubit clifford gates

    :return: iterator covering each single-qubit clifford gate
    :rtype: map
    """
    a, b = local_clifford_composition()

    def flatten_gates(c):
        return c[0] + c[1]  # where c is a tuple of lists

    return map(flatten_gates, itertools.product(a, b))

simplify_local_clifford(gate_list)

Simplify the list of basic gates that represents a local Clifford

Parameters:

Name Type Description Default
gate_list list

original list of basic gates that represents a local Clifford

required

Returns:

Type Description
list

simplified list of basic gates that represents the same local Clifford

Source code in graphiq/circuit/ops.py
def simplify_local_clifford(gate_list):
    """
    Simplify the list of basic gates that represents a local Clifford

    :param gate_list: original list of basic gates that represents a local Clifford
    :type gate_list: list
    :return: simplified list of basic gates that represents the same local Clifford
    :rtype: list
    """
    matrix = local_clifford_to_matrix_map(gate_list)

    return find_local_clifford_by_matrix(matrix)

graphiq.circuit.register

Register

Register class object, the class includes a dictionary which map the register type as the key and the register array as the value.

Source code in graphiq/circuit/register.py
class Register:
    """
    Register class object, the class includes a dictionary which map the register type as the key and the register array
    as the value.
    """

    def __init__(self, reg_dict, is_multi_qubit: bool = False):
        """
        Constructor for the register class

        :param reg_dict: register dictionary
        :type reg_dict: dict
        :param is_multi_qubit: variable that indicate support for multi-qubit register
        :type is_multi_qubit: bool
        :return: this function returns nothing
        :rtype: None
        """
        # Check empty data
        if not reg_dict:
            raise ValueError("Register dict can not be None or empty")

        # Check if input data is numerical
        for key in reg_dict:
            if not all([isinstance(item, int) for item in reg_dict[key]]):
                raise ValueError("The input data contains non-numerical value")

        # Check if not multi-qubit register but input value more than 1
        for key in reg_dict:
            if reg_dict[key] and set(reg_dict[key]) != {1} and not is_multi_qubit:
                raise ValueError(
                    f"Register is not multi-qubit register but has value more than 1"
                )

        self._registers = reg_dict
        self.is_multi_qubit = is_multi_qubit

    @property
    def register(self):
        return self._registers.copy()

    def __getitem__(self, key):
        return self._registers[key]

    def __setitem__(self, key, value):
        # Check value is numerical
        if not all([isinstance(item, int) for item in value]):
            raise ValueError("The input data contains non-numerical value")

        # Check if not multi-qubit register but has value more than 1
        if value and set(value) != {1} and not self.is_multi_qubit:
            raise ValueError(f"The register only supports single-qubit registers")
        self._registers[key] = value

    @property
    def n_quantum(self):
        q_sum = 0

        for key in self._registers:
            if key != "c":
                q_sum += len(self._registers[key])
        return q_sum

    def add_register(self, reg_type: str, size: int = 1):
        """
        Function that add a quantum/classical register to the register dict

        :param reg_type: 'p' for a photonic quantum register, 'e' for an emitter quantum register,
                         'c' for a classical register
        :type reg_type: str
        :param size: the new register size
        :type size: int
        :raises ValueError: if new_size is not greater than the current register size
        :return: the index number of the added register
        :rtype: int
        """
        if reg_type not in self._registers:
            raise ValueError(
                f"reg_type must be 'e' (emitter qubit), 'p' (photonic qubit), 'c' (classical bit)"
            )
        if size < 1:
            raise ValueError(f"{reg_type} register size must be at least one")
        if size > 1 and not self.is_multi_qubit:
            raise ValueError(
                f"Can not add register of size {size}, multiple qubit register is not supported"
            )
        self._registers[reg_type].append(size)
        return len(self._registers[reg_type]) - 1

    def expand_register(self, reg_type: str, register: int, new_size: int = 1):
        """
        Function to expand quantum/classical registers

        :param register: the register index of the register to expand
        :type register: int
        :param new_size: the new register size
        :type register: int
        :param reg_type: 'p' for a photonic quantum register, 'e' for an emitter quantum register,
                         'c' for a classical register
        :type reg_type: str
        :raises ValueError: if new_size is not greater than the current register size
        :return: this function returns nothing
        :rtype: None
        """
        if reg_type not in self._registers:
            raise ValueError(
                "reg_type must be 'e' (emitter register), 'p' (photonic register), "
                "or 'c' (classical register)"
            )
        if new_size > 1 and not self.is_multi_qubit:
            raise ValueError(
                f"Can not expand register to size {new_size}, multiple qubit register is not supported"
                f"(they must have a size of 1)"
            )
        curr_reg = self._registers[reg_type]
        curr_size = curr_reg[register]

        if new_size <= curr_size:
            raise ValueError(
                f"New register size {new_size} is not greater than the current size {curr_size}"
            )
        curr_reg[register] = new_size

    def next_register(self, reg_type: str, register: int):
        """
        Provides the index of the next register in the provided register. This allows the user to query
        which register they should add next, should they decide to expand the register

        :param reg_type: indicate register type, can be "p", "e", or "c"
        :type reg_type: str
        :param register: the register index {0, ..., N - 1} for N emitter quantum registers
        :type register: int
        :return: the index of the next register
        :rtype: int (non-negative)
        """
        if reg_type not in self._registers:
            raise ValueError(
                "Register type must be 'p' (quantum photonic), 'e' (quantum emitter), or 'c' (classical)"
            )
        return self._registers[reg_type][register]

__init__(reg_dict, is_multi_qubit=False)

Constructor for the register class

Parameters:

Name Type Description Default
reg_dict dict

register dictionary

required
is_multi_qubit bool

variable that indicate support for multi-qubit register

False

Returns:

Type Description
None

this function returns nothing

Source code in graphiq/circuit/register.py
def __init__(self, reg_dict, is_multi_qubit: bool = False):
    """
    Constructor for the register class

    :param reg_dict: register dictionary
    :type reg_dict: dict
    :param is_multi_qubit: variable that indicate support for multi-qubit register
    :type is_multi_qubit: bool
    :return: this function returns nothing
    :rtype: None
    """
    # Check empty data
    if not reg_dict:
        raise ValueError("Register dict can not be None or empty")

    # Check if input data is numerical
    for key in reg_dict:
        if not all([isinstance(item, int) for item in reg_dict[key]]):
            raise ValueError("The input data contains non-numerical value")

    # Check if not multi-qubit register but input value more than 1
    for key in reg_dict:
        if reg_dict[key] and set(reg_dict[key]) != {1} and not is_multi_qubit:
            raise ValueError(
                f"Register is not multi-qubit register but has value more than 1"
            )

    self._registers = reg_dict
    self.is_multi_qubit = is_multi_qubit

add_register(reg_type, size=1)

Function that add a quantum/classical register to the register dict

Parameters:

Name Type Description Default
reg_type str

'p' for a photonic quantum register, 'e' for an emitter quantum register, 'c' for a classical register

required
size int

the new register size

1

Returns:

Type Description
int

the index number of the added register

Raises:

Type Description
ValueError

if new_size is not greater than the current register size

Source code in graphiq/circuit/register.py
def add_register(self, reg_type: str, size: int = 1):
    """
    Function that add a quantum/classical register to the register dict

    :param reg_type: 'p' for a photonic quantum register, 'e' for an emitter quantum register,
                     'c' for a classical register
    :type reg_type: str
    :param size: the new register size
    :type size: int
    :raises ValueError: if new_size is not greater than the current register size
    :return: the index number of the added register
    :rtype: int
    """
    if reg_type not in self._registers:
        raise ValueError(
            f"reg_type must be 'e' (emitter qubit), 'p' (photonic qubit), 'c' (classical bit)"
        )
    if size < 1:
        raise ValueError(f"{reg_type} register size must be at least one")
    if size > 1 and not self.is_multi_qubit:
        raise ValueError(
            f"Can not add register of size {size}, multiple qubit register is not supported"
        )
    self._registers[reg_type].append(size)
    return len(self._registers[reg_type]) - 1

expand_register(reg_type, register, new_size=1)

Function to expand quantum/classical registers

Parameters:

Name Type Description Default
register int

the register index of the register to expand

required
new_size int

the new register size

1
reg_type str

'p' for a photonic quantum register, 'e' for an emitter quantum register, 'c' for a classical register

required

Returns:

Type Description
None

this function returns nothing

Raises:

Type Description
ValueError

if new_size is not greater than the current register size

Source code in graphiq/circuit/register.py
def expand_register(self, reg_type: str, register: int, new_size: int = 1):
    """
    Function to expand quantum/classical registers

    :param register: the register index of the register to expand
    :type register: int
    :param new_size: the new register size
    :type register: int
    :param reg_type: 'p' for a photonic quantum register, 'e' for an emitter quantum register,
                     'c' for a classical register
    :type reg_type: str
    :raises ValueError: if new_size is not greater than the current register size
    :return: this function returns nothing
    :rtype: None
    """
    if reg_type not in self._registers:
        raise ValueError(
            "reg_type must be 'e' (emitter register), 'p' (photonic register), "
            "or 'c' (classical register)"
        )
    if new_size > 1 and not self.is_multi_qubit:
        raise ValueError(
            f"Can not expand register to size {new_size}, multiple qubit register is not supported"
            f"(they must have a size of 1)"
        )
    curr_reg = self._registers[reg_type]
    curr_size = curr_reg[register]

    if new_size <= curr_size:
        raise ValueError(
            f"New register size {new_size} is not greater than the current size {curr_size}"
        )
    curr_reg[register] = new_size

next_register(reg_type, register)

Provides the index of the next register in the provided register. This allows the user to query which register they should add next, should they decide to expand the register

Parameters:

Name Type Description Default
reg_type str

indicate register type, can be "p", "e", or "c"

required
register int

the register index {0, ..., N - 1} for N emitter quantum registers

required

Returns:

Type Description
int (non-negative)

the index of the next register

Source code in graphiq/circuit/register.py
def next_register(self, reg_type: str, register: int):
    """
    Provides the index of the next register in the provided register. This allows the user to query
    which register they should add next, should they decide to expand the register

    :param reg_type: indicate register type, can be "p", "e", or "c"
    :type reg_type: str
    :param register: the register index {0, ..., N - 1} for N emitter quantum registers
    :type register: int
    :return: the index of the next register
    :rtype: int (non-negative)
    """
    if reg_type not in self._registers:
        raise ValueError(
            "Register type must be 'p' (quantum photonic), 'e' (quantum emitter), or 'c' (classical)"
        )
    return self._registers[reg_type][register]