from __future__ import annotations
import logging
import os
import string
from abc import ABC, abstractmethod
from collections import Counter
from typing import Any, Union
import numpy as np
import pulser
import torch
from pulser.backend import EmulationConfig
from pulser.backends import QutipBackendV2 as PulserQutipBackend
from pulser.sequence.sequence import Sequence as PulserSequence
from qoolqit.execution.utils import BackendName, ResultType
AVAILABLE_BACKENDS = {BackendName.QUTIP: PulserQutipBackend}
AVAILABLE_CONFIGS = {BackendName.QUTIP: EmulationConfig}
if os.name == "posix":
# As of this writing, EmuMPS doesn't work under Windows.
from emu_mps import MPSBackend, MPSConfig
AVAILABLE_BACKENDS[BackendName.EMUMPS] = MPSBackend
AVAILABLE_CONFIGS[BackendName.EMUMPS] = MPSConfig
OutputType = Union[np.ndarray, list[Counter]]
class BaseBackend(ABC):
def __init__(
self,
seq: PulserSequence,
name: BackendName = BackendName.QUTIP,
result_type: ResultType = ResultType.STATEVECTOR,
**backend_params: Any,
) -> None:
self.seq = seq
self.name = name
self.result_type = result_type
self.backend_params = backend_params
# Get the selected backend
self.backend_cls = AVAILABLE_BACKENDS[name]
# Get the appropriate config
self.config_cls = AVAILABLE_CONFIGS[name]
def build_config(self, runs: int = 100, evaluation_times: list[float] = [1.0]) -> None:
# Add the necessary observables based on the expected result type
obs = self.backend_params.get("observables", [])
if len(obs) == 0:
if self.result_type == ResultType.BITSTRINGS:
obs.append(
pulser.backend.BitStrings(evaluation_times=evaluation_times, num_shots=runs)
)
elif self.result_type == ResultType.STATEVECTOR:
obs.append(pulser.backend.StateResult(evaluation_times=evaluation_times))
self.backend_params["observables"] = obs
# Set default values for the config
self.backend_params.setdefault("log_level", logging.WARNING)
# Build the config object
self.config = self.config_cls(**self.backend_params)
def build_backend(self) -> None:
# Build the local backend
self.backend = self.backend_cls(self.seq, config=self.config)
@abstractmethod
def run(self) -> Any:
pass
if os.name == "posix":
import emu_mps
class EmuMPSBackend(BaseBackend):
"""Emu-MPS backend."""
def __init__(
self,
seq: PulserSequence,
result_type: ResultType = ResultType.STATEVECTOR,
**backend_params: Any,
):
super().__init__(seq, BackendName.EMUMPS, result_type, **backend_params)
def contract_mps(self, mps_state: emu_mps.MPS) -> torch.Tensor:
"""
Contract a MPS state into a full state vector.
Args:
mps_state (MPS): MPS state to contract
Returns:
A flattened torch.Tensor representing the state vector.
"""
n = len(mps_state.factors)
# Use ascii letters to build einsum subscripts
letters = list(string.ascii_lowercase)
einsum_subs = []
for i in range(n):
left = letters[i]
phys = letters[n + i]
right = letters[i + 1]
einsum_subs.append(f"{left}{phys}{right}")
einsum_str = ",".join(einsum_subs) + "->" + "".join(letters[n : 2 * n])
result = torch.einsum(einsum_str, *mps_state.factors)
return result.flatten().cpu()
def run(self, runs: int = 100, evaluation_times: list[float] = [1.0]) -> OutputType:
# Build the config and the backend
self.build_config(runs, evaluation_times)
self.build_backend()
# run the simulation
result = self.backend.run()
# Get initial state vector
initial_state = self.backend._config.initial_state
if initial_state is None:
initial_state = emu_mps.MPS.from_state_amplitudes(
eigenstates=("r", "g"),
amplitudes={"g" * len(self.seq.register.qubits): 1.0},
)
if self.result_type == ResultType.STATEVECTOR:
# Constract MPS states to get state vectors
if len(evaluation_times) == 1:
state_vecs = [self.contract_mps(state) for state in result.state]
else:
state_vecs = [self.contract_mps(initial_state)] + [
self.contract_mps(state) for state in result.state
]
state_vecs = np.array(state_vecs)
return state_vecs
elif self.result_type == ResultType.BITSTRINGS:
if len(evaluation_times) == 1:
bitstrings = result.get_tagged_results()["bitstrings"]
else:
bitstrings = [
initial_state.sample(num_shots=runs)
] + result.get_tagged_results()["bitstrings"]
return bitstrings
class QutipBackend(BaseBackend):
"""Qutip backend."""
def __init__(
self,
seq: PulserSequence,
result_type: ResultType = ResultType.STATEVECTOR,
**backend_params: Any,
):
super().__init__(seq, BackendName.QUTIP, result_type, **backend_params)
def run(self, runs: int = 100, evaluation_times: list[float] = [1.0]) -> OutputType:
# Build the config and the backend
self.build_config(runs, evaluation_times)
self.build_backend()
# run the simulation
result = self.backend.run()
if self.result_type == ResultType.STATEVECTOR:
state_vecs = np.array(
[np.flip(state.to_qobj().full().flatten()) for state in result.state]
)
return state_vecs
elif self.result_type == ResultType.BITSTRINGS:
return result.get_tagged_results()["bitstrings"]