Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Measurement map #134

Merged
merged 6 commits into from
Mar 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion tangelo/toolboxes/measurements/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from .qubit_terms_grouping import group_qwc, exp_value_from_measurement_bases
from .qubit_terms_grouping import group_qwc, exp_value_from_measurement_bases,\
check_bases_commute_qwc, map_measurements_qwc
from .estimate_measurements import get_measurement_estimate
from .classical_shadows.classical_shadows import ClassicalShadow
from .classical_shadows.randomized import RandomizedClassicalShadow
Expand Down
65 changes: 60 additions & 5 deletions tangelo/toolboxes/measurements/qubit_terms_grouping.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,33 +24,88 @@
from tangelo.linq import Simulator


def group_qwc(qb_ham, seed=None):
def group_qwc(qb_ham, seed=None, n_repeat=1):
"""Wrapper around Openfermion functionality that takes as input a
QubitOperator and yields a collection of mesurement bases defining a
QubitOperator and yields a collection of measurement bases defining a
partition of groups of sub-operators with terms that are diagonal in the
same tensor product basis. Each sub-operator can be measured using the same
qubit post-rotations in expectation estimation. This uses the idea of
qubitwise commutativity (qwc).
qubitwise commutativity (qwc), and the minimum clique cover algorithm.

The resulting dictionary maps the measurement basis (key) to the list of
qubit operators whose expectation value can be computed using the
corresponding circuit. The size of this dictionary determines how many
quantum circuits need to be executed in order to provide the expectation
value of the input qubit operator.

The minimum clique cover algorithm can be initialized with a random seed
and can be repeated several times with different seeds in order to return
the run that produces the lowest number of groups.

Args:
operator (QubitOperator): the operator that will be split into
qb_ham (QubitOperator): the operator that will be split into
sub-operators (tensor product basis sets).
seed (int): default None. Random seed used to initialize the
numpy.RandomState pseudo-RNG.
n_repeat (int): Repeat with a different random seed, keep the outcome
resulting in the lowest number of groups.

Returns:
dict: a dictionary where each key defines a tensor product basis, and
each corresponding value is a QubitOperator with terms that are all
diagonal in that basis.
"""

return group_into_tensor_product_basis_sets(qb_ham, seed)
res = group_into_tensor_product_basis_sets(qb_ham, seed)
for i in range(n_repeat-1):
res2 = group_into_tensor_product_basis_sets(qb_ham)
if len(res2) < len(res):
res = res2
return res


def check_bases_commute_qwc(b1, b2):
""" Check whether two bases commute qubitwise.

Args:
b1 (tuple of (int, str)): the first measurement basis
b2 (tuple of (int, str)): the second measurement basis

Returns:
bool: whether or not the bases commute qubitwise
"""

b1_dict, b2_dict = dict(b1), dict(b2)
for i in set(b1_dict) & set(b2_dict):
if b1_dict[i] != b2_dict[i]:
return False
return True


def map_measurements_qwc(qwc_group_map):
""" Somewhat reverses a grouping dictionary, linking all measurement bases to the list of
group representatives that commute with them qubitwise. Useful to find all the bases
whose shots can be used in order to form data for other bases during a hardware experiment.

Args:
qwc_group_map (dict): maps a measurement basis to a qubit operator whose terms commute
with said measurement basis qubit-wise.
Returns:
dict: maps each of the bases to all group representatives that commute with them qubitwise.
"""

meas_map = dict()
meas_bases, op_bases = list(), list()
for k, v in qwc_group_map.items():
meas_bases.append(k)
op_bases.extend(v.terms.keys())

for b1 in op_bases:
for b2 in meas_bases:
if b1 and check_bases_commute_qwc(b1, b2):
meas_map[b1] = meas_map.get(b1, []) + [b2]

return meas_map


def exp_value_from_measurement_bases(sub_ops, histograms):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,73 @@

import unittest
import os

from openfermion import load_operator
from openfermion.ops import QubitOperator

from tangelo.linq import translator, Simulator, Circuit
from tangelo.helpers import measurement_basis_gates
from tangelo.toolboxes.measurements import group_qwc, exp_value_from_measurement_bases
from tangelo.toolboxes.measurements import group_qwc, exp_value_from_measurement_bases, \
check_bases_commute_qwc, map_measurements_qwc

path_data = os.path.dirname(os.path.abspath(__file__)) + '/data'


class TermsGroupingTest(unittest.TestCase):

def test_qubitwise_commutativity(self):
""" Tests for check_bases_commute_qwc function. """

I0 = ()
Z1 = ((1, 'Z'),)
X2 = ((2, 'X'),)
X0Z1 = ((0, 'X'), (1, 'Z'))
Z0Z1 = ((0, 'Z'), (1, 'Z'))

self.assertTrue(check_bases_commute_qwc(I0, Z1))
self.assertTrue(check_bases_commute_qwc(X2, Z1))
self.assertTrue(check_bases_commute_qwc(Z1, X0Z1))
self.assertTrue(check_bases_commute_qwc(Z1, Z0Z1))
self.assertFalse(check_bases_commute_qwc(Z0Z1, X0Z1))

def test_mmap_qwc_H2(self):
""" From a partitioned Hamiltonian, build reverse dictionary of measurements """

qb_ham = load_operator("mol_H2_qubitham.data", data_directory=path_data, plain_text=True)
partitioned_ham = group_qwc(qb_ham)
res = map_measurements_qwc(partitioned_ham)

for k, v in res.items():
if k in {((0, 'X'), (1, 'X'), (2, 'Y'), (3, 'Y')), ((0, 'X'), (1, 'Y'), (2, 'Y'), (3, 'X')),
((0, 'Y'), (1, 'X'), (2, 'X'), (3, 'Y')), ((0, 'Y'), (1, 'Y'), (2, 'X'), (3, 'X'))}:
self.assertTrue(v[0] == k)
else:
self.assertTrue(v[0] == ((0, 'Z'), (1, 'Z'), (2, 'Z'), (3, 'Z')))

def test_mmap_qwc_H10frag(self):
""" From a partitioned Hamiltonian, build reverse dictionary of measurements """

partitioned_ham = {
((0, 'X'), (1, 'X')): 0.25 * QubitOperator('X0 X1'),
((0, 'X'), (1, 'Z')): 0.25 * (QubitOperator('X0') + QubitOperator('X0 Z1') + QubitOperator('Z1')),
((0, 'Y'), (1, 'Y')): -0.25 * QubitOperator('Y0 Y1'),
((0, 'Z'), (1, 'X')): 0.25 * (QubitOperator('Z0') + QubitOperator('Z0 X1') + QubitOperator('X1')),
((0, 'Z'), (1, 'Z')): 0.25 * QubitOperator('Z0 Z1')
}
res = map_measurements_qwc(partitioned_ham)

ref_H10frag = {((0, 'X'), (1, 'X')): [((0, 'X'), (1, 'X'))],
((0, 'X'),): [((0, 'X'), (1, 'X')), ((0, 'X'), (1, 'Z'))],
((0, 'X'), (1, 'Z')): [((0, 'X'), (1, 'Z'))],
((1, 'Z'),): [((0, 'X'), (1, 'Z')), ((0, 'Z'), (1, 'Z'))],
((0, 'Y'), (1, 'Y')): [((0, 'Y'), (1, 'Y'))],
((0, 'Z'),): [((0, 'Z'), (1, 'X')), ((0, 'Z'), (1, 'Z'))],
((0, 'Z'), (1, 'X')): [((0, 'Z'), (1, 'X'))],
((1, 'X'),): [((0, 'X'), (1, 'X')), ((0, 'Z'), (1, 'X'))],
((0, 'Z'), (1, 'Z')): [((0, 'Z'), (1, 'Z'))]}

self.assertDictEqual(res, ref_H10frag)

def test_qubitwise_commutativity_of_H2(self):
""" The JW Pauli hamiltonian of H2 at optimal geometry is a 15-term operator. Using qubitwise-commutativity,
it is possible to get the expectation value of all 15 terms by only performing measurements in 5 distinct
Expand Down