/
quantum_state.py
503 lines (396 loc) · 17.4 KB
/
quantum_state.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
# This code is part of Qiskit.
#
# (C) Copyright IBM 2017, 2019.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.
"""
Abstract QuantumState class.
"""
from __future__ import annotations
import copy
from abc import abstractmethod
import numpy as np
from qiskit.quantum_info.operators.base_operator import BaseOperator
from qiskit.quantum_info.operators.channel.quantum_channel import QuantumChannel
from qiskit.quantum_info.operators.op_shape import OpShape
from qiskit.quantum_info.operators.operator import Operator
from qiskit.result.counts import Counts
class QuantumState:
"""Abstract quantum state base class"""
def __init__(self, op_shape: OpShape | None = None):
"""Initialize a QuantumState object.
Args:
op_shape (OpShape): Optional, an OpShape object for state dimensions.
.. note::
If `op_shape`` is specified it will take precedence over other
kwargs.
"""
self._op_shape = op_shape
# RNG for measure functions
self._rng_generator = None
# Set higher priority than Numpy array and matrix classes
__array_priority__ = 20
def __eq__(self, other):
return isinstance(other, self.__class__) and self.dims() == other.dims()
@property
def dim(self):
"""Return total state dimension."""
return self._op_shape.shape[0]
@property
def num_qubits(self):
"""Return the number of qubits if a N-qubit state or None otherwise."""
return self._op_shape.num_qubits
@property
def _rng(self):
if self._rng_generator is None:
return np.random.default_rng()
return self._rng_generator
def dims(self, qargs=None):
"""Return tuple of input dimension for specified subsystems."""
return self._op_shape.dims_l(qargs)
def copy(self):
"""Make a copy of current operator."""
return copy.deepcopy(self)
def seed(self, value=None):
"""Set the seed for the quantum state RNG."""
if value is None:
self._rng_generator = None
elif isinstance(value, np.random.Generator):
self._rng_generator = value
else:
self._rng_generator = np.random.default_rng(value)
@abstractmethod
def is_valid(self, atol=None, rtol=None):
"""Return True if a valid quantum state."""
pass
@abstractmethod
def to_operator(self):
"""Convert state to matrix operator class"""
pass
@abstractmethod
def conjugate(self):
"""Return the conjugate of the operator."""
pass
@abstractmethod
def trace(self):
"""Return the trace of the quantum state as a density matrix."""
pass
@abstractmethod
def purity(self):
"""Return the purity of the quantum state."""
pass
@abstractmethod
def tensor(self, other: QuantumState) -> QuantumState:
"""Return the tensor product state self ⊗ other.
Args:
other (QuantumState): a quantum state object.
Returns:
QuantumState: the tensor product operator self ⊗ other.
Raises:
QiskitError: if other is not a quantum state.
"""
pass
@abstractmethod
def expand(self, other: QuantumState) -> QuantumState:
"""Return the tensor product state other ⊗ self.
Args:
other (QuantumState): a quantum state object.
Returns:
QuantumState: the tensor product state other ⊗ self.
Raises:
QiskitError: if other is not a quantum state.
"""
pass
def _add(self, other):
"""Return the linear combination self + other.
Args:
other (QuantumState): a state object.
Returns:
QuantumState: the linear combination self + other.
Raises:
NotImplementedError: if subclass does not support addition.
"""
raise NotImplementedError(f"{type(self)} does not support addition")
def _multiply(self, other):
"""Return the scalar multipled state other * self.
Args:
other (complex): a complex number.
Returns:
QuantumState: the scalar multipled state other * self.
Raises:
NotImplementedError: if subclass does not support scala
multiplication.
"""
raise NotImplementedError(f"{type(self)} does not support scalar multiplication")
@abstractmethod
def evolve(self, other: Operator | QuantumChannel, qargs: list | None = None) -> QuantumState:
"""Evolve a quantum state by the operator.
Args:
other (Operator or QuantumChannel): The operator to evolve by.
qargs (list): a list of QuantumState subsystem positions to apply
the operator on.
Returns:
QuantumState: the output quantum state.
Raises:
QiskitError: if the operator dimension does not match the
specified QuantumState subsystem dimensions.
"""
pass
@abstractmethod
def expectation_value(self, oper: BaseOperator, qargs: None | list = None) -> complex:
"""Compute the expectation value of an operator.
Args:
oper (BaseOperator): an operator to evaluate expval.
qargs (None or list): subsystems to apply the operator on.
Returns:
complex: the expectation value.
"""
pass
@abstractmethod
def probabilities(self, qargs: None | list = None, decimals: None | int = None) -> np.ndarray:
"""Return the subsystem measurement probability vector.
Measurement probabilities are with respect to measurement in the
computation (diagonal) basis.
Args:
qargs (None or list): subsystems to return probabilities for,
if None return for all subsystems (Default: None).
decimals (None or int): the number of decimal places to round
values. If None no rounding is done (Default: None).
Returns:
np.array: The Numpy vector array of probabilities.
"""
pass
def probabilities_dict(self, qargs: None | list = None, decimals: None | int = None) -> dict:
"""Return the subsystem measurement probability dictionary.
Measurement probabilities are with respect to measurement in the
computation (diagonal) basis.
This dictionary representation uses a Ket-like notation where the
dictionary keys are qudit strings for the subsystem basis vectors.
If any subsystem has a dimension greater than 10 comma delimiters are
inserted between integers so that subsystems can be distinguished.
Args:
qargs (None or list): subsystems to return probabilities for,
if None return for all subsystems (Default: None).
decimals (None or int): the number of decimal places to round
values. If None no rounding is done (Default: None).
Returns:
dict: The measurement probabilities in dict (ket) form.
"""
return self._vector_to_dict(
self.probabilities(qargs=qargs, decimals=decimals),
self.dims(qargs),
string_labels=True,
)
def sample_memory(self, shots: int, qargs: None | list = None) -> np.ndarray:
"""Sample a list of qubit measurement outcomes in the computational basis.
Args:
shots (int): number of samples to generate.
qargs (None or list): subsystems to sample measurements for,
if None sample measurement of all
subsystems (Default: None).
Returns:
np.array: list of sampled counts if the order sampled.
Additional Information:
This function *samples* measurement outcomes using the measure
:meth:`probabilities` for the current state and `qargs`. It does
not actually implement the measurement so the current state is
not modified.
The seed for random number generator used for sampling can be
set to a fixed value by using the stats :meth:`seed` method.
"""
# Get measurement probabilities for measured qubits
probs = self.probabilities(qargs)
# Generate list of possible outcome string labels
labels = self._index_to_ket_array(
np.arange(len(probs)), self.dims(qargs), string_labels=True
)
return self._rng.choice(labels, p=probs, size=shots)
def sample_counts(self, shots: int, qargs: None | list = None) -> Counts:
"""Sample a dict of qubit measurement outcomes in the computational basis.
Args:
shots (int): number of samples to generate.
qargs (None or list): subsystems to sample measurements for,
if None sample measurement of all
subsystems (Default: None).
Returns:
Counts: sampled counts dictionary.
Additional Information:
This function *samples* measurement outcomes using the measure
:meth:`probabilities` for the current state and `qargs`. It does
not actually implement the measurement so the current state is
not modified.
The seed for random number generator used for sampling can be
set to a fixed value by using the stats :meth:`seed` method.
"""
# Sample list of outcomes
samples = self.sample_memory(shots, qargs=qargs)
# Combine all samples into a counts dictionary
inds, counts = np.unique(samples, return_counts=True)
return Counts(zip(inds, counts))
def measure(self, qargs: list | None = None) -> tuple:
"""Measure subsystems and return outcome and post-measure state.
Note that this function uses the QuantumStates internal random
number generator for sampling the measurement outcome. The RNG
seed can be set using the :meth:`seed` method.
Args:
qargs (list or None): subsystems to sample measurements for,
if None sample measurement of all
subsystems (Default: None).
Returns:
tuple: the pair ``(outcome, state)`` where ``outcome`` is the
measurement outcome string label, and ``state`` is the
collapsed post-measurement state for the corresponding
outcome.
"""
# Sample a single measurement outcome from probabilities
dims = self.dims(qargs)
probs = self.probabilities(qargs)
sample = self._rng.choice(len(probs), p=probs, size=1)
# Format outcome
outcome = self._index_to_ket_array(sample, self.dims(qargs), string_labels=True)[0]
# Convert to projector for state update
proj = np.zeros(len(probs), dtype=complex)
proj[sample] = 1 / np.sqrt(probs[sample])
# Update state object
# TODO: implement a more efficient state update method for
# diagonal matrix multiplication
ret = self.evolve(Operator(np.diag(proj), input_dims=dims, output_dims=dims), qargs=qargs)
return outcome, ret
@staticmethod
def _index_to_ket_array(
inds: np.ndarray, dims: tuple, string_labels: bool = False
) -> np.ndarray:
"""Convert an index array into a ket array.
Args:
inds (np.array): an integer index array.
dims (tuple): a list of subsystem dimensions.
string_labels (bool): return ket as string if True, otherwise
return as index array (Default: False).
Returns:
np.array: an array of ket strings if string_label=True, otherwise
an array of ket lists.
"""
shifts = [1]
for dim in dims[:-1]:
shifts.append(shifts[-1] * dim)
kets = np.array([(inds // shift) % dim for dim, shift in zip(dims, shifts)])
if string_labels:
max_dim = max(dims)
char_kets = np.asarray(kets, dtype=np.str_)
str_kets = char_kets[0]
for row in char_kets[1:]:
if max_dim > 10:
str_kets = np.char.add(",", str_kets)
str_kets = np.char.add(row, str_kets)
return str_kets.T
return kets.T
@staticmethod
def _vector_to_dict(vec, dims, decimals=None, string_labels=False):
"""Convert a vector to a ket dictionary.
This representation will not show zero values in the output dict.
Args:
vec (array): a Numpy vector array.
dims (tuple): subsystem dimensions.
decimals (None or int): number of decimal places to round to.
(See Numpy.round), if None no rounding
is done (Default: None).
string_labels (bool): return ket as string if True, otherwise
return as index array (Default: False).
Returns:
dict: the vector in dictionary `ket` form.
"""
# Get indices of non-zero elements
vals = vec if decimals is None else vec.round(decimals=decimals)
(inds,) = vals.nonzero()
# Convert to ket tuple based on subsystem dimensions
kets = QuantumState._index_to_ket_array(inds, dims, string_labels=string_labels)
# Make dict of tuples
if string_labels:
return dict(zip(kets, vec[inds]))
return {tuple(ket): val for ket, val in zip(kets, vals[inds])}
@staticmethod
def _matrix_to_dict(mat, dims, decimals=None, string_labels=False):
"""Convert a matrix to a ket dictionary.
This representation will not show zero values in the output dict.
Args:
mat (array): a Numpy matrix array.
dims (tuple): subsystem dimensions.
decimals (None or int): number of decimal places to round to.
(See Numpy.round), if None no rounding
is done (Default: None).
string_labels (bool): return ket as string if True, otherwise
return as index array (Default: False).
Returns:
dict: the matrix in dictionary `ket` form.
"""
# Get indices of non-zero elements
vals = mat if decimals is None else mat.round(decimals=decimals)
(
inds_row,
inds_col,
) = vals.nonzero()
# Convert to ket tuple based on subsystem dimensions
bras = QuantumState._index_to_ket_array(inds_row, dims, string_labels=string_labels)
kets = QuantumState._index_to_ket_array(inds_col, dims, string_labels=string_labels)
# Make dict of tuples
if string_labels:
return {
f"{ket}|{bra}": val for ket, bra, val in zip(kets, bras, vals[inds_row, inds_col])
}
return {
(tuple(ket), tuple(bra)): val
for ket, bra, val in zip(kets, bras, vals[inds_row, inds_col])
}
@staticmethod
def _subsystem_probabilities(
probs: np.ndarray, dims: tuple, qargs: None | list = None
) -> np.ndarray:
"""Marginalize a probability vector according to subsystems.
Args:
probs (np.array): a probability vector Numpy array.
dims (tuple): subsystem dimensions.
qargs (None or list): a list of subsystems to return
marginalized probabilities for. If None return all
probabilities (Default: None).
Returns:
np.array: the marginalized probability vector flattened
for the specified qargs.
"""
if qargs is None:
return probs
# Convert qargs to tensor axes
probs_tens = np.reshape(probs, list(reversed(dims)))
ndim = probs_tens.ndim
qargs_axes = [ndim - 1 - i for i in reversed(qargs)]
# Get sum axis for marginalized subsystems
sum_axis = tuple(i for i in range(ndim) if i not in qargs_axes)
if sum_axis:
probs_tens = np.sum(probs_tens, axis=sum_axis)
qargs_axes = np.argsort(np.argsort(qargs_axes))
# Permute probability vector for desired qargs order
probs_tens = np.transpose(probs_tens, axes=qargs_axes)
new_probs = np.reshape(probs_tens, (probs_tens.size,))
return new_probs
# Overloads
def __and__(self, other):
return self.evolve(other)
def __xor__(self, other):
return self.tensor(other)
def __mul__(self, other):
return self._multiply(other)
def __truediv__(self, other):
return self._multiply(1 / other)
def __rmul__(self, other):
return self.__mul__(other)
def __add__(self, other):
return self._add(other)
def __sub__(self, other):
return self._add(-other)
def __neg__(self):
return self._multiply(-1)