Skip to content

Commit d1ed02f

Browse files
idk3babbush
authored andcommitted
Added verbose and correct simulation ordering for jellium (#122)
* Added verbose and correct simulation ordering for jellium * Python3 string formatting * Added test for verbose=True in jellium Trotter error bound
1 parent e250399 commit d1ed02f

File tree

2 files changed

+126
-87
lines changed

2 files changed

+126
-87
lines changed

src/fermilib/utils/_dual_basis_trotter_error.py

Lines changed: 86 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
"""Module to compute Trotter errors in the plane-wave dual basis."""
1414
from __future__ import absolute_import
1515
from future.utils import iteritems, itervalues
16-
from math import sqrt
1716

1817
import numpy
1918

@@ -261,7 +260,7 @@ def trivially_double_commutes_dual_basis(term_a, term_b, term_c):
261260

262261

263262
def dual_basis_error_operator(terms, indices=None, is_hopping_operator=None,
264-
jellium_only=False):
263+
jellium_only=False, verbose=False):
265264
"""Determine the difference between the exact generator of unitary
266265
evolution and the approximate generator given by the second-order
267266
Trotter-Suzuki expansion.
@@ -275,6 +274,7 @@ def dual_basis_error_operator(terms, indices=None, is_hopping_operator=None,
275274
rather than the full dual basis Hamiltonian (i.e. whether
276275
c_i = c for all number operators i^ i, or whether they
277276
depend on i as is possible in the general case).
277+
verbose: Whether to print percentage progress.
278278
279279
Returns:
280280
The difference between the true and effective generators of time
@@ -284,9 +284,18 @@ def dual_basis_error_operator(terms, indices=None, is_hopping_operator=None,
284284
Size Required for Accurate Quantum Simulation of Quantum Chemistry".
285285
"""
286286
more_info = bool(indices)
287+
n_terms = len(terms)
288+
289+
if verbose:
290+
import time
291+
start = time.time()
287292

288293
error_operator = FermionOperator.zero()
289-
for beta in range(len(terms)):
294+
for beta in range(n_terms):
295+
if verbose and beta % (n_terms // 30) == 0:
296+
print('%4.3f percent done in' % (
297+
(float(beta) / n_terms) ** 3 * 100), time.time() - start)
298+
290299
for alpha in range(beta + 1):
291300
for alpha_prime in range(beta):
292301
# If we have pre-computed info on indices, use it to determine
@@ -326,7 +335,7 @@ def dual_basis_error_operator(terms, indices=None, is_hopping_operator=None,
326335

327336

328337
def dual_basis_error_bound(terms, indices=None, is_hopping_operator=None,
329-
jellium_only=False):
338+
jellium_only=False, verbose=False):
330339
"""Numerically upper bound the error in the ground state energy
331340
for the second-order Trotter-Suzuki expansion.
332341
@@ -339,6 +348,7 @@ def dual_basis_error_bound(terms, indices=None, is_hopping_operator=None,
339348
rather than the full dual basis Hamiltonian (i.e. whether
340349
c_i = c for all number operators i^ i, or whether they
341350
depend on i as is possible in the general case).
351+
verbose: Whether to print percentage progress.
342352
343353
Returns:
344354
A float upper bound on norm of error in the ground state energy.
@@ -350,19 +360,22 @@ def dual_basis_error_bound(terms, indices=None, is_hopping_operator=None,
350360
"""
351361
# Return the 1-norm of the error operator (upper bound on error).
352362
return numpy.sum(numpy.absolute(list(dual_basis_error_operator(
353-
terms, indices, is_hopping_operator, jellium_only).terms.values())))
363+
terms, indices, is_hopping_operator,
364+
jellium_only, verbose).terms.values())))
354365

355366

356-
def ordered_dual_basis_terms_grouped_by_type_with_info(dual_basis_hamiltonian):
367+
def simulation_ordered_grouped_dual_basis_terms_with_info(
368+
dual_basis_hamiltonian, input_ordering=None):
357369
"""Give terms from the dual basis Hamiltonian in simulated order.
358370
359-
Roughly uses the simulation ordering, grouping terms into hopping
371+
Uses the simulation ordering, grouping terms into hopping
360372
(i^ j + j^ i) and number (i^j^ i j + c_i i^ i + c_j j^ j) operators.
361373
Pre-computes term information (indices each operator acts on, as
362374
well as whether each operator is a hopping operator.
363375
364376
Args:
365377
dual_basis_hamiltonian (FermionOperator): The Hamiltonian.
378+
input_ordering (list): The initial Jordan-Wigner canonical order.
366379
367380
Returns:
368381
A 3-tuple of terms from the plane-wave dual basis Hamiltonian in
@@ -371,99 +384,91 @@ def ordered_dual_basis_terms_grouped_by_type_with_info(dual_basis_hamiltonian):
371384
"""
372385
zero = FermionOperator.zero()
373386
hamiltonian = dual_basis_hamiltonian
374-
375387
n_qubits = count_qubits(hamiltonian)
376388

377389
ordered_terms = []
378390
ordered_indices = []
379391
ordered_is_hopping_operator = []
380392

381-
# Number of times the two-number term i^ j^ i j appears for given i
382-
# (stored in the ith position of the list).
383-
two_number_operator_appearances = [0] * n_qubits
384-
385-
for i in range(n_qubits):
386-
for j in range(n_qubits):
387-
two_number_action = ((i, 1), (j, 1), (i, 0), (j, 0))
388-
if hamiltonian.terms.get(two_number_action):
389-
# Increment the number of times we've found two-number
390-
# operators containing i and j.
391-
two_number_operator_appearances[i] += 1
392-
two_number_operator_appearances[j] += 1
393-
394-
# Iterate over the different possible first qubit indices.
395-
for i in range(n_qubits):
396-
# Iterate over possible offsets. Each stage of the algorithm
397-
# simulates a different offset.
398-
for j in range(1, n_qubits):
399-
# The index_left is the low index, index_right is high.
400-
index_left = min(i, (i + j) % n_qubits)
401-
index_right = max(i, (i + j) % n_qubits)
402-
403-
# Operators of the hopping term l^ r + r^ l.
404-
hopping_action1 = ((index_left, 1), (index_right, 0))
405-
hopping_action2 = ((index_right, 1), (index_left, 0))
406-
407-
# Operators of the two-number term r^ l^ r l (after normal-
408-
# ordering).
409-
two_number_action = ((index_right, 1), (index_left, 1),
410-
(index_right, 0), (index_left, 0))
411-
412-
# Single-number terms l^ l and r^ r.
413-
left_number_action = ((index_left, 1), (index_left, 0))
414-
right_number_action = ((index_right, 1), (index_right, 0))
415-
416-
# Calculate the hopping operator in the Hamiltonian.
417-
hopping_operator = (
418-
FermionOperator(hopping_action1,
419-
hamiltonian.terms.get(hopping_action1, 0.0)) +
420-
FermionOperator(hopping_action2,
421-
hamiltonian.terms.get(hopping_action2, 0.0)))
422-
hopping_operator.compress()
423-
# Divide by two to avoid double-counting the operator.
424-
hopping_operator /= 2.0
425-
426-
# Calculate the two-number operator in the Hamiltonian.
427-
number_operator = FermionOperator(two_number_action,
428-
hamiltonian.terms.get(
429-
two_number_action, 0.0))
430-
# If it's zero, we have nothing to simulate at this stage;
431-
# otherwise, group l^ l and r^ r with it.
432-
if not number_operator.isclose(zero):
433-
number_operator += (
434-
FermionOperator(
435-
left_number_action,
436-
hamiltonian.terms.get(left_number_action, 0.0) /
437-
two_number_operator_appearances[index_left]) +
438-
FermionOperator(
439-
right_number_action,
440-
hamiltonian.terms.get(right_number_action, 0.0) /
441-
two_number_operator_appearances[index_right]))
442-
number_operator.compress()
443-
# Divide by two to avoid double-counting.
444-
number_operator /= 2.0
393+
# If no input mode ordering is specified, default to range(n_qubits).
394+
if not input_ordering:
395+
input_ordering = list(range(n_qubits))
396+
397+
# Half a second-order Trotter step reverses the input ordering: this tells
398+
# us how much we need to include in the ordered list of terms.
399+
final_ordering = list(reversed(input_ordering))
400+
401+
# Follow odd-even transposition sort. In alternating steps, swap each even
402+
# qubits with the odd qubit to its right, and in the next step swap each
403+
# the odd qubits with the even qubit to its right. Do this until the input
404+
# ordering has been reversed.
405+
odd = 0
406+
while input_ordering != final_ordering:
407+
for i in range(odd, n_qubits - 1, 2):
408+
# Always keep the max on the left to avoid having to normal order.
409+
left = max(input_ordering[i], input_ordering[i + 1])
410+
right = min(input_ordering[i], input_ordering[i + 1])
411+
412+
# Calculate the hopping operators in the Hamiltonian.
413+
left_hopping_operator = FermionOperator(
414+
((left, 1), (right, 0)), hamiltonian.terms.get(
415+
((left, 1), (right, 0)), 0.0))
416+
right_hopping_operator = FermionOperator(
417+
((right, 1), (left, 0)), hamiltonian.terms.get(
418+
((right, 1), (left, 0)), 0.0))
419+
420+
# Calculate the two-number operator l^ r^ l r in the Hamiltonian.
421+
two_number_operator = FermionOperator(
422+
((left, 1), (right, 1), (left, 0), (right, 0)),
423+
hamiltonian.terms.get(
424+
((left, 1), (right, 1), (left, 0), (right, 0)), 0.0))
425+
426+
# Calculate the left number operator, left^ left.
427+
left_number_operator = FermionOperator(
428+
((left, 1), (left, 0)), hamiltonian.terms.get(
429+
((left, 1), (left, 0)), 0.0))
430+
431+
# Calculate the right number operator, right^ right.
432+
right_number_operator = FermionOperator(
433+
((right, 1), (right, 0)), hamiltonian.terms.get(
434+
((right, 1), (right, 0)), 0.0))
435+
436+
# Divide single-number terms by n_qubits-1 to avoid over-counting.
437+
# Each qubit is swapped n_qubits-1 times total.
438+
left_number_operator /= (n_qubits - 1)
439+
right_number_operator /= (n_qubits - 1)
445440

446441
# If the overall hopping operator isn't close to zero, append it.
447442
# Include the indices it acts on and that it's a hopping operator.
448-
if not hopping_operator.isclose(zero):
449-
ordered_terms.append(hopping_operator)
450-
ordered_indices.append(set((index_left, index_right)))
443+
if not (left_hopping_operator +
444+
right_hopping_operator).isclose(zero):
445+
ordered_terms.append(left_hopping_operator +
446+
right_hopping_operator)
447+
ordered_indices.append(set((left, right)))
451448
ordered_is_hopping_operator.append(True)
452449

453450
# If the overall number operator isn't close to zero, append it.
454451
# Include the indices it acts on and that it's a number operator.
455-
if not number_operator.isclose(zero):
456-
ordered_terms.append(number_operator)
457-
ordered_indices.append(set((index_left, index_right)))
452+
if not (two_number_operator + left_number_operator +
453+
right_number_operator).isclose(zero):
454+
ordered_terms.append(two_number_operator +
455+
left_number_operator +
456+
right_number_operator)
457+
ordered_indices.append(set((left, right)))
458458
ordered_is_hopping_operator.append(False)
459459

460+
# Track the current Jordan-Wigner canonical ordering.
461+
input_ordering[i], input_ordering[i + 1] = (input_ordering[i + 1],
462+
input_ordering[i])
463+
464+
# Alternate even and odd steps of the reversal procedure.
465+
odd = 1 - odd
466+
460467
return (ordered_terms, ordered_indices, ordered_is_hopping_operator)
461468

462469

463470
def ordered_dual_basis_terms_no_info(dual_basis_hamiltonian):
464-
"""Give terms from the dual basis Hamiltonian in simulated order.
465-
466-
Orders the terms by dictionary output.
471+
"""Give terms from the dual basis Hamiltonian in dictionary output order.
467472
468473
Args:
469474
dual_basis_hamiltonian (FermionOperator): The Hamiltonian.

src/fermilib/utils/_dual_basis_trotter_error_test.py

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -283,18 +283,31 @@ def test_error_bound(self):
283283
self.assertAlmostEqual(dual_basis_error_bound(
284284
self.terms, jellium_only=True), 6.92941899358)
285285

286-
def test_error_bound_using_info(self):
286+
def test_error_bound_using_info_1d(self):
287287
# Generate the Hamiltonian.
288288
hamiltonian = dual_basis_jellium_hamiltonian(grid_length=4,
289289
dimension=1)
290290

291291
# Unpack result into terms, indices they act on, and whether they're
292292
# hopping operators.
293-
result = ordered_dual_basis_terms_grouped_by_type_with_info(
293+
result = simulation_ordered_grouped_dual_basis_terms_with_info(
294294
hamiltonian)
295295
terms, indices, is_hopping = result
296296
self.assertAlmostEqual(dual_basis_error_bound(
297-
terms, indices, is_hopping), 4.05696310313)
297+
terms, indices, is_hopping), 7.4239378440953283)
298+
299+
def test_error_bound_using_info_2d_verbose(self):
300+
# Generate the Hamiltonian.
301+
hamiltonian = dual_basis_jellium_hamiltonian(grid_length=3,
302+
dimension=2)
303+
304+
# Unpack result into terms, indices they act on, and whether they're
305+
# hopping operators.
306+
result = simulation_ordered_grouped_dual_basis_terms_with_info(
307+
hamiltonian)
308+
terms, indices, is_hopping = result
309+
self.assertAlmostEqual(0.052213321121580794, dual_basis_error_bound(
310+
terms, indices, is_hopping, jellium_only=True, verbose=True))
298311

299312

300313
class OrderedDualBasisTermsMoreInfoTest(unittest.TestCase):
@@ -312,7 +325,7 @@ def test_sum_of_ordered_terms_equals_full_hamiltonian(self):
312325
hamiltonian = dual_basis_jellium_hamiltonian(
313326
grid_length, dimension, wigner_seitz_radius, n_particles)
314327

315-
terms = ordered_dual_basis_terms_grouped_by_type_with_info(
328+
terms = simulation_ordered_grouped_dual_basis_terms_with_info(
316329
hamiltonian)[0]
317330
terms_total = sum(terms, FermionOperator.zero())
318331

@@ -340,7 +353,7 @@ def test_correct_indices_terms_with_info(self):
340353

341354
# Unpack result into terms, indices they act on, and whether they're
342355
# hopping operators.
343-
result = ordered_dual_basis_terms_grouped_by_type_with_info(
356+
result = simulation_ordered_grouped_dual_basis_terms_with_info(
344357
hamiltonian)
345358
terms, indices, is_hopping = result
346359

@@ -367,7 +380,7 @@ def test_is_hopping_operator_terms_with_info(self):
367380

368381
# Unpack result into terms, indices they act on, and whether they're
369382
# hopping operators.
370-
result = ordered_dual_basis_terms_grouped_by_type_with_info(
383+
result = simulation_ordered_grouped_dual_basis_terms_with_info(
371384
hamiltonian)
372385
terms, indices, is_hopping = result
373386

@@ -377,6 +390,27 @@ def test_is_hopping_operator_terms_with_info(self):
377390
single_term[0][0] == single_term[1][0])
378391
self.assertEqual(is_hopping_term, is_hopping[i])
379392

393+
def test_total_length(self):
394+
grid_length = 8
395+
dimension = 1
396+
wigner_seitz_radius = 10.0
397+
inverse_filling_fraction = 2
398+
n_qubits = grid_length ** dimension
399+
400+
# Compute appropriate length scale.
401+
n_particles = n_qubits // inverse_filling_fraction
402+
403+
hamiltonian = dual_basis_jellium_hamiltonian(
404+
grid_length, dimension, wigner_seitz_radius, n_particles)
405+
406+
# Unpack result into terms, indices they act on, and whether they're
407+
# hopping operators.
408+
result = simulation_ordered_grouped_dual_basis_terms_with_info(
409+
hamiltonian)
410+
terms, indices, is_hopping = result
411+
412+
self.assertEqual(len(terms), n_qubits * (n_qubits - 1))
413+
380414

381415
class OrderedDualBasisTermsNoInfoTest(unittest.TestCase):
382416
def test_all_terms_in_dual_basis_jellium_hamiltonian(self):

0 commit comments

Comments
 (0)