forked from sandbox-quantum/Tangelo
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Trim trivial qubits from circuit and Hamiltonian (sandbox-quantum#296)
Remove qubits that are in a deterministic state in quantum circuits, and simplify qubit Hamiltonian accordingly, in order to reduce resource requirements while computing expectation values.
- Loading branch information
1 parent
23104ce
commit c4d0a2a
Showing
3 changed files
with
264 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
92 changes: 92 additions & 0 deletions
92
tangelo/toolboxes/operators/tests/test_trim_trivial_qubits.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
# Copyright 2023 Good Chemistry Company. | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
|
||
import os | ||
import unittest | ||
|
||
import numpy as np | ||
from openfermion.linalg import qubit_operator_sparse | ||
from openfermion.utils import load_operator | ||
|
||
from tangelo.linq import Gate, Circuit, get_backend | ||
from tangelo.toolboxes.operators import QubitOperator | ||
from tangelo.toolboxes.ansatz_generator.ansatz_utils import exp_pauliword_to_gates | ||
from tangelo.toolboxes.operators.trim_trivial_qubits import trim_trivial_qubits, trim_trivial_operator, trim_trivial_circuit, is_bitflip_gate | ||
|
||
|
||
pwd_this_test = os.path.dirname(os.path.abspath(__file__)) | ||
|
||
qb_ham = load_operator("H4_JW_spinupfirst.data", data_directory=pwd_this_test+"/data", plain_text=True) | ||
|
||
# Generate reference and test circuits using single qcc generator | ||
ref_qcc_op = 0.2299941483397896 * 0.5 * QubitOperator("Y0 X1 X2 X3") | ||
qcc_op = 0.2299941483397896 * 0.5 * QubitOperator("Y1 X3 X5 X7") | ||
|
||
ref_mf_gates = [Gate("RX", 0, parameter=np.pi), Gate("X", 2)] | ||
|
||
mf_gates = [ | ||
Gate("RZ", 0, parameter=np.pi/2), Gate("RX", 0, parameter=3.14159), | ||
Gate("RX", 1, parameter=np.pi), Gate("RZ", 2, parameter=np.pi), | ||
Gate("X", 4), Gate("X", 5), Gate("Z", 6), | ||
Gate("RZ", 6, parameter=np.pi), Gate("RX", 8, parameter=-3*np.pi), | ||
Gate("X", 8), Gate("RZ", 9, parameter=np.pi), Gate("Z", 9) | ||
] | ||
|
||
ref_pauli_words_gates = sum((exp_pauliword_to_gates(pword, coef) for pword, coef in ref_qcc_op.terms.items()), start=[]) | ||
pauli_words_gates = sum((exp_pauliword_to_gates(pword, coef) for pword, coef in qcc_op.terms.items()), start=[]) | ||
|
||
ref_circ = Circuit(ref_mf_gates + ref_pauli_words_gates) | ||
circ = Circuit(mf_gates + pauli_words_gates) | ||
|
||
# Reference energy for H4 molecule with single QCC generator | ||
ref_value = -1.8039875664891176 | ||
|
||
# Reference indices and states to be removed from system | ||
ref_trim_states = {0: 1, 2: 0, 4: 1, 6: 0, 8: 0, 9: 0} | ||
|
||
# Reference for which mf_gates are bitflip gates | ||
ref_bool = [False, True, True, False, True, True, False, False, True, True, False, False] | ||
|
||
sim = get_backend() | ||
|
||
|
||
class TrimTrivialQubits(unittest.TestCase): | ||
def test_trim_trivial_operator(self): | ||
""" Test if trimming operator returns the correct eigenvalue """ | ||
|
||
trimmed_operator = trim_trivial_operator(qb_ham, trim_states={key: ref_trim_states[key] for key in [0, 2, 4, 6]}, reindex=False) | ||
self.assertAlmostEqual(np.min(np.linalg.eigvalsh(qubit_operator_sparse(trimmed_operator).todense())), ref_value, places=5) | ||
|
||
def test_is_bitflip_gate(self): | ||
""" Test if bitflip gate function correctly identifies bitflip gates """ | ||
self.assertEqual(ref_bool, [is_bitflip_gate(g) for g in mf_gates]) | ||
|
||
def test_trim_trivial_circuit(self): | ||
""" Test if circuit trimming returns the correct circuit, states, and indices """ | ||
|
||
trimmed_circuit, trim_states = trim_trivial_circuit(circ) | ||
self.assertEqual(ref_circ._gates, trimmed_circuit._gates) | ||
self.assertEqual(ref_trim_states, trim_states) | ||
|
||
def test_trim_trivial_qubits(self): | ||
""" Test if trim trivial qubit function produces correct and compatible circuits and operators """ | ||
|
||
trimmed_operator, trimmed_circuit = trim_trivial_qubits(qb_ham, circ) | ||
self.assertAlmostEqual(np.min(np.linalg.eigvalsh(qubit_operator_sparse(trimmed_operator).todense())), ref_value, places=5) | ||
self.assertEqual(ref_circ._gates, trimmed_circuit._gates) | ||
self.assertAlmostEqual(sim.get_expectation_value(trimmed_operator, trimmed_circuit), ref_value, places=5) | ||
|
||
|
||
if __name__ == "__main__": | ||
unittest.main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
# Copyright 2023 Good Chemistry Company. | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
|
||
import numpy as np | ||
|
||
from tangelo.toolboxes.operators import QubitOperator, count_qubits | ||
from tangelo.linq import Circuit | ||
from tangelo.linq.helpers.circuits import pauli_string_to_of, pauli_of_to_string | ||
|
||
|
||
def trim_trivial_operator(qu_op, trim_states, n_qubits=None, reindex=True): | ||
""" | ||
Calculate expectation values of a QubitOperator acting on qubits in a | ||
trivial |0> or |1> state. Return a trimmed QubitOperator with updated coefficients | ||
Args: | ||
qu_op (QubitOperator): Operator to trim | ||
trim_states (dict): Dictionary mapping qubit indices to states to trim, e.g. {1: 0, 3: 1} | ||
n_qubits (int): Optional, number of qubits in full system | ||
reindex (bool): Optional, if True, remaining qubits will be reindexed | ||
Returns: | ||
QubitOperator : trimmed QubitOperator with updated coefficients | ||
""" | ||
|
||
qu_op_trim = QubitOperator() | ||
n_qubits = count_qubits(qu_op) if n_qubits is None else n_qubits | ||
|
||
# Calculate expectation values of trivial qubits, update coefficients | ||
for op, coeff in qu_op.terms.items(): | ||
term = pauli_of_to_string(op, n_qubits) | ||
c = np.ones(len(trim_states)) | ||
new_term = term | ||
for i, qubit in enumerate(trim_states.keys()): | ||
if term[qubit] in {'X', 'Y'}: | ||
c[i] = 0 | ||
break | ||
elif (term[qubit], trim_states[qubit]) == ('Z', 1): | ||
c[i] = -1 | ||
|
||
new_term = new_term[:qubit - i] + new_term[qubit - i + 1:] if reindex else new_term[:qubit] + 'I' + new_term[qubit + 1:] | ||
|
||
if 0 in c: | ||
continue | ||
|
||
qu_op_trim += np.prod(c) * coeff * QubitOperator(pauli_string_to_of(new_term)) | ||
return qu_op_trim | ||
|
||
|
||
def is_bitflip_gate(gate, atol=1e-5): | ||
""" | ||
Check if a gate is a bitflip gate. | ||
A gate is a bitflip gate if it satisfies one of the following conditions: | ||
1. The gate name is either X, Y. | ||
2. The gate name is RX or RY, and has a parameter that is an odd multiple of pi. | ||
Args: | ||
gate (Gate): The gate to check. | ||
atol (float): Optional, the absolute tolerance for gate parameter | ||
Returns: | ||
bool: True if the gate is a single qubit bitflip gate, False otherwise. | ||
""" | ||
if gate is None: | ||
return False | ||
|
||
if gate.name in {"X", "Y"}: | ||
return True | ||
elif gate.name in {"RX", "RY"}: | ||
try: | ||
parameter_float = float(gate.parameter) | ||
except (TypeError, ValueError): | ||
return False | ||
|
||
# Check if parameter is close to an odd multiple of pi | ||
return abs(parameter_float % (np.pi * 2) - np.pi) <= atol | ||
else: | ||
return False | ||
|
||
|
||
def trim_trivial_circuit(circuit): | ||
""" | ||
Splits Circuit into entangled and unentangled components. | ||
Returns entangled Circuit, and the indices and states of unentangled qubits | ||
Args: | ||
circuit (Circuit): circuit to be trimmed | ||
Returns: | ||
Circuit : Trimmed, entangled circuit | ||
dict : dictionary mapping trimmed qubit indices to their states (0 or 1) | ||
""" | ||
# Split circuit and get relevant indices | ||
circs = circuit.split() | ||
e_indices = circuit.get_entangled_indices() | ||
|
||
# Find qubits with no gates applied to them, store qubit index and state |0> | ||
trim_states = {} | ||
for qubit_idx in set(range(circuit.width)) - set(circuit._qubit_indices): | ||
trim_states[qubit_idx] = 0 | ||
|
||
circuit_new = Circuit() | ||
# Go through circuit components, trim if trivial, otherwise append to new circuit | ||
for i, circ in enumerate(circs): | ||
if circ.width != 1 or circ.size not in (1, 2): | ||
circuit_new += circ | ||
continue | ||
|
||
# Calculate state of single qubit clifford circuits, ideally this would be done with a clifford simulator | ||
# for now only look at first two gate combinations typical of the QMF state in QCC methods | ||
gate0, gate1 = circ._gates[:2] + [None] * (2 - circ.size) | ||
gate_0_is_bitflip = is_bitflip_gate(gate0) | ||
gate_1_is_bitflip = is_bitflip_gate(gate1) | ||
|
||
if circ.size == 1: | ||
if gate0.name in {"RZ", "Z"}: | ||
qubit_idx = e_indices[i].pop() | ||
trim_states[qubit_idx] = 0 | ||
elif gate0.name in {"X", "RX"} and gate_0_is_bitflip: | ||
qubit_idx = e_indices[i].pop() | ||
trim_states[qubit_idx] = 1 | ||
else: | ||
circuit_new += circ | ||
elif circ.size == 2: | ||
if gate1.name in {"Z", "RZ"}: | ||
if gate0.name in {"RZ", "Z"}: | ||
qubit_idx = e_indices[i].pop() | ||
trim_states[qubit_idx] = 0 | ||
else: | ||
circuit_new += circ | ||
elif gate1.name in {"X", "RX"} and gate_1_is_bitflip: | ||
if gate0.name in {"RX", "X"} and gate_0_is_bitflip: | ||
qubit_idx = e_indices[i].pop() | ||
trim_states[qubit_idx] = 0 | ||
elif gate0.name in {"Z", "RZ"}: | ||
qubit_idx = e_indices[i].pop() | ||
trim_states[qubit_idx] = 1 | ||
else: | ||
circuit_new += circ | ||
else: | ||
circuit_new += circ | ||
return circuit_new, trim_states | ||
|
||
|
||
def trim_trivial_qubits(operator, circuit): | ||
""" | ||
Trim circuit and operator based on expectation values calculated from | ||
trivial components of the circuit. | ||
Args: | ||
operator (QubitOperator): Operator to trim | ||
circuit (Circuit): circuit to be trimmed | ||
Returns: | ||
QubitOperator : Trimmed qubit operator | ||
Circuit : Trimmed circuit | ||
""" | ||
trimmed_circuit, trim_states = trim_trivial_circuit(circuit) | ||
trimmed_operator = trim_trivial_operator(operator, trim_states, circuit.width, reindex=True) | ||
|
||
return trimmed_operator, trimmed_circuit |