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