# Copyright 2019-2024 Cambridge Quantum Computing
#
# 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.
from typing import TYPE_CHECKING
import numpy as np
from pytket.circuit import Circuit, Qubit
from pytket.partition import (
GraphColourMethod,
PauliPartitionStrat,
measurement_reduction,
)
from pytket.pauli import QubitPauliString
from .measurements import _all_pauli_measurements, append_pauli_measurement
from .operators import QubitPauliOperator
from .results import KwargTypes
if TYPE_CHECKING:
from pytket.backends.backend import Backend
[docs]
def expectation_from_shots(shot_table: np.ndarray) -> float:
"""Estimates the expectation value of a circuit from its shots.
Computes the parity of '1's across all bits to determine a +1 or -1 contribution
from each row, and returns the average.
:param shot_table: The table of shots to interpret.
:type shot_table: np.ndarray
:return: The expectation value in the range [-1, 1].
:rtype: float
"""
aritysum = 0.0
for row in shot_table:
aritysum += np.sum(row) % 2
return -2 * aritysum / len(shot_table) + 1
[docs]
def expectation_from_counts(counts: dict[tuple[int, ...], int]) -> float:
"""Estimates the expectation value of a circuit from shot counts.
Computes the parity of '1's across all bits to determine a +1 or -1 contribution
from each readout, and returns the weighted average.
:param counts: Counts of each measurement outcome observed.
:type counts: Dict[Tuple[int, ...], int]
:return: The expectation value in the range [-1, 1].
:rtype: float
"""
aritysum = 0.0
total_shots = 0
for row, count in counts.items():
aritysum += count * (sum(row) % 2)
total_shots += count
return -2 * aritysum / total_shots + 1
def _default_index(q: Qubit) -> int:
if q.reg_name != "q" or len(q.index) != 1:
raise ValueError("Non-default qubit register")
return int(q.index[0])
[docs]
def get_pauli_expectation_value(
state_circuit: Circuit,
pauli: QubitPauliString,
backend: "Backend",
n_shots: int | None = None,
) -> complex:
"""Estimates the expectation value of the given circuit with respect to the Pauli
term by preparing measurements in the appropriate basis, running on the backend and
interpreting the counts/statevector
:param state_circuit: Circuit that generates the desired state
:math:`\\left|\\psi\\right>`.
:type state_circuit: Circuit
:param pauli: Pauli operator
:type pauli: QubitPauliString
:param backend: pytket backend to run circuit on.
:type backend: Backend
:param n_shots: Number of shots to run if backend supports shots/counts. Set to None
to calculate using statevector if supported by the backend. Defaults to None
:type n_shots: Optional[int], optional
:return: :math:`\\left<\\psi | P | \\psi \\right>`
:rtype: float
"""
if not n_shots:
if not backend.valid_circuit(state_circuit):
state_circuit = backend.get_compiled_circuit(state_circuit)
if backend.supports_expectation:
return backend.get_pauli_expectation_value(state_circuit, pauli)
state = backend.run_circuit(state_circuit).get_state()
return complex(pauli.state_expectation(state))
measured_circ = state_circuit.copy()
append_pauli_measurement(pauli, measured_circ)
measured_circ = backend.get_compiled_circuit(measured_circ)
if backend.supports_counts:
counts = backend.run_circuit(measured_circ, n_shots=n_shots).get_counts()
return expectation_from_counts(counts)
if backend.supports_shots:
shot_table = backend.run_circuit(measured_circ, n_shots=n_shots).get_shots()
return expectation_from_shots(shot_table)
raise ValueError("Backend does not support counts or shots")
[docs]
def get_operator_expectation_value(
state_circuit: Circuit,
operator: QubitPauliOperator,
backend: "Backend",
n_shots: int | None = None,
partition_strat: PauliPartitionStrat | None = None,
colour_method: GraphColourMethod = GraphColourMethod.LargestFirst,
**kwargs: KwargTypes,
) -> complex:
"""Estimates the expectation value of the given circuit with respect to the operator
based on its individual Pauli terms. If the QubitPauliOperator has symbolic values
the expectation value will also be symbolic. The input circuit must belong to the
default qubit register and have contiguous qubit ordering.
:param state_circuit: Circuit that generates the desired state
:math:`\\left|\\psi\\right>`
:type state_circuit: Circuit
:param operator: Operator :math:`H`. Currently does not support free symbols for the
purpose of obtaining expectation values.
:type operator: QubitPauliOperator
:param backend: pytket backend to run circuit on.
:type backend: Backend
:param n_shots: Number of shots to run if backend supports shots/counts. None will
force the backend to give the full state if available. Defaults to None
:type n_shots: Optional[int], optional
:param partition_strat: If retrieving shots, can perform measurement reduction using
a chosen strategy
:type partition_strat: Optional[PauliPartitionStrat], optional
:return: :math:`\\left<\\psi | H | \\psi \\right>`
:rtype: complex
"""
if not n_shots:
if not backend.valid_circuit(state_circuit):
state_circuit = backend.get_compiled_circuit(state_circuit)
try:
coeffs: list[complex] = [complex(v) for v in operator._dict.values()]
except TypeError:
raise ValueError("QubitPauliOperator contains unevaluated symbols.")
if backend.supports_expectation and (
backend.expectation_allows_nonhermitian or all(z.imag == 0 for z in coeffs)
):
return backend.get_operator_expectation_value(state_circuit, operator)
result = backend.run_circuit(state_circuit)
state = result.get_state()
return operator.state_expectation(state)
energy: complex
id_string = QubitPauliString()
if id_string in operator._dict:
energy = complex(operator[id_string])
else:
energy = 0
if not partition_strat:
operator_without_id = QubitPauliOperator(
{p: c for p, c in operator._dict.items() if (p != id_string)}
)
coeffs = [complex(c) for c in operator_without_id._dict.values()]
pauli_circuits = list(
_all_pauli_measurements(operator_without_id, state_circuit)
)
handles = backend.process_circuits(
backend.get_compiled_circuits(pauli_circuits),
n_shots,
valid_check=True,
**kwargs,
)
results = backend.get_results(handles)
if backend.supports_counts:
for result, coeff in zip(results, coeffs):
counts = result.get_counts()
energy += coeff * expectation_from_counts(counts)
for handle in handles:
backend.pop_result(handle)
return energy
if backend.supports_shots:
for result, coeff in zip(results, coeffs):
shots = result.get_shots()
energy += coeff * expectation_from_shots(shots)
for handle in handles:
backend.pop_result(handle)
return energy
raise ValueError("Backend does not support counts or shots")
qubit_pauli_string_list = [p for p in operator._dict.keys() if (p != id_string)]
measurement_expectation = measurement_reduction(
qubit_pauli_string_list, partition_strat, colour_method
)
# note: this implementation requires storing all the results
# in memory simultaneously to filter through them.
measure_circs = []
for pauli_circ in measurement_expectation.measurement_circs:
circ = state_circuit.copy()
circ.append(pauli_circ)
measure_circs.append(circ)
handles = backend.process_circuits(
backend.get_compiled_circuits(measure_circs),
n_shots=n_shots,
valid_check=True,
**kwargs,
)
results = backend.get_results(handles)
for pauli_string in measurement_expectation.results:
bitmaps = measurement_expectation.results[pauli_string]
string_coeff = operator[pauli_string]
for bm in bitmaps:
index = bm.circ_index
aritysum = 0.0
if backend.supports_counts:
counts = results[index].get_counts()
total_shots = 0
for row, count in counts.items():
aritysum += count * (sum(row[i] for i in bm.bits) % 2)
total_shots += count
e = (
((-1) ** bm.invert)
* string_coeff
* (-2 * aritysum / total_shots + 1)
)
energy += complex(e)
elif backend.supports_shots:
shots = results[index].get_shots()
for row in shots:
aritysum += sum(row[i] for i in bm.bits) % 2
e = (
((-1) ** bm.invert)
* string_coeff
* (-2 * aritysum / len(shots) + 1)
)
energy += complex(e)
else:
raise ValueError("Backend does not support counts or shots")
for handle in handles:
backend.pop_result(handle)
return energy