dbac 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dbac/__init__.py +12 -0
- dbac/dbqite.py +192 -0
- dbac/dme.py +160 -0
- dbac/gci.py +226 -0
- dbac/hbac.py +100 -0
- dbac/plotting/__init__.py +23 -0
- dbac/plotting/bloch.py +255 -0
- dbac/plotting/circuits.py +117 -0
- dbac/plotting/cooling.py +468 -0
- dbac/plotting/dme.py +62 -0
- dbac/plotting/gci.py +48 -0
- dbac/plotting/ptm.py +174 -0
- dbac/plotting/style.py +45 -0
- dbac/utils.py +137 -0
- dbac-0.1.0.dist-info/METADATA +138 -0
- dbac-0.1.0.dist-info/RECORD +17 -0
- dbac-0.1.0.dist-info/WHEEL +4 -0
dbac/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""dbac: physics and numerics for double-bracket algorithmic cooling (plotting lives in `dbac.plotting`)."""
|
|
2
|
+
|
|
3
|
+
import warnings
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
__version__ = "0.1.0"
|
|
8
|
+
|
|
9
|
+
# Silence noisy-but-harmless warnings: complex values being cast to real when
|
|
10
|
+
# reading off energies/fidelities, and GCI's "edge of interval" step-size notice.
|
|
11
|
+
warnings.filterwarnings("ignore", category=np.exceptions.ComplexWarning)
|
|
12
|
+
warnings.filterwarnings("ignore", message=".*edge of interval reached.*")
|
dbac/dbqite.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""
|
|
2
|
+
dbqite.py
|
|
3
|
+
|
|
4
|
+
One step of double-bracket quantum imaginary-time evolution (DB-QITE), the
|
|
5
|
+
cooling step of DBAC, for a single qubit with Hamiltonian H = -Z.
|
|
6
|
+
|
|
7
|
+
A step maps the current state psi to exp(i s rho') psi exp(-i s rho'), where
|
|
8
|
+
rho' = exp(i s H) |psi><psi| exp(-i s H) is a rotated copy of psi used as the
|
|
9
|
+
instruction of density matrix exponentiation (DME). All step functions share
|
|
10
|
+
the signature `step(psi, s) -> density matrix`, so they can be passed to
|
|
11
|
+
`dbac.gci.GCI` to run several steps.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"DME",
|
|
16
|
+
"QITE_GCI_dme",
|
|
17
|
+
"QITE_dme_select_M",
|
|
18
|
+
"initial_psi",
|
|
19
|
+
"ideal_DME",
|
|
20
|
+
"H", "H_sh", "eigs", "target_energy"
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
import numpy as np
|
|
24
|
+
from scipy.linalg import expm
|
|
25
|
+
from qibo import hamiltonians
|
|
26
|
+
from qibo.symbols import Z
|
|
27
|
+
|
|
28
|
+
# Constants and Hamiltonian setup
|
|
29
|
+
L = 1
|
|
30
|
+
#: The same Hamiltonian as a qibo ``SymbolicHamiltonian``, for qibo-circuit states.
|
|
31
|
+
H_sh = hamiltonians.SymbolicHamiltonian(Z(0), nqubits=1)
|
|
32
|
+
#: The Hamiltonian H = -Z = diag(-1, 1) whose ground state ``|0>`` DBAC cools towards.
|
|
33
|
+
H = np.array([[-1.-0.j, -0.-0.j],
|
|
34
|
+
[-0.-0.j, 1.-0.j]])
|
|
35
|
+
#: Eigenvalues of `H`: the ground-state energy -1 and the excited-state energy +1.
|
|
36
|
+
eigs = np.array([-1., 1.])
|
|
37
|
+
#: Energy of the target (ground) state ``|0>``.
|
|
38
|
+
target_energy = -1
|
|
39
|
+
|
|
40
|
+
def _as_density_matrix(psi: np.ndarray) -> np.ndarray:
|
|
41
|
+
"""Return `psi` as a density matrix: a 2x2 input is taken to already be one
|
|
42
|
+
(e.g. the output of a previous DB-QITE step, or a mixed state), a (2, 1)
|
|
43
|
+
state vector is turned into |psi><psi|."""
|
|
44
|
+
if psi.ndim == 2 and psi.shape[0] == psi.shape[1]:
|
|
45
|
+
return psi
|
|
46
|
+
psi = psi.reshape(-1, 1)
|
|
47
|
+
return psi @ psi.conj().T
|
|
48
|
+
|
|
49
|
+
def DME(instruction: np.ndarray, data: np.ndarray, t: float, M: int = 100) -> np.ndarray:
|
|
50
|
+
"""
|
|
51
|
+
Apply exp(i t instruction) to `data` with Trotterized density matrix
|
|
52
|
+
exponentiation, without ever exponentiating `instruction` itself.
|
|
53
|
+
|
|
54
|
+
Each of the `M` steps is the DME channel for duration t/M with a fresh copy
|
|
55
|
+
of `instruction`, written in closed form:
|
|
56
|
+
``cos^2 data + sin^2 instruction + i sin cos [instruction, data]``.
|
|
57
|
+
The result approaches ``exp(i t instruction) data exp(-i t instruction)``
|
|
58
|
+
with an error of order t^2 / M.
|
|
59
|
+
|
|
60
|
+
Note the argument order: the instruction comes first, unlike the functions
|
|
61
|
+
in `dbac.dme`.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
instruction (np.ndarray): 2x2 density matrix used as the Hamiltonian
|
|
65
|
+
(the instruction state).
|
|
66
|
+
data (np.ndarray): 2x2 density matrix to transform (the data state).
|
|
67
|
+
t (float): Total DME duration.
|
|
68
|
+
M (int): Number of Trotter steps, i.e. instruction copies consumed.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
np.ndarray: The transformed 2x2 data density matrix.
|
|
72
|
+
|
|
73
|
+
Example:
|
|
74
|
+
>>> import numpy as np
|
|
75
|
+
>>> from dbac.dbqite import DME
|
|
76
|
+
>>> zero = np.array([[1, 0], [0, 0]], dtype=complex) # data |0><0|
|
|
77
|
+
>>> plus = np.full((2, 2), 0.5, dtype=complex) # instruction |+><+|
|
|
78
|
+
>>> # t = pi/2 with one step is a full SWAP: the data becomes the instruction
|
|
79
|
+
>>> DME(plus, zero, t=np.pi / 2, M=1).real.round(3)
|
|
80
|
+
array([[0.5, 0.5],
|
|
81
|
+
[0.5, 0.5]])
|
|
82
|
+
"""
|
|
83
|
+
_t = t / M
|
|
84
|
+
c = np.cos(_t)
|
|
85
|
+
s = np.sin(_t)
|
|
86
|
+
for _ in range(M):
|
|
87
|
+
data = c**2 * data + s**2 * instruction + 1j * s * c * (instruction @ data - data @ instruction)
|
|
88
|
+
return data
|
|
89
|
+
|
|
90
|
+
def QITE_GCI_dme(psi: np.ndarray, s: float, M: int = 100) -> np.ndarray:
|
|
91
|
+
"""
|
|
92
|
+
One DB-QITE step with Trotterized DME, using `M` = 100 DME steps by default.
|
|
93
|
+
|
|
94
|
+
Equivalent to ``QITE_dme_select_M(M)(psi, s)``; prefer `QITE_dme_select_M`
|
|
95
|
+
when passing a step function with a specific `M` to `dbac.gci.GCI`.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
psi (np.ndarray): Current state, a (2, 1) state vector or 2x2 density matrix.
|
|
99
|
+
s (float): Step duration.
|
|
100
|
+
M (int): Number of DME steps.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
np.ndarray: The 2x2 density matrix after the step.
|
|
104
|
+
"""
|
|
105
|
+
QITE_GCI_dme.label = r"DME"
|
|
106
|
+
QITE_GCI_dme.marker = '^'
|
|
107
|
+
PSI = _as_density_matrix(psi)
|
|
108
|
+
U_t = expm(1j * s * H)
|
|
109
|
+
return DME(U_t @ PSI @ U_t.conj().T, PSI, s, M=M)
|
|
110
|
+
|
|
111
|
+
def QITE_dme_select_M(M: int = 100):
|
|
112
|
+
"""
|
|
113
|
+
Build a DB-QITE step function that uses `M` DME (Trotter) steps.
|
|
114
|
+
|
|
115
|
+
The returned function ``step(psi, s)`` performs one DBAC step of duration
|
|
116
|
+
`s` on `psi`: it rotates a copy of the state about H to get the
|
|
117
|
+
instruction ``exp(i s H) psi exp(-i s H)``, then applies it to `psi` with
|
|
118
|
+
`DME` using `M` steps. `M` = 1 and `M` = 2 correspond to circuits A and B
|
|
119
|
+
of the paper's experiment.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
M (int): Number of DME steps per DBAC step.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Callable[[np.ndarray, float], np.ndarray]: ``step(psi, s)``, which takes
|
|
126
|
+
a (2, 1) state vector or 2x2 density matrix and returns the 2x2 density
|
|
127
|
+
matrix after the step.
|
|
128
|
+
|
|
129
|
+
Example:
|
|
130
|
+
>>> import numpy as np
|
|
131
|
+
>>> from dbac.dbqite import QITE_dme_select_M, H
|
|
132
|
+
>>> from dbac.utils import rx_sweep
|
|
133
|
+
>>> psi = rx_sweep([0.5])[0] # energy 0, halfway to |1>
|
|
134
|
+
>>> rho = QITE_dme_select_M(1)(psi, np.pi / 4)
|
|
135
|
+
>>> round(float(np.trace(rho @ H).real), 3)
|
|
136
|
+
-0.5
|
|
137
|
+
"""
|
|
138
|
+
def QITE_GCI_dme(psi: np.ndarray, s: float, M: int = M) -> np.ndarray:
|
|
139
|
+
QITE_GCI_dme.label = r"DME"
|
|
140
|
+
QITE_GCI_dme.marker = '^'
|
|
141
|
+
PSI = _as_density_matrix(psi)
|
|
142
|
+
U_t = expm(1j * s * H)
|
|
143
|
+
return DME(U_t @ PSI @ U_t.conj().T, PSI, s, M=M)
|
|
144
|
+
return QITE_GCI_dme
|
|
145
|
+
|
|
146
|
+
def initial_psi(psi: np.ndarray, s: float) -> np.ndarray:
|
|
147
|
+
"""
|
|
148
|
+
A step function that does nothing: returns `psi` unchanged.
|
|
149
|
+
|
|
150
|
+
Useful as the "no cooling" baseline, since it has the same signature as the
|
|
151
|
+
other step functions and can be passed to `dbac.gci.GCI`.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
psi (np.ndarray): Current state.
|
|
155
|
+
s (float): Step duration (unused).
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
np.ndarray: The input state.
|
|
159
|
+
"""
|
|
160
|
+
initial_psi.label = "Initial state"
|
|
161
|
+
initial_psi.marker = 'o'
|
|
162
|
+
return psi
|
|
163
|
+
|
|
164
|
+
def ideal_DME(psi: np.ndarray, s: float) -> np.ndarray:
|
|
165
|
+
"""
|
|
166
|
+
One DB-QITE step with the reflection applied exactly (the limit M -> infinity).
|
|
167
|
+
|
|
168
|
+
Same as ``QITE_dme_select_M(M)(psi, s)`` for very large `M`, but computes
|
|
169
|
+
``exp(i s rho')`` by matrix exponential instead of DME. For this step the
|
|
170
|
+
paper gives the energy afterwards in closed form,
|
|
171
|
+
``E0 - 2 sin^2(s) (1 - E0^2) [(1 - cos s) E0 + cos s]``.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
psi (np.ndarray): Current state, a (2, 1) state vector or 2x2 density matrix.
|
|
175
|
+
s (float): Step duration.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
np.ndarray: The 2x2 density matrix after the step.
|
|
179
|
+
|
|
180
|
+
Example:
|
|
181
|
+
>>> import numpy as np
|
|
182
|
+
>>> from dbac.dbqite import ideal_DME, H
|
|
183
|
+
>>> from dbac.utils import rx_sweep
|
|
184
|
+
>>> rho = ideal_DME(rx_sweep([0.5])[0], np.pi / 4)
|
|
185
|
+
>>> round(float(np.trace(rho @ H).real), 3) # -1/sqrt(2)
|
|
186
|
+
-0.707
|
|
187
|
+
"""
|
|
188
|
+
PSI = _as_density_matrix(psi)
|
|
189
|
+
U_t = expm(1j * s * H)
|
|
190
|
+
instruction = U_t @ PSI @ U_t.conj().T
|
|
191
|
+
V = expm(1j * s * instruction)
|
|
192
|
+
return V @ PSI @ V.conj().T
|
dbac/dme.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""
|
|
2
|
+
dme.py
|
|
3
|
+
|
|
4
|
+
Implementations of the density matrix exponentiation (DME) channel, which
|
|
5
|
+
applies ``exp(i t rho) sigma exp(-i t rho)`` to a data state `sigma` using
|
|
6
|
+
copies of an instruction state `rho`, without knowing `rho`.
|
|
7
|
+
|
|
8
|
+
All functions take ``(data, instruction, t, M)``: the data and instruction
|
|
9
|
+
2x2 density matrices, the total duration `t`, and the number of Trotter steps
|
|
10
|
+
`M` (instruction copies consumed). The approximate implementations return
|
|
11
|
+
``(final_data, history)`` where `history` holds the data state after each of
|
|
12
|
+
the `M` steps. They compute the same channel in different ways so they can be
|
|
13
|
+
checked against each other (the paper's Fig. S2).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"dme_expm", "dme_qibo", "dme_channel", "dme_channel_equivalent", "ptrace"
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
from scipy.linalg import expm
|
|
22
|
+
from qibo.quantum_info.linalg_operations import partial_trace
|
|
23
|
+
from qibo.gates import SWAP
|
|
24
|
+
|
|
25
|
+
def dme_expm(data, instruction, t, M=None):
|
|
26
|
+
"""
|
|
27
|
+
The exact evolution ``exp(i t instruction) data exp(-i t instruction)``,
|
|
28
|
+
i.e. the M -> infinity limit that DME approximates. The reference for the
|
|
29
|
+
other implementations.
|
|
30
|
+
|
|
31
|
+
Parameters:
|
|
32
|
+
data (np.ndarray): 2x2 data density matrix.
|
|
33
|
+
instruction (np.ndarray): 2x2 instruction density matrix (the Hamiltonian).
|
|
34
|
+
t (float): Duration.
|
|
35
|
+
M: Ignored; accepted so the signature matches the other implementations.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
np.ndarray: The evolved 2x2 data density matrix (not a tuple).
|
|
39
|
+
"""
|
|
40
|
+
V = expm(1j * t * instruction)
|
|
41
|
+
return V @ data @ V.conj().T
|
|
42
|
+
|
|
43
|
+
def dme_qibo(data, instruction, t, M):
|
|
44
|
+
"""
|
|
45
|
+
DME built from qibo's SWAP gate matrix and qibo's partial trace.
|
|
46
|
+
|
|
47
|
+
Same channel as `dme_channel`; useful to cross-check the numpy
|
|
48
|
+
implementation against qibo.
|
|
49
|
+
|
|
50
|
+
Parameters:
|
|
51
|
+
data (np.ndarray): 2x2 data density matrix.
|
|
52
|
+
instruction (np.ndarray): 2x2 instruction density matrix.
|
|
53
|
+
t (float): Total duration, split into `M` steps of ``t / M``.
|
|
54
|
+
M (int): Number of Trotter steps.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
tuple[np.ndarray, list[np.ndarray]]: The final data state, and the data
|
|
58
|
+
state after each step.
|
|
59
|
+
"""
|
|
60
|
+
rho_list = []
|
|
61
|
+
_t = t / M
|
|
62
|
+
# Not qibo's `matrix_exponentiation`: in qibo 0.3.x it returns expm(phase * M)
|
|
63
|
+
# without the factor of i, i.e. a non-unitary operator.
|
|
64
|
+
u = expm(1j * _t * SWAP(0, 1).matrix())
|
|
65
|
+
dme_state = np.kron(data, instruction)
|
|
66
|
+
for _ in range(M):
|
|
67
|
+
dme_state = u @ dme_state @ u.T.conj()
|
|
68
|
+
data = partial_trace(dme_state, [1])
|
|
69
|
+
rho_list.append(data)
|
|
70
|
+
dme_state = np.kron(data, instruction)
|
|
71
|
+
return data, rho_list
|
|
72
|
+
|
|
73
|
+
swap = np.array([[1, 0, 0, 0],
|
|
74
|
+
[0, 0, 1, 0],
|
|
75
|
+
[0, 1, 0, 0],
|
|
76
|
+
[0, 0, 0, 1]], dtype=complex)
|
|
77
|
+
|
|
78
|
+
def ptrace(matrix, index=1, n1=2, n2=2):
|
|
79
|
+
"""
|
|
80
|
+
Partial trace of a bipartite ``(n1 * n2) x (n1 * n2)`` matrix.
|
|
81
|
+
|
|
82
|
+
Parameters:
|
|
83
|
+
matrix (np.ndarray): Operator on subsystems 1 (dimension `n1`) and 2
|
|
84
|
+
(dimension `n2`).
|
|
85
|
+
index (int): Which subsystem to trace **out**: 1 traces out subsystem 2
|
|
86
|
+
and keeps subsystem 1; 2 traces out subsystem 1 and keeps subsystem 2.
|
|
87
|
+
n1, n2 (int): Dimensions of the two subsystems.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
np.ndarray: The reduced operator.
|
|
91
|
+
"""
|
|
92
|
+
current_tensor = matrix.reshape([n1, n2, n1, n2])
|
|
93
|
+
if index == 1:
|
|
94
|
+
return np.trace(current_tensor, axis1=1, axis2=3)
|
|
95
|
+
elif index == 2:
|
|
96
|
+
return np.trace(current_tensor, axis1=0, axis2=2)
|
|
97
|
+
else:
|
|
98
|
+
print('Error')
|
|
99
|
+
|
|
100
|
+
def dme_channel(data, instruction, t, M):
|
|
101
|
+
"""
|
|
102
|
+
DME as on hardware: apply a partial SWAP ``exp(i (t / M) SWAP)`` to
|
|
103
|
+
``data (x) instruction``, trace out the instruction, and repeat `M` times
|
|
104
|
+
with a fresh instruction copy.
|
|
105
|
+
|
|
106
|
+
Parameters:
|
|
107
|
+
data (np.ndarray): 2x2 data density matrix.
|
|
108
|
+
instruction (np.ndarray): 2x2 instruction density matrix.
|
|
109
|
+
t (float): Total duration, split into `M` steps of ``t / M``.
|
|
110
|
+
M (int): Number of Trotter steps (instruction copies consumed).
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
tuple[np.ndarray, list[np.ndarray]]: The final data state, and the data
|
|
114
|
+
state after each step.
|
|
115
|
+
|
|
116
|
+
Example:
|
|
117
|
+
>>> import numpy as np
|
|
118
|
+
>>> from dbac.dme import dme_channel, dme_expm
|
|
119
|
+
>>> data = np.array([[1, 0], [0, 0]], dtype=complex)
|
|
120
|
+
>>> instruction = np.full((2, 2), 0.5, dtype=complex)
|
|
121
|
+
>>> exact = dme_expm(data, instruction, 1.0)
|
|
122
|
+
>>> [round(float(np.linalg.norm(exact - dme_channel(data, instruction, 1.0, M)[0])), 4)
|
|
123
|
+
... for M in (1, 10, 100)] # error shrinks like 1/M
|
|
124
|
+
[0.5969, 0.0757, 0.0079]
|
|
125
|
+
"""
|
|
126
|
+
rho_list = []
|
|
127
|
+
_t = t / M
|
|
128
|
+
u = expm(1j * _t * swap)
|
|
129
|
+
dme_state = np.kron(data, instruction)
|
|
130
|
+
for _ in range(M):
|
|
131
|
+
dme_state = u @ dme_state @ u.T.conj()
|
|
132
|
+
data = ptrace(dme_state)
|
|
133
|
+
rho_list.append(data)
|
|
134
|
+
dme_state = np.kron(data, instruction)
|
|
135
|
+
return data, rho_list
|
|
136
|
+
|
|
137
|
+
def dme_channel_equivalent(data, instruction, t, M):
|
|
138
|
+
"""
|
|
139
|
+
DME using the closed form of one channel step,
|
|
140
|
+
``cos^2 data + sin^2 instruction + i sin cos [instruction, data]``
|
|
141
|
+
(with angle ``t / M``), which avoids building two-qubit matrices.
|
|
142
|
+
|
|
143
|
+
Parameters:
|
|
144
|
+
data (np.ndarray): 2x2 data density matrix.
|
|
145
|
+
instruction (np.ndarray): 2x2 instruction density matrix.
|
|
146
|
+
t (float): Total duration, split into `M` steps of ``t / M``.
|
|
147
|
+
M (int): Number of Trotter steps.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
tuple[np.ndarray, list[np.ndarray]]: The final data state, and the data
|
|
151
|
+
state after each step.
|
|
152
|
+
"""
|
|
153
|
+
rho_list = []
|
|
154
|
+
_t = t / M
|
|
155
|
+
c = np.cos(_t)
|
|
156
|
+
s = np.sin(_t)
|
|
157
|
+
for _ in range(M):
|
|
158
|
+
data = c**2 * data + s**2 * instruction + 1j * s * c * (instruction @ data - data @ instruction)
|
|
159
|
+
rho_list.append(data)
|
|
160
|
+
return data, rho_list
|
dbac/gci.py
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GCI: Group Commutator Iteration, the driver that runs the DBAC recursion.
|
|
3
|
+
|
|
4
|
+
`GCI` applies a step function (from `dbac.dbqite`) repeatedly, feeding each
|
|
5
|
+
output state back in as the next input, and records energy, fidelity and the
|
|
6
|
+
states along the way. Plotting of the results produced by
|
|
7
|
+
`GCI.execute_gci` lives in `dbac.plotting.gci.plot_gci_results`, so this module has
|
|
8
|
+
no matplotlib dependency.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"GCI",
|
|
13
|
+
"max_excitation"
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
import warnings
|
|
17
|
+
|
|
18
|
+
from qibo import hamiltonians, Circuit
|
|
19
|
+
from qibo.symbols import Z
|
|
20
|
+
import numpy as np
|
|
21
|
+
|
|
22
|
+
class GCI:
|
|
23
|
+
"""
|
|
24
|
+
Run the DBAC recursion: apply a step function `k` times, each time to the
|
|
25
|
+
output of the previous step, and track energy and fidelity.
|
|
26
|
+
|
|
27
|
+
The step durations are chosen in one of two modes, selected by `nmb_steps`:
|
|
28
|
+
|
|
29
|
+
- **fixed schedule** (``nmb_steps=None``): `times_dbr` lists the duration
|
|
30
|
+
of each step, ``[s_1, ..., s_k]``.
|
|
31
|
+
- **greedy line search** (``nmb_steps=k``): `times_dbr` lists candidate
|
|
32
|
+
durations; every step uses the candidate that gives the lowest energy.
|
|
33
|
+
A warning is emitted if the best candidate is at the edge of the list.
|
|
34
|
+
|
|
35
|
+
Nothing is computed until `execute_gci` is called.
|
|
36
|
+
|
|
37
|
+
Parameters:
|
|
38
|
+
gci_step (Callable): Step function ``step(psi, s) -> state``, e.g.
|
|
39
|
+
``dbac.dbqite.QITE_dme_select_M(1)`` or ``dbac.dbqite.ideal_DME``.
|
|
40
|
+
state_of_recursion (np.ndarray): Initial state, a (2, 1) state vector
|
|
41
|
+
or 2x2 density matrix (or a qibo ``Circuit``).
|
|
42
|
+
nmb_steps (int or None): Number of steps for the line-search mode, or
|
|
43
|
+
None for the fixed-schedule mode.
|
|
44
|
+
times_dbr (sequence[float]): The schedule (fixed mode) or the
|
|
45
|
+
candidate durations (line-search mode).
|
|
46
|
+
please_be_verbose (bool): Unused; kept for backwards compatibility.
|
|
47
|
+
|
|
48
|
+
Example:
|
|
49
|
+
>>> import numpy as np
|
|
50
|
+
>>> from dbac.gci import GCI
|
|
51
|
+
>>> from dbac.dbqite import QITE_dme_select_M
|
|
52
|
+
>>> from dbac.utils import rx_sweep
|
|
53
|
+
>>> psi = rx_sweep([0.5])[0]
|
|
54
|
+
>>> results = GCI(QITE_dme_select_M(1), psi, None, [np.pi / 4, np.pi / 4]).execute_gci()
|
|
55
|
+
>>> [round(float(F), 3) for F in results[-1]] # fidelity with |0> after 0, 1, 2 steps
|
|
56
|
+
[0.5, 0.75, 0.875]
|
|
57
|
+
"""
|
|
58
|
+
def __init__(self, gci_step, state_of_recursion, nmb_steps, times_dbr, please_be_verbose=True):
|
|
59
|
+
self.gci_step = gci_step
|
|
60
|
+
self.state_of_recursion = state_of_recursion
|
|
61
|
+
self.psi0 = state_of_recursion
|
|
62
|
+
self.nmb_steps = nmb_steps
|
|
63
|
+
self.times_dbr = times_dbr
|
|
64
|
+
self.please_be_verbose = please_be_verbose
|
|
65
|
+
self.states = [state_of_recursion]
|
|
66
|
+
|
|
67
|
+
# Fixed problem setup for 1-qubit Z Hamiltonian
|
|
68
|
+
self.L = 1
|
|
69
|
+
self.H_sh = hamiltonians.SymbolicHamiltonian(Z(0), nqubits=1)
|
|
70
|
+
self.H = np.array([[-1.-0.j, -0.-0.j],
|
|
71
|
+
[-0.-0.j, 1.-0.j]])
|
|
72
|
+
self.eigs = np.array([-1., 1.])
|
|
73
|
+
self.target_energy = -1
|
|
74
|
+
|
|
75
|
+
# Initial tracking
|
|
76
|
+
self.results = []
|
|
77
|
+
self.fidelities = []
|
|
78
|
+
self.s_mins = [0]
|
|
79
|
+
self.state_0 = np.array([1, 0], dtype=complex)
|
|
80
|
+
self.fidelities.append(self.fidelity_with_0(state_of_recursion))
|
|
81
|
+
self.results.append(self.compute_moments(state_of_recursion))
|
|
82
|
+
|
|
83
|
+
if isinstance(state_of_recursion, Circuit):
|
|
84
|
+
self.circuit_depths = [0]
|
|
85
|
+
self.circuits = [state_of_recursion]
|
|
86
|
+
|
|
87
|
+
def fidelity_with_0(self, state):
|
|
88
|
+
"""Fidelity of a state vector or density matrix with ``|0>``."""
|
|
89
|
+
shape = np.shape(state)
|
|
90
|
+
if shape[0] == shape[1]: # density matrix
|
|
91
|
+
return np.real(np.conj(self.state_0).T @ state @ self.state_0)
|
|
92
|
+
else: # state vector
|
|
93
|
+
dm = state @ state.T.conj()
|
|
94
|
+
return np.real(np.conj(self.state_0).T @ dm @ self.state_0)
|
|
95
|
+
|
|
96
|
+
def compute_moments(self, psi):
|
|
97
|
+
"""
|
|
98
|
+
Energy moments of a state with respect to H = -Z.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
tuple: ``(E, E2, variance)`` with ``E = <H>``, ``E2 = <H^2>`` and
|
|
102
|
+
``variance = E2 - E^2``. For a density matrix the values are
|
|
103
|
+
complex-typed (take ``np.real``); for a qibo circuit only ``E`` is
|
|
104
|
+
computed.
|
|
105
|
+
"""
|
|
106
|
+
if isinstance(psi, Circuit):
|
|
107
|
+
return self.H_sh.expectation(psi(self.psi0.T[0]).state()), None, None
|
|
108
|
+
shape = np.shape(psi)
|
|
109
|
+
if shape[0] == shape[1]: # density matrix
|
|
110
|
+
E = np.trace(psi @ self.H)
|
|
111
|
+
S = np.trace(psi @ self.H @ self.H)
|
|
112
|
+
else: # state vector
|
|
113
|
+
E = (psi.conj().T @ self.H @ psi)[0, 0].real
|
|
114
|
+
S = (psi.conj().T @ self.H @ self.H @ psi)[0, 0].real
|
|
115
|
+
return E, S, S - E**2
|
|
116
|
+
|
|
117
|
+
def execute_gci(self):
|
|
118
|
+
"""
|
|
119
|
+
Run the recursion and return everything it recorded.
|
|
120
|
+
|
|
121
|
+
Every list in the result has one entry for the initial state followed
|
|
122
|
+
by one entry per step.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
tuple: An 11-tuple, best read from both ends (``results[0]`` for
|
|
126
|
+
energies, ``results[-1]`` for fidelities, ``results[-2]`` for the
|
|
127
|
+
final state):
|
|
128
|
+
|
|
129
|
+
0. energies: ``Tr(rho H)`` after each step (take ``np.real``).
|
|
130
|
+
1. step sizes: the durations used, starting with 0.
|
|
131
|
+
2. variances: energy variance after each step.
|
|
132
|
+
3. durations: cumulative evolution time, the sum of ``s_j ** 2``.
|
|
133
|
+
4. label: display label of the step function.
|
|
134
|
+
5. marker: matplotlib marker of the step function.
|
|
135
|
+
6. circuit depths: for qibo-circuit states only, else None.
|
|
136
|
+
7. circuits: for qibo-circuit states only, else None.
|
|
137
|
+
8. states: the state after each step.
|
|
138
|
+
9. final state: the last entry of `states`.
|
|
139
|
+
10. fidelities: fidelity with ``|0>`` after each step.
|
|
140
|
+
"""
|
|
141
|
+
if self.nmb_steps is None:
|
|
142
|
+
for s in self.times_dbr:
|
|
143
|
+
self.state_of_recursion = self.gci_step(self.state_of_recursion, s)
|
|
144
|
+
self.results.append(self.compute_moments(self.state_of_recursion))
|
|
145
|
+
self.fidelities.append(self.fidelity_with_0(self.state_of_recursion))
|
|
146
|
+
self.states.append(self.state_of_recursion)
|
|
147
|
+
self.s_mins = [0] + list(self.times_dbr)
|
|
148
|
+
else:
|
|
149
|
+
for k in range(self.nmb_steps):
|
|
150
|
+
test_es = [self.compute_moments(self.gci_step(self.state_of_recursion, dt))[0] for dt in self.times_dbr]
|
|
151
|
+
ind = np.argmin(test_es)
|
|
152
|
+
if ind == 0 or ind == len(self.times_dbr) - 1:
|
|
153
|
+
warnings.warn(f"{k} {ind} {self.times_dbr[ind]} --- Warning edge of interval reached")
|
|
154
|
+
s_min = self.times_dbr[ind]
|
|
155
|
+
self.state_of_recursion = self.gci_step(self.state_of_recursion, s_min)
|
|
156
|
+
self.results.append(self.compute_moments(self.state_of_recursion))
|
|
157
|
+
self.fidelities.append(self.fidelity_with_0(self.state_of_recursion))
|
|
158
|
+
self.s_mins.append(s_min)
|
|
159
|
+
self.states.append(self.state_of_recursion)
|
|
160
|
+
if isinstance(self.state_of_recursion, Circuit):
|
|
161
|
+
self.circuit_depths.append(len(self.state_of_recursion.queue) * 3)
|
|
162
|
+
self.circuits.append(self.state_of_recursion)
|
|
163
|
+
|
|
164
|
+
energies = [r[0] for r in self.results]
|
|
165
|
+
variances = [r[2] for r in self.results]
|
|
166
|
+
|
|
167
|
+
# Add default display labels if not already set
|
|
168
|
+
if not hasattr(self.gci_step, 'exponent'):
|
|
169
|
+
self.gci_step.exponent = 2
|
|
170
|
+
if not hasattr(self.gci_step, 'label'):
|
|
171
|
+
self.gci_step.label = ''
|
|
172
|
+
if not hasattr(self.gci_step, 'marker'):
|
|
173
|
+
self.gci_step.marker = '-'
|
|
174
|
+
|
|
175
|
+
durations = (
|
|
176
|
+
[sum(s**self.gci_step.exponent for s in self.s_mins[1:x]) for x in range(1, len(self.s_mins) + 1)]
|
|
177
|
+
if self.gci_step.exponent > 0 else self.s_mins
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
energies, self.s_mins, variances,
|
|
182
|
+
durations,
|
|
183
|
+
self.gci_step.label, self.gci_step.marker,
|
|
184
|
+
self.circuit_depths if isinstance(self.state_of_recursion, Circuit) else None,
|
|
185
|
+
self.circuits if isinstance(self.state_of_recursion, Circuit) else None,
|
|
186
|
+
self.states,
|
|
187
|
+
self.state_of_recursion,
|
|
188
|
+
self.fidelities
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
def max_excitation(thetas, fidelity, target_fidelity, initial_tolerance=1e-5, max_tolerance=0.2, step=1e-3):
|
|
192
|
+
"""
|
|
193
|
+
Find where a cooling curve first reaches a target fidelity.
|
|
194
|
+
|
|
195
|
+
Scans ``(thetas[i], fidelity[i])`` in order and returns the first
|
|
196
|
+
``thetas[i]`` whose fidelity is within a tolerance of `target_fidelity`.
|
|
197
|
+
The tolerance starts at `initial_tolerance` and grows by `step` until a
|
|
198
|
+
match is found or it exceeds `max_tolerance`. With a sweep ordered from
|
|
199
|
+
hot to cold, the result is the hottest initial state that is still cooled
|
|
200
|
+
to the target: everything colder ends up at least as cold.
|
|
201
|
+
|
|
202
|
+
Parameters:
|
|
203
|
+
thetas (sequence[float]): x-values of the curve, e.g. the initial
|
|
204
|
+
fidelities or angles of a sweep.
|
|
205
|
+
fidelity (sequence[float]): Final fidelity for each x-value.
|
|
206
|
+
target_fidelity (float): Fidelity to look for.
|
|
207
|
+
initial_tolerance (float): Starting tolerance.
|
|
208
|
+
max_tolerance (float): Largest tolerance tried before giving up.
|
|
209
|
+
step (float): Increment of the tolerance.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
float or None: The matching x-value, or None if no point is within
|
|
213
|
+
`max_tolerance` of the target.
|
|
214
|
+
|
|
215
|
+
Example:
|
|
216
|
+
>>> from dbac.gci import max_excitation
|
|
217
|
+
>>> max_excitation([0.1, 0.2, 0.3, 0.4], [0.5, 0.7, 0.9, 0.95], target_fidelity=0.9)
|
|
218
|
+
0.3
|
|
219
|
+
"""
|
|
220
|
+
tolerance = initial_tolerance
|
|
221
|
+
while tolerance <= max_tolerance:
|
|
222
|
+
for xi, yi in zip(thetas, fidelity):
|
|
223
|
+
if abs(yi - target_fidelity) <= tolerance:
|
|
224
|
+
return xi
|
|
225
|
+
tolerance += step
|
|
226
|
+
return None
|
dbac/hbac.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""
|
|
2
|
+
hbac.py
|
|
3
|
+
|
|
4
|
+
Heat-bath algorithmic cooling (HBAC) baseline used for comparison against
|
|
5
|
+
DBAC. HBAC repeatedly SWAPs entropy onto a fresh, highly-polarized "reset"
|
|
6
|
+
qubit and discards it, compressing the joint state towards its ground state
|
|
7
|
+
one qubit at a time.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
__all__ = ["refresh_reset_qubit", "compress_density_matrix", "hbac"]
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
from dbac.dme import ptrace
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def refresh_reset_qubit(rho, epsilon_b):
|
|
18
|
+
"""
|
|
19
|
+
Reset the last qubit of a joint density matrix to a thermal state with
|
|
20
|
+
polarization `epsilon_b`, by rescaling each 2x2 block of `rho` that acts
|
|
21
|
+
on that qubit.
|
|
22
|
+
|
|
23
|
+
Parameters:
|
|
24
|
+
rho (ndarray): 2^n x 2^n density matrix of n qubits.
|
|
25
|
+
epsilon_b (float): Polarization of the reset qubit.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
ndarray: New density matrix after resetting the last qubit.
|
|
29
|
+
"""
|
|
30
|
+
d = rho.shape[0]
|
|
31
|
+
assert rho.shape == (d, d), "Input must be a square matrix"
|
|
32
|
+
assert (d & (d - 1)) == 0, "Dimension must be a power of 2"
|
|
33
|
+
|
|
34
|
+
num_blocks = d // 2 # since each M_ij is 2x2
|
|
35
|
+
new_rho = np.zeros_like(rho, dtype=complex)
|
|
36
|
+
|
|
37
|
+
thermal_matrix = np.array([
|
|
38
|
+
[(1 + epsilon_b) / 2, 0],
|
|
39
|
+
[0, (1 - epsilon_b) / 2],
|
|
40
|
+
])
|
|
41
|
+
|
|
42
|
+
for i in range(num_blocks):
|
|
43
|
+
for j in range(num_blocks):
|
|
44
|
+
row_start, row_end = i * 2, i * 2 + 2
|
|
45
|
+
col_start, col_end = j * 2, j * 2 + 2
|
|
46
|
+
m_ij = rho[row_start:row_end, col_start:col_end]
|
|
47
|
+
new_rho[row_start:row_end, col_start:col_end] = np.trace(m_ij) * thermal_matrix
|
|
48
|
+
|
|
49
|
+
return new_rho
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def compress_density_matrix(rho):
|
|
53
|
+
"""
|
|
54
|
+
Sort-and-compress step of HBAC: reorder the populations of `rho` in
|
|
55
|
+
descending order, discarding coherences, to approximate the state
|
|
56
|
+
achievable by the optimal (sorting) unitary.
|
|
57
|
+
|
|
58
|
+
Parameters:
|
|
59
|
+
rho (ndarray): Density matrix (Hermitian, positive semidefinite, trace 1).
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
ndarray: Diagonal density matrix with sorted probabilities.
|
|
63
|
+
"""
|
|
64
|
+
assert rho.shape[0] == rho.shape[1], "Matrix must be square"
|
|
65
|
+
assert np.allclose(rho, rho.conj().T), "Matrix must be Hermitian"
|
|
66
|
+
|
|
67
|
+
probs = np.real(np.diag(rho))
|
|
68
|
+
sorted_probs = np.sort(probs)[::-1]
|
|
69
|
+
return np.diag(sorted_probs)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def hbac(rho, epsilon_b=0.8, step=1):
|
|
73
|
+
"""
|
|
74
|
+
Run `step` rounds of heat-bath algorithmic cooling (reset + compress) on
|
|
75
|
+
`rho`, then trace out every qubit except the first (the target qubit,
|
|
76
|
+
which the sorting step cools).
|
|
77
|
+
|
|
78
|
+
Parameters:
|
|
79
|
+
rho (ndarray): 2^n x 2^n density matrix, last qubit is the reset qubit.
|
|
80
|
+
epsilon_b (float): Polarization of the fresh reset qubit each round,
|
|
81
|
+
i.e. populations ``(1 +- epsilon_b) / 2``.
|
|
82
|
+
step (int): Number of reset-and-compress rounds.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
ndarray: 2x2 reduced density matrix of the first qubit.
|
|
86
|
+
|
|
87
|
+
Example:
|
|
88
|
+
>>> import numpy as np
|
|
89
|
+
>>> from dbac.hbac import hbac
|
|
90
|
+
>>> mixed = np.eye(2) / 2 # maximally mixed qubit
|
|
91
|
+
>>> rho = np.kron(np.kron(mixed, mixed), mixed)
|
|
92
|
+
>>> hbac(rho, epsilon_b=0.8, step=6).real.round(3)
|
|
93
|
+
array([[0.985, 0. ],
|
|
94
|
+
[0. , 0.015]])
|
|
95
|
+
"""
|
|
96
|
+
for _ in range(step):
|
|
97
|
+
rho = refresh_reset_qubit(rho, epsilon_b)
|
|
98
|
+
rho = compress_density_matrix(rho)
|
|
99
|
+
d = rho.shape[0]
|
|
100
|
+
return ptrace(rho, index=1, n1=2, n2=int(d / 2))
|