318 lines
12 KiB
Python
318 lines
12 KiB
Python
"""Implementation of Quantum Matcha Tea backend."""
|
|
|
|
import re
|
|
from dataclasses import dataclass
|
|
|
|
import numpy as np
|
|
import qiskit
|
|
import qmatchatea
|
|
import qtealeaves
|
|
from qibo.backends import NumpyBackend
|
|
from qibo.config import raise_error
|
|
|
|
from qibotn.backends.abstract import QibotnBackend
|
|
from qibotn.result import TensorNetworkResult
|
|
|
|
|
|
@dataclass
|
|
class QMatchaTeaBackend(QibotnBackend, NumpyBackend):
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
|
|
self.name = "qibotn"
|
|
self.platform = "qmatchatea"
|
|
|
|
# Set default configurations
|
|
self.configure_tn_simulation()
|
|
self._setup_backend_specifics()
|
|
|
|
def configure_tn_simulation(
|
|
self,
|
|
ansatz: str = "MPS",
|
|
convergence_params=None,
|
|
):
|
|
"""Configure TN simulation given Quantum Matcha Tea interface.
|
|
|
|
Args:
|
|
ansatz (str): tensor network ansatz. It can be tree tensor network "TTN"
|
|
or Matrix Product States "MPS" (default).
|
|
convergence_params (qmatchatea.utils.QCConvergenceParameters):
|
|
convergence parameters class adapted to the quantum computing
|
|
execution. See https://baltig.infn.it/quantum_matcha_tea/py_api_quantum_matcha_tea/-/blob/master/qmatchatea/utils/utils.py?ref_type=heads#L540
|
|
for more instructions. If not passed, the default values proposed
|
|
by Quantum Matcha Tea's authors are set.
|
|
"""
|
|
|
|
# Set configurations or defaults
|
|
self.convergence_params = (
|
|
convergence_params or qmatchatea.QCConvergenceParameters()
|
|
)
|
|
self.ansatz = ansatz
|
|
|
|
def _setup_backend_specifics(self):
|
|
"""Configure qmatchatea QCBackend object."""
|
|
|
|
qmatchatea_device = (
|
|
"cpu" if "CPU" in self.device else "gpu" if "GPU" in self.device else None
|
|
)
|
|
qmatchatea_precision = (
|
|
"C"
|
|
if self.precision == "single"
|
|
else "Z" if self.precision == "double" else "A"
|
|
)
|
|
|
|
# TODO: once MPI is available for Python, integrate it here
|
|
self.qmatchatea_backend = qmatchatea.QCBackend(
|
|
backend="PY", # The only alternative is Fortran, but we use Python here
|
|
precision=qmatchatea_precision,
|
|
device=qmatchatea_device,
|
|
ansatz=self.ansatz,
|
|
)
|
|
|
|
def execute_circuit(
|
|
self,
|
|
circuit,
|
|
initial_state=None,
|
|
nshots=None,
|
|
prob_type=None,
|
|
return_array=False,
|
|
**prob_kwargs,
|
|
):
|
|
"""Execute a Qibo quantum circuit using tensor network simulation.
|
|
|
|
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: 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
|
|
# as written in the docstrings or by Python too (see ``io_info`` argument of
|
|
# ``qmatchatea.interface.run_simulation`` function)
|
|
if initial_state is not None:
|
|
raise_error(
|
|
NotImplementedError,
|
|
f"Backend {self.name}-{self.platform} currently does not support initial state.",
|
|
)
|
|
|
|
if prob_type == None:
|
|
prob_type = "U"
|
|
prob_kwargs = {"num_samples": 500}
|
|
|
|
# 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)
|
|
|
|
# Initialize the TNObservable object
|
|
observables = qtealeaves.observables.TNObservables()
|
|
|
|
# Shots
|
|
if nshots is not None:
|
|
observables += qtealeaves.observables.TNObsProjective(num_shots=nshots)
|
|
|
|
# Probabilities
|
|
observables += qtealeaves.observables.TNObsProbabilities(
|
|
prob_type=prob_type,
|
|
**prob_kwargs,
|
|
)
|
|
|
|
# State
|
|
observables += qtealeaves.observables.TNState2File(name="temp", formatting="U")
|
|
|
|
results = qmatchatea.run_simulation(
|
|
circ=circuit,
|
|
convergence_parameters=self.convergence_params,
|
|
transpilation_parameters=run_qk_params,
|
|
backend=self.qmatchatea_backend,
|
|
observables=observables,
|
|
)
|
|
|
|
if circuit.num_qubits < 20 and return_array:
|
|
statevector = results.statevector
|
|
else:
|
|
statevector = None
|
|
|
|
return TensorNetworkResult(
|
|
nqubits=circuit.num_qubits,
|
|
backend=self,
|
|
measures=results.measures,
|
|
measured_probabilities=results.measure_probabilities,
|
|
prob_type=prob_type,
|
|
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 _qibocirc_to_qiskitcirc(self, qibo_circuit) -> qiskit.QuantumCircuit:
|
|
"""Convert a Qibo Circuit into a Qiskit Circuit."""
|
|
# Convert the circuit to QASM 2.0 to qiskit
|
|
qasm_circuit = qibo_circuit.to_qasm()
|
|
qiskit_circuit = qiskit.QuantumCircuit.from_qasm_str(qasm_circuit)
|
|
|
|
# Transpile the circuit to adapt it to the linear structure of the MPS,
|
|
# with the constraint of having only the gates basis_gates
|
|
qiskit_circuit = qmatchatea.preprocessing.preprocess(
|
|
qiskit_circuit,
|
|
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
|