Skip to content

API specification

The emu-sv API is based on the specification here. Concretely, the classes are as follows:

SVBackend

Bases: EmulatorBackend

A backend for emulating Pulser sequences using state vectors and sparse matrices.

Source code in pulser/backend/abc.py
def __init__(
    self,
    sequence: pulser.Sequence,
    *,
    config: EmulationConfig | None = None,
    mimic_qpu: bool = False,
) -> None:
    """Initializes the backend."""
    super().__init__(sequence, mimic_qpu=mimic_qpu)
    config = config or self.default_config
    if not isinstance(config, EmulationConfig):
        raise TypeError(
            "'config' must be an instance of 'EmulationConfig', "
            f"not {type(config)}."
        )
    # See the BackendConfig definition to see why this works
    self._config = type(self.default_config)(**config._backend_options)

run()

Emulates the given sequence.

RETURNS DESCRIPTION
Results

the simulation results

Source code in emu_sv/sv_backend.py
def run(self) -> Results:
    """
    Emulates the given sequence.

    Returns:
        the simulation results
    """
    assert isinstance(self._config, SVConfig)

    pulser_data = PulserData(
        sequence=self._sequence, config=self._config, dt=self._config.dt
    )
    self.target_times = pulser_data.target_times
    self.time = time.time()
    omega, delta, phi = pulser_data.omega, pulser_data.delta, pulser_data.phi

    nsteps = omega.shape[0]
    nqubits = omega.shape[1]

    self.results = Results(atom_order=(), total_duration=self.target_times[-1])
    self.statistics = Statistics(
        evaluation_times=[t / self.target_times[-1] for t in self.target_times],
        data=[],
        timestep_count=nsteps,
    )

    if self._config.initial_state is not None:
        state = self._config.initial_state
        state = StateVector(state.vector.clone(), gpu=state.vector.is_cuda)
    else:
        state = StateVector.make(nqubits, gpu=self._config.gpu)

    for step in range(nsteps):
        dt = self.target_times[step + 1] - self.target_times[step]

        state.vector, H = do_time_step(
            dt * _TIME_CONVERSION_COEFF,
            omega[step],
            delta[step],
            phi[step],
            pulser_data.full_interaction_matrix,
            state.vector,
            self._config.krylov_tolerance,
        )

        # callbacks in observables and self.statistics in H
        # have "# type: ignore[arg-type]" because H has it's own type
        # meaning H is not inherited from Operator class.
        # We decided that ignore[arg-type] is better compared to
        # having many unused NotImplemented methods
        for callback in self._config.observables:
            callback(
                self._config,
                self.target_times[step + 1] / self.target_times[-1],
                state,
                H,  # type: ignore[arg-type]
                self.results,
            )

        self.statistics.data.append(time.time() - self.time)
        self.statistics(
            self._config,
            self.target_times[step + 1] / self.target_times[-1],
            state,
            H,  # type: ignore[arg-type]
            self.results,
        )
        self.time = time.time()

    return self.results

SVConfig

Bases: EmulationConfig

The configuration of the emu-sv SVBackend. The kwargs passed to this class are passed on to the base class. See the API for that class for a list of available options.

PARAMETER DESCRIPTION
dt

the timestep size that the solver uses. Note that observables are only calculated if the evaluation_times are divisible by dt.

TYPE: int DEFAULT: 10

max_krylov_dim

the size of the krylov subspace that the Lanczos algorithm maximally builds

TYPE: int DEFAULT: 100

krylov_tolerance

the Lanczos algorithm uses this as the convergence tolerance

TYPE: float DEFAULT: 1e-10

gpu

Use 1 gpu if True, and a GPU is available, otherwise, cpu. Will cause errors if True when a gpu is not available

TYPE: bool DEFAULT: True

kwargs

arguments that are passed to the base class

TYPE: Any DEFAULT: {}

Examples:

>>> gpu = True
>>> dt = 1 #this will impact the runtime
>>> krylov_tolerance = 1e-8 #the simulation will be faster, but less accurate
>>> SVConfig(gpu=gpu, dt=dt, krylov_tolerance=krylov_tolerance,
>>>     with_modulation=True) #the last arg is taken from the base class
Source code in emu_sv/sv_config.py
def __init__(
    self,
    *,
    dt: int = 10,
    max_krylov_dim: int = 100,
    krylov_tolerance: float = 1e-10,
    gpu: bool = True,
    interaction_cutoff: float = 0.0,
    log_level: int = logging.INFO,
    log_file: pathlib.Path | None = None,
    **kwargs: Any,
):
    kwargs.setdefault("observables", [BitStrings(evaluation_times=[1.0])])
    super().__init__(
        dt=dt,
        max_krylov_dim=max_krylov_dim,
        gpu=gpu,
        krylov_tolerance=krylov_tolerance,
        interaction_cutoff=interaction_cutoff,
        log_level=log_level,
        log_file=log_file,
        **kwargs,
    )

    self.monkeypatch_observables()

    self.logger = logging.getLogger("global_logger")
    if log_file is None:
        logging.basicConfig(
            level=log_level, format="%(message)s", stream=sys.stdout, force=True
        )  # default to stream = sys.stderr
    else:
        logging.basicConfig(
            level=log_level,
            format="%(message)s",
            filename=str(log_file),
            filemode="w",
            force=True,
        )
    if (self.noise_model.runs != 1 and self.noise_model.runs is not None) or (
        self.noise_model.samples_per_run != 1
        and self.noise_model.samples_per_run is not None
    ):
        self.logger.warning(
            "Warning: The runs and samples_per_run "
            "values of the NoiseModel are ignored!"
        )

StateVector

Bases: State[complex, Tensor]

Represents a quantum state vector in a computational basis.

This class extends the State class to handle state vectors, providing various utilities for initialization, normalization, manipulation, and measurement. The state vector must have a length that is a power of 2, representing 2ⁿ basis states for n qubits.

ATTRIBUTE DESCRIPTION
vector

1D tensor representation of a state vector.

gpu

store the vector on GPU if True, otherwise on CPU

Source code in emu_sv/state_vector.py
def __init__(
    self,
    vector: torch.Tensor,
    *,
    gpu: bool = True,
):
    # NOTE: this accepts also zero vectors.

    assert math.log2(
        len(vector)
    ).is_integer(), "The number of elements in the vector should be power of 2"

    device = "cuda" if gpu and DEVICE_COUNT > 0 else "cpu"
    self.vector = vector.to(dtype=dtype, device=device)

n_qudits property

The number of qudits in the state.

__add__(other)

Sum of two state vectors

PARAMETER DESCRIPTION
other

the vector to add to this vector

TYPE: State

RETURNS DESCRIPTION
StateVector

The summed state

Source code in emu_sv/state_vector.py
def __add__(self, other: State) -> StateVector:
    """Sum of two state vectors

    Args:
        other: the vector to add to this vector

    Returns:
        The summed state
    """
    assert isinstance(other, StateVector), "`Other` state can only be a StateVector"
    return StateVector(self.vector + other.vector, gpu=self.vector.is_cuda)

__rmul__(scalar)

Scalar multiplication

PARAMETER DESCRIPTION
scalar

the scalar to multiply with

TYPE: complex

RETURNS DESCRIPTION
StateVector

The scaled state

Source code in emu_sv/state_vector.py
def __rmul__(self, scalar: complex) -> StateVector:
    """Scalar multiplication

    Args:
        scalar: the scalar to multiply with

    Returns:
        The scaled state
    """
    return StateVector(scalar * self.vector, gpu=self.vector.is_cuda)

inner(other)

Compute . The type of other must be StateVector.

PARAMETER DESCRIPTION
other

the other state

TYPE: State

RETURNS DESCRIPTION
Tensor

the inner product

Source code in emu_sv/state_vector.py
def inner(self, other: State) -> torch.Tensor:
    """
    Compute <self|other>. The type of other must be StateVector.

    Args:
        other: the other state

    Returns:
        the inner product
    """
    assert isinstance(other, StateVector), "Other state must be a StateVector"
    assert (
        self.vector.shape == other.vector.shape
    ), "States do not have the same shape"

    # by our internal convention inner and norm return to cpu
    return torch.vdot(self.vector, other.vector).cpu()

make(num_sites, gpu=True) classmethod

Returns a State vector in the ground state |00..0>.

PARAMETER DESCRIPTION
num_sites

the number of qubits

TYPE: int

gpu

whether gpu or cpu

TYPE: bool DEFAULT: True

RETURNS DESCRIPTION
StateVector

The described state

Examples:

>>> StateVector.make(2,gpu=False)
tensor([1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j], dtype=torch.complex128)
Source code in emu_sv/state_vector.py
@classmethod
def make(cls, num_sites: int, gpu: bool = True) -> StateVector:
    """
    Returns a State vector in the ground state |00..0>.

    Args:
        num_sites: the number of qubits
        gpu: whether gpu or cpu

    Returns:
        The described state

    Examples:
        >>> StateVector.make(2,gpu=False)
        tensor([1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j], dtype=torch.complex128)


    """

    result = cls.zero(num_sites=num_sites, gpu=gpu)
    result.vector[0] = 1.0
    return result

norm()

Returns the norm of the state

RETURNS DESCRIPTION
Tensor

the norm of the state

Source code in emu_sv/state_vector.py
def norm(self) -> torch.Tensor:
    """Returns the norm of the state

    Returns:
        the norm of the state
    """
    nrm: torch.Tensor = torch.linalg.vector_norm(self.vector).cpu()
    return nrm

sample(*, num_shots=1000, one_state=None, p_false_pos=0.0, p_false_neg=0.0)

Samples bitstrings, taking into account the specified error rates.

PARAMETER DESCRIPTION
num_shots

how many bitstrings to sample

TYPE: int DEFAULT: 1000

p_false_pos

the rate at which a 0 is read as a 1

TYPE: float DEFAULT: 0.0

p_false_neg

teh rate at which a 1 is read as a 0

TYPE: float DEFAULT: 0.0

RETURNS DESCRIPTION
Counter[str]

the measured bitstrings, by count

Source code in emu_sv/state_vector.py
def sample(
    self,
    *,
    num_shots: int = 1000,
    one_state: Eigenstate | None = None,
    p_false_pos: float = 0.0,
    p_false_neg: float = 0.0,
) -> Counter[str]:
    """
    Samples bitstrings, taking into account the specified error rates.

    Args:
        num_shots: how many bitstrings to sample
        p_false_pos: the rate at which a 0 is read as a 1
        p_false_neg: teh rate at which a 1 is read as a 0

    Returns:
        the measured bitstrings, by count
    """
    assert p_false_neg == p_false_pos == 0.0, "Error rates must be 0.0"

    probabilities = torch.abs(self.vector) ** 2

    outcomes = torch.multinomial(probabilities, num_shots, replacement=True)

    # Convert outcomes to bitstrings and count occurrences
    counts = Counter(
        [index_to_bitstring(self.n_qudits, outcome) for outcome in outcomes]
    )

    # NOTE: false positives and negatives
    return counts

zero(num_sites, gpu=True) classmethod

Returns a zero uninitialized "state" vector. Warning, this has no physical meaning as-is!

PARAMETER DESCRIPTION
num_sites

the number of qubits

TYPE: int

gpu

whether gpu or cpu

TYPE: bool DEFAULT: True

RETURNS DESCRIPTION
StateVector

The zero state

Examples:

>>> StateVector.zero(2,gpu=False)
tensor([0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j], dtype=torch.complex128)
Source code in emu_sv/state_vector.py
@classmethod
def zero(cls, num_sites: int, gpu: bool = True) -> StateVector:
    """
    Returns a zero uninitialized "state" vector. Warning, this has no physical meaning as-is!

    Args:
        num_sites: the number of qubits
        gpu: whether gpu or cpu

    Returns:
        The zero state

    Examples:
        >>> StateVector.zero(2,gpu=False)
        tensor([0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j], dtype=torch.complex128)
    """

    device = "cuda" if gpu and DEVICE_COUNT > 0 else "cpu"
    vector = torch.zeros(2**num_sites, dtype=dtype, device=device)
    return cls(vector, gpu=gpu)

inner

Wrapper around StateVector.inner.

PARAMETER DESCRIPTION
left

StateVector argument

TYPE: StateVector

right

StateVector argument

TYPE: StateVector

RETURNS DESCRIPTION
Tensor

the inner product

Examples:

>>> factor = math.sqrt(2.0)
>>> basis = ("r","g")
>>> string_state1 = {"gg":1.0,"rr":1.0}
>>> state1 = StateVector.from_state_string(basis=basis,
    ... nqubits=nqubits,strings=string_state1)
>>> string_state2 = {"gr":1.0/factor,"rr":1.0/factor}
>>> state2 = StateVector.from_state_string(basis=basis,
    ... nqubits=nqubits,strings=string_state2)
>>> state1 = StateVector.from_state_amplitudes(eigenstates=basis,
...     amplitudes=string_state1)
>>> string_state2 = {"gr":1.0/factor,"rr":1.0/factor}
>>> state2 = StateVector.from_state_amplitudes(eigenstates=basis,
...     amplitudes=string_state2)
>>> inner(state1,state2).item()
(0.49999999144286444+0j)
Source code in emu_sv/state_vector.py
def inner(left: StateVector, right: StateVector) -> torch.Tensor:
    """
    Wrapper around StateVector.inner.

    Args:
        left:  StateVector argument
        right: StateVector argument

    Returns:
        the inner product

    Examples:
        >>> factor = math.sqrt(2.0)
        >>> basis = ("r","g")
        >>> string_state1 = {"gg":1.0,"rr":1.0}
        >>> state1 = StateVector.from_state_string(basis=basis,
            ... nqubits=nqubits,strings=string_state1)
        >>> string_state2 = {"gr":1.0/factor,"rr":1.0/factor}
        >>> state2 = StateVector.from_state_string(basis=basis,
            ... nqubits=nqubits,strings=string_state2)

        >>> state1 = StateVector.from_state_amplitudes(eigenstates=basis,
        ...     amplitudes=string_state1)
        >>> string_state2 = {"gr":1.0/factor,"rr":1.0/factor}
        >>> state2 = StateVector.from_state_amplitudes(eigenstates=basis,
        ...     amplitudes=string_state2)
        >>> inner(state1,state2).item()
        (0.49999999144286444+0j)
    """

    assert (left.vector.shape == right.vector.shape) and (left.vector.dim() == 1), (
        "Shape of left.vector and right.vector should be",
        " the same and both need to be 1D tesnor",
    )
    return left.inner(right)

DenseOperator

Bases: Operator[complex, Tensor, StateVector]

DenseOperator in EMU-SV use dense matrices

Source code in emu_sv/dense_operator.py
def __init__(
    self,
    matrix: torch.Tensor,
    *,
    gpu: bool = True,
):
    device = "cuda" if gpu and DEVICE_COUNT > 0 else "cpu"
    self.matrix = matrix.to(dtype=dtype, device=device)

__add__(other)

Element-wise addition of two DenseOperators.

PARAMETER DESCRIPTION
other

a DenseOperator instance.

TYPE: Operator

RETURNS DESCRIPTION
DenseOperator

A new DenseOperator representing the sum.

Source code in emu_sv/dense_operator.py
def __add__(self, other: Operator) -> DenseOperator:
    """
    Element-wise addition of two DenseOperators.

    Args:
        other: a DenseOperator instance.

    Returns:
        A new DenseOperator representing the sum.
    """
    assert isinstance(
        other, DenseOperator
    ), "DenseOperator can only be added to another DenseOperator."
    return DenseOperator(self.matrix + other.matrix)

__matmul__(other)

Compose two DenseOperators via matrix multiplication.

PARAMETER DESCRIPTION
other

a DenseOperator instance.

TYPE: Operator

RETURNS DESCRIPTION
DenseOperator

A new DenseOperator representing the product self @ other.

Source code in emu_sv/dense_operator.py
def __matmul__(self, other: Operator) -> DenseOperator:
    """
    Compose two DenseOperators via matrix multiplication.

    Args:
        other: a DenseOperator instance.

    Returns:
        A new DenseOperator representing the product `self @ other`.
    """
    assert isinstance(
        other, DenseOperator
    ), "DenseOperator can only be multiplied with a DenseOperator."
    return DenseOperator(self.matrix @ other.matrix)

__rmul__(scalar)

Scalar multiplication of the DenseOperator.

PARAMETER DESCRIPTION
scalar

a number to scale the operator.

TYPE: complex

RETURNS DESCRIPTION
DenseOperator

A new DenseOperator scaled by the given scalar.

Source code in emu_sv/dense_operator.py
def __rmul__(self, scalar: complex) -> DenseOperator:
    """
    Scalar multiplication of the DenseOperator.

    Args:
        scalar: a number to scale the operator.

    Returns:
        A new DenseOperator scaled by the given scalar.
    """

    return DenseOperator(scalar * self.matrix)

apply_to(other)

Apply the DenseOperator to a given StateVector.

PARAMETER DESCRIPTION
other

a StateVector instance.

TYPE: State

RETURNS DESCRIPTION
StateVector

A new StateVector after applying the operator.

Source code in emu_sv/dense_operator.py
def apply_to(self, other: State) -> StateVector:
    """
    Apply the DenseOperator to a given StateVector.

    Args:
        other: a StateVector instance.

    Returns:
        A new StateVector after applying the operator.
    """
    assert isinstance(
        other, StateVector
    ), "DenseOperator can only be applied to a StateVector."

    return StateVector(self.matrix @ other.vector)

expect(state)

Compute the expectation value of the operator with respect to a state.

PARAMETER DESCRIPTION
state

a StateVector instance.

TYPE: State

RETURNS DESCRIPTION
Tensor

The expectation value as a float or complex number.

Source code in emu_sv/dense_operator.py
def expect(self, state: State) -> torch.Tensor:
    """
    Compute the expectation value of the operator with respect to a state.

    Args:
        state: a StateVector instance.

    Returns:
        The expectation value as a float or complex number.
    """
    assert isinstance(
        state, StateVector
    ), "Only expectation values of StateVectors are supported."

    return torch.vdot(state.vector, self.apply_to(state).vector).cpu()