Skip to content

1. States

graphiq.state.QuantumState

The QuantumState class is the unified API for accessing state representation backends. It contains one state representation.

Source code in graphiq/state.py
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
class QuantumState:
    """
    The QuantumState class is the unified API for accessing state representation backends.
    It contains one state representation.
    """

    def __init__(self, data, rep_type=None, mixed=False):
        """
        Creates the QuantumState class with one initial representation

        :param data: valid data input for "rep_type".
            Density matrices representations support np.ndarray or int inputs
            Stabilizer representations take int or StabilizerTableau
            Graph representations take networkx.Graph
        :type data: list OR numpy.ndarray OR Graph OR nx.Graph or CliffordTableau
        :param rep_type: selected representation to initialize;
            if not specified, the default choice is the density matrix if the number of qubits is
            less than the threshold value or stabilizer otherwise.
        :type rep_type: str
        :param mixed: boolean flag to initialize as a mixed state or not (mainly used for Stabilizer rep_type)
        :type mixed: boolean
        :return: nothing
        :rtype: None
        """

        self.mixed = mixed
        self._rep_type = self._get_rep_type_name(rep_type)
        valid, self.n_qubits = self.validate_data(data)

        if not valid:
            raise TypeError(
                f"Data's type is incorrect. Input data is {data}, which is of type {type(data)}"
            )
        if rep_type is None:
            if (
                self.n_qubits < DENSITY_MATRIX_QUBIT_THRESH
                and DensityMatrix.valid_datatype(data)
            ):
                self._rep_data = self._initialize_dm(data)
            elif Stabilizer.valid_datatype(data):
                self._rep_data = self._initialize_stabilizer(data)
            elif Graph.valid_datatype(data):
                self._rep_data = self._initialize_graph(data)
            elif DensityMatrix.valid_datatype(data):
                raise ValueError(
                    "Data's type is correct for density matrix representation, but state size exceeds the "
                    "recommended size for density matrix representation"
                )
            else:
                raise ValueError(
                    "Data's type is invalid for initializing a QuantumState"
                )

        elif isinstance(rep_type, str):
            self._rep_data = self._initialize_representation(rep_type, data)

        else:
            raise ValueError("passed rep_type argument must be a String")

    @property
    def rep_data(self):
        """
        Representation data

        :return: representation data
        :rtype: DensityMatrix or Stabilizer or Graph
        """
        return self._rep_data

    @rep_data.setter
    def rep_data(self, data):
        self._rep_data = self._initialize_representation(self._rep_type, data)

    @property
    def rep_type(self):
        """
        Representation type

        :return: representation type
        :rtype: str
        """
        return self._rep_type

    @rep_type.setter
    def rep_type(self, new_rep_type):
        self._rep_type = self._get_rep_type_name(new_rep_type)

    def partial_trace(self, keep, dims):
        r"""
        Calculates the partial trace on all state representations which are currently defined

        :param keep:  An array of indices of the spaces to keep. For instance, if the space is
                    $A \times B \times C \times D$, and we want to trace out B and D, keep = [0,2]
        :type keep: list OR numpy.ndarray
        :param dims: An array of the dimensions of each space. For instance,
                    if the space is $A \times B \times C \times D$,
                    dims = [$dim_A$, $dim_B$, $dim_C$, $dim_D$]
        :type dims: list OR numpy.ndarray
        :return: nothing
        :rtype: None
        """

        if self._rep_type == "g":
            raise NotImplementedError(
                "Partial trace not yet implemented on graph state"
            )
        else:
            self._rep_data.partial_trace(keep, dims)

    def show(self, show=True, ax=None):
        """
        Plots the state representation using matplotlib formatting

        :param show: if True, the state representation is plotted. Otherwise, it is drawn but not plotted
        :type show: bool
        :param ax: axis/axes on which to plot the state representation
        :type ax: matplotlib.Axis
        :return: fig, ax (the figure and axes on which data was plotted)
        :rtype: matplotlib.Figure, matplotlib.Axis
        """
        # Use visualizers module
        if self._rep_type == "dm":
            fig, ax = self._rep_data.draw(show=show, style="heat")
        elif self._rep_type == "g":
            fig, ax = self._rep_data.draw(show=show, ax=ax, with_labels=True)
        elif self._rep_type == "s":
            raise NotImplementedError(
                "No visualization tool is implemented for stabilizer backend."
            )
        else:
            raise ValueError("Invalid representation type.")

        return fig, ax

    def _initialize_dm(self, data):
        """
        Initializes a density matrix based on the data

        :param data: either a graph or ndarray matrix
        :type data: Graph OR nx.Graph OR numpy.ndarray
        :raises AssertionError: if the density matrix being initialized does not have self.n_qubits
        """
        if isinstance(data, Graph) or isinstance(data, nx.Graph):
            dm = DensityMatrix.from_graph(data)
        else:
            dm = DensityMatrix(data)

        assert dm.data.shape[0] == dm.data.shape[1] == 2**self.n_qubits
        return dm

    def _initialize_graph(self, data):
        """
        Initializes a graph state based on the data

        :param data: data to construct the Graph representation
        :type data: networkX.Graph
        :raises AssertionError: if the graph being initialized does not have self.n_qubits
        """
        graph = Graph(data)

        assert graph.n_qubits == self.n_qubits, (
            f"Expected {self.n_qubits} qubits, " f"graph rep_type has {graph.n_qubits}"
        )
        return graph

    def _initialize_stabilizer(self, data):
        """
        Initializes a stabilizer state based on the data

        :param data: data to construct the stabilizer state representation
        :type data: Stabilizer or int or CliffordTableau or MixedStabilizer or list[(float, CliffordTableau)]
        """
        if not self.mixed:
            if isinstance(data, Stabilizer):
                return data
            else:
                stabilizer = Stabilizer(data)
        else:
            if isinstance(data, MixedStabilizer):
                return data
            else:
                stabilizer = MixedStabilizer(data)

        assert stabilizer.n_qubits == self.n_qubits, (
            f"Expected {self.n_qubits} qubits, "
            f"Stabilizer representation has {stabilizer.n_qubits}"
        )
        return stabilizer

    def _initialize_representation(self, rep_type, data):
        """
        Helper function to initialize any given representation

        :param rep_type: representation type to initialize
        :type rep_type: str
        :param data: data with which the rep_type should be initialized
        :type data: int OR Graph OR nx.Graph OR numpy.ndarray
        :raises ValueError: if rep_type is invalid
        """
        if rep_type in ("dm", "density matrix"):
            if self.n_qubits > DENSITY_MATRIX_QUBIT_THRESH:
                warnings.warn(
                    UserWarning(
                        "Density matrix is not recommended for a state of this size"
                    )
                )
            return self._initialize_dm(data)
        elif rep_type in ("g", "graph"):
            return self._initialize_graph(data)
        elif rep_type in ("s", "stab", "stabilizer"):
            return self._initialize_stabilizer(data)
        else:
            raise ValueError("Passed rep_type is invalid")

    def _density_to_graph(self, rep):
        """
        Helper function. Convert a density matrix representation to a graph representation.

        :param rep: density matrix representation
        :type rep: DensityMatrix
        :return: graph representation
        :rtype: Graph or MixedGraph
        """
        new_data = rc.density_to_graph(rep.data)
        if isinstance(new_data, list):
            new_rep = MixedGraph(new_data)
        else:
            new_rep = Graph(new_data)
        return new_rep

    def _density_to_stabilizer(self, rep):
        """
        Helper function. Convert a density matrix representation to a stabilizer representation

        :param rep: density matrix representation
        :type rep: DensityMatrix
        :return: stabilizer representation
        :rtype: Stabilizer or MixedStabilizer
        """
        new_data = rc.density_to_stabilizer(rep.data)
        # new_data is of type list[(float, StabilizerTableau)]
        if self.mixed:
            new_tab_list = []
            for p_i, s_i in new_data:
                new_tab_list.append((p_i, CliffordTableau(s_i)))

            new_rep = MixedStabilizer(new_tab_list)
        else:
            new_tableau = CliffordTableau(new_data[0][1])
            new_rep = Stabilizer(new_tableau)
        return new_rep

    def _stabilizer_to_density(self, rep):
        """
        Helper function. Convert a stabilizer representation to density matrix

        :param rep: initial representation
        :type rep: Stabilizer or MixedStabilizer
        :return: a density matrix
        :rtype: DensityMatrix
        """
        if self.mixed:
            data_list = []
            assert isinstance(rep, MixedStabilizer)
            for p_i, t_i in rep.mixture:
                data_list.append((p_i, t_i.to_stabilizer()))
            rho = rc.stabilizer_to_density(data_list)
        else:
            rho = rc.stabilizer_to_density(rep.data.to_stabilizer())
        return DensityMatrix(rho)

    def _stabilizer_to_graph(self, rep):
        """
        Convert a stabilizer representation to a graph representation

        :param rep: stabilizer representation
        :type rep: Stabilizer or MixedStabilizer
        :return: graph representation
        :rtype: Graph or MixedGraph
        """
        if self.mixed:
            assert isinstance(rep, MixedStabilizer)
            data_list = []
            for p_i, t_i in rep.mixture:
                data_list.append((p_i, t_i.to_stabilizer()))
            graph_list = rc.stabilizer_to_graph(data_list)
            return MixedGraph(graph_list)
        else:
            graph_list = rc.stabilizer_to_graph(rep.data)
            return Graph(graph_list[0][1])

    def _graph_to_density(self, rep):
        """
        Helper function. Convert a graph representation to a density matrix representation

        :param rep: graph representation
        :type rep: Graph or MixedGraph
        :return: density matrix representation
        :rtype: DensityMatrix
        """
        if self.mixed:
            assert isinstance(rep, MixedGraph)
            rho = 0
            for p_i, t_i in rep.mixture:
                rho += p_i * rc.graph_to_density(t_i.data)
        else:
            rho = rc.graph_to_density(rep.data)
        return DensityMatrix(rho)

    def _graph_to_stabilizer(self, rep):
        """
        Helper function. Convert a graph representation to a stabilizer representation

        :param rep: graph representation
        :type rep: Graph or MixedGraph
        :return: stabilizer representation
        :rtype: Stabilizer or MixedStabilizer
        """
        new_data = rc.graph_to_stabilizer(rep.data)
        # new_data is of type list[(float, StabilizerTableau)]
        if self.mixed:
            new_tab_list = []
            for p_i, s_i in new_data:
                new_tab_list.append((p_i, CliffordTableau(s_i)))

            new_rep = MixedStabilizer(new_tab_list)
        else:
            new_tableau = CliffordTableau(new_data[0][1])
            new_rep = Stabilizer(new_tableau)
        return new_rep

    def _identity_fun(self, rep):
        return rep

    @staticmethod
    def _get_rep_type_name(rep_type):
        if rep_type in ("s", "stab", "stabilizer"):
            return "s"
        elif rep_type in ("dm", "density matrix"):
            return "dm"
        elif rep_type in ("g", "graph"):
            return "g"
        elif rep_type is None:
            return None
        else:
            raise ValueError(
                f"QuantumState does not support the representation of type {rep_type}"
            )

    def convert_representation(self, new_rep_type):
        """
        Convert to a representation specified by new_rep_type

        :param new_rep_type: new representation type
        :type new_rep_type: str
        :return: nothing
        :rtype: None
        """
        rep_type = self._get_rep_type_name(new_rep_type)
        if rep_type is None:
            raise ValueError("Cannot convert representation to None type")

        conversion_dict = {
            ("dm", "g"): self._density_to_graph,
            ("dm", "s"): self._density_to_stabilizer,
            ("s", "dm"): self._stabilizer_to_density,
            ("s", "g"): self._stabilizer_to_graph,
            ("g", "dm"): self._graph_to_density,
            ("g", "s"): self._graph_to_stabilizer,
            ("dm", "dm"): self._identity_fun,
            ("s", "s"): self._identity_fun,
            ("g", "g"): self._identity_fun,
        }
        if self._rep_type != rep_type:
            if rep_type == "dm" and self.n_qubits > DENSITY_MATRIX_QUBIT_THRESH:
                warnings.warn(
                    UserWarning(
                        "Density matrix is not recommended for a state of this size"
                    )
                )

            tmp_data = self._rep_data
            conversion_func = conversion_dict[(self._rep_type, rep_type)]

            self._rep_data = conversion_func(tmp_data)
            self._rep_type = rep_type

    @classmethod
    def validate_data(cls, data):
        """
        Validate data type for input data

        :param data: input data
        :type data: int or np.ndarray or CliffordTableau or nx.Graph
        :return: True and the number of qubits if the data type is valid
        :rtype: bool, int
        """
        valid = True
        if isinstance(data, int):
            n_qubits = data
        elif isinstance(data, np.ndarray):
            assert (
                data.shape[0] == data.shape[1]
            ), "Input data is a matrix but it is not a square matrix."
            n_qubits = int(np.log2(data.shape[0]))
        elif isinstance(data, CliffordTableau):
            n_qubits = data.n_qubits
        elif isinstance(data, nx.Graph):
            n_qubits = data.number_of_nodes()
        elif isinstance(data, list):
            if isinstance(data[0][1], CliffordTableau) or isinstance(
                data[0][1], nx.Graph
            ):
                n_qubits = data[0][1].n_qubits
            else:
                return False, None
        else:
            valid = False
            n_qubits = None
        return valid, n_qubits

    def copy(self):
        """
        Make a copy of this QuantumState object

        :return: a copy of the current QuantumState object
        :rtype: QuantumState
        """
        return copy.deepcopy(self)

rep_data property writable

Representation data

Returns:

Type Description
DensityMatrix | Stabilizer | Graph

representation data

rep_type property writable

Representation type

Returns:

Type Description
str

representation type

__init__(data, rep_type=None, mixed=False)

Creates the QuantumState class with one initial representation

Parameters:

Name Type Description Default
data list OR numpy.ndarray OR Graph OR nx.Graph | CliffordTableau

valid data input for "rep_type". Density matrices representations support np.ndarray or int inputs Stabilizer representations take int or StabilizerTableau Graph representations take networkx.Graph

required
rep_type str

selected representation to initialize; if not specified, the default choice is the density matrix if the number of qubits is less than the threshold value or stabilizer otherwise.

None
mixed boolean

boolean flag to initialize as a mixed state or not (mainly used for Stabilizer rep_type)

False

Returns:

Type Description
None

nothing

Source code in graphiq/state.py
def __init__(self, data, rep_type=None, mixed=False):
    """
    Creates the QuantumState class with one initial representation

    :param data: valid data input for "rep_type".
        Density matrices representations support np.ndarray or int inputs
        Stabilizer representations take int or StabilizerTableau
        Graph representations take networkx.Graph
    :type data: list OR numpy.ndarray OR Graph OR nx.Graph or CliffordTableau
    :param rep_type: selected representation to initialize;
        if not specified, the default choice is the density matrix if the number of qubits is
        less than the threshold value or stabilizer otherwise.
    :type rep_type: str
    :param mixed: boolean flag to initialize as a mixed state or not (mainly used for Stabilizer rep_type)
    :type mixed: boolean
    :return: nothing
    :rtype: None
    """

    self.mixed = mixed
    self._rep_type = self._get_rep_type_name(rep_type)
    valid, self.n_qubits = self.validate_data(data)

    if not valid:
        raise TypeError(
            f"Data's type is incorrect. Input data is {data}, which is of type {type(data)}"
        )
    if rep_type is None:
        if (
            self.n_qubits < DENSITY_MATRIX_QUBIT_THRESH
            and DensityMatrix.valid_datatype(data)
        ):
            self._rep_data = self._initialize_dm(data)
        elif Stabilizer.valid_datatype(data):
            self._rep_data = self._initialize_stabilizer(data)
        elif Graph.valid_datatype(data):
            self._rep_data = self._initialize_graph(data)
        elif DensityMatrix.valid_datatype(data):
            raise ValueError(
                "Data's type is correct for density matrix representation, but state size exceeds the "
                "recommended size for density matrix representation"
            )
        else:
            raise ValueError(
                "Data's type is invalid for initializing a QuantumState"
            )

    elif isinstance(rep_type, str):
        self._rep_data = self._initialize_representation(rep_type, data)

    else:
        raise ValueError("passed rep_type argument must be a String")

convert_representation(new_rep_type)

Convert to a representation specified by new_rep_type

Parameters:

Name Type Description Default
new_rep_type str

new representation type

required

Returns:

Type Description
None

nothing

Source code in graphiq/state.py
def convert_representation(self, new_rep_type):
    """
    Convert to a representation specified by new_rep_type

    :param new_rep_type: new representation type
    :type new_rep_type: str
    :return: nothing
    :rtype: None
    """
    rep_type = self._get_rep_type_name(new_rep_type)
    if rep_type is None:
        raise ValueError("Cannot convert representation to None type")

    conversion_dict = {
        ("dm", "g"): self._density_to_graph,
        ("dm", "s"): self._density_to_stabilizer,
        ("s", "dm"): self._stabilizer_to_density,
        ("s", "g"): self._stabilizer_to_graph,
        ("g", "dm"): self._graph_to_density,
        ("g", "s"): self._graph_to_stabilizer,
        ("dm", "dm"): self._identity_fun,
        ("s", "s"): self._identity_fun,
        ("g", "g"): self._identity_fun,
    }
    if self._rep_type != rep_type:
        if rep_type == "dm" and self.n_qubits > DENSITY_MATRIX_QUBIT_THRESH:
            warnings.warn(
                UserWarning(
                    "Density matrix is not recommended for a state of this size"
                )
            )

        tmp_data = self._rep_data
        conversion_func = conversion_dict[(self._rep_type, rep_type)]

        self._rep_data = conversion_func(tmp_data)
        self._rep_type = rep_type

copy()

Make a copy of this QuantumState object

Returns:

Type Description
QuantumState

a copy of the current QuantumState object

Source code in graphiq/state.py
def copy(self):
    """
    Make a copy of this QuantumState object

    :return: a copy of the current QuantumState object
    :rtype: QuantumState
    """
    return copy.deepcopy(self)

partial_trace(keep, dims)

Calculates the partial trace on all state representations which are currently defined

Parameters:

Name Type Description Default
keep list OR numpy.ndarray

An array of indices of the spaces to keep. For instance, if the space is \(A \times B \times C \times D\), and we want to trace out B and D, keep = [0,2]

required
dims list OR numpy.ndarray

An array of the dimensions of each space. For instance, if the space is \(A \times B \times C \times D\), dims = [\(dim_A\), \(dim_B\), \(dim_C\), \(dim_D\)]

required

Returns:

Type Description
None

nothing

Source code in graphiq/state.py
def partial_trace(self, keep, dims):
    r"""
    Calculates the partial trace on all state representations which are currently defined

    :param keep:  An array of indices of the spaces to keep. For instance, if the space is
                $A \times B \times C \times D$, and we want to trace out B and D, keep = [0,2]
    :type keep: list OR numpy.ndarray
    :param dims: An array of the dimensions of each space. For instance,
                if the space is $A \times B \times C \times D$,
                dims = [$dim_A$, $dim_B$, $dim_C$, $dim_D$]
    :type dims: list OR numpy.ndarray
    :return: nothing
    :rtype: None
    """

    if self._rep_type == "g":
        raise NotImplementedError(
            "Partial trace not yet implemented on graph state"
        )
    else:
        self._rep_data.partial_trace(keep, dims)

show(show=True, ax=None)

Plots the state representation using matplotlib formatting

Parameters:

Name Type Description Default
show bool

if True, the state representation is plotted. Otherwise, it is drawn but not plotted

True
ax matplotlib.Axis

axis/axes on which to plot the state representation

None

Returns:

Type Description
matplotlib.Figure, matplotlib.Axis

fig, ax (the figure and axes on which data was plotted)

Source code in graphiq/state.py
def show(self, show=True, ax=None):
    """
    Plots the state representation using matplotlib formatting

    :param show: if True, the state representation is plotted. Otherwise, it is drawn but not plotted
    :type show: bool
    :param ax: axis/axes on which to plot the state representation
    :type ax: matplotlib.Axis
    :return: fig, ax (the figure and axes on which data was plotted)
    :rtype: matplotlib.Figure, matplotlib.Axis
    """
    # Use visualizers module
    if self._rep_type == "dm":
        fig, ax = self._rep_data.draw(show=show, style="heat")
    elif self._rep_type == "g":
        fig, ax = self._rep_data.draw(show=show, ax=ax, with_labels=True)
    elif self._rep_type == "s":
        raise NotImplementedError(
            "No visualization tool is implemented for stabilizer backend."
        )
    else:
        raise ValueError("Invalid representation type.")

    return fig, ax

validate_data(data) classmethod

Validate data type for input data

Parameters:

Name Type Description Default
data int | np.ndarray | CliffordTableau | nx.Graph

input data

required

Returns:

Type Description
bool, int

True and the number of qubits if the data type is valid

Source code in graphiq/state.py
@classmethod
def validate_data(cls, data):
    """
    Validate data type for input data

    :param data: input data
    :type data: int or np.ndarray or CliffordTableau or nx.Graph
    :return: True and the number of qubits if the data type is valid
    :rtype: bool, int
    """
    valid = True
    if isinstance(data, int):
        n_qubits = data
    elif isinstance(data, np.ndarray):
        assert (
            data.shape[0] == data.shape[1]
        ), "Input data is a matrix but it is not a square matrix."
        n_qubits = int(np.log2(data.shape[0]))
    elif isinstance(data, CliffordTableau):
        n_qubits = data.n_qubits
    elif isinstance(data, nx.Graph):
        n_qubits = data.number_of_nodes()
    elif isinstance(data, list):
        if isinstance(data[0][1], CliffordTableau) or isinstance(
            data[0][1], nx.Graph
        ):
            n_qubits = data[0][1].n_qubits
        else:
            return False, None
    else:
        valid = False
        n_qubits = None
    return valid, n_qubits

graphiq.backends.density_matrix.state.DensityMatrix

Bases: StateRepresentationBase

Density matrix of a state

Source code in graphiq/backends/density_matrix/state.py
class DensityMatrix(StateRepresentationBase):
    """
    Density matrix of a state
    """

    def __init__(self, data, normalized=True, *args, **kwargs):
        """
        Construct a DensityMatrix object from a numpy.ndarray or from the number of qubits. If an integer is specified,
        then the state is initialized as a product state of :math:`|0\\rangle` with the given number of qubits.

        :param data: density matrix or the number of qubits
        :type data: numpy.ndarray or int
        :param normalized: whether the state is normalized
        :type normalized: bool
        :return: nothing
        :rtype: None
        """

        if isinstance(data, np.ndarray):
            if not dmf.is_psd(data):
                # check if state_data is positive semi-definite
                raise ValueError("The input matrix is not a valid density matrix")

            if normalized and not np.equal(np.trace(data), 1):
                data = data / np.trace(data)
        elif isinstance(data, int):
            # initialize as a tensor product of |0> state
            data = dmf.create_n_product_state(data, dmf.state_ketz0())
        else:
            raise TypeError("Input must be a numpy.ndarray or an integer")

        super().__init__(data, *args, **kwargs)

    @classmethod
    def from_graph(cls, graph):
        """
        Builds a density matrix representation from a graph (either nx.Graph or a Graph representation)

        :param graph: the graph from which we will build a density matrix
        :type graph: networkx.Graph OR Graph
        :raises TypeError: if the input graph is neither nx.Graph or Graph
        :return: a DensityMatrix representation with the data contained by graph
        :rtype: DensityMatrix
        """
        if isinstance(graph, Graph):
            return cls(graph_to_density(graph.data))
        else:
            return cls(graph_to_density(graph))

    @classmethod
    def valid_datatype(cls, data):
        return isinstance(data, (int, np.ndarray))

    @property
    def trace(self):
        """
        Return the trace of the state

        :return: the trace of the state
        :rtype: float
        """
        return np.trace(self.data)

    @property
    def normalized(self):
        """
        Return whether the state is normalized, that is, trace is 1

        :return: whether the state is normalized
        :rtype: bool
        """
        return np.allclose(self.trace, 1.0)

    def apply_unitary(self, unitary):
        """
        Apply a unitary to the state.
        Assumes the dimensions match; Otherwise, raise ValueError

        :param unitary: unitary matrix to apply
        :type unitary: numpy.ndarray
        :raises ValueError: if the density matrix of the state has a different size from the unitary gate to be applied
        :return: nothing
        :rtype: None
        """
        if self._data.shape == unitary.shape:
            self._data = unitary @ self._data @ np.transpose(np.conjugate(unitary))
            # to avoid small numerical error that causes the state non-Hermitian
            self._data = dmf.hermitianize(self._data)
        else:
            raise ValueError(
                "The density matrix of the state has a different size from the unitary gate to be applied."
            )

    def apply_channel(self, kraus_ops):
        """
        Apply a quantum channel on the state where the quantum channel is described by Kraus representation.
        Assumes the dimensions match; Otherwise, raise ValueError

        :param kraus_ops: a list of Kraus operators of the channel
        :type kraus_ops: list[numpy.ndarray]
        :raises ValueError: if Kraus operators have wrong dimensions.
        :return: nothing
        :rtype: None
        """
        tmp_state = 0
        if len(kraus_ops) == 0:
            return
        if self._data.shape[0] == kraus_ops[0].shape[1]:
            for i in range(len(kraus_ops)):
                tmp_state = tmp_state + kraus_ops[i] @ self._data @ np.conjugate(
                    kraus_ops[i].T
                )
            # to avoid small numerical error that causes the state non-Hermitian
            self._data = dmf.hermitianize(tmp_state)
        else:
            raise ValueError("Kraus operators have wrong dimensions.")

    def apply_measurement(self, projectors, measurement_determinism="probabilistic"):
        """
        Apply a measurement, either deterministically (with a certain outcome) or probabilistically

        :param projectors: a list of projective measurements in the computational basis
        :type projectors: list[numpy.ndarray]
        :param measurement_determinism: if "probabilistic", measurement results are probabilistically selected
                                    if 1, measurement results default to 1 unless the probability of measuring p(1) = 0
                                    if 0, measurement results default to 0 unless the probability of measuring p(0) = 0
        :type measurement_determinism: str/int
        :return: the measurement outcome
        :rtype: int
        """

        if self._data.shape == projectors[0].shape:
            probs = []
            for m in projectors:
                prob = np.real(np.trace(self._data @ m))
                if prob < 0:
                    prob = 0
                probs.append(prob)
            probs = np.array(probs)
            if measurement_determinism == "probabilistic":
                outcome = numpy.random.choice([0, 1], p=probs / np.sum(probs))
            elif measurement_determinism == 1:
                if probs[1] > 0:
                    outcome = 1
                else:
                    outcome = 0

            elif measurement_determinism == 0:
                if probs[1] < 1:
                    outcome = 0
                else:
                    outcome = 1
            else:
                raise ValueError(
                    f'measurement_determinism parameter must be "probabilistic", 0, or 1'
                )

            m, norm = projectors[outcome], probs[outcome]

            # this assumes that the projector, m, has the properties: m = sqrt(m) and m = m.dag()
            self._data = (m @ self._data @ np.transpose(np.conjugate(m))) / norm

        else:
            raise ValueError(
                "The density matrix of the state has a different size from the POVM elements."
            )
        return outcome

    def apply_measurement_controlled_gate(
        self, projectors, target_gate, measurement_determinism=1
    ):
        """
        Apply a measurement, either deterministically (with a certain outcome) or probabilistically
        and conditioned on the measurement outcome, apply the target_gate

        :param projectors: a list of projective measurements in the computational basis
        :type projectors: list[numpy.ndarray]
        :param target_gate: the gate to be applied if the measurement outcome is 1
        :type target_gate: numpy.ndarray
        :param measurement_determinism: if "probabilistic", measurement results are probabilistically selected
                                    if 1, measurement results default to 1 unless the probability of measuring p(1) = 0
                                    if 0, measurement results default to 0 unless the probability of measuring p(0) = 0
        :type measurement_determinism: str/int
        :raises AssertionError: if target_gate has different dimensions from the density matrix of the state
        :return: the measurement outcome
        :rtype: int
        """
        assert self._data.shape == target_gate.shape
        outcome = self.apply_measurement(projectors, measurement_determinism)
        if outcome == 1:
            self.apply_unitary(target_gate)
        return outcome

    def partial_trace(self, keep, dims):
        """
        Take the partial trace of the state

        :param keep:  An array of indices of the spaces to keep. For instance, if the space is
                    :math:`A \\times B \\times C \\times D` and we want to trace out B and D, keep = [0,2]
        :type keep: list OR numpy.ndarray
        :param dims: An array of the dimensions of each space. For instance,
                    if the space is :math:`A \\times B \\times C \\times D`,
                    dims = [dim_A, dim_B, dim_C, dim_D]
        :type dims: list OR numpy.ndarray
        :return:
        :rtype:
        """
        self.data = dmf.partial_trace(self.data, keep, dims)

    def draw(self, style="bar", show=True):
        """
        Draw a bar graph or heatmap of the DensityMatrix representation data

        :param style: 'bar' for bar plot, 'heat' for heatmap
        :type style: str
        :param show: if True, show the density matrix plot. Otherwise, draw the density matrix plot but do not show
        :type show: bool
        :return: fig, axes on which the state is drawn
        :rtype: matplotlib.Figure, matplotlib.Axes

        """
        # TODO: add a "ax" parameter to match the other viewing utils
        if style == "bar":
            fig, axs = density_matrix_bars(self.data)
        else:
            fig, axs = density_matrix_heatmap(self.data)

        if show:
            plt.show()

        return fig, axs

    def __eq__(self, other):
        """
        Compare two DensityMatrix objects and return True if the underlying density matrices are equal
        (up to precision)

        :param other: another DensityMatrix object
        :type other: DensityMatrix
        :return: True if they are equal; False otherwise
        :rtype: bool
        """
        return np.allclose(self._data, other.data)

normalized property

Return whether the state is normalized, that is, trace is 1

Returns:

Type Description
bool

whether the state is normalized

trace property

Return the trace of the state

Returns:

Type Description
float

the trace of the state

__eq__(other)

Compare two DensityMatrix objects and return True if the underlying density matrices are equal (up to precision)

Parameters:

Name Type Description Default
other DensityMatrix

another DensityMatrix object

required

Returns:

Type Description
bool

True if they are equal; False otherwise

Source code in graphiq/backends/density_matrix/state.py
def __eq__(self, other):
    """
    Compare two DensityMatrix objects and return True if the underlying density matrices are equal
    (up to precision)

    :param other: another DensityMatrix object
    :type other: DensityMatrix
    :return: True if they are equal; False otherwise
    :rtype: bool
    """
    return np.allclose(self._data, other.data)

__init__(data, normalized=True, *args, **kwargs)

Construct a DensityMatrix object from a numpy.ndarray or from the number of qubits. If an integer is specified, then the state is initialized as a product state of :math:|0\rangle with the given number of qubits.

Parameters:

Name Type Description Default
data numpy.ndarray | int

density matrix or the number of qubits

required
normalized bool

whether the state is normalized

True

Returns:

Type Description
None

nothing

Source code in graphiq/backends/density_matrix/state.py
def __init__(self, data, normalized=True, *args, **kwargs):
    """
    Construct a DensityMatrix object from a numpy.ndarray or from the number of qubits. If an integer is specified,
    then the state is initialized as a product state of :math:`|0\\rangle` with the given number of qubits.

    :param data: density matrix or the number of qubits
    :type data: numpy.ndarray or int
    :param normalized: whether the state is normalized
    :type normalized: bool
    :return: nothing
    :rtype: None
    """

    if isinstance(data, np.ndarray):
        if not dmf.is_psd(data):
            # check if state_data is positive semi-definite
            raise ValueError("The input matrix is not a valid density matrix")

        if normalized and not np.equal(np.trace(data), 1):
            data = data / np.trace(data)
    elif isinstance(data, int):
        # initialize as a tensor product of |0> state
        data = dmf.create_n_product_state(data, dmf.state_ketz0())
    else:
        raise TypeError("Input must be a numpy.ndarray or an integer")

    super().__init__(data, *args, **kwargs)

apply_channel(kraus_ops)

Apply a quantum channel on the state where the quantum channel is described by Kraus representation. Assumes the dimensions match; Otherwise, raise ValueError

Parameters:

Name Type Description Default
kraus_ops list[numpy.ndarray]

a list of Kraus operators of the channel

required

Returns:

Type Description
None

nothing

Raises:

Type Description
ValueError

if Kraus operators have wrong dimensions.

Source code in graphiq/backends/density_matrix/state.py
def apply_channel(self, kraus_ops):
    """
    Apply a quantum channel on the state where the quantum channel is described by Kraus representation.
    Assumes the dimensions match; Otherwise, raise ValueError

    :param kraus_ops: a list of Kraus operators of the channel
    :type kraus_ops: list[numpy.ndarray]
    :raises ValueError: if Kraus operators have wrong dimensions.
    :return: nothing
    :rtype: None
    """
    tmp_state = 0
    if len(kraus_ops) == 0:
        return
    if self._data.shape[0] == kraus_ops[0].shape[1]:
        for i in range(len(kraus_ops)):
            tmp_state = tmp_state + kraus_ops[i] @ self._data @ np.conjugate(
                kraus_ops[i].T
            )
        # to avoid small numerical error that causes the state non-Hermitian
        self._data = dmf.hermitianize(tmp_state)
    else:
        raise ValueError("Kraus operators have wrong dimensions.")

apply_measurement(projectors, measurement_determinism='probabilistic')

Apply a measurement, either deterministically (with a certain outcome) or probabilistically

Parameters:

Name Type Description Default
projectors list[numpy.ndarray]

a list of projective measurements in the computational basis

required
measurement_determinism str/int

if "probabilistic", measurement results are probabilistically selected if 1, measurement results default to 1 unless the probability of measuring p(1) = 0 if 0, measurement results default to 0 unless the probability of measuring p(0) = 0

'probabilistic'

Returns:

Type Description
int

the measurement outcome

Source code in graphiq/backends/density_matrix/state.py
def apply_measurement(self, projectors, measurement_determinism="probabilistic"):
    """
    Apply a measurement, either deterministically (with a certain outcome) or probabilistically

    :param projectors: a list of projective measurements in the computational basis
    :type projectors: list[numpy.ndarray]
    :param measurement_determinism: if "probabilistic", measurement results are probabilistically selected
                                if 1, measurement results default to 1 unless the probability of measuring p(1) = 0
                                if 0, measurement results default to 0 unless the probability of measuring p(0) = 0
    :type measurement_determinism: str/int
    :return: the measurement outcome
    :rtype: int
    """

    if self._data.shape == projectors[0].shape:
        probs = []
        for m in projectors:
            prob = np.real(np.trace(self._data @ m))
            if prob < 0:
                prob = 0
            probs.append(prob)
        probs = np.array(probs)
        if measurement_determinism == "probabilistic":
            outcome = numpy.random.choice([0, 1], p=probs / np.sum(probs))
        elif measurement_determinism == 1:
            if probs[1] > 0:
                outcome = 1
            else:
                outcome = 0

        elif measurement_determinism == 0:
            if probs[1] < 1:
                outcome = 0
            else:
                outcome = 1
        else:
            raise ValueError(
                f'measurement_determinism parameter must be "probabilistic", 0, or 1'
            )

        m, norm = projectors[outcome], probs[outcome]

        # this assumes that the projector, m, has the properties: m = sqrt(m) and m = m.dag()
        self._data = (m @ self._data @ np.transpose(np.conjugate(m))) / norm

    else:
        raise ValueError(
            "The density matrix of the state has a different size from the POVM elements."
        )
    return outcome

apply_measurement_controlled_gate(projectors, target_gate, measurement_determinism=1)

Apply a measurement, either deterministically (with a certain outcome) or probabilistically and conditioned on the measurement outcome, apply the target_gate

Parameters:

Name Type Description Default
projectors list[numpy.ndarray]

a list of projective measurements in the computational basis

required
target_gate numpy.ndarray

the gate to be applied if the measurement outcome is 1

required
measurement_determinism str/int

if "probabilistic", measurement results are probabilistically selected if 1, measurement results default to 1 unless the probability of measuring p(1) = 0 if 0, measurement results default to 0 unless the probability of measuring p(0) = 0

1

Returns:

Type Description
int

the measurement outcome

Raises:

Type Description
AssertionError

if target_gate has different dimensions from the density matrix of the state

Source code in graphiq/backends/density_matrix/state.py
def apply_measurement_controlled_gate(
    self, projectors, target_gate, measurement_determinism=1
):
    """
    Apply a measurement, either deterministically (with a certain outcome) or probabilistically
    and conditioned on the measurement outcome, apply the target_gate

    :param projectors: a list of projective measurements in the computational basis
    :type projectors: list[numpy.ndarray]
    :param target_gate: the gate to be applied if the measurement outcome is 1
    :type target_gate: numpy.ndarray
    :param measurement_determinism: if "probabilistic", measurement results are probabilistically selected
                                if 1, measurement results default to 1 unless the probability of measuring p(1) = 0
                                if 0, measurement results default to 0 unless the probability of measuring p(0) = 0
    :type measurement_determinism: str/int
    :raises AssertionError: if target_gate has different dimensions from the density matrix of the state
    :return: the measurement outcome
    :rtype: int
    """
    assert self._data.shape == target_gate.shape
    outcome = self.apply_measurement(projectors, measurement_determinism)
    if outcome == 1:
        self.apply_unitary(target_gate)
    return outcome

apply_unitary(unitary)

Apply a unitary to the state. Assumes the dimensions match; Otherwise, raise ValueError

Parameters:

Name Type Description Default
unitary numpy.ndarray

unitary matrix to apply

required

Returns:

Type Description
None

nothing

Raises:

Type Description
ValueError

if the density matrix of the state has a different size from the unitary gate to be applied

Source code in graphiq/backends/density_matrix/state.py
def apply_unitary(self, unitary):
    """
    Apply a unitary to the state.
    Assumes the dimensions match; Otherwise, raise ValueError

    :param unitary: unitary matrix to apply
    :type unitary: numpy.ndarray
    :raises ValueError: if the density matrix of the state has a different size from the unitary gate to be applied
    :return: nothing
    :rtype: None
    """
    if self._data.shape == unitary.shape:
        self._data = unitary @ self._data @ np.transpose(np.conjugate(unitary))
        # to avoid small numerical error that causes the state non-Hermitian
        self._data = dmf.hermitianize(self._data)
    else:
        raise ValueError(
            "The density matrix of the state has a different size from the unitary gate to be applied."
        )

draw(style='bar', show=True)

Draw a bar graph or heatmap of the DensityMatrix representation data

Parameters:

Name Type Description Default
style str

'bar' for bar plot, 'heat' for heatmap

'bar'
show bool

if True, show the density matrix plot. Otherwise, draw the density matrix plot but do not show

True

Returns:

Type Description
matplotlib.Figure, matplotlib.Axes

fig, axes on which the state is drawn

Source code in graphiq/backends/density_matrix/state.py
def draw(self, style="bar", show=True):
    """
    Draw a bar graph or heatmap of the DensityMatrix representation data

    :param style: 'bar' for bar plot, 'heat' for heatmap
    :type style: str
    :param show: if True, show the density matrix plot. Otherwise, draw the density matrix plot but do not show
    :type show: bool
    :return: fig, axes on which the state is drawn
    :rtype: matplotlib.Figure, matplotlib.Axes

    """
    # TODO: add a "ax" parameter to match the other viewing utils
    if style == "bar":
        fig, axs = density_matrix_bars(self.data)
    else:
        fig, axs = density_matrix_heatmap(self.data)

    if show:
        plt.show()

    return fig, axs

from_graph(graph) classmethod

Builds a density matrix representation from a graph (either nx.Graph or a Graph representation)

Parameters:

Name Type Description Default
graph networkx.Graph OR Graph

the graph from which we will build a density matrix

required

Returns:

Type Description
DensityMatrix

a DensityMatrix representation with the data contained by graph

Raises:

Type Description
TypeError

if the input graph is neither nx.Graph or Graph

Source code in graphiq/backends/density_matrix/state.py
@classmethod
def from_graph(cls, graph):
    """
    Builds a density matrix representation from a graph (either nx.Graph or a Graph representation)

    :param graph: the graph from which we will build a density matrix
    :type graph: networkx.Graph OR Graph
    :raises TypeError: if the input graph is neither nx.Graph or Graph
    :return: a DensityMatrix representation with the data contained by graph
    :rtype: DensityMatrix
    """
    if isinstance(graph, Graph):
        return cls(graph_to_density(graph.data))
    else:
        return cls(graph_to_density(graph))

partial_trace(keep, dims)

Take the partial trace of the state

Parameters:

Name Type Description Default
keep list OR numpy.ndarray

An array of indices of the spaces to keep. For instance, if the space is :math:A \times B \times C \times D and we want to trace out B and D, keep = [0,2]

required
dims list OR numpy.ndarray

An array of the dimensions of each space. For instance, if the space is :math:A \times B \times C \times D, dims = [dim_A, dim_B, dim_C, dim_D]

required

Returns:

Type Description
Source code in graphiq/backends/density_matrix/state.py
def partial_trace(self, keep, dims):
    """
    Take the partial trace of the state

    :param keep:  An array of indices of the spaces to keep. For instance, if the space is
                :math:`A \\times B \\times C \\times D` and we want to trace out B and D, keep = [0,2]
    :type keep: list OR numpy.ndarray
    :param dims: An array of the dimensions of each space. For instance,
                if the space is :math:`A \\times B \\times C \\times D`,
                dims = [dim_A, dim_B, dim_C, dim_D]
    :type dims: list OR numpy.ndarray
    :return:
    :rtype:
    """
    self.data = dmf.partial_trace(self.data, keep, dims)

graphiq.backends.stabilizer.state.Stabilizer

Bases: StateRepresentationBase

Source code in graphiq/backends/stabilizer/state.py
class Stabilizer(StateRepresentationBase):
    def __init__(self, data, *args, **kwargs):
        super().__init__(data, *args, **kwargs)
        if isinstance(data, int):
            self._tableau = CliffordTableau(data)
        elif isinstance(data, CliffordTableau):
            self._tableau = data
        else:
            raise TypeError(
                f"Cannot initialize the stabilizer representation with datatype: {type(data)}"
            )

    @classmethod
    def valid_datatype(cls, data):
        return isinstance(data, (int, CliffordTableau))

    @property
    def n_qubits(self):
        """
        Returns the number of qubits in the stabilizer state

        :return: the number of qubits in the state
        :rtype: int
        """
        return self._tableau.n_qubits

    @property
    def tableau(self):
        """
        The data that represents the state given by this Stabilizer representation

        :return: the underlying representation
        :rtype: CliffordTableau
        """
        return self._tableau

    @tableau.setter
    def tableau(self, value):
        """
        Set the data that represents the state given by this Stabilizer representation

        :param value: a new tableau or a parameter to initialize a new tableau
        :type value: int or CliffordTableau
        :return: nothing
        :rtype: None
        """
        if isinstance(value, int):
            self._tableau = CliffordTableau(value)
        elif isinstance(value, CliffordTableau):
            self._tableau = value
        else:
            raise TypeError("Must use CliffordTableau for the stabilizer's tableau")

    @property
    def data(self):
        """
        The data that represents the state given by this Stabilizer representation

        :return: the tableau that represents this state
        :rtype: CliffordTableau
        """
        return self.tableau

    @data.setter
    def data(self, value):
        """
        Set the data that represents the state given by this Stabilizer representation

        :param value: a new tableau or a parameter to initialize a new tableau
        :type value: CliffordTableau or int
        :return: nothing
        :rtype: None
        """
        self.tableau = value

    def apply_unitary(self, qubit_position, unitary):
        raise NotImplementedError(
            "Stabilizer backend does not support general unitary operation."
        )

    def apply_circuit(self, gate_list_str, reverse=False):
        """
        Apply a quantum circuit to the tableau

        :param gate_list_str: a list of gates in the circuit
        :type gate_list_str: list[tuple]
        :param reverse: a parameter to indicate whether running the inverse circuit
        :type reverse: bool
        :return: nothing
        :rtype: None
        """
        transform.run_circuit(self._tableau, gate_list_str, reverse=reverse)

    def apply_measurement(
        self, qubit_position, measurement_determinism="probabilistic"
    ):
        """
        Apply the measurement in the computational basis to a given qubit

        :param qubit_position: the qubit position where the measurement is applied
        :type qubit_position: int
        :param measurement_determinism: if "probabilistic", measurement results are probabilistically selected
                if 1, measurement results default to 1 unless the probability of measuring p(1) = 0
                if 0, measurement results default to 0 unless the probability of measuring p(0) = 0
        :type measurement_determinism: str/int
        :return: the measurement outcome
        :rtype: int
        """
        (
            self._tableau,
            outcome,
            _,
        ) = sfc.z_measurement_gate(
            self._tableau, qubit_position, measurement_determinism
        )
        return outcome

    def apply_x_measurement(
        self, qubit_position, measurement_determinism="probabilistic"
    ):
        """
        Apply the measurement in the computational basis to a given qubit

        :param qubit_position: the qubit position where the measurement is applied
        :type qubit_position: int
        :param measurement_determinism: if "probabilistic", measurement results are probabilistically selected
                if 1, measurement results default to 1 unless the probability of measuring p(1) = 0
                if 0, measurement results default to 0 unless the probability of measuring p(0) = 0
        :type measurement_determinism: str/int
        :return: the measurement outcome
        :rtype: int
        """
        (
            self._tableau,
            outcome,
            _,
        ) = sfc.x_measurement_gate(
            self._tableau, qubit_position, measurement_determinism
        )
        return outcome

    def apply_hadamard(self, qubit_position):
        """
        Apply the Hadamard gate to the Stabilizer

        :param qubit_position: the qubit position where the gate is applied
        :type qubit_position: int
        :return: nothing
        :rtype: None
        """
        self._tableau = transform.hadamard_gate(self._tableau, qubit_position)

    def apply_cnot(self, control, target):
        """
        Apply CNOT gate to the Stabilizer

        :param control: the control qubit position where the gate is applied
        :type control: int
        :param target: the target qubit position where the gate is applied
        :type target: int
        :return: nothing
        :rtype: None
        """
        self._tableau = transform.cnot_gate(self._tableau, control, target)

    def apply_cz(self, control, target):
        """
        Apply CZ gate to the Stabilizer

        :param control: the control qubit position where the gate is applied
        :type control: int
        :param target: the target qubit position where the gate is applied
        :type target: int
        :return: nothing
        :rtype: None
        """
        self._tableau = transform.control_z_gate(self._tableau, control, target)

    def apply_phase(self, qubit_position):
        """
        Apply the phase gate to the Stabilizer

        :param qubit_position: the qubit position where the gate is applied
        :type qubit_position: int
        :return: nothing
        :rtype: None
        """
        self._tableau = transform.phase_gate(self._tableau, qubit_position)

    def apply_phase_dagger(self, qubit_position):
        """
        Apply the phase dagger gate to the Stabilizer

        :param qubit_position: the qubit position where the gate is applied
        :type qubit_position: int
        :return: nothing
        :rtype: None
        """
        self._tableau = transform.phase_dagger_gate(self._tableau, qubit_position)

    def apply_sigmax(self, qubit_position):
        """
        Apply the X gate to the Stabilizer

        :param qubit_position: the qubit position where the gate is applied
        :type qubit_position: int
        :return: nothing
        :rtype: None
        """
        self._tableau = transform.x_gate(self._tableau, qubit_position)

    def apply_sigmay(self, qubit_position):
        """
        Apply the Y gate to the Stabilizer

        :param qubit_position: the qubit position where the gate is applied
        :type qubit_position: int
        :return: nothing
        :rtype: None
        """
        self._tableau = transform.y_gate(self._tableau, qubit_position)

    def apply_sigmaz(self, qubit_position):
        """
        Apply the Z gate to the Stabilizer

        :param qubit_position: the qubit position where the gate is applied
        :type qubit_position: int
        :return: nothing
        :rtype: None
        """
        self._tableau = transform.z_gate(self._tableau, qubit_position)

    def reset_qubit(self, qubit_position, measurement_determinism="probabilistic"):
        """
        Reset a given qubit to :math:`|0\\rangle` state after disentangling it from the rest

        :param qubit_position: the qubit position to be reset
        :type qubit_position: int
        :param measurement_determinism: if "probabilistic", measurement results are probabilistically selected
                if 1, measurement results default to 1 unless the probability of measuring p(1) = 0
                if 0, measurement results default to 0 unless the probability of measuring p(0) = 0
        :type measurement_determinism: str/int
        :return: nothing
        :rtype: None
        """
        self._tableau = sfc.reset_z(
            self._tableau, qubit_position, 0, measurement_determinism
        )

    def remove_qubit(self, qubit_position, measurement_determinism="probabilistic"):
        """
        Trace out one qubit after disentangling it from the rest

        :param qubit_position: the qubit position to be traced out
        :type qubit_position: int
        :param measurement_determinism: if "probabilistic", measurement results are probabilistically selected
                if 1, measurement results default to 1 unless the probability of measuring p(1) = 0
                if 0, measurement results default to 0 unless the probability of measuring p(0) = 0
        :type measurement_determinism: str/int
        :return: nothing
        :rtype: None
        """
        self._tableau = sfc.remove_qubit(
            self._tableau, qubit_position, measurement_determinism
        )

    def trace_out_qubits(
        self, qubit_positions, measurement_determinism="probabilistic"
    ):
        """
        Trace out qubits after disentangling them from the rest

        :param qubit_positions: the qubit positions to be traced out
        :type qubit_positions: list[int]
        :param measurement_determinism: if "probabilistic", measurement results are probabilistically selected
                if 1, measurement results default to 1 unless the probability of measuring p(1) = 0
                if 0, measurement results default to 0 unless the probability of measuring p(0) = 0
        :type measurement_determinism: str/int
        :return: nothing
        :rtype: None
        """
        self._tableau = sfc.partial_trace(
            self._tableau,
            keep=qubit_positions,
            dims=self.n_qubits * [2],
            measurement_determinism=measurement_determinism,
        )

    def partial_trace(self, keep, dims):
        """
        Trace out qubits after disentangling them from the rest

        :param keep:
        :type keep: list[int] or numpy.ndarray
        :param dims:
        :type dims:
        :return: nothing
        :rtype: None
        """
        self._tableau = sfc.partial_trace(self._tableau, keep=keep, dims=dims)

    def __str__(self):
        """
        Return a string representation of this state representation

        :return: a string representation of this state representation
        :rtype: str
        """
        return self._tableau.__str__()

    def __eq__(self, other):
        """
        Compare two Stabilizer objects

        :param other: the other Stabilizer to be compared
        :type other: Stabilizer
        :return: True if the stabilizer tableaux of two Stabilizer objects are the same
        :rtype: bool
        """
        tableau1 = canonical_form(self.data.to_stabilizer())
        tableau2 = canonical_form(other.data.to_stabilizer())
        return tableau1 == tableau2

data property writable

The data that represents the state given by this Stabilizer representation

Returns:

Type Description
CliffordTableau

the tableau that represents this state

n_qubits property

Returns the number of qubits in the stabilizer state

Returns:

Type Description
int

the number of qubits in the state

tableau property writable

The data that represents the state given by this Stabilizer representation

Returns:

Type Description
CliffordTableau

the underlying representation

__eq__(other)

Compare two Stabilizer objects

Parameters:

Name Type Description Default
other Stabilizer

the other Stabilizer to be compared

required

Returns:

Type Description
bool

True if the stabilizer tableaux of two Stabilizer objects are the same

Source code in graphiq/backends/stabilizer/state.py
def __eq__(self, other):
    """
    Compare two Stabilizer objects

    :param other: the other Stabilizer to be compared
    :type other: Stabilizer
    :return: True if the stabilizer tableaux of two Stabilizer objects are the same
    :rtype: bool
    """
    tableau1 = canonical_form(self.data.to_stabilizer())
    tableau2 = canonical_form(other.data.to_stabilizer())
    return tableau1 == tableau2

__str__()

Return a string representation of this state representation

Returns:

Type Description
str

a string representation of this state representation

Source code in graphiq/backends/stabilizer/state.py
def __str__(self):
    """
    Return a string representation of this state representation

    :return: a string representation of this state representation
    :rtype: str
    """
    return self._tableau.__str__()

apply_circuit(gate_list_str, reverse=False)

Apply a quantum circuit to the tableau

Parameters:

Name Type Description Default
gate_list_str list[tuple]

a list of gates in the circuit

required
reverse bool

a parameter to indicate whether running the inverse circuit

False

Returns:

Type Description
None

nothing

Source code in graphiq/backends/stabilizer/state.py
def apply_circuit(self, gate_list_str, reverse=False):
    """
    Apply a quantum circuit to the tableau

    :param gate_list_str: a list of gates in the circuit
    :type gate_list_str: list[tuple]
    :param reverse: a parameter to indicate whether running the inverse circuit
    :type reverse: bool
    :return: nothing
    :rtype: None
    """
    transform.run_circuit(self._tableau, gate_list_str, reverse=reverse)

apply_cnot(control, target)

Apply CNOT gate to the Stabilizer

Parameters:

Name Type Description Default
control int

the control qubit position where the gate is applied

required
target int

the target qubit position where the gate is applied

required

Returns:

Type Description
None

nothing

Source code in graphiq/backends/stabilizer/state.py
def apply_cnot(self, control, target):
    """
    Apply CNOT gate to the Stabilizer

    :param control: the control qubit position where the gate is applied
    :type control: int
    :param target: the target qubit position where the gate is applied
    :type target: int
    :return: nothing
    :rtype: None
    """
    self._tableau = transform.cnot_gate(self._tableau, control, target)

apply_cz(control, target)

Apply CZ gate to the Stabilizer

Parameters:

Name Type Description Default
control int

the control qubit position where the gate is applied

required
target int

the target qubit position where the gate is applied

required

Returns:

Type Description
None

nothing

Source code in graphiq/backends/stabilizer/state.py
def apply_cz(self, control, target):
    """
    Apply CZ gate to the Stabilizer

    :param control: the control qubit position where the gate is applied
    :type control: int
    :param target: the target qubit position where the gate is applied
    :type target: int
    :return: nothing
    :rtype: None
    """
    self._tableau = transform.control_z_gate(self._tableau, control, target)

apply_hadamard(qubit_position)

Apply the Hadamard gate to the Stabilizer

Parameters:

Name Type Description Default
qubit_position int

the qubit position where the gate is applied

required

Returns:

Type Description
None

nothing

Source code in graphiq/backends/stabilizer/state.py
def apply_hadamard(self, qubit_position):
    """
    Apply the Hadamard gate to the Stabilizer

    :param qubit_position: the qubit position where the gate is applied
    :type qubit_position: int
    :return: nothing
    :rtype: None
    """
    self._tableau = transform.hadamard_gate(self._tableau, qubit_position)

apply_measurement(qubit_position, measurement_determinism='probabilistic')

Apply the measurement in the computational basis to a given qubit

Parameters:

Name Type Description Default
qubit_position int

the qubit position where the measurement is applied

required
measurement_determinism str/int

if "probabilistic", measurement results are probabilistically selected if 1, measurement results default to 1 unless the probability of measuring p(1) = 0 if 0, measurement results default to 0 unless the probability of measuring p(0) = 0

'probabilistic'

Returns:

Type Description
int

the measurement outcome

Source code in graphiq/backends/stabilizer/state.py
def apply_measurement(
    self, qubit_position, measurement_determinism="probabilistic"
):
    """
    Apply the measurement in the computational basis to a given qubit

    :param qubit_position: the qubit position where the measurement is applied
    :type qubit_position: int
    :param measurement_determinism: if "probabilistic", measurement results are probabilistically selected
            if 1, measurement results default to 1 unless the probability of measuring p(1) = 0
            if 0, measurement results default to 0 unless the probability of measuring p(0) = 0
    :type measurement_determinism: str/int
    :return: the measurement outcome
    :rtype: int
    """
    (
        self._tableau,
        outcome,
        _,
    ) = sfc.z_measurement_gate(
        self._tableau, qubit_position, measurement_determinism
    )
    return outcome

apply_phase(qubit_position)

Apply the phase gate to the Stabilizer

Parameters:

Name Type Description Default
qubit_position int

the qubit position where the gate is applied

required

Returns:

Type Description
None

nothing

Source code in graphiq/backends/stabilizer/state.py
def apply_phase(self, qubit_position):
    """
    Apply the phase gate to the Stabilizer

    :param qubit_position: the qubit position where the gate is applied
    :type qubit_position: int
    :return: nothing
    :rtype: None
    """
    self._tableau = transform.phase_gate(self._tableau, qubit_position)

apply_phase_dagger(qubit_position)

Apply the phase dagger gate to the Stabilizer

Parameters:

Name Type Description Default
qubit_position int

the qubit position where the gate is applied

required

Returns:

Type Description
None

nothing

Source code in graphiq/backends/stabilizer/state.py
def apply_phase_dagger(self, qubit_position):
    """
    Apply the phase dagger gate to the Stabilizer

    :param qubit_position: the qubit position where the gate is applied
    :type qubit_position: int
    :return: nothing
    :rtype: None
    """
    self._tableau = transform.phase_dagger_gate(self._tableau, qubit_position)

apply_sigmax(qubit_position)

Apply the X gate to the Stabilizer

Parameters:

Name Type Description Default
qubit_position int

the qubit position where the gate is applied

required

Returns:

Type Description
None

nothing

Source code in graphiq/backends/stabilizer/state.py
def apply_sigmax(self, qubit_position):
    """
    Apply the X gate to the Stabilizer

    :param qubit_position: the qubit position where the gate is applied
    :type qubit_position: int
    :return: nothing
    :rtype: None
    """
    self._tableau = transform.x_gate(self._tableau, qubit_position)

apply_sigmay(qubit_position)

Apply the Y gate to the Stabilizer

Parameters:

Name Type Description Default
qubit_position int

the qubit position where the gate is applied

required

Returns:

Type Description
None

nothing

Source code in graphiq/backends/stabilizer/state.py
def apply_sigmay(self, qubit_position):
    """
    Apply the Y gate to the Stabilizer

    :param qubit_position: the qubit position where the gate is applied
    :type qubit_position: int
    :return: nothing
    :rtype: None
    """
    self._tableau = transform.y_gate(self._tableau, qubit_position)

apply_sigmaz(qubit_position)

Apply the Z gate to the Stabilizer

Parameters:

Name Type Description Default
qubit_position int

the qubit position where the gate is applied

required

Returns:

Type Description
None

nothing

Source code in graphiq/backends/stabilizer/state.py
def apply_sigmaz(self, qubit_position):
    """
    Apply the Z gate to the Stabilizer

    :param qubit_position: the qubit position where the gate is applied
    :type qubit_position: int
    :return: nothing
    :rtype: None
    """
    self._tableau = transform.z_gate(self._tableau, qubit_position)

apply_x_measurement(qubit_position, measurement_determinism='probabilistic')

Apply the measurement in the computational basis to a given qubit

Parameters:

Name Type Description Default
qubit_position int

the qubit position where the measurement is applied

required
measurement_determinism str/int

if "probabilistic", measurement results are probabilistically selected if 1, measurement results default to 1 unless the probability of measuring p(1) = 0 if 0, measurement results default to 0 unless the probability of measuring p(0) = 0

'probabilistic'

Returns:

Type Description
int

the measurement outcome

Source code in graphiq/backends/stabilizer/state.py
def apply_x_measurement(
    self, qubit_position, measurement_determinism="probabilistic"
):
    """
    Apply the measurement in the computational basis to a given qubit

    :param qubit_position: the qubit position where the measurement is applied
    :type qubit_position: int
    :param measurement_determinism: if "probabilistic", measurement results are probabilistically selected
            if 1, measurement results default to 1 unless the probability of measuring p(1) = 0
            if 0, measurement results default to 0 unless the probability of measuring p(0) = 0
    :type measurement_determinism: str/int
    :return: the measurement outcome
    :rtype: int
    """
    (
        self._tableau,
        outcome,
        _,
    ) = sfc.x_measurement_gate(
        self._tableau, qubit_position, measurement_determinism
    )
    return outcome

partial_trace(keep, dims)

Trace out qubits after disentangling them from the rest

Parameters:

Name Type Description Default
keep list[int] | numpy.ndarray
required
dims
required

Returns:

Type Description
None

nothing

Source code in graphiq/backends/stabilizer/state.py
def partial_trace(self, keep, dims):
    """
    Trace out qubits after disentangling them from the rest

    :param keep:
    :type keep: list[int] or numpy.ndarray
    :param dims:
    :type dims:
    :return: nothing
    :rtype: None
    """
    self._tableau = sfc.partial_trace(self._tableau, keep=keep, dims=dims)

remove_qubit(qubit_position, measurement_determinism='probabilistic')

Trace out one qubit after disentangling it from the rest

Parameters:

Name Type Description Default
qubit_position int

the qubit position to be traced out

required
measurement_determinism str/int

if "probabilistic", measurement results are probabilistically selected if 1, measurement results default to 1 unless the probability of measuring p(1) = 0 if 0, measurement results default to 0 unless the probability of measuring p(0) = 0

'probabilistic'

Returns:

Type Description
None

nothing

Source code in graphiq/backends/stabilizer/state.py
def remove_qubit(self, qubit_position, measurement_determinism="probabilistic"):
    """
    Trace out one qubit after disentangling it from the rest

    :param qubit_position: the qubit position to be traced out
    :type qubit_position: int
    :param measurement_determinism: if "probabilistic", measurement results are probabilistically selected
            if 1, measurement results default to 1 unless the probability of measuring p(1) = 0
            if 0, measurement results default to 0 unless the probability of measuring p(0) = 0
    :type measurement_determinism: str/int
    :return: nothing
    :rtype: None
    """
    self._tableau = sfc.remove_qubit(
        self._tableau, qubit_position, measurement_determinism
    )

reset_qubit(qubit_position, measurement_determinism='probabilistic')

Reset a given qubit to :math:|0\rangle state after disentangling it from the rest

Parameters:

Name Type Description Default
qubit_position int

the qubit position to be reset

required
measurement_determinism str/int

if "probabilistic", measurement results are probabilistically selected if 1, measurement results default to 1 unless the probability of measuring p(1) = 0 if 0, measurement results default to 0 unless the probability of measuring p(0) = 0

'probabilistic'

Returns:

Type Description
None

nothing

Source code in graphiq/backends/stabilizer/state.py
def reset_qubit(self, qubit_position, measurement_determinism="probabilistic"):
    """
    Reset a given qubit to :math:`|0\\rangle` state after disentangling it from the rest

    :param qubit_position: the qubit position to be reset
    :type qubit_position: int
    :param measurement_determinism: if "probabilistic", measurement results are probabilistically selected
            if 1, measurement results default to 1 unless the probability of measuring p(1) = 0
            if 0, measurement results default to 0 unless the probability of measuring p(0) = 0
    :type measurement_determinism: str/int
    :return: nothing
    :rtype: None
    """
    self._tableau = sfc.reset_z(
        self._tableau, qubit_position, 0, measurement_determinism
    )

trace_out_qubits(qubit_positions, measurement_determinism='probabilistic')

Trace out qubits after disentangling them from the rest

Parameters:

Name Type Description Default
qubit_positions list[int]

the qubit positions to be traced out

required
measurement_determinism str/int

if "probabilistic", measurement results are probabilistically selected if 1, measurement results default to 1 unless the probability of measuring p(1) = 0 if 0, measurement results default to 0 unless the probability of measuring p(0) = 0

'probabilistic'

Returns:

Type Description
None

nothing

Source code in graphiq/backends/stabilizer/state.py
def trace_out_qubits(
    self, qubit_positions, measurement_determinism="probabilistic"
):
    """
    Trace out qubits after disentangling them from the rest

    :param qubit_positions: the qubit positions to be traced out
    :type qubit_positions: list[int]
    :param measurement_determinism: if "probabilistic", measurement results are probabilistically selected
            if 1, measurement results default to 1 unless the probability of measuring p(1) = 0
            if 0, measurement results default to 0 unless the probability of measuring p(0) = 0
    :type measurement_determinism: str/int
    :return: nothing
    :rtype: None
    """
    self._tableau = sfc.partial_trace(
        self._tableau,
        keep=qubit_positions,
        dims=self.n_qubits * [2],
        measurement_determinism=measurement_determinism,
    )

graphiq.backends.stabilizer.state.MixedStabilizer

Bases: StateRepresentationBase

A mixed state representation using the stabilizer formalism, where the mixture is represented as a list of pure states (tableaus) and an associated mixture probability.

Source code in graphiq/backends/stabilizer/state.py
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
class MixedStabilizer(StateRepresentationBase):
    """
    A mixed state representation using the stabilizer formalism, where the mixture is represented as a list of
    pure states (tableaus) and an associated mixture probability.
    """

    def __init__(self, data, *args, **kwargs):
        if isinstance(data, int):
            self._mixture = [
                (1.0, CliffordTableau(data)),
            ]
        elif isinstance(data, CliffordTableau):
            self._mixture = [
                (1.0, data),
            ]
        elif isinstance(data, list):
            assert all(
                isinstance(p_i, float) and isinstance(t_i, CliffordTableau)
                for (p_i, t_i) in data
            )
            self._mixture = data
        else:
            raise TypeError(
                f"Cannot initialize the stabilizer representation with datatype: {type(data)}"
            )

    @classmethod
    def valid_datatype(cls, data):
        valid = isinstance(data, (int, CliffordTableau, list))
        if isinstance(data, list):
            valid = valid and all(
                isinstance(p_i, float) and isinstance(t_i, CliffordTableau)
                for (p_i, t_i) in data
            )
        return valid

    @property
    def n_qubits(self):
        """
        Returns the number of qubits in the stabilizer state

        :return: the number of qubits in the state
        :rtype: int
        """
        return self._mixture[0][1].n_qubits

    @property
    def mixture(self):
        """
        The mixture of pure states, represented as a list of tableaus and associated probabilities.

        :return: the mixture as a list of (probability_i, tableau_i)
        :rtype: list
        """
        return self._mixture

    @mixture.setter
    def mixture(self, value):
        """
        Sets the mixture of pure states, represented as a list of tableaus and associated probabilities.

        :param value: a new mixture list, pure tableau, or a parameter to initialize a new tableau
        :type value: list or int or CliffordTableau
        :return: the mixture as a list of (probability_i, tableau_i)
        :rtype: list
        """
        if isinstance(value, list):
            assert all(
                isinstance(p_i, float) and isinstance(t_i, CliffordTableau)
                for (p_i, t_i) in value
            )
            assert (
                len(set([t_i.n_qubits for p_i, t_i in value])) == 1
            )  # all tableaux are same number of qubits
            self._mixture = value

        elif isinstance(value, CliffordTableau):
            self._mixture = [(1.0, value)]

        elif isinstance(value, int):
            self._mixture = [(1.0, CliffordTableau(value))]

        else:
            raise TypeError(
                "Must use a list of CliffordTableau for the mixed stabilizer"
            )

    @property
    def data(self):
        """
        The data that represents the state given by the MixedStabilizer representation

        :return: the mixture that represents this state
        :rtype: list
        """
        return self.mixture

    @data.setter
    def data(self, value):
        """
        Set the data that represents the state given by the MixedStabilizer representation

        :param value: a new tableau or a parameter to initialize a new tableau
        :type value: CliffordTableau or int
        :return: nothing
        :rtype: None
        """
        self.mixture = value

    @property
    def probability(self):
        r"""
        Computes the total probability as the summed probability of all pure states in the mixture
        $\sum_i p_i \\ \forall (p_i, \mathcal{T}_i)$.

        :return: sum of probabilities
        :rtype: float
        """
        return sum(p_i for p_i, t_i in self.mixture)

    @property
    def tableau(self):
        return TypeError(
            "Simulating using a mixed state representation, no tableau defined."
        )

    def reduce(self):
        """
        Reduce the number of tableaux store in the mixture by comparing the Hamming distance between them.
        Probabilities are summed and one tableau removed if they are the same.

        :return: nothing
        :rtype: None
        """
        # TODO: explore other ways of reduction and further simplification using a standard form.
        mixture_temp = self._mixture
        mixture_reduce = []
        while len(mixture_temp) != 0:
            p0, t0 = mixture_temp[0]
            mixture_temp.pop(0)
            for i, (p_i, t_i) in enumerate(mixture_temp):
                if np.count_nonzero(t0 != t_i) == 0:
                    p0 += p_i
                    mixture_temp.pop(i)

            mixture_reduce.append((p0, t0))
        self._mixture = mixture_reduce

    def apply_unitary(self, qubit_position, unitary):
        raise NotImplementedError(
            "Stabilizer backend does not support general unitary operation."
        )

    def apply_conditioned_gate(self, qubit_position, outcomes, gate=None):
        """
        Apply a single-qubit gate, conditioned on a classical measurement outcome.

        :param qubit_position: int
        :param outcomes: list of measurement outcomes for each tableau in the mixture
        :param gate: str, one of 'x', 'y', 'z', or 'h'
        :return:
        """
        assert isinstance(outcomes, list)
        assert len(outcomes) == len(self._mixture)

        if gate == "x":
            trans = transform.x_gate
        elif gate == "y":
            trans = transform.y_gate
        elif gate == "z":
            trans = transform.z_gate
        elif gate == "h":
            trans = transform.hadamard_gate
        else:
            raise NotImplementedError(
                "Gate must be provided for conditioning measurement outcomes."
            )
        for i, outcome in enumerate(outcomes):
            if outcome == 1:
                p_i, t_i = self._mixture[i]
                self._mixture[i] = (p_i, trans(t_i, qubit_position))

    def apply_measurement(
        self, qubit_position, measurement_determinism="probabilistic"
    ):
        """
        Apply the measurement in the computational basis to a given qubit. For the MixedStabilizer state,
        we measure the outcome for each tableau in the mixture, returning a list of outcomes.

        # todo think of classical probabilities being stored on the c-registers?

        :param qubit_position: the qubit position where the measurement is applied
        :type qubit_position: int
        :param measurement_determinism: if "probabilistic", measurement results are probabilistically selected
                if 1, measurement results default to 1 unless the probability of measuring p(1) = 0
                if 0, measurement results default to 0 unless the probability of measuring p(0) = 0
        :type measurement_determinism: str/int
        :return: the measurement outcome
        :rtype: list
        """
        outcomes = []
        for i, (p_i, t_i) in enumerate(self._mixture):
            tableau, outcome, x_p = sfc.z_measurement_gate(
                t_i, qubit_position, measurement_determinism=measurement_determinism
            )
            outcomes.append(outcome)
            self._mixture[i] = (p_i, tableau)

        return outcomes

    def apply_hadamard(self, qubit_position):
        """
        Apply the Hadamard gate to the Stabilizer

        :param qubit_position: the qubit position where the gate is applied
        :type qubit_position: int
        :return: nothing
        :rtype: None
        """
        self._mixture = [
            (p_i, transform.hadamard_gate(t_i, qubit_position))
            for (p_i, t_i) in self._mixture
        ]

    def apply_cnot(self, control, target):
        """
        Apply CNOT gate to the Stabilizer

        :param control: the control qubit position where the gate is applied
        :type control: int
        :param target: the target qubit position where the gate is applied
        :type target: int
        :return: nothing
        :rtype: None
        """
        self._mixture = [
            (p_i, transform.cnot_gate(t_i, control, target))
            for (p_i, t_i) in self._mixture
        ]

    def apply_cz(self, control, target):
        """
        Apply CZ gate to the Stabilizer

        :param control: the control qubit position where the gate is applied
        :type control: int
        :param target: the target qubit position where the gate is applied
        :type target: int
        :return: nothing
        :rtype: None
        """
        self._mixture = [
            (p_i, transform.control_z_gate(t_i, control, target))
            for (p_i, t_i) in self._mixture
        ]

    def apply_phase(self, qubit_position):
        """
        Apply the phase gate to the Stabilizer

        :param qubit_position: the qubit position where the gate is applied
        :type qubit_position: int
        :return: nothing
        :rtype: None
        """
        self._mixture = [
            (p_i, transform.phase_gate(t_i, qubit_position))
            for (p_i, t_i) in self._mixture
        ]

    def apply_phase_dagger(self, qubit_position):
        """
        Apply the phase dagger gate to the Stabilizer

        :param qubit_position: the qubit position where the gate is applied
        :type qubit_position: int
        :return: nothing
        :rtype: None
        """
        self._mixture = [
            (p_i, transform.phase_dagger_gate(t_i, qubit_position))
            for (p_i, t_i) in self._mixture
        ]

    def apply_sigmax(self, qubit_position):
        """
        Apply the X gate to the Stabilizer

        :param qubit_position: the qubit position where the gate is applied
        :type qubit_position: int
        :return: nothing
        :rtype: None
        """
        self._mixture = [
            (p_i, transform.x_gate(t_i, qubit_position)) for (p_i, t_i) in self._mixture
        ]

    def apply_sigmay(self, qubit_position):
        """
        Apply the Y gate to the Stabilizer

        :param qubit_position: the qubit position where the gate is applied
        :type qubit_position: int
        :return: nothing
        :rtype: None
        """
        self._mixture = [
            (p_i, transform.y_gate(t_i, qubit_position)) for (p_i, t_i) in self._mixture
        ]

    def apply_sigmaz(self, qubit_position):
        """
        Apply the Z gate to the Stabilizer

        :param qubit_position: the qubit position where the gate is applied
        :type qubit_position: int
        :return: nothing
        :rtype: None
        """
        self._mixture = [
            (p_i, transform.z_gate(t_i, qubit_position)) for (p_i, t_i) in self._mixture
        ]

    def reset_qubit(self, qubit_position, measurement_determinism="probabilistic"):
        r"""
        Reset a given qubit to $|0\rangle$ state after disentangling it from the rest

        :param qubit_position: the qubit position to be reset
        :type qubit_position: int
        :param measurement_determinism: if "probabilistic", measurement results are probabilistically selected
                if 1, measurement results default to 1 unless the probability of measuring p(1) = 0
                if 0, measurement results default to 0 unless the probability of measuring p(0) = 0
        :type measurement_determinism: str/int
        :return: nothing
        :rtype: None
        """
        self._mixture = [
            (p_i, sfc.reset_z(t_i, qubit_position, 0, measurement_determinism))
            for (p_i, t_i) in self._mixture
        ]

    def remove_qubit(self, qubit_position, measurement_determinism="probabilistic"):
        """
        Trace out one qubit after disentangling it from the rest

        :param qubit_position: the qubit position to be traced out
        :type qubit_position: int
        :param measurement_determinism: if "probabilistic", measurement results are probabilistically selected
                if 1, measurement results default to 1 unless the probability of measuring p(1) = 0
                if 0, measurement results default to 0 unless the probability of measuring p(0) = 0
        :type measurement_determinism: str/int
        :return: nothing
        :rtype: None
        """
        self._mixture = [
            (p_i, sfc.remove_qubit(t_i, qubit_position, measurement_determinism))
            for (p_i, t_i) in self._mixture
        ]

    def trace_out_qubits(
        self, qubit_positions, measurement_determinism="probabilistic"
    ):
        """
        Trace out qubits after disentangling them from the rest

        :param qubit_positions: the qubit positions to be traced out
        :type qubit_positions: list[int]
        :param measurement_determinism: if "probabilistic", measurement results are probabilistically selected
                if 1, measurement results default to 1 unless the probability of measuring p(1) = 0
                if 0, measurement results default to 0 unless the probability of measuring p(0) = 0
        :type measurement_determinism: str/int
        :return: nothing
        :rtype: None
        """
        self._mixture = [
            (
                p_i,
                sfc.partial_trace(
                    t_i,
                    keep=qubit_positions,
                    dims=self.n_qubits * [2],
                    measurement_determinism=measurement_determinism,
                ),
            )
            for (p_i, t_i) in self._mixture
        ]

    def partial_trace(self, keep, dims):
        """
        Trace out qubits after disentangling them from the rest

        :param keep: the qubit positions to be kept
        :type keep: list[int] or numpy.ndarray
        :param dims: dimension of each subsystem
        :type dims: list[int] or numpy.ndarray
        :return: nothing
        :rtype: None
        """
        self._mixture = [
            (
                p_i,
                sfc.partial_trace(
                    t_i,
                    keep=keep,
                    dims=dims,
                ),
            )
            for (p_i, t_i) in self._mixture
        ]

    def sort(self):
        """
        Sort the mixture according to descending order of probabilities

        :return: nothing
        :rtype: None
        """
        self.mixture = sorted(self._mixture, key=lambda item: item[0], reverse=True)

    def __str__(self):
        """
        Return a string representation of this state representation

        :return: a string representation of this state representation
        :rtype: str
        """
        s = f"{self.__class__.__name__} | {len(self._mixture)} tableaux in mixture"
        return s

    def __eq__(self, other):
        """
        Compare two MixedStabilizer objects

        :param other: the other MixedStabilizer to be compared
        :type other: MixedStabilizer
        :return: True if the stabilizer tableaux of two MixedStabilizer objects are the same
        :rtype: bool
        """
        # Treat two objects the same if and only if they have the same probability distribution
        # and same set of tableaux with the same probability
        if len(self._mixture) != len(other.mixture):
            return False
        # sort first
        self.sort()
        other.sort()
        for i in range(len(self._mixture)):
            if not np.isclose(self._mixture[i][0], other.mixture[i][0]):
                return False
            else:
                if self._mixture[i][1] != other.mixture[i][1]:
                    return False

        return True

data property writable

The data that represents the state given by the MixedStabilizer representation

Returns:

Type Description
list

the mixture that represents this state

mixture property writable

The mixture of pure states, represented as a list of tableaus and associated probabilities.

Returns:

Type Description
list

the mixture as a list of (probability_i, tableau_i)

n_qubits property

Returns the number of qubits in the stabilizer state

Returns:

Type Description
int

the number of qubits in the state

probability property

Computes the total probability as the summed probability of all pure states in the mixture \(\sum_i p_i \\ \forall (p_i, \mathcal{T}_i)\).

Returns:

Type Description
float

sum of probabilities

__eq__(other)

Compare two MixedStabilizer objects

Parameters:

Name Type Description Default
other MixedStabilizer

the other MixedStabilizer to be compared

required

Returns:

Type Description
bool

True if the stabilizer tableaux of two MixedStabilizer objects are the same

Source code in graphiq/backends/stabilizer/state.py
def __eq__(self, other):
    """
    Compare two MixedStabilizer objects

    :param other: the other MixedStabilizer to be compared
    :type other: MixedStabilizer
    :return: True if the stabilizer tableaux of two MixedStabilizer objects are the same
    :rtype: bool
    """
    # Treat two objects the same if and only if they have the same probability distribution
    # and same set of tableaux with the same probability
    if len(self._mixture) != len(other.mixture):
        return False
    # sort first
    self.sort()
    other.sort()
    for i in range(len(self._mixture)):
        if not np.isclose(self._mixture[i][0], other.mixture[i][0]):
            return False
        else:
            if self._mixture[i][1] != other.mixture[i][1]:
                return False

    return True

__str__()

Return a string representation of this state representation

Returns:

Type Description
str

a string representation of this state representation

Source code in graphiq/backends/stabilizer/state.py
def __str__(self):
    """
    Return a string representation of this state representation

    :return: a string representation of this state representation
    :rtype: str
    """
    s = f"{self.__class__.__name__} | {len(self._mixture)} tableaux in mixture"
    return s

apply_cnot(control, target)

Apply CNOT gate to the Stabilizer

Parameters:

Name Type Description Default
control int

the control qubit position where the gate is applied

required
target int

the target qubit position where the gate is applied

required

Returns:

Type Description
None

nothing

Source code in graphiq/backends/stabilizer/state.py
def apply_cnot(self, control, target):
    """
    Apply CNOT gate to the Stabilizer

    :param control: the control qubit position where the gate is applied
    :type control: int
    :param target: the target qubit position where the gate is applied
    :type target: int
    :return: nothing
    :rtype: None
    """
    self._mixture = [
        (p_i, transform.cnot_gate(t_i, control, target))
        for (p_i, t_i) in self._mixture
    ]

apply_conditioned_gate(qubit_position, outcomes, gate=None)

Apply a single-qubit gate, conditioned on a classical measurement outcome.

Parameters:

Name Type Description Default
qubit_position

int

required
outcomes

list of measurement outcomes for each tableau in the mixture

required
gate

str, one of 'x', 'y', 'z', or 'h'

None

Returns:

Type Description
Source code in graphiq/backends/stabilizer/state.py
def apply_conditioned_gate(self, qubit_position, outcomes, gate=None):
    """
    Apply a single-qubit gate, conditioned on a classical measurement outcome.

    :param qubit_position: int
    :param outcomes: list of measurement outcomes for each tableau in the mixture
    :param gate: str, one of 'x', 'y', 'z', or 'h'
    :return:
    """
    assert isinstance(outcomes, list)
    assert len(outcomes) == len(self._mixture)

    if gate == "x":
        trans = transform.x_gate
    elif gate == "y":
        trans = transform.y_gate
    elif gate == "z":
        trans = transform.z_gate
    elif gate == "h":
        trans = transform.hadamard_gate
    else:
        raise NotImplementedError(
            "Gate must be provided for conditioning measurement outcomes."
        )
    for i, outcome in enumerate(outcomes):
        if outcome == 1:
            p_i, t_i = self._mixture[i]
            self._mixture[i] = (p_i, trans(t_i, qubit_position))

apply_cz(control, target)

Apply CZ gate to the Stabilizer

Parameters:

Name Type Description Default
control int

the control qubit position where the gate is applied

required
target int

the target qubit position where the gate is applied

required

Returns:

Type Description
None

nothing

Source code in graphiq/backends/stabilizer/state.py
def apply_cz(self, control, target):
    """
    Apply CZ gate to the Stabilizer

    :param control: the control qubit position where the gate is applied
    :type control: int
    :param target: the target qubit position where the gate is applied
    :type target: int
    :return: nothing
    :rtype: None
    """
    self._mixture = [
        (p_i, transform.control_z_gate(t_i, control, target))
        for (p_i, t_i) in self._mixture
    ]

apply_hadamard(qubit_position)

Apply the Hadamard gate to the Stabilizer

Parameters:

Name Type Description Default
qubit_position int

the qubit position where the gate is applied

required

Returns:

Type Description
None

nothing

Source code in graphiq/backends/stabilizer/state.py
def apply_hadamard(self, qubit_position):
    """
    Apply the Hadamard gate to the Stabilizer

    :param qubit_position: the qubit position where the gate is applied
    :type qubit_position: int
    :return: nothing
    :rtype: None
    """
    self._mixture = [
        (p_i, transform.hadamard_gate(t_i, qubit_position))
        for (p_i, t_i) in self._mixture
    ]

apply_measurement(qubit_position, measurement_determinism='probabilistic')

Apply the measurement in the computational basis to a given qubit. For the MixedStabilizer state, we measure the outcome for each tableau in the mixture, returning a list of outcomes.

todo think of classical probabilities being stored on the c-registers?

Parameters:

Name Type Description Default
qubit_position int

the qubit position where the measurement is applied

required
measurement_determinism str/int

if "probabilistic", measurement results are probabilistically selected if 1, measurement results default to 1 unless the probability of measuring p(1) = 0 if 0, measurement results default to 0 unless the probability of measuring p(0) = 0

'probabilistic'

Returns:

Type Description
list

the measurement outcome

Source code in graphiq/backends/stabilizer/state.py
def apply_measurement(
    self, qubit_position, measurement_determinism="probabilistic"
):
    """
    Apply the measurement in the computational basis to a given qubit. For the MixedStabilizer state,
    we measure the outcome for each tableau in the mixture, returning a list of outcomes.

    # todo think of classical probabilities being stored on the c-registers?

    :param qubit_position: the qubit position where the measurement is applied
    :type qubit_position: int
    :param measurement_determinism: if "probabilistic", measurement results are probabilistically selected
            if 1, measurement results default to 1 unless the probability of measuring p(1) = 0
            if 0, measurement results default to 0 unless the probability of measuring p(0) = 0
    :type measurement_determinism: str/int
    :return: the measurement outcome
    :rtype: list
    """
    outcomes = []
    for i, (p_i, t_i) in enumerate(self._mixture):
        tableau, outcome, x_p = sfc.z_measurement_gate(
            t_i, qubit_position, measurement_determinism=measurement_determinism
        )
        outcomes.append(outcome)
        self._mixture[i] = (p_i, tableau)

    return outcomes

apply_phase(qubit_position)

Apply the phase gate to the Stabilizer

Parameters:

Name Type Description Default
qubit_position int

the qubit position where the gate is applied

required

Returns:

Type Description
None

nothing

Source code in graphiq/backends/stabilizer/state.py
def apply_phase(self, qubit_position):
    """
    Apply the phase gate to the Stabilizer

    :param qubit_position: the qubit position where the gate is applied
    :type qubit_position: int
    :return: nothing
    :rtype: None
    """
    self._mixture = [
        (p_i, transform.phase_gate(t_i, qubit_position))
        for (p_i, t_i) in self._mixture
    ]

apply_phase_dagger(qubit_position)

Apply the phase dagger gate to the Stabilizer

Parameters:

Name Type Description Default
qubit_position int

the qubit position where the gate is applied

required

Returns:

Type Description
None

nothing

Source code in graphiq/backends/stabilizer/state.py
def apply_phase_dagger(self, qubit_position):
    """
    Apply the phase dagger gate to the Stabilizer

    :param qubit_position: the qubit position where the gate is applied
    :type qubit_position: int
    :return: nothing
    :rtype: None
    """
    self._mixture = [
        (p_i, transform.phase_dagger_gate(t_i, qubit_position))
        for (p_i, t_i) in self._mixture
    ]

apply_sigmax(qubit_position)

Apply the X gate to the Stabilizer

Parameters:

Name Type Description Default
qubit_position int

the qubit position where the gate is applied

required

Returns:

Type Description
None

nothing

Source code in graphiq/backends/stabilizer/state.py
def apply_sigmax(self, qubit_position):
    """
    Apply the X gate to the Stabilizer

    :param qubit_position: the qubit position where the gate is applied
    :type qubit_position: int
    :return: nothing
    :rtype: None
    """
    self._mixture = [
        (p_i, transform.x_gate(t_i, qubit_position)) for (p_i, t_i) in self._mixture
    ]

apply_sigmay(qubit_position)

Apply the Y gate to the Stabilizer

Parameters:

Name Type Description Default
qubit_position int

the qubit position where the gate is applied

required

Returns:

Type Description
None

nothing

Source code in graphiq/backends/stabilizer/state.py
def apply_sigmay(self, qubit_position):
    """
    Apply the Y gate to the Stabilizer

    :param qubit_position: the qubit position where the gate is applied
    :type qubit_position: int
    :return: nothing
    :rtype: None
    """
    self._mixture = [
        (p_i, transform.y_gate(t_i, qubit_position)) for (p_i, t_i) in self._mixture
    ]

apply_sigmaz(qubit_position)

Apply the Z gate to the Stabilizer

Parameters:

Name Type Description Default
qubit_position int

the qubit position where the gate is applied

required

Returns:

Type Description
None

nothing

Source code in graphiq/backends/stabilizer/state.py
def apply_sigmaz(self, qubit_position):
    """
    Apply the Z gate to the Stabilizer

    :param qubit_position: the qubit position where the gate is applied
    :type qubit_position: int
    :return: nothing
    :rtype: None
    """
    self._mixture = [
        (p_i, transform.z_gate(t_i, qubit_position)) for (p_i, t_i) in self._mixture
    ]

partial_trace(keep, dims)

Trace out qubits after disentangling them from the rest

Parameters:

Name Type Description Default
keep list[int] | numpy.ndarray

the qubit positions to be kept

required
dims list[int] | numpy.ndarray

dimension of each subsystem

required

Returns:

Type Description
None

nothing

Source code in graphiq/backends/stabilizer/state.py
def partial_trace(self, keep, dims):
    """
    Trace out qubits after disentangling them from the rest

    :param keep: the qubit positions to be kept
    :type keep: list[int] or numpy.ndarray
    :param dims: dimension of each subsystem
    :type dims: list[int] or numpy.ndarray
    :return: nothing
    :rtype: None
    """
    self._mixture = [
        (
            p_i,
            sfc.partial_trace(
                t_i,
                keep=keep,
                dims=dims,
            ),
        )
        for (p_i, t_i) in self._mixture
    ]

reduce()

Reduce the number of tableaux store in the mixture by comparing the Hamming distance between them. Probabilities are summed and one tableau removed if they are the same.

Returns:

Type Description
None

nothing

Source code in graphiq/backends/stabilizer/state.py
def reduce(self):
    """
    Reduce the number of tableaux store in the mixture by comparing the Hamming distance between them.
    Probabilities are summed and one tableau removed if they are the same.

    :return: nothing
    :rtype: None
    """
    # TODO: explore other ways of reduction and further simplification using a standard form.
    mixture_temp = self._mixture
    mixture_reduce = []
    while len(mixture_temp) != 0:
        p0, t0 = mixture_temp[0]
        mixture_temp.pop(0)
        for i, (p_i, t_i) in enumerate(mixture_temp):
            if np.count_nonzero(t0 != t_i) == 0:
                p0 += p_i
                mixture_temp.pop(i)

        mixture_reduce.append((p0, t0))
    self._mixture = mixture_reduce

remove_qubit(qubit_position, measurement_determinism='probabilistic')

Trace out one qubit after disentangling it from the rest

Parameters:

Name Type Description Default
qubit_position int

the qubit position to be traced out

required
measurement_determinism str/int

if "probabilistic", measurement results are probabilistically selected if 1, measurement results default to 1 unless the probability of measuring p(1) = 0 if 0, measurement results default to 0 unless the probability of measuring p(0) = 0

'probabilistic'

Returns:

Type Description
None

nothing

Source code in graphiq/backends/stabilizer/state.py
def remove_qubit(self, qubit_position, measurement_determinism="probabilistic"):
    """
    Trace out one qubit after disentangling it from the rest

    :param qubit_position: the qubit position to be traced out
    :type qubit_position: int
    :param measurement_determinism: if "probabilistic", measurement results are probabilistically selected
            if 1, measurement results default to 1 unless the probability of measuring p(1) = 0
            if 0, measurement results default to 0 unless the probability of measuring p(0) = 0
    :type measurement_determinism: str/int
    :return: nothing
    :rtype: None
    """
    self._mixture = [
        (p_i, sfc.remove_qubit(t_i, qubit_position, measurement_determinism))
        for (p_i, t_i) in self._mixture
    ]

reset_qubit(qubit_position, measurement_determinism='probabilistic')

Reset a given qubit to \(|0\rangle\) state after disentangling it from the rest

Parameters:

Name Type Description Default
qubit_position int

the qubit position to be reset

required
measurement_determinism str/int

if "probabilistic", measurement results are probabilistically selected if 1, measurement results default to 1 unless the probability of measuring p(1) = 0 if 0, measurement results default to 0 unless the probability of measuring p(0) = 0

'probabilistic'

Returns:

Type Description
None

nothing

Source code in graphiq/backends/stabilizer/state.py
def reset_qubit(self, qubit_position, measurement_determinism="probabilistic"):
    r"""
    Reset a given qubit to $|0\rangle$ state after disentangling it from the rest

    :param qubit_position: the qubit position to be reset
    :type qubit_position: int
    :param measurement_determinism: if "probabilistic", measurement results are probabilistically selected
            if 1, measurement results default to 1 unless the probability of measuring p(1) = 0
            if 0, measurement results default to 0 unless the probability of measuring p(0) = 0
    :type measurement_determinism: str/int
    :return: nothing
    :rtype: None
    """
    self._mixture = [
        (p_i, sfc.reset_z(t_i, qubit_position, 0, measurement_determinism))
        for (p_i, t_i) in self._mixture
    ]

sort()

Sort the mixture according to descending order of probabilities

Returns:

Type Description
None

nothing

Source code in graphiq/backends/stabilizer/state.py
def sort(self):
    """
    Sort the mixture according to descending order of probabilities

    :return: nothing
    :rtype: None
    """
    self.mixture = sorted(self._mixture, key=lambda item: item[0], reverse=True)

trace_out_qubits(qubit_positions, measurement_determinism='probabilistic')

Trace out qubits after disentangling them from the rest

Parameters:

Name Type Description Default
qubit_positions list[int]

the qubit positions to be traced out

required
measurement_determinism str/int

if "probabilistic", measurement results are probabilistically selected if 1, measurement results default to 1 unless the probability of measuring p(1) = 0 if 0, measurement results default to 0 unless the probability of measuring p(0) = 0

'probabilistic'

Returns:

Type Description
None

nothing

Source code in graphiq/backends/stabilizer/state.py
def trace_out_qubits(
    self, qubit_positions, measurement_determinism="probabilistic"
):
    """
    Trace out qubits after disentangling them from the rest

    :param qubit_positions: the qubit positions to be traced out
    :type qubit_positions: list[int]
    :param measurement_determinism: if "probabilistic", measurement results are probabilistically selected
            if 1, measurement results default to 1 unless the probability of measuring p(1) = 0
            if 0, measurement results default to 0 unless the probability of measuring p(0) = 0
    :type measurement_determinism: str/int
    :return: nothing
    :rtype: None
    """
    self._mixture = [
        (
            p_i,
            sfc.partial_trace(
                t_i,
                keep=qubit_positions,
                dims=self.n_qubits * [2],
                measurement_determinism=measurement_determinism,
            ),
        )
        for (p_i, t_i) in self._mixture
    ]

graphiq.backends.graph.state.Graph

Bases: StateRepresentationBase

Graph representation of a graph state. As the intermediate states of the process may not be graph states (but assuming still stabilizer states), we may need to keep track of local Clifford gates that convert the state to the graph state represented by the graph.

Source code in graphiq/backends/graph/state.py
class Graph(StateRepresentationBase):
    """
    Graph representation of a graph state.
    As the intermediate states of the process may not be graph states (but assuming still stabilizer states),
    we may need to keep track of local Clifford gates that
    convert the state to the graph state represented by the graph.
    """

    def __init__(self, data, clifford_dict=None, *args, **kwargs):
        """
        Create a Graph representation object

        :param data: data used to construct the representation
        :type data: networkX.Graph
        :param clifford_dict: a dictionary that stores local Clifford for each node
        :type clifford_dict: dict
        :return: nothing
        :rtype: None
        """

        super().__init__(data, *args, **kwargs)
        if isinstance(data, nx.Graph):
            self._data = data
            if clifford_dict is None:
                for node in self._data.nodes:
                    self._data.nodes[node]["LC"] = [ops.Identity]
            else:
                for node in self._data.nodes:
                    if node in clifford_dict.keys():

                        self._data.nodes[node]["LC"] = _compile_lc_gates(
                            clifford_dict[node]
                        )
                    else:
                        self._data.nodes[node]["LC"] = [ops.Identity]
        else:
            raise TypeError(
                f"Cannot initialize the graph representation with datatype: {type(data)}"
            )

    @classmethod
    def valid_datatype(cls, data):
        """
        Validate the data type of the input data

        :param data: input data
        :type data: any
        :return: whether the data type is allowed for this class
        :rtype: bool
        """
        return isinstance(data, nx.Graph)

    @property
    def n_qubits(self):
        return self.n_nodes

    def find_lc(self, node_id):
        """
        Find the local Clifford gates corresponding to a node

        :param node_id: the node index
        :type node_id: int
        :return: local Clifford gates
        :rtype: ops.OneQubitOperationBase
        """
        if self._data.has_node(node_id):
            return self._data.nodes[node_id]["LC"]
        else:
            raise ValueError(f"Node with node ID {node_id} does not exist.")

    def update_lc(self, node_id, lc_gate):
        """
        Find the local Clifford gates corresponding to a node

        :param node_id: the node index
        :type node_id: int
        :param lc_gate: a list of local Clifford gates
        :type lc_gate: list(str)
        :return: local Clifford gates
        :rtype: ops.OneQubitOperationBase
        """
        if not self._data.has_node(node_id):
            raise ValueError(f"Node with node ID {node_id} does not exist.")
        if lc_gate is None or len(lc_gate) == 0:
            self._data.nodes[node_id]["LC"] = [ops.Identity]
        else:
            self._data.nodes[node_id]["LC"] = _compile_lc_gates(lc_gate)

    def add_node(self, node_to_add, lc_gate=None):
        """
        Add a node to the graph.

        :param node_to_add: node id to add to the Graph representation
        :type node_to_add: int
        :param lc_gate: a list of local Clifford gates
        :type lc_gate: list(str)
        :raises ValueError: if node_to_add is of an invalid datatype
        :return: nothing
        :rtype: None
        """
        if self._data.has_node(node_to_add):
            return
        if lc_gate is None:
            gate_to_add = [ops.Identity]
        else:
            gate_to_add = _compile_lc_gates(lc_gate)
        self._data.add_node(node_to_add, LC=gate_to_add)

    def add_edge(self, first_node, second_node):
        """
        Add an edge between two nodes. If any of these two nodes does not exist, no edge is added.

        :param first_node: the first node on which to add an edge
        :type first_node: int
        :param second_node: the second node on which to add an edge
        :type second_node: int
        :return: nothing
        :rtype: None
        """
        if not self._data.has_node(first_node):
            self.add_node(first_node)
        if not self._data.has_node(second_node):
            self.add_node(second_node)
        self._data.add_edge(first_node, second_node)

    def get_edges(self):
        """
        Get all graph edges (entangled pairs) in the Graph representation

        :return: graph edges
        :rtype: list
        """
        return list(self.data.edges)

    def get_nodes(self):
        """
        Get all graph nodes (qubits) in the Graph representation

        :return: all nodes in the Graph
        :rtype: list
        """
        return list(self.data.nodes)

    def lc_equivalent(self, other_graph, mode="deterministic"):
        r"""
        Determines whether two graph states are local-Clifford equivalent or not, given the adjacency matrices of the two.
        It takes two adjacency matrices as input and returns a numpy.ndarray containing $n (2 \times 2 array)s$
        = clifford operations on each qubit.

        :param other_graph: the other graph against which to check LC equivalence
        :type other_graph: Graph
        :param mode: the chosen mode for finding solutions. It can be either 'deterministic' (default) or 'random'.
        :type mode: str
        :raises AssertionError: if the number of rows in the row reduced matrix is less than the rank of coefficient
            matrix or if the number of linearly dependent columns is not equal to $4n - rank$
            (for $n$ being the number of nodes in the graph)
        :return: If a solution is found, returns True and an array of single-qubit Clifford $2 \times 2$ matrices
            in the symplectic formalism. If not, graphs are not LC equivalent and returns False, None.
        :rtype: bool, numpy.ndarray or None
        """
        g1 = nx.to_numpy_array(self.data).astype(int)
        g2 = nx.to_numpy_array(other_graph.data).astype(int)
        return is_lc_equivalent(g1, g2, mode=mode)

    @property
    def n_nodes(self):
        """
        Returns the number of nodes in the Graph

        :return: the number of nodes in the Graph
        :rtype: int
        """
        return self._data.number_of_nodes()

    def get_neighbors(self, node_id):
        """
        Return the list of all neighbors (i.e. nodes connected by an edge) of the node with node_id

        :param node_id: the ID of the node which we want to find the neighbours of
        :type node_id: int
        :return: a list of neighbours for the node with node_id
        :rtype: list
        """
        if self._data.has_node(node_id):
            return list(self._data.neighbors(node_id))
        else:
            raise ValueError(f"Node with node ID {node_id} does not exist.")

    def draw(self, show=True, ax=None, with_labels=True):
        """
        Draw the underlying networkX graph

        :param show: if True, the Graph is shown. If False, the Graph is drawn but not displayed
        :type show: bool
        :param ax: axis on which to draw the plot (optional)
        :type ax: matplotlib.Axis
        :param with_labels:
        :type with_labels:
        :return: nothing
        :rtype: None
        """
        draw_graph(self, show=show, ax=ax, with_labels=with_labels)

    def local_complementation(self, node_id, copy=False):
        """
        Takes the local complementation of the graph on the node indexed by node_id.

        Local complementation: let n(node) be the set of neighbours of node. If a, b in n(node) and (a, b) is in
        the set of edges E of graph, then remove (a, b) from E. If a, b in n(node) and (a, b) is NOT in E, then
        add (a, b) into E.

        The current implementation does not consider local Clifford gates. It assumes graph states.
        TODO: deal with general stabilizer states

        :param node_id: the ID of the node around which local complementation should take place
        :type node_id: int
        :return: the graph after the local complementation
        :rtype: Graph
        """

        if not Graph.is_graph_state(self):
            raise NotImplementedError(
                "Local complementation is not "
                "implemented for non-graph stabilizer states."
            )
        if copy:
            output_graph = self.copy()
        else:
            output_graph = self
        neighbors = self.get_neighbors(node_id)
        neighbor_pairs = itertools.combinations(neighbors, 2)
        for a, b in neighbor_pairs:
            if output_graph.data.has_edge(a, b):
                output_graph.data.remove_edge(a, b)
            else:
                output_graph.data.add_edge(a, b)
        return output_graph

    def copy(self):
        """
        Create a copy of this object

        :return: a copy of this Graph object
        :rtype: Graph
        """
        return copy.deepcopy(self)

    @classmethod
    def is_graph_state(cls, graph):
        """

        :param graph: an instance of Graph
        :type graph: Graph
        :return: True if graph is a graph state; False if graph is a non-graph stabilizer state
        :rtype: bool
        """
        assert isinstance(graph, Graph)
        # Check if all local Clifford gates are Identity
        for node in graph.data.nodes:
            if graph.data.nodes[node]["LC"] != [ops.Identity]:
                return False
        return True

n_nodes property

Returns the number of nodes in the Graph

Returns:

Type Description
int

the number of nodes in the Graph

__init__(data, clifford_dict=None, *args, **kwargs)

Create a Graph representation object

Parameters:

Name Type Description Default
data networkX.Graph

data used to construct the representation

required
clifford_dict dict

a dictionary that stores local Clifford for each node

None

Returns:

Type Description
None

nothing

Source code in graphiq/backends/graph/state.py
def __init__(self, data, clifford_dict=None, *args, **kwargs):
    """
    Create a Graph representation object

    :param data: data used to construct the representation
    :type data: networkX.Graph
    :param clifford_dict: a dictionary that stores local Clifford for each node
    :type clifford_dict: dict
    :return: nothing
    :rtype: None
    """

    super().__init__(data, *args, **kwargs)
    if isinstance(data, nx.Graph):
        self._data = data
        if clifford_dict is None:
            for node in self._data.nodes:
                self._data.nodes[node]["LC"] = [ops.Identity]
        else:
            for node in self._data.nodes:
                if node in clifford_dict.keys():

                    self._data.nodes[node]["LC"] = _compile_lc_gates(
                        clifford_dict[node]
                    )
                else:
                    self._data.nodes[node]["LC"] = [ops.Identity]
    else:
        raise TypeError(
            f"Cannot initialize the graph representation with datatype: {type(data)}"
        )

add_edge(first_node, second_node)

Add an edge between two nodes. If any of these two nodes does not exist, no edge is added.

Parameters:

Name Type Description Default
first_node int

the first node on which to add an edge

required
second_node int

the second node on which to add an edge

required

Returns:

Type Description
None

nothing

Source code in graphiq/backends/graph/state.py
def add_edge(self, first_node, second_node):
    """
    Add an edge between two nodes. If any of these two nodes does not exist, no edge is added.

    :param first_node: the first node on which to add an edge
    :type first_node: int
    :param second_node: the second node on which to add an edge
    :type second_node: int
    :return: nothing
    :rtype: None
    """
    if not self._data.has_node(first_node):
        self.add_node(first_node)
    if not self._data.has_node(second_node):
        self.add_node(second_node)
    self._data.add_edge(first_node, second_node)

add_node(node_to_add, lc_gate=None)

Add a node to the graph.

Parameters:

Name Type Description Default
node_to_add int

node id to add to the Graph representation

required
lc_gate list(str)

a list of local Clifford gates

None

Returns:

Type Description
None

nothing

Raises:

Type Description
ValueError

if node_to_add is of an invalid datatype

Source code in graphiq/backends/graph/state.py
def add_node(self, node_to_add, lc_gate=None):
    """
    Add a node to the graph.

    :param node_to_add: node id to add to the Graph representation
    :type node_to_add: int
    :param lc_gate: a list of local Clifford gates
    :type lc_gate: list(str)
    :raises ValueError: if node_to_add is of an invalid datatype
    :return: nothing
    :rtype: None
    """
    if self._data.has_node(node_to_add):
        return
    if lc_gate is None:
        gate_to_add = [ops.Identity]
    else:
        gate_to_add = _compile_lc_gates(lc_gate)
    self._data.add_node(node_to_add, LC=gate_to_add)

copy()

Create a copy of this object

Returns:

Type Description
Graph

a copy of this Graph object

Source code in graphiq/backends/graph/state.py
def copy(self):
    """
    Create a copy of this object

    :return: a copy of this Graph object
    :rtype: Graph
    """
    return copy.deepcopy(self)

draw(show=True, ax=None, with_labels=True)

Draw the underlying networkX graph

Parameters:

Name Type Description Default
show bool

if True, the Graph is shown. If False, the Graph is drawn but not displayed

True
ax matplotlib.Axis

axis on which to draw the plot (optional)

None
with_labels
True

Returns:

Type Description
None

nothing

Source code in graphiq/backends/graph/state.py
def draw(self, show=True, ax=None, with_labels=True):
    """
    Draw the underlying networkX graph

    :param show: if True, the Graph is shown. If False, the Graph is drawn but not displayed
    :type show: bool
    :param ax: axis on which to draw the plot (optional)
    :type ax: matplotlib.Axis
    :param with_labels:
    :type with_labels:
    :return: nothing
    :rtype: None
    """
    draw_graph(self, show=show, ax=ax, with_labels=with_labels)

find_lc(node_id)

Find the local Clifford gates corresponding to a node

Parameters:

Name Type Description Default
node_id int

the node index

required

Returns:

Type Description
ops.OneQubitOperationBase

local Clifford gates

Source code in graphiq/backends/graph/state.py
def find_lc(self, node_id):
    """
    Find the local Clifford gates corresponding to a node

    :param node_id: the node index
    :type node_id: int
    :return: local Clifford gates
    :rtype: ops.OneQubitOperationBase
    """
    if self._data.has_node(node_id):
        return self._data.nodes[node_id]["LC"]
    else:
        raise ValueError(f"Node with node ID {node_id} does not exist.")

get_edges()

Get all graph edges (entangled pairs) in the Graph representation

Returns:

Type Description
list

graph edges

Source code in graphiq/backends/graph/state.py
def get_edges(self):
    """
    Get all graph edges (entangled pairs) in the Graph representation

    :return: graph edges
    :rtype: list
    """
    return list(self.data.edges)

get_neighbors(node_id)

Return the list of all neighbors (i.e. nodes connected by an edge) of the node with node_id

Parameters:

Name Type Description Default
node_id int

the ID of the node which we want to find the neighbours of

required

Returns:

Type Description
list

a list of neighbours for the node with node_id

Source code in graphiq/backends/graph/state.py
def get_neighbors(self, node_id):
    """
    Return the list of all neighbors (i.e. nodes connected by an edge) of the node with node_id

    :param node_id: the ID of the node which we want to find the neighbours of
    :type node_id: int
    :return: a list of neighbours for the node with node_id
    :rtype: list
    """
    if self._data.has_node(node_id):
        return list(self._data.neighbors(node_id))
    else:
        raise ValueError(f"Node with node ID {node_id} does not exist.")

get_nodes()

Get all graph nodes (qubits) in the Graph representation

Returns:

Type Description
list

all nodes in the Graph

Source code in graphiq/backends/graph/state.py
def get_nodes(self):
    """
    Get all graph nodes (qubits) in the Graph representation

    :return: all nodes in the Graph
    :rtype: list
    """
    return list(self.data.nodes)

is_graph_state(graph) classmethod

Parameters:

Name Type Description Default
graph Graph

an instance of Graph

required

Returns:

Type Description
bool

True if graph is a graph state; False if graph is a non-graph stabilizer state

Source code in graphiq/backends/graph/state.py
@classmethod
def is_graph_state(cls, graph):
    """

    :param graph: an instance of Graph
    :type graph: Graph
    :return: True if graph is a graph state; False if graph is a non-graph stabilizer state
    :rtype: bool
    """
    assert isinstance(graph, Graph)
    # Check if all local Clifford gates are Identity
    for node in graph.data.nodes:
        if graph.data.nodes[node]["LC"] != [ops.Identity]:
            return False
    return True

lc_equivalent(other_graph, mode='deterministic')

Determines whether two graph states are local-Clifford equivalent or not, given the adjacency matrices of the two. It takes two adjacency matrices as input and returns a numpy.ndarray containing \(n (2 \times 2 array)s\) = clifford operations on each qubit.

Parameters:

Name Type Description Default
other_graph Graph

the other graph against which to check LC equivalence

required
mode str

the chosen mode for finding solutions. It can be either 'deterministic' (default) or 'random'.

'deterministic'

Returns:

Type Description
bool, numpy.ndarray | None

If a solution is found, returns True and an array of single-qubit Clifford \(2 \times 2\) matrices in the symplectic formalism. If not, graphs are not LC equivalent and returns False, None.

Raises:

Type Description
AssertionError

if the number of rows in the row reduced matrix is less than the rank of coefficient matrix or if the number of linearly dependent columns is not equal to \(4n - rank\) (for \(n\) being the number of nodes in the graph)

Source code in graphiq/backends/graph/state.py
def lc_equivalent(self, other_graph, mode="deterministic"):
    r"""
    Determines whether two graph states are local-Clifford equivalent or not, given the adjacency matrices of the two.
    It takes two adjacency matrices as input and returns a numpy.ndarray containing $n (2 \times 2 array)s$
    = clifford operations on each qubit.

    :param other_graph: the other graph against which to check LC equivalence
    :type other_graph: Graph
    :param mode: the chosen mode for finding solutions. It can be either 'deterministic' (default) or 'random'.
    :type mode: str
    :raises AssertionError: if the number of rows in the row reduced matrix is less than the rank of coefficient
        matrix or if the number of linearly dependent columns is not equal to $4n - rank$
        (for $n$ being the number of nodes in the graph)
    :return: If a solution is found, returns True and an array of single-qubit Clifford $2 \times 2$ matrices
        in the symplectic formalism. If not, graphs are not LC equivalent and returns False, None.
    :rtype: bool, numpy.ndarray or None
    """
    g1 = nx.to_numpy_array(self.data).astype(int)
    g2 = nx.to_numpy_array(other_graph.data).astype(int)
    return is_lc_equivalent(g1, g2, mode=mode)

local_complementation(node_id, copy=False)

Takes the local complementation of the graph on the node indexed by node_id.

Local complementation: let n(node) be the set of neighbours of node. If a, b in n(node) and (a, b) is in the set of edges E of graph, then remove (a, b) from E. If a, b in n(node) and (a, b) is NOT in E, then add (a, b) into E.

The current implementation does not consider local Clifford gates. It assumes graph states. TODO: deal with general stabilizer states

Parameters:

Name Type Description Default
node_id int

the ID of the node around which local complementation should take place

required

Returns:

Type Description
Graph

the graph after the local complementation

Source code in graphiq/backends/graph/state.py
def local_complementation(self, node_id, copy=False):
    """
    Takes the local complementation of the graph on the node indexed by node_id.

    Local complementation: let n(node) be the set of neighbours of node. If a, b in n(node) and (a, b) is in
    the set of edges E of graph, then remove (a, b) from E. If a, b in n(node) and (a, b) is NOT in E, then
    add (a, b) into E.

    The current implementation does not consider local Clifford gates. It assumes graph states.
    TODO: deal with general stabilizer states

    :param node_id: the ID of the node around which local complementation should take place
    :type node_id: int
    :return: the graph after the local complementation
    :rtype: Graph
    """

    if not Graph.is_graph_state(self):
        raise NotImplementedError(
            "Local complementation is not "
            "implemented for non-graph stabilizer states."
        )
    if copy:
        output_graph = self.copy()
    else:
        output_graph = self
    neighbors = self.get_neighbors(node_id)
    neighbor_pairs = itertools.combinations(neighbors, 2)
    for a, b in neighbor_pairs:
        if output_graph.data.has_edge(a, b):
            output_graph.data.remove_edge(a, b)
        else:
            output_graph.data.add_edge(a, b)
    return output_graph

update_lc(node_id, lc_gate)

Find the local Clifford gates corresponding to a node

Parameters:

Name Type Description Default
node_id int

the node index

required
lc_gate list(str)

a list of local Clifford gates

required

Returns:

Type Description
ops.OneQubitOperationBase

local Clifford gates

Source code in graphiq/backends/graph/state.py
def update_lc(self, node_id, lc_gate):
    """
    Find the local Clifford gates corresponding to a node

    :param node_id: the node index
    :type node_id: int
    :param lc_gate: a list of local Clifford gates
    :type lc_gate: list(str)
    :return: local Clifford gates
    :rtype: ops.OneQubitOperationBase
    """
    if not self._data.has_node(node_id):
        raise ValueError(f"Node with node ID {node_id} does not exist.")
    if lc_gate is None or len(lc_gate) == 0:
        self._data.nodes[node_id]["LC"] = [ops.Identity]
    else:
        self._data.nodes[node_id]["LC"] = _compile_lc_gates(lc_gate)

valid_datatype(data) classmethod

Validate the data type of the input data

Parameters:

Name Type Description Default
data any

input data

required

Returns:

Type Description
bool

whether the data type is allowed for this class

Source code in graphiq/backends/graph/state.py
@classmethod
def valid_datatype(cls, data):
    """
    Validate the data type of the input data

    :param data: input data
    :type data: any
    :return: whether the data type is allowed for this class
    :rtype: bool
    """
    return isinstance(data, nx.Graph)