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
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
class Ansaetze:
    def get_available():
        return [
            Ansaetze.No_Ansatz,
            Ansaetze.Circuit_1,
            Ansaetze.Circuit_6,
            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 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 Hardware-Efficient ansatz, as proposed in
            https://arxiv.org/pdf/2309.03279

            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.RX(w[w_idx], wires=q)
                w_idx += 1
                qml.RY(w[w_idx], wires=q)
                w_idx += 1
                qml.RX(w[w_idx], wires=q)
                w_idx += 1

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

    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

            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.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 * 2
            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*2
            because for >1 qubits there are three gates

            Args:
                w (np.ndarray): weight vector of size n_layers*(n_qubits*2)
                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 Circuit9 ansatz.

            Length of flattened vector must be n_qubits

            Args:
                w (np.ndarray): weight vector of size n_layers*n_qubits
                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_6(Circuit):
        @staticmethod
        def n_params_per_layer(n_qubits: int) -> int:
            if n_qubits > 1:
                return n_qubits * 3 + n_qubits**2
            else:
                log.warning("Number of Qubits < 2, no entanglement available")
                return 4

        @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 Circuit6 ansatz.

            Length of flattened vector must be
                n_qubits * 4 + n_qubits * (n_qubits - 1) =
                n_qubits * 3 + n_qubits**2

            Args:
                w (np.ndarray): weight vector of size
                    n_layers * (n_qubits * 3 + 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

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

            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 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*6)
                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
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
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
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
@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
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
class Circuit_15(Circuit):
    @staticmethod
    def n_params_per_layer(n_qubits: int) -> int:
        if n_qubits > 1:
            return n_qubits * 2
        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*2
        because for >1 qubits there are three gates

        Args:
            w (np.ndarray): weight vector of size n_layers*(n_qubits*2)
            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*2 because for >1 qubits there are three gates

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
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
@staticmethod
def build(w: np.ndarray, n_qubits: int):
    """
    Creates a Circuit15 ansatz.

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

    Args:
        w (np.ndarray): weight vector of size n_layers*(n_qubits*2)
        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
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
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

        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.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

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
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
@staticmethod
def build(w: np.ndarray, n_qubits: int):
    """
    Creates a Circuit18 ansatz.

    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.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
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
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
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
@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_6 #

Bases: Circuit

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

    @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 Circuit6 ansatz.

        Length of flattened vector must be
            n_qubits * 4 + n_qubits * (n_qubits - 1) =
            n_qubits * 3 + n_qubits**2

        Args:
            w (np.ndarray): weight vector of size
                n_layers * (n_qubits * 3 + 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

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

        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 Circuit6 ansatz.

Length of flattened vector must be n_qubits * 4 + n_qubits * (n_qubits - 1) = n_qubits * 3 + n_qubits**2

Parameters:

Name Type Description Default
w ndarray

weight vector of size n_layers * (n_qubits * 3 + n_qubits**2)

required
n_qubits int

number of qubits

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

    Length of flattened vector must be
        n_qubits * 4 + n_qubits * (n_qubits - 1) =
        n_qubits * 3 + n_qubits**2

    Args:
        w (np.ndarray): weight vector of size
            n_layers * (n_qubits * 3 + 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

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

    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_9 #

Bases: Circuit

Source code in qml_essentials/ansaetze.py
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
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 Circuit9 ansatz.

        Length of flattened vector must be n_qubits

        Args:
            w (np.ndarray): weight vector of size n_layers*n_qubits
            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 Circuit9 ansatz.

Length of flattened vector must be n_qubits

Parameters:

Name Type Description Default
w ndarray

weight vector of size n_layers*n_qubits

required
n_qubits int

number of qubits

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

    Length of flattened vector must be n_qubits

    Args:
        w (np.ndarray): weight vector of size n_layers*n_qubits
        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
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
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 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 Hardware-Efficient ansatz, as proposed in
        https://arxiv.org/pdf/2309.03279

        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.RX(w[w_idx], wires=q)
            w_idx += 1
            qml.RY(w[w_idx], wires=q)
            w_idx += 1
            qml.RX(w[w_idx], wires=q)
            w_idx += 1

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

build(w, n_qubits) staticmethod #

Creates a Hardware-Efficient ansatz, as proposed in https://arxiv.org/pdf/2309.03279

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
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
@staticmethod
def build(w: np.ndarray, n_qubits: int):
    """
    Creates a Hardware-Efficient ansatz, as proposed in
    https://arxiv.org/pdf/2309.03279

    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.RX(w[w_idx], wires=q)
        w_idx += 1
        qml.RY(w[w_idx], wires=q)
        w_idx += 1
        qml.RX(w[w_idx], wires=q)
        w_idx += 1

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

No_Entangling #

Bases: Circuit

Source code in qml_essentials/ansaetze.py
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
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
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
@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
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
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*6)
            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_qubits6)

required
n_qubits int

number of qubits

required
Source code in qml_essentials/ansaetze.py
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
@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*6)
        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
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
class Model:
    """
    A quantum circuit model.
    """

    def __init__(
        self,
        n_qubits: int,
        n_layers: int,
        circuit_type: Union[str, Circuit],
        data_reupload: bool = True,
        initialization: str = "random",
        initialization_domain: List[float] = [0, 2 * np.pi],
        output_qubit: Union[List[int], int] = -1,
        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, Circuit): 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

        lightning_threshold = 12

        # Initialize ansatz
        # only weak check for str. We trust the user to provide sth useful
        if isinstance(circuit_type, str):
            self.pqc: Callable[[Optional[np.ndarray], int], int] = getattr(
                Ansaetze, circuit_type or "No_Ansatz"
            )()
        else:
            self.pqc = circuit_type()

        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}.")
        # calculate the shape of the parameter vector here, we will re-use this in init.
        self._params_shape: Tuple[int, int] = (
            impl_n_layers,
            self.pqc.n_params_per_layer(self.n_qubits),
        )
        # this will also be re-used in the init method,
        # however, only if nothing is provided
        self._inialization_strategy = initialization
        self._initialization_domain = initialization_domain

        # ..here! where we only require a rng
        self.initialize_params(np.random.default_rng(random_seed))

        # 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"
                    if self.n_qubits < lightning_threshold
                    else "lightning.qubit"
                ),
                shots=self.shots,
                wires=self.n_qubits,
            ),
            interface="autograd" if self.shots is not None else "auto",
            diff_method="parameter-shift" if self.shots is not None else "best",
        )
        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 initialize_params(
        self,
        rng,
        repeat: int = None,
        initialization: str = None,
        initialization_domain: List[float] = None,
    ) -> None:
        """
        Initializes the parameters of the model.

        Args:
            rng: A random number generator to use for initialization.
            repeat: The number of times to repeat the parameters.
                If None, the number of layers is used.
            initialization: The strategy to use for parameter initialization.
                If None, the strategy specified in the constructor is used.
            initialization_domain: The domain to use for parameter initialization.
                If None, the domain specified in the constructor is used.

        Returns:
            None
        """
        params_shape = (
            self._params_shape if repeat is None else [*self._params_shape, repeat]
        )
        # use existing strategy if not specified
        initialization = initialization or self._inialization_strategy
        initialization_domain = initialization_domain or self._initialization_domain

        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

        if initialization == "random":
            self.params: np.ndarray = rng.uniform(
                *initialization_domain, 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(
                *initialization_domain, params_shape, requires_grad=True
            )
            self.params = set_control_params(self.params, 0)
        elif initialization == "pi-controlled":
            self.params: np.ndarray = rng.uniform(
                *initialization_domain, 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}."
        )

    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 layer in range(0, self.n_layers):
            self.pqc(params[layer], self.n_qubits)

            if self.data_reupload or layer == 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,
                    )

            qml.Barrier(wires=list(range(self.n_qubits)), only_visual=True)

        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:
                return [qml.expval(qml.PauliZ(q)) for q in range(self.n_qubits)]
            # local measurement(s)
            elif isinstance(self.output_qubit, int):
                return qml.expval(qml.PauliZ(self.output_qubit))
            # n-local measurenment
            elif isinstance(self.output_qubit, list):
                obs = qml.simplify(
                    qml.Hamiltonian(
                        [1.0] * self.n_qubits,
                        [qml.PauliZ(q) for q in self.output_qubit],
                    )
                )
                return qml.expval(obs)
            else:
                raise ValueError(
                    f"Invalid parameter 'output_qubit': {self.output_qubit}.\
                        Must be int, list or -1."
                )
        # 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, figure=False) -> None:
        if isinstance(self.circuit, qml.qnn.torch.TorchLayer):
            # TODO: throws strange argument error if not catched
            return ""

        if figure:
            result = qml.draw_mpl(self.circuit)(params=self.params, inputs=inputs)
        else:
            result = qml.draw(self.circuit)(params=self.params, inputs=inputs)
        return result

    def draw(self, inputs=None, figure=False) -> None:
        return self._draw(inputs, figure)

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

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

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

        Args:
            params (Optional[np.ndarray]): Weight vector of shape
                [n_layers, n_qubits*n_params_per_layer].
                If None, model internal parameters are used.
            inputs (Optional[np.ndarray]): Input vector of shape [1].
                If None, zeros are used.
            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=params,
            inputs=inputs,
            noise_params=noise_params,
            cache=cache,
            execution_type=execution_type,
            force_mean=force_mean,
        )

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

        Args:
            params (Optional[np.ndarray]): Weight vector of shape
                [n_layers, n_qubits*n_params_per_layer].
                If None, model internal parameters are used.
            inputs (Optional[np.ndarray]): Input vector of shape [1].
                If None, zeros are used.
            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

        if params is None:
            params = self.params
        else:
            if numpy_boxes.ArrayBox == type(params):
                self.params = params._value
            else:
                self.params = params

        # 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": self.params,  # use safe-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,  # use arraybox params
                    inputs=inputs,
                )
            else:
                if isinstance(self.circuit, qml.qnn.torch.TorchLayer):
                    result = self.circuit(
                        inputs=inputs,
                    )
                else:
                    result = self.circuit(
                        params=params,  # use arraybox params
                        inputs=inputs,
                    )

        if isinstance(result, list):
            result = np.stack(result)

        if self.execution_type == "expval" and self.output_qubit == -1:

            # Calculating mean value after stacking, to not
            # discard gradient information
            if force_mean:
                # exception for torch layer because it swaps batch and output dimension
                if isinstance(self.circuit, qml.qnn.torch.TorchLayer):
                    result = result.mean(axis=-1)
                else:
                    result = result.mean(axis=0)

        if len(result.shape) == 3 and result.shape[0] == 1:
            result = result[0]

        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

Optional[Dict[str, float]]

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=None, inputs=None, noise_params=None, cache=False, execution_type=None, force_mean=False) #

Perform a forward pass of the quantum circuit.

Parameters:

Name Type Description Default
params Optional[ndarray]

Weight vector of shape [n_layers, n_qubits*n_params_per_layer]. If None, model internal parameters are used.

None
inputs Optional[ndarray]

Input vector of shape [1]. If None, zeros are used.

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

    Args:
        params (Optional[np.ndarray]): Weight vector of shape
            [n_layers, n_qubits*n_params_per_layer].
            If None, model internal parameters are used.
        inputs (Optional[np.ndarray]): Input vector of shape [1].
            If None, zeros are used.
        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=params,
        inputs=inputs,
        noise_params=noise_params,
        cache=cache,
        execution_type=execution_type,
        force_mean=force_mean,
    )

__init__(n_qubits, n_layers, circuit_type, data_reupload=True, initialization='random', initialization_domain=[0, 2 * np.pi], output_qubit=-1, 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, Circuit)

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.

-1
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
 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
def __init__(
    self,
    n_qubits: int,
    n_layers: int,
    circuit_type: Union[str, Circuit],
    data_reupload: bool = True,
    initialization: str = "random",
    initialization_domain: List[float] = [0, 2 * np.pi],
    output_qubit: Union[List[int], int] = -1,
    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, Circuit): 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

    lightning_threshold = 12

    # Initialize ansatz
    # only weak check for str. We trust the user to provide sth useful
    if isinstance(circuit_type, str):
        self.pqc: Callable[[Optional[np.ndarray], int], int] = getattr(
            Ansaetze, circuit_type or "No_Ansatz"
        )()
    else:
        self.pqc = circuit_type()

    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}.")
    # calculate the shape of the parameter vector here, we will re-use this in init.
    self._params_shape: Tuple[int, int] = (
        impl_n_layers,
        self.pqc.n_params_per_layer(self.n_qubits),
    )
    # this will also be re-used in the init method,
    # however, only if nothing is provided
    self._inialization_strategy = initialization
    self._initialization_domain = initialization_domain

    # ..here! where we only require a rng
    self.initialize_params(np.random.default_rng(random_seed))

    # 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"
                if self.n_qubits < lightning_threshold
                else "lightning.qubit"
            ),
            shots=self.shots,
            wires=self.n_qubits,
        ),
        interface="autograd" if self.shots is not None else "auto",
        diff_method="parameter-shift" if self.shots is not None else "best",
    )
    self.circuit_mixed: qml.QNode = qml.QNode(
        self._circuit,
        qml.device("default.mixed", shots=self.shots, wires=self.n_qubits),
    )

    log.debug(self._draw())

initialize_params(rng, repeat=None, initialization=None, initialization_domain=None) #

Initializes the parameters of the model.

Parameters:

Name Type Description Default
rng

A random number generator to use for initialization.

required
repeat int

The number of times to repeat the parameters. If None, the number of layers is used.

None
initialization str

The strategy to use for parameter initialization. If None, the strategy specified in the constructor is used.

None
initialization_domain List[float]

The domain to use for parameter initialization. If None, the domain specified in the constructor is used.

None

Returns:

Type Description
None

None

Source code in qml_essentials/model.py
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
def initialize_params(
    self,
    rng,
    repeat: int = None,
    initialization: str = None,
    initialization_domain: List[float] = None,
) -> None:
    """
    Initializes the parameters of the model.

    Args:
        rng: A random number generator to use for initialization.
        repeat: The number of times to repeat the parameters.
            If None, the number of layers is used.
        initialization: The strategy to use for parameter initialization.
            If None, the strategy specified in the constructor is used.
        initialization_domain: The domain to use for parameter initialization.
            If None, the domain specified in the constructor is used.

    Returns:
        None
    """
    params_shape = (
        self._params_shape if repeat is None else [*self._params_shape, repeat]
    )
    # use existing strategy if not specified
    initialization = initialization or self._inialization_strategy
    initialization_domain = initialization_domain or self._initialization_domain

    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

    if initialization == "random":
        self.params: np.ndarray = rng.uniform(
            *initialization_domain, 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(
            *initialization_domain, params_shape, requires_grad=True
        )
        self.params = set_control_params(self.params, 0)
    elif initialization == "pi-controlled":
        self.params: np.ndarray = rng.uniform(
            *initialization_domain, 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}."
    )

Entanglement#

from qml_essentials.entanglement import Entanglement
Source code in qml_essentials/entanglement.py
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
class Entanglement:

    @staticmethod
    def meyer_wallach(
        model: Model,
        n_samples: Optional[int | None],
        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.
            n_samples (int): Number of samples per qubit.
                If None or < 0, the current parameters of the model are used
            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.
        """
        rng = np.random.default_rng(seed)
        if n_samples is not None and n_samples > 0:
            assert seed is not None, "Seed must be provided when samples > 0"
            # TODO: maybe switch to JAX rng
            model.initialize_params(rng=rng, repeat=n_samples)
            params = model.params
        else:
            if seed is not None:
                log.warning("Seed is ignored when samples is 0")

            if len(model.params.shape) <= 2:
                params = model.params.reshape(*model.params.shape, 1)
            else:
                log.info(f"Using sample size of model params: {model.params.shape[-1]}")
                params = model.params

        n_samples = params.shape[-1]
        mw_measure = np.zeros(n_samples, dtype=complex)
        qb = list(range(model.n_qubits))

        # TODO: vectorize in future iterations
        for i in range(n_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 = model(params=params[:, :, i], execution_type="density", **kwargs)

            entropy = 0

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

            mw_measure[i] = 1 - entropy / model.n_qubits

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

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

        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.

required
n_samples int

Number of samples per qubit. If None or < 0, the current parameters of the model are used

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
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
@staticmethod
def meyer_wallach(
    model: Model,
    n_samples: Optional[int | None],
    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.
        n_samples (int): Number of samples per qubit.
            If None or < 0, the current parameters of the model are used
        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.
    """
    rng = np.random.default_rng(seed)
    if n_samples is not None and n_samples > 0:
        assert seed is not None, "Seed must be provided when samples > 0"
        # TODO: maybe switch to JAX rng
        model.initialize_params(rng=rng, repeat=n_samples)
        params = model.params
    else:
        if seed is not None:
            log.warning("Seed is ignored when samples is 0")

        if len(model.params.shape) <= 2:
            params = model.params.reshape(*model.params.shape, 1)
        else:
            log.info(f"Using sample size of model params: {model.params.shape[-1]}")
            params = model.params

    n_samples = params.shape[-1]
    mw_measure = np.zeros(n_samples, dtype=complex)
    qb = list(range(model.n_qubits))

    # TODO: vectorize in future iterations
    for i in range(n_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 = model(params=params[:, :, i], execution_type="density", **kwargs)

        entropy = 0

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

        mw_measure[i] = 1 - entropy / model.n_qubits

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

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

    return float(entangling_capability)

Expressibility#

from qml_essentials.expressibility import Expressibility
Source code in qml_essentials/expressibility.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
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
class Expressibility:
    @staticmethod
    def _sample_state_fidelities(
        model: Model,
        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): Function that models the quantum circuit.
            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)

        # Generate random parameter sets
        # We need two sets of parameters, as we are computing fidelities for a
        # pair of random state vectors
        model.initialize_params(rng=rng, repeat=n_samples * 2)

        n_x_samples = len(x_samples)

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

        # 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=model.params,
                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
            )
            # TODO: abs instead?
            fidelities[idx] = np.real(fidelity)

        return fidelities

    @staticmethod
    def state_fidelities(
        seed: int,
        n_samples: int,
        n_bins: int,
        n_input_samples: int,
        input_domain: List[float],
        model: Model,
        scale: bool = False,
        **kwargs: Any,
    ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
        """
        Sample the state fidelities and histogram them into a 2D array.

        Args:
            seed (int): Random number generator seed.
            n_samples (int): Number of parameter sets to generate.
            n_bins (int): Number of histogram bins.
            n_input_samples (int): Number of input samples.
            input_domain (List[float]): Input domain.
            model (Callable): Function that models the quantum circuit.
            scale (bool): Whether to scale the number of samples and bins.
            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.
        """
        if scale:
            n_samples = np.power(2, model.n_qubits) * n_samples
            n_bins = model.n_qubits * n_bins

        if input_domain is None or n_input_samples is None or n_input_samples == 0:
            x = np.zeros((1))
            n_input_samples = 1
        else:
            x = np.linspace(*input_domain, 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((n_input_samples, n_bins))

        y: np.ndarray = np.linspace(0, 1, n_bins + 1)

        for i, f in enumerate(fidelities):
            z[i], _ = np.histogram(f, bins=y)

        z = z / n_samples

        if z.shape[0] == 1:
            z = z.flatten()

        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,
        scale: bool = False,
    ) -> 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
            scale (bool): whether to scale the number of bins

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

        x = np.linspace(0, 1, n_bins)

        if cache:
            name = f"haar_{n_qubits}q_{n_bins}s_{'scaled' if scale else ''}.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, scale=False) 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
scale bool

whether to scale the number of bins

False

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
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
@staticmethod
def haar_integral(
    n_qubits: int,
    n_bins: int,
    cache: bool = True,
    scale: bool = False,
) -> 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
        scale (bool): whether to scale the number of bins

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

    x = np.linspace(0, 1, n_bins)

    if cache:
        name = f"haar_{n_qubits}q_{n_bins}s_{'scaled' if scale else ''}.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
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
@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(seed, n_samples, n_bins, n_input_samples, input_domain, model, scale=False, **kwargs) staticmethod #

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

Parameters:

Name Type Description Default
seed int

Random number generator seed.

required
n_samples int

Number of parameter sets to generate.

required
n_bins int

Number of histogram bins.

required
n_input_samples int

Number of input samples.

required
input_domain List[float]

Input domain.

required
model Callable

Function that models the quantum circuit.

required
scale bool

Whether to scale the number of samples and bins.

False
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
 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
@staticmethod
def state_fidelities(
    seed: int,
    n_samples: int,
    n_bins: int,
    n_input_samples: int,
    input_domain: List[float],
    model: Model,
    scale: bool = False,
    **kwargs: Any,
) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
    """
    Sample the state fidelities and histogram them into a 2D array.

    Args:
        seed (int): Random number generator seed.
        n_samples (int): Number of parameter sets to generate.
        n_bins (int): Number of histogram bins.
        n_input_samples (int): Number of input samples.
        input_domain (List[float]): Input domain.
        model (Callable): Function that models the quantum circuit.
        scale (bool): Whether to scale the number of samples and bins.
        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.
    """
    if scale:
        n_samples = np.power(2, model.n_qubits) * n_samples
        n_bins = model.n_qubits * n_bins

    if input_domain is None or n_input_samples is None or n_input_samples == 0:
        x = np.zeros((1))
        n_input_samples = 1
    else:
        x = np.linspace(*input_domain, 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((n_input_samples, n_bins))

    y: np.ndarray = np.linspace(0, 1, n_bins + 1)

    for i, f in enumerate(fidelities):
        z[i], _ = np.histogram(f, bins=y)

    z = z / n_samples

    if z.shape[0] == 1:
        z = z.flatten()

    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
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Coefficients:

    @staticmethod
    def sample_coefficients(model: Model, **kwargs) -> np.ndarray:
        """
        Sample the Fourier coefficients of a given model
        using Pennylane fourier.coefficients function.

        Note that the coefficients are complex numbers, but the imaginary part
        of the coefficients should be very close to zero, since the expectation
        values of the Pauli operators are real numbers.

        Args:
            model (Model): The model to sample.
            kwargs (Any): Additional keyword arguments for the model function.

        Returns:
            np.ndarray: The sampled Fourier coefficients.
        """
        kwargs.setdefault("force_mean", True)
        kwargs.setdefault("execution_type", "expval")

        partial_circuit = partial(model, model.params, **kwargs)
        coeffs = coefficients(partial_circuit, 1, model.degree)

        if not np.isclose(np.sum(coeffs).imag, 0.0, rtol=1.0e-5):
            raise ValueError(
                f"Spectrum is not real. Imaginary part of coefficients is:\
                {np.sum(coeffs).imag}"
            )

        return coeffs

sample_coefficients(model, **kwargs) staticmethod #

Sample the Fourier coefficients of a given model using Pennylane fourier.coefficients function.

Note that the coefficients are complex numbers, but the imaginary part of the coefficients should be very close to zero, since the expectation values of the Pauli operators are real numbers.

Parameters:

Name Type Description Default
model Model

The model to sample.

required
kwargs Any

Additional keyword arguments for the model function.

{}

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
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@staticmethod
def sample_coefficients(model: Model, **kwargs) -> np.ndarray:
    """
    Sample the Fourier coefficients of a given model
    using Pennylane fourier.coefficients function.

    Note that the coefficients are complex numbers, but the imaginary part
    of the coefficients should be very close to zero, since the expectation
    values of the Pauli operators are real numbers.

    Args:
        model (Model): The model to sample.
        kwargs (Any): Additional keyword arguments for the model function.

    Returns:
        np.ndarray: The sampled Fourier coefficients.
    """
    kwargs.setdefault("force_mean", True)
    kwargs.setdefault("execution_type", "expval")

    partial_circuit = partial(model, model.params, **kwargs)
    coeffs = coefficients(partial_circuit, 1, model.degree)

    if not np.isclose(np.sum(coeffs).imag, 0.0, rtol=1.0e-5):
        raise ValueError(
            f"Spectrum is not real. Imaginary part of coefficients is:\
            {np.sum(coeffs).imag}"
        )

    return coeffs