From bdbe5cb555bb0c5db5ca2df2d7d4277cd3e11838 Mon Sep 17 00:00:00 2001 From: Wong Zi Cheng <70616433+chmwzc@users.noreply.github.com> Date: Thu, 7 Mar 2024 07:31:59 +0000 Subject: [PATCH 01/31] Add function to check if two Pauli strings commute --- src/qibochem/measurement/optimization.py | 32 ++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/qibochem/measurement/optimization.py b/src/qibochem/measurement/optimization.py index 13ac4ae..7a4ab5c 100644 --- a/src/qibochem/measurement/optimization.py +++ b/src/qibochem/measurement/optimization.py @@ -2,6 +2,38 @@ from qibo import gates +def check_terms_commutativity(term1, term2, qubitwise=False): + """ + Check if terms 1 and 2 are mutually commuting. The 'qubitwise' flag determines if the check is for general + commutativity (False), or the stricter qubitwise commutativity. + + Args: + term1/term2: Lists of strings representing a single Pauli term. E.g. ["X0", "Z1", "Y3"]. Obtained from a Qibo + SymbolicTerm as ``[factor.name for factor in term.factors]``. + qubitwise: Determines if the check is for general commutativity, or the stricter qubitwise commutativity + + Returns: + bool: Do terms 1 and 2 commute? + """ + # Get a list of common qubits for each term + common_qubits = sorted( + {_term[1] for _term in term1 if _term[0] != "I"} & {_term[1] for _term in term2 if _term[0] != "I"} + ) + if not common_qubits: + return True + # Get the single Pauli operators for the common qubits for both Pauli terms + term1_ops = [_op for _op in term1 if _op[1] in common_qubits] + term2_ops = [_op for _op in term2 if _op[1] in common_qubits] + if qubitwise: + # Qubitwise: Compare the Pauli terms at the common qubits. Any difference => False + return all(_op1 == _op2 for _op1, _op2 in zip(term1_ops, term2_ops)) + # General commutativity: + # Get the number of single Pauli operators that do NOT commute + n_noncommuting_ops = sum(_op1 != _op2 for _op1, _op2 in zip(term1_ops, term2_ops)) + # term1 and term2 have general commutativity iff n_noncommuting_ops is even + return n_noncommuting_ops % 2 == 0 + + def measurement_basis_rotations(hamiltonian, n_qubits, grouping=None): """ Split up and sort the Hamiltonian terms to get the basis rotation gates to be applied to a quantum circuit for the From 78435b7a67a4c2a4fad84aee419efb74c299ab8f Mon Sep 17 00:00:00 2001 From: Wong Zi Cheng <70616433+chmwzc@users.noreply.github.com> Date: Tue, 9 Apr 2024 04:41:27 +0000 Subject: [PATCH 02/31] Rename expectation.py to result.py --- src/qibochem/measurement/__init__.py | 2 +- src/qibochem/measurement/optimization.py | 8 ++++---- src/qibochem/measurement/{expectation.py => result.py} | 0 tests/test_basis_rotation.py | 6 ++---- tests/test_hardware_efficient.py | 2 +- tests/test_hf_circuit.py | 6 +++--- tests/test_molecule.py | 2 +- tests/test_ucc.py | 2 +- 8 files changed, 13 insertions(+), 15 deletions(-) rename src/qibochem/measurement/{expectation.py => result.py} (100%) diff --git a/src/qibochem/measurement/__init__.py b/src/qibochem/measurement/__init__.py index 661f18d..e72e669 100644 --- a/src/qibochem/measurement/__init__.py +++ b/src/qibochem/measurement/__init__.py @@ -1 +1 @@ -from qibochem.measurement.expectation import expectation +from qibochem.measurement.result import expectation diff --git a/src/qibochem/measurement/optimization.py b/src/qibochem/measurement/optimization.py index 7a4ab5c..ac8b432 100644 --- a/src/qibochem/measurement/optimization.py +++ b/src/qibochem/measurement/optimization.py @@ -17,13 +17,13 @@ def check_terms_commutativity(term1, term2, qubitwise=False): """ # Get a list of common qubits for each term common_qubits = sorted( - {_term[1] for _term in term1 if _term[0] != "I"} & {_term[1] for _term in term2 if _term[0] != "I"} + {_term[1:] for _term in term1 if _term[0] != "I"} & {_term[1:] for _term in term2 if _term[0] != "I"} ) if not common_qubits: return True # Get the single Pauli operators for the common qubits for both Pauli terms - term1_ops = [_op for _op in term1 if _op[1] in common_qubits] - term2_ops = [_op for _op in term2 if _op[1] in common_qubits] + term1_ops = [_op for _op in term1 if _op[1:] in common_qubits] + term2_ops = [_op for _op in term2 if _op[1:] in common_qubits] if qubitwise: # Qubitwise: Compare the Pauli terms at the common qubits. Any difference => False return all(_op1 == _op2 for _op1, _op2 in zip(term1_ops, term2_ops)) @@ -61,7 +61,7 @@ def measurement_basis_rotations(hamiltonian, n_qubits, grouping=None): if z_only_terms: result.append(([gates.M(_i) for _i in range(n_qubits)], z_only_terms)) else: - result.append(([], [])) + result.append(([], [])) # No terms with only Z's # Then add the X/Y terms in if xy_terms: if grouping is None: diff --git a/src/qibochem/measurement/expectation.py b/src/qibochem/measurement/result.py similarity index 100% rename from src/qibochem/measurement/expectation.py rename to src/qibochem/measurement/result.py diff --git a/tests/test_basis_rotation.py b/tests/test_basis_rotation.py index 8f4dc8a..49b1a3f 100644 --- a/tests/test_basis_rotation.py +++ b/tests/test_basis_rotation.py @@ -7,10 +7,8 @@ from qibo import Circuit, gates, models from qibochem.ansatz import basis_rotation -from qibochem.driver.molecule import Molecule -from qibochem.measurement.expectation import expectation - -# from qibo.optimizers import optimize +from qibochem.driver import Molecule +from qibochem.measurement import expectation def test_unitary(): diff --git a/tests/test_hardware_efficient.py b/tests/test_hardware_efficient.py index e9ad4c7..56420e6 100644 --- a/tests/test_hardware_efficient.py +++ b/tests/test_hardware_efficient.py @@ -5,7 +5,7 @@ from qibochem.ansatz import he_circuit from qibochem.driver import Molecule -from qibochem.measurement.expectation import expectation +from qibochem.measurement import expectation def test_he_circuit(): diff --git a/tests/test_hf_circuit.py b/tests/test_hf_circuit.py index bd5bbe3..d6cdab0 100644 --- a/tests/test_hf_circuit.py +++ b/tests/test_hf_circuit.py @@ -4,9 +4,9 @@ import pytest -from qibochem.ansatz.hf_reference import hf_circuit -from qibochem.driver.molecule import Molecule -from qibochem.measurement.expectation import expectation +from qibochem.ansatz import hf_circuit +from qibochem.driver import Molecule +from qibochem.measurement import expectation @pytest.mark.parametrize( diff --git a/tests/test_molecule.py b/tests/test_molecule.py index 0b63ace..2fe1aed 100644 --- a/tests/test_molecule.py +++ b/tests/test_molecule.py @@ -12,7 +12,7 @@ from qibo.symbols import Z from qibochem.driver import Molecule -from qibochem.measurement.expectation import expectation +from qibochem.measurement import expectation @pytest.mark.parametrize( diff --git a/tests/test_ucc.py b/tests/test_ucc.py index 56d70e1..d6d6cc7 100644 --- a/tests/test_ucc.py +++ b/tests/test_ucc.py @@ -9,7 +9,7 @@ from qibo import Circuit, gates, symbols from qibo.hamiltonians import SymbolicHamiltonian -from qibochem.ansatz.hf_reference import hf_circuit +from qibochem.ansatz import hf_circuit from qibochem.ansatz.ucc import ( expi_pauli, generate_excitations, From a22239464c0a285c02c11084374320a1264946bd Mon Sep 17 00:00:00 2001 From: Wong Zi Cheng <70616433+chmwzc@users.noreply.github.com> Date: Tue, 9 Apr 2024 08:01:53 +0000 Subject: [PATCH 03/31] Add test coverage --- tests/test_measurement_optimisation.py | 30 ++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 tests/test_measurement_optimisation.py diff --git a/tests/test_measurement_optimisation.py b/tests/test_measurement_optimisation.py new file mode 100644 index 0000000..e3c217f --- /dev/null +++ b/tests/test_measurement_optimisation.py @@ -0,0 +1,30 @@ +""" +Test functionality to reduce the measurement cost of running VQE +""" + +import pytest + +# from qibochem.driver import Molecule +# from qibochem.measurement import expectation +from qibochem.measurement.optimization import check_terms_commutativity + +# from qibo import Circuit, gates +# from qibo.hamiltonians import SymbolicHamiltonian +# from qibo.symbols import X, Z + + +@pytest.mark.parametrize( + "term1,term2,qwc_expected,gc_expected", + [ + (["X0"], ["Z0"], False, False), + (["X0"], ["Z1"], True, True), + (["X0", "X1"], ["Y0", "Y1"], False, True), + (["X0", "Y1"], ["Y0", "Y1"], False, False), + ], +) +def test_check_terms_commutativity(term1, term2, qwc_expected, gc_expected): + """Do two Pauli strings commute (qubit-wise or generally)?""" + qwc_result = check_terms_commutativity(term1, term2, qubitwise=True) + assert qwc_result == qwc_expected + gc_result = check_terms_commutativity(term1, term2) + assert gc_result == gc_expected From 08686f2bd412e760891329df61fa38343936bb02 Mon Sep 17 00:00:00 2001 From: Wong Zi Cheng <70616433+chmwzc@users.noreply.github.com> Date: Thu, 11 Apr 2024 02:59:00 +0000 Subject: [PATCH 04/31] Change input of commutativity test function From list of one-qubit Pauli operators, to `" ".join(list)` --- src/qibochem/measurement/optimization.py | 69 +++++++++++++++++++----- tests/test_measurement_optimisation.py | 31 ++++++++--- 2 files changed, 81 insertions(+), 19 deletions(-) diff --git a/src/qibochem/measurement/optimization.py b/src/qibochem/measurement/optimization.py index ac8b432..8c46878 100644 --- a/src/qibochem/measurement/optimization.py +++ b/src/qibochem/measurement/optimization.py @@ -1,29 +1,34 @@ +""" +Functions for optimising the measurement cost of obtaining the expectation value +""" + +import networkx as nx import numpy as np from qibo import gates -def check_terms_commutativity(term1, term2, qubitwise=False): +def check_terms_commutativity(term1, term2, qubitwise): """ Check if terms 1 and 2 are mutually commuting. The 'qubitwise' flag determines if the check is for general commutativity (False), or the stricter qubitwise commutativity. Args: - term1/term2: Lists of strings representing a single Pauli term. E.g. ["X0", "Z1", "Y3"]. Obtained from a Qibo - SymbolicTerm as ``[factor.name for factor in term.factors]``. - qubitwise: Determines if the check is for general commutativity, or the stricter qubitwise commutativity + term1/term2: Strings representing a single Pauli term. E.g. "X0 Z1 Y3". Obtained from a Qibo SymbolicTerm as + ``" ".join(factor.name for factor in term.factors)``. + qubitwise (bool): Determines if the check is for general commutativity, or the stricter qubitwise commutativity Returns: bool: Do terms 1 and 2 commute? """ # Get a list of common qubits for each term - common_qubits = sorted( - {_term[1:] for _term in term1 if _term[0] != "I"} & {_term[1:] for _term in term2 if _term[0] != "I"} - ) + common_qubits = {_term[1:] for _term in term1.split() if _term[0] != "I"} & { + _term[1:] for _term in term2.split() if _term[0] != "I" + } if not common_qubits: return True # Get the single Pauli operators for the common qubits for both Pauli terms - term1_ops = [_op for _op in term1 if _op[1:] in common_qubits] - term2_ops = [_op for _op in term2 if _op[1:] in common_qubits] + term1_ops = [_op for _op in term1.split() if _op[1:] in common_qubits] + term2_ops = [_op for _op in term2.split() if _op[1:] in common_qubits] if qubitwise: # Qubitwise: Compare the Pauli terms at the common qubits. Any difference => False return all(_op1 == _op2 for _op1, _op2 in zip(term1_ops, term2_ops)) @@ -34,6 +39,45 @@ def check_terms_commutativity(term1, term2, qubitwise=False): return n_noncommuting_ops % 2 == 0 +def group_commuting_terms(terms_list, qubitwise): + """ + Groups the terms in terms_list into as few groups as possible, where all the terms in each group commute + mutually == Finding the minimum clique cover (i.e. as few cliques as possible) for the graph whereby each node + is a Pauli string, and an edge exists between two nodes iff they commute. + + This is equivalent to the graph colouring problem of the complement graph (i.e. edge between nodes if they DO NOT + commute), which this function follows. + + Args: + terms_list: List of strings that contain X/Y. The strings should follow the output from + ``[factor.name for factor in term.factors]``, where term is a Qibo SymbolicTerm. E.g. ["X0", "Z1"] + qubitwise: Determines if the check is for general commutativity, or the stricter qubitwise commutativity + + Returns: + list: Containing groups (lists) of Pauli strings that all commute mutually + """ + # Need to convert the lists in _terms_list to strings so that they can be used as graph nodes + terms_str = ["".join(term) for term in terms_list] + # print(terms_str) + + G = nx.Graph() + # Complement graph: Add all the terms as nodes first, then add edges between nodes if they DO NOT commute + G.add_nodes_from(terms_str) + G.add_edges_from( + (term1, term2) + for _i1, term1 in enumerate(terms_str) + for _i2, term2 in enumerate(terms_str) + if _i2 > _i1 and not check_terms_commutativity(term1, term2, qubitwise) + ) + + # Solve using Greedy Colouring on NetworkX + term_groups = nx.coloring.greedy_color(G) + group_ids = set(term_groups.values()) + sorted_groups = [[group.split() for group, group_id in term_groups.items() if group_id == _id] for _id in group_ids] + print(sorted_groups) + return sorted_groups + + def measurement_basis_rotations(hamiltonian, n_qubits, grouping=None): """ Split up and sort the Hamiltonian terms to get the basis rotation gates to be applied to a quantum circuit for the @@ -52,17 +96,16 @@ def measurement_basis_rotations(hamiltonian, n_qubits, grouping=None): empty lists - ``([], [])`` - if there are no Z terms present. """ result = [] - # Split up the Z and X/Y terms first + # Split up the Z and X/Y terms z_only_terms = [ term for term in hamiltonian.terms if not any(factor.name[0] in ("X", "Y") for factor in term.factors) ] xy_terms = [term for term in hamiltonian.terms if term not in z_only_terms] - # Add the Z terms into result first + # Add the Z terms into result first, followed by the terms with X/Y's if z_only_terms: result.append(([gates.M(_i) for _i in range(n_qubits)], z_only_terms)) else: - result.append(([], [])) # No terms with only Z's - # Then add the X/Y terms in + result.append(([], [])) if xy_terms: if grouping is None: result += [ diff --git a/tests/test_measurement_optimisation.py b/tests/test_measurement_optimisation.py index e3c217f..e96b650 100644 --- a/tests/test_measurement_optimisation.py +++ b/tests/test_measurement_optimisation.py @@ -6,7 +6,10 @@ # from qibochem.driver import Molecule # from qibochem.measurement import expectation -from qibochem.measurement.optimization import check_terms_commutativity +from qibochem.measurement.optimization import ( + check_terms_commutativity, + group_commuting_terms, +) # from qibo import Circuit, gates # from qibo.hamiltonians import SymbolicHamiltonian @@ -16,15 +19,31 @@ @pytest.mark.parametrize( "term1,term2,qwc_expected,gc_expected", [ - (["X0"], ["Z0"], False, False), - (["X0"], ["Z1"], True, True), - (["X0", "X1"], ["Y0", "Y1"], False, True), - (["X0", "Y1"], ["Y0", "Y1"], False, False), + ("X0", "Z0", False, False), + ("X0", "Z1", True, True), + ("X0 X1", "Y0 Y1", False, True), + ("X0 Y1", "Y0 Y1", False, False), ], ) def test_check_terms_commutativity(term1, term2, qwc_expected, gc_expected): """Do two Pauli strings commute (qubit-wise or generally)?""" qwc_result = check_terms_commutativity(term1, term2, qubitwise=True) assert qwc_result == qwc_expected - gc_result = check_terms_commutativity(term1, term2) + gc_result = check_terms_commutativity(term1, term2, qubitwise=False) assert gc_result == gc_expected + + +# @pytest.mark.parametrize( +# "term_list,qwc_expected,gc_expected", +# [ +# ([["X0"], ["Z0"], ["Z1"], ["Z0", "Z1"]], {[["Z0"], ["Z1"], ["Z0", "Z1"]], ["X0"]}, {[["Z0"], ["Z1"], ["Z0", "Z1"]], ["X0"]}), +# # (["X0"], ["Z1"], True, True), +# # (["X0", "X1"], ["Y0", "Y1"], False, True), +# # (["X0", "Y1"], ["Y0", "Y1"], False, False), +# ], +# ) +# def test_group_commuting_terms(term_list, qwc_expected, gc_expected): +# qwc_result = group_commuting_terms(term_list, qubitwise=True) +# assert set(qwc_result) == qwc_expected +# gc_result = group_commuting_terms(term_list, qubitwise=False) +# assert set(gc_result) == gc_expected From 1b9f05a00a449005c87d4476b74c124eb23a3740 Mon Sep 17 00:00:00 2001 From: Wong Zi Cheng <70616433+chmwzc@users.noreply.github.com> Date: Thu, 11 Apr 2024 03:34:57 +0000 Subject: [PATCH 05/31] De-bug function and tests for grouping H terms --- src/qibochem/measurement/optimization.py | 21 ++++++++----------- tests/test_measurement_optimisation.py | 26 +++++++++++------------- 2 files changed, 21 insertions(+), 26 deletions(-) diff --git a/src/qibochem/measurement/optimization.py b/src/qibochem/measurement/optimization.py index 8c46878..abd4bb6 100644 --- a/src/qibochem/measurement/optimization.py +++ b/src/qibochem/measurement/optimization.py @@ -49,32 +49,29 @@ def group_commuting_terms(terms_list, qubitwise): commute), which this function follows. Args: - terms_list: List of strings that contain X/Y. The strings should follow the output from - ``[factor.name for factor in term.factors]``, where term is a Qibo SymbolicTerm. E.g. ["X0", "Z1"] + terms_list: List of strings. The strings should follow the output from + ``" ".join(factor.name for factor in term.factors)``, where term is a Qibo SymbolicTerm. E.g. "X0 Z1". qubitwise: Determines if the check is for general commutativity, or the stricter qubitwise commutativity Returns: list: Containing groups (lists) of Pauli strings that all commute mutually """ - # Need to convert the lists in _terms_list to strings so that they can be used as graph nodes - terms_str = ["".join(term) for term in terms_list] - # print(terms_str) - G = nx.Graph() # Complement graph: Add all the terms as nodes first, then add edges between nodes if they DO NOT commute - G.add_nodes_from(terms_str) + G.add_nodes_from(terms_list) G.add_edges_from( (term1, term2) - for _i1, term1 in enumerate(terms_str) - for _i2, term2 in enumerate(terms_str) + for _i1, term1 in enumerate(terms_list) + for _i2, term2 in enumerate(terms_list) if _i2 > _i1 and not check_terms_commutativity(term1, term2, qubitwise) ) - # Solve using Greedy Colouring on NetworkX term_groups = nx.coloring.greedy_color(G) group_ids = set(term_groups.values()) - sorted_groups = [[group.split() for group, group_id in term_groups.items() if group_id == _id] for _id in group_ids] - print(sorted_groups) + # Sort results so that test results will be replicable + sorted_groups = sorted( + sorted(group for group, group_id in term_groups.items() if group_id == _id) for _id in group_ids + ) return sorted_groups diff --git a/tests/test_measurement_optimisation.py b/tests/test_measurement_optimisation.py index e96b650..80349ab 100644 --- a/tests/test_measurement_optimisation.py +++ b/tests/test_measurement_optimisation.py @@ -33,17 +33,15 @@ def test_check_terms_commutativity(term1, term2, qwc_expected, gc_expected): assert gc_result == gc_expected -# @pytest.mark.parametrize( -# "term_list,qwc_expected,gc_expected", -# [ -# ([["X0"], ["Z0"], ["Z1"], ["Z0", "Z1"]], {[["Z0"], ["Z1"], ["Z0", "Z1"]], ["X0"]}, {[["Z0"], ["Z1"], ["Z0", "Z1"]], ["X0"]}), -# # (["X0"], ["Z1"], True, True), -# # (["X0", "X1"], ["Y0", "Y1"], False, True), -# # (["X0", "Y1"], ["Y0", "Y1"], False, False), -# ], -# ) -# def test_group_commuting_terms(term_list, qwc_expected, gc_expected): -# qwc_result = group_commuting_terms(term_list, qubitwise=True) -# assert set(qwc_result) == qwc_expected -# gc_result = group_commuting_terms(term_list, qubitwise=False) -# assert set(gc_result) == gc_expected +@pytest.mark.parametrize( + "term_list,qwc_expected,gc_expected", + [ + (["X0 Z1", "X0", "Z0", "Z0 Z1"], [["X0", "X0 Z1"], ["Z0", "Z0 Z1"]], [["X0", "X0 Z1"], ["Z0", "Z0 Z1"]]), + (["X0 Y1 Z2", "X1 X2", "Z1 Y2"], [["X0 Y1 Z2"], ["X1 X2"], ["Z1 Y2"]], [["X0 Y1 Z2", "X1 X2", "Z1 Y2"]]), + ], +) +def test_group_commuting_terms(term_list, qwc_expected, gc_expected): + qwc_result = group_commuting_terms(term_list, qubitwise=True) + assert qwc_result == qwc_expected + gc_result = group_commuting_terms(term_list, qubitwise=False) + assert gc_result == gc_expected From b8fd8c36e98bd834942d30d7aa5ec099b001cfd6 Mon Sep 17 00:00:00 2001 From: Wong Zi Cheng <70616433+chmwzc@users.noreply.github.com> Date: Thu, 11 Apr 2024 06:09:57 +0000 Subject: [PATCH 06/31] Add function to add M gates for a group of H terms --- src/qibochem/measurement/optimization.py | 27 +++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/qibochem/measurement/optimization.py b/src/qibochem/measurement/optimization.py index abd4bb6..acceb80 100644 --- a/src/qibochem/measurement/optimization.py +++ b/src/qibochem/measurement/optimization.py @@ -75,6 +75,29 @@ def group_commuting_terms(terms_list, qubitwise): return sorted_groups +def qwc_measurement_gates(grouped_terms): + """ + Get the list of (basis rotation) measurement gates to be added to the circuit. The measurements from the resultant + circuit can then be used to obtain the expectation values of ALL the terms in grouped_terms directly. + + Args: + grouped_terms (list): List of SymbolicTerms that mutually commutes (qubitwise) + + Returns: + list: Measurement gates to be appended to the Qibo circuit + """ + m_gates = {} + for term in grouped_terms: + for factor in term.factors: + if m_gates.get(factor.target_qubit) is None and factor.name[0] != "I": + m_gates[factor.target_qubit] = gates.M(factor.target_qubit, basis=type(factor.gate)) + return list(m_gates.values()) + + +def qwc_measurements(terms_list): + pass + + def measurement_basis_rotations(hamiltonian, n_qubits, grouping=None): """ Split up and sort the Hamiltonian terms to get the basis rotation gates to be applied to a quantum circuit for the @@ -82,7 +105,7 @@ def measurement_basis_rotations(hamiltonian, n_qubits, grouping=None): Args: hamiltonian (SymbolicHamiltonian): Hamiltonian (that only contains X/Y terms?) - n_qubits: Number of qubits in the quantum circuit. + n_qubits: Number of qubits in the quantum circuit grouping: Whether or not to group the X/Y terms together, i.e. use the same set of measurements to get the expectation values of a group of terms simultaneously. Default value of ``None`` will not group any terms together, which is the only option currently implemented. @@ -118,6 +141,8 @@ def measurement_basis_rotations(hamiltonian, n_qubits, grouping=None): ) for term in xy_terms ] + elif grouping == "qwc": + result += qwc_measurements(xy_terms) else: raise NotImplementedError("Not ready yet!") return result From e616db4dcead296165c7d87f2173b7dd7aeab676 Mon Sep 17 00:00:00 2001 From: Wong Zi Cheng <70616433+chmwzc@users.noreply.github.com> Date: Thu, 11 Apr 2024 06:38:49 +0000 Subject: [PATCH 07/31] Finish drafting QWC code. To add tests next --- src/qibochem/measurement/optimization.py | 50 ++++++++++++++++-------- 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/src/qibochem/measurement/optimization.py b/src/qibochem/measurement/optimization.py index acceb80..c706723 100644 --- a/src/qibochem/measurement/optimization.py +++ b/src/qibochem/measurement/optimization.py @@ -66,13 +66,13 @@ def group_commuting_terms(terms_list, qubitwise): if _i2 > _i1 and not check_terms_commutativity(term1, term2, qubitwise) ) # Solve using Greedy Colouring on NetworkX - term_groups = nx.coloring.greedy_color(G) - group_ids = set(term_groups.values()) + sorted_groups = nx.coloring.greedy_color(G) + group_ids = set(sorted_groups.values()) # Sort results so that test results will be replicable - sorted_groups = sorted( - sorted(group for group, group_id in term_groups.items() if group_id == _id) for _id in group_ids + term_groups = sorted( + sorted(group for group, group_id in sorted_groups.items() if group_id == _id) for _id in group_ids ) - return sorted_groups + return term_groups def qwc_measurement_gates(grouped_terms): @@ -95,7 +95,24 @@ def qwc_measurement_gates(grouped_terms): def qwc_measurements(terms_list): - pass + """ + Sort out a list of Hamiltonian terms into separate groups of mutually qubitwise commuting terms, and returns the + grouped terms along with their associated measurement gates + + Args: + terms_list: Iterable of SymbolicTerms + + Returns: + list: List of two-tuples, with each tuple given as ([`list of measurement gates`], [term1, term2, ...]), where + term1, term2, ... are SymbolicTerms. + """ + ham_terms = {" ".join(factor.name for factor in term.factors): term for term in terms_list} + term_groups = group_commuting_terms(ham_terms.keys(), qubitwise=True) + result = [] + for term_group in term_groups: + symbolic_terms = [ham_terms[term] for term in term_group] + result.append((qwc_measurement_gates(symbolic_terms), symbolic_terms)) + return result def measurement_basis_rotations(hamiltonian, n_qubits, grouping=None): @@ -106,14 +123,14 @@ def measurement_basis_rotations(hamiltonian, n_qubits, grouping=None): Args: hamiltonian (SymbolicHamiltonian): Hamiltonian (that only contains X/Y terms?) n_qubits: Number of qubits in the quantum circuit - grouping: Whether or not to group the X/Y terms together, i.e. use the same set of measurements to get the expectation - values of a group of terms simultaneously. Default value of ``None`` will not group any terms together, which is - the only option currently implemented. + grouping: Whether or not to group the X/Y terms together, i.e. use the same set of measurements to get the + expectation values of a group of terms simultaneously. Default value of ``None`` will not group any terms + together, which is the only option currently implemented. Returns: list: List of two-tuples, with each tuple given as ([`list of measurement gates`], [term1, term2, ...]), where - term1, term2, ... are SymbolicTerms. The first tuple always corresponds to all the Z terms present, which will be two - empty lists - ``([], [])`` - if there are no Z terms present. + term1, term2, ... are SymbolicTerms. The first tuple always corresponds to all the Z terms present, which + will be two empty lists - ``([], [])`` - if there are no Z terms present. """ result = [] # Split up the Z and X/Y terms @@ -155,11 +172,12 @@ def allocate_shots(grouped_terms, n_shots, method=None, max_shots_per_term=None) Args: grouped_terms (list): Output of measurement_basis_rotations(hamiltonian, n_qubits, grouping=None n_shots (int): Total number of shots to be allocated - method (str): How to allocate the shots. The available options are: ``"c"``/``"coefficients"``: ``n_shots`` is distributed - based on the relative magnitudes of the term coefficients, ``"u"``/``"uniform"``: ``n_shots`` is distributed evenly - amongst each term. Default value: ``"c"``. - max_shots_per_term (int): Upper limit for the number of shots allocated to an individual group of terms. If not given, - will be defined as a fraction (largest coefficient over the sum of all coefficients in the Hamiltonian) of ``n_shots``. + method (str): How to allocate the shots. The available options are: ``"c"``/``"coefficients"``: ``n_shots`` is + distributed based on the relative magnitudes of the term coefficients, ``"u"``/``"uniform"``: ``n_shots`` + is distributed evenly amongst each term. Default value: ``"c"``. + max_shots_per_term (int): Upper limit for the number of shots allocated to an individual group of terms. If not + given, will be defined as a fraction (largest coefficient over the sum of all coefficients in the + Hamiltonian) of ``n_shots``. Returns: list: A list containing the number of shots to be used for each group of Pauli terms respectively. From 5a21b778b68441a4077efd3a4c9f99234473149f Mon Sep 17 00:00:00 2001 From: Wong Zi Cheng <70616433+chmwzc@users.noreply.github.com> Date: Thu, 11 Apr 2024 06:49:42 +0000 Subject: [PATCH 08/31] Update H2 sample expectation test to use QWC --- tests/test_expectation_samples.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tests/test_expectation_samples.py b/tests/test_expectation_samples.py index 0407e4d..1c1929e 100644 --- a/tests/test_expectation_samples.py +++ b/tests/test_expectation_samples.py @@ -103,8 +103,8 @@ def test_expectation_invalid_shot_allocation(): @pytest.mark.parametrize( "n_shots_per_pauli_term,threshold", [ - (True, 0.005), # 10000 shots used for each term in Hamiltonian - (False, 0.015), # 10000 shots divided between each Pauli string in Hamiltonian + (True, 0.005), # 5000 shots used for each term in Hamiltonian + (False, 0.015), # 5000 shots divided between each Pauli string in Hamiltonian ], ) def test_h2_hf_energy(n_shots_per_pauli_term, threshold): @@ -121,8 +121,13 @@ def test_h2_hf_energy(n_shots_per_pauli_term, threshold): # Molecular Hamiltonian and the HF expectation value hamiltonian = h2.hamiltonian() - n_shots = 10000 + n_shots = 5000 hf_energy = expectation( - circuit, hamiltonian, from_samples=True, n_shots_per_pauli_term=n_shots_per_pauli_term, n_shots=n_shots + circuit, + hamiltonian, + from_samples=True, + n_shots_per_pauli_term=n_shots_per_pauli_term, + n_shots=n_shots, + group_pauli_terms="qwc", ) assert pytest.approx(hf_energy, abs=threshold) == h2_ref_energy From cb69ed163ca779768c246663f7b3c08157e16f38 Mon Sep 17 00:00:00 2001 From: Wong Zi Cheng <70616433+chmwzc@users.noreply.github.com> Date: Thu, 11 Apr 2024 09:14:18 +0000 Subject: [PATCH 09/31] Bug fix and add more tests --- src/qibochem/measurement/result.py | 4 +++- tests/test_expectation_samples.py | 30 ++++++++++++++++++++++++-- tests/test_measurement_optimisation.py | 6 ------ 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/src/qibochem/measurement/result.py b/src/qibochem/measurement/result.py index e2f7c12..cb2bf6b 100644 --- a/src/qibochem/measurement/result.py +++ b/src/qibochem/measurement/result.py @@ -31,7 +31,9 @@ def pauli_term_measurement_expectation(pauli_term, frequencies): pauli_z = [Z(int(factor.target_qubit)) for factor in pauli_term.factors if factor.name[0] != "I"] z_only_ham = SymbolicHamiltonian(pauli_term.coefficient * reduce(lambda x, y: x * y, pauli_z, 1.0)) # Can now apply expectation_from_samples directly - return z_only_ham.expectation_from_samples(frequencies) + return z_only_ham.expectation_from_samples( + frequencies, qubit_map=[factor.target_qubit for factor in pauli_term.factors] + ) def expectation( diff --git a/tests/test_expectation_samples.py b/tests/test_expectation_samples.py index 1c1929e..0eb6648 100644 --- a/tests/test_expectation_samples.py +++ b/tests/test_expectation_samples.py @@ -5,7 +5,7 @@ import pytest from qibo import Circuit, gates from qibo.hamiltonians import SymbolicHamiltonian -from qibo.symbols import X, Z +from qibo.symbols import X, Y, Z from qibochem.driver import Molecule from qibochem.measurement import expectation @@ -100,11 +100,37 @@ def test_expectation_invalid_shot_allocation(): ) +@pytest.mark.parametrize( + "hamiltonian", + [ + SymbolicHamiltonian(Z(0) + X(0) * Y(1) + Z(0) * Y(2)), + SymbolicHamiltonian(Y(0) + Z(1) + X(0) * Z(2)), + ], +) +def test_qwc_functionality(hamiltonian): + """Small scale tests of QWC functionality""" + n_qubits = 3 + circuit = Circuit(n_qubits) + circuit.add(gates.RX(_i, 0.1 * _i) for _i in range(n_qubits)) + circuit.add(gates.CNOT(_i, _i + 1) for _i in range(n_qubits - 1)) + circuit.add(gates.RZ(_i, 0.2 * _i) for _i in range(n_qubits)) + expected = expectation(circuit, hamiltonian) + n_shots = 5000 + test = expectation( + circuit, + hamiltonian, + from_samples=True, + n_shots=n_shots, + group_pauli_terms="qwc", + ) + assert test == pytest.approx(expected, abs=0.05) + + @pytest.mark.parametrize( "n_shots_per_pauli_term,threshold", [ (True, 0.005), # 5000 shots used for each term in Hamiltonian - (False, 0.015), # 5000 shots divided between each Pauli string in Hamiltonian + (False, 0.02), # 5000 shots divided between each Pauli string in Hamiltonian ], ) def test_h2_hf_energy(n_shots_per_pauli_term, threshold): diff --git a/tests/test_measurement_optimisation.py b/tests/test_measurement_optimisation.py index 80349ab..4ed2877 100644 --- a/tests/test_measurement_optimisation.py +++ b/tests/test_measurement_optimisation.py @@ -4,17 +4,11 @@ import pytest -# from qibochem.driver import Molecule -# from qibochem.measurement import expectation from qibochem.measurement.optimization import ( check_terms_commutativity, group_commuting_terms, ) -# from qibo import Circuit, gates -# from qibo.hamiltonians import SymbolicHamiltonian -# from qibo.symbols import X, Z - @pytest.mark.parametrize( "term1,term2,qwc_expected,gc_expected", From 5be727d24fe6c6f8a23d7d0a951689c0b80093d0 Mon Sep 17 00:00:00 2001 From: Wong Zi Cheng <70616433+chmwzc@users.noreply.github.com> Date: Wed, 17 Apr 2024 08:24:44 +0000 Subject: [PATCH 10/31] Split up samples part of expectation function Following Qibo, now two separate functions again: `expectation` and `expectation_from_samples` --- src/qibochem/measurement/result.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/qibochem/measurement/result.py b/src/qibochem/measurement/result.py index cb2bf6b..a915269 100644 --- a/src/qibochem/measurement/result.py +++ b/src/qibochem/measurement/result.py @@ -11,6 +11,18 @@ ) +def expectation(circuit: qibo.models.Circuit, hamiltonian: SymbolicHamiltonian): + """ + Expectation value using state vector simulations + TODO: Docstring + + """ + # Expectation value from state vector simulation + result = circuit(nshots=1) + state_ket = result.state() + return hamiltonian.expectation(state_ket) + + def symbolic_term_to_symbol(symbolic_term): """Convert a single Pauli word in the form of a Qibo SymbolicTerm to a Qibo Symbol""" return symbolic_term.coefficient * reduce(lambda x, y: x * y, symbolic_term.factors, 1.0) @@ -36,10 +48,9 @@ def pauli_term_measurement_expectation(pauli_term, frequencies): ) -def expectation( +def expectation_from_samples( circuit: qibo.models.Circuit, hamiltonian: SymbolicHamiltonian, - from_samples: bool = False, n_shots: int = 1000, group_pauli_terms=None, n_shots_per_pauli_term: bool = True, @@ -52,8 +63,6 @@ def expectation( Args: circuit (qibo.models.Circuit): Quantum circuit ansatz hamiltonian (qibo.hamiltonians.SymbolicHamiltonian): Molecular Hamiltonian - from_samples (bool): Whether the expectation value calculation uses samples or the simulated - state vector. Default: ``False``; Results are from a state vector simulation n_shots (int): Number of times the circuit is run if ``from_samples=True``. Default: ``1000`` group_pauli_terms: Whether or not to group Pauli X/Y terms in the Hamiltonian together to reduce the measurement cost. Default: ``None``; each of the Hamiltonian terms containing X/Y are in their own individual groups. @@ -67,12 +76,6 @@ def expectation( Returns: float: Hamiltonian expectation value """ - if not from_samples: - # Expectation value from state vector simulation - result = circuit(nshots=1) - state_ket = result.state() - return hamiltonian.expectation(state_ket) - # From sample measurements: # (Eventually) measurement_basis_rotations will be used to group up some terms so that one # set of measurements can be used for multiple X/Y terms From 423bb3565932fb2ef9c8cbe77c3599b716d7cf21 Mon Sep 17 00:00:00 2001 From: Wong Zi Cheng <70616433+chmwzc@users.noreply.github.com> Date: Wed, 17 Apr 2024 08:31:35 +0000 Subject: [PATCH 11/31] Update tests and fix __init__.py --- src/qibochem/measurement/__init__.py | 2 +- tests/test_expectation_samples.py | 22 ++++++++++------------ 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/qibochem/measurement/__init__.py b/src/qibochem/measurement/__init__.py index e72e669..89bdbf9 100644 --- a/src/qibochem/measurement/__init__.py +++ b/src/qibochem/measurement/__init__.py @@ -1 +1 @@ -from qibochem.measurement.result import expectation +from qibochem.measurement.result import expectation, expectation_from_samples diff --git a/tests/test_expectation_samples.py b/tests/test_expectation_samples.py index 0eb6648..a2d1678 100644 --- a/tests/test_expectation_samples.py +++ b/tests/test_expectation_samples.py @@ -8,7 +8,7 @@ from qibo.symbols import X, Y, Z from qibochem.driver import Molecule -from qibochem.measurement import expectation +from qibochem.measurement import expectation, expectation_from_samples from qibochem.measurement.optimization import ( allocate_shots, measurement_basis_rotations, @@ -23,12 +23,12 @@ (X(0), [gates.H(0)], 1.0), ], ) -def test_expectation_samples(terms, gates_to_add, expected): - """Test from_samples functionality of expectation function with various Hamiltonians""" +def test_expectation_from_samples(terms, gates_to_add, expected): + """Test expectation_from_samples function with various Hamiltonians""" hamiltonian = SymbolicHamiltonian(terms) circuit = Circuit(2) circuit.add(gates_to_add) - result = expectation(circuit, hamiltonian, from_samples=True) + result = expectation_from_samples(circuit, hamiltonian) assert result == expected @@ -84,8 +84,8 @@ def test_expectation_manual_shot_allocation(gates_to_add, shot_allocation, thres circuit = Circuit(1) circuit.add(gates_to_add) hamiltonian = SymbolicHamiltonian(Z(0) + X(0)) - result = expectation( - circuit, hamiltonian, from_samples=True, n_shots_per_pauli_term=False, shot_allocation=shot_allocation + result = expectation_from_samples( + circuit, hamiltonian, n_shots_per_pauli_term=False, shot_allocation=shot_allocation ) assert pytest.approx(result, abs=threshold) == expected @@ -95,8 +95,8 @@ def test_expectation_invalid_shot_allocation(): hamiltonian = SymbolicHamiltonian(Z(0) + X(0)) shot_allocation = (1,) with pytest.raises(AssertionError): - _ = expectation( - circuit, hamiltonian, from_samples=True, n_shots_per_pauli_term=False, shot_allocation=shot_allocation + _ = expectation_from_samples( + circuit, hamiltonian, n_shots_per_pauli_term=False, shot_allocation=shot_allocation ) @@ -116,10 +116,9 @@ def test_qwc_functionality(hamiltonian): circuit.add(gates.RZ(_i, 0.2 * _i) for _i in range(n_qubits)) expected = expectation(circuit, hamiltonian) n_shots = 5000 - test = expectation( + test = expectation_from_samples( circuit, hamiltonian, - from_samples=True, n_shots=n_shots, group_pauli_terms="qwc", ) @@ -148,10 +147,9 @@ def test_h2_hf_energy(n_shots_per_pauli_term, threshold): hamiltonian = h2.hamiltonian() n_shots = 5000 - hf_energy = expectation( + hf_energy = expectation_from_samples( circuit, hamiltonian, - from_samples=True, n_shots_per_pauli_term=n_shots_per_pauli_term, n_shots=n_shots, group_pauli_terms="qwc", From 43598112aa19faa26dca5697b3b818ee33c9b82d Mon Sep 17 00:00:00 2001 From: Wong Zi Cheng <70616433+chmwzc@users.noreply.github.com> Date: Wed, 17 Apr 2024 09:39:08 +0000 Subject: [PATCH 12/31] Refactor old code to use new QWC-related functions --- src/qibochem/measurement/optimization.py | 26 +++++++----------------- src/qibochem/measurement/result.py | 6 +++--- 2 files changed, 10 insertions(+), 22 deletions(-) diff --git a/src/qibochem/measurement/optimization.py b/src/qibochem/measurement/optimization.py index c706723..a1cffce 100644 --- a/src/qibochem/measurement/optimization.py +++ b/src/qibochem/measurement/optimization.py @@ -7,7 +7,7 @@ from qibo import gates -def check_terms_commutativity(term1, term2, qubitwise): +def check_terms_commutativity(term1: str, term2: str, qubitwise: bool): """ Check if terms 1 and 2 are mutually commuting. The 'qubitwise' flag determines if the check is for general commutativity (False), or the stricter qubitwise commutativity. @@ -108,10 +108,10 @@ def qwc_measurements(terms_list): """ ham_terms = {" ".join(factor.name for factor in term.factors): term for term in terms_list} term_groups = group_commuting_terms(ham_terms.keys(), qubitwise=True) - result = [] - for term_group in term_groups: - symbolic_terms = [ham_terms[term] for term in term_group] - result.append((qwc_measurement_gates(symbolic_terms), symbolic_terms)) + result = [ + (qwc_measurement_gates(symbolic_terms := [ham_terms[term] for term in term_group]), symbolic_terms) + for term_group in term_groups + ] return result @@ -140,24 +140,12 @@ def measurement_basis_rotations(hamiltonian, n_qubits, grouping=None): xy_terms = [term for term in hamiltonian.terms if term not in z_only_terms] # Add the Z terms into result first, followed by the terms with X/Y's if z_only_terms: - result.append(([gates.M(_i) for _i in range(n_qubits)], z_only_terms)) + result.append((qwc_measurement_gates(z_only_terms), z_only_terms)) else: result.append(([], [])) if xy_terms: if grouping is None: - result += [ - ( - [ - gates.M(int(factor.target_qubit), basis=type(factor.gate)) - for factor in term.factors - if factor.name[0] != "I" - ], - [ - term, - ], - ) - for term in xy_terms - ] + result += [(qwc_measurement_gates([term]), [term]) for term in xy_terms] elif grouping == "qwc": result += qwc_measurements(xy_terms) else: diff --git a/src/qibochem/measurement/result.py b/src/qibochem/measurement/result.py index a915269..1eb94d2 100644 --- a/src/qibochem/measurement/result.py +++ b/src/qibochem/measurement/result.py @@ -57,8 +57,7 @@ def expectation_from_samples( shot_allocation=None, ) -> float: """ - Calculate expectation value of some Hamiltonian using either the state vector or sample measurements from running a - quantum circuit + Calculate expectation value of some Hamiltonian using sample measurements from running a Qibo quantum circuit Args: circuit (qibo.models.Circuit): Quantum circuit ansatz @@ -106,7 +105,8 @@ def expectation_from_samples( total += sum(pauli_term_measurement_expectation(term, frequencies) for term in terms) else: z_ham = SymbolicHamiltonian(sum(symbolic_term_to_symbol(term) for term in terms)) - total += z_ham.expectation_from_samples(frequencies) + qubit_map = sorted({factor.target_qubit for term in terms for factor in term.factors}) + total += z_ham.expectation_from_samples(frequencies, qubit_map=qubit_map) # Add the constant term if present. Note: Energies (in chemistry) are all real values total += hamiltonian.constant.real return total From f22587ece851044cc28466200f8259317a18b779 Mon Sep 17 00:00:00 2001 From: Wong Zi Cheng <70616433+chmwzc@users.noreply.github.com> Date: Thu, 18 Apr 2024 03:44:20 +0000 Subject: [PATCH 13/31] Backup small fixes before starting on major edits --- src/qibochem/measurement/optimization.py | 9 ++++----- src/qibochem/measurement/result.py | 8 +++----- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/qibochem/measurement/optimization.py b/src/qibochem/measurement/optimization.py index a1cffce..07899a8 100644 --- a/src/qibochem/measurement/optimization.py +++ b/src/qibochem/measurement/optimization.py @@ -84,14 +84,14 @@ def qwc_measurement_gates(grouped_terms): grouped_terms (list): List of SymbolicTerms that mutually commutes (qubitwise) Returns: - list: Measurement gates to be appended to the Qibo circuit + list, list: (Qubits with measurement gates, Measurement gates to be appended to the Qibo circuit) """ m_gates = {} for term in grouped_terms: for factor in term.factors: if m_gates.get(factor.target_qubit) is None and factor.name[0] != "I": m_gates[factor.target_qubit] = gates.M(factor.target_qubit, basis=type(factor.gate)) - return list(m_gates.values()) + return list(m_gates.keys()), list(m_gates.values()) def qwc_measurements(terms_list): @@ -115,14 +115,13 @@ def qwc_measurements(terms_list): return result -def measurement_basis_rotations(hamiltonian, n_qubits, grouping=None): +def measurement_basis_rotations(hamiltonian, grouping=None): """ Split up and sort the Hamiltonian terms to get the basis rotation gates to be applied to a quantum circuit for the respective (group of) terms in the Hamiltonian Args: hamiltonian (SymbolicHamiltonian): Hamiltonian (that only contains X/Y terms?) - n_qubits: Number of qubits in the quantum circuit grouping: Whether or not to group the X/Y terms together, i.e. use the same set of measurements to get the expectation values of a group of terms simultaneously. Default value of ``None`` will not group any terms together, which is the only option currently implemented. @@ -158,7 +157,7 @@ def allocate_shots(grouped_terms, n_shots, method=None, max_shots_per_term=None) Allocate shots to each group of terms in the Hamiltonian for calculating the expectation value of the Hamiltonian. Args: - grouped_terms (list): Output of measurement_basis_rotations(hamiltonian, n_qubits, grouping=None + grouped_terms (list): Output of measurement_basis_rotations n_shots (int): Total number of shots to be allocated method (str): How to allocate the shots. The available options are: ``"c"``/``"coefficients"``: ``n_shots`` is distributed based on the relative magnitudes of the term coefficients, ``"u"``/``"uniform"``: ``n_shots`` diff --git a/src/qibochem/measurement/result.py b/src/qibochem/measurement/result.py index 1eb94d2..d26545d 100644 --- a/src/qibochem/measurement/result.py +++ b/src/qibochem/measurement/result.py @@ -28,7 +28,7 @@ def symbolic_term_to_symbol(symbolic_term): return symbolic_term.coefficient * reduce(lambda x, y: x * y, symbolic_term.factors, 1.0) -def pauli_term_measurement_expectation(pauli_term, frequencies): +def pauli_term_measurement_expectation(pauli_term, frequencies, qubit_map): """ Calculate the expectation value of a single general Pauli string for some measurement frequencies @@ -43,9 +43,7 @@ def pauli_term_measurement_expectation(pauli_term, frequencies): pauli_z = [Z(int(factor.target_qubit)) for factor in pauli_term.factors if factor.name[0] != "I"] z_only_ham = SymbolicHamiltonian(pauli_term.coefficient * reduce(lambda x, y: x * y, pauli_z, 1.0)) # Can now apply expectation_from_samples directly - return z_only_ham.expectation_from_samples( - frequencies, qubit_map=[factor.target_qubit for factor in pauli_term.factors] - ) + return z_only_ham.expectation_from_samples(frequencies, qubit_map=qubit_map) def expectation_from_samples( @@ -78,7 +76,7 @@ def expectation_from_samples( # From sample measurements: # (Eventually) measurement_basis_rotations will be used to group up some terms so that one # set of measurements can be used for multiple X/Y terms - grouped_terms = measurement_basis_rotations(hamiltonian, circuit.nqubits, grouping=group_pauli_terms) + grouped_terms = measurement_basis_rotations(hamiltonian, grouping=group_pauli_terms) # Check shot_allocation argument if not using n_shots_per_pauli_term if not n_shots_per_pauli_term: From b710b5b2055e2968ef9562513fd52745051bdc32 Mon Sep 17 00:00:00 2001 From: Wong Zi Cheng <70616433+chmwzc@users.noreply.github.com> Date: Thu, 18 Apr 2024 10:00:34 +0000 Subject: [PATCH 14/31] Bugfix: Add qubit_map from M gates.target_qubits --- src/qibochem/measurement/optimization.py | 4 ++-- src/qibochem/measurement/result.py | 4 ++-- tests/test_expectation_samples.py | 29 +++++++++++++++++++----- 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/qibochem/measurement/optimization.py b/src/qibochem/measurement/optimization.py index 07899a8..39427c7 100644 --- a/src/qibochem/measurement/optimization.py +++ b/src/qibochem/measurement/optimization.py @@ -84,14 +84,14 @@ def qwc_measurement_gates(grouped_terms): grouped_terms (list): List of SymbolicTerms that mutually commutes (qubitwise) Returns: - list, list: (Qubits with measurement gates, Measurement gates to be appended to the Qibo circuit) + list: Measurement gates to be appended to the Qibo circuit """ m_gates = {} for term in grouped_terms: for factor in term.factors: if m_gates.get(factor.target_qubit) is None and factor.name[0] != "I": m_gates[factor.target_qubit] = gates.M(factor.target_qubit, basis=type(factor.gate)) - return list(m_gates.keys()), list(m_gates.values()) + return list(m_gates.values()) def qwc_measurements(terms_list): diff --git a/src/qibochem/measurement/result.py b/src/qibochem/measurement/result.py index d26545d..f8e51b2 100644 --- a/src/qibochem/measurement/result.py +++ b/src/qibochem/measurement/result.py @@ -1,6 +1,5 @@ from functools import reduce -import numpy as np import qibo from qibo.hamiltonians import SymbolicHamiltonian from qibo.symbols import Z @@ -96,11 +95,12 @@ def expectation_from_samples( result = _circuit(nshots=n_shots if n_shots_per_pauli_term else shot_allocation[_i]) frequencies = result.frequencies(binary=True) + qubit_map = sorted(qubit for gate in measurement_gates for qubit in gate.target_qubits) if frequencies: # Needed because might have cases whereby no shots allocated to a group # First term is all Z terms, can use expectation_from_samples directly. # Otherwise, need to use the general pauli_term_measurement_expectation function if _i > 0: - total += sum(pauli_term_measurement_expectation(term, frequencies) for term in terms) + total += sum(pauli_term_measurement_expectation(term, frequencies, qubit_map) for term in terms) else: z_ham = SymbolicHamiltonian(sum(symbolic_term_to_symbol(term) for term in terms)) qubit_map = sorted({factor.target_qubit for term in terms for factor in term.factors}) diff --git a/tests/test_expectation_samples.py b/tests/test_expectation_samples.py index a2d1678..ba17c97 100644 --- a/tests/test_expectation_samples.py +++ b/tests/test_expectation_samples.py @@ -13,6 +13,21 @@ allocate_shots, measurement_basis_rotations, ) +from qibochem.measurement.result import pauli_term_measurement_expectation + + +@pytest.mark.parametrize( + "term,frequencies,qubit_map,expected", + [ + (X(0), {"10": 5}, [0, 1], -1.0), + (X(2), {"010": 5}, [0, 2, 5], -1.0), + (Y(4), {"110": 5}, [0, 2, 4], 1.0), + ], +) +def test_pauli_term_measurement_expectation(term, frequencies, qubit_map, expected): + symbolic_term = SymbolicHamiltonian(term).terms[0] + result = pauli_term_measurement_expectation(symbolic_term, frequencies, qubit_map) + assert result == expected, "{term}" @pytest.mark.parametrize( @@ -24,7 +39,6 @@ ], ) def test_expectation_from_samples(terms, gates_to_add, expected): - """Test expectation_from_samples function with various Hamiltonians""" hamiltonian = SymbolicHamiltonian(terms) circuit = Circuit(2) circuit.add(gates_to_add) @@ -36,7 +50,7 @@ def test_measurement_basis_rotations_error(): """If unknown measurement grouping scheme used""" hamiltonian = SymbolicHamiltonian(Z(0) + X(0)) with pytest.raises(NotImplementedError): - _ = measurement_basis_rotations(hamiltonian, 2, grouping="test") + _ = measurement_basis_rotations(hamiltonian, grouping="test") @pytest.mark.parametrize( @@ -51,7 +65,7 @@ def test_measurement_basis_rotations_error(): ) def test_allocate_shots(method, max_shots_per_term, expected): hamiltonian = SymbolicHamiltonian(94 * Z(0) + Z(1) + 5 * X(0)) - grouped_terms = measurement_basis_rotations(hamiltonian, 1) + grouped_terms = measurement_basis_rotations(hamiltonian) n_shots = 200 assert ( allocate_shots(grouped_terms, method=method, n_shots=n_shots, max_shots_per_term=max_shots_per_term) == expected @@ -61,14 +75,14 @@ def test_allocate_shots(method, max_shots_per_term, expected): def test_allocate_shots_coefficient_edge_case(): """Edge cases of allocate_shots""" hamiltonian = SymbolicHamiltonian(Z(0) + X(0)) - grouped_terms = measurement_basis_rotations(hamiltonian, 1) + grouped_terms = measurement_basis_rotations(hamiltonian) n_shots = 1 assert allocate_shots(grouped_terms, n_shots=n_shots) in ([0, 1], [1, 0]) def test_allocate_shots_input_validity(): hamiltonian = SymbolicHamiltonian(94 * Z(0) + Z(1) + 5 * X(0)) - grouped_terms = measurement_basis_rotations(hamiltonian, 1) + grouped_terms = measurement_basis_rotations(hamiltonian) with pytest.raises(NameError): _ = allocate_shots(grouped_terms, n_shots=1, method="wrong") @@ -103,6 +117,7 @@ def test_expectation_invalid_shot_allocation(): @pytest.mark.parametrize( "hamiltonian", [ + SymbolicHamiltonian(X(0) + Y(2)), SymbolicHamiltonian(Z(0) + X(0) * Y(1) + Z(0) * Y(2)), SymbolicHamiltonian(Y(0) + Z(1) + X(0) * Z(2)), ], @@ -122,7 +137,9 @@ def test_qwc_functionality(hamiltonian): n_shots=n_shots, group_pauli_terms="qwc", ) - assert test == pytest.approx(expected, abs=0.05) + assert test == pytest.approx( + expected, abs=0.05 + ), f"Failure: {[[factor.name for factor in term.factors] for term in hamiltonian.terms]}" @pytest.mark.parametrize( From 50de0d9879e62b9a0e583db1a9139ffe9c224195 Mon Sep 17 00:00:00 2001 From: Wong Zi Cheng <70616433+chmwzc@users.noreply.github.com> Date: Fri, 19 Apr 2024 04:05:00 +0000 Subject: [PATCH 15/31] Update docstrings --- src/qibochem/measurement/optimization.py | 6 +++--- src/qibochem/measurement/result.py | 24 +++++++++++++----------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/qibochem/measurement/optimization.py b/src/qibochem/measurement/optimization.py index 39427c7..9906495 100644 --- a/src/qibochem/measurement/optimization.py +++ b/src/qibochem/measurement/optimization.py @@ -121,10 +121,11 @@ def measurement_basis_rotations(hamiltonian, grouping=None): respective (group of) terms in the Hamiltonian Args: - hamiltonian (SymbolicHamiltonian): Hamiltonian (that only contains X/Y terms?) + hamiltonian (SymbolicHamiltonian): Hamiltonian of interest grouping: Whether or not to group the X/Y terms together, i.e. use the same set of measurements to get the expectation values of a group of terms simultaneously. Default value of ``None`` will not group any terms - together, which is the only option currently implemented. + together, while ``"qwc"`` will group qubitwise commuting terms together, and return the measurement gates + associated with each group of X/Y terms Returns: list: List of two-tuples, with each tuple given as ([`list of measurement gates`], [term1, term2, ...]), where @@ -177,7 +178,6 @@ def allocate_shots(grouped_terms, n_shots, method=None, max_shots_per_term=None) [sum(abs(term.coefficient.real) for term in terms) for (_, terms) in grouped_terms] ) max_shots_per_term = int(np.ceil(n_shots * (np.max(term_coefficients) / sum(term_coefficients)))) - # max_shots_per_term = min(max_shots_per_term, 250) # Is there an optimal value - Explore further? max_shots_per_term = min(n_shots, max_shots_per_term) # Don't let max_shots_per_term > n_shots if manually defined n_terms = len(grouped_terms) diff --git a/src/qibochem/measurement/result.py b/src/qibochem/measurement/result.py index f8e51b2..7cdbcba 100644 --- a/src/qibochem/measurement/result.py +++ b/src/qibochem/measurement/result.py @@ -13,10 +13,14 @@ def expectation(circuit: qibo.models.Circuit, hamiltonian: SymbolicHamiltonian): """ Expectation value using state vector simulations - TODO: Docstring + Args: + circuit (qibo.models.Circuit): Quantum circuit ansatz + hamiltonian (qibo.hamiltonians.SymbolicHamiltonian): Molecular Hamiltonian + + Returns: + float: Expectation value of the Hamiltonian for the given circuit """ - # Expectation value from state vector simulation result = circuit(nshots=1) state_ket = result.state() return hamiltonian.expectation(state_ket) @@ -59,9 +63,10 @@ def expectation_from_samples( Args: circuit (qibo.models.Circuit): Quantum circuit ansatz hamiltonian (qibo.hamiltonians.SymbolicHamiltonian): Molecular Hamiltonian - n_shots (int): Number of times the circuit is run if ``from_samples=True``. Default: ``1000`` - group_pauli_terms: Whether or not to group Pauli X/Y terms in the Hamiltonian together to reduce the measurement cost. - Default: ``None``; each of the Hamiltonian terms containing X/Y are in their own individual groups. + n_shots (int): Number of times the circuit is run. Default: ``1000`` + group_pauli_terms: Whether or not to group Pauli X/Y terms in the Hamiltonian together to reduce the measurement + cost. Available options: ``None``: (Default) Hamiltonian terms containing X/Y are not grouped together, and + ``"qwc"``: Terms that commute qubitwise are grouped together n_shots_per_pauli_term (bool): Whether or not ``n_shots`` is used for each Pauli term in the Hamiltonian, or for *all* the terms in the Hamiltonian. Default: ``True``; ``n_shots`` are used to get the expectation value for each term in the Hamiltonian. @@ -72,9 +77,7 @@ def expectation_from_samples( Returns: float: Hamiltonian expectation value """ - # From sample measurements: - # (Eventually) measurement_basis_rotations will be used to group up some terms so that one - # set of measurements can be used for multiple X/Y terms + # Group up Hamiltonian terms to reduce the measurement cost grouped_terms = measurement_basis_rotations(hamiltonian, grouping=group_pauli_terms) # Check shot_allocation argument if not using n_shots_per_pauli_term @@ -83,7 +86,7 @@ def expectation_from_samples( shot_allocation = allocate_shots(grouped_terms, n_shots) assert len(shot_allocation) == len( grouped_terms - ), "shot_allocation list must be the same size as the number of grouped terms!" + ), f"shot_allocation list ({len(shot_allocation)}) doesn't match the number of grouped terms ({len(grouped_terms)})" total = 0.0 for _i, (measurement_gates, terms) in enumerate(grouped_terms): @@ -98,12 +101,11 @@ def expectation_from_samples( qubit_map = sorted(qubit for gate in measurement_gates for qubit in gate.target_qubits) if frequencies: # Needed because might have cases whereby no shots allocated to a group # First term is all Z terms, can use expectation_from_samples directly. - # Otherwise, need to use the general pauli_term_measurement_expectation function if _i > 0: total += sum(pauli_term_measurement_expectation(term, frequencies, qubit_map) for term in terms) + # Otherwise, need to use the general pauli_term_measurement_expectation function else: z_ham = SymbolicHamiltonian(sum(symbolic_term_to_symbol(term) for term in terms)) - qubit_map = sorted({factor.target_qubit for term in terms for factor in term.factors}) total += z_ham.expectation_from_samples(frequencies, qubit_map=qubit_map) # Add the constant term if present. Note: Energies (in chemistry) are all real values total += hamiltonian.constant.real From 7be0bd3ac83b44d7c71672b2314eb3c6d3bea6c8 Mon Sep 17 00:00:00 2001 From: Wong Zi Cheng <70616433+chmwzc@users.noreply.github.com> Date: Mon, 22 Apr 2024 08:31:17 +0000 Subject: [PATCH 16/31] Add module docstring --- src/qibochem/measurement/result.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/qibochem/measurement/result.py b/src/qibochem/measurement/result.py index 7cdbcba..88769f3 100644 --- a/src/qibochem/measurement/result.py +++ b/src/qibochem/measurement/result.py @@ -1,3 +1,8 @@ +""" +Functions for obtaining the expectation value for some given circuit and Hamiltonian, either from a state +vector simulation, or from sample measurements +""" + from functools import reduce import qibo From 9d0887d42eb3818921adc26f22e820011103e736 Mon Sep 17 00:00:00 2001 From: Wong Zi Cheng <70616433+chmwzc@users.noreply.github.com> Date: Tue, 23 Apr 2024 03:08:33 +0000 Subject: [PATCH 17/31] Draft expectation_from_samples API documentation --- doc/source/api-reference/measurement.rst | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/doc/source/api-reference/measurement.rst b/doc/source/api-reference/measurement.rst index e1e43fc..93835f3 100644 --- a/doc/source/api-reference/measurement.rst +++ b/doc/source/api-reference/measurement.rst @@ -2,10 +2,22 @@ Measurement =========== -This section covers the API reference for obtaining the expectation value of a (molecular) Hamiltonian +This section covers the API reference for obtaining the expectation value of a (molecular) Hamiltonian, either from a +state vector simulation, or from sample measurements. + +Expectation value of Hamiltonian +-------------------------------- + +.. autofunction:: qibochem.measurement.result.expectation + +.. autofunction:: qibochem.measurement.result.expectation_from_samples + +Measurement cost reduction +-------------------------- + +The following functions are used for reducing and optimising the measurement cost of obtaining the Hamiltonian +expectation value when sample measurements are used. .. autofunction:: qibochem.measurement.optimization.measurement_basis_rotations .. autofunction:: qibochem.measurement.optimization.allocate_shots - -.. autofunction:: qibochem.measurement.expectation From f129a18d535bd49859a2c101efa2701729beba19 Mon Sep 17 00:00:00 2001 From: Wong Zi Cheng <70616433+chmwzc@users.noreply.github.com> Date: Tue, 23 Apr 2024 03:58:00 +0000 Subject: [PATCH 18/31] QWC tutorial, first draft --- doc/source/tutorials/measurement.rst | 41 ++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/doc/source/tutorials/measurement.rst b/doc/source/tutorials/measurement.rst index a1ba1cb..28acead 100644 --- a/doc/source/tutorials/measurement.rst +++ b/doc/source/tutorials/measurement.rst @@ -1,8 +1,45 @@ -Measurement -=========== +Expectation from samples +------------------------ The previous examples were all carried out using state vector simulations of the quantum circuit. However, in actual quantum hardware, the expectation value of the molecular Hamiltonian for a parameterized quantum circuit have to be estimated by using repeated executions of the circuit, or shots in short. + +.. code-block + H2/STO-3G, JW Hamiltonian + +For example, in the H2/STO-3G system, there are 14 terms that comprise the molecular Hamiltonian, +which means that the expectation value for each of the individual Pauli terms have to be obtained using circuit measurements, before summing them up to obtain the overall expectation value of the molecular Hamiltonian. + +This process of obtaining the electronic energy (Hamiltonian expectation value) is still reasonable for a small system. +Unfortunately, the number of terms in a molecular Hamiltonian scales on the order of O(N^4), where N is the number of qubits. + +.. code-block + N2/STO-3G, JW Hamiltonian + +Even for a relatively small molecule with the minimal STO-3G basis set, there are already (?!?) terms to measure. +Going further, if the electronic energy is obtained towards the goal of running a VQE, it has to be repeated for each step of the VQE. +Clearly, the measurement cost of running VQE has the potential to become astronomically large, and is a significant practical challenge today. + + +Reducing the measurement cost +---------------------------- + +In the examples above, the Hamiltonian expectation values were obtained using a separate set of circuit measurements for each individual term in the molecular Hamiltonian. + +However, we know from quantum mechanics that if two observables (the indvidual Pauli terms in the Hamiltonian) commute, they can be measured simultaneously. + +.. Some math? + + +The question is how to use this in practice? + + + + +OLD TEXT, TO BE EDITED +---------------------- + + Qibochem provides this functionality using the :code:`AbstractHamiltonian.expectation_from_samples` method implemented in Qibo. The example below is taken from the Bravyi-Kitaev transformed Hamiltonian for molecular H\ :sub:`2` in minimal basis of Hartree-Fock orbitals, at 0.70 Angstroms separation between H nuclei, From 069b96ae8b8d1dd68e02dd6606f6050f2d6ca1f6 Mon Sep 17 00:00:00 2001 From: Wong Zi Cheng <70616433+chmwzc@users.noreply.github.com> Date: Tue, 23 Apr 2024 08:01:07 +0000 Subject: [PATCH 19/31] Finish outlining draft of QWC tutorial --- doc/source/tutorials/measurement.rst | 56 ++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 4 deletions(-) diff --git a/doc/source/tutorials/measurement.rst b/doc/source/tutorials/measurement.rst index 28acead..9e067f7 100644 --- a/doc/source/tutorials/measurement.rst +++ b/doc/source/tutorials/measurement.rst @@ -1,5 +1,5 @@ Expectation from samples ------------------------- +======================== The previous examples were all carried out using state vector simulations of the quantum circuit. However, in actual quantum hardware, the expectation value of the molecular Hamiltonian for a parameterized quantum circuit have to be estimated by using repeated executions of the circuit, or shots in short. @@ -22,24 +22,72 @@ Clearly, the measurement cost of running VQE has the potential to become astrono Reducing the measurement cost ----------------------------- +----------------------------- In the examples above, the Hamiltonian expectation values were obtained using a separate set of circuit measurements for each individual term in the molecular Hamiltonian. However, we know from quantum mechanics that if two observables (the indvidual Pauli terms in the Hamiltonian) commute, they can be measured simultaneously. +More precisely, if two observables commute, they have a common eigenbasis. .. Some math? +In other words, a single set of measurements, carried out in the common eigenbasis, can be used to obtain the expectation values of two (or more) commuting observables simultaneously! +What remains is then how to apply the above towards the reduction of the measurement cost in practice. + + +Grouping Hamiltonian terms +-------------------------- + +First, there is the question of how to sort the Hamiltonian terms into separate groups of mutually commuting terms; i.e. each term in a group commutes with every other term in the same group. +As a smaller number of groups would require a smaller number of measurements, it is clear that it is desirable to have as few groups as possible. (ZC note: Phrasing) + +.. Picture of graphs with commuting terms + + +In the above example, blah blah complete graphs and blah blah, duno what can commute with dunno what and dunno what, but it would be better if so and so was grouped with. + +The problem of finding the smallest possible number of groups is equivalent to the minimum clique cover problem, i.e. finding the smallest number of cliques (groups) + + +PennyLane: "Unfortunately, that’s where our good fortune ends—the minimum clique cover problem is known to be NP-hard, meaning there is no known (classical) solution to finding the optimum/minimum clique cover in polynomial time. +Thankfully, there is a silver lining: we know of polynomial-time algorithms for finding approximate solutions to the minimum clique cover problem" +, and these algorithms are available in the NetworkX library: + +.. Example for H for some system -The question is how to use this in practice? +Qubit-wise commuting terms +-------------------------- +After obtaining groups of mutually commuting observables, it remains to find the shared eigenbasis for all terms in the group, and to prepare a set of measurements carried out in this basis. +Unfortunately, this is not trivial: Need to diagonalize matrix here there, combine each eigenvector, blah blah. +However, if the stricter condition of qubit-wise commutativty is enforced, it becomes simple to obtain the shared eigenbasis. + + + +Putting everything together +--------------------------- + +ZC note: Can put the text from the current example here. Show how much Hamiltonian cost reduced for electronic energy evaluation, then extend to each step in VQE. + +.. Code with individual functions + +For convenience, the above has been combined into the ``expectation_from_samples`` function (add link) + +.. Code calling expectation_from_sample directly + + +Final notes +----------- + +(New): Lastly, it may be possible that using a single set of measurements may be undesirable due to errors and uncertainty in the measurement results being propagated across a number of terms. +If a single set of measurements are used for an individual Pauli term, any issues with this set of measurements would not extend to the expectation value of the other Hamiltonian terms. +There are some suggestions towards mitigating this issue. (ref) OLD TEXT, TO BE EDITED ---------------------- - Qibochem provides this functionality using the :code:`AbstractHamiltonian.expectation_from_samples` method implemented in Qibo. The example below is taken from the Bravyi-Kitaev transformed Hamiltonian for molecular H\ :sub:`2` in minimal basis of Hartree-Fock orbitals, at 0.70 Angstroms separation between H nuclei, From 75c7863a99d75f9e4bcf8b4bbf09e1ef9129ae24 Mon Sep 17 00:00:00 2001 From: Wong Zi Cheng <70616433+chmwzc@users.noreply.github.com> Date: Wed, 24 Apr 2024 07:07:21 +0000 Subject: [PATCH 20/31] Continue drafting QWC tutorial --- doc/source/tutorials/measurement.rst | 81 +++++++++++++++++++++------- 1 file changed, 62 insertions(+), 19 deletions(-) diff --git a/doc/source/tutorials/measurement.rst b/doc/source/tutorials/measurement.rst index 9e067f7..710fb75 100644 --- a/doc/source/tutorials/measurement.rst +++ b/doc/source/tutorials/measurement.rst @@ -2,51 +2,94 @@ Expectation from samples ======================== The previous examples were all carried out using state vector simulations of the quantum circuit. -However, in actual quantum hardware, the expectation value of the molecular Hamiltonian for a parameterized quantum circuit have to be estimated by using repeated executions of the circuit, or shots in short. +However, in actual quantum hardware, the expectation value of the molecular Hamiltonian for a parameterized quantum circuit has to be estimated using repeated executions of the circuit, or shots in short. + +.. code-block:: python + + from qibochem.driver import Molecule + from qibochem.ansatz import hf_circuit + from qibochem.measurement import expectation, expectation_from_samples + + # Build the H2 molecule and get the molecular Hamiltonian + h2 = Molecule([("H", (0.0, 0.0, 0.0)), ("H", (0.0, 0.0, 0.7))]) + h2.run_pyscf() + hamiltonian = h2.hamiltonian() + print(f"Number of terms in the Hamiltonian: {len(hamiltonian.terms)}") + + # Construct a basic Hartree-Fock circuit + circuit = hf_circuit(h2.nso, h2.nelec) + + # Expectation value using a state vector simulation: + exact_result = expectation(circuit, hamiltonian) + # Expectation value using (simulated) shots + shots_result = expectation_from_samples(circuit, hamiltonian, n_shots=1000) + print(f"\nExact result: {exact_result:.8f}") + # There will be a small difference between the exact result and the results with shots + print(f"Shots result: {shots_result:.8f}") + + +.. code-block:: output + + Number of terms in the Hamiltonian: 14 + + Exact result: -1.11734903 + Shots result: -1.11260552 -.. code-block - H2/STO-3G, JW Hamiltonian -For example, in the H2/STO-3G system, there are 14 terms that comprise the molecular Hamiltonian, -which means that the expectation value for each of the individual Pauli terms have to be obtained using circuit measurements, before summing them up to obtain the overall expectation value of the molecular Hamiltonian. +In the case of the H\ :sub:`2`/STO-3G (4 qubit) example above, there are 14 terms that comprise the molecular Hamiltonian. +In practice, the expectation value for each of the individual Pauli terms have to be obtained using circuit measurements, before summing them up to obtain the overall expectation value of the molecular Hamiltonian. This process of obtaining the electronic energy (Hamiltonian expectation value) is still reasonable for a small system. -Unfortunately, the number of terms in a molecular Hamiltonian scales on the order of O(N^4), where N is the number of qubits. +However, the number of Pauli terms in a molecular Hamiltonian scales on the order of :math:`O(N^4)`, where N is the number of qubits. + +.. code-block:: python + + from qibochem.driver import Molecule + + # Build the N2 molecule and get the molecular Hamiltonian + n2 = Molecule([("N", (0.0, 0.0, 0.0)), ("N", (0.0, 0.0, 1.1))]) + n2.run_pyscf() + hamiltonian = n2.hamiltonian() + print(f"Number of terms in the Hamiltonian: {len(hamiltonian.terms)}") + + +.. code-block:: output + + Number of terms in the Hamiltonian: 2950 -.. code-block - N2/STO-3G, JW Hamiltonian -Even for a relatively small molecule with the minimal STO-3G basis set, there are already (?!?) terms to measure. -Going further, if the electronic energy is obtained towards the goal of running a VQE, it has to be repeated for each step of the VQE. +Even for the relatively small N\ :sub:`2` molecule with the minimal STO-3G basis set, there are already 2950 (!) terms to measure. +Going further, if the electronic energy is evaluated as part of the process of running a VQE, it has to be repeated for each step of the VQE. Clearly, the measurement cost of running VQE has the potential to become astronomically large, and is a significant practical challenge today. Reducing the measurement cost ----------------------------- -In the examples above, the Hamiltonian expectation values were obtained using a separate set of circuit measurements for each individual term in the molecular Hamiltonian. - +So far, we have assumed that the Hamiltonian expectation values have to be obtained using an independent set of circuit measurements for each term in the molecular Hamiltonian. However, we know from quantum mechanics that if two observables (the indvidual Pauli terms in the Hamiltonian) commute, they can be measured simultaneously. -More precisely, if two observables commute, they have a common eigenbasis. +More precisely, if two observables commute, they have a common eigenbasis, i.e. + +.. math:: -.. Some math? + [A, B] = 0 \implies \exists \underset{~}{x} \text{ such that } A \underset{~}{x} = a \underset{~}{x} \text{ and } B \underset{~}{x} = b \underset{~}{x} -In other words, a single set of measurements, carried out in the common eigenbasis, can be used to obtain the expectation values of two (or more) commuting observables simultaneously! -What remains is then how to apply the above towards the reduction of the measurement cost in practice. +In other words, a single set of measurements, carried out in the common eigenbasis, can be used to obtain the expectation values of two (or more) commuting observables simultaneously. +What remains is then how to apply the above fact towards the reduction of the measurement cost in practice. Grouping Hamiltonian terms -------------------------- First, there is the question of how to sort the Hamiltonian terms into separate groups of mutually commuting terms; i.e. each term in a group commutes with every other term in the same group. -As a smaller number of groups would require a smaller number of measurements, it is clear that it is desirable to have as few groups as possible. (ZC note: Phrasing) +Less groups would mean that a smaller number of measurements are required, which is our eventual goal: .. Picture of graphs with commuting terms -In the above example, blah blah complete graphs and blah blah, duno what can commute with dunno what and dunno what, but it would be better if so and so was grouped with. +In the above example, blah blah complete graphs and blah blah, duno what can commute with dunno what and dunno what, but it would be better if so and so was grouped with so and so. +This problem of finding the smallest possible number of groups is equivalent to the minimum clique cover problem, i.e. finding the smallest number of cliques (groups) of complete graphs. -The problem of finding the smallest possible number of groups is equivalent to the minimum clique cover problem, i.e. finding the smallest number of cliques (groups) PennyLane: "Unfortunately, that’s where our good fortune ends—the minimum clique cover problem is known to be NP-hard, meaning there is no known (classical) solution to finding the optimum/minimum clique cover in polynomial time. From 70c473b6ee54ec1a449db8c03b6dabee0b12e6e7 Mon Sep 17 00:00:00 2001 From: Wong Zi Cheng <70616433+chmwzc@users.noreply.github.com> Date: Wed, 24 Apr 2024 09:01:25 +0000 Subject: [PATCH 21/31] More stuff added to QWC tutorial --- doc/source/tutorials/measurement.rst | 31 +++++++++++++++++++++------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/doc/source/tutorials/measurement.rst b/doc/source/tutorials/measurement.rst index 710fb75..019a857 100644 --- a/doc/source/tutorials/measurement.rst +++ b/doc/source/tutorials/measurement.rst @@ -90,22 +90,37 @@ Less groups would mean that a smaller number of measurements are required, which In the above example, blah blah complete graphs and blah blah, duno what can commute with dunno what and dunno what, but it would be better if so and so was grouped with so and so. This problem of finding the smallest possible number of groups is equivalent to the minimum clique cover problem, i.e. finding the smallest number of cliques (groups) of complete graphs. +Although this is a NP-hard problem, there are polynomial-time algorithms for solving this, and these algorithms are available in the NetworkX library: +.. Example using networkx to find minClique for H for some system -PennyLane: "Unfortunately, that’s where our good fortune ends—the minimum clique cover problem is known to be NP-hard, meaning there is no known (classical) solution to finding the optimum/minimum clique cover in polynomial time. -Thankfully, there is a silver lining: we know of polynomial-time algorithms for finding approximate solutions to the minimum clique cover problem" -, and these algorithms are available in the NetworkX library: - -.. Example for H for some system Qubit-wise commuting terms -------------------------- -After obtaining groups of mutually commuting observables, it remains to find the shared eigenbasis for all terms in the group, and to prepare a set of measurements carried out in this basis. -Unfortunately, this is not trivial: Need to diagonalize matrix here there, combine each eigenvector, blah blah. +After obtaining groups of mutually commuting observables, it remains to find the shared eigenbasis for all terms in the group, and to prepare a set of measurements carried out in this common eigenbasis. +To do this, the standard measurement basis (the Z-basis) has to be transformed using a unitary matrix, which has columns corresponding to the simultaneous eigenvectors of the commuting Pauli terms. +Unfortunately, this approach has its own problems: mainly, the eigenvectors for a general system with N qubits is of dimension :math:`2^N`, which means that the unitary matrix would scale exponentially, rendering it classically intractable. + +However, if the stricter condition of *qubit-wise commutativty* is enforced, the problem becomes much simpler. +First, recall that a general Pauli term can be expressed as a tensor product of Pauli operators, each acting on an individual qubit. + +.. math:: + + h_i = \bigotimes_{i}^{N} P_i -However, if the stricter condition of qubit-wise commutativty is enforced, it becomes simple to obtain the shared eigenbasis. +where :math:`P_i` is a Pauli operator (:math:`I, X, Y, Z`), and :math:`i` is the qubit index. +Then, two Pauli terms commute qubit-wise if their respective single-qubit Pauli operators acting on qubit :math:`i` commute with each other, for all qubits :math:`i`. +For example, the terms :math:`XIZ` and :math:`IYZ` are qubit-wise commuting because :math:`[X, I] = 0`, :math:`[I, Y] = 0`, and :math:`[I, Z] = 0`. +The advantage of the stricter qubitwise commutativity condition is that the common eigenbasis of the commuting terms can be immediately expressed as a tensor product of single qubit Pauli operation. +More specifically, the measurement basis for any qubit is simply the non-:math:`I` observable of interest for that qubit. + +For the :math:`XIZ` and :math:`IYZ` example, we can thus use only one set of measurements in the `XYZ` basis, to obtain the expectation values of both terms simulaneously: + +.. code:: python + + from qibo.hamiltonians import SymbolicHamiltonian Putting everything together From 3cd18b29c509ab2b742dd9fe113972bd983030fe Mon Sep 17 00:00:00 2001 From: Wong Zi Cheng <70616433+chmwzc@users.noreply.github.com> Date: Wed, 24 Apr 2024 09:39:21 +0000 Subject: [PATCH 22/31] Finish explaining QWC and an example Left with the last bits...! --- doc/source/tutorials/measurement.rst | 63 ++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/doc/source/tutorials/measurement.rst b/doc/source/tutorials/measurement.rst index 019a857..8336d67 100644 --- a/doc/source/tutorials/measurement.rst +++ b/doc/source/tutorials/measurement.rst @@ -120,7 +120,70 @@ For the :math:`XIZ` and :math:`IYZ` example, we can thus use only one set of mea .. code:: python + from qibo import Circuit, gates from qibo.hamiltonians import SymbolicHamiltonian + from qibo.symbols import I, X, Y, Z + + from qibochem.measurement import expectation + from qibochem.measurement.result import pauli_term_measurement_expectation + + # Define the two Pauli terms + term1 = SymbolicHamiltonian(X(0)*I(1)*Z(2)) + term2 = SymbolicHamiltonian(I(0)*Y(1)*Z(2)) + + # Define a random circuit + n_qubits = 3 + arbitrary_float = 0.1 + circuit = Circuit(n_qubits) + circuit.add(gates.RX(_i, arbitrary_float) for _i in range(n_qubits)) + circuit.add(gates.RZ(_i, arbitrary_float) for _i in range(n_qubits)) + circuit.add(gates.CNOT(_i, _i+1) for _i in range(n_qubits - 1)) + circuit.add(gates.RX(_i, 2*arbitrary_float) for _i in range(n_qubits)) + circuit.add(gates.RZ(_i, 2*arbitrary_float) for _i in range(n_qubits)) + + # Get the exact result using a state vector simulation + _circuit = circuit.copy() + exact_term1 = expectation(_circuit, term1) + exact_term2 = expectation(_circuit, term2) + + # We want to rotate our measurement basis to the 'XYZ' basis: + circuit.add(gates.M(0, basis=type(X(0).gate))) + circuit.add(gates.M(1, basis=type(Y(1).gate))) # RX(0.5*pi) + circuit.add(gates.M(2, basis=type(Z(2).gate))) # Computational basis remains unchanged + print(circuit.draw()) + + # Now run the circuit to get the circuit measurements + result = circuit(nshots=10000) + frequencies = result.frequencies(binary=True) + # pauli_term_measurement_expectation is a Qibochem function for calculating the expectation value of Hamiltonians with non-Z terms + shots_term1 = pauli_term_measurement_expectation(term1.terms[0], frequencies, qubit_map=range(n_qubits)) + shots_term2 = pauli_term_measurement_expectation(term2.terms[0], frequencies, qubit_map=range(n_qubits)) + + # Compare the output: + print("\nXIZ:") + print(f"Exact result: {exact_term1:.5f}") + print(f" From shots: {shots_term1:.5f}") + + print("\nIYZ:") + print(f"Exact result: {exact_term2:.5f}") + print(f" From shots: {shots_term2:.5f}") + + +.. code-block:: output + + q0: ─RX─RZ─o───RX─RZ─H─M─ + q1: ─RX─RZ─X─o─RX─RZ─U─M─ + q2: ─RX─RZ───X─RX─RZ─M─── + + XIZ: + Exact result: 0.02847 + From shots: 0.03320 + + IYZ: + Exact result: -0.19465 + From shots: -0.19360 + +Again, there is a slight difference between the exact expectation value and the one obtained from shots because of the randomness involved in the circuit measurements. Putting everything together From d4c6448aa87ff31ca716d045c8c2c27b358585b8 Mon Sep 17 00:00:00 2001 From: Wong Zi Cheng <70616433+chmwzc@users.noreply.github.com> Date: Thu, 25 Apr 2024 03:16:01 +0000 Subject: [PATCH 23/31] Finish first draft of QWC tutorial --- doc/source/tutorials/measurement.rst | 120 ++++++++++++++++++++++++--- 1 file changed, 108 insertions(+), 12 deletions(-) diff --git a/doc/source/tutorials/measurement.rst b/doc/source/tutorials/measurement.rst index 8336d67..3ed2b10 100644 --- a/doc/source/tutorials/measurement.rst +++ b/doc/source/tutorials/measurement.rst @@ -44,6 +44,7 @@ However, the number of Pauli terms in a molecular Hamiltonian scales on the orde .. code-block:: python + # Warning: This code block might take a few minutes to run from qibochem.driver import Molecule # Build the N2 molecule and get the molecular Hamiltonian @@ -58,7 +59,7 @@ However, the number of Pauli terms in a molecular Hamiltonian scales on the orde Number of terms in the Hamiltonian: 2950 -Even for the relatively small N\ :sub:`2` molecule with the minimal STO-3G basis set, there are already 2950 (!) terms to measure. +Even for the relatively small N\ :sub:`2` molecule with the minimal STO-3G basis set, there are already 2950 (!!!) terms to measure. Going further, if the electronic energy is evaluated as part of the process of running a VQE, it has to be repeated for each step of the VQE. Clearly, the measurement cost of running VQE has the potential to become astronomically large, and is a significant practical challenge today. @@ -147,8 +148,8 @@ For the :math:`XIZ` and :math:`IYZ` example, we can thus use only one set of mea exact_term2 = expectation(_circuit, term2) # We want to rotate our measurement basis to the 'XYZ' basis: - circuit.add(gates.M(0, basis=type(X(0).gate))) - circuit.add(gates.M(1, basis=type(Y(1).gate))) # RX(0.5*pi) + circuit.add(gates.M(0, basis=type(X(0).gate))) # H gate + circuit.add(gates.M(1, basis=type(Y(1).gate))) # RX(0.5*pi) gate circuit.add(gates.M(2, basis=type(Z(2).gate))) # Computational basis remains unchanged print(circuit.draw()) @@ -183,26 +184,123 @@ For the :math:`XIZ` and :math:`IYZ` example, we can thus use only one set of mea Exact result: -0.19465 From shots: -0.19360 -Again, there is a slight difference between the exact expectation value and the one obtained from shots because of the randomness involved in the circuit measurements. +Again, there is a slight difference between the actual expectation value and the one obtained from shots because of the element of randomness involved in simulating the circuit measurements. Putting everything together --------------------------- -ZC note: Can put the text from the current example here. Show how much Hamiltonian cost reduced for electronic energy evaluation, then extend to each step in VQE. +We demonstate how the whole process of grouping qubit-wise commuting Pauli terms to reduce the measurement cost can be carried out here. +This example is taken from the Bravyi-Kitaev transformed Hamiltonian for molecular H\ :sub:`2` in the minimal STO-3G basis of Hartree-Fock orbitals, at 0.70 Angstroms separation between H nuclei, +as was done in [#f1]_. -.. Code with individual functions +First, the molecular Hamiltonian is of the form: -For convenience, the above has been combined into the ``expectation_from_samples`` function (add link) +.. math:: + + H = g_0 I + g_1 Z_0 + g_2 Z_0 + g_3 Z_0 Z_1 + g_4 Y_0 Y_1 + g_5 X_0 X_1 + +where the :math:`g_i` coefficients are some real numbers. +The :math:`I` term is a constant, and can be ignored. The graph representing which Pauli terms are qubit-wise commuting is given below: + +.. Figure: Graph for BK H + +We then have to solve the problem of finding the smallest possible number of complete subgraphs (groups of Pauli terms). + +.. code-block:: python + + import networkx as nx + + from qibochem.measurement.optimization import check_terms_commutativity + + # Define the Pauli terms as strings + pauli_terms = ["Z0", "Z1", "Z0 Z1", "X0 X1", "Y0 Y1"] + + G = nx.Graph() + G.add_nodes_from(pauli_terms) + + # Solving for the minimum clique cover is equivalent to the graph colouring problem for the complement graph + G.add_edges_from( + (term1, term2) + for _i1, term1 in enumerate(pauli_terms) + for _i2, term2 in enumerate(pauli_terms) + if _i2 > _i1 and not check_terms_commutativity(term1, term2, qubitwise=True) + ) + + sorted_groups = nx.coloring.greedy_color(G) + group_ids = set(sorted_groups.values()) + term_groups = [ + [group for group, group_id in sorted_groups.items() if group_id == _id] + for _id in group_ids + ] + print(f"Grouped terms: {term_groups}") + +.. code-block:: output + + Grouped terms: [['X0 X1'], ['Y0 Y1'], ['Z0', 'Z1', 'Z0 Z1']] + + +Now that we have sorted the Pauli terms into separate groups of qubit-wise commuting terms, it remains to find the shared eigenbasis for each group. +This is trivial for this example, since the first two groups (``['X0 X1']`` and ``['Y0 Y1']``) are single member groups, +and there is no need to rotate the measurement basis for the third and largest group (``['Z0', 'Z1', 'Z0 Z1']``), which consists of only Z terms. + +Lastly, the entire procedure has been combined into the ``expectation_from_samples`` function in Qibochem (add link). +The utility of this functionality can be seen when we limit the number of shots used: + + +.. code-block:: python + + from qibo import models, gates + from qibo.symbols import X, Y, Z + from qibo.hamiltonians import SymbolicHamiltonian + + from qibochem.measurement import expectation, expectation_from_samples + + # Bravyi-Kitaev tranformed Hamiltonian for H2 at 0.7 Angstroms. + # Symmetry considerations were used to reduce the system to only 2 qubits + bk_ham_form = -0.4584 + 0.3593*Z(0) - 0.4826*Z(1) + 0.5818*Z(0)*Z(1) + 0.0896*X(0)*X(1) + 0.0896*Y(0)*Y(1) + bk_ham = SymbolicHamiltonian(bk_ham_form) + + # Define a random circuit + n_qubits = 2 + arbitrary_float = 0.1 + circuit = Circuit(n_qubits) + circuit.add(gates.RX(_i, arbitrary_float) for _i in range(n_qubits)) + circuit.add(gates.RZ(_i, arbitrary_float) for _i in range(n_qubits)) + circuit.add(gates.CNOT(_i, _i+1) for _i in range(n_qubits - 1)) + circuit.add(gates.RX(_i, 2*arbitrary_float) for _i in range(n_qubits)) + circuit.add(gates.RZ(_i, 2*arbitrary_float) for _i in range(n_qubits)) + + # Get the result using a state vector simulation + _circuit = circuit.copy() + exact_result = expectation(_circuit, bk_ham) + + n_shots = 100 + # From shots, grouping the terms together using QWC: + _circuit = circuit.copy() + qwc_result = expectation_from_samples(_circuit, bk_ham, n_shots=n_shots, group_pauli_terms="qwc") + # From shots, without grouping the terms together + _circuit = circuit.copy() + ungrouped_result = expectation_from_samples(_circuit, bk_ham, n_shots=n_shots, group_pauli_terms=None) + + # Compare the results: + print(f"Exact result: {exact_result:.7f}") + print(f"Shots result: {qwc_result:.7f} (Using QWC)") + print(f"Shots result: {ungrouped_result:.7f} (Without grouping)") + + +.. code-block:: output -.. Code calling expectation_from_sample directly + Exact result: -0.0171209 + Shots result: -0.0155220 (Using QWC) + Shots result: -0.0074520 (Without grouping) Final notes ----------- -(New): Lastly, it may be possible that using a single set of measurements may be undesirable due to errors and uncertainty in the measurement results being propagated across a number of terms. -If a single set of measurements are used for an individual Pauli term, any issues with this set of measurements would not extend to the expectation value of the other Hamiltonian terms. +Lastly, it may be possible that using a single set of measurements may be undesirable due to errors and uncertainty in the measurement results being propagated across a number of terms. +If a single set of measurements are used for each individual Pauli term, any issues with this set of measurements would not extend to the expectation value of the other Hamiltonian terms. There are some suggestions towards mitigating this issue. (ref) @@ -211,8 +309,6 @@ OLD TEXT, TO BE EDITED Qibochem provides this functionality using the :code:`AbstractHamiltonian.expectation_from_samples` method implemented in Qibo. -The example below is taken from the Bravyi-Kitaev transformed Hamiltonian for molecular H\ :sub:`2` in minimal basis of Hartree-Fock orbitals, at 0.70 Angstroms separation between H nuclei, -as was done in [#f1]_: Hamiltonian expectation value From 71d4bf79c0446a45183a5c3c6274f0ad296b7bf3 Mon Sep 17 00:00:00 2001 From: Wong Zi Cheng <70616433+chmwzc@users.noreply.github.com> Date: Thu, 25 Apr 2024 03:27:06 +0000 Subject: [PATCH 24/31] Cleanup old text --- doc/source/tutorials/measurement.rst | 73 ---------------------------- 1 file changed, 73 deletions(-) diff --git a/doc/source/tutorials/measurement.rst b/doc/source/tutorials/measurement.rst index 3ed2b10..ab440b9 100644 --- a/doc/source/tutorials/measurement.rst +++ b/doc/source/tutorials/measurement.rst @@ -296,79 +296,6 @@ The utility of this functionality can be seen when we limit the number of shots Shots result: -0.0074520 (Without grouping) -Final notes ------------ - -Lastly, it may be possible that using a single set of measurements may be undesirable due to errors and uncertainty in the measurement results being propagated across a number of terms. -If a single set of measurements are used for each individual Pauli term, any issues with this set of measurements would not extend to the expectation value of the other Hamiltonian terms. -There are some suggestions towards mitigating this issue. (ref) - - -OLD TEXT, TO BE EDITED ----------------------- - -Qibochem provides this functionality using the :code:`AbstractHamiltonian.expectation_from_samples` method implemented in Qibo. - - - -Hamiltonian expectation value ------------------------------ - -.. code-block:: python - - from qibo import models, gates - from qibo.symbols import X, Y, Z - from qibo.hamiltonians import SymbolicHamiltonian - import numpy as np - from qibochem.measurement.expectation import expectation - from scipy.optimize import minimize - - # Bravyi-Kitaev tranformed Hamiltonian for H2 at 0.7 Angstroms - bk_ham_form = -0.4584 + 0.3593*Z(0) - 0.4826*Z(1) + 0.5818*Z(0)*Z(1) + 0.0896*X(0)*X(1) + 0.0896*Y(0)*Y(1) - bk_ham = SymbolicHamiltonian(bk_ham_form) - nuc_repulsion = 0.7559674441714287 - - circuit = models.Circuit(2) - circuit.add(gates.X(0)) - circuit.add(gates.RX(0, -np.pi/2, trainable=False)) - circuit.add(gates.RY(1, np.pi/2, trainable=False)) - circuit.add(gates.CNOT(1, 0)) - circuit.add(gates.RZ(0, theta=0.0)) - circuit.add(gates.CNOT(1, 0)) - circuit.add(gates.RX(0, np.pi/2, trainable=False)) - circuit.add(gates.RY(1, -np.pi/2, trainable=False)) - - print(circuit.draw()) - - def energy_expectation_samples(parameters, circuit, hamiltonian, nshots=1024): - return expectation(circuit, hamiltonian, from_samples=True, n_shots=nshots) - - parameters = [0.5] - nshots = 8192 - vqe_uccsd = minimize(energy_expectation_samples, parameters, args=(circuit, bk_ham, nshots), method='Powell') - print(vqe_uccsd) - print('VQE UCCSD loss: ', vqe_uccsd.fun) - print('nuclear repulsion:', nuc_repulsion) - print('VQE UCCSD energy: ', vqe_uccsd.fun + nuc_repulsion) - - -.. code-block:: output - - q0: ─X──RX─X─RZ─X─RX─ - q1: ─RY────o────o─RY─ - message: Optimization terminated successfully. - success: True - status: 0 - fun: -1.8841124999999999 - x: [ 2.188e+00] - nit: 2 - direc: [[ 1.000e+00]] - nfev: 23 - VQE UCCSD loss: -1.8841124999999999 - nuclear repulsion: 0.7559674441714287 - VQE UCCSD energy: -1.128145055828571 - - .. rubric:: References .. [#f1] P. J. J. O'Malley et al. 'Scalable Quantum Simulation of Molecular Energies' Phys. Rev. X (2016) 6, 031007. From 99278a346be6581eeaa81c1c6ef80daab70c7520 Mon Sep 17 00:00:00 2001 From: Wong Zi Cheng <70616433+chmwzc@users.noreply.github.com> Date: Thu, 25 Apr 2024 03:48:52 +0000 Subject: [PATCH 25/31] Minor edits before starting on grouping section --- doc/source/tutorials/measurement.rst | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/doc/source/tutorials/measurement.rst b/doc/source/tutorials/measurement.rst index ab440b9..50c8743 100644 --- a/doc/source/tutorials/measurement.rst +++ b/doc/source/tutorials/measurement.rst @@ -85,15 +85,13 @@ Grouping Hamiltonian terms First, there is the question of how to sort the Hamiltonian terms into separate groups of mutually commuting terms; i.e. each term in a group commutes with every other term in the same group. Less groups would mean that a smaller number of measurements are required, which is our eventual goal: -.. Picture of graphs with commuting terms +.. TODO: Picture of graphs with commuting terms In the above example, blah blah complete graphs and blah blah, duno what can commute with dunno what and dunno what, but it would be better if so and so was grouped with so and so. This problem of finding the smallest possible number of groups is equivalent to the minimum clique cover problem, i.e. finding the smallest number of cliques (groups) of complete graphs. -Although this is a NP-hard problem, there are polynomial-time algorithms for solving this, and these algorithms are available in the NetworkX library: - -.. Example using networkx to find minClique for H for some system +Although this is a NP-hard problem, there are polynomial-time algorithms for solving this, and these algorithms are available in the NetworkX library (see below). Qubit-wise commuting terms @@ -104,20 +102,20 @@ To do this, the standard measurement basis (the Z-basis) has to be transformed u Unfortunately, this approach has its own problems: mainly, the eigenvectors for a general system with N qubits is of dimension :math:`2^N`, which means that the unitary matrix would scale exponentially, rendering it classically intractable. However, if the stricter condition of *qubit-wise commutativty* is enforced, the problem becomes much simpler. -First, recall that a general Pauli term can be expressed as a tensor product of Pauli operators, each acting on an individual qubit. +First, recall that a general Pauli term can be expressed as a tensor product of single qubit Pauli operators: .. math:: h_i = \bigotimes_{i}^{N} P_i where :math:`P_i` is a Pauli operator (:math:`I, X, Y, Z`), and :math:`i` is the qubit index. -Then, two Pauli terms commute qubit-wise if their respective single-qubit Pauli operators acting on qubit :math:`i` commute with each other, for all qubits :math:`i`. +Then, two Pauli terms commute qubit-wise if their respective Pauli operators that act on qubit :math:`i` commute with each other, for all qubits :math:`i`. For example, the terms :math:`XIZ` and :math:`IYZ` are qubit-wise commuting because :math:`[X, I] = 0`, :math:`[I, Y] = 0`, and :math:`[I, Z] = 0`. -The advantage of the stricter qubitwise commutativity condition is that the common eigenbasis of the commuting terms can be immediately expressed as a tensor product of single qubit Pauli operation. +The advantage of the stricter qubitwise commutativity condition is that the common eigenbasis of the commuting terms can be immediately expressed as a tensor product of single qubit Pauli operations. More specifically, the measurement basis for any qubit is simply the non-:math:`I` observable of interest for that qubit. -For the :math:`XIZ` and :math:`IYZ` example, we can thus use only one set of measurements in the `XYZ` basis, to obtain the expectation values of both terms simulaneously: +For :math:`XIZ` and :math:`IYZ`, we can thus use only one set of measurements in the `XYZ` basis, to obtain the expectation values of both terms simulaneously: .. code:: python @@ -184,7 +182,7 @@ For the :math:`XIZ` and :math:`IYZ` example, we can thus use only one set of mea Exact result: -0.19465 From shots: -0.19360 -Again, there is a slight difference between the actual expectation value and the one obtained from shots because of the element of randomness involved in simulating the circuit measurements. +Again, there is a slight difference between the actual expectation value and the one obtained from shots because of the element of randomness involved in the circuit measurements. Putting everything together @@ -205,7 +203,7 @@ The :math:`I` term is a constant, and can be ignored. The graph representing whi .. Figure: Graph for BK H -We then have to solve the problem of finding the smallest possible number of complete subgraphs (groups of Pauli terms). +We then have to solve the minimum clique cover problem of finding the smallest possible number of complete subgraphs (groups of Pauli terms). .. code-block:: python @@ -293,7 +291,7 @@ The utility of this functionality can be seen when we limit the number of shots Exact result: -0.0171209 Shots result: -0.0155220 (Using QWC) - Shots result: -0.0074520 (Without grouping) + Shots result: -0.0074520 (Without any grouping) .. rubric:: References From 8bec1e5df7f00a9ad5cb3804c4c0d16d4700be01 Mon Sep 17 00:00:00 2001 From: Wong Zi Cheng <70616433+chmwzc@users.noreply.github.com> Date: Thu, 25 Apr 2024 09:20:08 +0000 Subject: [PATCH 26/31] Finalise draft QWC tutorial (almost) TODO: Left with adding figures and references! --- doc/source/api-reference/measurement.rst | 2 + doc/source/tutorials/measurement.rst | 51 ++++++++++++++++-------- 2 files changed, 37 insertions(+), 16 deletions(-) diff --git a/doc/source/api-reference/measurement.rst b/doc/source/api-reference/measurement.rst index 93835f3..027909a 100644 --- a/doc/source/api-reference/measurement.rst +++ b/doc/source/api-reference/measurement.rst @@ -10,6 +10,8 @@ Expectation value of Hamiltonian .. autofunction:: qibochem.measurement.result.expectation +.. _expectation-samples: + .. autofunction:: qibochem.measurement.result.expectation_from_samples Measurement cost reduction diff --git a/doc/source/tutorials/measurement.rst b/doc/source/tutorials/measurement.rst index 50c8743..ecdde12 100644 --- a/doc/source/tutorials/measurement.rst +++ b/doc/source/tutorials/measurement.rst @@ -67,14 +67,15 @@ Clearly, the measurement cost of running VQE has the potential to become astrono Reducing the measurement cost ----------------------------- -So far, we have assumed that the Hamiltonian expectation values have to be obtained using an independent set of circuit measurements for each term in the molecular Hamiltonian. +So far, we have assumed that the expectation value for every term in the molecular Hamiltonian has to be obtained using an independent set of circuit measurements. However, we know from quantum mechanics that if two observables (the indvidual Pauli terms in the Hamiltonian) commute, they can be measured simultaneously. More precisely, if two observables commute, they have a common eigenbasis, i.e. .. math:: - [A, B] = 0 \implies \exists \underset{~}{x} \text{ such that } A \underset{~}{x} = a \underset{~}{x} \text{ and } B \underset{~}{x} = b \underset{~}{x} + [A, B] = 0 \implies \exists x \text{ such that } A x = a x \text{ and } B x = b x +where :math:`a` and :math:`b` are real numbers and :math:`x` is a vector. In other words, a single set of measurements, carried out in the common eigenbasis, can be used to obtain the expectation values of two (or more) commuting observables simultaneously. What remains is then how to apply the above fact towards the reduction of the measurement cost in practice. @@ -83,15 +84,28 @@ Grouping Hamiltonian terms -------------------------- First, there is the question of how to sort the Hamiltonian terms into separate groups of mutually commuting terms; i.e. each term in a group commutes with every other term in the same group. -Less groups would mean that a smaller number of measurements are required, which is our eventual goal: +Less groups would imply that a smaller number of measurements are required, which is our eventual goal. -.. TODO: Picture of graphs with commuting terms +The 14 terms from the molecular Hamiltonian of H\ :sub:`2` with the Jordan-Wigner mapping are as follows: +.. math:: + + H = -0.10973 I + 0.16988 (Z_0 + Z_1) - 0.21886 (Z_2 + Z_3) + 0.16821 Z_0 Z_1 + 0.12005 (Z_0 Z_2 + Z_1 Z_3) + 0.16549 (Z_0 Z_3 + Z_1 Z_2) + 0.17395 Z_2 Z_3 + - 0.04544 (X_0 X_1 Y_2 Y_3 + Y_0 Y_1 X_2 X_3 - X_0 Y_1 Y_2 X_3 - Y_0 X_1 X_2 Y_3) + +For simplicity, we will only look at a selected subset of the Hamiltonian terms. +These terms can be represented as a graph: + +.. image:: h2_terms.svg -In the above example, blah blah complete graphs and blah blah, duno what can commute with dunno what and dunno what, but it would be better if so and so was grouped with so and so. -This problem of finding the smallest possible number of groups is equivalent to the minimum clique cover problem, i.e. finding the smallest number of cliques (groups) of complete graphs. +.. TODO: Fix image! -Although this is a NP-hard problem, there are polynomial-time algorithms for solving this, and these algorithms are available in the NetworkX library (see below). +where the nodes of the graph are the Pauli terms, and with edges connecting two nodes only if they commute; +e.g. the :math:`Z_0` term commutes with the :math:`Z_2 Z_3` term, but not with the :math:`X_0 X_1 Y_2 Y_3` term. + +It can be seen that a group of commuting terms forms a complete subgraph; i.e. each of the nodes in the subgraph have an edge (are directly connected) to all other members in the subgraph. +In other words, our problem of finding the smallest possible number of groups is the minimum clique cover problem, i.e. finding the smallest number of cliques (groups) of complete graphs. +Although this is a NP-hard problem, there are polynomial-time algorithms for solving this, and these algorithms are available in the NetworkX library (example below). Qubit-wise commuting terms @@ -99,7 +113,7 @@ Qubit-wise commuting terms After obtaining groups of mutually commuting observables, it remains to find the shared eigenbasis for all terms in the group, and to prepare a set of measurements carried out in this common eigenbasis. To do this, the standard measurement basis (the Z-basis) has to be transformed using a unitary matrix, which has columns corresponding to the simultaneous eigenvectors of the commuting Pauli terms. -Unfortunately, this approach has its own problems: mainly, the eigenvectors for a general system with N qubits is of dimension :math:`2^N`, which means that the unitary matrix would scale exponentially, rendering it classically intractable. +Unfortunately, this approach has its own problems: mainly, the eigenvectors for a general system with N qubits is of dimension :math:`2^N`, which means that the unitary transformation matrix would scale exponentially, rendering it classically intractable for large systems. However, if the stricter condition of *qubit-wise commutativty* is enforced, the problem becomes much simpler. First, recall that a general Pauli term can be expressed as a tensor product of single qubit Pauli operators: @@ -110,12 +124,12 @@ First, recall that a general Pauli term can be expressed as a tensor product of where :math:`P_i` is a Pauli operator (:math:`I, X, Y, Z`), and :math:`i` is the qubit index. Then, two Pauli terms commute qubit-wise if their respective Pauli operators that act on qubit :math:`i` commute with each other, for all qubits :math:`i`. -For example, the terms :math:`XIZ` and :math:`IYZ` are qubit-wise commuting because :math:`[X, I] = 0`, :math:`[I, Y] = 0`, and :math:`[I, Z] = 0`. +For example, the terms :math:`X_0 I_1 Z_2` and :math:`I_0 Y_1 Z_2` are qubit-wise commuting because :math:`[X_0, I_0] = 0`, :math:`[I_1, Y_1] = 0`, and :math:`[I_2, Z_2] = 0`. -The advantage of the stricter qubitwise commutativity condition is that the common eigenbasis of the commuting terms can be immediately expressed as a tensor product of single qubit Pauli operations. -More specifically, the measurement basis for any qubit is simply the non-:math:`I` observable of interest for that qubit. +The advantage of using the stricter qubitwise commutativity condition is that the common eigenbasis of the commuting terms can be immediately expressed as a tensor product of single qubit Pauli operations. +More specifically, the measurement basis for any qubit is simply the non-:math:`I` observable of interest for that qubit, for any Pauli term in the group. -For :math:`XIZ` and :math:`IYZ`, we can thus use only one set of measurements in the `XYZ` basis, to obtain the expectation values of both terms simulaneously: +For :math:`X_0 I_1 Z_2` and :math:`I_0 Y_1 Z_2`, we can thus use only one set of measurements in the :math:`X_0 Y_1 Z_2` basis, to obtain the expectation values of both terms simulaneously: .. code:: python @@ -201,7 +215,9 @@ First, the molecular Hamiltonian is of the form: where the :math:`g_i` coefficients are some real numbers. The :math:`I` term is a constant, and can be ignored. The graph representing which Pauli terms are qubit-wise commuting is given below: -.. Figure: Graph for BK H +.. image:: bk_ham_graph.svg + +.. TODO: Figure: Graph for BK H We then have to solve the minimum clique cover problem of finding the smallest possible number of complete subgraphs (groups of Pauli terms). @@ -239,11 +255,11 @@ We then have to solve the minimum clique cover problem of finding the smallest p Now that we have sorted the Pauli terms into separate groups of qubit-wise commuting terms, it remains to find the shared eigenbasis for each group. -This is trivial for this example, since the first two groups (``['X0 X1']`` and ``['Y0 Y1']``) are single member groups, +This is trivial, since the first two groups (``['X0 X1']`` and ``['Y0 Y1']``) are single member groups, and there is no need to rotate the measurement basis for the third and largest group (``['Z0', 'Z1', 'Z0 Z1']``), which consists of only Z terms. -Lastly, the entire procedure has been combined into the ``expectation_from_samples`` function in Qibochem (add link). -The utility of this functionality can be seen when we limit the number of shots used: +Lastly, the entire procedure has been combined into the :ref:`expectation_from_samples ` function in Qibochem. +An example of its usage is given below: .. code-block:: python @@ -294,6 +310,9 @@ The utility of this functionality can be seen when we limit the number of shots Shots result: -0.0074520 (Without any grouping) +As shown in the above example, the utility of using qubit-wise commutativity to reduce the measurement cost of evaluating the electronic energy can be seen when the number of shots available are limited. + + .. rubric:: References .. [#f1] P. J. J. O'Malley et al. 'Scalable Quantum Simulation of Molecular Energies' Phys. Rev. X (2016) 6, 031007. From 10170fce8f5791167d59474b0c3abe19b95cbdef Mon Sep 17 00:00:00 2001 From: Wong Zi Cheng <70616433+chmwzc@users.noreply.github.com> Date: Fri, 26 Apr 2024 03:13:14 +0000 Subject: [PATCH 27/31] Add Figures and reference paper --- doc/source/tutorials/bk_ham_graph.svg | 261 ++++++++++++++ doc/source/tutorials/h2_terms.svg | 471 ++++++++++++++++++++++++++ doc/source/tutorials/measurement.rst | 18 +- 3 files changed, 742 insertions(+), 8 deletions(-) create mode 100644 doc/source/tutorials/bk_ham_graph.svg create mode 100644 doc/source/tutorials/h2_terms.svg diff --git a/doc/source/tutorials/bk_ham_graph.svg b/doc/source/tutorials/bk_ham_graph.svg new file mode 100644 index 0000000..fa68cd3 --- /dev/null +++ b/doc/source/tutorials/bk_ham_graph.svg @@ -0,0 +1,261 @@ + + + + + + + + 2024-04-26T10:46:13.821747 + image/svg+xml + + + Matplotlib v3.8.2, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/source/tutorials/h2_terms.svg b/doc/source/tutorials/h2_terms.svg new file mode 100644 index 0000000..7dc7918 --- /dev/null +++ b/doc/source/tutorials/h2_terms.svg @@ -0,0 +1,471 @@ + + + + + + + + 2024-04-26T10:43:57.415513 + image/svg+xml + + + Matplotlib v3.8.2, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/source/tutorials/measurement.rst b/doc/source/tutorials/measurement.rst index ecdde12..58f580d 100644 --- a/doc/source/tutorials/measurement.rst +++ b/doc/source/tutorials/measurement.rst @@ -79,6 +79,7 @@ where :math:`a` and :math:`b` are real numbers and :math:`x` is a vector. In other words, a single set of measurements, carried out in the common eigenbasis, can be used to obtain the expectation values of two (or more) commuting observables simultaneously. What remains is then how to apply the above fact towards the reduction of the measurement cost in practice. +One simple approach is to group qubit-wise commuting terms together, and use a single set of measurements for each group of Pauli terms. [#f1]_ Grouping Hamiltonian terms -------------------------- @@ -98,14 +99,15 @@ These terms can be represented as a graph: .. image:: h2_terms.svg -.. TODO: Fix image! where the nodes of the graph are the Pauli terms, and with edges connecting two nodes only if they commute; e.g. the :math:`Z_0` term commutes with the :math:`Z_2 Z_3` term, but not with the :math:`X_0 X_1 Y_2 Y_3` term. It can be seen that a group of commuting terms forms a complete subgraph; i.e. each of the nodes in the subgraph have an edge (are directly connected) to all other members in the subgraph. In other words, our problem of finding the smallest possible number of groups is the minimum clique cover problem, i.e. finding the smallest number of cliques (groups) of complete graphs. -Although this is a NP-hard problem, there are polynomial-time algorithms for solving this, and these algorithms are available in the NetworkX library (example below). +In the figure above, we can see two possible solutions to this problem: +``[["Z0", "Z1", "Z2", "Z3", "Z0 Z1", "Z2 Z3"], ["X0 Y1 Y2 X3", "Y0 X1 X2 Y3", "X0 X1 Y2 Y3", "Y0 Y1 X2 X3"]]``, or ``[["Z0", "Z1", "Z2", "Z3"], ["Z0 Z1", "Z2 Z3", "X0 Y1 Y2 X3", "Y0 X1 X2 Y3", "X0 X1 Y2 Y3", "Y0 Y1 X2 X3"]]``. +This is a NP-hard problem in general. there are polynomial-time algorithms for solving this, and these algorithms are available in the NetworkX library (example below). Qubit-wise commuting terms @@ -204,7 +206,7 @@ Putting everything together We demonstate how the whole process of grouping qubit-wise commuting Pauli terms to reduce the measurement cost can be carried out here. This example is taken from the Bravyi-Kitaev transformed Hamiltonian for molecular H\ :sub:`2` in the minimal STO-3G basis of Hartree-Fock orbitals, at 0.70 Angstroms separation between H nuclei, -as was done in [#f1]_. +as was done in [#f2]_. First, the molecular Hamiltonian is of the form: @@ -215,10 +217,6 @@ First, the molecular Hamiltonian is of the form: where the :math:`g_i` coefficients are some real numbers. The :math:`I` term is a constant, and can be ignored. The graph representing which Pauli terms are qubit-wise commuting is given below: -.. image:: bk_ham_graph.svg - -.. TODO: Figure: Graph for BK H - We then have to solve the minimum clique cover problem of finding the smallest possible number of complete subgraphs (groups of Pauli terms). .. code-block:: python @@ -253,6 +251,8 @@ We then have to solve the minimum clique cover problem of finding the smallest p Grouped terms: [['X0 X1'], ['Y0 Y1'], ['Z0', 'Z1', 'Z0 Z1']] +.. image:: bk_ham_graph.svg + Now that we have sorted the Pauli terms into separate groups of qubit-wise commuting terms, it remains to find the shared eigenbasis for each group. This is trivial, since the first two groups (``['X0 X1']`` and ``['Y0 Y1']``) are single member groups, @@ -315,4 +315,6 @@ As shown in the above example, the utility of using qubit-wise commutativity to .. rubric:: References -.. [#f1] P. J. J. O'Malley et al. 'Scalable Quantum Simulation of Molecular Energies' Phys. Rev. X (2016) 6, 031007. +.. [#f1] V. Verteletskyi et al. "Measurement Optimization in the Variational Quantum Eigensolver Using a Minimum Clique Cover", J. Chem. Phys. (2020) 152, 124114 + +.. [#f2] P. J. J. O'Malley et al. "Scalable Quantum Simulation of Molecular Energies", Phys. Rev. X (2016) 6, 031007 From d08fb1da242f810b2eb1f0401f8551c14d31f09c Mon Sep 17 00:00:00 2001 From: Wong Zi Cheng <70616433+chmwzc@users.noreply.github.com> Date: Mon, 29 Apr 2024 02:10:32 +0000 Subject: [PATCH 28/31] Update Figures; Minor text changes --- doc/source/tutorials/bk_ham_graph.svg | 265 +------------- doc/source/tutorials/h2_terms.svg | 475 +------------------------- doc/source/tutorials/measurement.rst | 14 +- 3 files changed, 17 insertions(+), 737 deletions(-) diff --git a/doc/source/tutorials/bk_ham_graph.svg b/doc/source/tutorials/bk_ham_graph.svg index fa68cd3..87cbe23 100644 --- a/doc/source/tutorials/bk_ham_graph.svg +++ b/doc/source/tutorials/bk_ham_graph.svg @@ -1,261 +1,4 @@ - - - - - - - - 2024-04-26T10:46:13.821747 - image/svg+xml - - - Matplotlib v3.8.2, https://matplotlib.org/ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + +
Z0 Z1
Z0 Z1
Z0
Z0
Z1
Z1
Y0 Y1
Y0 Y1
X0 X1
X0 X1
Text is not SVG - cannot display
diff --git a/doc/source/tutorials/h2_terms.svg b/doc/source/tutorials/h2_terms.svg index 7dc7918..1afb4b3 100644 --- a/doc/source/tutorials/h2_terms.svg +++ b/doc/source/tutorials/h2_terms.svg @@ -1,471 +1,4 @@ - - - - - - - - 2024-04-26T10:43:57.415513 - image/svg+xml - - - Matplotlib v3.8.2, https://matplotlib.org/ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + +
Y0 Y1 X2 X3
Y0 Y1 X2 X3
Y0 X1 X2 Y3
Y0 X1 X2 Y3
X0 Y1 Y2 X3
X0 Y1 Y2 X3
X0 X1 Y2 Y3
X0 X1 Y2 Y3
Z2 Z3
Z2 Z3
Z0 Z1
Z0 Z1
Z2
Z2
Z1
Z1
Z0
Z0
Z3
Z3
Text is not SVG - cannot display
diff --git a/doc/source/tutorials/measurement.rst b/doc/source/tutorials/measurement.rst index 58f580d..227e743 100644 --- a/doc/source/tutorials/measurement.rst +++ b/doc/source/tutorials/measurement.rst @@ -105,9 +105,10 @@ e.g. the :math:`Z_0` term commutes with the :math:`Z_2 Z_3` term, but not with t It can be seen that a group of commuting terms forms a complete subgraph; i.e. each of the nodes in the subgraph have an edge (are directly connected) to all other members in the subgraph. In other words, our problem of finding the smallest possible number of groups is the minimum clique cover problem, i.e. finding the smallest number of cliques (groups) of complete graphs. -In the figure above, we can see two possible solutions to this problem: + +For the figure above, we can see two possible solutions to this problem: ``[["Z0", "Z1", "Z2", "Z3", "Z0 Z1", "Z2 Z3"], ["X0 Y1 Y2 X3", "Y0 X1 X2 Y3", "X0 X1 Y2 Y3", "Y0 Y1 X2 X3"]]``, or ``[["Z0", "Z1", "Z2", "Z3"], ["Z0 Z1", "Z2 Z3", "X0 Y1 Y2 X3", "Y0 X1 X2 Y3", "X0 X1 Y2 Y3", "Y0 Y1 X2 X3"]]``. -This is a NP-hard problem in general. there are polynomial-time algorithms for solving this, and these algorithms are available in the NetworkX library (example below). +Although this is a NP-hard problem in general, there are polynomial-time algorithms that yield approximate solutions, and these algorithms are available in the NetworkX library (see example below). Qubit-wise commuting terms @@ -129,7 +130,7 @@ Then, two Pauli terms commute qubit-wise if their respective Pauli operators tha For example, the terms :math:`X_0 I_1 Z_2` and :math:`I_0 Y_1 Z_2` are qubit-wise commuting because :math:`[X_0, I_0] = 0`, :math:`[I_1, Y_1] = 0`, and :math:`[I_2, Z_2] = 0`. The advantage of using the stricter qubitwise commutativity condition is that the common eigenbasis of the commuting terms can be immediately expressed as a tensor product of single qubit Pauli operations. -More specifically, the measurement basis for any qubit is simply the non-:math:`I` observable of interest for that qubit, for any Pauli term in the group. +More specifically, the measurement basis for any qubit is simply the non-:math:`I` observable of interest for that qubit, and this holds for all the Pauli terms in the group. For :math:`X_0 I_1 Z_2` and :math:`I_0 Y_1 Z_2`, we can thus use only one set of measurements in the :math:`X_0 Y_1 Z_2` basis, to obtain the expectation values of both terms simulaneously: @@ -217,7 +218,11 @@ First, the molecular Hamiltonian is of the form: where the :math:`g_i` coefficients are some real numbers. The :math:`I` term is a constant, and can be ignored. The graph representing which Pauli terms are qubit-wise commuting is given below: +.. image:: bk_ham_graph.svg + We then have to solve the minimum clique cover problem of finding the smallest possible number of complete subgraphs (groups of Pauli terms). +As the solution in this particular example is trivial, the sample code below is mainly for demonstrative purposes: + .. code-block:: python @@ -251,12 +256,11 @@ We then have to solve the minimum clique cover problem of finding the smallest p Grouped terms: [['X0 X1'], ['Y0 Y1'], ['Z0', 'Z1', 'Z0 Z1']] -.. image:: bk_ham_graph.svg - Now that we have sorted the Pauli terms into separate groups of qubit-wise commuting terms, it remains to find the shared eigenbasis for each group. This is trivial, since the first two groups (``['X0 X1']`` and ``['Y0 Y1']``) are single member groups, and there is no need to rotate the measurement basis for the third and largest group (``['Z0', 'Z1', 'Z0 Z1']``), which consists of only Z terms. +We thus require a total of three sets of measurements to obtain the expectation values for the initial five Pauli terms. Lastly, the entire procedure has been combined into the :ref:`expectation_from_samples ` function in Qibochem. An example of its usage is given below: From f8b0b6b25fc6320d12b9db8f832c37f10c66b22a Mon Sep 17 00:00:00 2001 From: Wong Zi Cheng <70616433+chmwzc@users.noreply.github.com> Date: Mon, 29 Apr 2024 03:34:19 +0000 Subject: [PATCH 29/31] Minor updates --- doc/source/tutorials/measurement.rst | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/doc/source/tutorials/measurement.rst b/doc/source/tutorials/measurement.rst index 227e743..607dc3f 100644 --- a/doc/source/tutorials/measurement.rst +++ b/doc/source/tutorials/measurement.rst @@ -199,7 +199,7 @@ For :math:`X_0 I_1 Z_2` and :math:`I_0 Y_1 Z_2`, we can thus use only one set of Exact result: -0.19465 From shots: -0.19360 -Again, there is a slight difference between the actual expectation value and the one obtained from shots because of the element of randomness involved in the circuit measurements. +Again, there is a slight difference between the actual expectation value and the one obtained from shots because of the element of statistical noise in the circuit measurements. Putting everything together @@ -268,7 +268,7 @@ An example of its usage is given below: .. code-block:: python - from qibo import models, gates + from qibo import Circuit, gates from qibo.symbols import X, Y, Z from qibo.hamiltonians import SymbolicHamiltonian @@ -297,21 +297,23 @@ An example of its usage is given below: # From shots, grouping the terms together using QWC: _circuit = circuit.copy() qwc_result = expectation_from_samples(_circuit, bk_ham, n_shots=n_shots, group_pauli_terms="qwc") + qwc_shots_required = n_shots * 3 # 3 groups of terms # From shots, without grouping the terms together _circuit = circuit.copy() ungrouped_result = expectation_from_samples(_circuit, bk_ham, n_shots=n_shots, group_pauli_terms=None) + ungrouped_shots_required = n_shots * len(bk_ham.terms) # 5 individual Pauli terms # Compare the results: print(f"Exact result: {exact_result:.7f}") - print(f"Shots result: {qwc_result:.7f} (Using QWC)") - print(f"Shots result: {ungrouped_result:.7f} (Without grouping)") + print(f"Shots result: {qwc_result:.7f} (Using QWC, {qwc_shots_required} shots used)") + print(f"Shots result: {ungrouped_result:.7f} (Without grouping, {ungrouped_shots_required} shots used)") .. code-block:: output Exact result: -0.0171209 - Shots result: -0.0155220 (Using QWC) - Shots result: -0.0074520 (Without any grouping) + Shots result: -0.0205140 (Using QWC, 300 shots used) + Shots result: -0.0069460 (Without grouping, 500 shots used) As shown in the above example, the utility of using qubit-wise commutativity to reduce the measurement cost of evaluating the electronic energy can be seen when the number of shots available are limited. From 57dd30560136198a951077397b413db76c844564 Mon Sep 17 00:00:00 2001 From: Adrian Mak Date: Mon, 29 Apr 2024 16:44:31 +0800 Subject: [PATCH 30/31] doc: minor changes to tutorials --- doc/source/tutorials/ansatz.rst | 2 +- doc/source/tutorials/measurement.rst | 22 ++++++++++++++++------ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/doc/source/tutorials/ansatz.rst b/doc/source/tutorials/ansatz.rst index 86dac96..7b602ae 100644 --- a/doc/source/tutorials/ansatz.rst +++ b/doc/source/tutorials/ansatz.rst @@ -232,4 +232,4 @@ The orthonormal molecular orbitals :math:`\phi` are optimized by a direct minimi .. [#f6] Piela, L. (2007). 'Ideas of Quantum Chemistry'. Elsevier B. V., the Netherlands. -.. [#f7] Clements W. R. et al., 'Optimal Design for Universal Multiport Interferometers', Optica 3 (2016) 1460. +.. [#f7] Clements, W. R. et al., 'Optimal Design for Universal Multiport Interferometers', Optica 3 (2016) 1460. diff --git a/doc/source/tutorials/measurement.rst b/doc/source/tutorials/measurement.rst index 607dc3f..0596bc5 100644 --- a/doc/source/tutorials/measurement.rst +++ b/doc/source/tutorials/measurement.rst @@ -75,6 +75,7 @@ More precisely, if two observables commute, they have a common eigenbasis, i.e. [A, B] = 0 \implies \exists x \text{ such that } A x = a x \text{ and } B x = b x + where :math:`a` and :math:`b` are real numbers and :math:`x` is a vector. In other words, a single set of measurements, carried out in the common eigenbasis, can be used to obtain the expectation values of two (or more) commuting observables simultaneously. What remains is then how to apply the above fact towards the reduction of the measurement cost in practice. @@ -91,8 +92,12 @@ The 14 terms from the molecular Hamiltonian of H\ :sub:`2` with the Jordan-Wigne .. math:: - H = -0.10973 I + 0.16988 (Z_0 + Z_1) - 0.21886 (Z_2 + Z_3) + 0.16821 Z_0 Z_1 + 0.12005 (Z_0 Z_2 + Z_1 Z_3) + 0.16549 (Z_0 Z_3 + Z_1 Z_2) + 0.17395 Z_2 Z_3 - - 0.04544 (X_0 X_1 Y_2 Y_3 + Y_0 Y_1 X_2 X_3 - X_0 Y_1 Y_2 X_3 - Y_0 X_1 X_2 Y_3) + \begin{align*ed*} + H = & -0.10973 I + 0.16988 (Z_0 + Z_1) - 0.21886 (Z_2 + Z_3) + 0.16821 Z_0 Z_1 \\ + & + 0.12005 (Z_0 Z_2 + Z_1 Z_3) + 0.16549 (Z_0 Z_3 + Z_1 Z_2) + 0.17395 Z_2 Z_3 \\ + & - 0.04544 (X_0 X_1 Y_2 Y_3 + Y_0 Y_1 X_2 X_3 - X_0 Y_1 Y_2 X_3 - Y_0 X_1 X_2 Y_3) + \end{align*ed*} + For simplicity, we will only look at a selected subset of the Hamiltonian terms. These terms can be represented as a graph: @@ -125,6 +130,7 @@ First, recall that a general Pauli term can be expressed as a tensor product of h_i = \bigotimes_{i}^{N} P_i + where :math:`P_i` is a Pauli operator (:math:`I, X, Y, Z`), and :math:`i` is the qubit index. Then, two Pauli terms commute qubit-wise if their respective Pauli operators that act on qubit :math:`i` commute with each other, for all qubits :math:`i`. For example, the terms :math:`X_0 I_1 Z_2` and :math:`I_0 Y_1 Z_2` are qubit-wise commuting because :math:`[X_0, I_0] = 0`, :math:`[I_1, Y_1] = 0`, and :math:`[I_2, Z_2] = 0`. @@ -206,8 +212,9 @@ Putting everything together --------------------------- We demonstate how the whole process of grouping qubit-wise commuting Pauli terms to reduce the measurement cost can be carried out here. -This example is taken from the Bravyi-Kitaev transformed Hamiltonian for molecular H\ :sub:`2` in the minimal STO-3G basis of Hartree-Fock orbitals, at 0.70 Angstroms separation between H nuclei, -as was done in [#f2]_. + +This example is taken from the Bravyi-Kitaev transformed Hamiltonian for molecular H\ :sub:`2` in the minimal STO-3G basis of Hartree-Fock orbitals, at 0.70 Angstrom distance between H nuclei. [#f2]_ + First, the molecular Hamiltonian is of the form: @@ -215,6 +222,7 @@ First, the molecular Hamiltonian is of the form: H = g_0 I + g_1 Z_0 + g_2 Z_0 + g_3 Z_0 Z_1 + g_4 Y_0 Y_1 + g_5 X_0 X_1 + where the :math:`g_i` coefficients are some real numbers. The :math:`I` term is a constant, and can be ignored. The graph representing which Pauli terms are qubit-wise commuting is given below: @@ -263,6 +271,7 @@ and there is no need to rotate the measurement basis for the third and largest g We thus require a total of three sets of measurements to obtain the expectation values for the initial five Pauli terms. Lastly, the entire procedure has been combined into the :ref:`expectation_from_samples ` function in Qibochem. + An example of its usage is given below: @@ -319,8 +328,9 @@ An example of its usage is given below: As shown in the above example, the utility of using qubit-wise commutativity to reduce the measurement cost of evaluating the electronic energy can be seen when the number of shots available are limited. + .. rubric:: References -.. [#f1] V. Verteletskyi et al. "Measurement Optimization in the Variational Quantum Eigensolver Using a Minimum Clique Cover", J. Chem. Phys. (2020) 152, 124114 +.. [#f1] Verteletskyi, V. et al. "Measurement Optimization in the Variational Quantum Eigensolver Using a Minimum Clique Cover", J. Chem. Phys. (2020) 152, 124114 -.. [#f2] P. J. J. O'Malley et al. "Scalable Quantum Simulation of Molecular Energies", Phys. Rev. X (2016) 6, 031007 +.. [#f2] O'Malley, P. J. J. et al. "Scalable Quantum Simulation of Molecular Energies", Phys. Rev. X (2016) 6, 031007 From 469351753641ad5b568ac37f129965458647aa7d Mon Sep 17 00:00:00 2001 From: Adrian Mak Date: Mon, 29 Apr 2024 16:47:17 +0800 Subject: [PATCH 31/31] doc: fix typo --- doc/source/tutorials/measurement.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/tutorials/measurement.rst b/doc/source/tutorials/measurement.rst index 0596bc5..58c18b9 100644 --- a/doc/source/tutorials/measurement.rst +++ b/doc/source/tutorials/measurement.rst @@ -92,11 +92,11 @@ The 14 terms from the molecular Hamiltonian of H\ :sub:`2` with the Jordan-Wigne .. math:: - \begin{align*ed*} + \begin{align*} H = & -0.10973 I + 0.16988 (Z_0 + Z_1) - 0.21886 (Z_2 + Z_3) + 0.16821 Z_0 Z_1 \\ & + 0.12005 (Z_0 Z_2 + Z_1 Z_3) + 0.16549 (Z_0 Z_3 + Z_1 Z_2) + 0.17395 Z_2 Z_3 \\ & - 0.04544 (X_0 X_1 Y_2 Y_3 + Y_0 Y_1 X_2 X_3 - X_0 Y_1 Y_2 X_3 - Y_0 X_1 X_2 Y_3) - \end{align*ed*} + \end{align*} For simplicity, we will only look at a selected subset of the Hamiltonian terms.