Skip to content

References

Ansaetze#

from qml_essentials.ansaetze import Ansaetze
Source code in qml_essentials/ansaetze.py
 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
class Ansaetze:
    def get_available():
        return [
            Ansaetze.No_Ansatz,
            Ansaetze.Circuit_1,
            Ansaetze.Circuit_9,
            Ansaetze.Circuit_15,
            Ansaetze.Circuit_18,
            Ansaetze.Circuit_19,
            Ansaetze.No_Entangling,
            Ansaetze.Strongly_Entangling,
            Ansaetze.Hardware_Efficient,
        ]

    class No_Ansatz(Circuit):
        @staticmethod
        def n_params_per_layer(n_qubits: int) -> int:
            return 0

        @staticmethod
        def get_control_indices(n_qubits: int) -> Optional[np.ndarray]:
            return None

        @staticmethod
        def build(w: np.ndarray, n_qubits: int):
            pass

    class Hardware_Efficient(Circuit):
        @staticmethod
        def n_params_per_layer(n_qubits: int) -> int:
            if n_qubits > 1:
                return n_qubits * 3
            else:
                log.warning("Number of Qubits < 2, no entanglement available")
                return 2

        @staticmethod
        def get_control_indices(n_qubits: int) -> Optional[np.ndarray]:
            return None

        @staticmethod
        def build(w: np.ndarray, n_qubits: int):
            """
            Creates a Circuit19 ansatz.

            Length of flattened vector must be n_qubits*3-1
            because for >1 qubits there are three gates

            Args:
                w (np.ndarray): weight vector of size n_layers*(n_qubits*3-1)
                n_qubits (int): number of qubits
            """
            w_idx = 0
            for q in range(n_qubits):
                qml.RY(w[w_idx], wires=q)
                w_idx += 1
                qml.RZ(w[w_idx], wires=q)
                w_idx += 1

            if n_qubits > 1:
                for q in range(n_qubits - 1):
                    qml.CZ(wires=[q, q + 1])

    class Circuit_19(Circuit):
        @staticmethod
        def n_params_per_layer(n_qubits: int) -> int:
            if n_qubits > 1:
                return n_qubits * 3
            else:
                log.warning("Number of Qubits < 2, no entanglement available")
                return 2

        @staticmethod
        def get_control_indices(n_qubits: int) -> Optional[np.ndarray]:
            if n_qubits > 1:
                return [-n_qubits, None, None]
            else:
                return None

        @staticmethod
        def build(w: np.ndarray, n_qubits: int):
            """
            Creates a Circuit19 ansatz.

            Length of flattened vector must be n_qubits*3-1
            because for >1 qubits there are three gates

            Args:
                w (np.ndarray): weight vector of size n_layers*(n_qubits*3-1)
                n_qubits (int): number of qubits
            """
            w_idx = 0
            for q in range(n_qubits):
                qml.RX(w[w_idx], wires=q)
                w_idx += 1
                qml.RZ(w[w_idx], wires=q)
                w_idx += 1

            if n_qubits > 1:
                for q in range(n_qubits):
                    qml.CRX(
                        w[w_idx],
                        wires=[n_qubits - q - 1, (n_qubits - q) % n_qubits],
                    )
                    w_idx += 1

    class Circuit_18(Circuit):
        @staticmethod
        def n_params_per_layer(n_qubits: int) -> int:
            if n_qubits > 1:
                return n_qubits * 3
            else:
                log.warning("Number of Qubits < 2, no entanglement available")
                return 2

        @staticmethod
        def get_control_indices(n_qubits: int) -> Optional[np.ndarray]:
            if n_qubits > 1:
                return [-n_qubits, None, None]
            else:
                return None

        @staticmethod
        def build(w: np.ndarray, n_qubits: int):
            """
            Creates a Circuit18 ansatz.

            Length of flattened vector must be n_qubits*3-1
            because for >1 qubits there are three gates

            Args:
                w (np.ndarray): weight vector of size n_layers*(n_qubits*3-1)
                n_qubits (int): number of qubits
            """
            w_idx = 0
            for q in range(n_qubits):
                qml.RX(w[w_idx], wires=q)
                w_idx += 1
                qml.RZ(w[w_idx], wires=q)
                w_idx += 1

            if n_qubits > 1:
                for q in range(n_qubits):
                    qml.CRZ(
                        w[w_idx],
                        wires=[n_qubits - q - 1, (n_qubits - q) % n_qubits],
                    )
                    w_idx += 1

    class Circuit_15(Circuit):
        @staticmethod
        def n_params_per_layer(n_qubits: int) -> int:
            if n_qubits > 1:
                return n_qubits * 3
            else:
                log.warning("Number of Qubits < 2, no entanglement available")
                return 2

        @staticmethod
        def get_control_indices(n_qubits: int) -> Optional[np.ndarray]:
            return None

        @staticmethod
        def build(w: np.ndarray, n_qubits: int):
            """
            Creates a Circuit15 ansatz.

            Length of flattened vector must be n_qubits*3-1
            because for >1 qubits there are three gates

            Args:
                w (np.ndarray): weight vector of size n_layers*(n_qubits*3-1)
                n_qubits (int): number of qubits
            """
            raise NotImplementedError  # Did not figured out the entangling sequence yet

            w_idx = 0
            for q in range(n_qubits):
                qml.RX(w[w_idx], wires=q)
                w_idx += 1

            if n_qubits > 1:
                for q in range(n_qubits):
                    qml.CNOT(wires=[n_qubits - q - 1, (n_qubits - q) % n_qubits])

            for q in range(n_qubits):
                qml.RZ(w[w_idx], wires=q)
                w_idx += 1

    class Circuit_9(Circuit):
        @staticmethod
        def n_params_per_layer(n_qubits: int) -> int:
            return n_qubits

        @staticmethod
        def get_control_indices(n_qubits: int) -> Optional[np.ndarray]:
            return None

        @staticmethod
        def build(w: np.ndarray, n_qubits: int):
            """
            Creates a Circuit19 ansatz.

            Length of flattened vector must be n_qubits*3-1
            because for >1 qubits there are three gates

            Args:
                w (np.ndarray): weight vector of size n_layers*(n_qubits*3-1)
                n_qubits (int): number of qubits
            """
            w_idx = 0
            for q in range(n_qubits):
                qml.Hadamard(wires=q)

            if n_qubits > 1:
                for q in range(n_qubits - 1):
                    qml.CZ(wires=[n_qubits - q - 2, n_qubits - q - 1])

            for q in range(n_qubits):
                qml.RX(w[w_idx], wires=q)
                w_idx += 1

    class Circuit_1(Circuit):
        @staticmethod
        def n_params_per_layer(n_qubits: int) -> int:
            return n_qubits * 2

        @staticmethod
        def get_control_indices(n_qubits: int) -> Optional[np.ndarray]:
            return None

        @staticmethod
        def build(w: np.ndarray, n_qubits: int):
            """
            Creates a Circuit1 ansatz.

            Length of flattened vector must be n_qubits*2

            Args:
                w (np.ndarray): weight vector of size n_layers*(n_qubits*2)
                n_qubits (int): number of qubits
            """
            w_idx = 0
            for q in range(n_qubits):
                qml.RX(w[w_idx], wires=q)
                w_idx += 1
                qml.RZ(w[w_idx], wires=q)
                w_idx += 1

    class Strongly_Entangling(Circuit):
        @staticmethod
        def n_params_per_layer(n_qubits: int) -> int:
            if n_qubits > 1:
                return n_qubits * 6
            else:
                log.warning("Number of Qubits < 2, no entanglement available")
                return 2

        @staticmethod
        def get_control_indices(n_qubits: int) -> Optional[np.ndarray]:
            return None

        @staticmethod
        def build(w: np.ndarray, n_qubits: int) -> None:
            """
            Creates a StronglyEntanglingLayers ansatz.

            Args:
                w (np.ndarray): weight vector of size n_layers*(n_qubits*3)
                n_qubits (int): number of qubits
            """
            w_idx = 0
            for q in range(n_qubits):
                qml.Rot(w[w_idx], w[w_idx + 1], w[w_idx + 2], wires=q)
                w_idx += 3

            if n_qubits > 1:
                for q in range(n_qubits):
                    qml.CNOT(wires=[q, (q + 1) % n_qubits])

            for q in range(n_qubits):
                qml.Rot(w[w_idx], w[w_idx + 1], w[w_idx + 2], wires=q)
                w_idx += 3

            if n_qubits > 1:
                for q in range(n_qubits):
                    qml.CNOT(wires=[q, (q + n_qubits // 2) % n_qubits])

    class No_Entangling(Circuit):
        @staticmethod
        def n_params_per_layer(n_qubits: int) -> int:
            return n_qubits * 3

        @staticmethod
        def get_control_indices(n_qubits: int) -> Optional[np.ndarray]:
            return None

        @staticmethod
        def build(w: np.ndarray, n_qubits: int):
            """
            Creates a circuit without entangling, but with U3 gates on all qubits

            Length of flattened vector must be n_qubits*3

            Args:
                w (np.ndarray): weight vector of size n_layers*(n_qubits*3)
                n_qubits (int): number of qubits
            """
            w_idx = 0
            for q in range(n_qubits):
                qml.Rot(w[w_idx], w[w_idx + 1], w[w_idx + 2], wires=q)
                w_idx += 3

Circuit_1 #

Bases: Circuit

Source code in qml_essentials/ansaetze.py
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
class Circuit_1(Circuit):
    @staticmethod
    def n_params_per_layer(n_qubits: int) -> int:
        return n_qubits * 2

    @staticmethod
    def get_control_indices(n_qubits: int) -> Optional[np.ndarray]:
        return None

    @staticmethod
    def build(w: np.ndarray, n_qubits: int):
        """
        Creates a Circuit1 ansatz.

        Length of flattened vector must be n_qubits*2

        Args:
            w (np.ndarray): weight vector of size n_layers*(n_qubits*2)
            n_qubits (int): number of qubits
        """
        w_idx = 0
        for q in range(n_qubits):
            qml.RX(w[w_idx], wires=q)
            w_idx += 1
            qml.RZ(w[w_idx], wires=q)
            w_idx += 1

build(w, n_qubits) staticmethod #

Creates a Circuit1 ansatz.

Length of flattened vector must be n_qubits*2

Parameters:

Name Type Description Default
w ndarray

weight vector of size n_layers(n_qubits2)

required
n_qubits int

number of qubits

required
Source code in qml_essentials/ansaetze.py
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
@staticmethod
def build(w: np.ndarray, n_qubits: int):
    """
    Creates a Circuit1 ansatz.

    Length of flattened vector must be n_qubits*2

    Args:
        w (np.ndarray): weight vector of size n_layers*(n_qubits*2)
        n_qubits (int): number of qubits
    """
    w_idx = 0
    for q in range(n_qubits):
        qml.RX(w[w_idx], wires=q)
        w_idx += 1
        qml.RZ(w[w_idx], wires=q)
        w_idx += 1

Circuit_15 #

Bases: Circuit

Source code in qml_essentials/ansaetze.py
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
class Circuit_15(Circuit):
    @staticmethod
    def n_params_per_layer(n_qubits: int) -> int:
        if n_qubits > 1:
            return n_qubits * 3
        else:
            log.warning("Number of Qubits < 2, no entanglement available")
            return 2

    @staticmethod
    def get_control_indices(n_qubits: int) -> Optional[np.ndarray]:
        return None

    @staticmethod
    def build(w: np.ndarray, n_qubits: int):
        """
        Creates a Circuit15 ansatz.

        Length of flattened vector must be n_qubits*3-1
        because for >1 qubits there are three gates

        Args:
            w (np.ndarray): weight vector of size n_layers*(n_qubits*3-1)
            n_qubits (int): number of qubits
        """
        raise NotImplementedError  # Did not figured out the entangling sequence yet

        w_idx = 0
        for q in range(n_qubits):
            qml.RX(w[w_idx], wires=q)
            w_idx += 1

        if n_qubits > 1:
            for q in range(n_qubits):
                qml.CNOT(wires=[n_qubits - q - 1, (n_qubits - q) % n_qubits])

        for q in range(n_qubits):
            qml.RZ(w[w_idx], wires=q)
            w_idx += 1

build(w, n_qubits) staticmethod #

Creates a Circuit15 ansatz.

Length of flattened vector must be n_qubits*3-1 because for >1 qubits there are three gates

Parameters:

Name Type Description Default
w ndarray

weight vector of size n_layers(n_qubits3-1)

required
n_qubits int

number of qubits

required
Source code in qml_essentials/ansaetze.py
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
@staticmethod
def build(w: np.ndarray, n_qubits: int):
    """
    Creates a Circuit15 ansatz.

    Length of flattened vector must be n_qubits*3-1
    because for >1 qubits there are three gates

    Args:
        w (np.ndarray): weight vector of size n_layers*(n_qubits*3-1)
        n_qubits (int): number of qubits
    """
    raise NotImplementedError  # Did not figured out the entangling sequence yet

    w_idx = 0
    for q in range(n_qubits):
        qml.RX(w[w_idx], wires=q)
        w_idx += 1

    if n_qubits > 1:
        for q in range(n_qubits):
            qml.CNOT(wires=[n_qubits - q - 1, (n_qubits - q) % n_qubits])

    for q in range(n_qubits):
        qml.RZ(w[w_idx], wires=q)
        w_idx += 1

Circuit_18 #

Bases: Circuit

Source code in qml_essentials/ansaetze.py
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
class Circuit_18(Circuit):
    @staticmethod
    def n_params_per_layer(n_qubits: int) -> int:
        if n_qubits > 1:
            return n_qubits * 3
        else:
            log.warning("Number of Qubits < 2, no entanglement available")
            return 2

    @staticmethod
    def get_control_indices(n_qubits: int) -> Optional[np.ndarray]:
        if n_qubits > 1:
            return [-n_qubits, None, None]
        else:
            return None

    @staticmethod
    def build(w: np.ndarray, n_qubits: int):
        """
        Creates a Circuit18 ansatz.

        Length of flattened vector must be n_qubits*3-1
        because for >1 qubits there are three gates

        Args:
            w (np.ndarray): weight vector of size n_layers*(n_qubits*3-1)
            n_qubits (int): number of qubits
        """
        w_idx = 0
        for q in range(n_qubits):
            qml.RX(w[w_idx], wires=q)
            w_idx += 1
            qml.RZ(w[w_idx], wires=q)
            w_idx += 1

        if n_qubits > 1:
            for q in range(n_qubits):
                qml.CRZ(
                    w[w_idx],
                    wires=[n_qubits - q - 1, (n_qubits - q) % n_qubits],
                )
                w_idx += 1

build(w, n_qubits) staticmethod #

Creates a Circuit18 ansatz.

Length of flattened vector must be n_qubits*3-1 because for >1 qubits there are three gates

Parameters:

Name Type Description Default
w ndarray

weight vector of size n_layers(n_qubits3-1)

required
n_qubits int

number of qubits

required
Source code in qml_essentials/ansaetze.py
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
@staticmethod
def build(w: np.ndarray, n_qubits: int):
    """
    Creates a Circuit18 ansatz.

    Length of flattened vector must be n_qubits*3-1
    because for >1 qubits there are three gates

    Args:
        w (np.ndarray): weight vector of size n_layers*(n_qubits*3-1)
        n_qubits (int): number of qubits
    """
    w_idx = 0
    for q in range(n_qubits):
        qml.RX(w[w_idx], wires=q)
        w_idx += 1
        qml.RZ(w[w_idx], wires=q)
        w_idx += 1

    if n_qubits > 1:
        for q in range(n_qubits):
            qml.CRZ(
                w[w_idx],
                wires=[n_qubits - q - 1, (n_qubits - q) % n_qubits],
            )
            w_idx += 1

Circuit_19 #

Bases: Circuit

Source code in qml_essentials/ansaetze.py
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
class Circuit_19(Circuit):
    @staticmethod
    def n_params_per_layer(n_qubits: int) -> int:
        if n_qubits > 1:
            return n_qubits * 3
        else:
            log.warning("Number of Qubits < 2, no entanglement available")
            return 2

    @staticmethod
    def get_control_indices(n_qubits: int) -> Optional[np.ndarray]:
        if n_qubits > 1:
            return [-n_qubits, None, None]
        else:
            return None

    @staticmethod
    def build(w: np.ndarray, n_qubits: int):
        """
        Creates a Circuit19 ansatz.

        Length of flattened vector must be n_qubits*3-1
        because for >1 qubits there are three gates

        Args:
            w (np.ndarray): weight vector of size n_layers*(n_qubits*3-1)
            n_qubits (int): number of qubits
        """
        w_idx = 0
        for q in range(n_qubits):
            qml.RX(w[w_idx], wires=q)
            w_idx += 1
            qml.RZ(w[w_idx], wires=q)
            w_idx += 1

        if n_qubits > 1:
            for q in range(n_qubits):
                qml.CRX(
                    w[w_idx],
                    wires=[n_qubits - q - 1, (n_qubits - q) % n_qubits],
                )
                w_idx += 1

build(w, n_qubits) staticmethod #

Creates a Circuit19 ansatz.

Length of flattened vector must be n_qubits*3-1 because for >1 qubits there are three gates

Parameters:

Name Type Description Default
w ndarray

weight vector of size n_layers(n_qubits3-1)

required
n_qubits int

number of qubits

required
Source code in qml_essentials/ansaetze.py
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
@staticmethod
def build(w: np.ndarray, n_qubits: int):
    """
    Creates a Circuit19 ansatz.

    Length of flattened vector must be n_qubits*3-1
    because for >1 qubits there are three gates

    Args:
        w (np.ndarray): weight vector of size n_layers*(n_qubits*3-1)
        n_qubits (int): number of qubits
    """
    w_idx = 0
    for q in range(n_qubits):
        qml.RX(w[w_idx], wires=q)
        w_idx += 1
        qml.RZ(w[w_idx], wires=q)
        w_idx += 1

    if n_qubits > 1:
        for q in range(n_qubits):
            qml.CRX(
                w[w_idx],
                wires=[n_qubits - q - 1, (n_qubits - q) % n_qubits],
            )
            w_idx += 1

Circuit_9 #

Bases: Circuit

Source code in qml_essentials/ansaetze.py
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
class Circuit_9(Circuit):
    @staticmethod
    def n_params_per_layer(n_qubits: int) -> int:
        return n_qubits

    @staticmethod
    def get_control_indices(n_qubits: int) -> Optional[np.ndarray]:
        return None

    @staticmethod
    def build(w: np.ndarray, n_qubits: int):
        """
        Creates a Circuit19 ansatz.

        Length of flattened vector must be n_qubits*3-1
        because for >1 qubits there are three gates

        Args:
            w (np.ndarray): weight vector of size n_layers*(n_qubits*3-1)
            n_qubits (int): number of qubits
        """
        w_idx = 0
        for q in range(n_qubits):
            qml.Hadamard(wires=q)

        if n_qubits > 1:
            for q in range(n_qubits - 1):
                qml.CZ(wires=[n_qubits - q - 2, n_qubits - q - 1])

        for q in range(n_qubits):
            qml.RX(w[w_idx], wires=q)
            w_idx += 1

build(w, n_qubits) staticmethod #

Creates a Circuit19 ansatz.

Length of flattened vector must be n_qubits*3-1 because for >1 qubits there are three gates

Parameters:

Name Type Description Default
w ndarray

weight vector of size n_layers(n_qubits3-1)

required
n_qubits int

number of qubits

required
Source code in qml_essentials/ansaetze.py
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
@staticmethod
def build(w: np.ndarray, n_qubits: int):
    """
    Creates a Circuit19 ansatz.

    Length of flattened vector must be n_qubits*3-1
    because for >1 qubits there are three gates

    Args:
        w (np.ndarray): weight vector of size n_layers*(n_qubits*3-1)
        n_qubits (int): number of qubits
    """
    w_idx = 0
    for q in range(n_qubits):
        qml.Hadamard(wires=q)

    if n_qubits > 1:
        for q in range(n_qubits - 1):
            qml.CZ(wires=[n_qubits - q - 2, n_qubits - q - 1])

    for q in range(n_qubits):
        qml.RX(w[w_idx], wires=q)
        w_idx += 1

Hardware_Efficient #

Bases: Circuit

Source code in qml_essentials/ansaetze.py
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
class Hardware_Efficient(Circuit):
    @staticmethod
    def n_params_per_layer(n_qubits: int) -> int:
        if n_qubits > 1:
            return n_qubits * 3
        else:
            log.warning("Number of Qubits < 2, no entanglement available")
            return 2

    @staticmethod
    def get_control_indices(n_qubits: int) -> Optional[np.ndarray]:
        return None

    @staticmethod
    def build(w: np.ndarray, n_qubits: int):
        """
        Creates a Circuit19 ansatz.

        Length of flattened vector must be n_qubits*3-1
        because for >1 qubits there are three gates

        Args:
            w (np.ndarray): weight vector of size n_layers*(n_qubits*3-1)
            n_qubits (int): number of qubits
        """
        w_idx = 0
        for q in range(n_qubits):
            qml.RY(w[w_idx], wires=q)
            w_idx += 1
            qml.RZ(w[w_idx], wires=q)
            w_idx += 1

        if n_qubits > 1:
            for q in range(n_qubits - 1):
                qml.CZ(wires=[q, q + 1])

build(w, n_qubits) staticmethod #

Creates a Circuit19 ansatz.

Length of flattened vector must be n_qubits*3-1 because for >1 qubits there are three gates

Parameters:

Name Type Description Default
w ndarray

weight vector of size n_layers(n_qubits3-1)

required
n_qubits int

number of qubits

required
Source code in qml_essentials/ansaetze.py
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
@staticmethod
def build(w: np.ndarray, n_qubits: int):
    """
    Creates a Circuit19 ansatz.

    Length of flattened vector must be n_qubits*3-1
    because for >1 qubits there are three gates

    Args:
        w (np.ndarray): weight vector of size n_layers*(n_qubits*3-1)
        n_qubits (int): number of qubits
    """
    w_idx = 0
    for q in range(n_qubits):
        qml.RY(w[w_idx], wires=q)
        w_idx += 1
        qml.RZ(w[w_idx], wires=q)
        w_idx += 1

    if n_qubits > 1:
        for q in range(n_qubits - 1):
            qml.CZ(wires=[q, q + 1])

No_Entangling #

Bases: Circuit

Source code in qml_essentials/ansaetze.py
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
class No_Entangling(Circuit):
    @staticmethod
    def n_params_per_layer(n_qubits: int) -> int:
        return n_qubits * 3

    @staticmethod
    def get_control_indices(n_qubits: int) -> Optional[np.ndarray]:
        return None

    @staticmethod
    def build(w: np.ndarray, n_qubits: int):
        """
        Creates a circuit without entangling, but with U3 gates on all qubits

        Length of flattened vector must be n_qubits*3

        Args:
            w (np.ndarray): weight vector of size n_layers*(n_qubits*3)
            n_qubits (int): number of qubits
        """
        w_idx = 0
        for q in range(n_qubits):
            qml.Rot(w[w_idx], w[w_idx + 1], w[w_idx + 2], wires=q)
            w_idx += 3

build(w, n_qubits) staticmethod #

Creates a circuit without entangling, but with U3 gates on all qubits

Length of flattened vector must be n_qubits*3

Parameters:

Name Type Description Default
w ndarray

weight vector of size n_layers(n_qubits3)

required
n_qubits int

number of qubits

required
Source code in qml_essentials/ansaetze.py
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
@staticmethod
def build(w: np.ndarray, n_qubits: int):
    """
    Creates a circuit without entangling, but with U3 gates on all qubits

    Length of flattened vector must be n_qubits*3

    Args:
        w (np.ndarray): weight vector of size n_layers*(n_qubits*3)
        n_qubits (int): number of qubits
    """
    w_idx = 0
    for q in range(n_qubits):
        qml.Rot(w[w_idx], w[w_idx + 1], w[w_idx + 2], wires=q)
        w_idx += 3

Strongly_Entangling #

Bases: Circuit

Source code in qml_essentials/ansaetze.py
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
class Strongly_Entangling(Circuit):
    @staticmethod
    def n_params_per_layer(n_qubits: int) -> int:
        if n_qubits > 1:
            return n_qubits * 6
        else:
            log.warning("Number of Qubits < 2, no entanglement available")
            return 2

    @staticmethod
    def get_control_indices(n_qubits: int) -> Optional[np.ndarray]:
        return None

    @staticmethod
    def build(w: np.ndarray, n_qubits: int) -> None:
        """
        Creates a StronglyEntanglingLayers ansatz.

        Args:
            w (np.ndarray): weight vector of size n_layers*(n_qubits*3)
            n_qubits (int): number of qubits
        """
        w_idx = 0
        for q in range(n_qubits):
            qml.Rot(w[w_idx], w[w_idx + 1], w[w_idx + 2], wires=q)
            w_idx += 3

        if n_qubits > 1:
            for q in range(n_qubits):
                qml.CNOT(wires=[q, (q + 1) % n_qubits])

        for q in range(n_qubits):
            qml.Rot(w[w_idx], w[w_idx + 1], w[w_idx + 2], wires=q)
            w_idx += 3

        if n_qubits > 1:
            for q in range(n_qubits):
                qml.CNOT(wires=[q, (q + n_qubits // 2) % n_qubits])

build(w, n_qubits) staticmethod #

Creates a StronglyEntanglingLayers ansatz.

Parameters:

Name Type Description Default
w ndarray

weight vector of size n_layers(n_qubits3)

required
n_qubits int

number of qubits

required
Source code in qml_essentials/ansaetze.py
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
@staticmethod
def build(w: np.ndarray, n_qubits: int) -> None:
    """
    Creates a StronglyEntanglingLayers ansatz.

    Args:
        w (np.ndarray): weight vector of size n_layers*(n_qubits*3)
        n_qubits (int): number of qubits
    """
    w_idx = 0
    for q in range(n_qubits):
        qml.Rot(w[w_idx], w[w_idx + 1], w[w_idx + 2], wires=q)
        w_idx += 3

    if n_qubits > 1:
        for q in range(n_qubits):
            qml.CNOT(wires=[q, (q + 1) % n_qubits])

    for q in range(n_qubits):
        qml.Rot(w[w_idx], w[w_idx + 1], w[w_idx + 2], wires=q)
        w_idx += 3

    if n_qubits > 1:
        for q in range(n_qubits):
            qml.CNOT(wires=[q, (q + n_qubits // 2) % n_qubits])

Model#

from qml_essentials.model import Model

A quantum circuit model.

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

    def __init__(
        self,
        n_qubits: int,
        n_layers: int,
        circuit_type: str,
        data_reupload: bool = True,
        initialization: str = "random",
        output_qubit: Union[List[int], int] = 0,
        shots: Optional[int] = None,
        random_seed: int = 1000,
    ) -> None:
        """
        Initialize the quantum circuit model.
        Parameters will have the shape [impl_n_layers, parameters_per_layer]
        where impl_n_layers is the number of layers provided and added by one
        depending if data_reupload is True and parameters_per_layer is given by
        the chosen ansatz.

        The model is initialized with the following parameters as defaults:
        - noise_params: None
        - execution_type: "expval"
        - shots: None

        Args:
            n_qubits (int): The number of qubits in the circuit.
            n_layers (int): The number of layers in the circuit.
            circuit_type (str): The type of quantum circuit to use.
                If None, defaults to "no_ansatz".
            data_reupload (bool, optional): Whether to reupload data to the
                quantum device on each measurement. Defaults to True.
            initialization (str, optional): The strategy to initialize the parameters.
                Can be "random", "zeros", "zero-controlled", "pi", or "pi-controlled".
                Defaults to "random".
            output_qubit (List[int], int, optional): The index of the output
                qubit (or qubits). When set to -1 all qubits are measured, or a
                global measurement is conducted, depending on the execution
                type.
            shots (Optional[int], optional): The number of shots to use for
                the quantum device. Defaults to None.
            random_seed (int, optional): seed for the random number generator
                in initialization is "random", Defaults to 1000.

        Returns:
            None
        """
        # Initialize default parameters needed for circuit evaluation
        self.noise_params: Optional[Dict[str, float]] = None
        self.execution_type: Optional[str] = "expval"
        self.shots = shots
        self.output_qubit: Union[List[int], int] = output_qubit

        # Copy the parameters
        self.n_qubits: int = n_qubits
        self.n_layers: int = n_layers
        self.data_reupload: bool = data_reupload

        # Initialize ansatz
        self.pqc: Callable[[Optional[np.ndarray], int], int] = getattr(
            Ansaetze, circuit_type or "no_ansatz"
        )()

        log.info(f"Using {circuit_type} circuit.")

        if data_reupload:
            impl_n_layers: int = n_layers + 1  # we need L+1 according to Schuld et al.
            self.degree = n_layers * n_qubits
        else:
            impl_n_layers: int = n_layers
            self.degree = 1

        log.info(f"Number of implicit layers set to {impl_n_layers}.")

        params_shape: Tuple[int, int] = (
            impl_n_layers,
            self.pqc.n_params_per_layer(self.n_qubits),
        )

        def set_control_params(params: np.ndarray, value: float) -> np.ndarray:
            indices = self.pqc.get_control_indices(self.n_qubits)
            if indices is None:
                warnings.warn(
                    f"Specified {initialization} but circuit\
                    does not contain controlled rotation gates.\
                    Parameters are intialized randomly.",
                    UserWarning,
                )
            else:
                params[:, indices[0] : indices[1] : indices[2]] = (
                    np.ones_like(params[:, indices[0] : indices[1] : indices[2]])
                    * value
                )
            return params

        rng = np.random.default_rng(random_seed)
        if initialization == "random":
            self.params: np.ndarray = rng.uniform(
                0, 2 * np.pi, params_shape, requires_grad=True
            )
        elif initialization == "zeros":
            self.params: np.ndarray = np.zeros(params_shape, requires_grad=True)
        elif initialization == "pi":
            self.params: np.ndarray = np.ones(params_shape, requires_grad=True) * np.pi
        elif initialization == "zero-controlled":
            self.params: np.ndarray = rng.uniform(
                0, 2 * np.pi, params_shape, requires_grad=True
            )
            self.params = set_control_params(self.params, 0)
        elif initialization == "pi-controlled":
            self.params: np.ndarray = rng.uniform(
                0, 2 * np.pi, params_shape, requires_grad=True
            )
            self.params = set_control_params(self.params, np.pi)
        else:
            raise Exception("Invalid initialization method")

        log.info(
            f"Initialized parameters with shape {self.params.shape}\
            using strategy {initialization}."
        )

        # Initialize two circuits, one with the default device and
        # one with the mixed device
        # which allows us to later route depending on the state_vector flag
        self.circuit: qml.QNode = qml.QNode(
            self._circuit,
            qml.device("default.qubit", shots=self.shots, wires=self.n_qubits),
        )
        self.circuit_mixed: qml.QNode = qml.QNode(
            self._circuit,
            qml.device("default.mixed", shots=self.shots, wires=self.n_qubits),
        )

        log.debug(self._draw())

    @property
    def noise_params(self) -> Optional[Dict[str, float]]:
        """
        Gets the noise parameters of the model.

        Returns:
            Optional[Dict[str, float]]: A dictionary of noise parameters or None if not set.
        """
        return self._noise_params

    @noise_params.setter
    def noise_params(self, value: Optional[Dict[str, float]]) -> None:
        """
        Sets the noise parameters of the model.

        Args:
            value (Optional[Dict[str, float]]): A dictionary of noise parameters.
                If all values are 0.0, the noise parameters are set to None.

        Returns:
            None
        """
        if value is not None and all(np == 0.0 for np in value.values()):
            value = None
        self._noise_params = value

    @property
    def execution_type(self) -> str:
        """
        Gets the execution type of the model.

        Returns:
            str: The execution type, one of 'density', 'expval', or 'probs'.
        """
        return self._execution_type

    @execution_type.setter
    def execution_type(self, value: str) -> None:
        if value not in ["density", "expval", "probs"]:
            raise ValueError(f"Invalid execution type: {value}.")

        if value == "density" and self.output_qubit != -1:
            warnings.warn(
                f"{value} measurement does ignore output_qubit, which is "
                f"{self.output_qubit}.",
                UserWarning,
            )

        if value == "probs" and self.shots is None:
            warnings.warn(
                "Setting execution_type to probs without specifying shots.", UserWarning
            )

        if value == "density" and self.shots is not None:
            warnings.warn(
                "Setting execution_type to density with specified shots.", UserWarning
            )

        self._execution_type = value

    @property
    def shots(self) -> Optional[int]:
        """
        Gets the number of shots to use for the quantum device.

        Returns:
            Optional[int]: The number of shots.
        """
        return self._shots

    @shots.setter
    def shots(self, value: Optional[int]) -> None:
        """
        Sets the number of shots to use for the quantum device.

        Args:
            value (Optional[int]): The number of shots. If an integer less than or equal to 0 is provided, it is set to None.

        Returns:
            None
        """
        if type(value) is int and value <= 0:
            value = None
        self._shots = value

    def _iec(
        self,
        inputs: np.ndarray,
        data_reupload: bool = True,
    ) -> None:
        """
        Creates an AngleEncoding using RX gates

        Args:
            inputs (np.ndarray): length of vector must be 1, shape (1,)
            data_reupload (bool, optional): Whether to reupload the data
                for the IEC or not, default is True.

        Returns:
            None
        """
        if inputs is None:
            # initialize to zero
            inputs = np.array([[0]])
        elif len(inputs.shape) == 1:
            # add a batch dimension
            inputs = inputs.reshape(-1, 1)

        if data_reupload:
            if inputs.shape[1] == 1:
                for q in range(self.n_qubits):
                    qml.RX(inputs[:, 0], wires=q)
            elif inputs.shape[1] == 2:
                for q in range(self.n_qubits):
                    qml.RX(inputs[:, 0], wires=q)
                    qml.RY(inputs[:, 1], wires=q)
            elif inputs.shape[1] == 3:
                for q in range(self.n_qubits):
                    qml.Rot(inputs[:, 0], inputs[:, 1], inputs[:, 2], wires=q)
            else:
                raise ValueError(
                    "The number of parameters for this IEC cannot be greater than 3"
                )
        else:
            if inputs.shape[1] == 1:
                qml.RX(inputs[:, 0], wires=0)
            elif inputs.shape[1] == 2:
                qml.RX(inputs[:, 0], wires=0)
                qml.RY(inputs[:, 1], wires=0)
            elif inputs.shape[1] == 3:
                qml.Rot(inputs[:, 0], inputs[:, 1], inputs[:, 2], wires=0)
            else:
                raise ValueError(
                    "The number of parameters for this IEC cannot be greater than 3"
                )

    def _circuit(
        self,
        params: np.ndarray,
        inputs: np.ndarray,
    ) -> Union[float, np.ndarray]:
        """
        Creates a circuit with noise.

        Args:
            params (np.ndarray): weight vector of shape
                [n_layers, n_qubits*n_params_per_layer]
            inputs (np.ndarray): input vector of size 1
        Returns:
            Union[float, np.ndarray]: Expectation value of PauliZ(0)
                of the circuit if state_vector is False and exp_val is True,
                otherwise the density matrix of all qubits.
        """

        for l in range(0, self.n_layers):
            self.pqc(params[l], self.n_qubits)

            if self.data_reupload or l == 0:
                self._iec(inputs, data_reupload=self.data_reupload)

            if self.noise_params is not None:
                for q in range(self.n_qubits):
                    qml.BitFlip(self.noise_params.get("BitFlip", 0.0), wires=q)
                    qml.PhaseFlip(self.noise_params.get("PhaseFlip", 0.0), wires=q)
                    qml.AmplitudeDamping(
                        self.noise_params.get("AmplitudeDamping", 0.0), wires=q
                    )
                    qml.PhaseDamping(
                        self.noise_params.get("PhaseDamping", 0.0), wires=q
                    )
                    qml.DepolarizingChannel(
                        self.noise_params.get("DepolarizingChannel", 0.0),
                        wires=q,
                    )

        if self.data_reupload:
            self.pqc(params[-1], self.n_qubits)

        # run mixed simualtion and get density matrix
        if self.execution_type == "density":
            return qml.density_matrix(wires=list(range(self.n_qubits)))
        # run default simulation and get expectation value
        elif self.execution_type == "expval":
            # global measurement (tensored Pauli Z, i.e. parity)
            if self.output_qubit == -1:
                obs = qml.Hamiltonian(
                    [1.0] * self.n_qubits,
                    [qml.PauliZ(q) for q in range(self.n_qubits)],
                    simplify=True,
                )
                return qml.expval(obs)
            # local measurement(s)
            elif isinstance(self.output_qubit, int):
                return qml.expval(qml.PauliZ(self.output_qubit))
            else:
                return [qml.expval(qml.PauliZ(q)) for q in self.output_qubit]
        # run default simulation and get probs
        elif self.execution_type == "probs":
            if self.output_qubit == -1:
                return qml.probs(wires=list(range(self.n_qubits)))
            else:
                return qml.probs(wires=self.output_qubit)
        else:
            raise ValueError(f"Invalid execution_type: {self.execution_type}.")

    def _draw(self, inputs=None) -> None:
        result = qml.draw(self.circuit)(params=self.params, inputs=inputs)
        return result

    def __repr__(self) -> str:
        return self._draw()

    def __str__(self) -> str:
        return self._draw()

    def __call__(
        self,
        params: np.ndarray,
        inputs: np.ndarray,
        noise_params: Optional[Dict[str, float]] = None,
        cache: Optional[bool] = False,
        execution_type: Optional[str] = None,
    ) -> np.ndarray:
        """
        Perform a forward pass of the quantum circuit.

        Args:
            params (np.ndarray): Weight vector of shape
                [n_layers, n_qubits*n_params_per_layer].
            inputs (np.ndarray): Input vector of shape [1].
            noise_params (Optional[Dict[str, float]], optional): The noise parameters.
                Defaults to None which results in the last
                set noise parameters being used.
            cache (Optional[bool], optional): Whether to cache the results.
                Defaults to False.
            execution_type (str, optional): The type of execution.
                Must be one of 'expval', 'density', or 'probs'.
                Defaults to None which results in the last set execution type
                being used.

        Returns:
            np.ndarray: The output of the quantum circuit.
                The shape depends on the execution_type.
                - If execution_type is 'expval', returns an ndarray of shape
                    (1,) if output_qubit is -1, else (len(output_qubit),).
                - If execution_type is 'density', returns an ndarray
                    of shape (2**n_qubits, 2**n_qubits).
                - If execution_type is 'probs', returns an ndarray
                    of shape (2**n_qubits,) if output_qubit is -1, else
                    (2**len(output_qubit),).
        """
        # Call forward method which handles the actual caching etc.
        return self._forward(params, inputs, noise_params, cache, execution_type)

    def _forward(
        self,
        params: np.ndarray,
        inputs: np.ndarray,
        noise_params: Optional[Dict[str, float]] = None,
        cache: Optional[bool] = False,
        execution_type: Optional[str] = None,
    ) -> np.ndarray:
        """
        Perform a forward pass of the quantum circuit.

        Args:
            params (np.ndarray): Weight vector of shape
                [n_layers, n_qubits*n_params_per_layer].
            inputs (np.ndarray): Input vector of shape [1].
            noise_params (Optional[Dict[str, float]], optional): The noise parameters.
                Defaults to None which results in the last
                set noise parameters being used.
            cache (Optional[bool], optional): Whether to cache the results.
                Defaults to False.
            execution_type (str, optional): The type of execution.
                Must be one of 'expval', 'density', or 'probs'.
                Defaults to None which results in the last set execution type
                being used.

        Returns:
            np.ndarray: The output of the quantum circuit.
                The shape depends on the execution_type.
                - If execution_type is 'expval', returns an ndarray of shape
                    (1,) if output_qubit is -1, else (len(output_qubit),).
                - If execution_type is 'density', returns an ndarray
                    of shape (2**n_qubits, 2**n_qubits).
                - If execution_type is 'probs', returns an ndarray
                    of shape (2**n_qubits,) if output_qubit is -1, else
                    (2**len(output_qubit),).

        Raises:
            NotImplementedError: If the number of shots is not None or if the
                expectation value is True.
        """
        # set the parameters as object attributes
        if noise_params is not None:
            self.noise_params = noise_params
        if execution_type is not None:
            self.execution_type = execution_type

        # the qasm representation contains the bound parameters, thus it is ok to hash that
        hs = hashlib.md5(
            repr(
                {
                    "n_qubits": self.n_qubits,
                    "n_layers": self.n_layers,
                    "pqc": self.pqc.__class__.__name__,
                    "dru": self.data_reupload,
                    "params": params,
                    "noise_params": self.noise_params,
                    "execution_type": self.execution_type,
                    "inputs": inputs,
                    "output_qubit": self.output_qubit,
                }
            ).encode("utf-8")
        ).hexdigest()

        result: Optional[np.ndarray] = None
        if cache:
            name: str = f"pqc_{hs}.npy"

            cache_folder: str = ".cache"
            if not os.path.exists(cache_folder):
                os.mkdir(cache_folder)

            file_path: str = os.path.join(cache_folder, name)

            if os.path.isfile(file_path):
                result = np.load(file_path)

        if result is None:
            # if density matrix requested or noise params used
            if self.execution_type == "density" or self.noise_params is not None:
                result = self.circuit_mixed(
                    params=params,
                    inputs=inputs,
                )
            else:
                result = self.circuit(
                    params=params,
                    inputs=inputs,
                )

        if self.execution_type == "expval" and isinstance(self.output_qubit, list):
            result = np.stack(result)

        if cache:
            np.save(file_path, result)

        return result

execution_type: str property writable #

Gets the execution type of the model.

Returns:

Name Type Description
str str

The execution type, one of 'density', 'expval', or 'probs'.

noise_params: Optional[Dict[str, float]] property writable #

Gets the noise parameters of the model.

Returns:

Type Description
Optional[Dict[str, float]]

Optional[Dict[str, float]]: A dictionary of noise parameters or None if not set.

shots: Optional[int] property writable #

Gets the number of shots to use for the quantum device.

Returns:

Type Description
Optional[int]

Optional[int]: The number of shots.

__call__(params, inputs, noise_params=None, cache=False, execution_type=None) #

Perform a forward pass of the quantum circuit.

Parameters:

Name Type Description Default
params ndarray

Weight vector of shape [n_layers, n_qubits*n_params_per_layer].

required
inputs ndarray

Input vector of shape [1].

required
noise_params Optional[Dict[str, float]]

The noise parameters. Defaults to None which results in the last set noise parameters being used.

None
cache Optional[bool]

Whether to cache the results. Defaults to False.

False
execution_type str

The type of execution. Must be one of 'expval', 'density', or 'probs'. Defaults to None which results in the last set execution type being used.

None

Returns:

Type Description
ndarray

np.ndarray: The output of the quantum circuit. The shape depends on the execution_type. - If execution_type is 'expval', returns an ndarray of shape (1,) if output_qubit is -1, else (len(output_qubit),). - If execution_type is 'density', returns an ndarray of shape (2n_qubits, 2n_qubits). - If execution_type is 'probs', returns an ndarray of shape (2n_qubits,) if output_qubit is -1, else (2len(output_qubit),).

Source code in qml_essentials/model.py
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
def __call__(
    self,
    params: np.ndarray,
    inputs: np.ndarray,
    noise_params: Optional[Dict[str, float]] = None,
    cache: Optional[bool] = False,
    execution_type: Optional[str] = None,
) -> np.ndarray:
    """
    Perform a forward pass of the quantum circuit.

    Args:
        params (np.ndarray): Weight vector of shape
            [n_layers, n_qubits*n_params_per_layer].
        inputs (np.ndarray): Input vector of shape [1].
        noise_params (Optional[Dict[str, float]], optional): The noise parameters.
            Defaults to None which results in the last
            set noise parameters being used.
        cache (Optional[bool], optional): Whether to cache the results.
            Defaults to False.
        execution_type (str, optional): The type of execution.
            Must be one of 'expval', 'density', or 'probs'.
            Defaults to None which results in the last set execution type
            being used.

    Returns:
        np.ndarray: The output of the quantum circuit.
            The shape depends on the execution_type.
            - If execution_type is 'expval', returns an ndarray of shape
                (1,) if output_qubit is -1, else (len(output_qubit),).
            - If execution_type is 'density', returns an ndarray
                of shape (2**n_qubits, 2**n_qubits).
            - If execution_type is 'probs', returns an ndarray
                of shape (2**n_qubits,) if output_qubit is -1, else
                (2**len(output_qubit),).
    """
    # Call forward method which handles the actual caching etc.
    return self._forward(params, inputs, noise_params, cache, execution_type)

__init__(n_qubits, n_layers, circuit_type, data_reupload=True, initialization='random', output_qubit=0, shots=None, random_seed=1000) #

Initialize the quantum circuit model. Parameters will have the shape [impl_n_layers, parameters_per_layer] where impl_n_layers is the number of layers provided and added by one depending if data_reupload is True and parameters_per_layer is given by the chosen ansatz.

The model is initialized with the following parameters as defaults: - noise_params: None - execution_type: "expval" - shots: None

Parameters:

Name Type Description Default
n_qubits int

The number of qubits in the circuit.

required
n_layers int

The number of layers in the circuit.

required
circuit_type str

The type of quantum circuit to use. If None, defaults to "no_ansatz".

required
data_reupload bool

Whether to reupload data to the quantum device on each measurement. Defaults to True.

True
initialization str

The strategy to initialize the parameters. Can be "random", "zeros", "zero-controlled", "pi", or "pi-controlled". Defaults to "random".

'random'
output_qubit (List[int], int)

The index of the output qubit (or qubits). When set to -1 all qubits are measured, or a global measurement is conducted, depending on the execution type.

0
shots Optional[int]

The number of shots to use for the quantum device. Defaults to None.

None
random_seed int

seed for the random number generator in initialization is "random", Defaults to 1000.

1000

Returns:

Type Description
None

None

Source code in qml_essentials/model.py
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
def __init__(
    self,
    n_qubits: int,
    n_layers: int,
    circuit_type: str,
    data_reupload: bool = True,
    initialization: str = "random",
    output_qubit: Union[List[int], int] = 0,
    shots: Optional[int] = None,
    random_seed: int = 1000,
) -> None:
    """
    Initialize the quantum circuit model.
    Parameters will have the shape [impl_n_layers, parameters_per_layer]
    where impl_n_layers is the number of layers provided and added by one
    depending if data_reupload is True and parameters_per_layer is given by
    the chosen ansatz.

    The model is initialized with the following parameters as defaults:
    - noise_params: None
    - execution_type: "expval"
    - shots: None

    Args:
        n_qubits (int): The number of qubits in the circuit.
        n_layers (int): The number of layers in the circuit.
        circuit_type (str): The type of quantum circuit to use.
            If None, defaults to "no_ansatz".
        data_reupload (bool, optional): Whether to reupload data to the
            quantum device on each measurement. Defaults to True.
        initialization (str, optional): The strategy to initialize the parameters.
            Can be "random", "zeros", "zero-controlled", "pi", or "pi-controlled".
            Defaults to "random".
        output_qubit (List[int], int, optional): The index of the output
            qubit (or qubits). When set to -1 all qubits are measured, or a
            global measurement is conducted, depending on the execution
            type.
        shots (Optional[int], optional): The number of shots to use for
            the quantum device. Defaults to None.
        random_seed (int, optional): seed for the random number generator
            in initialization is "random", Defaults to 1000.

    Returns:
        None
    """
    # Initialize default parameters needed for circuit evaluation
    self.noise_params: Optional[Dict[str, float]] = None
    self.execution_type: Optional[str] = "expval"
    self.shots = shots
    self.output_qubit: Union[List[int], int] = output_qubit

    # Copy the parameters
    self.n_qubits: int = n_qubits
    self.n_layers: int = n_layers
    self.data_reupload: bool = data_reupload

    # Initialize ansatz
    self.pqc: Callable[[Optional[np.ndarray], int], int] = getattr(
        Ansaetze, circuit_type or "no_ansatz"
    )()

    log.info(f"Using {circuit_type} circuit.")

    if data_reupload:
        impl_n_layers: int = n_layers + 1  # we need L+1 according to Schuld et al.
        self.degree = n_layers * n_qubits
    else:
        impl_n_layers: int = n_layers
        self.degree = 1

    log.info(f"Number of implicit layers set to {impl_n_layers}.")

    params_shape: Tuple[int, int] = (
        impl_n_layers,
        self.pqc.n_params_per_layer(self.n_qubits),
    )

    def set_control_params(params: np.ndarray, value: float) -> np.ndarray:
        indices = self.pqc.get_control_indices(self.n_qubits)
        if indices is None:
            warnings.warn(
                f"Specified {initialization} but circuit\
                does not contain controlled rotation gates.\
                Parameters are intialized randomly.",
                UserWarning,
            )
        else:
            params[:, indices[0] : indices[1] : indices[2]] = (
                np.ones_like(params[:, indices[0] : indices[1] : indices[2]])
                * value
            )
        return params

    rng = np.random.default_rng(random_seed)
    if initialization == "random":
        self.params: np.ndarray = rng.uniform(
            0, 2 * np.pi, params_shape, requires_grad=True
        )
    elif initialization == "zeros":
        self.params: np.ndarray = np.zeros(params_shape, requires_grad=True)
    elif initialization == "pi":
        self.params: np.ndarray = np.ones(params_shape, requires_grad=True) * np.pi
    elif initialization == "zero-controlled":
        self.params: np.ndarray = rng.uniform(
            0, 2 * np.pi, params_shape, requires_grad=True
        )
        self.params = set_control_params(self.params, 0)
    elif initialization == "pi-controlled":
        self.params: np.ndarray = rng.uniform(
            0, 2 * np.pi, params_shape, requires_grad=True
        )
        self.params = set_control_params(self.params, np.pi)
    else:
        raise Exception("Invalid initialization method")

    log.info(
        f"Initialized parameters with shape {self.params.shape}\
        using strategy {initialization}."
    )

    # Initialize two circuits, one with the default device and
    # one with the mixed device
    # which allows us to later route depending on the state_vector flag
    self.circuit: qml.QNode = qml.QNode(
        self._circuit,
        qml.device("default.qubit", shots=self.shots, wires=self.n_qubits),
    )
    self.circuit_mixed: qml.QNode = qml.QNode(
        self._circuit,
        qml.device("default.mixed", shots=self.shots, wires=self.n_qubits),
    )

    log.debug(self._draw())

Entanglement#

from qml_essentials.entanglement import Entanglement
Source code in qml_essentials/entanglement.py
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
class Entanglement:

    @staticmethod
    def meyer_wallach(
        model: Callable,  # type: ignore
        n_samples: int,
        seed: Optional[int],
        **kwargs: Any
    ) -> float:
        """
        Calculates the entangling capacity of a given quantum circuit
        using Meyer-Wallach measure.

        Args:
            model (Callable): Function that models the quantum circuit. It must
                have a `n_qubits` attribute representing the number of qubits.
                It must accept a `params` argument representing the parameters
                of the circuit.
            n_samples (int): Number of samples per qubit.
            seed (Optional[int]): Seed for the random number generator.
            kwargs (Any): Additional keyword arguments for the model function.

        Returns:
            float: Entangling capacity of the given circuit. It is guaranteed
                to be between 0.0 and 1.0.
        """
        def _meyer_wallach(
            evaluate: Callable[[np.ndarray], np.ndarray],
            n_qubits: int,
            samples: int,
            params: np.ndarray,
        ) -> float:
            """
            Calculates the Meyer-Wallach sampling of the entangling capacity
            of a quantum circuit.

            Args:
                evaluate (Callable[[np.ndarray], np.ndarray]): Callable that
                    evaluates the quantum circuit It must accept a `params`
                    argument representing the parameters of the circuit and may
                    accept additional keyword arguments.
                n_qubits (int): Number of qubits in the circuit
                samples (int): Number of samples to be taken
                params (np.ndarray): Parameters of the instructor. Shape:
                    (samples, *model.params.shape)

            Returns:
                float: Entangling capacity of the given circuit. It is
                    guaranteed to be between 0.0 and 1.0
            """
            assert (
                params.shape[0] == samples
            ), "Number of samples does not match number of parameters"

            mw_measure = np.zeros(samples, dtype=complex)
            qb = list(range(n_qubits))

            for i in range(samples):
                # implicitly set input to none in case it's not needed
                kwargs.setdefault("inputs", None)
                # explicitly set execution type because everything else won't work
                U = evaluate(params=params[i], execution_type="density", **kwargs)

                entropy = 0

                for j in range(n_qubits):
                    density = qml.math.partial_trace(U, qb[:j] + qb[j + 1 :])
                    entropy += np.trace((density @ density).real)

                mw_measure[i] = 1 - entropy / n_qubits

            mw = 2 * np.sum(mw_measure.real) / samples

            # catch floating point errors
            return min(max(mw, 0.0), 1.0)

        if n_samples > 0:
            assert seed is not None, "Seed must be provided when samples > 0"
            # TODO: maybe switch to JAX rng
            rng = np.random.default_rng(seed)
            params = rng.uniform(0, 2 * np.pi, size=(n_samples, *model.params.shape))
        else:
            if seed is not None:
                log.warning("Seed is ignored when samples is 0")
            n_samples = 1
            params = model.params.reshape(1, *model.params.shape)

        entangling_capability = _meyer_wallach(
            evaluate=model,
            n_qubits=model.n_qubits,
            samples=n_samples,
            params=params,
        )

        return float(entangling_capability)

meyer_wallach(model, n_samples, seed, **kwargs) staticmethod #

Calculates the entangling capacity of a given quantum circuit using Meyer-Wallach measure.

Parameters:

Name Type Description Default
model Callable

Function that models the quantum circuit. It must have a n_qubits attribute representing the number of qubits. It must accept a params argument representing the parameters of the circuit.

required
n_samples int

Number of samples per qubit.

required
seed Optional[int]

Seed for the random number generator.

required
kwargs Any

Additional keyword arguments for the model function.

{}

Returns:

Name Type Description
float float

Entangling capacity of the given circuit. It is guaranteed to be between 0.0 and 1.0.

Source code in qml_essentials/entanglement.py
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
@staticmethod
def meyer_wallach(
    model: Callable,  # type: ignore
    n_samples: int,
    seed: Optional[int],
    **kwargs: Any
) -> float:
    """
    Calculates the entangling capacity of a given quantum circuit
    using Meyer-Wallach measure.

    Args:
        model (Callable): Function that models the quantum circuit. It must
            have a `n_qubits` attribute representing the number of qubits.
            It must accept a `params` argument representing the parameters
            of the circuit.
        n_samples (int): Number of samples per qubit.
        seed (Optional[int]): Seed for the random number generator.
        kwargs (Any): Additional keyword arguments for the model function.

    Returns:
        float: Entangling capacity of the given circuit. It is guaranteed
            to be between 0.0 and 1.0.
    """
    def _meyer_wallach(
        evaluate: Callable[[np.ndarray], np.ndarray],
        n_qubits: int,
        samples: int,
        params: np.ndarray,
    ) -> float:
        """
        Calculates the Meyer-Wallach sampling of the entangling capacity
        of a quantum circuit.

        Args:
            evaluate (Callable[[np.ndarray], np.ndarray]): Callable that
                evaluates the quantum circuit It must accept a `params`
                argument representing the parameters of the circuit and may
                accept additional keyword arguments.
            n_qubits (int): Number of qubits in the circuit
            samples (int): Number of samples to be taken
            params (np.ndarray): Parameters of the instructor. Shape:
                (samples, *model.params.shape)

        Returns:
            float: Entangling capacity of the given circuit. It is
                guaranteed to be between 0.0 and 1.0
        """
        assert (
            params.shape[0] == samples
        ), "Number of samples does not match number of parameters"

        mw_measure = np.zeros(samples, dtype=complex)
        qb = list(range(n_qubits))

        for i in range(samples):
            # implicitly set input to none in case it's not needed
            kwargs.setdefault("inputs", None)
            # explicitly set execution type because everything else won't work
            U = evaluate(params=params[i], execution_type="density", **kwargs)

            entropy = 0

            for j in range(n_qubits):
                density = qml.math.partial_trace(U, qb[:j] + qb[j + 1 :])
                entropy += np.trace((density @ density).real)

            mw_measure[i] = 1 - entropy / n_qubits

        mw = 2 * np.sum(mw_measure.real) / samples

        # catch floating point errors
        return min(max(mw, 0.0), 1.0)

    if n_samples > 0:
        assert seed is not None, "Seed must be provided when samples > 0"
        # TODO: maybe switch to JAX rng
        rng = np.random.default_rng(seed)
        params = rng.uniform(0, 2 * np.pi, size=(n_samples, *model.params.shape))
    else:
        if seed is not None:
            log.warning("Seed is ignored when samples is 0")
        n_samples = 1
        params = model.params.reshape(1, *model.params.shape)

    entangling_capability = _meyer_wallach(
        evaluate=model,
        n_qubits=model.n_qubits,
        samples=n_samples,
        params=params,
    )

    return float(entangling_capability)

Expressibility#

from qml_essentials.expressibility import Expressibility
Source code in qml_essentials/expressibility.py
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
class Expressibility:
    @staticmethod
    def _sample_state_fidelities(
        model: Callable[
            [np.ndarray, np.ndarray], np.ndarray
        ],  # type: ignore[name-defined]
        x_samples: np.ndarray,
        n_samples: int,
        seed: int,
        kwargs: Any,
    ) -> np.ndarray:
        """
        Compute the fidelities for each pair of input samples and parameter sets.

        Args:
            model (Callable[[np.ndarray, np.ndarray], np.ndarray]):
                Function that evaluates the model. It must accept inputs
                and params as arguments
                and return an array of shape (n_samples, n_features).
            x_samples (np.ndarray): Array of shape (n_input_samples, n_features)
                containing the input samples.
            n_samples (int): Number of parameter sets to generate.
            seed (int): Random number generator seed.
            kwargs (Any): Additional keyword arguments for the model function.

        Returns:
            np.ndarray: Array of shape (n_input_samples, n_samples)
            containing the fidelities.
        """
        rng = np.random.default_rng(seed)

        # Number of input samples
        n_x_samples = len(x_samples)

        # Initialize array to store fidelities
        fidelities: np.ndarray = np.zeros((n_x_samples, n_samples))

        # Generate random parameter sets
        w: np.ndarray = (
            2 * np.pi * (1 - 2 * rng.random(size=[*model.params.shape, n_samples * 2]))
        )

        # Batch input samples and parameter sets for efficient computation
        x_samples_batched: np.ndarray = x_samples.reshape(1, -1).repeat(
            n_samples * 2, axis=0
        )

        # Compute the fidelity for each pair of input samples and parameters
        for idx in range(n_x_samples):

            # Evaluate the model for the current pair of input samples and parameters
            # Execution type is explicitly set to density
            sv: np.ndarray = model(
                inputs=x_samples_batched[:, idx],
                params=w,
                execution_type="density",
                **kwargs,
            )
            sqrt_sv1: np.ndarray = np.sqrt(sv[:n_samples])

            # Compute the fidelity using the partial trace of the statevector
            fidelity: np.ndarray = (
                np.trace(
                    np.sqrt(sqrt_sv1 * sv[n_samples:] * sqrt_sv1),
                    axis1=1,
                    axis2=2,
                )
                ** 2
            )
            fidelities[idx] = np.real(fidelity)

        return fidelities

    @staticmethod
    def state_fidelities(
        n_bins: int,
        n_samples: int,
        n_input_samples: int,
        seed: int,
        model: Callable,  # type: ignore
        **kwargs: Any,
    ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
        """
        Sample the state fidelities and histogram them into a 2D array.

        Args:
            n_bins (int): Number of histogram bins.
            n_samples (int): Number of parameter sets to generate.
            n_input_samples (int): Number of samples for the input domain in [-pi, pi]
            seed (int): Random number generator seed.
            model (Callable): 
            kwargs (Any): Additional keyword arguments for the model function.

        Returns:
            Tuple[np.ndarray, np.ndarray, np.ndarray]: Tuple containing the
                input samples, bin edges, and histogram values.
        """
        epsilon = 1e-5

        x_domain = [-1 * np.pi, 1 * np.pi]
        x = np.linspace(x_domain[0], x_domain[1], n_input_samples, requires_grad=False)

        fidelities = Expressibility._sample_state_fidelities(
            x_samples=x,
            n_samples=n_samples,
            seed=seed,
            model=model,
            kwargs=kwargs,
        )
        z: np.ndarray = np.zeros((len(x), n_bins))

        y: np.ndarray = np.linspace(0, 1 + epsilon, n_bins + 1)
        # FIXME: somehow I get nan's in the histogram,
        # when directly creating bins until n
        # workaround hack is to add a small epsilon
        # could it be related to sampling issues?
        for i, f in enumerate(fidelities):
            z[i], _ = np.histogram(f, bins=y)

        # Transpose because this allows a direct comparison with the haar integral
        z = z / n_samples

        return x, y, z

    @staticmethod
    def _haar_probability(fidelity: float, n_qubits: int) -> float:
        """
        Calculates theoretical probability density function for random Haar states
        as proposed by Sim et al. (https://arxiv.org/abs/1905.10876).

        Args:
            fidelity (float): fidelity of two parameter assignments in [0, 1]
            n_qubits (int): number of qubits in the quantum system

        Returns:
            float: probability for a given fidelity
        """
        N = 2**n_qubits

        prob = (N - 1) * (1 - fidelity) ** (N - 2)
        return prob

    @staticmethod
    def _sample_haar_integral(n_qubits: int, n_bins: int) -> np.ndarray:
        """
        Calculates theoretical probability density function for random Haar states
        as proposed by Sim et al. (https://arxiv.org/abs/1905.10876) and bins it
        into a 2D-histogram.

        Args:
            n_qubits (int): number of qubits in the quantum system
            n_bins (int): number of histogram bins

        Returns:
            np.ndarray: probability distribution for all fidelities
        """
        dist = np.zeros(n_bins)
        for idx in range(n_bins):
            v = (1 / n_bins) * idx
            u = v + (1 / n_bins)
            dist[idx], _ = integrate.quad(
                Expressibility._haar_probability, v, u, args=(n_qubits,)
            )

        return dist

    @staticmethod
    def haar_integral(
        n_qubits: int,
        n_bins: int,
        cache: bool = True,
    ) -> Tuple[np.ndarray, np.ndarray]:
        """
        Calculates theoretical probability density function for random Haar states
        as proposed by Sim et al. (https://arxiv.org/abs/1905.10876) and bins it
        into a 3D-histogram.

        Args:
            n_qubits (int): number of qubits in the quantum system
            n_bins (int): number of histogram bins
            cache (bool): whether to cache the haar integral

        Returns:
            Tuple[np.ndarray, np.ndarray]:
                - x component (bins): the input domain
                - y component (probabilities): the haar probability density
                  funtion for random Haar states
        """
        x = np.linspace(0, 1, n_bins)

        if cache:
            name = f"haar_{n_qubits}q_{n_bins}s.npy"

            cache_folder = ".cache"
            if not os.path.exists(cache_folder):
                os.mkdir(cache_folder)

            file_path = os.path.join(cache_folder, name)

            if os.path.isfile(file_path):
                y = np.load(file_path)
                return x, y

        y = Expressibility._sample_haar_integral(n_qubits, n_bins)

        if cache:
            np.save(file_path, y)

        return x, y

    @staticmethod
    def kullback_leibler_divergence(
        vqc_prob_dist: np.ndarray,
        haar_dist: np.ndarray,
    ) -> np.ndarray:
        """
        Calculates the KL divergence between two probability distributions (Haar
        probability distribution and the fidelity distribution sampled from a VQC).

        Args:
            vqc_prob_dist (np.ndarray): VQC fidelity probability distribution.
                Should have shape (n_inputs_samples, n_bins)
            haar_dist (np.ndarray): Haar probability distribution with shape.
                Should have shape (n_bins, )

        Returns:
            np.ndarray: Array of KL-Divergence values for all values in axis 1
        """
        if len(vqc_prob_dist.shape) > 1:
            assert all([haar_dist.shape == p.shape for p in vqc_prob_dist]), (
                "All probabilities for inputs should have the same shape as Haar. "
                f"Got {haar_dist.shape} for Haar and {vqc_prob_dist.shape} for VQC"
            )
        else:
            vqc_prob_dist = vqc_prob_dist.reshape((1, -1))

        kl_divergence = np.zeros(vqc_prob_dist.shape[0])
        for idx, p in enumerate(vqc_prob_dist):
            kl_divergence[idx] = np.sum(rel_entr(p, haar_dist))

        return kl_divergence

haar_integral(n_qubits, n_bins, cache=True) staticmethod #

Calculates theoretical probability density function for random Haar states as proposed by Sim et al. (https://arxiv.org/abs/1905.10876) and bins it into a 3D-histogram.

Parameters:

Name Type Description Default
n_qubits int

number of qubits in the quantum system

required
n_bins int

number of histogram bins

required
cache bool

whether to cache the haar integral

True

Returns:

Type Description
Tuple[ndarray, ndarray]

Tuple[np.ndarray, np.ndarray]: - x component (bins): the input domain - y component (probabilities): the haar probability density funtion for random Haar states

Source code in qml_essentials/expressibility.py
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
@staticmethod
def haar_integral(
    n_qubits: int,
    n_bins: int,
    cache: bool = True,
) -> Tuple[np.ndarray, np.ndarray]:
    """
    Calculates theoretical probability density function for random Haar states
    as proposed by Sim et al. (https://arxiv.org/abs/1905.10876) and bins it
    into a 3D-histogram.

    Args:
        n_qubits (int): number of qubits in the quantum system
        n_bins (int): number of histogram bins
        cache (bool): whether to cache the haar integral

    Returns:
        Tuple[np.ndarray, np.ndarray]:
            - x component (bins): the input domain
            - y component (probabilities): the haar probability density
              funtion for random Haar states
    """
    x = np.linspace(0, 1, n_bins)

    if cache:
        name = f"haar_{n_qubits}q_{n_bins}s.npy"

        cache_folder = ".cache"
        if not os.path.exists(cache_folder):
            os.mkdir(cache_folder)

        file_path = os.path.join(cache_folder, name)

        if os.path.isfile(file_path):
            y = np.load(file_path)
            return x, y

    y = Expressibility._sample_haar_integral(n_qubits, n_bins)

    if cache:
        np.save(file_path, y)

    return x, y

kullback_leibler_divergence(vqc_prob_dist, haar_dist) staticmethod #

Calculates the KL divergence between two probability distributions (Haar probability distribution and the fidelity distribution sampled from a VQC).

Parameters:

Name Type Description Default
vqc_prob_dist ndarray

VQC fidelity probability distribution. Should have shape (n_inputs_samples, n_bins)

required
haar_dist ndarray

Haar probability distribution with shape. Should have shape (n_bins, )

required

Returns:

Type Description
ndarray

np.ndarray: Array of KL-Divergence values for all values in axis 1

Source code in qml_essentials/expressibility.py
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
@staticmethod
def kullback_leibler_divergence(
    vqc_prob_dist: np.ndarray,
    haar_dist: np.ndarray,
) -> np.ndarray:
    """
    Calculates the KL divergence between two probability distributions (Haar
    probability distribution and the fidelity distribution sampled from a VQC).

    Args:
        vqc_prob_dist (np.ndarray): VQC fidelity probability distribution.
            Should have shape (n_inputs_samples, n_bins)
        haar_dist (np.ndarray): Haar probability distribution with shape.
            Should have shape (n_bins, )

    Returns:
        np.ndarray: Array of KL-Divergence values for all values in axis 1
    """
    if len(vqc_prob_dist.shape) > 1:
        assert all([haar_dist.shape == p.shape for p in vqc_prob_dist]), (
            "All probabilities for inputs should have the same shape as Haar. "
            f"Got {haar_dist.shape} for Haar and {vqc_prob_dist.shape} for VQC"
        )
    else:
        vqc_prob_dist = vqc_prob_dist.reshape((1, -1))

    kl_divergence = np.zeros(vqc_prob_dist.shape[0])
    for idx, p in enumerate(vqc_prob_dist):
        kl_divergence[idx] = np.sum(rel_entr(p, haar_dist))

    return kl_divergence

state_fidelities(n_bins, n_samples, n_input_samples, seed, model, **kwargs) staticmethod #

Sample the state fidelities and histogram them into a 2D array.

Parameters:

Name Type Description Default
n_bins int

Number of histogram bins.

required
n_samples int

Number of parameter sets to generate.

required
n_input_samples int

Number of samples for the input domain in [-pi, pi]

required
seed int

Random number generator seed.

required
model Callable
required
kwargs Any

Additional keyword arguments for the model function.

{}

Returns:

Type Description
Tuple[ndarray, ndarray, ndarray]

Tuple[np.ndarray, np.ndarray, np.ndarray]: Tuple containing the input samples, bin edges, and histogram values.

Source code in qml_essentials/expressibility.py
 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
@staticmethod
def state_fidelities(
    n_bins: int,
    n_samples: int,
    n_input_samples: int,
    seed: int,
    model: Callable,  # type: ignore
    **kwargs: Any,
) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
    """
    Sample the state fidelities and histogram them into a 2D array.

    Args:
        n_bins (int): Number of histogram bins.
        n_samples (int): Number of parameter sets to generate.
        n_input_samples (int): Number of samples for the input domain in [-pi, pi]
        seed (int): Random number generator seed.
        model (Callable): 
        kwargs (Any): Additional keyword arguments for the model function.

    Returns:
        Tuple[np.ndarray, np.ndarray, np.ndarray]: Tuple containing the
            input samples, bin edges, and histogram values.
    """
    epsilon = 1e-5

    x_domain = [-1 * np.pi, 1 * np.pi]
    x = np.linspace(x_domain[0], x_domain[1], n_input_samples, requires_grad=False)

    fidelities = Expressibility._sample_state_fidelities(
        x_samples=x,
        n_samples=n_samples,
        seed=seed,
        model=model,
        kwargs=kwargs,
    )
    z: np.ndarray = np.zeros((len(x), n_bins))

    y: np.ndarray = np.linspace(0, 1 + epsilon, n_bins + 1)
    # FIXME: somehow I get nan's in the histogram,
    # when directly creating bins until n
    # workaround hack is to add a small epsilon
    # could it be related to sampling issues?
    for i, f in enumerate(fidelities):
        z[i], _ = np.histogram(f, bins=y)

    # Transpose because this allows a direct comparison with the haar integral
    z = z / n_samples

    return x, y, z

Coefficients#

from qml_essentials.coefficients import Coefficients
Source code in qml_essentials/coefficients.py
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class Coefficients:

    def sample_coefficients(model: Model) -> np.ndarray:
        """
        Sample the Fourier coefficients of a given model.

        Args:
            model (Model): The model to sample.

        Returns:
            np.ndarray: The sampled Fourier coefficients.
        """
        partial_circuit = partial(model, model.params)
        return coefficients(partial_circuit, 1, model.degree)

sample_coefficients(model) #

Sample the Fourier coefficients of a given model.

Parameters:

Name Type Description Default
model Model

The model to sample.

required

Returns:

Type Description
ndarray

np.ndarray: The sampled Fourier coefficients.

Source code in qml_essentials/coefficients.py
 9
10
11
12
13
14
15
16
17
18
19
20
def sample_coefficients(model: Model) -> np.ndarray:
    """
    Sample the Fourier coefficients of a given model.

    Args:
        model (Model): The model to sample.

    Returns:
        np.ndarray: The sampled Fourier coefficients.
    """
    partial_circuit = partial(model, model.params)
    return coefficients(partial_circuit, 1, model.degree)