Skip to content

Commit 534b95f

Browse files
authored
Merge pull request #1686 from qiboteam/exp_fix
Support simultaneously diagonalizable terms in ``exp_from_circuit``
2 parents 2db2576 + 64ac0a9 commit 534b95f

File tree

3 files changed

+98
-39
lines changed

3 files changed

+98
-39
lines changed

src/qibo/hamiltonians/hamiltonians.py

Lines changed: 72 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ def expectation_from_samples(self, freq, qubit_map=None):
135135
diag = self.backend.np.reshape(diag, self.nqubits * (2,))
136136
if qubit_map is None:
137137
qubit_map = range(self.nqubits)
138-
diag = self.backend.np.transpose(diag, axes=qubit_map).ravel()
138+
diag = self.backend.np.transpose(diag, qubit_map).ravel()
139139
# select only the elements with non-zero counts
140140
diag = diag[[int(state, 2) for state in freq.keys()]]
141141
counts = self.backend.cast(list(freq.values()), dtype=diag.dtype) / sum(
@@ -373,6 +373,35 @@ def terms(self):
373373
self.constant += term.coefficient
374374
return terms
375375

376+
@cached_property
377+
def diagonal_terms(self) -> list[list[SymbolicTerm]]:
378+
"""List of terms that can be diagonalized simultaneously, i.e. that
379+
commute with each other. In detail each element of the list is a sublist
380+
of commuting ``SymbolicTerm``s.
381+
"""
382+
diagonal_terms = []
383+
terms = self.terms
384+
# loop until there are no more terms
385+
while len(terms) > 0:
386+
# take the first (remaining) term
387+
t0 = terms[0]
388+
diagonal_term = [t0]
389+
removable_indices = {
390+
0,
391+
}
392+
# look for all the following terms that commute with the
393+
# first one and among themselves
394+
for i, t1 in enumerate(terms[1:], 1):
395+
# commutes with all the other terms -> append it
396+
if all(term.commute(t1) for term in diagonal_term):
397+
diagonal_term.append(t1)
398+
removable_indices.add(i)
399+
# append the new sublist of commuting terms found
400+
diagonal_terms.append(diagonal_term)
401+
# remove them from the original terms
402+
terms = [term for i, term in enumerate(terms) if i not in removable_indices]
403+
return diagonal_terms
404+
376405
@property
377406
def matrix(self):
378407
"""Returns the full matrix representation.
@@ -502,46 +531,56 @@ def expectation_from_circuit(self, circuit: "Circuit", nshots: int = 1000) -> fl
502531
from qibo import gates
503532

504533
rotated_circuits = []
505-
coefficients = []
506534
Z_observables = []
507535
qubit_maps = []
508-
for term in self.terms:
509-
# store coefficient
510-
coefficients.append(term.coefficient)
511-
# Only care about non-I terms
512-
non_identity_factors = [
513-
factor for factor in term.factors if factor.name[0] != "I"
514-
]
515-
# build diagonal observable
516-
Z_observables.append(
517-
SymbolicHamiltonian(
518-
prod(Z(factor.target_qubit) for factor in non_identity_factors),
536+
# loop over the terms that can be diagonalized simultaneously
537+
for terms in self.diagonal_terms:
538+
# for each term that can be diagonalized simultaneously
539+
# extract the coefficient, Z observable and qubit map
540+
# the basis rotation, instead, will be the same
541+
tmp_obs = []
542+
measurements = {}
543+
for term in terms:
544+
# Only care about non-I terms
545+
non_identity_factors = []
546+
# prepare the measurement basis and append it to the circuit
547+
for factor in term.factors:
548+
if factor.name[0] != "I":
549+
non_identity_factors.append(factor)
550+
q = factor.target_qubit
551+
if q not in measurements:
552+
measurements[q] = gates.M(q, basis=factor.gate.__class__)
553+
554+
# build diagonal observable
555+
# including the coefficient
556+
symb_obs = prod(
557+
Z(factor.target_qubit) for factor in non_identity_factors
558+
)
559+
symb_obs = SymbolicHamiltonian(
560+
term.coefficient * symb_obs,
519561
nqubits=circuit.nqubits,
520562
backend=self.backend,
521563
)
522-
)
564+
tmp_obs.append(symb_obs)
565+
523566
# Get the qubits we want to measure for each term
524-
qubit_map = sorted(factor.target_qubit for factor in non_identity_factors)
525-
# prepare the measurement basis and append it to the circuit
526-
measurements = [
527-
gates.M(factor.target_qubit, basis=factor.gate.__class__)
528-
for factor in non_identity_factors
529-
]
567+
qubit_maps.append(sorted(measurements.keys()))
568+
Z_observables.append(tmp_obs)
569+
530570
circ_copy = circuit.copy(True)
531-
circ_copy.add(measurements)
571+
circ_copy.add(list(measurements.values()))
532572
rotated_circuits.append(circ_copy)
533-
# for mapping the obtained sample frequencies to the original qubits
534-
qubit_maps.append(qubit_map)
535-
frequencies = [
536-
result.frequencies()
537-
for result in self.backend.execute_circuits(rotated_circuits, nshots=nshots)
538-
]
539-
return sum(
540-
coeff * obs.expectation_from_samples(freq, qubit_map)
541-
for coeff, freq, obs, qubit_map in zip(
542-
coefficients, frequencies, Z_observables, qubit_maps
543-
)
544-
)
573+
574+
# execute the circuits
575+
results = self.backend.execute_circuits(rotated_circuits, nshots=nshots)
576+
577+
# construct the expectation value for each diagonal term
578+
# and sum all together
579+
expval = 0.0
580+
for res, obs, qmap in zip(results, Z_observables, qubit_maps):
581+
freq = res.frequencies()
582+
expval += sum(o.expectation_from_samples(freq, qmap) for o in obs)
583+
return self.constant + expval
545584

546585
def expectation_from_samples(self, freq: dict, qubit_map: list = None) -> float:
547586
"""

src/qibo/hamiltonians/terms.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -208,12 +208,20 @@ def matrix(self):
208208
where ``ntargets`` is the number of qubits included in the factors
209209
of this term.
210210
"""
211-
matrices = [
212-
reduce(self.backend.np.matmul, self.matrix_map.get(q))
213-
for q in self.target_qubits
214-
]
211+
matrices = list(self.qubit_to_matrix_map.values())
215212
return complex(self.coefficient) * reduce(self.backend.np.kron, matrices)
216213

214+
@cached_property
215+
def qubit_to_matrix_map(self) -> dict:
216+
"""Dictionary mapping each qubit to the corresponding global matrix
217+
that acts on it, i.e. the product of the matrices of all the factors
218+
acting on it.
219+
"""
220+
return {
221+
q: reduce(self.backend.np.matmul, self.matrix_map.get(q))
222+
for q in self.target_qubits
223+
}
224+
217225
def copy(self):
218226
"""Creates a shallow copy of the term with the same attributes."""
219227
new = self.__class__(self.coefficient)
@@ -236,6 +244,15 @@ def __call__(self, backend, state, nqubits, density_matrix=False):
236244
)
237245
return self.coefficient * state
238246

247+
def commute(self, term) -> bool:
248+
"""Check whether this term commutes with another term."""
249+
for q in set(self.target_qubits).intersection(set(term.target_qubits)):
250+
m1 = self.qubit_to_matrix_map.get(q)
251+
m2 = term.qubit_to_matrix_map.get(q)
252+
if not self.backend.np.all(m1 @ m2 == m2 @ m1):
253+
return False
254+
return True
255+
239256

240257
class TermGroup(list):
241258
"""Collection of multiple :class:`qibo.hamiltonians.terms.HamiltonianTerm` objects.

tests/test_hamiltonians.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -305,10 +305,13 @@ def test_hamiltonian_expectation_from_samples_with_some_zero_counts(backend, qma
305305

306306
def test_hamiltonian_expectation_from_circuit(backend):
307307
"""Test Hamiltonian expectation value calculation."""
308-
backend.set_seed(12)
308+
seed = 12
309+
backend.set_seed(seed)
309310

310311
nshots = 4 * 10**6
311-
observable = I(0) * Z(1) + X(0) * Z(1) + Y(0) * X(2) / 2 - Z(0) * (1 - Y(1)) ** 3
312+
observable = (
313+
3.14 + I(0) * Z(1) + X(0) * Z(1) + Y(0) * X(2) / 2 - Z(0) * (1 - Y(1)) ** 3
314+
)
312315
exp, H, c = non_exact_expectation_test_setup(backend, observable)
313316
exp_from_samples = H.expectation_from_circuit(c, nshots=nshots)
314317
backend.assert_allclose(exp, exp_from_samples, atol=1e-2)

0 commit comments

Comments
 (0)