feat: add method to compute expectation values from symbolic form of hamiltonians
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
"""Implementation of Quantum Matcha Tea backend."""
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
|
||||
import numpy as np
|
||||
import qiskit
|
||||
import qmatchatea
|
||||
import qtealeaves
|
||||
@@ -53,31 +55,38 @@ class QMatchaTeaBackend(QibotnBackend):
|
||||
prob_type="U",
|
||||
**prob_kwargs,
|
||||
):
|
||||
"""Execute a Qibo quantum circuit through a tensor network simulation.
|
||||
The execution returns a ``TensorNetworkResults`` object, which can be
|
||||
used to reconstruct the system state (in the system size is < 30), the
|
||||
frequencies (if a number of shots is provided) and the probabibilities.
|
||||
Different methods are available for the probabilities computation,
|
||||
according.
|
||||
"""Execute a Qibo quantum circuit using tensor network simulation.
|
||||
|
||||
to the Quantum Matcha Tea implementation; in particular:
|
||||
- "E": even probability measure, where probabilities are measured going down evenly on the
|
||||
probability tree, and you neglect a branch (a state) if the probability is too low;
|
||||
- "G": greedy probability measure, where you follow the state going from the most probable to
|
||||
the least probable, until you reach a given coverage (sum of probabilities);
|
||||
- "U": optimal probability measure, called unbiased, since differently from the previous
|
||||
methods it is unbiased. The explanation of this one is
|
||||
a bit tough, but it is the best possible. See https://arxiv.org/abs/2401.10330.
|
||||
This method returns a ``TensorNetworkResult`` object, which provides:
|
||||
- Reconstruction of the system state (if the system size is < 20).
|
||||
- Frequencies (if the number of shots is specified).
|
||||
- Probabilities computed using various methods.
|
||||
|
||||
The following probability computation methods are available, as implemented
|
||||
in Quantum Matcha Tea:
|
||||
- **"E" (Even):** Probabilities are computed by evenly descending the probability tree,
|
||||
pruning branches (states) with probabilities below a threshold.
|
||||
- **"G" (Greedy):** Probabilities are computed by following the most probable states
|
||||
in descending order until reaching a given coverage (sum of probabilities).
|
||||
- **"U" (Unbiased):** An optimal probability measure that is unbiased and designed
|
||||
for best performance. See https://arxiv.org/abs/2401.10330 for details.
|
||||
|
||||
Args:
|
||||
circuit: the Qibo circuit we want to execute;
|
||||
initial_state: the initial state, usually the vacuum in tensor network
|
||||
simulations;
|
||||
nshots: number of shots, if shot-noise simulation is performed;
|
||||
prob_type: it can be "E", "G" or "U" (default);
|
||||
prob_kwargs: extra parameters required by qmatchatea to compute the
|
||||
probabilities. "U" requires ``num_samples`` while "E" and "G" require
|
||||
``prob_threshold``.
|
||||
circuit: A Qibo circuit to execute.
|
||||
initial_state: The initial state of the system (default is the vacuum state
|
||||
for tensor network simulations).
|
||||
nshots: The number of shots for shot-noise simulation (optional).
|
||||
prob_type: The probability computation method. Must be one of:
|
||||
- "E" (Even)
|
||||
- "G" (Greedy)
|
||||
- "U" (Unbiased) [default].
|
||||
prob_kwargs: Additional parameters required for probability computation:
|
||||
- For "U", requires ``num_samples``.
|
||||
- For "E" and "G", requires ``prob_threshold``.
|
||||
|
||||
Returns:
|
||||
TensorNetworkResult: An object with methods to reconstruct the state,
|
||||
compute probabilities, and generate frequencies.
|
||||
"""
|
||||
|
||||
# TODO: verify if the QCIO mechanism of matcha is supported by Fortran only
|
||||
@@ -89,6 +98,9 @@ class QMatchaTeaBackend(QibotnBackend):
|
||||
f"Backend {self.name}-{self.platform} currently does not support initial state.",
|
||||
)
|
||||
|
||||
# To be sure the setup is correct and no modifications have been done
|
||||
self._setup_qmatchatea_backend()
|
||||
|
||||
# TODO: check
|
||||
circuit = self._qibocirc_to_qiskitcirc(circuit)
|
||||
run_qk_params = qmatchatea.preprocessing.qk_transpilation_params(False)
|
||||
@@ -117,7 +129,7 @@ class QMatchaTeaBackend(QibotnBackend):
|
||||
observables=observables,
|
||||
)
|
||||
|
||||
if circuit.num_qubits < 30:
|
||||
if circuit.num_qubits < 20:
|
||||
statevector = results.statevector
|
||||
else:
|
||||
statevector = None
|
||||
@@ -131,6 +143,47 @@ class QMatchaTeaBackend(QibotnBackend):
|
||||
statevector=statevector,
|
||||
)
|
||||
|
||||
def expectation(self, circuit, observable):
|
||||
"""Compute the expectation value of a Qibo-friendly ``observable`` on
|
||||
the Tensor Network constructed from a Qibo ``circuit``.
|
||||
|
||||
This method takes a Qibo-style symbolic Hamiltonian (e.g., `X(0)*Z(1) + 2.0*Y(2)*Z(0)`)
|
||||
as the observable, converts it into a Quantum Matcha Tea (qmatchatea) observable
|
||||
(using `TNObsTensorProduct` and `TNObsWeightedSum`), and computes its expectation
|
||||
value using the provided circuit.
|
||||
|
||||
Args:
|
||||
circuit: A Qibo quantum circuit object on which the expectation value
|
||||
is computed. The circuit should be compatible with the qmatchatea
|
||||
Tensor Network backend.
|
||||
observable: The observable whose expectation value we want to compute.
|
||||
This must be provided in the symbolic Hamiltonian form supported by Qibo
|
||||
(e.g., `X(0)*Y(1)` or `Z(0)*Z(1) + 1.5*Y(2)`).
|
||||
|
||||
Returns:
|
||||
qmatchatea.SimulationResult [TEMPORARY]
|
||||
"""
|
||||
|
||||
# From Qibo to Qiskit
|
||||
circuit = self._qibocirc_to_qiskitcirc(circuit)
|
||||
run_qk_params = qmatchatea.preprocessing.qk_transpilation_params(False)
|
||||
|
||||
operators = qmatchatea.QCOperators()
|
||||
observables = qtealeaves.observables.TNObservables()
|
||||
# Add custom observable
|
||||
observables += self._qiboobs_to_qmatchaobs(hamiltonian_form=observable)
|
||||
|
||||
results = qmatchatea.run_simulation(
|
||||
circ=circuit,
|
||||
convergence_parameters=self.convergence_params,
|
||||
transpilation_parameters=run_qk_params,
|
||||
backend=self.qmatchatea_backend,
|
||||
observables=observables,
|
||||
operators=operators,
|
||||
)
|
||||
|
||||
return np.real(results.observables["custom_hamiltonian"])
|
||||
|
||||
def configure_tn_simulation(
|
||||
self,
|
||||
ansatz: str = "MPS",
|
||||
@@ -187,3 +240,93 @@ class QMatchaTeaBackend(QibotnBackend):
|
||||
qk_params=qmatchatea.preprocessing.qk_transpilation_params(),
|
||||
)
|
||||
return qiskit_circuit
|
||||
|
||||
def _qiboobs_to_qmatchaobs(
|
||||
self, hamiltonian_form, observable_name="custom_hamiltonian"
|
||||
):
|
||||
"""Convert a Qibo-style symbolic expression (e.g. '2.0*Y2*Z0 + Z0*Z2')
|
||||
into a qmatchatea ``TNObsWeightedSum`` observable.
|
||||
|
||||
The parsing logic here assumes:
|
||||
- Each term may have an optional leading coefficient (defaults to 1.0).
|
||||
- Each operator is a single-letter from [XYZI] plus a qubit index (e.g., 'X2' means X on qubit 2).
|
||||
- Terms are separated by '+' (and optionally '-') signs. If negative, we parse it as a negative coefficient.
|
||||
|
||||
Args:
|
||||
hamiltonian_form: e.g. 'Y2*Z0 + 2.5*Z0*Z2'
|
||||
observable_name (str): A name for the resulting ``TNObsWeightedSum``.
|
||||
|
||||
Returns:
|
||||
TNObsWeightedSum: An observable suitable for qmatchatea.
|
||||
"""
|
||||
hamiltonian_form = str(hamiltonian_form)
|
||||
|
||||
# Collect all the simple terms in the string and preserve the sign
|
||||
# whenever a coefficient is negative
|
||||
hamiltonian_form = hamiltonian_form.replace("-", "+-")
|
||||
raw_terms = [t.strip() for t in hamiltonian_form.split("+") if t.strip()]
|
||||
|
||||
coeff_list = []
|
||||
|
||||
# Regex for leading coefficient: e.g. "2.5*" or "-0.3*"
|
||||
# group(1) will capture the numeric part, group(0) includes the sign if present
|
||||
leading_coeff_pattern = re.compile(r"^([+-]?\d+(\.\d+)?)\*")
|
||||
|
||||
for i, hamiltonian_term in enumerate(raw_terms):
|
||||
# Set default coefficient to 1.0
|
||||
coeff = 1.0
|
||||
# Look for a leading numeric coefficient
|
||||
match = leading_coeff_pattern.search(hamiltonian_term)
|
||||
|
||||
if match:
|
||||
# Parse that coefficient
|
||||
coeff = float(match.group(1))
|
||||
|
||||
# Remove that portion from the term string so only operators remain
|
||||
hamiltonian_term = leading_coeff_pattern.sub(
|
||||
"", hamiltonian_term, count=1
|
||||
)
|
||||
|
||||
# Now isolate the single terms in the product (if there are more than 1)
|
||||
operators_qubits = hamiltonian_term.split("*")
|
||||
|
||||
# Prepare lists for qmatchatea
|
||||
operator_names, acting_on_qubits = [], []
|
||||
|
||||
# Each sub-term is e.g. "Y2", so operator = "Y", qubit = 2
|
||||
# We assume the operator is the single letter, the rest is the qubit index
|
||||
for operator in operators_qubits:
|
||||
operator = operator.strip()
|
||||
|
||||
# Use a regex to split the operator and the qubit index
|
||||
match = re.match(r"([^\d]+)(\d+)", operator)
|
||||
if match:
|
||||
operator_name = match.group(
|
||||
1
|
||||
) # All characters before the number (e.g., 'XYZ')
|
||||
qubit_index = int(match.group(2)) # The number part (e.g., 2)
|
||||
|
||||
operator_names.append(operator_name)
|
||||
acting_on_qubits.append([qubit_index])
|
||||
|
||||
# Build collection of tensor product operators (tpo)
|
||||
if i == 0:
|
||||
tpo = qtealeaves.observables.TNObsTensorProduct(
|
||||
name=f"{hamiltonian_term}",
|
||||
operators=operator_names,
|
||||
sites=acting_on_qubits,
|
||||
)
|
||||
else:
|
||||
tpo += qtealeaves.observables.TNObsTensorProduct(
|
||||
name=f"{hamiltonian_term}",
|
||||
operators=operator_names,
|
||||
sites=acting_on_qubits,
|
||||
)
|
||||
# And also keep track of coefficients
|
||||
coeff_list.append(coeff)
|
||||
|
||||
# Combine everything into a WeightedSum
|
||||
obs_sum = qtealeaves.observables.TNObsWeightedSum(
|
||||
name=observable_name, tp_operators=tpo, coeffs=coeff_list, use_itpo=False
|
||||
)
|
||||
return obs_sum
|
||||
|
||||
Reference in New Issue
Block a user