-
Notifications
You must be signed in to change notification settings - Fork 416
Expand file tree
/
Copy pathslater_determinants.py
More file actions
289 lines (240 loc) · 11.2 KB
/
slater_determinants.py
File metadata and controls
289 lines (240 loc) · 11.2 KB
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
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""This module contains functions for compiling circuits to prepare
Slater determinants and fermionic Gaussian states."""
import numpy
from openfermion.config import EQ_TOLERANCE
from openfermion.ops import QuadraticHamiltonian
from openfermion.linalg.givens_rotations import (
fermionic_gaussian_decomposition,
givens_decomposition,
)
from openfermion.linalg.sparse_tools import (
jw_configuration_state,
jw_sparse_givens_rotation,
jw_sparse_particle_hole_transformation_last_mode,
)
def gaussian_state_preparation_circuit(
quadratic_hamiltonian, occupied_orbitals=None, spin_sector=None
):
r"""Obtain the description of a circuit which prepares a fermionic Gaussian
state.
Fermionic Gaussian states can be regarded as eigenstates of quadratic
Hamiltonians. If the Hamiltonian conserves particle number, then these are
just Slater determinants. See arXiv:1711.05395 for a detailed description
of how this procedure works.
The circuit description is returned as a sequence of elementary
operations; operations that can be performed in parallel are grouped
together. Each elementary operation is either
- the string 'pht', indicating the particle-hole transformation
on the last fermionic mode, which is the operator $\mathcal{B}$
such that
$$
\begin{align}
\mathcal{B} a_N \mathcal{B}^\dagger &= a_N^\dagger,\\
\mathcal{B} a_j \mathcal{B}^\dagger &= a_j, \quad
j = 1, \ldots, N-1,
\end{align}
$$
or
- a tuple $(i, j, \theta, \varphi)$, indicating the operation
$$
\exp[i \varphi a_j^\dagger a_j]
\exp[\theta (a_i^\dagger a_j - a_j^\dagger a_i)],
$$
a Givens rotation of modes $i$ and $j$ by angles
$\theta$ and $\varphi$.
Args:
quadratic_hamiltonian(QuadraticHamiltonian):
The Hamiltonian whose eigenstate is desired.
occupied_orbitals(list):
A list of integers representing the indices of the occupied
orbitals in the desired Gaussian state. If this is None
(the default), then it is assumed that the ground state is
desired, i.e., the orbitals with negative energies are filled.
spin_sector (optional str): An optional integer specifying
a spin sector to restrict to: 0 for spin-up and 1 for
spin-down. If specified, the returned circuit acts on modes
indexed by spatial indices (rather than spin indices).
Should only be specified if the Hamiltonian
includes a spin degree of freedom and spin-up modes
do not interact with spin-down modes.
Returns
-------
circuit_description (list[tuple]):
A list of operations describing the circuit. Each operation
is a tuple of objects describing elementary operations that
can be performed in parallel. Each elementary operation
is either the string 'pht', indicating a particle-hole
transformation on the last fermionic mode, or a tuple of
the form $(i, j, \theta, \varphi)$,
indicating a Givens rotation
of modes $i$ and $j$ by angles $\theta$
and $\varphi$.
start_orbitals (list):
The occupied orbitals to start with. This describes the
initial state that the circuit should be applied to: it should
be a Slater determinant (in the computational basis) with these
orbitals filled.
"""
if not isinstance(quadratic_hamiltonian, QuadraticHamiltonian):
raise ValueError('Input must be an instance of QuadraticHamiltonian.')
(orbital_energies, transformation_matrix, _) = (
quadratic_hamiltonian.diagonalizing_bogoliubov_transform(spin_sector=spin_sector)
)
if quadratic_hamiltonian.conserves_particle_number:
if occupied_orbitals is None:
# The ground state is desired, so we fill the orbitals that have
# negative energy
occupied_orbitals = numpy.where(orbital_energies < 0.0)[0]
# Get the unitary rows which represent the Slater determinant
slater_determinant_matrix = transformation_matrix[occupied_orbitals]
# Get the circuit description
circuit_description = slater_determinant_preparation_circuit(slater_determinant_matrix)
start_orbitals = range(len(occupied_orbitals))
else:
# TODO implement this
if spin_sector is not None:
raise NotImplementedError("Not yet supported")
# Rearrange the transformation matrix because the circuit generation
# routine expects it to describe annihilation operators rather than
# creation operators
n_qubits = quadratic_hamiltonian.n_qubits
left_block = transformation_matrix[:, :n_qubits]
right_block = transformation_matrix[:, n_qubits:]
# Can't use numpy.block because that requires numpy>=1.13.0
new_transformation_matrix = numpy.empty((n_qubits, 2 * n_qubits), dtype=complex)
new_transformation_matrix[:, :n_qubits] = numpy.conjugate(right_block)
new_transformation_matrix[:, n_qubits:] = numpy.conjugate(left_block)
# Get the circuit description
decomposition, left_decomposition, _, _ = fermionic_gaussian_decomposition(
new_transformation_matrix
)
if occupied_orbitals is None:
# The ground state is desired, so the circuit should be applied
# to the vaccuum state
start_orbitals = []
circuit_description = list(reversed(decomposition))
else:
start_orbitals = occupied_orbitals
# The circuit won't be applied to the ground state, so we need to
# use left_decomposition
circuit_description = list(reversed(decomposition + left_decomposition))
return circuit_description, start_orbitals
def slater_determinant_preparation_circuit(slater_determinant_matrix):
r"""Obtain the description of a circuit which prepares a Slater determinant.
The input is an $N_f \times N$ matrix $Q$ with orthonormal
rows. Such a matrix describes the Slater determinant
$$
b^\dagger_1 \cdots b^\dagger_{N_f} \lvert \text{vac} \rangle,
$$
where
$$
b^\dagger_j = \sum_{k = 1}^N Q_{jk} a^\dagger_k.
$$
The output is the description of a circuit which prepares this
Slater determinant, up to a global phase.
The starting state which the circuit should be applied to
is a Slater determinant (in the computational basis) with
the first $N_f$ orbitals filled.
Args:
slater_determinant_matrix: The matrix $Q$ which describes the
Slater determinant to be prepared.
Returns:
circuit_description:
A list of operations describing the circuit. Each operation
is a tuple of elementary operations that can be performed in
parallel. Each elementary operation is a tuple of the form
$(i, j, \theta, \varphi)$, indicating a Givens rotation
of modes $i$ and $j$ by angles $\theta$
and $\varphi$.
"""
decomposition, _, _ = givens_decomposition(slater_determinant_matrix)
circuit_description = list(reversed(decomposition))
return circuit_description
def jw_get_gaussian_state(quadratic_hamiltonian, occupied_orbitals=None):
"""Compute an eigenvalue and eigenstate of a quadratic Hamiltonian.
Eigenstates of a quadratic Hamiltonian are also known as fermionic
Gaussian states.
Args:
quadratic_hamiltonian(QuadraticHamiltonian):
The Hamiltonian whose eigenstate is desired.
occupied_orbitals(list):
A list of integers representing the indices of the occupied
orbitals in the desired Gaussian state. If this is None
(the default), then it is assumed that the ground state is
desired, i.e., the orbitals with negative energies are filled.
Returns
-------
energy (float):
The eigenvalue.
state (sparse):
The eigenstate in scipy.sparse csc format.
"""
if not isinstance(quadratic_hamiltonian, QuadraticHamiltonian):
raise ValueError('Input must be an instance of QuadraticHamiltonian.')
n_qubits = quadratic_hamiltonian.n_qubits
# Compute the energy
orbital_energies, _, constant = quadratic_hamiltonian.diagonalizing_bogoliubov_transform()
if occupied_orbitals is None:
# The ground energy is desired
if quadratic_hamiltonian.conserves_particle_number:
num_negative_energies = numpy.count_nonzero(orbital_energies < -EQ_TOLERANCE)
occupied_orbitals = range(num_negative_energies)
else:
occupied_orbitals = []
energy = numpy.sum(orbital_energies[occupied_orbitals]) + constant
# Obtain the circuit that prepares the Gaussian state
circuit_description, start_orbitals = gaussian_state_preparation_circuit(
quadratic_hamiltonian, occupied_orbitals
)
# Initialize the starting state
state = jw_configuration_state(start_orbitals, n_qubits)
# Apply the circuit
if not quadratic_hamiltonian.conserves_particle_number:
particle_hole_transformation = jw_sparse_particle_hole_transformation_last_mode(n_qubits)
for parallel_ops in circuit_description:
for op in parallel_ops:
if op == 'pht':
state = particle_hole_transformation.dot(state)
else:
i, j, theta, phi = op
state = jw_sparse_givens_rotation(i, j, theta, phi, n_qubits).dot(state)
return energy, state
def jw_slater_determinant(slater_determinant_matrix):
r"""Obtain a Slater determinant.
The input is an $N_f \times N$ matrix $Q$ with orthonormal
rows. Such a matrix describes the Slater determinant
$$
b^\dagger_1 \cdots b^\dagger_{N_f} \lvert \text{vac} \rangle,
$$
where
$$
b^\dagger_j = \sum_{k = 1}^N Q_{jk} a^\dagger_k.
$$
Args:
slater_determinant_matrix: The matrix $Q$ which describes the
Slater determinant to be prepared.
Returns:
The Slater determinant as a sparse matrix.
"""
circuit_description = slater_determinant_preparation_circuit(slater_determinant_matrix)
start_orbitals = range(slater_determinant_matrix.shape[0])
n_qubits = slater_determinant_matrix.shape[1]
# Initialize the starting state
state = jw_configuration_state(start_orbitals, n_qubits)
# Apply the circuit
for parallel_ops in circuit_description:
for op in parallel_ops:
i, j, theta, phi = op
state = jw_sparse_givens_rotation(i, j, theta, phi, n_qubits).dot(state)
return state