From 85aa542eb6414434859708e36a9d923256a139d6 Mon Sep 17 00:00:00 2001 From: HGSilveri Date: Fri, 6 Dec 2024 16:44:28 +0100 Subject: [PATCH] Definition of QutipState --- .../pulser_simulation/__init__.py | 2 + .../pulser_simulation/qutip_state.py | 200 ++++++++++++++++++ 2 files changed, 202 insertions(+) create mode 100644 pulser-simulation/pulser_simulation/qutip_state.py diff --git a/pulser-simulation/pulser_simulation/__init__.py b/pulser-simulation/pulser_simulation/__init__.py index cddf5e04f..e0966bc11 100644 --- a/pulser-simulation/pulser_simulation/__init__.py +++ b/pulser-simulation/pulser_simulation/__init__.py @@ -16,6 +16,7 @@ from pulser import EmulatorConfig, NoiseModel from pulser_simulation._version import __version__ as __version__ +from pulser_simulation.qutip_state import QutipState from pulser_simulation.qutip_backend import QutipBackend from pulser_simulation.simconfig import SimConfig from pulser_simulation.simulation import QutipEmulator @@ -23,6 +24,7 @@ __all__ = [ "EmulatorConfig", "NoiseModel", + "QutipState", "QutipBackend", "QutipEmulator", "SimConfig", diff --git a/pulser-simulation/pulser_simulation/qutip_state.py b/pulser-simulation/pulser_simulation/qutip_state.py new file mode 100644 index 000000000..c652d1986 --- /dev/null +++ b/pulser-simulation/pulser_simulation/qutip_state.py @@ -0,0 +1,200 @@ +# Copyright 2024 Pulser Development Team +# +# 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. +"""Definition of QutipState and QutipOperator.""" +from __future__ import annotations + +import math +from collections.abc import Collection, Mapping, Sequence +from typing import SupportsComplex, Type, TypeVar + +import numpy as np +import qutip +import qutip.qobj + +from pulser.backend.state import Eigenstate, State + +QutipStateType = TypeVar("QutipStateType", bound="QutipState") + +QuditOp = Mapping[str, SupportsComplex] +TensorOp = Sequence[tuple[QuditOp, Collection[int]]] +FullOp = Sequence[tuple[SupportsComplex, TensorOp]] + + +class QutipState(State[SupportsComplex, complex]): + """A quantum state stored as a qutip.Qobj. + + Args: + state: The state as a qutip.Qobj. Can be a statevector + or a density matrix. + eigenstates: The eigenstates that form a qudit's eigenbasis, each + given as an individual character. The order of the eigenstates + matters, as for eigenstates ("a", "b", ...), "a" will be + associated to eigenvector (1, 0, ...), "b" to (0, 1, ...) and + so on. + """ + + def __init__( + self, state: qutip.Qobj, *, eigenstates: Sequence[Eigenstate] + ): + """Initializes a QutipState.""" + self._validate_eigenstates(eigenstates) + self._eigenstates = tuple(eigenstates) + valid_types = ("ket", "bra", "oper") + if not isinstance(state, qutip.Qobj) or state.type not in valid_types: + raise TypeError( + "'state' must be a qutip.Qobj with one of types " + f"{valid_types}, not {state!r}." + ) + self._state = state.dag() if state.isbra else state + self._validate_shape(self._state.shape, self.qudit_dim) + + @property + def n_qudits(self) -> int: + """The number of qudits in the state.""" + return round(math.log(self._state.shape[0], self.qudit_dim)) + + def overlap(self, other: QutipState) -> float: + """Compute the overlap between this state and another of the same type. + + Generally computes Tr[AB] for mixed states A and B, which + corresponds to ||^2 for pure states A=|a> dict[str, float]: + """Extracts the probabilties of measuring each basis state combination. + + Args: + cutoff: The value below which a probability is considered to be + zero. + + Returns: + A mapping between basis state combinations and their respective + probabilities. + """ + if not self._state.isket: + probs = np.abs(self._state.diag()) + else: + probs = (np.abs(self._state.full()) ** 2).flatten() + non_zero = probs > cutoff + return dict( + zip(np.array(self.basis_states)[non_zero], probs[non_zero]) + ) + + @classmethod + def from_state_amplitudes( + cls: Type[QutipStateType], + *, + eigenstates: Sequence[Eigenstate], + amplitudes: dict[str, SupportsComplex], + ) -> QutipStateType: + """Construct the state from its basis states' amplitudes. + + Args: + eigenstates: The basis states (e.g., ('r', 'g')). + amplitudes: A mapping between basis state combinations and + complex amplitudes. + + Returns: + The state constructed from the amplitudes. + """ + cls._validate_eigenstates(eigenstates) + basis_states = list(amplitudes) + n_qudits = len(basis_states[0]) + if not all( + len(bs) == n_qudits and set(bs) <= set(eigenstates) + for bs in basis_states + ): + raise ValueError( + "All basis states must be combinations of eigenstates with the" + f" same length. Expected combinations of {eigenstates}, each " + f" with {n_qudits} elements." + ) + + qudit_dim = len(eigenstates) + + def make_qobj(basis_state: str) -> qutip.Qobj: + return qutip.tensor( + [ + qutip.basis(qudit_dim, eigenstates.index(s)) + for s in basis_state + ] + ) + + # Start with an empty Qobj with the right dimension + state = make_qobj(eigenstates[0] * n_qudits) * 0 + for basis_state, amp in amplitudes.items(): + state += complex(amp) * make_qobj(basis_state) + + return cls(state, eigenstates=eigenstates) + + def __repr__(self) -> str: + return "\n".join( + [ + "QutipState", + "----------", + f"Eigenstates: {self.eigenstates}", + self._state.__repr__(), + ] + ) + + @staticmethod + def _validate_eigenstates(eigenstates: Sequence[Eigenstate]) -> None: + if any(not isinstance(s, str) or len(s) != 1 for s in eigenstates): + raise ValueError( + "All eigenstates must be represented by single characters." + ) + if len(eigenstates) != len(set(eigenstates)): + raise ValueError("'eigenstates' can't contain repeated entries.") + + @staticmethod + def _validate_shape(shape: tuple[int, int], qudit_dim: int) -> None: + expected_n_qudits = math.log(shape[0], qudit_dim) + if not np.isclose(expected_n_qudits, round(expected_n_qudits)): + raise ValueError( + f"A qutip.Qobj with shape {shape} can't represent " + f"a collection of {qudit_dim}-level qudits." + )